Docker audit fix

How to Bind Docker Published Ports to Loopback on VPS

Publishing a port with -p 8080:80 binds it to 0.0.0.0, and Docker's iptables rules are evaluated before UFW, so the port answers from the internet even when the host firewall denies it. Bind published ports to 127.0.0.1 instead.

Hiren KalariyaLast reviewed: Oct 19, 2026Check port-publishing
High
severity
Yes
needs root or sudo
3
results it can return
1 of 5
checks in this audit

Every threshold on this page is transcribed from the docker-security-network-supply-chain audit script that CtrlOps runs, and a build check fails if the two ever disagree. See all 5 Network & Supply Chain checks, or how the audit runs.

High severityNeeds sudo

What this check reads

It takes the running container ids from docker ps -q and then, for each one, asks docker inspect for the container name and for every host binding under .NetworkSettings.Ports, formatted as HostIp:HostPort. A binding whose host IP is 0.0.0.0, :: or empty counts as "all interfaces", and the first such binding on a container is kept as the evidence. One container with a binding like that is enough for the WARN, which prints each offender as container(ip:port); if no container has one, the check PASSes.

When it applies

Needs a reachable daemon and at least one running container; otherwise SKIP. It reads .NetworkSettings.Ports, so it sees only published ports - a container reachable solely on a user-defined Docker network, or one using --net=host (which publishes nothing but exposes everything), produces no finding. A host IP of 0.0.0.0, :: or empty counts as "all interfaces"; a binding to a specific address, including a private LAN address, passes. It reports the configured binding, not whether anything upstream filters the traffic.

What each result means

Result thresholds this check applies
ResultWhenWhat it means
SKIPNo running containersNothing was inspected, so no conclusion was drawn about this host's exposure. A stopped container that would publish on all interfaces is not examined either.
WARNAny container publishes on all interfacesThe named containers accept connections on every host interface, and Docker's rules run ahead of UFW, so those ports are reachable from outside even where the firewall says deny.
PASSAll published ports are bound to a specific IP (or none are published)Every published port names a specific host address, or nothing is published at all. Traffic reaching a container through --net=host is invisible to this test.

Why it matters

-p 8080:80 is -p 0.0.0.0:8080:80. Docker programs nat and FORWARD rules (DOCKER chain) that are evaluated before UFW/firewalld INPUT rules, so a published port is reachable from the internet even when ufw status says it is denied. Docker's own documentation describes this and recommends binding to a specific address, using the DOCKER-USER chain, or setting "ip": "127.0.0.1" in daemon.json. CIS Docker 5.13 "Ensure that incoming container traffic is bound to a specific host interface"; OWASP Docker Cheat Sheet Rule #5 (network).

Why it fails, and when it is wrong

  • A reverse proxy (Traefik/nginx) intentionally publishes 80/443 on all interfaces; that is the correct design, but it still WARNs here. Everything else (databases, admin UIs, metrics) should be on 127.0.0.1 or unpublished.
  • A cloud security group that blocks the port mitigates exposure; the check cannot see it. Confirm with an external scan.
  • IPv6: Docker 27+ publishes on :: too when IPv6 is enabled on the bridge; ip6tables rules are required for the same bypass behaviour.
  • Rootless Docker publishes through RootlessKit and the host firewall does apply in most configurations.

How to fix it

Find what publishes on all interfaces

docker ps --format '{{.Names}}\t{{.Ports}}' | grep -E '0\.0\.0\.0|:::'

Anything listed here answers from outside, whatever ufw status says.

Bind to loopback, or stop publishing

ports:
  - "127.0.0.1:5432:5432"     # local only

Or remove the mapping entirely and let the app reach the service over a user-defined network. A reverse proxy is the one service that should keep 80 and 443 on all interfaces.

Make loopback the daemon default

Add this to /etc/docker/daemon.json and restart Docker, so a bare -p 8080:80 added later binds to loopback rather than every interface.

{ "ip": "127.0.0.1" }

Filter in DOCKER-USER for ports that stay public

Docker evaluates the DOCKER-USER chain before its own rules, so this is where host-level filtering for published ports belongs.

sudo iptables -I DOCKER-USER -i eth0 ! -s 203.0.113.0/24 -p tcp --dport 5432 -j DROP

Or install ufw-docker, which wires UFW into that chain.

Recreate and test from outside

Port bindings are fixed when a container is created.

docker compose up -d --force-recreate
nmap -Pn -p 5432 <server-public-ip>      # run from another machine; expected: filtered or closed

Verify the fix

docker ps --format '{{.Names}}\t{{.Ports}}'
# expected: 127.0.0.1:8080->8080/tcp, not 0.0.0.0:8080->8080/tcp

docker inspect -f '{{.Name}}{{range $p, $c := .NetworkSettings.Ports}}{{range $c}} {{.HostIp}}:{{.HostPort}}{{end}}{{end}}' $(docker ps -q)

# What the host is really listening on, and who owns it:
sudo ss -tlnp | grep -i docker

# The question that actually matters - reachability from outside:
nmap -Pn -p 8080 <server-public-ip>       # expected: filtered/closed

Debugging

This one message covers two different cases: the audit could not reach the Docker daemon, or it reached it and nothing was running. To tell them apart, check systemctl is-active docker, then docker ps as the audit user and sudo docker ps, and run the audit as root or with sudo if only the second works. If both work and list nothing, there is genuinely nothing to evaluate, and a stopped container that would publish widely is not reported. The Docker Inventory check reports the unreachable case under its own message and walks through it in more detail.

This is the point of the check, not a false positive. Docker inserts its own rules in the DOCKER chain of nat/filter, which are evaluated before UFW's rules, so ufw deny 8080 does not stop a published container port. Confirm with sudo iptables -t nat -L DOCKER -n and test from outside with nmap.

Publish to loopback (-p 127.0.0.1:8080:8080, Compose ports: ["127.0.0.1:8080:8080"]) and put a reverse proxy in front, or install ufw-docker so UFW rules apply to Docker traffic. Recreate the container afterwards - port bindings are fixed at creation.

--net=host bypasses port publishing entirely and shows no ports here. Check with docker inspect -f '{{.HostConfig.NetworkMode}}' $(docker ps -q).

Then the host firewall must be Docker-aware. Verify the deny actually works from an external address before accepting the finding; a rule that Docker bypasses is worse than no rule, because it looks protective.

Sources

How the script reads this

Next

Re-run the Network & Supply Chain audit after applying the fix and confirm this check moves to PASS. CtrlOps runs all 5 checks over your existing SSH connection and scores the result, so the change is visible without reading another config file.

All 5 Network & Supply Chain fixes
Audit your fleet

Find every one of these on every server, in one click

CtrlOps runs this audit over your existing SSH connection - no agents, no scripts to manage. $7/user/month after a 1 month free trial - no credit card required.

Windows

✓ Start instantly·✓ No credit card·✓ No sneaky autorenewals