Docker and fail2ban – How I solved it (for me)

2021-12-07 Updated version

Docker is great when running your own services in an isolated ephemeral setup.
I’ve been using this pattern for quite a while now (approx since Linux VServer had been introduced, afterwards with plain LXC and then with Docker).

But SMTP, IMAP and other services are very attractive to not so nice people. And one might want to add some security to the services, eg. using layer 7 level information to block network traffic on layer 3/4.
So as soon as there are like bruteforce login attacks, drop the eg. TCP/IP paket for the IP of the attacker.

Normally you’d use fail2ban out of the box. It provides pretty good detections for the most well known service implementations and integrates the counter measures (eg. iptable based actions) very good into your OS.

With docker this is a little bit different. Docker also uses the iptables network utils for passing the incomming traffic on public IP towards an private IP by DNATting (Destination Network Address Translation) into a docker container private network.

Let’s have a look at the iptables chain flows:

packet_flow10.png
iptables chaining flow, source: http://xkr47.outerspace.dyndns.org/netfilter/packet_flow/

Let’s check the acual config:

$ iptables -t nat -L


Chain PREROUTING (policy ACCEPT)
target prot opt source destination
DOCKER all -- anywhere anywhere ADDRTYPE match dst-type LOCAL

Chain INPUT (policy ACCEPT)
target prot opt source destination

Chain POSTROUTING (policy ACCEPT)
target prot opt source destination
...
Chain DOCKER (2 references)
target prot opt source destination
...
DNAT tcp -- anywhere myhost.tdl tcp dpt:https to:10.10.10.10:443

So at a very early stage the traffic is passed on via DNAT. Solutions you can find for Docker and fail2ban mostly deal with the FORWARD chain.

This did not really work oput for me. Therefore I did setup my own fail2ban action (eg. /etc/fail2ban/action.d/iptables-mangle-allports.conf):

[INCLUDES]
before = iptables-common.conf
[Definition]
# Option: actionstart
# Notes.: command executed once at the start of Fail2Ban.
# Values: CMD
#
actionstart = -t filter -N f2b-<name>
              -t filter -A f2b-<name> -j
              -t filter -I INPUT -p -j f2b-<name>
              -t filter -I FORWARD -p -j f2b-<name>
              -t filter -I OUTPUT -p -j f2b-<name>
              -t filter -I DOCKER -p -j f2b-<name>
# Option: actionstop
# Notes.: command executed once at the end of Fail2Ban
# Values: CMD
#
actionstop = -t filter -D INPUT -p -j f2b-
             -t filter -D FORWARD -p -j f2b-<name>
             -t filter -D OUTPUT -p -j f2b-<name>
             -t filter -D DOCKER -p -j f2b-<name>
             <actionflush>
             -t filter -X f2b-<name>
# Option: actioncheck
# Notes.: command executed once before each actionban command
# Values: CMD
#
actioncheck = -t filter -n -L | grep -q 'f2b-[ \t]'

# Option: actionban
# Notes.: command executed when banning an IP. Take care that the command is executed with Fail2Ban user rights.
# Tags: See jail.conf(5) man page
# Values: CMD
#
actionban = -t filter -I f2b-<name> 1 -s -j REJECT --reject-with icmp-host-unreachable

# Option: actionunban
# Notes.: command executed when unbanning an IP. Take care that the
command is executed with Fail2Ban user rights.
# Tags: See jail.conf(5) man page
# Values: CMD
#
actionunban = -t filter -D f2b-<name> -s -j REJECT --reject-with icmp-host-unreachable
[Init]

I am now able to DROP the packages at a very eraly stage. Which helped me a lot.
Some jail example for my docker based mail setup, /etc/fail2ban/jail.d/mymail.conf:

[emailserver]
enabled = true
filter = mymail
logpath = /var/log/syslog
maxretry = 2
findtime = 72000
bantime = 7200
chain = PREROUTING
banaction = iptables-mangle-allports[name="emailserver", chain="PREROUTING"]

This uses the mymail filter, defined in /etc/fail2ban/filter.d/mymail.conf:

3 ban in 1 hour > Ban for 1 hour
[mymail]
enabled = true
filter = mymail
logpath = /var/log/syslog
maxretry = 2
findtime = 86400
bantime = 86400
banaction = iptables-mangle-allports[name="mymail"]

In my docker-compose.yaml, I’ve added a logging towards journald for the auth service used for the mail server:

version: '3'
services:
  auth:
    image: {{auth-service-image you use}}
    logging:
      driver: "journald"
      options:
      env: "mail_auth=true"
      tag: "{{.Name}}/{{.ID}}"