Files
nix-config/common/network/pia-vpn/README.md
googlebot a0fcacdcf9
All checks were successful
Check Flake / check-flake (push) Successful in 4m44s
Rewrite PIA VPN as multi-container bridge architecture
Replace the single VPN container (veth pair, host-side auth scripts) with a
multi-container setup on a shared bridge network:

- Dedicated VPN container handles all PIA auth, WireGuard config, NAT, and
  optional port forwarding DNAT
- Service containers default-route through VPN container (leak-proof by topology)
- Host runs tinyproxy on bridge for PIA API bootstrap before WG is up
- WG interface is still created in host netns and moved into VPN container
  namespace
- Monthly renewal to ensure that connection stays up (PIA allows connections to
  last up to 2 months)
- Drop OpenVPN support entirely
2026-02-24 23:11:46 -08:00

3.7 KiB

PIA VPN Multi-Container Module

Routes service containers through a PIA WireGuard VPN using a shared bridge network.

Architecture

                  internet
                     │
              ┌──────┴──────┐
              │    Host      │
              │  tinyproxy   │  ← PIA API bootstrap proxy
              │  10.100.0.1  │
              └──────┬───────┘
                     │ br-vpn (no IPMasquerade)
        ┌────────────┼──────────────┐
        │            │              │
 ┌──────┴──────┐ ┌───┴────┐  ┌─────┴──────┐
 │ VPN ctr     │ │ servarr│  │transmission│
 │ 10.100.0.2  │ │  .11   │  │   .10      │
 │ piaw (WG)   │ │        │  │            │
 │ gateway+NAT │ └────────┘  └────────────┘
 └─────────────┘
  • Host creates the WG interface (encrypted UDP stays in host netns) and runs tinyproxy on the bridge so the VPN container can bootstrap PIA auth before WG is up.
  • VPN container authenticates with PIA via the proxy, configures WG, sets up NAT (masquerade bridge→WG) and optional port forwarding DNAT.
  • Service containers default-route through the VPN container. No WG interface = no internet if VPN is down = leak-proof by topology.
  • Host reaches containers directly on the bridge for nginx reverse proxying.

Key design decisions

  • Bridge, not veth pairs: All containers share one bridge (br-vpn), so the VPN container can act as a single gateway. The host does NOT masquerade bridge traffic — only the VPN container does (through WG).
  • Port forwarding is implicit: If any container sets receiveForwardedPort, the VPN container automatically handles PIA port forwarding and DNAT. No separate toggle needed.
  • DNS through WG: Service containers use the VPN container as their DNS server. The VPN container runs systemd-resolved listening on its bridge IP, forwarding queries through the WG tunnel.
  • Monthly renewal: pia-vpn-setup uses Type=simple + Restart=always + RuntimeMaxSec=30d to periodically re-authenticate with PIA and get a fresh port forwarding signature (signatures expire after ~2 months). Service containers are unaffected during renewal.

Files

File Purpose
default.nix Options, bridge, tinyproxy, host firewall, WG interface creation, assertions
vpn-container.nix VPN container: PIA auth, WG config, NAT, DNAT, port refresh timer
service-container.nix Generates service containers with static IP and gateway→VPN
scripts.nix Bash function library for PIA API calls and WG configuration
ca.rsa.4096.crt PIA CA certificate for API TLS verification

Usage

pia-vpn = {
  enable = true;
  serverLocation = "swiss";

  containers.my-service = {
    ip = "10.100.0.10";
    mounts."/data".hostPath = "/data";
    config = { services.my-app.enable = true; };

    # Optional: receive PIA's forwarded port (at most one container)
    receiveForwardedPort = { port = 8080; protocol = "both"; };
    onPortForwarded = ''
      echo "PIA assigned port $PORT, forwarding to $TARGET_IP:8080"
    '';
  };
};

Debugging

# Check VPN container status
machinectl shell pia-vpn
systemctl status pia-vpn-setup
journalctl -u pia-vpn-setup

# Verify WG tunnel
wg show

# Check NAT/DNAT rules
iptables -t nat -L -v
iptables -L FORWARD -v

# From a service container — verify VPN routing
curl ifconfig.me

# Port refresh logs
journalctl -u pia-vpn-port-refresh