Docker documents the DOCKER-USER
chain (Add iptables policies before Docker’s rules):
Docker installs two custom iptables chains named DOCKER-USER
and
DOCKER
, and it ensures that incoming packets are always checked by
these two chains first. These chains are part of the FORWARD
chain.
While it's not documented, Docker won't complain if it already finds the DOCKER-USER
chain. That means that attempting to create this chain before adding a rule to it, and also ignoring an error if it already existed allows to add such rule before or after Docker was started during boot, without having to care about services order during boot.
Usually it even makes sense to have its own firewall rules added before network and services start, including Docker services and thus Docker, so usually one configures it before (probably with a Before=network-pre.target
or maybe just Before=network.target
etc. with systemd) and this requires creating the chain oneself.
Just insert in the script the command to create the DOCKER-USER
chain (the || true
isn't really needed, that's just to remind us to also not complain if the chain already existed).
#!/bin/sh
iptables -N DOCKER-USER || true
iptables -I DOCKER-USER -s 172.30.0.0/16 -j REJECT
Also indeed as OP already did, it's best to insert rather than append rules to this chain, because Docker usually adds a final -j RETURN
to it: if Docker created it first, appending rules after this would have no effect. To keep natural order that would have been done with -A
one can also number the insertions. For example to add an exception to OP's exception the rules would be:
iptables -I DOCKER-USER 1 -s 172.30.42.0/24 -j ACCEPT
iptables -I DOCKER-USER 2 -s 172.30.0.0/16 -j REJECT
To make the script idempotent (but yet not atomic), one can choose to flush the chain after its (attempted) creation, so in case it's restarted later, rules aren't duplicated. For the same reason (idempotence and identical result) add -j RETURN
at the end (Docker won't add it again if found if it was itself restarted again), even if this entry doesn't really matter. In this case, insertions won't matter anymore since the chain is guaranteed empty. With above example this would become:
#!/bin/sh
iptables -N DOCKER-USER || true
iptables -F DOCKER-USER
iptables -A DOCKER-USER -s 172.30.42.0/24 -j ACCEPT
iptables -A DOCKER-USER -s 172.30.0.0/16 -j REJECT
iptables -A DOCKER-USER -j RETURN
To be thorough on the subject, to also achieve ruleset atomicity when restarting this script later, so a packet doesn't get allowed right while the chain was just flushed, one can use iptables-restore
, which guarantees atomicity, in a different way than usual: single chain restoration rather than complete table restoration. Here restore only the DOCKER-USER
chain:
#!/bin/sh
iptables-restore --noflush << 'EOF'
*filter
:DOCKER-USER - [0:0]
-A DOCKER-USER -s 172.30.42.0/24 -j ACCEPT
-A DOCKER-USER -s 172.30.0.0/16 -j REJECT
-A DOCKER-USER -j RETURN
COMMIT
EOF