Your mesh ACL check returned nothing — because you asked the wrong firewall
I was auditing NetBird policies across a mixed fleet — the usual goal, where a management host may reach the fleet but the fleet may not reach back into it. Before changing a policy I wanted to see what each peer was actually enforcing, rather than what the management console claimed.
NetBird enforces ACLs inbound on the destination peer, as local firewall rules. So: log into the peer, dump the rules.
$ sudo nft list table ip netbird
$
Nothing. Exit code 0, no error, no output.
The obvious reading is that no rules exist and nothing is permitted. That reading is wrong, and acting on it means rewriting policies that were fine all along.
Two backends, one agent
The NetBird agent programs whichever firewall the host actually runs. Across a mixed fleet you get both:
Debian / Ubuntu — nftables:
sudo nft list table ip netbird
# rules reference sets named nb00000NN
RHEL / CentOS / anything on firewalld — iptables + ipset:
sudo iptables -S NETBIRD-ACL-INPUT
sudo ipset list | grep -i '^Name:.*nb'
# sets named like nb...-dport
Query a host with the wrong one of these and you do not get an error. nft list table ip netbird on an iptables host prints nothing and exits clean, because from nftables’ point of view the question is coherent — that table simply isn’t there.
Silence is indistinguishable from “no rules.” That is the entire trap.
Why this is worse than a normal wrong turn
Most bad commands announce themselves. This one produces output that looks like a legitimate finding, in the exact shape you were expecting, at the exact moment you’re deciding whether a security policy is too permissive.
Had I trusted it, the conclusion would have been “this peer permits nothing, the policy isn’t applying, go rewrite it” — confidently wrong, and the remediation would have loosened a working configuration in the name of fixing it.
A companion trap on the same host
While confirming reachability on the same RHEL box, I reached for the usual quick port test:
timeout 2 bash -c '</dev/tcp/10.0.0.5/9001' && echo OPEN || echo CLOSED
Every port came back CLOSED. Including ports I could see were listening.
RHEL builds bash without /dev/tcp support. The redirection fails, bash returns non-zero, and the idiom reports CLOSED for everything — open, closed, filtered, all identical. Same failure mode as the nft call: a confident wrong answer rather than an error. Use curl, nc, or ss instead:
curl -sS --max-time 3 -o /dev/null http://10.0.0.5:9001 && echo OPEN || echo CLOSED
What saved me was that ping had already proven the path was up. The two results contradicted each other, and that contradiction was the only reason I looked harder.
Detect the backend first
Don’t assume per-host; ask:
if command -v nft >/dev/null && sudo nft list tables 2>/dev/null | grep -q netbird; then
echo "== nftables =="
sudo nft list table ip netbird
elif sudo iptables -S 2>/dev/null | grep -q NETBIRD; then
echo "== iptables + ipset =="
sudo iptables -S NETBIRD-ACL-INPUT
sudo ipset list -n | grep '^nb'
else
echo "== NO NETBIRD RULES FOUND BY EITHER BACKEND =="
fi
The value is entirely in that last branch. It is the difference between “I looked and found nothing” and “I looked in the only two places it could be and found nothing” — and only the second is a finding.
While you’re in there, two things about NetBird’s own model that make the rules easier to read:
bidirectional: trueon a policy grants the reverse path too. If you want one-way access — host A reaches the fleet, fleet cannot reach A — this must be off. It is the single setting most likely to be quietly undoing your intent.- The
netbird-sshprotocol emits rules for both port 22 and 22022, not just 22. If you’re port-scoping a policy expecting one rule and see two, that’s why.
The takeaway
An empty result from a diagnostic is only meaningful if you have established the diagnostic can produce a non-empty one. Otherwise you have not measured anything — you have just failed to measure, which looks identical from the terminal and reads as evidence.
The practical version: give yourself a positive control. Run the command somewhere you know has rules and confirm it prints them. In a heterogeneous fleet, be specific about which “somewhere” — a check verified on Debian tells you nothing about what it does on RHEL, and the failure will be silent, plausible, and pointed in a dangerous direction.
Hostnames, paths, identifiers and product-specific strings in this post are illustrative. The failure modes, commands and fixes are real.