skip to content

How does loading an nftables ruleset with `nft -f rules.nft` differ from applying the same rules one `nft add` command at a time, and why do production nftables files usually begin with `flush ruleset`?

level: seniorimportance: should knowfreq 42%

answer

  1. one netlink batch, one commit
  2. all applied or none applied
  3. loading a file appends, not replaces
  4. reload twice, count your rules
  5. flush and load in the same commit

basics

~20 s

nft -f submits the whole file as one kernel transaction: it is applied completely or not at all, so there is no window with a half-built firewall. Files start with flush ruleset because loading is additive — reload without it and every rule is duplicated.

solid answer

~60 s

Each individual `nft add` is its own transaction, so a script building a ruleset step by step passes through every intermediate state, and a failure halfway leaves the host in one of them. `nft -f` sends the entire file to the kernel as a single atomic commit: a syntax error or a semantic error anywhere aborts the whole thing and the previously loaded ruleset stays in force, untouched. That is why a remote firewall change should always go through a file. The `flush ruleset` line matters because loading a file **adds** to what is already there — `add table` on an existing table is a no-op rather than a reset, and the chain bodies in the file append their rules again, so a second load leaves every chain holding two copies of every rule. Since `flush ruleset` is inside the same transaction, the flush and the new ruleset land together and there is no unfirewalled moment. Check first with `nft -c -f rules.nft`, and treat `nft list ruleset` as the authoritative view of what is actually loaded.

code

bash · 21 lines
bash
cat > /etc/nftables.conf <<'EOF'
#!/usr/sbin/nft -f
flush ruleset

table inet filter {
        chain input {
                type filter hook input priority filter; policy drop;
                ct state established,related accept
                ct state invalid drop
                iif lo accept
                tcp dport 22 accept
        }
        chain forward {
                type filter hook forward priority filter; policy drop;
        }
}
EOF

nft -c -f /etc/nftables.conf   # validate only, applies nothing
nft -f /etc/nftables.conf      # one atomic commit
nft list ruleset               # what the kernel actually holds

go deeper

for a junior

Know that the kernel loses all nftables rules at reboot, that the ruleset lives in a file loaded by the nftables service, and that nft list ruleset shows what is actually in force.

for a middle

Explain that nft -f is one atomic transaction while individual add commands are not, and why an additive load without flush ruleset duplicates every rule on the second run.

for a senior

Show the safe remote-change routine: validate with nft -c -f, scope the flush so you do not wipe other tenants' tables, keep established traffic accepted, and have a rollback that fires if you lose access.

for a principal

Decide how firewall state is owned across the fleet — generated from configuration and shipped as a whole file, versus mutated in place — and how that interacts with agents and runtimes that install their own tables on the same hosts.

## One transaction versus many nftables talks to the kernel over netlink, and its unit of change is a transaction. When you run `nft add rule ...`, the tool opens a transaction, puts one change in it and commits. Run twenty such commands and you have twenty commits, and the packet path is live between every pair of them. `nft -f rules.nft` parses the entire file first, turns it into one batch of netlink messages, and commits once. Two consequences follow, and both are the reason experienced people never build a firewall interactively on a machine they care about: 1. **All or nothing.** If line 40 has a typo, or refers to a chain that does not exist, or uses a match the kernel does not support, the commit fails and *nothing* from the file is applied. The ruleset that was loaded before is still loaded. You get an error message and a working host. 2. **No intermediate states.** There is never a moment when the drop policy is in force but the rule permitting your SSH session has not landed yet, or when half the NAT rules exist. Anyone who has locked themselves out of a remote box by adding a default-drop policy before the accept rules understands why this matters. ## Why the file starts with `flush ruleset` Loading is additive, not declarative. The file syntax looks declarative: ``` table inet filter { chain input { type filter hook input priority filter; policy drop; ct state established,related accept tcp dport 22 accept } } ``` but `table inet filter { ... }` means *ensure this table exists and apply what follows to it*. Loading the same file twice does not replace the chain; it appends its two rules a second time. The chain now has four rules, the duplicates are harmless in behaviour but ruinous for review, and after a few reloads nobody can read the output of `nft list ruleset` any more. `flush ruleset` as the first line removes every table in every family, and because it is part of the same transaction the removal and the reload commit together. The host is never briefly ruleless. ## Scoping the flush `flush ruleset` is a big hammer: it also deletes tables you did not write. Container runtimes, orchestrators and firewall frontends install their own tables, and wiping them mid-flight breaks connectivity for workloads that had nothing to do with your change. When something else shares the host, delete only your own table. The classic idiom makes the delete safe even on a first run: ``` table inet myfilter delete table inet myfilter table inet myfilter { ... } ``` The bare `table` line creates it if absent so the `delete` cannot fail on a missing object. nft 1.0.8 and later add `destroy table inet myfilter`, which deletes without erroring when the table is not there and expresses the same intent in one line. ## Check before you commit ```bash nft -c -f /etc/nftables.conf ``` `-c` (`--check`) parses and validates against the running kernel without applying anything. It catches typos and unsupported constructs. It cannot tell you the ruleset is *correct* — a perfectly valid file can still lock you out — so on a remote host the belt-and-braces move remains a scheduled rollback: arrange for the previous ruleset to be restored in a few minutes unless you cancel it. ## What a reload does not carry over A flush-and-reload builds fresh objects, so anything accumulated at runtime is gone: - **Counters** reset to zero. `nft -s list ruleset` produces a stateless dump precisely so a saved file does not carry stale counter values back in. - **Set contents** added with `nft add element` disappear unless the file declares them. A dynamically grown ban list starts empty again. - Connection tracking state is a separate subsystem and survives, which is why an established SSH session usually keeps working across a reload as long as the new ruleset accepts `ct state established,related`. ## `nft list ruleset` is the source of truth Whatever your files say, the loaded ruleset is what the kernel holds, and `nft list ruleset` prints it in full — every family, table, chain, set and rule, in one output. Add `-a` for handles when you need to delete a specific rule. Persistence is a separate concern: the kernel forgets everything at reboot, so the ruleset lives in a file (`/etc/nftables.conf` on Debian-family systems, `/etc/sysconfig/nftables.conf` on RHEL-family) loaded at boot by the `nftables` service. Saving with `nft list ruleset > /etc/nftables.conf` works, but a hand-written, reviewed, version-controlled file is the better artefact — and remember the saved dump needs the `flush ruleset` line put back at the top.

  • You must reload your own table on a host that also runs a container runtime with its own nftables tables. What do you do instead of `flush ruleset`?
    Scope the replacement to your table: emit `table inet myfilter`, then `delete table inet myfilter`, then the full definition — the bare create makes the delete safe on a first run. On nft 1.0.8+ `destroy table inet myfilter` does the same in one line. Everything stays in the same transaction, and the runtime's tables are untouched.
  • A reload succeeds but your dynamically built ban list is empty afterwards. Why, and what would you change?
    Flush-and-reload recreates the set, so only elements declared in the file survive; anything added later with `nft add element` is gone. Either dump the set with `nft list set` and re-add its elements after loading, or keep the ban list in a table you do not replace, so a routine policy reload never touches it.
  • `nft -c -f` passed, yet applying the file locked you out of the host. What does the check actually guarantee?
    Only that the file parses and that every construct is supported by the running kernel — it is a syntax and feature check, not a policy review. A perfectly valid default-drop ruleset with no rule for your management port is accepted. On remote hosts, pair the load with a scheduled restore of the previous ruleset that you cancel once you have confirmed access.

nft -f is a database transaction: the whole file commits or the old ruleset stands. Applying rules one command at a time is a sequence of separate commits, and a failure leaves you in whichever half-finished state you reached.

saying these in an interview costs you the question

  • Thinks a file load replaces the existing ruleset
  • Says a failed line applies the rules before it
  • Believes flush ruleset only affects the current table
  • Assumes counters and set elements survive a reload
  • Treats the config file, not the kernel, as current state

context