Firewall Archaeology: Auditing an Inherited nftables Ruleset Without a Maintenance Window
Every sysadmin eventually inherits a firewall that looks like it was built by three people who never met, over several years, with at least one panic change made at 2 a.m. In 2026, nftables is the default on current Linux distributions, but that does not mean the ruleset is modern. It often contains compatibility chains, orphaned NAT rules, and accepts that nobody can explain.
The goal of an audit is not to produce a beautiful ruleset. It is to produce a defensible one. You want to know what each rule does, who owns it, and how to undo your changes without opening a support ticket with your ISP or your colo provider.
Snapshot before you interpret
Start by taking a backup that you can read, diff, and restore. Do not start by deleting the weird rule. The weird rule may be the only thing keeping the basement router reachable.
sudo nft list ruleset > nft-$(date +%F).nft
sudo nft -j list ruleset | jq . > nft-$(date +%F).json
sudo sha256sum nft-*.nft
The JSON version is handy when you want to inventory chains and rules without fighting formatting. The text version is what you will feed back into nft later. Keep both. If your ruleset includes comments, great. If it does not, that is the first thing your candidate ruleset should fix.
Also grab handles and counters while you are in there:
sudo nft -a list ruleset
sudo nft list tables
Handles are not permanent identifiers, but they are useful during the audit when you need to point at one specific rule and say, this one is suspicious.
Map rules to services and owners
A port number is not an owner. You need to connect the rule to a process, a systemd unit, and ideally a human or team. Start with the listening sockets and work backward.
sudo ss -tulpn
sudo systemctl list-units --type=service --state=running
If you find an accept rule for TCP 8080, confirm what is actually listening on 8080. If nothing is listening, the rule may be leftover from a decommissioned app. If something is listening but the service is not in your inventory, that is a finding, not a cleanup task.
Build a simple table: chain, handle, port, protocol, service, owner, and ticket reference. For anything unknown, write unknown. Do not guess. Then add comments to the candidate ruleset:
tcp dport 8080 comment "app: inventory-api owner: platform" accept
That comment will save someone else the same archaeology exercise later. It also makes diffs readable when you review the change with a teammate.
Hunt the compatibility shims
On many systems, iptables still exists as a compatibility layer over nftables. That is not automatically bad, but it means you may have two ways to manage the same packet path. Check what the commands actually are:
sudo iptables -V
sudo ip6tables -V
sudo iptables-save
find /etc -maxdepth 3 \( -name 'iptables*' -o -name 'nftables*' \) -print 2>/dev/null
If iptables reports nf_tables, your iptables commands are translating into nftables. If it reports legacy, you have a separate xtables path to account for. Either way, look for service managers and container tooling that write their own chains. Docker, ufw, and firewalld can all leave fingerprints:
sudo nft list ruleset | grep -E 'DOCKER|ufw|firewalld'
Do not rip these out just because they look redundant. First determine which service owns them and whether disabling that service is part of your change. The audit is about reducing surprise, not winning an argument with the package maintainer.
Rehearse in a network namespace
Production is a terrible place to discover a syntax error or a missing established rule. A network namespace gives you a separate nftables ruleset to test against without touching the host firewall.
sudo ip netns add fwaudit
sudo ip netns exec fwaudit nft --check -f candidate.nft
sudo ip netns exec fwaudit nft -f candidate.nft
sudo ip netns exec fwaudit nft list ruleset
That catches syntax problems and table-level mistakes. If you want to test traffic, add a veth pair and put one end in the namespace. For most audits, the namespace is enough to validate that the ruleset loads and that your chain structure is sane. It is not a full simulation of your ISP, your switches, or your users, so keep the rollback plan ready anyway.
The audit checklist
- Back up the live ruleset in text and JSON, then hash the backup.
- Inventory every table, chain, policy, jump, and empty chain.
- Match inbound accepts to listening sockets and running services.
- Assign an owner to every rule, or mark it unknown.
- Review NAT, forwarding, and input rules separately. They fail in different ways.
- Identify iptables, ufw, firewalld, Docker, and other compatibility paths.
- Add comments and stable descriptions to the candidate ruleset.
- Run nft --check and load the candidate in a test namespace.
- Schedule an automatic rollback before applying anything to production.
- Keep console or out-of-band access available during the change.
Rollback is a feature, not an apology
A firewall change without a rollback plan is just a hope with a shell prompt. Create a backup, then create a rollback file that flushes the ruleset and restores the backup.
sudo sh -c 'nft list ruleset > /root/nft-backup-$(date +%F-%H%M).nft'
sudo sh -c '{ echo flush ruleset; cat /root/nft-backup-2026-11-09-1200.nft; } > /root/nft-rollback.nft'
sudo systemd-run --on-active=15min --unit=fw-rollback /usr/sbin/nft -f /root/nft-rollback.nft
Adjust the backup filename to match what you actually created. The transient timer gives you a window to validate the new rules. If you lock yourself out, wait for the timer. If everything works, cancel it:
sudo systemctl stop fw-rollback.timer
sudo systemctl list-timers 'fw-rollback*'
Test the rollback file in the namespace too. A rollback plan you have never executed is a rumor.
The point of the exercise
A clean nftables ruleset is not the one with the fewest lines. It is the one where you can explain why each line exists, which service depends on it, and what happens if it disappears. That is the difference between a firewall and a fossil record. If you cannot name the owner, the rule is a liability. Audit it, document it, test it, and give the next sysadmin a fighting chance.