If you are using a firewall like ufw or firewalld and docker you may encounter the problem that docker bypasses the firewall rules.
Goal
- The firewall rules should count for whole host system - so including Docker containers with port mappings
- A Docker container should be accessible from the internet if and only if the host port used in Docker container port mapping is allowed in the firewall
- The approach should not break container networking
Existing Approaches
I found following approaches that try to fix the problem. However, each approach introduced another problem:
- Just do not use docker. Podman for example obeys firewall rules by default. Problem: Some can not or may not want to switch to a different container runtime, but I generally recommend checking if this is an option for you. This article includes a comparison.
- Use external firewall like security groups in Openstack instead of ufw or firewalld. Problem: Not available in my case.
- Just do not map ports in docker. Problem: may introduce security risk because of no single source of truth for exposed host ports.
- Disabling iptables for docker. Problem: Containers can not access internet.
- Configuring the firewall to ignore port mappings. Problem: The port inside the container have to be allowed in the host firewall. If multiple containers use same port and only one should be allowed we have to additionally specify container IP or service name. In summary: Counterintuitive, complex and error-prone.
Approach
Idea: Disable iptables for docker and configure firewalld to allow container networking. It is based on this Medium article by Erfan Sahafnejad and several posts. I tested it with Ubuntu 22.04.2 LTS but the concept should also work on other Linux.
Security Implications
It is important to notice, that this approach can have security implications depending on the setup, e.g., as described Keval Kapdee's post. In his setup, he operated a mail server in a docker container with a similar configuration as discussed in this article. Due to the configured masquarading of packets, from the mailserver perspective, all packets originate from the IP address 172.22.1.1
, which is listed as a trusted address in Postfix by default. Therefore, Postfix relayed all requests from every internet IP address as these were seen as originating from a trusted address. In summary, this approach is not suited for use cases, which use original internet IP addresses, e.g., for access control.
Overall, especially in production setups, I recommend using other approaches such as Podman instead of the approach discussed in this article.
Preparation
If you added any configuration to iptables regarding docker before, remove it first.
If ufw is installed and active, disable it:
ufw disable
Install and activate firewalld:
apt update && apt install firewalld -y
systemctl enable --now firewalld
# Confirm that the service is running
firewall-cmd --state
Compared to ufw, firewalld is more powerful - it provides features that we need for upcoming firewall configurations. However, it also just a convenient frontend for iptables. Learn more about firewalld here: https://docs.rockylinux.org/guides/security/firewalld-beginners/
Disable iptables for Docker
Disable iptables for docker in /etc/docker/daemon.json
so it should look like follows:
{
"iptables": false
}
If /etc/docker/daemon.json
does not exist, create the file first.
Restart docker:
systemctl restart docker
Already at this point, only container ports that are allowed in firewall should be reachable from the internet. However, as a side effect of disabling iptables in docker, we broke container internet access: From the inside of containers we can not access the internet anymore.
docker run --rm busybox ping -c4 8.8.8.8
# PING 8.8.8.8 (8.8.8.8): 56 data bytes
#
# --- 8.8.8.8 ping statistics ---
# 4 packets transmitted, 0 packets received, 100% packet loss
Configure firewalld
At next, we configure firewalld to enable docker container networking.
Add Masquerading to the zone which leads out to the Internet, typically public
:
# Masquerading allows for docker ingress and egress (this is the juicy bit)
firewall-cmd --zone=public --add-masquerade --permanent
# Reload firewall to apply permanent rules
firewall-cmd --reload
Sources: https://serverfault.com/a/987687 and https://serverfault.com/a/1046550
Additionally, in order to enable docker containers accessing host ports, add docker interface to the trusted
zone:
# Show interfaces to find out docker interface name
ip link show
# Assumes docker interface is docker0
firewall-cmd --permanent --zone=trusted --add-interface=docker0
firewall-cmd --reload
systemctl restart docker
Sources: https://unix.stackexchange.com/a/225845 and https://unix.stackexchange.com/a/333356
So far, docker containers that are not attached to a docker network can access the internet. But containers that are attached can still not. This is often the case when using docker compose.
If we try to ping Google DNS server this is the result:
docker run --rm busybox ping -c4 8.8.8.8
# PING 8.8.8.8 (8.8.8.8): 56 data bytes
# 64 bytes from 8.8.8.8: seq=0 ttl=58 time=3.699 ms
# 64 bytes from 8.8.8.8: seq=1 ttl=58 time=3.588 ms
# 64 bytes from 8.8.8.8: seq=2 ttl=58 time=3.587 ms
# 64 bytes from 8.8.8.8: seq=3 ttl=58 time=3.518 ms
#
# --- 8.8.8.8 ping statistics ---
# 4 packets transmitted, 4 packets received, 0% packet loss
# round-trip min/avg/max = 3.518/3.598/3.699 ms
# Create docker network for testing purpose (can be deleted later)
docker network create --driver bridge mynet
docker run --rm --net mynet busybox ping -c4 8.8.8.8
# PING 8.8.8.8 (8.8.8.8): 56 data bytes
#
# --- 8.8.8.8 ping statistics ---
# 4 packets transmitted, 0 packets received, 100% packet loss
To fix this, add your network interface to public
zone:
# Show public ip
curl -4 ip.gwdg.de
# Show interfaces to find out network interface name with your public IP
ip addr
# Assumes network interface with your public IP is eth0
# (ens18 is also a name I came accross)
firewall-cmd --permanent --zone=public --add-interface=eth0
firewall-cmd --reload
Networking should work properly now and therefore containers should be able to access the internet.
If we now try to ping Google DNS server again, it works as expected:
docker run --rm busybox ping -c4 8.8.8.8
# PING 8.8.8.8 (8.8.8.8): 56 data bytes
# 64 bytes from 8.8.8.8: seq=0 ttl=58 time=3.641 ms
# 64 bytes from 8.8.8.8: seq=1 ttl=58 time=3.565 ms
# 64 bytes from 8.8.8.8: seq=2 ttl=58 time=3.605 ms
# 64 bytes from 8.8.8.8: seq=3 ttl=58 time=3.546 ms
#
# --- 8.8.8.8 ping statistics ---
# 4 packets transmitted, 4 packets received, 0% packet loss
# round-trip min/avg/max = 3.546/3.589/3.641 ms
docker run --rm --net mynet busybox ping -c4 8.8.8.8
# PING 8.8.8.8 (8.8.8.8): 56 data bytes
# 64 bytes from 8.8.8.8: seq=0 ttl=58 time=3.671 ms
# 64 bytes from 8.8.8.8: seq=1 ttl=58 time=3.644 ms
# 64 bytes from 8.8.8.8: seq=2 ttl=58 time=3.561 ms
# 64 bytes from 8.8.8.8: seq=3 ttl=58 time=3.508 ms
#
# --- 8.8.8.8 ping statistics ---
# 4 packets transmitted, 4 packets received, 0% packet loss
# round-trip min/avg/max = 3.508/3.596/3.671 ms
Open Firewall Ports
In the end, open the desired ports for your service to allow incoming traffic, e.g. on port 8080:
firewall-cmd --permanent --zone=public --add-port=8080/tcp
# Reload firewall to apply permanent rules
firewall-cmd --reload
Extra: Testing
It is important to run tests to ensure the whole setup is working properly. Although the actual tests depend on your setup, here are some statements that may be important to verify:
- Container running on allowed port can be accessed from internet
- Container running on not allowed port can not be accessed from internet
- Container can access internet
- Container with new docker network can access internet
- Container can access service running on host system port
- Container can access other container inside same docker network
To start a webserver on 8080 you can run:
docker run --restart unless-stopped -p 8080:80 -d nginx
To curl a service running on host system port from inside of a container you can run:
# Show docker interface ip address
ip addr
# ...
# 3: docker0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
# link/ether 02:42:37:6b:6e:2a brd ff:ff:ff:ff:ff:ff
# inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
# valid_lft forever preferred_lft forever
# ...
# Assumes docker interface ip is 172.17.0.1
# and service runs on port 80
docker run --rm curlimages/curl -v http://172.17.0.1:80/
Further Reading
- Good firewalld guide: https://docs.rockylinux.org/guides/security/firewalld-beginners/
- Great article for understanding container networking (and also the steps of this article): https://iximiuz.com/en/posts/container-networking-is-simple/
Sources
- https://erfansahaf.medium.com/why-docker-and-firewall-dont-get-along-with-each-other-ddca7a002e10
- https://github.com/docker/for-linux/issues/955#issuecomment-621141128
- https://docs.docker.com/network/iptables/#prevent-docker-from-manipulating-iptables
- https://serverfault.com/a/987687
- https://serverfault.com/a/1046550
- https://unix.stackexchange.com/a/225845
Top comments (9)
Thanks a lot! This is the solution I have been searching for months. The only way to circumvent the problem so far was to define a security rule on the firewall at the cloud provider level.
Your solution is what is fixing my problem completely. Thank you.
Thank your for the great post!
Does setting masquerade on the public interface bring any security implications?
Traffic reaching the 'eth0' interface with a destination other the server ip forwarded now instead of filtered. When this traffic originates from the docker interface/ips forwardind is great but how is it the other way around?
Does traffic from the internet with destination e.g. one of your docker containers is routed as well bypassing the firewall filter rules?
So far, I am not aware of any specific vulnerabilities introduced by masquerading. However, this may depend on the actual usage / setup. I strongly recommend to extensively check whether the setup works as intended.
Regarding traffic from the internet with destination container: Packets from the internet with destination set to a container IP address, do not reach your server in first place. Container IP addresses are only accessible within the local network on the server. This article explains this among other interesting details.
If the server IP address is set as destination in a packet, the firewall rules do apply.
See my reddit post for a reason why this does have security implications :_)
You are right. Thank you for sharing! I will update the article to include the implications described in your post.
This works, but not for ipv6. I have both ipv4 and ipv6 enabled throughout and most applications prefer ipv6. I cannot reach a container by ipv6 from elsewhere in my network.
Adding rich rule 'rule family="ipv6" masquerade' to zone public fixed outbound ipv6 traffic. I haven't yet found anything for inbound traffic though.
@soerenmetje it seems to work very well but in this way I can't use fail2ban...
containers isn't able to log the external ip addresses...
is there a workaround for fail2ban?
I haven't dealt with that yet. If you find a workaround, feel free to post it here.