0

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

1 Answer 1

1

You need to re-read the Docker packet filtering and firewalls, section "Restrict external connections to containers".

By default, all external source IPs are allowed to connect to ports that have been published to the Docker host's addresses.

To allow only a specific IP or network to access the containers, insert a negated rule at the top of the DOCKER-USER filter chain

1
  • I dug a bit more on that subject and you were right ! I found this answer and added the following rule : sudo iptables -I DOCKER-USER -i enp1s0 -m conntrack --ctdir ORIGINAL -j DROP And it worked ! I just thought that I wasn't expected to use iptables directly since I'm using firewalld. Thank you ! Commented May 3 at 14:49

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.