CoursesAdvanced container securityRuntime & network hardening

Container network hardening

Default-deny egress, no inter-container comms, block metadata.

Advanced15 min · lesson 18 of 25

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.

terminal
# 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.

terminal
# 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.

terminal
# 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
A flat network turns one foothold into many
The default bridge with open egress is why a single compromised container so often becomes a cluster-wide incident: the attacker scans the flat network, reaches the database directly, and exfiltrates over unrestricted outbound. Segment with user-defined/internal networks, block the metadata IP, and default-deny egress — the network is a control surface, not just plumbing.