Why Docker Can Bypass UFW (And How to Fix It)

Docker can expose ports even when UFW is configured to block them. Here's why it happens and how to fix it safely.

Jacob avatar
  • Jacob
  • 4 min read
Image generated by Nano Banana 2

Introduction

Recently, I configured a VPS to host my web app - The Scoreboard App. It’s backed by a PostgreSQL database and Redis for caching, both running inside Docker. These services only need to be accessed by my back-end service, which runs on the same machine, so they don’t need any public access. I decided on using UFW (Uncomplicated Firewall) to lock down the server. By default, UFW denies all incoming traffic, and you add rules to open specific ports. For example, you can allow incoming HTTPS traffic like so:

sudo ufw allow 443 # HTTPS traffic uses port 443

Perfect, the server should now block traffic to Postgres and Redis, right?

That’s what I thought. Unfortunately, this was not the case. A quick port scan using Nmap (a network mapper/port scanner) revealed that ports 5432 and 6379 were open to the public!

PORT     STATE SERVICE
22/tcp   open  ssh
443/tcp  open  https
5432/tcp open  postgresql
6379/tcp open  redis

So my first thought was I must’ve configured UFW incorrectly. Running sudo ufw status verbose revealed:

# ufw status verbose
Status: active
Logging: on (low)
Default: deny (incoming), allow (outgoing), deny (routed)
New profiles: skip

To                         Action      From
--                         ------      ----
22/tcp                     ALLOW IN    Anywhere
443/tcp                    ALLOW IN    Anywhere

Nope, that’s all set up correctly - the default incoming rule is deny, and only ports 22 and 443 have been explicitly allowed.

So what’s going on?

By looking at the Docker docs we can see that Docker configures firewall rules with iptables for containers with exposed ports:

| Docker creates iptables rules in the host’s network namespace for bridge networks

UFW under the hood configures iptables. However, Docker inserts its own iptables rules that are evaluated before UFW’s filtering rules. So despite UFW reporting that the ports were closed, Docker had effectively exposed them. This is intended behaviour - UFW isn’t broken, Docker’s rules just take precedence. The reason for this is that UFW primarily manages the filter table, while Docker modifies both the nat and filter tables - adding rules that can accept and forward traffic before UFW’s rules are applied.

Let’s have a closer look at the docker-compose.yml configuration that caused this:

redis:
  image: redis:7-alpine
  restart: unless-stopped
  ports:
    # This line bypasses UFW
    - 6379:6379

In this configuration, we’ve said map port 6379 on the host to port 6379 in the container. Because we didn’t specify a host interface, Docker binds the port to all available interfaces (0.0.0.0), exposing it to the entire internet.

The Solution

I fixed this in two ways. The first was to update my docker-compose.yml to only listen on localhost (127.0.0.1):

redis:
  image: redis:7-alpine
  restart: unless-stopped
  ports:
    # Now this port is only opened to connections on the same host
    - 127.0.0.1:6379:6379

This is the recommended solution, and usually does the trick. However, to avoid similar issues in the future, I decided to also configure iptables to prevent Docker from exposing ports to the public. This was done by adding the following to /etc/ufw/after.rules:

# Ensure this is under `*filter`

# Define the DOCKER-USER chain
:DOCKER-USER - [0:0]

# Block external traffic. NOTE: `eth0` may be different on your machine
-A DOCKER-USER -i eth0 -j DROP

# Allow everything else (internal/local)
-A DOCKER-USER -j RETURN

Save the file and reload UFW to apply the changes with:

sudo ufw reload

This is a bit of a nuclear option - it blocks all external traffic routed to Docker containers. If you have public-facing services running inside Docker (like an Nginx reverse proxy or your actual web app), this will break them. This fix is only suitable if Docker is strictly for internal backend services.

Now Docker won’t expose anything to the public, so UFW has full control over what traffic is allowed in. A quick Nmap scan confirms the fix:

PORT     STATE    SERVICE
22/tcp   open     ssh
443/tcp  open     https
5432/tcp filtered postgresql
6379/tcp filtered redis

As you can see, Redis and Postgres are now blocked to the public.

Ensuring the Ports Stay Closed

To stay on top of this, I use Uptime Kuma to monitor my services. I’ve configured a monitor for both Postgres and Redis - these attempt to connect to the TCP port of each and notify me if the port is open (using the “Upside Down Mode”). If I ever accidentally expose the ports in the future, I’ll get a notification immediately, allowing me to fix it before it becomes a problem.

Conclusion

If you’re ever running Docker containers, please remember that Docker may expose ports in a way that bypasses UFW rules unless configured carefully. To avoid surprises, always be explicit about your port bindings in your docker-compose.yml.

But my biggest takeaway? Never blindly trust your configuration. Always verify your firewall rules with a quick Nmap scan.

Stay secure, and happy coding!

Resources

Jacob

Written by : Jacob

I am a software engineer that loves exploring tech and figuring out how things work. I am passionate about learning and hope to share some of that passion with you!