Ansible Mailserver
Welcome to my simple mailserver setup via ansible.
This project is still work in progress, so maybe you will visit this site later on.
What we will setup:
- Postfix SMTP Server with MySQL user authentication
- Dovecot POP3/IMAP Server with MySQL user authentication
- Amavis Virus Scanner with ClamAV and DKIM
- Lets Encrypt Certbot for issuing SSL certificates
What we already have:
- Database Server with MySQL Schema
- ViMbAdmin: https://github.com/opensolutions/ViMbAdmin
- This Server with Debian Buster
Inventory
So we need our inventory. I have a cheap Hetzner Cloud Server for this project (https://www.hetzner.com/cloud). This server has an internal and an external ip address and I have assigned the hostname "mail01.example.com".
hosts.yml
:
---
all:
hosts:
mail01:
ansible_host: 78.47.192.000
private_ip: 10.0.10.3
mail_hostname: mail01.example.com
children:
mailservers:
hosts:
mail01:
Group Variables
Our setup will support multiple mailservers we need to specify our group variables. Since we use an external database server, we need to specify these credentials. Also we want to sign our "mydomain.com" emails with DKIM and want to establish a secure connection over the alias "mail.mydomain.com".
group_vars/mailservers.yml
:
---
mail_postmaster: postmaster@example.com
MYSQL_HOST: 10.0.10.6
MYSQL_USER: vimbadmin
MYSQL_PASSWORD: !!!CHANGE_ME!!!
MYSQL_DATABASE: vimbadmin
DKIM_DOMAINS:
- mydomain.com
CERTBOT_EMAIL: certbot@example.com
ALIAS_DOMAINS:
- mail.mydomain.com
Playbook
So we are setup so far and start writing our playbook. Since we complete rely on our role this playbook is very simple
mailservers.yml
:
---
- hosts: mailservers
remote_user: ansible
become: yes
vars_files:
- group_vars/mailservers.yml
roles:
- mailserver
Writing our Role
So this segment is splitted into various parts. We need handlers, tasks and templates.
Handlers
So we use handlers to restart and enable our services. So they are only invoked when something is changed.
roles/mailserver/handlers/main.yml
:
---
- name: restart dovecot
service:
name: dovecot
state: restarted
enabled: yes
- name: restart postfix
service:
name: postfix
state: restarted
enabled: yes
- name: restart amavis
service:
name: amavis
state: restarted
enabled: yes
- name: restart clamav-daemon
service:
name: clamav-daemon
state: restarted
enabled: yes
- name: restart clamav-freshclam
service:
name: clamav-freshclam
state: restarted
enabled: yes
Tasks
I've split the tasks in separate files so that the role will be simple to read:
Main
In the Main Tasks file we create the mail user and group for our virtual mailboxes. Then we include all other sub tasks.
roles/mailserver/tasks/main.yml
:
---
- name: create vmail group
group:
name: vmail
gid: 5000
state: present
- name: create vmail user
user:
name: vmail
group: vmail
shell: /bin/bash
append: yes
home: /var/vmail/
state: present
uid: 5000
- name: setup dovecot
import_tasks: dovecot.yml
- name: setup postfix
import_tasks: postfix.yml
- name: setup amavis
import_tasks: amavis.yml
- name: setup clamav
import_tasks: clamav.yml
- name: setup certbot
import_tasks: certbot.yml
Dovecot
Dovecot is our POP3/IMAP server and will handle the mail access for the users. We install all needed packages, create the general configuration file, the mysql configuration and diffie hellman parameters. The configuration files will be explained in the templates section of this document.
roles/mailserver/tasks/dovecot.yml
:
---
- name: add dovecot repo key
apt_key:
url: https://repo.dovecot.org/DOVECOT-REPO-GPG
state: present
- name: add dovecot repo
apt_repository:
repo: deb https://repo.dovecot.org/ce-2.3-latest/debian/{{ ansible_lsb["codename"] }} {{ ansible_lsb["codename"] }} main
state: present
filename: dovecot
update_cache: yes
- name: update the repository cache and install dovecot packages
apt:
name: "{{ packages }}"
state: latest
update_cache: yes
vars:
packages:
- dovecot-core
- dovecot-imapd
- dovecot-lmtpd
- dovecot-managesieved
- dovecot-mysql
- dovecot-sieve
- name: copy dovecot config file
template:
src: dovecot/dovecot.j2
dest: /etc/dovecot/dovecot.conf
notify:
- restart dovecot
- name: copy dovecot mysql config file
template:
src: dovecot/mysql.j2
dest: /etc/dovecot/dovecot-mysql.conf
notify:
- restart dovecot
- name: create diffie hellman parameters (this takes a very long time)
command: openssl dhparam -out /etc/ssl/dhparam4096.pem 4096
args:
creates: /etc/ssl/dhparam4096.pem
notify:
- restart dovecot
Postfix
To deliver our emails, we need postfix as an SMTP server. As with dovecot we copy the general config file and mysql config files for our virtual mailboxes.
roles/mailserver/tasks/postfix.yml
:
---
- name: update the repository cache and install postfix packages
apt:
name: "{{ packages }}"
state: latest
update_cache: yes
vars:
packages:
- postfix
- postfix-mysql
- postfix-pcre
- postfix-sqlite
- name: copy postfix main config file
template:
src: postfix/main.j2
dest: /etc/postfix/main.cf
notify:
- restart postfix
- name: copy postfix master config file
template:
src: postfix/master.j2
dest: /etc/postfix/master.cf
notify:
- restart postfix
- name: create /etc/postfix/mysql/
file:
path: /etc/postfix/mysql/
state: directory
owner: root
group: root
mode: 0775
- name: copy postfix virtual alias maps config file
template:
src: postfix/postfix-mysql-virtual_alias_maps.j2
dest: /etc/postfix/mysql/postfix-mysql-virtual_alias_maps.cf
notify:
- restart postfix
- name: copy postfix virtual domains maps config file
template:
src: postfix/postfix-mysql-virtual_domains_maps.j2
dest: /etc/postfix/mysql/postfix-mysql-virtual_domains_maps.cf
notify:
- restart postfix
- name: copy postfix virtual mailbox maps config file
template:
src: postfix/postfix-mysql-virtual_mailbox_maps.j2
dest: /etc/postfix/mysql/postfix-mysql-virtual_mailbox_maps.cf
notify:
- restart postfix
- name: copy postfix virtual transport maps config file
template:
src: postfix/postfix-mysql-virtual_transport_maps.j2
dest: /etc/postfix/mysql/postfix-mysql-virtual_transport_maps.cf
notify:
- restart postfix
Amavis
To scan our emails for viruses and other malicious files we need an anti virus solution. Amavis uses ClamAV to achieve this goal. Also we use Spamassassin to analyze emails and block spam. A wide variety of DNS based block lists are used to help spam blocking.
roles/mailserver/tasks/amavis.yml
:
---
- name: update the repository cache and install amavis packages
apt:
name: "{{ packages }}"
state: latest
update_cache: yes
vars:
packages:
- amavisd-new
- libdbi-perl
- spamassassin
- libdbd-mysql-perl
notify:
- restart amavis
- name: create amavis config file
template:
src: amavis/amavis.j2
dest: /etc/amavis/conf.d/50-user
notify:
- restart amavis
- name: create dkim keys
command: amavisd-new genrsa /var/lib/amavis/db/dkim_{{ item }}.key 2048
args:
creates: /var/lib/amavis/db/dkim_{{ item }}.key
with_list: "{{ DKIM_DOMAINS }}"
ClamAV
As already mentioned, Amavis uses ClamAV to scan our mails after viruses, so we need to setup that tool too.
roles/mailserver/tasks/clamav.yml
:
---
- name: Update the repository cache and install ClamAV packages
apt:
name: "{{ packages }}"
state: latest
update_cache: yes
vars:
packages:
- clamav
- clamav-base
- clamav-daemon
- clamav-freshclam
notify:
- restart clamav-daemon
- restart clamav-freshclam
- name: Füge ClamAV User zur Amavis Gruppe hinzu
user:
name: clamav
group: amavis
append: yes
notify:
- restart clamav-daemon
- restart clamav-freshclam
- name: Copy Clam Daemon Config File
template:
src: clamav/clamd.j2
dest: /etc/clamav/clamd.conf
notify:
- restart clamav-daemon
- name: Copy Clam Freshclam Config File
template:
src: clamav/freshclam.j2
dest: /etc/clamav/freshclam.conf
notify:
- restart clamav-freshclam
Lets Encrypt Certbot
To secure the client connections and send mails to TLS enabled mailservers we need valid ssl certificates. Lets Encrypt offers them for free and in a very automated way. Since we don't have a webserver installed, we use the standalone method.
As you may see we don't use the ansible module (because I don't like it), so there is a simple bash script which will be explained in the templates section.
roles/mailserver/tasks/certbot.yml
:
---
- name: Update the repository cache and install certbot
apt:
name: certbot
state: latest
update_cache: yes
- name: Copy Create Certificate Script
template:
src: certbot/create-certificate.j2
dest: /etc/letsencrypt/certificate.bash
mode: 0755
- name: Install Certificates
command: /etc/letsencrypt/certificate.bash
args:
creates: /etc/letsencrypt/live/{{ mail_hostname }}
- name: Create Renew Cronjob
cron:
name: "Renew Certificates"
special_time: daily
job: "certbot renew -n -q --agree-tos --email {{ CERTBOT_EMAIL }} --standalone && systemctl restart postfix dovecot"
Templates
In ansible we don't use config files, but templates which represents them.
Dovecot
For dovecot we use a general config file which includes the whole configuration for mailbox handling.
roles/mailserver/templates/dovecot/dovecot.j2
:
# {{ ansible_managed }}
auth_mechanisms = plain login
listen = *
log_timestamp = "%Y-%m-%d %H:%M:%S "
login_log_format_elements = user= method=%m rip=%r lip=%l mpid=%e %c %k
mail_gid = vmail
mail_home = /var/vmail/%d/%n
mail_location = maildir:~/Maildir:LAYOUT=fs
mail_plugins = quota acl mail_log notify
mail_uid = vmail
managesieve_notify_capability = mailto
managesieve_sieve_capability = fileinto reject envelope encoded-character vacation subaddress comparator-i;ascii-numeric relational regex imap4flags copy include variables body enotify environment mailbox date index ihave duplicate mime foreverypart extracttext
namespace {
list = yes
location = maildir:%%h/Maildir:LAYOUT=fs:INDEXPVT=~/Maildir/Shared/%%u
prefix = Shared/%%u/
separator = /
subscriptions = yes
type = shared
}
namespace inbox {
inbox = yes
location =
mailbox Archiv {
special_use = \Archive
}
mailbox Archive {
auto = subscribe
special_use = \Archive
}
mailbox "Deleted Messages" {
special_use = \Trash
}
mailbox Drafts {
auto = subscribe
special_use = \Drafts
}
mailbox Entwürfe {
special_use = \Drafts
}
mailbox "Gelöschte Objekte" {
special_use = \Trash
}
mailbox Gesendet {
special_use = \Sent
}
mailbox Junk {
auto = subscribe
special_use = \Junk
}
mailbox Papierkorb {
special_use = \Trash
}
mailbox Sent {
auto = subscribe
special_use = \Sent
}
mailbox "Sent Messages" {
special_use = \Sent
}
mailbox Trash {
auto = subscribe
special_use = \Trash
}
prefix =
separator = /
}
passdb {
args = /etc/dovecot/dovecot-mysql.conf
driver = sql
}
plugin {
acl = vfile
acl_anyone = allow
acl_shared_dict = file:/var/vmail/shared-mailboxes.db
mail_log_events = delete undelete expunge
quota = maildir:User quota
quota_rule = Sent:storage=+10%%
quota_status_nouser = DUNNO
quota_status_overquota = 552 5.2.2 Mailbox is over quota
quota_status_success = DUNNO
sieve = ~/sieve/dovecot.sieve
sieve_before = /var/vmail/before.sieve
sieve_dir = ~/sieve
sieve_max_script_size = 1M
sieve_quota_max_scripts = 0
sieve_quota_max_storage = 0
}
protocols = imap sieve lmtp
service auth {
unix_listener /var/spool/postfix/private/auth_dovecot {
group = postfix
mode = 0660
user = postfix
}
unix_listener auth-master {
mode = 0600
user = vmail
}
unix_listener auth-userdb {
mode = 0600
user = vmail
}
user = root
}
service dict {
unix_listener dict {
group = dovecot
mode = 0660
user = vmail
}
}
service lmtp {
unix_listener /var/spool/postfix/private/dovecot-lmtp {
group = postfix
mode = 0600
user = postfix
}
user = vmail
}
service managesieve-login {
inet_listener sieve {
address = {{ private_ip }}
port = 4190
}
process_min_avail = 2
service_count = 1
vsz_limit = 128 M
}
service managesieve {
process_limit = 256
}
service quota-status {
client_limit = 1
executable = quota-status -p postfix
unix_listener /var/spool/postfix/private/quota-status {
group = postfix
mode = 0660
user = postfix
}
}
ssl_ca = D_REJECT,
final_bad_header_destiny => D_PASS,
final_spam_destiny => D_PASS,
terminate_dsn_on_notify_success => 0,
warnbadhsender => 1,
};
# "mail.domain.tld" bitte anpassen
$myhostname = "{{ mail_hostname }}";
# Wer wird über Viren, Spam und "bad header mails" informiert?
# Den Benutzer "postmaster" bitte nachträglich in ViMbAdmin erstellen (Alias möglich)
$virus_admin = "postmaster\@$mydomain";
$spam_admin = "postmaster\@$mydomain";
$banned_quarantine_to = "postmaster\@$mydomain";
$bad_header_quarantine_to = "postmaster\@$mydomain";
# DKIM kann verifiziert werden.
$enable_dkim_verification = 1;
# AR-Header darf gesetzt werden
$allowed_added_header_fields{lc('Authentication-Results')} = 1;
# DKIM-Signatur
# Gilt nur, wenn "originating = 1", ergo für die SUBMISSION policy bank
# "default" ist hierbei der Selector
# "domain.tld" als Domäne bitte anpassen
# "enable_dkim_signing" nur "1" setzen, wenn Mails wirklich signiert werden sollen.
# "/var/lib/amavis/db/dkim_domain.tld.key" sollte ebenso dem Namen der Domäne angepasst werden.
# Die TTL beträgt im Beispiel 7 Tage
# relaxed/relaxed beschreibt die Header/Body canonicalization, relaxed ist weniger restriktiv
$enable_dkim_signing = 1;
# BEGIN AUTO GENERATED DKIM CONFIGURATION
{% for DOMAIN in DKIM_DOMAINS %}
dkim_key('{{ DOMAIN }}', 'default', '/var/lib/amavis/db/dkim_{{ DOMAIN }}.key');
{% endfor %}
# END AUTO GENERATED DKIM CONFIGURATION
@dkim_signature_options_bysender_maps = (
{ '.' =>
{
ttl => 7*24*3600,
c => 'relaxed/relaxed'
}
}
);
# Viren- und Spamfilter ACL; werden automatisch ermittelt
@bypass_virus_checks_maps = (
\%bypass_virus_checks, \@bypass_virus_checks_acl, \$bypass_virus_checks_re);
@bypass_spam_checks_maps = (
\%bypass_spam_checks, \@bypass_spam_checks_acl, \$bypass_spam_checks_re);
#------------ Do not modify anything below this line -------------
1; # ensure a defined return
ClamAV
For ClamAV we need to configure the ClamAV Daemon
roles/mailserver/templates/clamav/clamd.j2
:
# {{ ansible_managed }}
#To reconfigure clamd run #dpkg-reconfigure clamav-daemon
LocalSocket /var/run/clamav/clamd.ctl
FixStaleSocket true
LocalSocketGroup amavis
LocalSocketMode 666
# TemporaryDirectory is not set to its default /tmp here to make overriding
# the default with environment variables TMPDIR/TMP/TEMP possible
User clamav
ScanMail true
ScanArchive true
ArchiveBlockEncrypted false
MaxDirectoryRecursion 15
FollowDirectorySymlinks false
FollowFileSymlinks false
ReadTimeout 180
MaxThreads 12
MaxConnectionQueueLength 15
LogSyslog false
LogRotate true
LogFacility LOG_LOCAL6
LogClean false
LogVerbose false
PreludeEnable no
PreludeAnalyzerName ClamAV
DatabaseDirectory /var/lib/clamav
OfficialDatabaseOnly false
SelfCheck 3600
Foreground false
Debug false
ScanPE true
MaxEmbeddedPE 10M
ScanOLE2 true
ScanPDF true
ScanHTML true
MaxHTMLNormalize 10M
MaxHTMLNoTags 2M
MaxScriptNormalize 5M
MaxZipTypeRcg 1M
ScanSWF true
ExitOnOOM false
LeaveTemporaryFiles false
AlgorithmicDetection true
ScanELF true
IdleTimeout 30
CrossFilesystems true
PhishingSignatures true
PhishingScanURLs true
PhishingAlwaysBlockSSLMismatch false
PhishingAlwaysBlockCloak false
PartitionIntersection false
DetectPUA false
ScanPartialMessages false
HeuristicScanPrecedence false
StructuredDataDetection false
CommandReadTimeout 30
SendBufTimeout 200
MaxQueue 100
ExtendedDetectionInfo true
OLE2BlockMacros false
ScanOnAccess false
AllowAllMatchScan true
ForceToDisk false
DisableCertCheck false
DisableCache false
MaxScanTime 120000
MaxScanSize 100M
MaxFileSize 25M
MaxRecursion 16
MaxFiles 10000
MaxPartitions 50
MaxIconsPE 100
PCREMatchLimit 10000
PCRERecMatchLimit 5000
PCREMaxFileSize 25M
ScanXMLDOCS true
ScanHWP3 true
MaxRecHWP3 16
StreamMaxLength 25M
LogFile /var/log/clamav/clamav.log
LogTime true
LogFileUnlock false
LogFileMaxSize 0
Bytecode true
BytecodeSecurity TrustSigned
BytecodeTimeout 60000
and Freshclam to get updated virus databases
roles/mailserver/templates/clamav/freshclam.j2
:
# {{ ansible_managed }}
DatabaseOwner clamav
UpdateLogFile /var/log/clamav/freshclam.log
LogVerbose false
LogSyslog false
LogFacility LOG_LOCAL6
LogFileMaxSize 0
LogRotate true
LogTime true
Foreground false
Debug false
MaxAttempts 5
DatabaseDirectory /var/lib/clamav
DNSDatabaseInfo current.cvd.clamav.net
ConnectTimeout 30
ReceiveTimeout 30
TestDatabases yes
ScriptedUpdates yes
CompressLocalDatabase no
SafeBrowsing false
Bytecode true
NotifyClamd /etc/clamav/clamd.conf
# Check for new database 24 times a day
Checks 24
DatabaseMirror db.local.clamav.net
DatabaseMirror database.clamav.net
Certbot
Certbot initializes it's configuration on first run, so we use this template to generate a bash script which we use to issue a certificate with all of our alias domains included.
roles/mailserver/templates/certbot/create-certificate.j2
:
#!/usr/bin/env bash
certbot certonly \
-d {{ mail_hostname }} \
--cert-name {{ mail_hostname }} \
{% if ALIAS_DOMAINS is defined %}
{% for DOMAIN in ALIAS_DOMAINS %}
-d {{ DOMAIN }} \
{% endfor %}
{% endif %}
-n \
--agree-tos \
--standalone \
--email {{ CERTBOT_EMAIL }}
Execution
So our whole role is finished. Now we need to execute our playbook:
ansible-playbook mailservers.yml