nftables Firewall Baseline for Linux Servers
Build a default-deny nftables firewall, harden network sysctls, and shrink your exposed service footprint on any Linux server.
A firewall is the cheapest, highest-leverage control you can put in front of a Linux server, yet most boxes ship with everything accepted and rely on services simply not listening. That assumption breaks the moment a daemon binds to 0.0.0.0 by accident. This guide builds a default-deny inet firewall with nftables, layers in the network sysctls that the firewall cannot enforce, and shows how to enumerate and shrink the set of services actually exposed. The result is a baseline you can drop onto any modern Debian, Ubuntu, RHEL, or Fedora host.
The case for default-deny
There are two ways to write firewall policy. A deny-list accepts everything and blocks specific bad traffic; it fails open, so any service you forget about stays reachable. An allow-list drops everything and accepts only what you name; it fails closed, so a mistake costs you one service's availability instead of a silent hole. For servers, default-deny is the only defensible choice. The minimum you must allow is loopback traffic, packets belonging to connections you already initiated (established,related), the ICMP messages the network needs to function, and the handful of inbound services this host genuinely offers, starting with SSH.
A complete nftables ruleset
nftables replaces the old iptables/ip6tables split with a single inet family that covers IPv4 and IPv6 at once. Write the policy to /etc/nftables.conf:
#!/usr/sbin/nft -f
flush ruleset
table inet filter {
chain input {
type filter hook input priority 0; policy drop;
ct state established,related accept
ct state invalid drop
iif lo accept
# Essential ICMP / ICMPv6
ip protocol icmp icmp type { echo-request, destination-unreachable, time-exceeded, parameter-problem } accept
ip6 nexthdr icmpv6 icmpv6 type { echo-request, destination-unreachable, time-exceeded, parameter-problem, packet-too-big, nd-neighbor-solicit, nd-neighbor-advert, nd-router-advert, nd-router-solicit } accept
# SSH
tcp dport ssh ct state new accept
# Log and drop the rest
limit rate 5/minute log prefix "nft-drop-in: " flags all counter drop
}
chain forward {
type filter hook forward priority 0; policy drop;
}
chain output {
type filter hook output priority 0; policy accept;
}
}
Load it, enable it at boot, and inspect the live ruleset:
sudo nft -f /etc/nftables.conf
sudo systemctl enable --now nftables
sudo nft list ruleset
Add other listeners explicitly, one rule each, for example tcp dport { 80, 443 } ct state new accept for a web server. Keep an existing SSH session open while you apply changes so a mistake cannot lock you out. See kernel hardening and services & systemd for the layers around this one.
firewalld and ufw front ends
On RHEL and Fedora, firewalld is the managed front end over nftables. Set a deny default zone and add services by name:
sudo firewall-cmd --set-default-zone=drop
sudo firewall-cmd --zone=drop --add-service=ssh
sudo firewall-cmd --runtime-to-permanent
On Ubuntu, ufw is the friendly wrapper:
sudo ufw default deny incoming
sudo ufw allow OpenSSH
sudo ufw enable
Pick one front end per host. Do not hand-edit /etc/nftables.conf while firewalld or ufw also manages the ruleset, or the two will overwrite each other.
Network hardening sysctls
The firewall cannot change how the kernel itself treats redirects, source routing, or spoofed packets. Set those in a dedicated drop-in at /etc/sysctl.d/90-network-hardening.conf:
# Reverse-path filtering (anti-spoofing)
net.ipv4.conf.all.rp_filter = 1
net.ipv4.conf.default.rp_filter = 1
# Ignore and never send ICMP redirects
net.ipv4.conf.all.accept_redirects = 0
net.ipv4.conf.default.accept_redirects = 0
net.ipv6.conf.all.accept_redirects = 0
net.ipv4.conf.all.send_redirects = 0
net.ipv4.conf.default.send_redirects = 0
# Reject source-routed packets
net.ipv4.conf.all.accept_source_route = 0
net.ipv6.conf.all.accept_source_route = 0
# SYN flood protection
net.ipv4.tcp_syncookies = 1
# Log spoofed / martian packets
net.ipv4.conf.all.log_martians = 1
# Ignore broadcast pings (smurf protection)
net.ipv4.icmp_echo_ignore_broadcasts = 1
# Disable IPv6 router advertisements if you do not use them
net.ipv6.conf.all.accept_ra = 0
net.ipv6.conf.default.accept_ra = 0
Apply every drop-in and confirm a value:
sudo sysctl --system
sysctl net.ipv4.tcp_syncookies
Reducing exposed services
A firewall is no excuse for running services you do not need. List every listening socket with the owning process:
sudo ss -tulpn
Anything bound to 0.0.0.0 or [::] is reachable from the network. Where a service is only consumed locally (a database, a metrics exporter, a cache), bind it to 127.0.0.1 in its config instead of relying solely on the firewall. Disable daemons you do not use at all rather than firewalling them off. Pair this with access & authentication and intrusion detection for defence in depth.
Verify
Confirm the firewall, sysctls, and listeners after every change:
sudo nft list ruleset
sudo systemctl is-enabled nftables
sysctl net.ipv4.conf.all.rp_filter net.ipv4.tcp_syncookies
sudo ss -tulpn
From an external host, scan a port you did not allow and confirm it is filtered:
nmap -Pn -p 22,80,443,3306 your.server.example
Only the ports you explicitly accepted should report open; everything else must be filtered or closed.