Container network hardening
Default-deny egress, no inter-container comms, block metadata.
A hardened image on a wide-open network is still a soft target. The default Docker bridge lets every container reach every other container and the whole internet, which is exactly what an attacker who lands a foothold wants: a flat network to pivot across and unrestricted egress to exfiltrate data and pull a second stage. Network hardening shrinks that to only the paths a workload legitimately needs.
Cut inter-container chatter and default egress
Two blunt wins first. Disable inter-container communication on the default bridge (icc=false) so containers cannot freely reach each other, and put workloads on purpose-built user-defined networks that only connect the services that must talk. For a container that needs no network at all — a batch job, a data-transform step — run it with --network none and it simply has no route anywhere. Isolation you do not grant cannot be abused.
# a job that never needs the network gets none — nothing to pivot to or exfiltrate through$ docker run --rm --network none alpine ip route# (empty) — only loopback exists# an "internal" user-defined network: no route to the outside world at all$ docker network create --internal backend$ docker run -d --name db --network backend postgres:16 # unreachable from the internet
Block the cloud metadata endpoint
On a cloud host, 169.254.169.254 serves the node’s IAM credentials over plain HTTP with no auth — and a compromised container (or an SSRF bug in your app) that reaches it can assume the node’s cloud role and pivot into the account. This is one of the most common and highest-impact container-to-cloud escalations, and it is a firewall rule away from closed. Block egress to that link-local address from every workload that does not explicitly need it.
# drop egress to the metadata IP for containers (host iptables/nftables)$ sudo iptables -I DOCKER-USER -d 169.254.169.254 -j DROP$ docker run --rm curlimages/curl -m 3 http://169.254.169.254/latest/meta-data/ ; echo exit=$?exit=28 # timed out — the credential endpoint is now unreachable from containers
Control egress, not just ingress
Most teams firewall inbound traffic and leave outbound wide open, but egress is how data leaves and how a second-stage payload arrives. Default-deny egress and allow only the destinations a service actually calls (its database, a specific API, DNS). At the host level the DOCKER-USER iptables chain is evaluated before Docker’s own rules, so it is the right place to enforce allow-lists; in orchestrated environments this is what NetworkPolicy egress rules do. Also bind published ports to specific interfaces (127.0.0.1) rather than 0.0.0.0 so a “local” port is not quietly public.
# default-deny egress for containers, then allow only what is needed (host firewall)$ sudo iptables -A DOCKER-USER -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT$ sudo iptables -A DOCKER-USER -p udp --dport 53 -j ACCEPT # DNS$ sudo iptables -A DOCKER-USER -d 10.0.0.0/8 -j ACCEPT # internal services$ sudo iptables -A DOCKER-USER -j DROP # deny the rest