← Blog

Alexis Β· 2026-09-29

WireGuard site-to-site VPN: homelab to VPS

The homelab runs on private RFC1918 addresses behind a consumer router. The VPS sits on the public internet with a static IP. Getting them to talk to each other β€” reliably, securely, and without exposing anything unnecessarily β€” is exactly what a site-to-site VPN is for.

I use WireGuard for this. It is fast, has a minimal attack surface (roughly 4,000 lines of code versus OpenVPN's ~100,000), and the configuration is simple enough to understand completely. No certificate authority, no complex PKI, just key pairs and allowed IP ranges.

What the tunnel achieves

With the tunnel in place:

Architecture

flowchart TD
    classDef internet fill:#f9f9f9,stroke:#333,stroke-width:2px;
    classDef vps fill:#e1f5fe,stroke:#01579b,stroke-width:2px;
    classDef homelab fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px;
    classDef tunnel fill:#fff3e0,stroke:#ef6c00,stroke-width:2px;

    Internet([Internet])

    subgraph VPS [VPS β€” Public IP]
        VPS_WG["WireGuard\n10.10.0.1/24"]
        VPS_NGINX["NGINX"]
        VPS_App["Rust app\nPort 9080"]
    end

    subgraph Tunnel [WireGuard Tunnel\n10.10.0.0/24 β€” encrypted UDP]
        direction LR
        TL[" "]
    end

    subgraph Homelab [Homelab β€” Private LAN]
        HL_WG["WireGuard\n10.10.0.2/24"]
        Forgejo["Forgejo\n192.168.10.x"]
        Runner["Forgejo runner"]
        Staging["Staging container"]
    end

    Internet -->|HTTPS| VPS_NGINX
    VPS_NGINX --> VPS_App
    VPS_WG <-->|encrypted UDP 51820| HL_WG
    Runner -->|deploy via tunnel| VPS_App
    Runner --> Staging

    class Internet internet;
    class VPS_WG,VPS_NGINX,VPS_App vps;
    class HL_WG,Forgejo,Runner,Staging homelab;

VPS configuration

On the VPS, WireGuard listens on UDP port 51820. The VPS has a static public IP, so it is the stable endpoint the homelab connects to:

# /etc/wireguard/wg0.conf β€” VPS
[Interface]
Address = 10.10.0.1/24
ListenPort = 51820
PrivateKey = <vps-private-key>

# Homelab peer
[Peer]
PublicKey = <homelab-public-key>
AllowedIPs = 10.10.0.2/32, 192.168.10.0/24

AllowedIPs on the VPS side includes the homelab's WireGuard address (10.10.0.2) and the homelab's LAN subnet (192.168.10.0/24). This tells the VPS kernel to route packets destined for the homelab LAN through the tunnel β€” the VPS can reach internal homelab hosts directly.

Enable IP forwarding on the VPS so traffic can be routed:

echo "net.ipv4.ip_forward=1" >> /etc/sysctl.conf && sysctl -p

Homelab configuration

The homelab peer has a dynamic public IP (a residential connection), so it initiates the connection to the VPS and maintains it with a keepalive:

# /etc/wireguard/wg0.conf β€” Homelab (pfSense or Linux)
[Interface]
Address = 10.10.0.2/24
PrivateKey = <homelab-private-key>

# VPS peer
[Peer]
PublicKey = <vps-public-key>
Endpoint = <vps-public-ip>:51820
AllowedIPs = 10.10.0.1/32
PersistentKeepalive = 25

PersistentKeepalive = 25 sends a handshake packet every 25 seconds. This keeps the tunnel alive through NAT and consumer routers that would otherwise drop idle UDP connections.

On pfSense the configuration lives under VPN β†’ WireGuard rather than a flat config file, but the parameters are identical.

Key generation

Generate key pairs on each side β€” never transfer private keys over the network:

# On each machine
wg genkey | tee privatekey | wg pubkey > publickey

Exchange only the public keys. Each side keeps its private key local.

Verifying the tunnel

# On either side
wg show

# Test reachability
ping 10.10.0.1   # from homelab β†’ VPS tunnel endpoint
ping 192.168.10.5  # from VPS β†’ homelab host

A successful handshake shows latest handshake in wg show output. If it is absent, check that UDP 51820 is open on the VPS firewall and that the public keys match what each side has configured for its peer.

Using the tunnel for CI/CD

The Forgejo runner on the homelab deploys to the VPS over the tunnel. The deploy script connects via SSH to 10.10.0.1 (the VPS's WireGuard address) rather than the public IP β€” traffic stays inside the encrypted tunnel, and the VPS SSH port does not need to be publicly accessible.

ssh -i /home/runner/.ssh/deploy_key [email protected] \
  "podman pull registry.home.internal/app:latest && \
   podman restart app"

Stack

WireGuard Β· pfSense Β· Linux Β· Podman Β· Forgejo CI