Using Docker and UFW [The easy way]
![Cover Image Using Docker and UFW [The easy way]](https://blog.francois-egner.de/static/img/dockerufw.jpg)
The goal/scenario⌗
To demonstrate the use of the knowledge imparted, I create a hypothetical scenario below that makes the following configurations easier to understand. The scenario is as follows: I am running a simple Apache2 web server on a virtual machine, on which I have a Docker container running that same Apache2 web server.
- The container is running in a 172.17.0.0/16 network and got 172.17.0.2/32 as its IP
- The apache2 web server inside the docker container is listening on port 80
- The host system is part of the 172.17.0.0/16 (IP: 172.17.0.1/32) network and also 192.168.1.0/24 (private network, IP: 192.168.1.156/32)
- There is another client inside the 192.168.1.0/24 (IP: 192.168.1.153/32) from which we will try to access the apache2 web server later on. This is our nginx proxy, that tries to forward a web request to the apache2 web server
Let’s define the end goal: The final configuration should forward all incoming HTTP requests from the second client arriving on port 8080 to the apache2 web server. The apache2 web server should be able to process the request and respond with the web page. In addition, UFW should ensure that only requests from the second client are forwarded to the apache2 web server.
The container was started as follows:
sudo docker run -d --name apache2-container -e TZ=UTC ubuntu/apache2:2.4-22.04_beta
Define our NAT iptable rules⌗
UFW uses iptables under the hood to allow and deny connections. Since iptables entries are not saved by default and must be recreated after each reboot, UFW also loads its at startup. UFW provides a configuration file that can also be used to specify your own iptables entries, which are then automatically loaded when UFW is started. We use this configuration file to create the necessary NAT entries to forward the desired traffic to the Docker container.
Let’s open the configuration file I was talking about earlier:
sudo nano /etc/ufw/before.rules
and paste the necessary NAT segement at the top of the file, right before the filter segment.
*nat
:PREROUTING ACCEPT [0:0]
-A PREROUTING -p tcp -d 192.168.1.156 --dport 80 -j DNAT --to 172.17.0.2:80
COMMIT
You may wonder what this rule segment does…. Let me explain:
*nat
This small piece signals UFW, when processing the configuration file, that the following are NAT rules and should also be processed in the same way.
:PREROUTING ACCEPT [0:0]
This line :PREROUTING ACCEPT [0:0] represents a checkpoint for incoming network packets. “PREROUTING” acts as this checkpoint where packets are examined before they proceed further into the network. “ACCEPT” means that the packets are allowed to pass through without any blocking or modification, and “[0:0]” indicates that no packets have been affected by this rule so far, implying that all incoming packets are currently permitted to enter the network without interference.
-A PREROUTING -p tcp -d 192.168.1.156 --dport 8080 -j DNAT --to 172.17.0.2:80
This is the actual forwarding rule that specifies what happens to packets that meet certain criteria. Let’s check the arguments:
-A PREROUTING
: This means that this rule is a rule for incoming packets-p tcp
: This means that this rule only affects TCP packets. This is important because HTTP requests are transmitted via TCP packets (see HTTP protocol)-d 192.168.1.156
: This means that the rules should only be applied to packets whose destination is 192.168.1.156. This makes sense because docker is running on the host with this IP and the second client just knows that somewhere on this IP, there lives a webserver. So the second client asks this IP, not the docker container one.--dport 8080
: This argument specifies the port at which the packets should “come in” to be processed by this rule. Thats what we set in our goal: HTTP requests should be send to 192.168.1.156 on port 8080-j DNAT
: Now this is the actual action that is being performed on the incoming packet that fits the criteria above. DNAT stand for Destination Network Address Translation which simply changes the destination IP in the matched IP packet.--to 172.17.0.2:80
: And this part is the part that we want to change the IP packet to. This will tell the NAT service to change the destination IP from 192.168.1.156 to 172.17.0.2 and the port from 8080 to 80. So the new target is the docker container hosting the apache2 web server at port 80
Because the docker host is also part of the docker network (172.17.0.0/16) it will forward the packet to the docker container. Exactly what we wanted! The request packets of your HTTP request are forwarded to the container. Nice. You have to load in this new rule to take affect. I did not find a working solution other than a reboot of the host itself, so I can just recommend you doing that to apply the rule.
Note
You can think of this rule to be at the very start of a big process chain that further processes the packet we just changed the destination of. We did not explicitly allow or deny this packet yet, so it will traverse all the upcoming rules. To actually allow or deny a packet based on finer grained criteria we will use UFW to create those allow/deny rules
Allow fowarding to the container⌗
As mentioned before, forwarding must still be explicitly allowed. This is useful if you want to allow access to a container from a specific IP and/or network. This could be done in the iptables rule itself, but it is much more convenient via UFW and much easier for multiple networks/IPs.
This again is just a small piece and pretty easy to understand
sudo ufw route allow proto tcp from 192.168.1.153 to 172.17.0.2 port 80
It is pretty straight forward:
route allow
: This specifies the rule being applied to routed packets only (which we did by our NAT rule)proto tcp
: This is basically the same as-p tcp
from 192.168.1.153
: Thats the interesting part. This part makes sure that only packets from our second client (IP: 192.168.1.153) are allow to be routedto 172.17.0.2 port 80
: This is yet again basically the same as--to 172.17.0.2:80
which defines the destination of the route that will be allowed. In this case its again our docker container serving the apache2 web server.
This rule is very similar to the NAT rule we created earlier. Yet it does provide one important feature: We reduced the source of the request to our nginx proxy, exactly as we wanted to ensure HTTP traffic between the only two machines that have to be involved in serving the website running inside the container.
To apply the new allow rule we just created we need to reload UFW:
sudo ufw reload
You may ask yourself: If the container (IP: 172.17.0.2) answers the request, how does the requesting client know that these response packets belong to the original request? Isnt the source IP of the response packets the one of the container (IP: 172.17.0.2)?
These are good questions and there is one answer to this: NAT (again).
Outgoing traffic of a bridge network will be nat’ed by docker automatically. In this case it is not DNAT but SNAT (Source Network Address Translation). This is necessarry because the second client expects the response packets coming from the client it sent the request to, which in our case is the docker host (IP: 192.168.156). So docker will automatically set the source IP in every related IP packet of the response to the one of the docker host (IP: 192.168.1.156).
Notes⌗
Because we didnt tell docker to stop creating iptable rules it is still functioning as expected for all other scenarios. So if you still want to expose ports via -p xxxx:yyyy
you can still do so if needed. But I highly recommend you to do not.
The reason we did not deactivate iptables rules generation by docker is the auto nat’ing for outgoing traffic. We could have done so but in that case we would also have to define a custom iptables rule to change the source IP of the response packets manually which is a pain to do for many networks at once. Just for the sake of completness I will add some notes regarding required actions to deactivate and manually SNAT’ing in the near future.
Happy hosting :)