Requirements

Docker Server Version: 20.10.12

On a server with Docker, I need to restrict external access using iptables as a firewall.

Iptables && Docker

iptables is divided into three levels: tables, chains and rules. We generally only use the filter table, which contains.

  • INPUT, input chain. Packets sent to this machine pass through this chain.
  • OUTPUT, the output chain. Packets sent from this machine pass through this chain.
  • FORWARD, the forwarding chain. Packets forwarded by this machine pass through this chain.

Since Docker connects the default virtual bridge (docker0) to the container’s default gateway (ens33) via NAT (Network Address Translation) by default, setting INPUT is useless and the FORWARD chain should be set. And since the FORWARD chain defaults to DROP , all forwarding is blocked by DOCKER-USER.

1
2
3
4
5
6
7
8
Chain FORWARD (policy DROP)
target     prot opt source               destination         
DOCKER-USER  all  --  0.0.0.0/0            0.0.0.0/0           
DOCKER-ISOLATION-STAGE-1  all  --  0.0.0.0/0            0.0.0.0/0           
ACCEPT     all  --  0.0.0.0/0            0.0.0.0/0            ctstate RELATED,ESTABLISHED
DOCKER     all  --  0.0.0.0/0            0.0.0.0/0           
ACCEPT     all  --  0.0.0.0/0            0.0.0.0/0           
ACCEPT     all  --  0.0.0.0/0            0.0.0.0/0   

Therefore, we have to use the DOCKER-USER chain to set the container access rules, which is implemented as follows.

Note: The firewall manager should be closed.

1
2
3
4
5
# Close the firewall manager
for target in firewalld python-firewall firewalld-filesystem iptables; do
  systemctl stop $target &>/dev/null || true
  systemctl disable $target &>/dev/null || true
done

Option 1

You need to install iptables-services: yum install -y iptables-services.

System’s rules

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 先允许所有,这里很重要,不加会被拒绝
iptables -P INPUT ACCEPT 
   
iptables -F  # 清空所有默认规则
iptables -X  # 清空所有自定义规则
iptables -Z  # 所有计数器归0
   
# 对外开放的端口
iptables -A INPUT -p tcp -m multiport --dport 22  -j ACCEPT
   
# 允许ping
iptables -A INPUT -p icmp -j ACCEPT
   
# 允许主动连接其他主机
iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
   
# 其他入站一律丢弃
iptables -P INPUT DROP
   
# 所有出站一律绿灯
iptables -P OUTPUT ACCEPT
   
# 保存规则
service iptables save

docker’s rules

1
2
# Restart docker, generate docker's rules
systemctl restart docker

Rules for the DOCKER-USER chain

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# 其他入站一律丢弃
iptables -I DOCKER-USER -i ens33 ! -s 127.0.0.1 -j DROP
   
# 允许主动连接其他主机
iptables -I DOCKER-USER -m state --state established,related -j ACCEPT
   
iptables -A DOCKER-USER -m conntrack --ctstate established,related -j ACCEPT
   
# 对外开放的端口
iptables -I DOCKER-USER -p tcp -m conntrack --ctorigdstport 80  -j ACCEPT
iptables -I DOCKER-USER -p tcp -m conntrack --ctorigdstport 443  -j ACCEPT
   
# 保存规则
service iptables save

ens33 is my node’s external network interface.

The iptables rule at this point

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# iptables -L -n
Chain INPUT (policy DROP)
target     prot opt source               destination         
ACCEPT     tcp  --  0.0.0.0/0            0.0.0.0/0            multiport dports 22
ACCEPT     icmp --  0.0.0.0/0            0.0.0.0/0            icmptype 0
ACCEPT     all  --  0.0.0.0/0            0.0.0.0/0            state RELATED,ESTABLISHED
   
Chain FORWARD (policy DROP)
target     prot opt source               destination         
DOCKER-USER  all  --  0.0.0.0/0            0.0.0.0/0           
DOCKER-ISOLATION-STAGE-1  all  --  0.0.0.0/0            0.0.0.0/0           
ACCEPT     all  --  0.0.0.0/0            0.0.0.0/0            ctstate RELATED,ESTABLISHED
DOCKER     all  --  0.0.0.0/0            0.0.0.0/0           
ACCEPT     all  --  0.0.0.0/0            0.0.0.0/0           
ACCEPT     all  --  0.0.0.0/0            0.0.0.0/0           
   
Chain OUTPUT (policy ACCEPT)
target     prot opt source               destination         
   
Chain DOCKER (1 references)
target     prot opt source               destination         
ACCEPT     tcp  --  0.0.0.0/0            172.17.0.2           tcp dpt:8080
ACCEPT     tcp  --  0.0.0.0/0            172.17.0.2           tcp dpt:80
ACCEPT     tcp  --  0.0.0.0/0            172.17.0.3           tcp dpt:80
   
Chain DOCKER-ISOLATION-STAGE-1 (1 references)
target     prot opt source               destination         
DOCKER-ISOLATION-STAGE-2  all  --  0.0.0.0/0            0.0.0.0/0           
RETURN     all  --  0.0.0.0/0            0.0.0.0/0           
   
Chain DOCKER-ISOLATION-STAGE-2 (1 references)
target     prot opt source               destination         
DROP       all  --  0.0.0.0/0            0.0.0.0/0           
RETURN     all  --  0.0.0.0/0            0.0.0.0/0           
   
Chain DOCKER-USER (1 references)
target     prot opt source               destination         
ACCEPT     tcp  --  0.0.0.0/0            0.0.0.0/0            ctorigdstport 80
ACCEPT     tcp  --  0.0.0.0/0            0.0.0.0/0            ctorigdstport 443
ACCEPT     all  --  0.0.0.0/0            0.0.0.0/0            state RELATED,ESTABLISHED
DROP       all  -- !127.0.0.1            0.0.0.0/0                     
RETURN     all  --  0.0.0.0/0            0.0.0.0/0       

This implementation has a drawback, that is, every time the docker container changes, you need to save the current iptables configuration, otherwise when restarting the iptables service, it will load the old rules, which will lead to confusing iptables rules.

Option 2

To solve the above problem, we can

  • Create a new chain called FILTERS into which network traffic from INPUT and DOCKER-USER is placed, and store this configuration in a file.
  • Create an iptables systemd service to reload the iptables rules.

Here we don’t need the system installation of iptables-services, use the command to uninstall it. yum remove -y iptables-services

Create the iptables.conf rule file

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
 cat <<EOF > /etc/iptables.conf
 *filter

 # Reset counters
 :INPUT ACCEPT [0:0]
 :FORWARD DROP [0:0]
 :OUTPUT ACCEPT [0:0]
 :DOCKER-USER - [0:0]
 :FILTERS - [0:0]

 # Flush
 -F INPUT
 -F DOCKER-USER
 -F FILTERS

 # Select
 -A INPUT -i lo -j ACCEPT
 -A INPUT -i ens33 -j FILTERS
 -A DOCKER-USER -i ens33 -j FILTERS
 -A DOCKER-USER -j RETURN

 # Filters
 ## icmp
 -A FILTERS -p icmp --icmp-type any -j ACCEPT

 ## Activate established connexions
 -A FILTERS -m state --state ESTABLISHED,RELATED -j ACCEPT
 -A FILTERS -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT

 ## Allow ssh
 -A FILTERS -p tcp -m multiport --dport 22  -j ACCEPT

 ## Allow all on http/https
 -A FILTERS -p tcp -m tcp -m conntrack --ctorigdstport 80 -j ACCEPT
 -A FILTERS -p tcp -m tcp -m conntrack --ctorigdstport 443 -j ACCEPT
    
 # Block all external
 -A FILTERS -i ens33 -j DROP
 -A FILTERS -j RETURN

 ## Commit
 COMMIT
 EOF

ens33 is my node’s external network interface

Create system services

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
 cat << EOF >  /usr/lib/systemd/system/iptables.service
 [Unit]
 Description=Restore iptables firewall rules
 Before=network-pre.target

 [Service]
 Type=oneshot
 ExecStart=/sbin/iptables-restore -n /etc/iptables.conf

 [Install]
 WantedBy=multi-user.target
 EOF

 systemctl enable iptables
 systemctl start iptables

iptables-restore -n turns off implicit global refresh and only performs our manual explicit refresh.

The iptables rule at this point

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
Chain INPUT (policy ACCEPT)
target     prot opt source               destination         
ACCEPT     all  --  0.0.0.0/0            0.0.0.0/0           
FILTERS    all  --  0.0.0.0/0            0.0.0.0/0           
   
Chain FORWARD (policy DROP)
target     prot opt source               destination         
DOCKER-USER  all  --  0.0.0.0/0            0.0.0.0/0           
DOCKER-ISOLATION-STAGE-1  all  --  0.0.0.0/0            0.0.0.0/0           
ACCEPT     all  --  0.0.0.0/0            0.0.0.0/0            ctstate RELATED,ESTABLISHED
DOCKER     all  --  0.0.0.0/0            0.0.0.0/0           
ACCEPT     all  --  0.0.0.0/0            0.0.0.0/0           
ACCEPT     all  --  0.0.0.0/0            0.0.0.0/0           
   
Chain OUTPUT (policy ACCEPT)
target     prot opt source               destination         
   
Chain DOCKER (1 references)
target     prot opt source               destination         
   
Chain DOCKER-ISOLATION-STAGE-1 (1 references)
target     prot opt source               destination         
DOCKER-ISOLATION-STAGE-2  all  --  0.0.0.0/0            0.0.0.0/0           
RETURN     all  --  0.0.0.0/0            0.0.0.0/0           
   
Chain DOCKER-ISOLATION-STAGE-2 (1 references)
target     prot opt source               destination         
DROP       all  --  0.0.0.0/0            0.0.0.0/0           
RETURN     all  --  0.0.0.0/0            0.0.0.0/0           
   
Chain DOCKER-USER (1 references)
target     prot opt source               destination         
FILTERS    all  --  0.0.0.0/0            0.0.0.0/0           
RETURN     all  --  0.0.0.0/0            0.0.0.0/0           
   
Chain FILTERS (2 references)
target     prot opt source               destination         
ACCEPT     icmp --  0.0.0.0/0            0.0.0.0/0            icmptype 255
ACCEPT     all  --  0.0.0.0/0            0.0.0.0/0            state RELATED,ESTABLISHED
ACCEPT     all  --  0.0.0.0/0            0.0.0.0/0            ctstate RELATED,ESTABLISHED
ACCEPT     tcp  --  0.0.0.0/0            0.0.0.0/0            multiport dports 22
ACCEPT     tcp  --  0.0.0.0/0            0.0.0.0/0            tcp ctorigdstport 80
ACCEPT     tcp  --  0.0.0.0/0            0.0.0.0/0            tcp ctorigdstport 443
DROP       all  --  0.0.0.0/0            0.0.0.0/0           
RETURN     all  --  0.0.0.0/0            0.0.0.0/0           

If we need to update the rules, just edit the /etc/iptables.conf file and execute systemctl restart iptables to reload the rules.