Wiki/Guides/Docker/14Mail.md
2025-04-10 04:10:54 +02:00

44 KiB

title, description, published, date, tags, editor, dateCreated
title description published date tags editor dateCreated
14 Mail true 2023-07-04T15:38:32.450Z markdown 2023-05-03T02:09:12.610Z

Email

Most people would recommend against hosting your own mailserver, But if you have a stable connection I don't see the issue. Yes you need to secure it, But imagine how much it would do for decentralisation if everyone hosted their own mailserver. That is why I present this easy guide to a full fledged mail server :)

Dockerfile

There is no official docker container for postfix or dovecot, so we will write our own, first we need to create a directory

mkdir -p ~/docker/mailserver

Now create a Dockerfile

vim ~/docker/mailserver/Dockerfile

Paste in the following content

FROM alpine
RUN apk update && apk add mysql postfix postfix-mysql dovecot dovecot-lmtpd dovecot-mysql dovecot-pigeonhole-plugin rspamd-client
RUN mkdir -p /var/spool/postfix/etc
RUN echo "nameserver 9.9.9.9" > /var/spool/postfix/etc/resolv.conf
ENTRYPOINT apk update && apk upgrade && cp /usr/bin/rspamc /usr/lib/dovecot/sieve/rspamc && dovecot && postfix start-fg

This simply installs postfix, dovecot and the extras we need. It also includes the rspamd client to control the spamserver we will set up later.

Now we need to create a docker-compose file

vim ~/docker/mailserver/docker-compose.yml

Paste in the following content

version: '3'

networks:
  mail:
    external: true
    name: mail
    ipam:
      config:
        - subnet: 172.20.11.0/24

services:
  postfix:
    build: .
    container_name: postfix
    restart: always
    ports:
      - 25:25
      - 465:465
      - 587:587
      - 993:993
      - 995:995
    volumes:
      - /data/mailserver/log:/var/log
      - /data/mailserver/mail:/var/mail
      #- /data/mailserver/postfix:/etc/postfix
      - /data/mailserver/dovecot:/etc/dovecot
      #- /data/mailserver/sieve:/usr/lib/dovecot/sieve
      - /etc/letsencrypt/:/etc/letsencrypt/
      - /etc/localtime:/etc/localtime:ro
      - /etc/timezone:/etc/timezone:ro
    networks:
      mail:
        ipv4_address: 172.20.11.10

We also need a network so create it with the following command

sudo docker network create --subnet=172.20.11.0/24 mail

We left 2 directories commented out in the compose file, this is because we want to copy the default configuration, so lets start the container

sudo docker-compose -f ~/docker/mailserver/docker-compose.yml up -d

The container should be running, so now we are going to copy over the config directories.

sudo docker cp postfix:/etc/postfix /data/mailserver
sudo docker cp postfix:/usr/lib/dovecot/sieve /data/mailserver

Now you can remove the # before the 3 volume lines in the docker compose file

vim ~/docker/mailserver/docker-compose.yml

So it looks like this

services:
  postfix:
    volumes:
      - /data/mailserver/postfix:/etc/postfix
      - /data/mailserver/sieve:/usr/lib/dovecot/sieve

And restart the container

sudo docker-compose -f ~/docker/nginx/docker-compose.yml down && sudo docker-compose -f ~/docker/nginx/docker-compose.yml up -d

Configuring Postfix

We need to configure postfix to send and receive emails, first we are going to start with our main config file

sudo vim /data/mailserver/postfix/main.cf

Replace the entire content with the following

## Acceptation
mydestination = localhost
mynetworks = localhost 172.20.0.0/16

## Network
mydomain = example.com
myhostname = example.com
myorigin = example.com
inet_protocols = ipv4
smtpd_banner = PTR_RECORD ESMTP $mail_name

## Recipient Restrictions
smtpd_recipient_restrictions =
  permit_mynetworks
  permit_sasl_authenticated
  reject_unauth_destination
  #reject_non_fqdn_sender
  #reject_non_fqdn_recipient
  #reject_invalid_hostname
  #reject_unknown_client_hostname
  #reject_unknown_sender_domain
  reject_unknown_recipient_domain
  reject_unlisted_recipient
  #reject_unknown_reverse_client_hostname
  check_recipient_access texthash:/etc/postfix/blocked_recipients
  check_sender_access texthash:/etc/postfix/blocked_senders

## Relay Restrictions
smtpd_relay_restrictions = $smtpd_recipient_restrictions

## HELO/EHLO Restrictions
smtpd_helo_restrictions =
   permit_mynetworks
   permit_sasl_authenticated
   #reject_invalid_helo_hostname
   #reject_non_fqdn_helo_hostname
   #reject_unknown_helo_hostname

## Sender Restrictions
smtpd_sender_restrictions =
   permit_mynetworks
   permit_sasl_authenticated
   #reject_unknown_reverse_client_hostname
   #reject_unknown_client_hostname

## Milter Settings
milter_protocol = 6
milter_mail_macros = i {mail_addr} {client_addr} {client_name} {auth_authen}
milter_default_action = accept
smtpd_milters = inet:172.20.11.40:11332
non_smtpd_milters = inet:172.20.11.40:11332

## Config
compatibility_level=3.6

## Logging
smtpd_tls_loglevel = 2
smtp_tls_loglevel = 2
smtpd_delay_reject = yes
maillog_file = /var/log/postfix.log

## Mapping to database
virtual_alias_maps = proxy:mysql:/etc/postfix/virtual_alias_maps.cf,proxy:mysql:/etc/postfix/virtual_alias_domains_maps.cf
virtual_alias_domains = proxy:mysql:/etc/postfix/virtual_alias_domains.cf
virtual_mailbox_maps = proxy:mysql:/etc/postfix/virtual_mailbox_maps.cf
virtual_mailbox_domains = proxy:mysql:/etc/postfix/virtual_mailbox_domains.cf

## Mailbox Settings
virtual_mailbox_base = /var/mail
virtual_mailbox_limit = 512000000
virtual_minimum_uid = 5000
virtual_transport = lmtp:unix:private/dovecot-lmtp
virtual_uid_maps = static:5000
virtual_gid_maps = static:5000
local_transport = virtual
virtual_uid_maps = static:5000
local_recipient_maps = $virtual_mailbox_maps

## HELO/EHLO Settings
smtpd_helo_required = yes

## SASL Settings
smtpd_sasl_auth_enable = yes
smtpd_sasl_type = dovecot
smtpd_sasl_path = private/auth
smtpd_sasl_local_domain = $mydomain
smtpd_sasl_authenticated_header = yes
broken_sasl_auth_clients = no
#smtpd_sasl_security_options = noanonymous
#smtpd_sasl_tls_security_options = noanonymous

## TSL Settings
tls_preempt_cipherlist = yes
tls_ssl_options = NO_COMPRESSION
tls_random_source = dev:/dev/urandom
#tls_high_cipherlist = EDH+CAMELLIA:EDH+aRSA:EECDH+aRSA+AESGCM:EECDH+aRSA+SHA384:EECDH+aRSA+SHA256:EECDH:+CAMELLIA256:+AES256:+CAMELLIA128:+AES128:+SSLv3:AES256-SHA:CAMELLIA128-SHA:AES128-SHA
smtpd_use_tls = yes
smtpd_tls_auth_only = yes
smtpd_tls_protocols = !SSLv2, !SSLv3, TLSv1, TLSv1.1, TLSv1.2, TLSv1.3
smtpd_tls_mandatory_protocols = !SSLv2, !SSLv3
smtpd_tls_received_header = yes
smtpd_tls_session_cache_timeout = 3600s
smtpd_tls_security_level = encrypt
smtpd_tls_cert_file = /etc/letsencrypt/live/example.com/fullchain.pem
smtpd_tls_key_file = /etc/letsencrypt/live/example.com/privkey.pem
#smtpd_tls_eecdh_grade = strong
#smtpd_tls_mandatory_ciphers = high
#smtpd_tls_exclude_ciphers = aNULL:eNULL:LOW:3DES:MD5:MEDIUM:EXP:PSK:DSS:RC4:SEED:ECDSA:CAMELLIA256-SHA
smtp_use_tls = yes
smtp_tls_protocols = !SSLv2, !SSLv3, TLSv1, TLSv1.1, TLSv1.2, TLSv1.3
smtp_tls_mandatory_protocols = !SSLv2, !SSLv3
smtp_tls_session_cache_timeout = 3600s
smtp_tls_security_level = encrypt
smtp_tls_cert_file = /etc/letsencrypt/live/example.com/fullchain.pem
smtp_tls_key_file = /etc/letsencrypt/live/example.com/privkey.pem
#smtp_tls_mandatory_ciphers = high
#smtp_tls_exclude_ciphers = aNULL:eNULL:LOW:3DES:MD5:MEDIUM:EXP:PSK:DSS:RC4:SEED:ECDSA:CAMELLIA256-SHA

## Limit Settings
smtpd_client_connection_rate_limit = 100
smtpd_client_message_rate_limit = 10000
anvil_rate_time_unit = 60
message_size_limit = 51200000
header_size_limit = 102400
default_process_limit = 1000
queue_minfree = 100000000
smtpd_error_sleep_time = 1s
smtpd_soft_error_limit = 10
smtpd_hard_error_limit = 20

## Privacy
disable_vrfy_command = yes
header_checks = regexp:/etc/postfix/header_checks

The configuration is much to big to cover in this guide, But I recommend you look up each and every line to figure out what it does exactly.

Just replace example.com with your own domain and put in the PTR record, You can also comment out the smtp banner line.

Now we are going to create the master.cf file

sudo vim /data/mailserver/postfix/master.cf

Replace the entire content with the following

# ==========================================================================
# service type  private unpriv  chroot  wakeup  maxproc command + args
#               (yes)   (yes)   (no)    (never) (100)
# ==========================================================================

## Incoming
smtp      inet  n       -       y       -       -       smtpd

submission inet n       -       y       -       -       smtpd
  -o syslog_name=postfix/submission
  -o smtpd_reject_unlisted_recipient=no
  -o smtpd_client_restrictions=permit_sasl_authenticated,reject
  -o milter_macro_daemon_name=ORIGINATING
#  -o smtpd_etrn_restrictions=reject
#  -o smtpd_helo_restrictions=permit_mynetworks,permit
#  -o smtpd_sender_restrictions=$mua_sender_restrictions
#  -o milter_macro_daemon_name=ORIGINATING

smtps     inet  n       -       y       -       -       smtpd
  -o syslog_name=postfix/smtps
  -o smtpd_tls_wrappermode=yes
  -o smtpd_client_restrictions=permit_sasl_authenticated,reject
  -o milter_macro_daemon_name=ORIGINATING
#  -o smtpd_reject_unlisted_recipient=no
#  -o smtpd_helo_restrictions=$mua_helo_restrictions
#  -o smtpd_sender_restrictions=$mua_sender_restrictions
#  -o milter_macro_daemon_name=ORIGINATING

pickup    unix  n       -       y       60      1       pickup

## Processing
cleanup   unix  n       -       y       -       0       cleanup
qmgr      unix  n       -       n       300     1       qmgr
#qmgr     unix  n       -       n       300     1       oqmgr
rewrite   unix  -       -       y       -       -       trivial-rewrite

## Outbound
error     unix  -       -       y       -       -       error
retry     unix  -       -       y       -       -       error
discard   unix  -       -       y       -       -       discard
local     unix  -       n       n       -       -       local
virtual   unix  -       n       n       -       -       virtual
lmtp      unix  -       -       y       -       -       lmtp
smtp      unix  -       -       y       -       -       smtp
relay     unix  -       -       y       -       -       smtp

## Helper
bounce    unix  -       -       y       -       0       bounce
defer     unix  -       -       y       -       0       bounce
trace     unix  -       -       y       -       0       bounce

## Logging
postlog   unix-dgram n  -       n       -       1       postlogd
anvil     unix  -       -       y       -       1       anvil

## Cache
scache    unix  -       -       y       -       1       scache
tlsmgr    unix  -       -       y       1000?   1       tlsmgr
flush     unix  n       -       y       1000?   0       flush

## Verification
verify    unix  -       -       y       -       1       verify

## Proxy
#tlsproxy  unix  -       -       y       -       0       tlsproxy
proxymap  unix  -       -       n       -       -       proxymap
proxywrite unix -       -       n       -       1       proxymap
        -o syslog_name=postfix/$service_name
#       -o smtp_helo_timeout=5 -o smtp_connect_timeout=5

## mailq
showq     unix  n       -       y       -       -       showq

## External Delivery Methods
maildrop  unix  -       n       n       -       -       pipe
  flags=DRhu user=vmail argv=/usr/bin/maildrop -d ${recipient}
uucp      unix  -       n       n       -       -       pipe
  flags=Fqhu user=uucp argv=uux -r -n -z -a$sender - $nexthop!rmail ($recipient)
ifmail    unix  -       n       n       -       -       pipe
  flags=F user=ftn argv=/usr/lib/ifmail/ifmail -r $nexthop ($recipient)
bsmtp     unix  -       n       n       -       -       pipe
  flags=Fq. user=bsmtp argv=/usr/lib/bsmtp/bsmtp -t$nexthop -f$sender $recipient
scalemail-backend unix	-	n	n	-	2	pipe
  flags=R user=scalemail argv=/usr/lib/scalemail/bin/scalemail-store ${nexthop} ${user} ${extension}
mailman   unix  -       n       n       -       -       pipe
  flags=FR user=list argv=/usr/lib/mailman/bin/postfix-to-mailman.py
  ${nexthop} ${user}
vacation    unix  -       n       n       -       -       pipe
  flags=Rq user=vacation argv=/var/spool/vacation/vacation.pl -f ${sender} -- ${recipient}
#dovecot   unix  -       n       n       -       -       pipe
#  flags=DRhu user=vmail:vmail argv=/usr/libexec/dovecot/deliver -f ${sender} -d ${recipient}

We need to create a simple file that will strip private information from the header like the client IP and username.

sudo vim /data/mailserver/postfix/header_checks

paste in the following content

/^Received:.*\(Authenticated sender:/ IGNORE

next up is a simple file that lists blocked recipients, very handy if you use a catchall and one of your emails has been compromised

sudo vim /data/mailserver/postfix/blocked_recipients

Paste in the following content

compromised@example.tld 550 Email address has been compromised and is no longer in use.
spam@example.tld 550 You can keep your spam.
retired@example.tld 550 The email address you are trying to reach has been retired.

The ones here are just examples, but you can add your own in the same format

Now we are going to create the same file, just for blocked senders

sudo vim /data/mailserver/postfix/blocked_senders

Paste in the following content

spamsender@example.tld 550 You can keep your spam.
spamsender@example2.tld 550 Server not accepting email from this sender.

Again you can add what you want, very handy for repeated offenders who don't offer an unsubscribe link.

In the main.cf configuration file we call for 5 files that do not exist yet, these files contain credentials and instructions to look into a database, we are going to create all 5 files, paste in the content and adjust the credentials so that they match your configuration

Create the file

sudo vim /data/mailserver/postfix/virtual_alias_domains.cf

paste in the following content and adjust the credentials

user = postfix
password = POSTFIXDATABASEUSERPASSWORD
hosts = 172.20.11.30
dbname = postfix
query = SELECT alias_domain FROM alias_domain WHERE alias_domain='%s' AND active = '1'

Create the file

sudo vim /data/mailserver/postfix/virtual_alias_domains_maps.cf

paste in the following content and adjust the credentials

user = postfix
password = POSTFIXDATABASEUSERPASSWORD
hosts = 172.20.11.30
dbname = postfix
query = SELECT goto FROM alias,alias_domain WHERE alias_domain.alias_domain = '%d' and alias.address = CONCAT('%u', '@', alias_domain.target_domain) AND alias.active = '1' AND alias_domain.active='1'

Create the file

sudo vim /data/mailserver/postfix/virtual_alias_maps.cf

paste in the following content and adjust the credentials

user = postfix
password = POSTFIXDATABASEUSERPASSWORD
hosts = 172.20.11.30
dbname = postfix
table = alias
select_field = goto
where_field = address

Create the file

sudo vim /data/mailserver/postfix/virtual_mailbox_domains.cf

paste in the following content and adjust the credentials

user = postfix
password = POSTFIXDATABASEUSERPASSWORD
hosts = 172.20.11.30
dbname = postfix
table = domain
select_field = domain
where_field = domain

Create the file

sudo vim /data/mailserver/postfix/virtual_mailbox_maps.cf

paste in the following content and adjust the credentials

user = postfix
password = POSTFIXDATABASEUSERPASSWORD
hosts = 172.20.11.30
dbname = postfix
table = mailbox
select_field = maildir
where_field = username

Postfix configuration should be done now

Configuring Dovecot

Dovecot will be our imap server, so that we can use a client to actually read our email

Lets start with the main configuration file

sudo vim /data/mailserver/dovecot/dovecot.conf

Paste in the following bytes

# Logging
log_path = /var/log/dovecot.log
mail_debug = yes

# Protocols
protocols = imap pop3 lmtp sieve
auth_mechanisms = login plain
#disable_plaintext_auth = yes

# Security
ssl = required
ssl_min_protocol = TLSv1.2
ssl_prefer_server_ciphers = yes
ssl_cert = </etc/letsencrypt/live/example.com/fullchain.pem
ssl_key = </etc/letsencrypt/live/example.com/privkey.pem
ssl_dh = </etc/dovecot/dh.pem
ssl_options = no_compression no_ticket
ssl_cipher_list = ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256

# Storage
mail_home = /var/mail/%d/%n
mail_location = maildir:~

# Inbox Config
namespace inbox {
  inbox = yes
  separator = /

  mailbox "Drafts" {
    auto = subscribe
    special_use = \Drafts
  }
  mailbox "Sent" {
    auto = subscribe
    special_use = \Sent
  }
  mailbox "Trash" {
    auto = subscribe
    special_use = \Trash
  }
  mailbox "Spam" {
    auto = subscribe
    special_use = \Junk
  }
  mailbox "Archive" {
    auto = subscribe
    special_use = \Archive
  }
}

passdb {
    driver = sql
    args = /etc/dovecot/dovecot-sql.conf
}

userdb {
    driver = sql
    args = /etc/dovecot/dovecot-sql.conf
}

service auth {
  unix_listener /var/spool/postfix/private/auth {
    group = postfix
    mode = 0660
    user = postfix
  }
  unix_listener /var/spool/postfix/private/dovecot-auth {
    group = postfix
    mode = 0660
    user = postfix
  }
  unix_listener auth-userdb {
    mode = 0600
    user = vmail
  }
  user = dovecot
}

service lmtp {
 unix_listener /var/spool/postfix/private/dovecot-lmtp {
   group = postfix
   mode = 0600
   user = postfix
  }
}

protocol imap {
  mail_plugins = $mail_plugins imap_sieve
  postmaster_address = postmaster@example.com
}

protocol pop3 {
  mail_plugins = $mail_plugins
}

protocol lmtp {
  mail_plugins = $mail_plugins sieve
}

!include conf.d/*.conf

now the mysql configuration

sudo vim /data/mailserver/dovecot/dovecot-sql.conf

Paste in the following content

driver = mysql
connect = host=172.20.11.30 dbname=postfix user=postfix password=POSTFIXDATABASEUSERPASSWORD
default_pass_scheme = SHA512-CRYPT
user_query = SELECT '/var/mail/%d/%n' as home, 'maildir:/var/mail/%d/%n' as mail, 5000 AS uid, 5000 AS gid, concat('dirsize:storage=',  quota) AS quota FROM mailbox WHERE username = '%u' AND active = '1'
password_query = SELECT username as user, password, '/var/mail/%d/%n' as userdb_home, 'maildir:/var/mail/%d/%n' as userdb_mail, 5000 as  userdb_uid, 5000 as userdb_gid FROM mailbox WHERE username = '%u' AND active = '1'

Now create the Sieve config file

sudo vim /data/mailserver/dovecot/conf.d/90-sieve.conf

Replace the entire content with the following

plugin {
  sieve = file:/usr/lib/dovecot/sieve/%d/%n/scripts;active=/usr/lib/dovecot/sieve/%d/%n/active-script.sieve
  sieve_before = /usr/lib/dovecot/sieve/spam-global.sieve
  imapsieve_mailbox1_name = Spam
  imapsieve_mailbox1_causes = COPY
  imapsieve_mailbox1_before = file:/usr/lib/dovecot/sieve/report-spam.sieve
  imapsieve_mailbox2_name = *
  imapsieve_mailbox2_from = Spam
  imapsieve_mailbox2_causes = COPY
  imapsieve_mailbox2_before = file:/usr/lib/dovecot/sieve/report-ham.sieve
  sieve_pipe_bin_dir = /usr/lib/dovecot/sieve
  sieve_global_extensions = +vnd.dovecot.pipe +vnd.dovecot.environment
  sieve_extensions = +fileinto +mailbox
  sieve_plugins = sieve_imapsieve sieve_extprograms
}

and finally the managesieve config file

sudo vim /data/mailserver/dovecot/conf.d/20-managesieve.conf

Replace the entire content with the following

service managesieve-login {
  inet_listener sieve {
    port = 4190
  }
}
service managesieve {
  process_limit = 1024
}
protocol sieve {
}

make sure the vmail user owns the mail directory

sudo chown -R 5000:root /data/mailserver/mail

Finally we are going to generate the dh file

sudo openssl dhparam -out /data/mailserver/dovecot/dh.pem 4096

For now it is done, we will continue with Sieve later

Create Database and user

We need to create a MariaDB database for postfix, so lets do that

sudo docker exec -it maridb mysql -p

Enter the Mysql root password you provided during the creation of the mariadb container and you should be in.

Now create a databases with the following command

create database postfix;

Create the user with

create user postfix@'172.20.11.10' identified by 'POSTFIXDATABASEPASSWORD';

Give privileges to the user on the database with

grant all privileges on postfix.* to postfix@'172.20.11.10';

And Flush the privileges with

flush privileges;

Exit mysql with

quit;

Now we have a user, but the database is still not in the mail network so we need to add the mariadb container to the mail network hy editing the mariadb compose file

vim ~/docker/mariadb/docker-compose.yml

Here add the following in the right place

services:
  mariadb:
    networks:
      mail:
        ipv4_address: 172.20.11.30

networks:
  mail:
    external: true
    name: mail

You probably already have the network keys, so in that case only copy the mail: keys with their properties and put them under the other networks in your compose file.

Finally we restart the mariadb container so it will be in the mailserver network

sudo docker-compose -f ~/docker/mariadb/docker-compose.yml down && sudo docker-compose -f ~/docker/mariadb/docker-compose.yml up -d

Configuring Sieve

Now we need to configure sieve so that messages put into spam are forwarded to the spamserver to train it. The starting of the container should already have created the folder, so we dive right into the configuration files

First we create the report ham file, when files are moved out of spam.

sudo vim /data/mailserver/sieve/report-ham.sieve

Paste in the following content

require ["vnd.dovecot.pipe", "copy", "imapsieve"];
pipe :copy "rspamc" ["-h", "172.20.11.40:11334", "learn_ham"];

Next we create the report spam file, for when files are moved into spam

sudo vim /data/mailserver/sieve/report-spam.sieve

Paste in the following content

require ["vnd.dovecot.pipe", "copy", "imapsieve"];
pipe :copy "rspamc" ["-h", "172.20.11.40:11334", "learn_spam"];

Finally we create the global spam file which will move incoming mail into spam

sudo vim /data/mailserver/sieve/spam-global.sieve

Paste in the following content

require ["fileinto","mailbox"];

if anyof(
    header :contains ["X-Spam-Flag"] "YES",
    header :contains ["X-Spam"] "Yes",
    header :contains ["Subject"] "*** SPAM ***"
    )
{
    fileinto :create "Spam";
    stop;
}

Now we only need to compile the 3 files so that sieve can use them

first we need to get into the mailserver

sudo docker exec -it postfix sh

Now we simply execute the commands 1 by 1

For report ham

sievec /usr/lib/dovecot/sieve/report-ham.sieve

For report spam

sievec /usr/lib/dovecot/sieve/report-spam.sieve

for global spam

sievec /usr/lib/dovecot/sieve/spam-global.sieve

finally exit the docker container

exit

Sievec should be ready to go.

Postfixadmin

We use Postfixadmin for creating and managing mailboxes and aliases We let it run in its own container with its own network so create a new directory

mkdir -p ~/docker/postfixadmin

Now create the docker-compose file

vim ~/docker/postfixadmin/docker-compose.yml

Add in the following lines

version: '3'

services:
  postfixadmin:
    image: postfixadmin
    container_name: postfixadmin
    restart: always
    volumes: 
      - /data/postfixadmin/config.local.php:/var/www/html/config.local.php:ro
      - /etc/localtime:/etc/localtime:ro
    networks:
      postfixadmin:
        ipv4_address: 172.20.71.10
    environment:
      POSTFIXADMIN_DB_TYPE: mysqli
      POSTFIXADMIN_DB_HOST: 172.20.71.30
      POSTFIXADMIN_DB_USER: postfix
      POSTFIXADMIN_DB_NAME: postfix
      POSTFIXADMIN_DB_PASSWORD: POSTFIXDATABASEUSERPASSWORD

networks:
  postfixadmin:
    external: true
    name: postfixadmin
    ipam:
      config:
        - subnet: 172.20.71.0/24

Create the config directory

sudo mkdir /data/postfixadmin

Lets take a look at the configuration file

sudo vim /data/postfixadmin/config.local.php

Paste in the following content

<?php
$CONF['configured'] = true;
$CONF['database_type'] = 'mysqli';
$CONF['database_host'] = '172.20.71.30';
$CONF['database_user'] = 'postfix';
$CONF['database_password'] = 'POSTFIXDATABASEUSERPASSWORD';
$CONF['database_name'] = 'postfix';
$CONF['setup_password'] = 'POSTFIXADMINSETUPPASSWORD';
#$CONF['encrypt'] = 'SHA512-CRYPT';
?>

The Setup Password is for setting up the admin account but first we need to create a network

sudo docker network create --subnet=172.20.71.0/24 postfixadmin

Now we need to add nginx to the network so open the compose file of nginx

vim ~/docker/nginx/docker-compose.yml

Here add the following in the right place

services:
  nginx:
    networks:
      postfixadmin:
        ipv4_address: 172.20.71.20

networks:
  postfixadmin:
    external: true
    name: postfixadmin

You probably already have the network keys, so in that case only copy the postfixadmin: keys with their properties and put them under the other networks in your compose file.

Now we need to do the same for mariadb

vim ~/docker/mariadb/docker-compose.yml

Here add the following in the right place

services:
  mariadb:
    networks:
      postfixadmin:
        ipv4_address: 172.20.71.30

networks:
  postfixadmin:
    external: true
    name: postfixadmin

Now we are going to create a second postfix user that can access from the postfixadmin container so get access to the mariaDB prompt

sudo docker exec -it maridb mysql -p

Enter the Mysql root password you provided during the creation of the mariadb container and you should be in.

Create the user with

create user postfix@'172.20.71.10' identified by 'POSTFIXDATABASEPASSWORD';

Give privileges to the user on the database with

grant all privileges on postfix.* to postfix@'172.20.71.10';

And Flush the privileges with

flush privileges;

Exit mysql with

quit;

Now we need to add the server block to the nginx config so create a new file

sudo vim /data/nginx/config/services/postfixadmin.active

Paste in the following and be sure to replace the domain name 6 times

server {
  server_name admin.example.com;
  listen      443 ssl;

# Settings
  autoindex off;

# Locations
  location / {
    auth_basic "Restricted Content";
    auth_basic_user_file /etc/nginx/auth/.postfixadmin;
    proxy_pass http://postfixadmin:80;
    proxy_http_version                 1.1;
    proxy_cache_bypass                 $http_upgrade;
    proxy_ssl_server_name              on;
    proxy_set_header Upgrade           $http_upgrade;
    proxy_set_header X-Real-IP         $remote_addr;
    proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_set_header X-Forwarded-Host  $host;
    proxy_set_header X-Forwarded-Port  $server_port;
    proxy_set_header Connection "upgrade";
    proxy_set_header Host $host;
    proxy_connect_timeout              60s;
    proxy_send_timeout                 60s;
    proxy_read_timeout                 60s;
  }

  location ~ /\.(?!well-known) {
    deny all;
  }

  location = /favicon.ico {
    log_not_found off;
  }

  location = /robots.txt {
    log_not_found off;
  }

# GZip
  gzip            on;
  gzip_vary       on;
  gzip_proxied    any;
  gzip_comp_level 6;
  gzip_types      text/plain text/css text/xml application/json application/javascript application/rss+xml application/atom+xml image/svg+xml;

# Headers
  add_header X-XSS-Protection          "1; mode=block" always;
  add_header X-Content-Type-Options    "nosniff" always;
  add_header X-Frame-Options "SAMEORIGIN";
  add_header Referrer-Policy           "no-referrer-when-downgrade" always;
  add_header Content-Security-Policy   "default-src 'self' http: https: ws: wss: data: blob: 'unsafe-inline'; frame-ancestors 'self';" always;
  add_header Permissions-Policy        "interest-cohort=()" always;
  add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;

# SSL
  ssl_certificate     /etc/letsencrypt/live/admin.example.com/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/admin.example.com/privkey.pem;
  ssl_trusted_certificate /etc/letsencrypt/live/admin.example.com/chain.pem;
}

# Redirect
server {
  listen 80;
  server_name admin.example.com;
  return 301 https://admin.example.com$request_uri;
}

Make sure you have a valid certificate for the domain

One last thing we need to do is create a authpasswd file so the interface is behind a extra layer of security

to do this we need some apache tools so install them

sudo pacman -S apache

now execute the following command be sure to replace USERNAME with a username

sudo htpasswd -c /data/nginx/config/auth/.postfixadmin USERNAME

now it will ask for a password, give it one and store it well.

Finally we can restart the relevant containers

first the database so that postfixadmin can access it

sudo docker-compose -f ~/docker/mariadb/docker-compose.yml down && sudo docker-compose -f ~/docker/mariadb/docker-compose.yml up -d

Now we start the postfixadmin container so nginx can find it

sudo docker-compose -f ~/docker/postfixadmin/docker-compose.yml up -d

Finally restart the nginx container

sudo docker-compose -f ~/docker/nginx/docker-compose.yml down && sudo docker-compose -f ~/docker/nginx/docker-compose.yml up -d

You should be able to go example.com/setup.php, first login with the http auth user and password you given with the htpasswd command then scroll down to generate setup password

Copy the setup password hash into your configuration file

sudo vim /data/postfixadmin/config.local.php

Now you should be able to create an admin user with the password you have given it You might have to restart the container for the setup password to work

Rspamd

Rspamd will be our spam solution, it will scan incoming and outgoing emails, it requires quite a lot of configuration files, but it simply runs circles around spamassassin.

Like always we start with a project directory

mkdir -p ~/docker/rspamd

for Rspamd there is again no official dockerfile, so we will create our own minimal image

vim ~/docker/rspamd/Dockerfile

Paste in the following content

FROM alpine
RUN mkdir /run/rspamd/ && touch /run/rspamd/rspamd.sock && chmod 600 /run/rspamd/rspamd.sock
RUN apk add --no-cache rspamd rspamd-client rspamd-controller rspamd-utils redis
ENTRYPOINT apk update && apk upgrade && redis-server /etc/redis.conf --daemonize yes && rspamd -i && while true; do sleep 10000; done

Now we create a docker compose file

vim ~/docker/rspamd/docker-compose.yml

Now paste in the following content

version: '3'

services:
  rspamd:
    build: .
    container_name: rspamd
    restart: always
    volumes:
      - /data/rspamd/log:/var/log/rspamd
      #- /data/rspamd/config:/etc/rspamd
      #- /data/rspamd/database:/var/lib/redis
      #- /data/rspamd/stats:/var/lib/rspamd
      #- /data/rspamd/www:/usr/share/rspamd/www
      - /etc/letsencrypt/:/etc/letsencrypt/
      - /etc/localtime:/etc/localtime:ro
      - /etc/timezone:/etc/timezone:ro
    networks:
      mail:
        ipv4_address: 172.20.11.40

networks:
  mail:
    external: true
    name: mail
    ipam:
      config:
        - subnet: 172.20.11.0/24

You might have noticed the # characters again, and the same strategy applies

sudo docker-compose -f ~/docker/rspamd/docker-compose.yml up -d

Now we need to copy the directories to the right place

sudo docker cp rspamd:/etc/rspamd /data/rspamd/config
sudo docker cp rspamd:/var/lib/redis /data/rspamd/database
sudo docker cp rspamd:/var/lib/rspamd /data/rspamd/stats
sudo docker cp rspamd:/usr/share/rspamd/www /data/rspamd/www

Now remove the # from the docker-compose file

vim ~/docker/rspamd/docker-compose.yml

So it looks like this

services:
  rspamd:
    volumes:
      - /data/rspamd/config:/etc/rspamd
      - /data/rspamd/database:/var/lib/redis
      - /data/rspamd/stats:/var/lib/rspamd
      - /data/rspamd/www:/usr/share/rspamd/www

Now simply restart the container

sudo docker-compose -f ~/docker/rspamd/docker-compose.yml down && sudo docker-compose -f ~/docker/rspamd/docker-compose.yml up -d

We need to adjust the redis config

sudo vim /data/rspamd/config/local.d/redis.conf

Paste in the following content

servers = "127.0.0.1";

This will add spamscore details to the headers.

sudo vim /data/rspamd/config/override.d/milter_headers.conf 

add in the following content

extended_spam_headers = true;

Now we need to adjust some small files for the listeners

first the worker-proxy

sudo vim /data/rspamd/config/local.d/worker-proxy.inc

Paste in the following lines

bind_socket = "0.0.0.0:11332";
milter = yes;
timeout = 120s;
upstream "local" {
  default = yes;
  self_scan = yes;
  hosts = "rspamd:11332";
}
count = 4;
max_retries = 5;
discard_on_reject = false;
quarantine_on_reject = false;
spam_header = "X-Spam";
reject_message = "Spam message rejected";

Now the worker-normal

sudo vim /data/rspamd/config/local.d/worker-normal.inc

paste in the following content

bind_socket = "0.0.0.0:11333";

And finally the worker-controller

We first need a hashed password to put in the file so go into the container

sudo docker exec -it rspamd sh

Now generate a hashed password

rspamadm pw

Give it your desired password and copy the result into the following file

sudo vim /data/rspamd/config/local.d/worker-controller.inc

Paste in the following content replacing the password obviously

password = "A Very Strong Password";
bind_socket = "0.0.0.0:11334";
mode=0622
secure_ip = "172.20.11.10";

Finally we create a file that will prevent outgoing mail being marked as spam

sudo vim /data/rspamd/config/local.d/settings.conf

Paste in the following content

authenticated {
  priority = high;
  authenticated = yes;
  apply {
    actions {
      "rewrite subject" = 100.0;
      "add header" = 100.0;
      "reject" = 100.0;
    }
  }
}

Now we can restart rspamd to make the changes take effect

sudo docker-compose -f ~/docker/rspamd/docker-compose.yml down && sudo docker-compose -f ~/docker/rspamd/docker-compose.yml up -d

Rspamd configuration is complete, now we just need to configure the reverse proxy so we can access it.

We simply create a file in the services directory of nginx

sudo vim /data/nginx/config/services/rspamd.active

Paste in the following content

server {
  server_name spam.example.com;
  listen 443 ssl;

# Settings
  client_max_body_size 100M;
  autoindex off;
  root /data/rspamd/www;

# Locations
  location / {
    auth_basic "Restricted Content";
    auth_basic_user_file /etc/nginx/auth/.rspamdhtpasswd;
    proxy_pass  http://172.20.11.40:11334;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header Host $http_host;
  }

# Headers
  add_header Strict-Transport-Security "max-age=31536000; includeSubdomains";
  add_header X-Content-Type-Options nosniff;
  add_header X-Frame-Options SAMEORIGIN;
  add_header X-XSS-Protection "1; mode=block";

# SSL
  ssl_protocols TLSv1.2 TLSv1.3;
  ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305;
  ssl_prefer_server_ciphers on;
  ssl_session_cache shared:TLS:10m;
  ssl_session_timeout 1d;
  ssl_stapling on;
  ssl_stapling_verify on;
  server_tokens off;
  ssl_certificate /etc/letsencrypt/live/spam.example.com/fullchain.pem;
  ssl_trusted_certificate /etc/letsencrypt/live/spam.example.com/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/spam.example.com/privkey.pem;
}

# Redirect
server {
  listen 80;
  server_name spam.example.com;
  return 301 https://spam.example.com$request_uri;
}

And we add Nginx to the mail network

vim ~/docker/nginx/docker-compose.yml

Here add the following in the right place

services:
  nginx:
    networks:
      mail:
        ipv4_address: 172.20.11.20

networks:
  mail:
    external: true
    name: mail

now execute the following command be sure to replace USERNAME with a username

sudo htpasswd -c /data/nginx/config/auth/.rspamdpasswd USERNAME

now it will ask for a password, give it one and store it well.

Now we simply restart nginx

sudo docker-compose -f ~/docker/nginx/docker-compose.yml down && sudo docker-compose -f ~/docker/nginx/docker-compose.yml up -d

RspamD should be up and running you can access it by going to spam.example.com first entering the auth user/pass and then the rspamd controller password you set.

Roundcube

Roundcube is a nice web email client, it is optional for this mailstack, but it can be very handy.

We start with a project directory

mkdir -p ~/docker/roundcube

Now create the docker-compose file

vim ~/docker/roundcube/docker-compose.yml

Paste in the following content

version: '3'

networks:
  roundcube:
    external: true
    name: roundcube

services:
  roundcube:
    image: roundcube/roundcubemail
    container_name: roundcube
    restart: always
    volumes: 
      - /data/roundcube/:/var/www/html/
    networks:
      roundcube:
        ipv4_address: 172.20.32.10

Roundcube will run in its own network so lets create that too

sudo docker network create --subnet=172.20.32.0/24 roundcube

Now we will add Nginx to the roundcube network

vim ~/docker/nginx/docker-compose.yml

Here add the following in the right place

services:
  nginx:
    networks:
      roundcube:
        ipv4_address: 172.20.32.20

networks:
  roundcube:
    external: true
    name: roundcube

And create the config file for Nginx

sudo vim /data/nginx/config/services/roundcube.active

Paste in the following content

server {
  server_name example.com;
  listen      443 ssl;

# Settings
  autoindex off;
  client_max_body_size 5000M;

# Locations
  location / {
    proxy_pass http://roundcube:80;
    proxy_http_version                 1.1;
    proxy_cache_bypass                 $http_upgrade;
    proxy_ssl_server_name              on;
    proxy_set_header Upgrade           $http_upgrade;
    proxy_set_header X-Real-IP         $remote_addr;
    proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_set_header X-Forwarded-Host  $host;
    proxy_set_header X-Forwarded-Port  $server_port;
    proxy_set_header Connection "upgrade";
    proxy_set_header Host $host;
    proxy_connect_timeout              6000s;
    proxy_send_timeout                 6000s;
    proxy_read_timeout                 6000s;
  }

  location ~ /\.(?!well-known) {
    deny all;
  }

  location = /favicon.ico {
    log_not_found off;
  }

  location = /robots.txt {
    log_not_found off;
  }

# GZip
  gzip            on;
  gzip_vary       on;
  gzip_proxied    any;
  gzip_comp_level 6;
  gzip_types      text/plain text/css text/xml application/json application/javascript application/rss+xml application/atom+xml image/svg+xml;

# Headers
  add_header X-XSS-Protection          "1; mode=block" always;
  add_header X-Content-Type-Options    "nosniff" always;
  add_header X-Frame-Options "SAMEORIGIN";
  add_header Referrer-Policy           "no-referrer-when-downgrade" always;
  #add_header Content-Security-Policy   "default-src 'self' http: https: ws: wss: data: blob: 'unsafe-inline'; frame-ancestors 'self';" always;
  add_header Permissions-Policy        "interest-cohort=()" always;
  add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;

# SSL
  ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
  ssl_trusted_certificate /etc/letsencrypt/live/example.com/chain.pem;
}

# Redirect
server {
  listen 80;
  server_name example.com;
  return 301 https://example.com$request_uri;
 }

Now we will add MariaDB to the Roundcube network

vim ~/docker/mariadb/docker-compose.yml

Here add the following in the right place

services:
  mariadb:
    networks:
      roundcube:
        ipv4_address: 172.20.32.30

networks:
  roundcube:
    external: true
    name: roundcube

Now we are going to restart the mariadb container so it is actually part of the network

sudo docker-compose -f ~/docker/mariadb/docker-compose.yml down && sudo docker-compose -f ~/docker/mariadb/docker-compose.yml up -d

Now we will create a database and user

sudo docker exec -it mariadb mysql -p

Enter the Mysql root password you provided during the creation of the mariadb container and you should be in.

Create a database with

create database roundcube;

Create the user with

create user roundcube@'172.20.32.10' identified by 'ROUNDCUBEDATABASEPASSWORD';

Give privileges to the user on the database with

grant all privileges on roundcube.* to roundcube@'172.20.32.10';

And Flush the privileges with

flush privileges;

Exit mysql with

quit;

Create the config directory

sudo mkdir -p /data/roundcube/config

Finally we create the configuration file

sudo vim /data/roundcube/config/config.inc.php

Paste in the following content

<?php
$config['db_dsnw'] = 'mysql://roundcube:ROUNDCUBEDATABASEPASSWORD@172.20.32.30/roundcube';
$config['db_dsnr'] = '';
$config['smtp_host'] = 'ssl://mail.example.com';
$config['imap_host'] = 'ssl://mail.example.com';
$config['skin'] = 'elastic';
$config['request_path'] = '/';
$config['log_driver'] = 'file';
$config['log_logins'] = true;
$config['support_url'] = '';
$config['temp_dir'] = '/tmp/roundcube-temp';
$config['enable_spellcheck'] = true;
$config['spellcheck_engine'] = 'pspell';
$config['zipdownload_selection'] = true;
#include(__DIR__ . '/config.docker.inc.php');

Now we start the container

sudo docker-compose -f ~/docker/roundcube/docker-compose.yml up -d

SPF

SPF is very simple, you just need to add the following line as a TXT record to your sending domain DNS records

v=spf1 mx -all

DKIM

DKIM is a little more complicated, but everything should be fine if you follow the guide

First we need to create a directory for the keys

sudo mkdir -p /data/rspamd/stats/dkim

Now we go into the rspamd container

sudo docker exec -it rspamd sh

Now we generate the key

rspamadm dkim_keygen -b 2048 -s 1 -k /var/lib/rspamd/dkim/1.key > /var/lib/rspamd/dkim/1.txt

Now cat the key

cat /var/lib/rspamd/dkim/1.txt

Now put the v the k and the p in your DNS records like so

1._domainkey	TXT		v=DKIM1; k=rsa; p=MIIBI.....IDAQAB

Be sure to put in the whole p= value it should start with MIIBI and and with IDAQAB with no spaces or " in between

You can exit the container

exit

Now we need to adjust the dkim configuration file for Rspamd

sudo vim /data/rspamd/config/local.d/dkim_signing.conf

Paste in the following content

path = "/var/lib/rspamd/dkim/$selector.key";
selector = "1";
allow_username_mismatch = true;

Restart RspamD to make it take effect

sudo docker-compose -f ~/docker/rspamd/docker-compose.yml down && sudo docker-compose -f ~/docker/rspamd/docker-compose.yml up -d

DMARC

DMARC is again very simple, It just tells the receiving server what to do if SPF or DKIM fail

Add the following TXT record to your DNS record

_dmarc		TXT		v=DMARC1; p=reject;

ARC

We can simply use the DKIM keys, so no need to generate anything, we just need to add some configuration

sudo vim /data/rspamd/config/local.d/arc.conf

Paste in the following content

path = "/var/lib/rspamd/dkim/$selector.key";
selector = "1";
allow_username_mismatch = true;
sign_authenticated = true;
sign_incoming = true;
use_domain = "header";
use_domain_sign_inbound = "recipient";
symbol_signed = "ARC_SIGNED";
sign_local = true;
auth_only = true;

Restart RspamD to make it take effect

sudo docker-compose -f ~/docker/rspamd/docker-compose.yml down && sudo docker-compose -f ~/docker/rspamd/docker-compose.yml up -d

Fail2ban

Rspamd and Postfixadmin are protected by nginx auth, We just need to protect Postfix, Dovecot and Roundcube so lets do them one by one

create the postfix file

sudo vim /etc/fail2ban/filter.d/postfixx.local

Paste in the following content

[INCLUDES]
before = common.conf

[Definition]
failregex = ^.*: lost connection after.*\[<HOST>\]$
            ^.*:.*\[<HOST>\]: SASL LOGIN authentication failed:.*$
            ^.*: warning: hostname .* does not resovlve to address <HOST>$
            ^.*: warning: non-SMTP command from .*\[<HOST>\].*$

create the dovecot file

sudo vim /etc/fail2ban/filter.d/dovecott.local

Paste in the following content

[INCLUDES]
before = common.conf

[Definition]
failregex = ^.*auth failed.*rip=<HOST>,.*$

create the roundcube file

sudo vim /etc/fail2ban/filter.d/roundcube.local

Paste in the following content

[INCLUDES]
before = common.conf

[Definition]
failregex = ^.*Login failed for .*Real-IP: <HOST>,.*$

Now we need to add the filters to our jails file

sudo vim /etc/fail2ban/jail.local

Add in the following lines

## Postfix
[postfixx]
enabled = true
logpath = /data/mailserver/log/postfix.log

## Dovecot
[dovecott]
enabled = true
logpath = /data/mailserver/log/dovecot.log

## Roundcube
[roundcube]
enabled = true
logpath = /data/roundcube/logs/errors.log

Now simply restart the service to make the changes take effect

sudo systemctl restart fail2ban

Be absolutely sure that it is running

sudo systemctl status fail2ban