DEV Community

Jesse D
Jesse D

Posted on

Building a Zero-Trust VPN: How I Wrapped OpenVPN and WireGuard with Modern Authentication

Building a Zero-Trust VPN: How I Wrapped OpenVPN and WireGuard with Modern Authentication

It started with my homelab.

I have a modest setup—a few Proxmox nodes, some Kubernetes clusters, network storage, the usual suspects. I wanted secure remote access without exposing everything to the internet. Simple ask, right?

The Search for a Solution

I tried the modern zero-trust solutions first:

Tailscale - Great product, but I wanted self-hosted. Their open-source Headscale works, but felt like I was fighting the architecture. Coordination servers, DERP relays, ACL policies in HuJSON—it's elegant if you buy into their model, but I wanted something closer to traditional VPN semantics.

NetBird - Promising concept, but the setup was painful. Multiple components to deploy, documentation gaps, and I spent more time debugging connectivity issues than actually using it. The mesh networking model also felt like overkill for my use case.

Firezone - Looked good on paper, but the WireGuard-only approach was limiting. Some of my older devices needed OpenVPN. And the complexity of the Elixir/Phoenix stack made me nervous about long-term maintenance.

Pritunl - Closer to what I wanted, but the enterprise licensing model and MongoDB dependency gave me pause. Also, the codebase felt dated.

Each solution had pieces I liked, but none hit the sweet spot: simple deployment, SSO integration, both OpenVPN and WireGuard, and actual zero-trust with per-user firewall rules.

So I built my own.

What I Built

A few simple commands. That's it.

GateKey is a zero-trust VPN solution that wraps OpenVPN and WireGuard to provide software-defined perimeter capabilities—while maintaining 100% compatibility with existing clients.

In this article, I'll walk through the architecture decisions, the technical challenges, and how you can deploy a zero-trust VPN for your organization or homelab.


The Problem with Traditional VPNs

If you've managed a corporate VPN, you know the pain:

  1. Certificate Management Hell - Generating, distributing, and rotating certificates manually
  2. Shared Secrets - One compromised credential = everyone's compromised
  3. Flat Network Access - Once you're in, you can reach everything
  4. No Identity Awareness - The VPN doesn't know who you are, just that you have a valid cert
  5. Manual Onboarding - New employee? Here's a 10-step guide to configure your VPN client

Modern identity providers solved authentication years ago. Why are VPNs still using 1990s-era certificate distribution?


The Zero-Trust Approach

GateKey flips the model:

Authenticate first, connect second.

Instead of:

User has certificate → User connects → Hope they're authorized
Enter fullscreen mode Exit fullscreen mode

We do:

User authenticates via SSO → System generates short-lived cert → User connects → Per-user firewall rules applied
Enter fullscreen mode Exit fullscreen mode

Key Principles

  1. Identity-First - Integrate with your existing IdP (Okta, Azure AD, Google Workspace)
  2. Short-Lived Credentials - Certificates expire in 24 hours (configurable), no manual rotation
  3. Per-User Firewall - Each user gets individualized network access based on their role
  4. Zero Standing Privileges - No access without active authentication

Here's what SSO login looks like in the web UI:


Architecture Deep-Dive

Here's how the pieces fit together:

Component Breakdown

Control Plane (Go + React)

  • Handles SSO authentication (OIDC/SAML)
  • Embedded Certificate Authority for on-demand cert generation
  • Policy engine for access rules
  • REST API for all operations

Gateway Agents (Go)

  • Lightweight agents that run alongside OpenVPN/WireGuard
  • Communicate with control plane via authenticated API
  • Manage nftables firewall rules per-user
  • Support both OpenVPN and WireGuard protocols

Embedded CA

  • No external PKI infrastructure required
  • Generates short-lived certificates on-demand
  • Supports graceful CA rotation with zero downtime

Technical Decision: Why Wrap Instead of Reimplement?

I considered building a custom VPN protocol. Then I remembered:

  1. OpenVPN and WireGuard are battle-tested - Millions of deployments, extensively audited
  2. Client compatibility - Every platform already has OpenVPN/WireGuard clients
  3. Security audits are expensive - Why audit a new protocol when proven ones exist?

Instead, GateKey uses hook scripts to intercept OpenVPN events:

# OpenVPN calls these scripts on client events
client-connect /usr/local/bin/gatekey-connect
client-disconnect /usr/local/bin/gatekey-disconnect
auth-user-pass-verify /usr/local/bin/gatekey-verify via-env
Enter fullscreen mode Exit fullscreen mode

When a client connects:

  1. OpenVPN triggers gatekey-verify with the client certificate
  2. Gateway agent validates the cert against the control plane
  3. Control plane checks: Is this cert valid? Is the user still authenticated? What access rules apply?
  4. Gateway receives the user's access rules and configures nftables
  5. User gets access only to their authorized resources

The VPN protocol itself is unchanged. We're just adding an authentication and authorization layer.


Per-User Firewall Rules with nftables

This is where zero-trust gets real. Each connected user gets their own firewall rules:

// internal/firewall/nftables.go
func (f *NFTablesFirewall) ApplyUserRules(userID string, vpnIP net.IP, rules []AccessRule) error {
    // Create a chain for this specific user
    chainName := fmt.Sprintf("user_%s", sanitize(userID))

    // Add rules for each allowed network
    for _, rule := range rules {
        nftCmd := fmt.Sprintf(
            "nft add rule inet gatekey %s ip saddr %s ip daddr %s accept",
            chainName, vpnIP, rule.CIDR,
        )
        if err := exec.Command("sh", "-c", nftCmd).Run(); err != nil {
            return fmt.Errorf("failed to add rule: %w", err)
        }
    }

    // Default deny for this user
    nftCmd := fmt.Sprintf(
        "nft add rule inet gatekey %s ip saddr %s drop",
        chainName, vpnIP,
    )
    return exec.Command("sh", "-c", nftCmd).Run()
}
Enter fullscreen mode Exit fullscreen mode

Access rules are managed through a clean admin interface:

When access rules change in the admin UI, gateways poll for updates every 10 seconds and immediately update firewall rules—no client reconnection required.


Short-Lived Certificates: The Key to Zero Trust

Traditional VPNs issue certificates valid for 1-5 years. If compromised, you're exposed for years.

GateKey certificates expire in 24 hours by default:

// internal/pki/ca.go
func (ca *CA) IssueCertificate(commonName string, validityHours int) (*x509.Certificate, error) {
    template := &x509.Certificate{
        SerialNumber: big.NewInt(time.Now().UnixNano()),
        Subject: pkix.Name{
            CommonName: commonName,
        },
        NotBefore:             time.Now(),
        NotAfter:              time.Now().Add(time.Duration(validityHours) * time.Hour),
        KeyUsage:              x509.KeyUsageDigitalSignature,
        ExtKeyUsage:           []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
        BasicConstraintsValid: true,
    }

    // Sign with our CA
    certDER, err := x509.CreateCertificate(rand.Reader, template, ca.cert, pubKey, ca.privateKey)
    // ...
}
Enter fullscreen mode Exit fullscreen mode

The CA management interface provides full visibility into certificate lifecycle:

The client CLI handles renewal automatically:

$ gatekey connect
# First time: Opens browser for SSO, downloads config, connects
# Subsequent times: Refreshes config if needed, connects immediately
Enter fullscreen mode Exit fullscreen mode

Users never see certificate expiration. They just authenticate and connect.


Dual Protocol Support: OpenVPN + WireGuard

Some environments need OpenVPN (maximum compatibility). Others want WireGuard (performance, mobile). GateKey supports both with the same zero-trust model:


# Gateway types in the database
gateways:
  - name: us-east-openvpn
    type: openvpn
    endpoint: vpn-east.company.com:1194

  - name: us-west-wireguard
    type: wireguard
    endpoint: vpn-west.company.com:51820
Enter fullscreen mode Exit fullscreen mode

Users can connect to either:

$ gatekey connect us-east-openvpn    # Uses OpenVPN
$ gatekey connect us-west-wireguard  # Uses WireGuard
$ gatekey status
Active connections:
  us-east-openvpn   connected   10.8.0.5    tun0
  us-west-wireguard connected   10.9.0.12   wg0
Enter fullscreen mode Exit fullscreen mode

Multi-gateway support means users can be connected to multiple VPNs simultaneously—each with their own firewall rules.


Mesh Networking: Site-to-Site Connectivity

Beyond traditional client-to-site VPN, GateKey supports hub-and-spoke mesh networking for site-to-site connectivity:

This enables:

  • Branch office connectivity - Connect remote offices to central resources
  • Multi-cloud networking - Link AWS, GCP, and on-prem networks
  • Zero-trust between sites - Same per-identity firewall rules apply

Network Topology Visualization

Monitor your entire VPN infrastructure in real-time:

The topology view shows:

  • All gateways, hubs, and spokes
  • Active VPN sessions
  • Connection health and bandwidth
  • User attribution for every connection

Geo-Fencing: IP-Based Access Control

Add another layer of security with geo-fencing rules:

  • Whitelist model - Only connections from explicitly allowed IP ranges
  • Hierarchical rules - User rules override group rules, which override global rules
  • Audit mode - Log violations without blocking (for testing)

Authentication Integration

GateKey integrates with your existing identity provider:

Supported providers:

  • Okta
  • Azure AD / Entra ID
  • Google Workspace
  • Keycloak
  • Any OIDC or SAML 2.0 compliant provider

The Admin Dashboard

A unified view of your zero-trust VPN infrastructure:


The User Experience

For end users, it's three commands:

# One-time setup
gatekey config init --server https://vpn.company.com

# Daily usage
gatekey login      # Opens browser for SSO
gatekey connect    # Downloads config, connects
gatekey disconnect # When done
Enter fullscreen mode Exit fullscreen mode

Or use the web UI to download a standard .ovpn file that works with any OpenVPN client.


Deployment Options

Kubernetes (Recommended)

helm repo add gatekey https://dye-tech.github.io/gatekey-helm-chart
helm install gatekey gatekey/gatekey -n gatekey --create-namespace
Enter fullscreen mode Exit fullscreen mode

Docker Compose

services:
  postgres:
    image: postgres:16-alpine
    environment:
      POSTGRES_DB: gatekey

  gatekey-server:
    image: dyetech/gatekey-server:latest
    environment:
      DATABASE_URL: postgres://gatekey:pass@postgres/gatekey

  gatekey-web:
    image: dyetech/gatekey-web:latest
    ports:
      - "80:8080"
Enter fullscreen mode Exit fullscreen mode

What I Learned

Building GateKey taught me several things:

  1. Don't reinvent crypto - Wrap proven protocols instead of creating new ones
  2. Hook scripts are powerful - OpenVPN's plugin system is underutilized
  3. nftables > iptables - Modern, atomic updates, better performance
  4. Short-lived credentials change everything - When certs expire in hours, compromise windows shrink dramatically
  5. Developer experience matters - gatekey connect should just work

Try It Out

GateKey is open source under Apache 2.0:

If you're tired of managing VPN certificates manually, or want to add zero-trust to your existing OpenVPN/WireGuard infrastructure, give it a try.


What's your experience with VPN infrastructure? Have you implemented zero-trust networking? I'd love to hear about your approach in the comments.


Top comments (0)