Linux Firewall Hardening with nftables: Replacing iptables in Production

Linux Firewall Hardening with nftables: Replacing iptables in Production

Problem

iptables is deprecated. nftables is the replacement in every modern Linux kernel (5.0+). Most teams either still use iptables (accumulating technical debt and missing nftables performance improvements), or have no host-level firewall at all, relying entirely on cloud security groups or Kubernetes network policies. Host-level firewalling provides defence in depth that survives misconfigured higher-level abstractions.

Threat Model

  • Adversary: Network-adjacent attacker scanning for open ports, or attacker who has compromised one service and is pivoting to other services on the same host.
  • Blast radius: Without host firewall, every listening port is reachable from the network. With nftables default-deny, only explicitly allowed ports are accessible.

Configuration

Web Server Ruleset

#!/usr/sbin/nft -f
# /etc/nftables.conf - hardened ruleset for a web server

flush ruleset

table inet filter {
    chain input {
        type filter hook input priority 0; policy drop;

        # Allow established and related connections
        ct state established,related accept

        # Drop invalid packets
        ct state invalid drop

        # Allow loopback
        iif lo accept

        # Allow ICMP (ping) - rate limited
        ip protocol icmp icmp type echo-request limit rate 5/second accept
        ip6 nexthdr icmpv6 icmpv6 type echo-request limit rate 5/second accept

        # Allow SSH - rate limited to prevent brute force
        tcp dport 22 ct state new limit rate 10/minute accept

        # Allow HTTP and HTTPS
        tcp dport { 80, 443 } accept

        # Log dropped packets (rate limited to prevent log flooding)
        limit rate 5/minute log prefix "nftables-drop: " level warn
    }

    chain forward {
        type filter hook forward priority 0; policy drop;
        # No forwarding on a web server
    }

    chain output {
        type filter hook output priority 0; policy accept;
        # Allow all outbound by default.
        # For stricter control, change to policy drop and allowlist.
    }
}

Database Server Ruleset

#!/usr/sbin/nft -f
# Hardened ruleset for a database server (PostgreSQL)

flush ruleset

table inet filter {
    # Define IP sets for allowed sources
    set app_servers {
        type ipv4_addr
        elements = { 10.0.1.10, 10.0.1.11, 10.0.1.12 }
    }

    set admin_hosts {
        type ipv4_addr
        elements = { 10.0.0.5 }
    }

    chain input {
        type filter hook input priority 0; policy drop;

        ct state established,related accept
        ct state invalid drop
        iif lo accept

        # SSH from admin hosts only
        ip saddr @admin_hosts tcp dport 22 accept

        # PostgreSQL from application servers only
        ip saddr @app_servers tcp dport 5432 accept

        # Prometheus node_exporter from monitoring
        ip saddr 10.0.3.0/24 tcp dport 9100 accept

        limit rate 5/minute log prefix "nftables-drop: " level warn
    }

    chain forward {
        type filter hook forward priority 0; policy drop;
    }

    chain output {
        type filter hook output priority 0; policy accept;
    }
}

Kubernetes Node Ruleset

#!/usr/sbin/nft -f
# Hardened ruleset for a Kubernetes node

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

        # SSH from bastion only
        ip saddr 10.0.0.5/32 tcp dport 22 accept

        # Kubelet API (from control plane)
        ip saddr 10.0.0.0/24 tcp dport 10250 accept

        # NodePort range (30000-32767) - if using NodePort services
        tcp dport 30000-32767 accept

        # Calico/Cilium CNI (BGP, VXLAN, health checks)
        ip saddr 10.0.0.0/8 tcp dport 179 accept       # BGP
        ip saddr 10.0.0.0/8 udp dport 4789 accept       # VXLAN
        ip saddr 10.0.0.0/8 tcp dport 4240 accept       # Cilium health

        # Pod CIDR (container traffic)
        ip saddr 10.244.0.0/16 accept

        limit rate 5/minute log prefix "nftables-drop: " level warn
    }

    chain forward {
        # MUST allow forwarding for container traffic
        type filter hook forward priority 0; policy accept;
    }

    chain output {
        type filter hook output priority 0; policy accept;
    }
}

Applying and Persisting

# Apply the ruleset
sudo nft -f /etc/nftables.conf

# Verify active rules
sudo nft list ruleset

# Enable nftables service for persistence across reboots
sudo systemctl enable nftables

# Test: attempt to connect to a blocked port
nc -zv host 3306
# Expected: Connection refused (if MySQL is not in the allowlist)

Migration from iptables

# Export current iptables rules as nftables format
sudo iptables-save | sudo iptables-restore-translate > /etc/nftables.conf

# Review and clean up the generated nftables config
# (iptables-translate output is functional but not optimised)

# Disable iptables and enable nftables
sudo systemctl disable iptables
sudo systemctl enable nftables
sudo systemctl start nftables

Expected Behaviour

  • nft list ruleset shows active rules matching the configured policy
  • Default policy is drop, only explicitly allowed traffic passes
  • SSH rate-limited to 10 new connections per minute
  • Database ports accessible only from allowed source IPs
  • Dropped packets logged (rate-limited to prevent log flooding)
  • Rules persist across reboots via systemd service

Trade-offs

Control Impact Risk Mitigation
Default-deny input policy Blocks all unexpected inbound traffic New services fail until firewall rule is added Add firewall rule updates to service deployment checklist.
SSH rate limiting Blocks SSH brute force Legitimate users may be rate-limited during high-connect periods Increase rate for trusted source IPs (admin set).
IP set for allowed sources Easy to manage allowed IPs IP changes require ruleset update Use DNS-based sets or integrate with cloud metadata for dynamic IPs.
nftables over iptables Better performance (atomic rule updates, native sets) Team must learn nftables syntax Syntax is straightforward; most iptables concepts map directly.

Failure Modes

Failure Symptom Detection Recovery
Ruleset locks out SSH Cannot SSH to the host SSH connection refused; only console access works Use console/BMC access. Fix the ruleset. Or: reboot (if nftables.conf is correct, it will reload correctly).
Missing rule for new service New service unreachable Service monitoring shows connection refused; users report outage Add the required port/source to the nftables ruleset. Apply with nft -f.
Forward chain blocks K8s pods Pod-to-pod communication fails on the node Pods show network errors; CNI health check fails Ensure forward chain policy is accept on Kubernetes nodes (required for container networking).

When to Consider a Managed Alternative

Host-level firewall management does not scale past 20+ hosts with different rulesets. Kubernetes network policies and cloud security groups provide more appropriate abstractions at scale.

  • Cloudflare (#29): Edge DDoS/WAF protection before traffic reaches your hosts.
  • CrowdSec: Collaborative IP blocking with community threat intelligence.
  • Managed K8s: Provider handles node firewall configuration.

Premium content pack: nftables ruleset collection. pre-built rulesets for web servers, databases, Kubernetes nodes, bastion hosts, and monitoring servers. Includes migration script from iptables.