Hardened Networking and WireGuard Mesh
Hardened Networking and WireGuard Mesh
Public IPs should not see Postgres. The boring default is: WireGuard mesh 10.100.0.0/24, keys via sops on /run/secrets, nftables so 5432 exists only on wg0.
Mental model
| Node | wg0 |
|---|---|
gw-01 |
10.100.0.1 |
app-01 / app-02 |
10.100.0.11 / .12 |
db-01 |
10.100.0.21 |
ops-01 |
10.100.0.30 |
Public: UDP 51820 on every peer; TCP 80/443 only on gw-01. Everything else: wg0 or drop.
Private keys never enter /nix/store. privateKeyFile points at sops.
persistentKeepalive = 25 keeps NAT mappings alive toward cloud UDP.
Worked examples
Case 2: Database host — Postgres only on wg0
Save as hosts/db-01.nix:
# hosts/db-01.nix
{
imports = [
../modules/base-system.nix
../modules/wireguard-mesh.nix
];
cluster.nodeIp = "10.100.0.21/24";
networking.firewall = {
enable = true;
interfaces.wg0.allowedTCPPorts = [ 5432 ];
interfaces.eth0.allowedTCPPorts = [ ];
};
}nix eval .#nixosConfigurations.db-01.config.networking.firewall.interfaces.wg0.allowedTCPPortsOutput:
[ 5432 ]
Case 3: Gateway — HTTP public, API only over mesh
# hosts/gw-01.nix fragment
{
cluster.nodeIp = "10.100.0.1/24";
networking.firewall.allowedTCPPorts = [ 80 443 ];
networking.firewall.interfaces.wg0.allowedTCPPorts = [ ];
}Caddy binds 0.0.0.0:443. It proxies to 10.100.0.11:8080, which is not open on the public NIC of app-01.
Case 4: App node does not open 8080 publicly
{
cluster.nodeIp = "10.100.0.11/24";
networking.firewall.interfaces.wg0.allowedTCPPorts = [ 8080 ];
}Health checks from Caddy use 10.100.0.11. A scanner on the cloud IP should not see 8080.
Case 5: Prove the path
On ops-01 (once deployed):
sudo wg show
ping -c 1 10.100.0.21
nc -zv 10.100.0.21 5432From a laptop not on the mesh, nc to the public IP port 5432 should fail. That is the test, not a green wg show alone.
sudo wg show wg0 dump
ip route get 10.100.0.21Handshake age of minutes is NAT; persistentKeepalive = 25. Handshake never: wrong pubkey, UDP 51820 blocked, or allowedIPs does not contain that /32.
The trap
The trap is privateKey = "…" in Nix. World-readable store. privateKeyFile + sops. Public keys in git are fine.
The other trap is allowedIPs = [ "0.0.0.0/0" ] turning the mesh into a default route. Use /32 per peer unless you mean to route the internet through gw-01.
The boring rule
- Mesh IPs in git. Private keys in sops →
/run/secrets. 5432/8080onwg0only.- Public: 80/443 on gateway, UDP 51820 on peers.
/32allowedIPsunless you are building a router.persistentKeepalive = 25behind NAT.
Try this
- Eval Case 2; confirm
eth0TCP list is empty. - Generate a keypair with
wg genkey | tee priv | wg pubkey; put only the pubkey in the module. - Two VMs, one peer each;
pingoverwg0. ss -lntpondb-01— Postgres on10.100.0.21:5432, not0.0.0.0.