A field guide to nftables egress filtering
Ingress filtering is table stakes — everyone blocks inbound ports they don't use. Far fewer teams control egress: what their boxes are allowed to talk to on the way out. That's a shame, because egress control is the difference between "an attacker popped a box" and "an attacker popped a box and exfiltrated everything."
The principle: a server that only needs DNS, NTP, and HTTPS to do its job should not be able to open arbitrary outbound connections. If something tries to, that's signal — and it's blocked.
A minimal default-deny output policy
Here's the shape we run on edge boxes. inet covers IPv4 and IPv6 in one table.
#!/usr/sbin/nft -f
flush ruleset
table inet filter {
chain input {
type filter hook input priority 0; policy drop;
iif "lo" accept
ct state established,related accept
ct state invalid drop
ip protocol icmp accept
ip6 nexthdr ipv6-icmp accept
tcp dport { 22, 80, 443 } ct state new accept
}
chain output {
type filter hook output priority 0; policy drop;
oif "lo" accept
ct state established,related accept
# the egress allowlist — and nothing else gets out
udp dport { 53, 67, 123 } accept
tcp dport { 53, 80, 443 } ct state new accept
}
}
Two details that bite people:
established,relatedinoutput. Without it, replies to inbound SSH never leave and you lock yourself out. Allow it before you tighten the policy.- DHCP and DNS.
udp 67keeps lease renewal alive;udp 53keeps name resolution working. Forget these and the box quietly falls off the network hours later.
Tightening further
The allowlist above still permits outbound 80/443 anywhere, which a determined attacker can abuse for exfil over HTTPS. The next step is to allowlist destinations, not just ports — your package mirror, your ACME provider, your metadata endpoint — using named sets you can update without editing the policy:
set egress_allow_v4 {
type ipv4_addr
flags interval
elements = { 10.0.0.0/8 } # plus your specific mirrors / ACME ranges
}
How far you take it depends on what the box does. A static web node can be locked down hard. A build runner that pulls from the whole internet can't — so you isolate that instead.
Why bother
Egress filtering doesn't stop the initial compromise. It changes what a compromise is worth. A box that can't phone home is a dead end. Combine it with good logging and the blocked outbound attempt becomes one of the highest-signal alerts you'll ever get — almost no false positives, because legitimate software doesn't try to reach places it isn't allowed.