Docker: reconcile the daemon's firewall rules with the board's nft ruleset #66
Labels
No labels
blocked-physical
cleanup
hardware
infra
kernel
P1-critical
P2-high
P3-normal
P4-later
reliability
security
upstream
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
tiagoagueda/a80#66
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Split out of the Docker enablement work.
Measured, before Docker is installed
nft list ruleset- a hand-writtentable inet filterwhose input chain istype filter hook input priority filter; policy drop, 25 lines totalnet.ipv4.ip_forward = 0iptablesis iptables-nft by alternatives defaultWhat Docker will do to that
Starting the daemon sets
ip_forward=1and installsDOCKER,DOCKER-USERandDOCKER-ISOLATION-*chains. The consequence that surprises people:Same class as the well-known Docker/UFW interaction. Not a bug to fix - a posture to
choose deliberately.
The backend choice is genuinely open
The Tier 0 kernel change enabled both paths on purpose:
NETFILTER_XTABLES_LEGACY+IP_NF_IPTABLES_LEGACYand its tablesNFT_COMPAT,NFT_NAT,NFT_MASQ,NFT_FIB_*So the kernel does not force the decision. Make it explicitly and write it down - the
two backends put rules in different places and a mixed setup is very hard to reason
about six months later.
Options
127.0.0.1and front everything with a reverse proxy whose own portis governed by the existing input chain. Keeps one place to reason about exposure.
DOCKER-USERfor allow/deny - Docker leaves that chain alone across restarts."iptables": falseand hand-manage NAT. Maximum control, and you own every bug.Done when
systemctl restart dockerand aftera reboot - verified, not assumed
ip_forwardbeing on is a recorded decision rather than a side effectprovision.shDone, and the board was in a worse state than this issue described.
What was actually happening
The hand-written
inet filterruleset haschain forward { policy drop; }, and Docker installsits own base chain at the same hook (
ip filter FORWARD). Both are evaluated, so a packetmust survive both - and ours had no rules at all. Measured, not reasoned about:
ping 1.1.1.1)bad addressContainer networking was entirely broken. My earlier "it works" check was worthless: the
image pull is host traffic, and host->container over loopback never traverses FORWARD.
And the hazard this issue was opened about was real but inverted. Published ports were not
exposed - they were unreachable, by the same drop that broke the containers. So the obvious
"fix" (relax the forward policy) would have restored container networking and exposed every
published port to the LAN in one stroke, with the input chain's
policy droplooking like itwas still protecting things.
The decision, implemented
Option 1 from the original post. The forward chain now allows egress specifically rather than
relaxing the policy:
Nothing inbound to a container is accepted. To publish a service: bind it to the loopback
(
-p 127.0.0.1:8080:80) and front it with a reverse proxy on the host, whose own port isgoverned by the input chain. There is a commented, worked example in the file for deliberately
exposing a container anyway.
Two things had to change for that to hold
/etc/nftables.confno longer doesflush ruleset. Docker keeps its tables in the sameruleset; flushing everything deletes them and nothing recreates them - container networking
simply stays broken while the firewall looks healthy. It deletes only
inet filternow.nftables.servicetears down withnft flush rulesetregardless of the configfile. A drop-in scopes
ExecStopthe same way. Without it,systemctl restart nftablesstill wiped Docker's tables - observed, then confirmed fixed.
Verified
0.0.0.0:8099, from ouranos127.0.0.1:8098, from ouranossystemctl restart dockersystemctl restart nftablesThe control matters: without it, "blocked" and "the test is broken" look identical. My first
attempt at this test was ambiguous because
curl -wprinted on both paths.ip_forward=1is now a recorded consequence rather than a side effect - Docker sets it, and theforward chain above is what makes that safe.