crefft Source 2026.10.4

nftables Style Guide

crefft > Infrastructure > nftables

The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in RFC 2119.

Quick Reference

TaskToolCommand
List the active rulesetnftnft list ruleset
List table names onlynftnft list tables
Snapshot the owned tablenftnft list table inet filter > rollback.nft
Validate a file, change nothingnftnft --check --file /etc/nftables.conf
Load a file atomicallynftnft --file /etc/nftables.conf
Arm a timed rollbacksystemd-runsystemd-run --unit=nft-rollback --collect --on-active=5min --timer-property=AccuracySec=1s /usr/sbin/nft --file rollback.nft
Cancel the rollbacksystemctlsystemctl stop nft-rollback.timer
Deploy a change end to endnft-deploynft-deploy /etc/nftables.conf (see Safe Rule Deployment)

This table has no “flush rules” entry. nft flush ruleset deletes every table on the host and leaves an empty ruleset in which no packet filtering happens at all, so it MUST NOT be run as a standalone command. Clear a ruleset only inside a complete replacement file (see Own One Table, Not the Whole Ruleset) or through Break-Glass Recovery.

Overview

TODO: Linux firewall configuration with nftables (replacement for iptables).

Safe Rule Deployment

Why

nft writes directly to the running kernel, and a firewall change travels the same network path it may remove. A candidate ruleset that omits the rule permitting the administrator’s own session ends that session the moment it loads, and the connection needed to undo the change is the connection the change just dropped. The remedies are physical access, out-of-band management, or a rollback armed before the change that fires without operator action.

Three nft properties make a safe procedure possible:

  • nft --check --file FILE validates a file against the running kernel and applies nothing, so a typo is caught before it reaches the packet path.
  • nft --file FILE is a single transaction. The kernel builds the new ruleset alongside the old one and swaps them in one step, so the host is never half-configured, and a file that fails on its last line changes nothing.
  • nft list ruleset output is valid nft --file input, so the previous state can be captured as a loadable rollback file.

Rules

  • Rule changes MUST be applied from a serial console, an out-of-band management interface, or a session that does not traverse the firewall being changed. Where none of those exists, a timed rollback MUST be armed before the candidate is loaded.
  • Every candidate file MUST pass nft --check --file FILE before it is loaded.
  • Every rule change MUST be a single nft --file transaction. Shell scripts that issue a sequence of nft add commands leave the host partially configured between commands.
  • A configuration file MUST replace only the tables it owns.
  • flush ruleset MUST NOT appear in any file loaded on a host where Docker, firewalld, libvirt, podman, or a CNI plugin manages its own tables.
  • nft flush ruleset MUST NOT be run as a standalone command outside Break-Glass Recovery.
  • The rollback MUST be armed before the candidate is loaded, and canceled only after management access has been reverified from a new session.
  • Rollback files SHOULD be kept in /var/lib/nftables-rollback with mode 0600, because a captured ruleset discloses the host’s entire access policy.

Own One Table, Not the Whole Ruleset

Docker creates the ip docker-bridges and ip6 docker-bridges tables and “expects to have full ownership of its tables”; firewalld, libvirt, and podman manage their own tables the same way. A file beginning flush ruleset deletes all of them, which silently removes container and virtual-machine connectivity along with the policy that was being replaced.

Replace one named table instead. destroy table removes the table if it is present and succeeds if it is not, so the file is idempotent and needs no guard:

# GOOD: /etc/nftables.conf replaces only the table this host owns.
#!/usr/sbin/nft -f
destroy table inet filter

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

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

    meta l4proto ipv6-icmp accept
    icmp type { echo-request, destination-unreachable, time-exceeded } accept

    tcp dport 22 accept
  }
}
# BAD: destroys every table on the host, including docker-bridges.
#!/usr/sbin/nft -f
flush ruleset

table inet filter {
  chain input {
    type filter hook input priority filter; policy drop;
    tcp dport 22 accept
  }
}

flush ruleset is acceptable in exactly one routine case: a host that owns its entire ruleset, where the file that flushes also defines every table the host needs. On any host running Docker, firewalld, libvirt, or podman, that condition does not hold.

destroy is version-sensitive. Debian 13’s nftables 1.1.3-1 accepts destroy table; Debian 12’s nftables 1.0.6 rejects it with syntax error, unexpected table. Upstream stable is 1.1.7. On 1.0.x, open the file with the create-then-delete pair instead, which cannot fail whether or not the table already exists:

table inet filter
delete table inet filter

table inet filter {
  # ...
}

Deploy With an Armed Rollback

nft-deploy captures the current definition of the owned table, validates the candidate, arms a rollback, and only then loads the candidate. Install it as /usr/local/sbin/nft-deploy with mode 0750:

#!/usr/bin/env bash
# /usr/local/sbin/nft-deploy - load a candidate ruleset with an armed rollback.
# Run from a serial console, out-of-band management, or a session that does
# not traverse the firewall being changed.
set -euo pipefail

candidate=${1:-/etc/nftables.conf}
family=${NFT_FAMILY:-inet}
table=${NFT_TABLE:-filter}
window=${NFT_ROLLBACK_WINDOW:-5min}

state=/var/lib/nftables-rollback
rollback="$state/rollback-$(date -u +%Y%m%dT%H%M%SZ).nft"

umask 077
mkdir -p "$state"

# 1. Capture the live definition of the table this host owns. The destroy
#    line makes the rollback file a complete replacement for that table and
#    leaves tables owned by Docker, firewalld or libvirt untouched.
printf 'destroy table %s %s\n' "$family" "$table" > "$rollback"
nft list table "$family" "$table" >> "$rollback" 2>/dev/null || true

# 2. Reject a rollback file or a candidate that does not parse. --check
#    validates against the running kernel without changing anything.
nft --check --file "$rollback"
nft --check --file "$candidate"

# 3. Arm the rollback BEFORE the candidate is loaded. AccuracySec is pinned
#    because systemd timers default to AccuracySec=1min.
systemd-run --unit=nft-rollback --collect \
  --on-active="$window" --timer-property=AccuracySec=1s \
  /usr/sbin/nft --file "$rollback"

# 4. Load the candidate atomically.
nft --file "$candidate"

cat <<MSG
Candidate loaded. Rollback to $rollback fires in $window.
Verify management access from a NEW, independent session, then cancel:
  systemctl stop nft-rollback.timer
MSG

The candidate MUST define only the table named by NFT_TABLE. The rollback restores that one table; a table created by a stray candidate is outside its reach.

--timer-property=AccuracySec=1s is REQUIRED. systemd timers default to AccuracySec=1min, and a rollback that drifts by up to a minute is a minute of lost management access.

Verify Before Canceling the Rollback

Health checks MUST run in a connection opened after the change, not in the session that applied it: an established connection survives on the conntrack established,related rule and proves nothing about new ones.

# From a workstation, on a NEW connection - not the deploy session.
ssh -o BatchMode=yes -o ConnectTimeout=5 admin@host true \
  && echo "management access OK"

# Confirm the intended policy is the live policy.
ssh admin@host nft list chain inet filter input

Cancel the rollback only once those checks pass:

systemctl stop nft-rollback.timer

If they fail, change nothing and wait: the armed timer restores the previous ruleset without any further access to the host.

Break-Glass Recovery

This is the only place flush ruleset belongs in a command an operator types. It MUST be used from a console or out-of-band session, and only when the host is unreachable and the rollback timer has already been lost.

Keep the recovery ruleset on disk as a complete replacement file rather than clearing the ruleset and leaving the host unprotected:

#!/usr/sbin/nft -f
# /etc/nftables.d/break-glass.nft
# BREAK GLASS ONLY. Replaces every table on this host with a minimal
# management-only policy. Tables owned by Docker, firewalld, libvirt and
# podman are destroyed; restart those services afterwards to rebuild them.
flush ruleset

table inet break-glass {
  chain input {
    type filter hook input priority filter; policy drop;

    iif "lo" accept
    ct state established,related accept
    meta l4proto ipv6-icmp accept

    ip saddr 203.0.113.0/24 tcp dport 22 accept
  }

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

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

Replace 203.0.113.0/24 with the administrative network. Then, at the console:

# Capture what is live before replacing it, for the incident record.
install -d -m 0700 /var/lib/nftables-rollback
install -m 0600 /dev/null /var/lib/nftables-rollback/pre-break-glass.nft
nft list ruleset > /var/lib/nftables-rollback/pre-break-glass.nft

nft --check --file /etc/nftables.d/break-glass.nft
nft --file /etc/nftables.d/break-glass.nft

A bare nft flush ruleset MUST NOT be used in its place. It leaves the host with no packet filtering at all rather than a reduced policy, and the difference is the whole exposure window until a real ruleset is loaded.

Base Configuration

TODO: /etc/nftables.conf structure, tables, chains, rules.

Common Rulesets

TODO: Default deny, SSH access, web server, Docker integration.

Rate Limiting

TODO: Connection rate limiting, SYN flood protection.

Logging

TODO: Logging dropped packets, integration with rsyslog/journald.

Persistence

Atomic rule loading is covered in Safe Rule Deployment.

TODO: nftables.service, boot ordering, configuration management.

See Also