What it is
Two VPN layers in parallel, both declared from my NixOS flake, stitching every node in my homelab into one routable fabric: a router-level WireGuard site-to-site tunnel (VPS ↔ pfSense) for public-to-home traffic, and a device-level Tailscale mesh (via self-hosted Headscale) for direct peer-to-peer access between every machine I own. pfSense VLANs segment the physical LAN; subnet routers bridge the mesh to non-Tailscale devices.
Why it exists
The two layers answer two different questions:
- WireGuard (Layer 1) answers "how does my public VPS talk to my home LAN as if they're on the same network?". It's a permanent, always-on tunnel at the router level. Nginx on the VPS proxies subdomains through this tunnel to home-LAN services — no port forwarding on the home router, no dependency on a SaaS tunnel provider.
- Tailscale / Headscale (Layer 2) answers "how does any single device I own reach any other, from anywhere, without punching holes?". It's a mesh overlay with per-device identity, where every peer gets a 100.64.0.x address regardless of physical location. My laptop in a café can SSH to my NAS through the mesh without caring whether it's at home or abroad.
Building both is intentional. WireGuard handles the high-bandwidth, router-level flows (reverse proxy → home services, NAS ↔ VPS backups). Tailscale handles device identity + ACLs + zero-config connectivity for mobile devices. They overlay the same physical network without conflicting.
Architecture — subnets & routing
Subnet legend
| Range | Purpose |
|---|---|
192.168.8.0/24 | Main home LAN (workstations, Proxmox, LXCs) |
192.168.20.0/24 | Storage VLAN (NAS, media clients) |
172.26.5.0/24 | WireGuard site-to-site + road warriors |
100.64.0.0/10 | Tailscale / Headscale mesh overlay |
10.0.0.0/8 | rootless Docker slirp4netns NAT (VPS internal) |
172.16.0.0/12 | Docker bridge networks (VPS internal) |
What I built
- WireGuard site-to-site tunnel (Layer 1) — VPS server at
172.26.5.155/24, pfSense peer at172.26.5.1,allowedIPs=192.168.8.0/24 · 192.168.20.0/24 · 172.26.5.1/32. pfSense outbound-NAT rule masquerades home traffic back through the tunnel so return paths work. - NFTables masquerade for road warriors — migrated off iptables because rootless Docker's iptables manipulation clobbered UFW; NFTables rules operate independently. Rule:
oifname eth0 ip saddr 172.26.5.0/24 masquerade. - Self-hosted Headscale (Layer 2 control plane) — runs on the VPS, replaces SaaS Tailscale coordination. Every NixOS profile in my flake (workstations, laptops, VPS, NAS, Proxmox LXCs, nix-darwin macOS) enrolls via declarative
services.tailscaleconfig. pfSense also joins as a Tailscale node with IP100.64.0.7. - Subnet routers bridge the mesh to non-Tailscale devices —
NAS_PRODandLXC_tailscaleboth advertise192.168.8.0/24+192.168.20.0/24. A peer joining the mesh from outside the LAN can reach home-LAN devices that don't run Tailscale. - Split DNS over Tailscale —
headscaleDnsSplit = { "*.local.akunito.com" = [ "100.64.0.7" ]; }routes internal domain queries to pfSense's Tailscale IP so names likenas.local.akunito.comresolve from anywhere on the mesh. - Nginx reverse proxy via WireGuard — every
*.akunito.comsubdomain terminates on the VPS Nginx, thenproxy_passes through the WG interface to a home-LAN service. Replaced a fleet of Cloudflare Tunnel containers (one per service) with a single Nginx stack. - Edge defense — Fail2Ban on the VPS reads real visitor IPs from
CF-Connecting-IP, bans propagate to Cloudflare's edge via API, so attackers are blocked at both the host and the CDN. - MTU hygiene — all WireGuard peers standardized to MTU 1280; kernel
rp_filter = 2so asymmetric routing through the site-to-site doesn't get dropped. - Declarative secrets — WireGuard private keys, preshared keys, Headscale keys all live in git-crypt-encrypted files under
secrets/; they're referenced declaratively by profile configs and installed via NixOS activation scripts. - pfSense as a declarative neighbor — while pfSense itself isn't NixOS, its Tailscale endpoint is treated as a first-class citizen in the flake (via SSH-managed config + Prometheus exporter on VPS). pfSense backup is pulled nightly to the NAS via rsync over SSH.
Results
- One public entry point (VPS) + one mesh (Headscale) = full-fleet connectivity. No port forwarding on the home router.
- Subdomains on WireGuard → home services via Nginx: cheaper, auditable, no SaaS dependency for the tunnel path.
- Mesh covers every device I own via self-hosted Headscale — phones, laptops, VPS, NAS, LXCs, pfSense — with split DNS for
*.local.akunito.com. - Declarative end-to-end — the entire network layer rebuilds from the NixOS flake; WireGuard and Tailscale configs are version-controlled alongside service definitions.
- Cost — ~€7/mo Netcup VPS covers the public-facing layer; Headscale replaces a SaaS tier that would otherwise scale with device count.
Stack
NixOS, WireGuard, Tailscale, Headscale (self-hosted), Nginx, NFTables, pfSense, Certbot, Fail2Ban, Cloudflare API, systemd, git-crypt, ZFS (NAS datasets exported over NFS/SMB).
Status
- Repo: private — declarative config lives in the
VPS_PROD,NAS_PROD,LXC_tailscale, and personal profiles ofmy-nixos-infrastructure. - Running: daily since the flake-ification of the VPS; replaced an Ubuntu LTS + UFW + Cloudflare Tunnel setup during 2024.
- Scope note: this project originally covered only the VPS WireGuard server; it now represents the full homelab network fabric (both VPN layers, pfSense VLANs, subnet routers, split DNS).
- Related:
my-homelab(what the network connects),my-nixos-infrastructure(how the profiles compose).
