When using Docker, a common thing to do is map a port from the container to the host machine using the
docker run -p in the CLI or the ports argument in a docker-compose file. However, if you are not careful you can end up exposing services to the world when they should only be accessible inside the host. Let’s see how this can happen and how to avoid it.
Map a port to the host
A port can be mapped from the container to the host using the
-p host:container argument in
docker run. An example would be to create an Nginx container and make it accessible from the host on port 1234:
$ docker run -p 1234:80 nginx
localhost:1234 from a browser gives us the Nginx welcome page:
That is great! We now have a working Nginx container accessible on port 1234 in the host machine.
However, let’s take a look at the open ports in our host:
$ netstat -tuln | grep LISTEN ... tcp6 0 0 :::1234 :::* LISTEN ...
Notice that it is listening for connections from any address on port 1234 (this is what
:::1234 means). Now anyone in the same network as the host can connect to the container and see the Nginx welcome page.
For example, if you have a MongoDB container running on your laptop while using the Wi-Fi of the hotel you are staying at, a hacker in the same network can connect to your container and steal all the information inside it. This is especially worrisome if the host is a production server with a public IP. It could end up exposing functionalities that should only be accessible inside the host, like internal APIs with sensitive information or destructive actions.
Why is Docker doing this?
By default, Docker edits the
iptables entries in the host machine, bypassing firewall configurations. It means that even if the sysadmin correctly configured the machine to deny all incoming traffic, Docker will ignore it and create a hole in the firewall. This behavior sure provides some ease of use, but create a lot of vulnerabilities, especially with a user that just started using Docker. Check this thread for more insight on the issue.
How to avoid this situation
The first thing to notice when trying to fix this is that if all applications that communicate with the container are also containerized, it’s not even necessary to expose a port on the host. Just let Docker manage the connections and reference the container inside the source code using the service defined in
docker-compose.yml. While looking for Docker examples on Google I noticed a lot of people still don’t use this feature even when both applications are containerized.
However, if you need an application in the host to connect to the container, the way to not expose your container to the world is to explicitly declare that the port should only be accessible from the localhost. This can be achieved prepending
127.0.0.1 to the host port number in
$ docker run -p "127.0.0.1:27017:27017" mongo
... mongodb: image: mongo ports: - "127.0.0.1:27017:27017" ...
Now our application is only accessible inside the host machine:
$ netstat -tuln | grep LISTEN ... tcp 0 0 127.0.0.1:1234 0.0.0.0:* LISTEN ...
However, some people reported that even explicitly referencing
127.0.0.1, the container could still be accessed from outside. For an extra layer of security, you can also modify the default Docker behavior and stop it from tampering with the
iptables altogether. For more info on how to do this, take a look at the links I provided at the end of the post.
We saw how Docker can expose your containers to the world if not used correctly. You should always check if you are exposing containers that should not be exposed to the world, specifying
127.0.0.1 as the address to be listening for connections. Also, always follow the best practices to secure your containers on production, setting appropriate authentication for them.
Some interesting reading to follow on if you are interested: