Skip to content

Instantly share code, notes, and snippets.

@CurlyMoo
Last active April 16, 2025 20:02
Show Gist options
  • Save CurlyMoo/b383afbf92b21ce634bf1628c318b2b1 to your computer and use it in GitHub Desktop.
Save CurlyMoo/b383afbf92b21ce634bf1628c318b2b1 to your computer and use it in GitHub Desktop.
Mailcow in a rootless docker

The quirks I encountered when installing mailcow in a rootless docker instance.

The issues below took me way to long to find a solution for, so I hope it will help others.

Passing real IP's

By default, a rootless docker instance doesn't allow passing the real IP's to the docker containers, instead the IP of network bridge is communicated. In case of docker this is 172.22.1.1. This is annoying in NGINX because you don't know where your visitors came from, but dangerous in mailcow installations. Because of this, all connections to Postfix will be considered as coming from the internal mailcow network and thus considered safe. The Postfix installation has now become an openrelay. This means that everyone connecting to port 25 can send emails through your mailcow instance. Which spammers will very soon do.

The only solution for this (as well for NGINX) is putting Postfix behind a HAProxy installation. HAProxy can add a header to each TCP-package in which the real IP is encapsulated. Postfix can be configured to read this header so it still knows the real IP a connection came from.

Simply put this in the HAProxy configuration:

frontend mail_smtp
   bind *:25
   mode tcp
   option tcplog
   default_backend backend_postfix_servers

backend backend_postfix_servers
   mode tcp
   server server1 127.0.0.1:1025 send-proxy check

In mailcow.conf, change the SMTP_PORT to 1025. And add these lines in the data/conf/postfix/extra.cf:

postscreen_upstream_proxy_protocol = haproxy
postscreen_upstream_proxy_timeout = 5s

Restart HAProxy on the host and mailcow as the docker user and you're fine again. A simple double check that HAProxy is actually connected to the Postfix instance can be done by following the logs.

Inside the mailcow-dockerized root folder run: docker compose logs -f postfix-mailcow

Each 2 seconds, you'll see this heartbeat message coming from HAProxy:

postfix-mailcow-1  | Apr 15 23:21:41 ea123f1cde4e postfix/postscreen[378]: CONNECT from [127.0.0.1]:48268 to [127.0.0.1]:1025
postfix-mailcow-1  | Apr 15 23:21:41 ea123f1cde4e postfix/postscreen[378]: ALLOWLISTED [127.0.0.1]:48268
postfix-mailcow-1  | Apr 15 23:21:41 ea123f1cde4e postfix/smtpd[19012]: connect from localhost[127.0.0.1]
postfix-mailcow-1  | Apr 15 23:21:41 ea123f1cde4e postfix/smtpd[19012]: lost connection after CONNECT from localhost[127.0.0.1]
postfix-mailcow-1  | Apr 15 23:21:41 ea123f1cde4e postfix/smtpd[19012]: disconnect from localhost[127.0.0.1] commands=0/0

Unbound can't ping

When installing a default rootless docker instance on Debian, it's missing the slirp4netns network driver. That driver can be installed with the docker-ce-rootless-extras package. The default driver installed doesn't allow any container to ping to the outside world. Simply, installing that package and restarting docker will make ping inside a docker container work again.

Fail2ban

Fail2ban is meant to run on a host network, which doesn't work in a rootless docker setup. A workaround is using the host fail2ban. However, you need to forward the logs to something that fail2ban can reliably read. I used the /dev/log UNIX Sock from rsyslog for this. Of course you need to enable rsyslog first. This allow a docker container to forward all logs to the syslog of the host.

Add this to the postfix-mailcow configuration in the docker-compose.yml file of mailcow:

logging:
  driver: syslog
  options:
    syslog-address: "unixgram:///dev/log"

Restart the docker compose postfix-mailcow docker container and you'll see the postfix logs end up in the root /var/log/syslog log.

The downside is that you'll see the heartbeat end up there as well. This is useless information for the sake of fail2ban. To exclude those lines add this to the /etc/rsyslog.d/50-default.conf file:

:msg, regex, "postfix.*127.0.0.1"  ~

For fail2ban to work edit the jail.local file you made earlier when hardening SSH and change these blocks to the following:

[postfix]
# To use another modes set filter parameter "mode" in jail.local:
enabled = true
mode    = more
port    = smtp,465,submission
#logpath = %(postfix_log)s
logpath = /var/log/syslog
backend = %(postfix_backend)s

[postfix-rbl]
enabled  = true
filter   = postfix[mode=rbl]
port     = smtp,465,submission
#logpath  = %(postfix_log)s
logpath  = /var/log/syslog
backend  = %(postfix_backend)s
maxretry = 1

[postfix-sasl]
enabled  = true
filter   = postfix[mode=auth]
port     = smtp,465,submission,imap,imaps,pop3,pop3s
# You might consider monitoring /var/log/mail.warn instead if you are
# running postfix since it would provide the same log lines at the
# "warn" level but overall at the smaller filesize.
logpath  = /var/log/syslog
#logpath = %(postfix_log)s
backend  = %(postfix_backend)s

And add these two additional ones:

[postfix-openrelay]
enabled   = true
port      = smtp,465,submission
filter    = postfix-openrelay
logpath   = /var/log/syslog

[postfix-blacklist]
enabled   = true
port      = smtp,465,submission
filter    = postfix-blacklist
logpath   = /var/log/syslog

The postfix-openrelay block blocks IP's that try to use the mailcow instance as an open relay. The postfix-blacklist blocks IP's that are recognized by sites like spamhaus fully using IPTables. For both to work create these two files with the respective content.

/etc/fail2ban/filter.d/postfix-blacklist.conf

[Definition]

_daemon = postfix-blacklist
failregex = .*postfix/dnsblog\[.*\].*addr <HOST> listed by domain.*

/etc/fail2ban/filter.d/postfix-openrelay.conf

[Definition]

_daemon = postfix-openrelay
failregex = .*postfix/smtpd\[.*\].*NOQUEUE.*\[<HOST>\].*Relay access denied.*

Restart fail2ban and the host fail2ban will take care of malicious IP's in mailcow.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment