skip to content

You create an nftables table, add a chain to it and add a drop rule to that chain, but the traffic is never filtered. What must an nftables chain declare before the kernel evaluates its rules, and how does a base chain differ from a regular chain?

level: middleimportance: must knowfreq 62%

answer

  1. nftables ships no chains at all
  2. the chain header line matters
  3. type, hook, priority — then policy
  4. jump targets need no attachment
  5. policy applies after the last rule

basics

~20 s

Only a base chain is attached to the packet path, and it must declare type, hook and priority — optionally policy. A chain without that header is a regular chain: it runs only when another rule jumps to it, so its rules are never reached on their own.

solid answer

~50 s

nftables has no built-in chains at all — that is one of the headline differences from the older tooling, where INPUT, FORWARD and OUTPUT always existed whether you wanted them or not. In nftables you create every chain, and a chain becomes part of the packet path only if it is declared as a **base chain**, with a header like `type filter hook input priority filter; policy drop;`. `type` is `filter`, `nat` or `route`; `hook` names the attachment point; `priority` is a signed number (or a keyword such as `filter`, `mangle`, `srcnat`) that orders chains against each other. A chain declared without that header is a regular chain: a container reached only by `jump` or `goto` from another rule. So the symptom described is almost always a chain created bare. Confirm with `nft list ruleset` and look at the chain's opening line — if there is no `type ... hook ...` clause, nothing will ever enter it.

code

bash · 8 lines
bash
nft add table inet filter
# base chain: attached to the packet path
nft add chain inet filter input '{ type filter hook input priority filter; policy drop; }'
# regular chain: reached only by jump/goto
nft add chain inet filter ssh_checks
nft add rule inet filter ssh_checks ct state new limit rate 3/minute accept
nft add rule inet filter input tcp dport 22 jump ssh_checks
nft list ruleset

go deeper

for a junior

Remember that nftables gives you no chains for free and that a chain only filters if it declares type, hook and priority. Be able to spot a missing header line in nft list ruleset output.

for a middle

Explain each part of the base-chain header, what a policy does and when it applies, and how regular chains plus jump/goto keep a large ruleset organised without adding hook registrations.

for a senior

Show you can reason about several base chains sharing a hook: ordering by priority, accept ending only the current chain, drop being final, and what that implies for a host where more than one thing writes rules.

for a principal

Set the convention for how chains are laid out across an estate — which priorities are reserved for which purpose, whether frontends are permitted, and how a default-drop posture is rolled out without locking out remote access.

## There are no built-in chains The first thing to internalise is that nftables ships an empty slate. There is no default table, no default chain, and no chain that exists merely because the firewall subsystem is loaded. A fresh host with `nft list ruleset` printing nothing has no rules and no chains — every table, chain and rule you want, you create. That design is deliberate: it means you pay only for what you register, and it means a chain's attachment to the packet path is an explicit, visible property rather than an implicit one. ## What makes a chain a base chain A **base chain** is a chain registered with the kernel at a packet-processing attachment point. It is created with a header clause: ```bash nft add chain inet filter input '{ type filter hook input priority 0; policy drop; }' ``` Four things appear there, three of them mandatory: - **`type`** — `filter`, `nat` or `route`. It declares what the chain is allowed to do; a `nat` chain, for example, is the only place NAT statements belong, and the kernel only registers connection-tracking-dependent machinery where it is needed. - **`hook`** — the attachment point, named with keywords such as `prerouting`, `input`, `forward`, `output`, `postrouting` (and `ingress`/`egress` for device-level chains). Which hooks are available depends on the family. - **`priority`** — a signed integer deciding the order in which chains on the same hook run, lowest first. Keywords exist for the classic values: `raw` (-300), `mangle` (-150), `dstnat` (-100), `filter` (0), `security` (50), `srcnat` (100). Using the keyword documents intent better than a bare number. - **`policy`** — optional, `accept` (the default) or `drop`. It is the verdict applied to a packet that reaches the end of the chain without any rule deciding its fate. ## Regular chains A chain declared with no header: ```bash nft add chain inet filter ssh_checks ``` is a **regular chain**. It is not attached to anything. Its rules are evaluated only when some rule elsewhere transfers control to it: ```bash nft add rule inet filter input tcp dport 22 jump ssh_checks ``` `jump` returns to the calling chain when the target chain ends without a terminal verdict; `goto` does not return — evaluation continues at the calling chain's *hook* level, not after the calling rule. Regular chains are how you keep a large ruleset readable, and they cost nothing when the guarding match is false. They also cannot carry a policy, because a policy is meaningless for a chain that is not the last word on a packet. ## Priority and multiple base chains on one hook Several base chains — from different tables, even different families — may register on the same hook. They all run, in ascending priority order. This is where a genuine misconception bites: an `accept` verdict in one base chain terminates evaluation *of that chain*, but the packet still goes on to the next base chain on the same hook, which is free to drop it. There is no "accept wins" rule. `drop`, by contrast, is immediate and final: the packet is discarded and no further chain sees it. The practical consequence is that a packet must survive *every* base chain on its path. That is what makes a hand-written table and a firewall frontend's table interact the way they do. ## Policy runs last, not first `policy drop` is not evaluated before the rules. Rules are walked in order; the policy applies only if none of them issued a verdict. It also applies only to base chains. A frequent operational injury is loading a `policy drop` input chain over a remote session before adding the rule that permits the session — the ruleset is valid, the rules just do not cover you. ## Diagnosing the reported symptom `nft list ruleset` prints the chain header verbatim, so the check is direct: ``` table inet filter { chain input { } } ``` No `type`/`hook`/`priority` line means a regular chain that nothing jumps to — rules inside it are dead code. Add `-a` to get rule handles when you need to delete a specific rule, and remember that a chain cannot be converted in place in older nft versions: you delete and recreate it with the header, which is another reason to keep the ruleset in a file and load it atomically.

  • Two base chains from different tables register on the same hook. One accepts a packet — can the other still drop it?
    Yes. `accept` ends evaluation of that chain only; the packet continues to the next base chain on the hook in priority order, and a `drop` there is final. Priority controls ordering, not authority. A packet has to survive every base chain on its path, which is why a hand-written table and a frontend's table effectively intersect rather than override one another.
  • What is the difference between `jump` and `goto` when moving to a regular chain?
    `jump` pushes a return address: when the target chain ends without a terminal verdict, evaluation resumes at the rule after the jump. `goto` does not return — when the target chain ends, evaluation continues at the hook level, so the rest of the calling chain is skipped. Use `jump` for a subroutine, `goto` for a one-way dispatch into a per-service or per-interface chain.
  • Why do people recommend loading a `policy drop` chain from a file rather than building it up with individual add commands?
    Because the policy is in force from the moment the chain is registered, while the rules that permit your session arrive later. Each `nft add` is its own transaction, so there is a real window where you are locked out. A file loaded with `nft -f` applies as one transaction, and `nft -c -f` checks it first — so the permit rules and the drop policy land together.

saying these in an interview costs you the question

  • Expects INPUT, FORWARD and OUTPUT to already exist
  • Thinks any chain you create is on the packet path
  • Believes an accept in one chain overrides a later drop
  • Says the chain policy is checked before the rules run
  • Assumes priority decides which chain's verdict wins

context