• Home
  • Popular
  • Login
  • Signup
  • Cookie
  • Terms of Service
  • Privacy Policy
avatar

Posted by User Bot


28 Apr, 2025

Updated at 20 May, 2025

Forward Docker port in firewalld only for specific interface

I'm trying to secure a VPS running Docker containers so that their exposed ports are only accessible through a VPN interface (in my case it's Tailscale). In order to do that, I read about firewalld and came up with the following rules:

sudo firewall-cmd --reload
sudo firewall-cmd --zone=trusted --add-interface=tailscale0
sudo firewall-cmd --zone=trusted --add-port=8080/tcp

Before adding Docker into the mix, I tested my rules with a simple Python server:

python -m http.server 8080

Note: From now on, I'll refer to my Tailscale IP as 100.100.100.100 and my public IP as 1.2.3.4.

As expected, the connection succeeds from Tailscale:

➜ telnet 100.100.100.100 8080

Trying 100.100.100.100...
Connected to 100.100.100.100.
Escape character is '^]'.

And fails from the public IP:

➜ telnet 1.2.3.4 8080

Trying 1.2.3.4...
telnet: connect to address 1.2.3.4: Connection refused
telnet: Unable to connect to remote host

Then I tried with a Docker container:

docker run -p 8080:80 --name test-http-server --restart always nginx:alpine

At first the server was accessible from anywhere, so I read a lot on the subject (see sources below for a subset of my readings) and managed to build the latest version of firewalld to date to get support for the (relatively new) StrictForwardPorts=yes option, which did indeed prevent the port from being exposed automatically.

But that also made my rules unable to forward the port unless I add a rule:

CONTAINER_IP=$(docker inspect -f '{{.NetworkSettings.Networks.bridge.IPAddress}}' test-http-server)
sudo firewall-cmd --zone=trusted --add-forward-port=port=8080:proto=tcp:toport=80:toaddr=${CONTAINER_IP}

Unfortunately now my server is accessible from all interfaces, even though the rules are in the trusted zone, where only the Tailscale interface is:

➜ telnet 1.2.3.4 8080

Trying 1.2.3.4...
Connected to 1.2.3.4.
Escape character is '^]'.
➜ telnet 100.100.100.100 8080

Trying 100.100.100.100...
Connected to 100.100.100.100.
Escape character is '^]'.

After much reading, failed attempts, and discussing with ChatGPT, I'm running out of ideas. I seem to have narrowed it down to the port forward rule that somehow seems to get around the firewalld zones, but I couldn't find another way to achieve my goal.

What am I missing/doing wrong ?

Notes

Note #1: I know that I could (and should) bind my container to my Tailscale interface, and that it would achieve my goal:

docker run -d -p 100.100.100.100:8080:80 --name test-http-server --restart always nginx:alpine

That's indeed what I intend to do when everything else is ready, but that's not enough for me, as I want my firewall rules to be foolproof against whatever mistake I could make with my containers, and against weak binding values my services (which might spawn containers on their own) might have hard-coded. The firewall should give me the "last word" over my packets.


Note #2: I guess I could achieve my goal using iptables, but I spent so much time already on this and, honestly, iptables is just too complicated for my needs. firewalld, on the other hand, seems to be a standard for easy-to-use firewalls.


Note #3: I tried changing the FirewallBackend setting from nftables (default) to iptables since Docker's firewall limitations mention that they are only compatible with iptables-nft and iptables-legacy and firewalld's homepage mentions that too at the bottom:

docker (iptables backend only)

I'm not sure what this means, but since there is iptables in both of these names and not in nftables, I figured this was worth a shot. To my greatest surprise, using this backend just brought me back to exactly the same spot I was with StrictForwardPorts=no: the server is accessible from both interfaces without the need to add the port forward rule.

Sources