Is your Docker exposed?
You set up UFW. You think your ports are blocked. Then you run docker run -p 6379:6379 redis and your Redis instance is reachable from the public internet — UFW notwithstanding. This is not a bug. It is Docker's documented default behaviour, and it catches experienced engineers by surprise every week.
The iptables trap: how Docker bypasses UFW
When you publish a port with -p, Docker doesn't just “open a port.” It inserts a new iptables rule into the DOCKER chain that ACCEPTS traffic on that port from all source addresses. This rule is evaluated beforeUFW's FORWARD rules — which means UFW DENY rules have no effect on Docker-published ports.
The OWASP Docker Security Cheat Sheet (Rule #5a) puts it plainly: Docker manages its own iptables and nftables rules directly and bypasses UFW entirely. Publishing a port with -p 8000:8000 opens that port to all interfaces and all source addresses, regardless of any firewall deny rules you have set.
The fix is simple but non-obvious: bind the host side of the port mapping to 127.0.0.1 instead of the default 0.0.0.0:
docker run -p 6379:6379 redis
docker run -p 8080:80 nginx
# binds to 0.0.0.0 by defaultdocker run -p 127.0.0.1:6379:6379 redis
docker run -p 127.0.0.1:8080:80 nginx
# only reachable from this machineIn docker-compose.yml the same principle applies: ports: ["6379:6379"] binds to 0.0.0.0; ports: ["127.0.0.1:6379:6379"] binds to localhost only.
Port 2375: the hole in the hull
The Docker daemon's unencrypted TCP API lives on port 2375. If it is running with -H tcp://0.0.0.0:2375, anyone who can reach that port has unauthenticated root-equivalent access to your entire host — not just one container. They can launch containers, mount your filesystem, steal secrets, or pivot to every other service on the machine.
The Docker documentation is explicit: exposing the daemon API over HTTP without TLS is not permitted — modern daemons will refuse to start in this configuration. But legacy setups, remote-CI environments, and copy-pasted StackOverflow configurations still ship with this hole open more often than you'd expect.
Akamai's Hunt Team documented a malware strain — last observed August 2025 — that uses masscan to identify exposed Docker daemons on port 2375, then immediately mounts the host filesystem into a fresh container to achieve full host compromise. The malware binary also contains dormant code referencing ports 9222 (Chrome DevTools Protocol) and 23 (Telnet), but Akamai confirmed that logic is currently unreachable and will not execute in the observed variant.
Port 9222: Chrome DevTools on the public internet
Automated browser testing inside Docker commonly runs Chromium with --remote-debugging-port=9222. When paired with a Docker -p 9222:9222 mapping that binds to 0.0.0.0, the DevTools Protocol endpoint is reachable from the internet — and it grants full remote control of the browser session, including cookies, sessions, and any authenticated tabs.
Anyone who can reach this port has full control of the browser: they can observe and replay traffic, steal session cookies for any logged-in site, and inject arbitrary JavaScript. Chrome DevTools Protocol has no built-in authentication — access equals ownership. If that browser session is running as part of an AI agent pipeline, it is a ready-made exfiltration channel.
How to check what's actually exposed
There are two ways to check your exposure: from inside the box, and from outside it. Both matter. Docker's iptables manipulation means the inside view can be misleading — a port that looks protected from the host's perspective may still be reachable from outside.
ss -tlnpShows which ports have open TCP listeners and which process owns them. Look for 0.0.0.0 in the Local Address column. Any service bound to 0.0.0.0 is a candidate for unintended exposure.
docker ps --format 'table {{.Names}}\t{{.Ports}}'Lists all running containers and their port mappings. A mapping like 0.0.0.0:6379->6379/tcp means that container port is published to all interfaces. A mapping like 127.0.0.1:6379->6379/tcp is safe.
nmap -sV -p 2375,6379,9222,5432 <your-server-ip>Run this from a machine outside your network. This is the attacker's view — it sees what's actually reachable, not what you think you've blocked. Replace the port list with all ports your services use.
What Keelix shows you
Keelix runs an outside-in exposure scan alongside the host and container checks. The result appears in your posture report exactly as you would see it in the home page demo:
The outside-in scan means you get the attacker's view of your box, not just an inventory of what's running. A misconfigured iptables chain that UFW thinks it owns shows up as a finding, not a false negative. Run the scan before you ship, gate CI on the exit code, and the hole gets caught before it goes live.
Fix it: four steps
Bind to 127.0.0.1, not 0.0.0.0
Change every -p mapping to -p 127.0.0.1:HOST:CONTAINER for services that don't need to be directly public. In docker-compose.yml: ports: ["127.0.0.1:6379:6379"]. Re-run docker ps and verify the 0.0.0.0 bindings are gone.
Never expose the Docker daemon on TCP
Run the daemon on the Unix socket only (the default). If you need remote access for CI or tooling, use SSH tunnelling (DOCKER_HOST=ssh://user@host) or mutual TLS — never -H tcp://0.0.0.0:2375 on a internet-connected host.
Apply a cloud security group as a second layer
Your cloud provider's security group or network ACL operates at the network edge, before traffic reaches your host's iptables rules. It is not a substitute for correct Docker bindings, but it is a valuable second layer. Deny all inbound traffic except the ports you explicitly need to be public.
Verify with an outside-in scan
After fixing the bindings, confirm the change with an outside-in scan — either nmap from an external host or keelix scan on the box itself. The outside-in view is what attackers see; the inside view (ss, netstat) can be misleading when Docker's iptables rules are in play.
Frequently asked
- Is my Docker container exposed to the internet?
- Possibly — and more likely than you'd expect. Every time you run `docker run -p 8080:80` or publish a port in docker-compose.yml without binding it to 127.0.0.1, Docker inserts iptables rules that open that port to all interfaces. These rules bypass host firewalls like UFW entirely, so a 'ufw deny 8080' rule will not protect you. Run `ss -tlnp` or `keelix scan` to see what's actually reachable from outside your box.
- Why does Docker bypass UFW?
- Docker manages its own iptables and nftables rules directly, inserting ACCEPT rules in the DOCKER chain before the FORWARD rules UFW relies on. This is a known, documented behaviour — not a bug — and it applies even when UFW is enabled and configured to deny the port. The OWASP Docker Security Cheat Sheet (Rule #5a) explicitly warns about this. The fix is to bind ports to 127.0.0.1 instead of 0.0.0.0 at publish time.
- What is the difference between binding to 0.0.0.0 and 127.0.0.1?
- 0.0.0.0 means 'listen on every network interface on this host', including the public IP. Any internet-connected client can reach the port if your cloud provider's security group or network firewall allows it — and because Docker bypasses UFW, the host firewall won't stop it. 127.0.0.1 (localhost) means 'listen only on the loopback interface' — only processes on the same machine can connect. For any service not meant to be directly public, use 127.0.0.1:HOST_PORT:CONTAINER_PORT in your -p mapping.
- Why is port 2375 dangerous?
- Port 2375 is the Docker daemon's unencrypted TCP API port. If your daemon is running with -H tcp://0.0.0.0:2375, anyone who can reach that port has unauthenticated root-equivalent access to your host: they can launch containers, mount filesystems, exfiltrate data, or take over the machine entirely. Attackers actively scan the internet for port 2375 using masscan — Akamai's Hunt Team documented a live malware strain doing exactly this in August 2025. The Docker daemon should never be exposed on TCP without mutual TLS, and ideally should use the Unix socket only.
Related
See exactly what's reachable from the internet.
Free, local-first, Apache-2.0. The outside-in scan runs on your box. Nothing leaves it.