Networking, Firewall, and Security
Networking, Firewall, and Security
Ad-hoc iptables -A and a NetworkManager GUI click are gone after a rebuild — or worse, they survive in unmanaged /etc and fight the next activation. The boring default is: firewall on, ports listed in Nix, NetworkManager on laptops, systemd-networkd on servers, secrets as files.
Mental model
NixOS enables a firewall by default (networking.firewall.enable is true). Incoming is denied unless you or a module open it. services.openssh.enable = true opens 22 unless you tell it not to (openFirewall = false on modules that offer that flag).
| Machine | Network backend |
|---|---|
| Desk laptop, Wi-Fi | networking.networkmanager.enable = true; |
| VM, cloud, rack server | networking.useNetworkd = true; (systemd-networkd) |
Do not enable both and hope. Pick one.
26.05 uses nftables for networking.firewall unless you set networking.nftables.enable = false (legacy iptables). nft list ruleset is the boring inspect command. networking.firewall.extraInputRules is an nft snippet when a typed option is missing — still better than a page of extraCommands.
internet
│
▼
nftables/iptables ← networking.firewall.allowedTCPPorts
│
▼
sshd / nginx / desk-api
WireGuard, Tailscale, and similar are modules (networking.wireguard, services.tailscale). They are not extra packages you systemctl start by hand.
Worked examples
Case 1: Hostname, DNS, firewall allow-list
Save as networking_config.nix:
# networking_config.nix
{
networking = {
hostName = "desk-gateway";
domain = "internal.corp";
firewall = {
enable = true;
allowPing = true; # ICMP; set false on a bastion if you mean it
allowedTCPPorts = [ 22 8080 ];
allowedTCPPortRanges = [
{ from = 9000; to = 9010; }
];
};
nameservers = [ "1.1.1.1" "9.9.9.9" ];
};
}sudo nixos-rebuild switch
sudo nft list ruleset | head
# or, if still on iptables backend:
sudo iptables -L INPUT -n | headPort 8080 is open because you listed it. Port 3000 is not. A node app that binds :3000 is reachable on localhost only until you add 3000.
Case 2: systemd-networkd static address (server)
Save as networkd.nix:
# networkd.nix
{
networking.useDHCP = false;
networking.useNetworkd = true;
networking.networkmanager.enable = false;
systemd.network.networks."10-lan" = {
matchConfig.Name = "en*";
networkConfig = {
Address = "10.20.0.10/24";
Gateway = "10.20.0.1";
DNS = "10.20.0.1";
};
};
}Match on en* (or a by-id name) in the lab. On metal, prefer matchConfig.MACAddress or Name = "enp1s0" after you have seen networkctl status.
networkctl status
ip -br addrCase 3: NetworkManager on the workstation
Save as nm.nix:
# nm.nix
{
networking.networkmanager.enable = true;
networking.useNetworkd = false;
networking.firewall.enable = true;
}Users who should click Wi-Fi:
users.users.deskadmin.extraGroups = [ "networkmanager" ];Do not also set networking.interfaces.eth0.ipv4.addresses unless you know you want NixOS’s scripted networking instead. One backend.
Case 4: WireGuard with a key file
Save as wireguard.nix:
# wireguard.nix
{
networking.firewall.allowedUDPPorts = [ 51820 ];
networking.wireguard.interfaces.wg0 = {
ips = [ "10.100.0.2/24" ];
listenPort = 51820;
privateKeyFile = "/run/secrets/wg-desk.key";
peers = [
{
publicKey = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
allowedIPs = [ "10.100.0.0/24" ];
endpoint = "vpn.desk.internal:51820";
persistentKeepalive = 25;
}
];
};
}privateKeyFile is a path. The key bytes are not in the flake. Generate with wg genkey on the machine (or decrypt with sops) and persist that file (impermanence: /persist).
sudo systemctl status wg-quick-wg0
# or wg0 depending on the module's unit name
ip -br addr show wg0Case 5: Trust a VPN interface without opening the world
# trusted.nix
{
networking.firewall = {
enable = true;
trustedInterfaces = [ "lo" "wg0" ];
allowedTCPPorts = [ 22 ];
};
}Traffic from wg0 is trusted. The public NIC still only has 22. That is how the desk API can listen on 10.100.0.2:8080 without listing 8080 on the internet.
Per-interface holes (no trust of the whole overlay):
{
networking.firewall.interfaces.wg0.allowedTCPPorts = [ 8080 5432 ];
}Postgres on wg0 only. checkReversePath = "loose"; is for asymmetric routing (some VPNs); "strict" (default on many setups) drops martian sources. Do not flip it to "loose" to “make WireGuard work” until nft / journalctl -k says rpfilter is the drop.
The trap
The trap is networking.firewall.enable = false; “to debug the app,” then committing the host. The next deploy is an open machine.
Open allowedTCPPorts = [ 3000 ]; instead. Leave the firewall on.
The other trap is putting a WireGuard private key in the attrset (privateKey = "…"). World-readable store. Same as a password in configuration.nix.
A third trap: networking.firewall.extraCommands with a page of iptables. Those rules are not typed, not easily reviewed, and break when the backend is nftables. Prefer allowedTCPPorts, interfaces.<name>.allowedTCPPorts, and trustedInterfaces.
A fourth trap: deploying firewall rules or SSH port changes over SSH using nixos-rebuild switch directly. If you typo a port or misconfigure an interface, your connection drops and the unbootable network state is saved as default. Use nixos-rebuild test --flake .#host first; if you lose access, a hypervisor or out-of-band reboot automatically reverts the machine.
The boring rule
- Firewall stays on. List ports. Do not disable it to debug.
- NetworkManager or networkd, not both.
- VPN keys as
privateKeyFile(sops / persist), never as Nix strings. - Test firewall and SSH updates with
nixos-rebuild testbefore committing withswitch. - Let service modules open their own ports, or set
openFirewalldeliberately. trustedInterfacesfor overlay NICs; orinterfaces.<name>.allowedTCPPortsif you do not trust the whole mesh.- nftables backend.
nft list ruleset.checkReversePathstays strict until proven.
Try this
- Add
8080toallowedTCPPorts, switch,nc -l 8080in one shell andnc $HOST 8080from another VM. Remove 8080 and confirm the second connect hangs/refuses. sudo nft list ruleset | grep -i 22(or iptables) afterservices.openssh.enable = truewith default firewall — 22 should be there.- Set
allowPing = falseon a lab VM,pingit from the host, then set it true again. - Generate a WireGuard keypair with
wg genkey | tee /tmp/k | wg pubkeyand do not paste the private half into a.nixfile. Put it in a file and reference the path.