Content-Type: text/plain

Docker has networking-related behaviour that in my opinion is at best surprising and at worst dangerous.

Here is a minimal but innocent-looking docker run line that exposes the external port 8080 to the internal port 80 of the container defined by the nginx image.

docker run -p 8080:80 nginx

The problem is the implicit meaning of 8080:80 which, when put more explicitly, can be written as 0.0.0.0:8080:801.

The 0.0.0.0 prefix is the bind address, i.e., all addresses on all interfaces. In other words, you are completely exposed.

But I'm firewalled!

You might be thinking this is an acceptable and common default, because even if the container is only intended to be accessed locally, the system in question is firewalled with iptables or an iptables-based firewall like ufw.

It would be reasonable to think so, but this is where Docker knows better: it has configured iptables to give itself priority over all your other rules, and it will allow external incoming connections anyway.

In fairness the documentation does state this clearly, but most people don't read documentation unless they have to.

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.

docker-compose is affected, too

docker-compose is used to declaratively run containers with a specific parameters, like exposed ports and volume paths. Rather than passing these values as arguments to the docker command they can be encoded as YAML in docker-compose.yml and then started using the simple command docker-compose up.

This minimal docker-compose.yml is functionally equivalent to the docker run line above.

version: '3'
services:
  app:
    image: nginx
    ports:
    - 8080:80

Because docker-compose is just calling the underlying Docker engine the result would be exactly the same here too.

Solution

The simplest solution is to make it a habit to always specify the bind address (e.g., 127.0.0.1) and never leave a port definition bare. This is a good habit to get into even when the intended address is 0.0.0.0 as it forces you to think about what you actually want.

A more robust general-purpose solution that entirely avoids this class of problem is to not use a local firewall at all. Use a hardware firewall or the one available from most cloud providers.

The lesson here is check your assumptions and always try to hack yourself first. Running regular port scans on your own infrastructure can be an invaluable tool to help you find unintentionally exposed services before someone else does.


  1. This isn't 100% equivalent. A lack of bind address will default to IPv4 and IPv6 if available, but using 0.0.0.0 limits it to IPv4.