Thursday, June 13, 2019

Securing Docker Ports with Firewalld (CentOS7, etc)

Docker and Firewalld


Overview

To secure Docker exposed ports from external access, so access is only allowed for named IP addresses, you can use firewalld rules.
I needed to play around a bit, as all the information I found so far is either not working, or just exposes ports to the public, which I wanted to avoid.
Tested on CentOS7 with Docker-CE 18.09.6
  1. Docker maintains IPTABLES chain "DOCKER-USER". 
  2. If you restart firewalld when docker is running, firewalld is removing the DOCKER-USER chain, so no Docker access is possible after this.
  3. Docker adds a default rule to the DOCKER-USER chain which allows all IPs to access (possibly unsecure).

We can achive secured Docker ports maintained by firewalld by letting firewalld create the DOCKER-USER chain, then apply iptables direct rules to secure the docker ports in this chain. When Docker is then started, it adds its allow-all rule to the bottom of our chain, but as we add a reject-all rule before, this rule is not in effect.

Configure firewalld

Example:
We expose Docker Ports 80 (HTTP) and 443 (HTTPS) of an NGINX docker container and want to allow access to this ports only by named IP addresses or subnets.

# 1. Stop Docker
systemctl stop docker

# 2. Recreate DOCKER-USER iptables chain in firewalld. Ignore any warnings
firewall-cmd --permanent --direct --remove-chain ipv4 filter DOCKER-USER
firewall-cmd --permanent --direct --remove-rules ipv4 filter DOCKER-USER
firewall-cmd --permanent --direct --add-chain ipv4 filter DOCKER-USER
   
# 3. Add iptables rules to DOCKER-USER chain

firewall-cmd --permanent --direct --add-rule ipv4 filter DOCKER-USER 1 \
  -m conntrack \
  --ctstate RELATED,ESTABLISHED -j ACCEPT \
  -m comment --comment 'Allow containers to connect to the outside world'

firewall-cmd --permanent --direct --add-rule ipv4 filter DOCKER-USER 1 \
  -j RETURN \
  -s 172.17.0.0/16 \
  -m comment --comment 'allow internal docker communication'

Note: Change the Docker Subnet address to your network settings (Could be 172.18.0.0/16 as well).

# 4. Add rules for IPs 1.2.3.4 and 5.6.7.8 allowed to access the Docker exposed ports 80/443. 
#    Precedence is 1 (so you can add more rules with precedence 0, later. See below)

firewall-cmd --permanent --direct --add-rule ipv4 filter DOCKER-USER 1 \
  -o docker0 \
  -p tcp -m multiport \
  --dports 80,443 -s 1.2.3.4/32 -j ACCEPT \
  -m comment \
  --comment 'Allow IP 1.2.3.4 to access http and https docker ports'

firewall-cmd --permanent --direct --add-rule ipv4 filter DOCKER-USER 1 \
  -o docker0 \
  -p tcp -m multiport \
  --dports 80,443 -s 5.6.7.8/32 -j ACCEPT \
  -m comment \
  --comment 'Allow IP 5.6.7.8 to access http and https docker ports'

# 5. Block all other IPs. This rule has lowest precedence, so you can add rules before this one later.
firewall-cmd --permanent --direct --add-rule ipv4 filter DOCKER-USER 10 \
  -j REJECT -m comment --comment 'reject all other traffic to DOCKER-USER'

# 6. Activate rules
firewall-cmd --reload

# 7. Start Docker
systemctl start docker


This must lead to a /etc/firewalld/direct.xml file like this:

<?xml version="1.0" encoding="utf-8"?>
<direct>
  <chain table="filter" ipv="ipv4" chain="DOCKER-USER"/>
  <rule priority="1" table="filter" ipv="ipv4" chain="DOCKER-USER">-m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT -m comment --comment 'Allow docker containers to connect to the outside world'</rule>
  <rule priority="1" table="filter" ipv="ipv4" chain="DOCKER-USER">-j RETURN -s 172.17.0.0/16 -m comment --comment 'allow internal docker communication'</rule>
  <rule priority="1" table="filter" ipv="ipv4" chain="DOCKER-USER">-p tcp -m multiport --dports 80,443 -s 1.2.3.4/32 -j ACCEPT -m comment --comment 'Allow IP 1.2.3.4 to access http and https docker ports'</rule>
  <rule priority="1" table="filter" ipv="ipv4" chain="DOCKER-USER">-p tcp -m multiport --dports 80,443 -s 5.6.7.8/32 -j ACCEPT -m comment --comment 'Allow IP 5.6.7.8 to access http and https docker ports'</rule>
  <rule priority="10" table="filter" ipv="ipv4" chain="DOCKER-USER">-j REJECT -m comment --comment 'reject all other traffic to DOCKER-USER'</rule>
</direct>



Docker Port Forwardings

If you mapped Docker Container ports to another host port (e.g. 8443:443), you must state the target NAT port (so the Docker Container port), and not the NAT source port (in the example above, you must open port 443/tcp, not port 8443/tcp).
I do not fully understand why it is so, but I assume NAT happens in chain "DOCKER" before "DOCKER-USER", but thats just an assumption.

Debug log

For debug purposes, you can add logging to the DOCKER-USER chain with highest priority. Perform "firewall-cmd --reload" to deactivate this logging again.
firewall-cmd --direct --add-rule ipv4 filter DOCKER-USER 0 \
  -j LOG --log-prefix ' DOCKER TCP: '

Tip

If you want to restart, you can stop firewalld, remove /etc/firewalld/direct.xml and start firewalld again.
Ensure Docker is NOT running when you want to restart.