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

1665 lines
44 KiB
Markdown

---
title: 14 Mail
description:
published: true
date: 2023-07-04T15:38:32.450Z
tags:
editor: markdown
dateCreated: 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