Files
nix-config/common/network/pia-vpn
Zuckerberg 9fdadd321c
All checks were successful
Check Flake / check-flake (push) Successful in 3m18s
Verify RSA-SHA256 signatures on all PIA API responses
Every PIA API response includes a trailing RSA-SHA256 signature
(line 1 = JSON, lines 3+ = base64-encoded signature) which was
previously ignored entirely. Add verifyPIAResponse() that checks
each response against PIA's public signing key before trusting
the data. On verification failure the service aborts and systemd
restarts it.

Also bump RestartSec to 5m to avoid hammering PIA servers on
repeated failures.
2026-02-26 22:06:35 -08:00
..

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