From 30760f2feed7216c75b32c48bbe0a9362cf50179 Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
<41898282+github-actions[bot]@users.noreply.github.com>
Date: Sun, 5 Jan 2025 20:02:28 +0000
Subject: [PATCH] deploy: e6d519b6f82f1d3a1fada4b24edaed6f64f5d3f0
---
edge/config/security/ssl/index.html | 30 ++++++--
edge/search/search_index.json | 2 +-
edge/sitemap.xml | 102 ++++++++++++++--------------
3 files changed, 76 insertions(+), 58 deletions(-)
diff --git a/edge/config/security/ssl/index.html b/edge/config/security/ssl/index.html
index 04497005..ed0f9771 100644
--- a/edge/config/security/ssl/index.html
+++ b/edge/config/security/ssl/index.html
@@ -3067,6 +3067,7 @@ docker run --detachCaddy is an open-source web server with built-in TLS certificate generation. You can use the official Docker image and write your own Caddyfile
.
Example
+While DMS does not need a webserver to work, this workaround will provision a TLS certificate for DMS to use by adding a dummy site block to trigger cert provisioning.
services:
# Basic Caddy service to provision certs:
reverse-proxy:
@@ -3090,8 +3091,10 @@ docker run --detach - ${CADDY_DATA_DIR}/certificates/acme-v02.api.letsencrypt.org-directory/mail.example.com/mail.example.com.crt:/etc/letsencrypt/live/mail.example.com/fullchain.pem
- ${CADDY_DATA_DIR}/certificates/acme-v02.api.letsencrypt.org-directory/mail.example.com/mail.example.com.key:/etc/letsencrypt/live/mail.example.com/privkey.pem
An explicit entry in your Caddyfile
config will have Caddy provision and renew a certificate for your DMS FQDN:
mail.example.com {
- tls internal {
+ # Optionally provision RSA 2048-bit certificate instead of ECDSA P-256:
+ tls {
key_type rsa2048
}
@@ -3100,12 +3103,15 @@ docker run --detach
While DMS does not need a webserver to work, this workaround will provision a TLS certificate for DMS to use.
+Info
+An explicit tls
directive affects only the site-address block it's used in:
tls internal
will create a local self-signed cert for testing. This targets only the site-address, unlike the global local_certs
option.key_type
can be used in the tls
block if you need to enforce RSA as the key type for certificates provisioned. The default is currently ECDSA (P-256).tls internal { ... }
if wanting to create a local self-signed cert, which may be useful for testing. This allows opt-in to use self-signed certs unlike the global local_certs
option.key_type
can be used in the tls
block if you need to enforce RSA as the key type for certificates provisioned. The default is currently ECDSA (P-256). This may improve compatibility with legacy clients.caddy-docker-proxy
Using lucaslorentz/caddy-docker-proxy
allows you to generate a Caddyfile
by adding labels to your services in compose.yaml
:
The path contains the certificate provisioner used. This path may be different from the example above for you and may change over time when multiple ACME provisioner services are used.
This can make the volume mounting for DMS to find the certificates non-deterministic, but you can restrict provisioning to single service via the acme_ca
setting.
NOTE: Bind mounting a file directly instead of a directory will mount by inode. If the file is updated at renewal and this modifies the inode on the host system, then the container will still point to the old certificate.
+If this happens, consider using our manual TLS type instead:
+services:
+ mailserver:
+ environment:
+ SSL_TYPE: manual
+ SSL_CERT_PATH: /srv/tls/mail.example.com/mail.example.com.crt
+ SSL_KEY_PATH: /srv/tls/mail.example.com/mail.example.com.key
+ volumes:
+ - ${CADDY_DATA_DIR}/certificates/acme-v02.api.letsencrypt.org-directory/mail.example.com/:/srv/tls/mail.example.com/:ro
+
Traefik is an open-source application proxy using the ACME protocol. Traefik can request certificates for domains and subdomains, and it will take care of renewals, challenge negotiations, etc.
diff --git a/edge/search/search_index.json b/edge/search/search_index.json index 18c7b091..87ef1e40 100644 --- a/edge/search/search_index.json +++ b/edge/search/search_index.json @@ -1 +1 @@ -{"config":{"lang":["en"],"separator":"[\\s\\-]+","pipeline":["stopWordFilter"]},"docs":[{"location":"","title":"Welcome to the Documentation fordocker-mailserver
!","text":"This Documentation is Versioned
Make sure to select the correct version of this documentation! It should match the version of the image you are using. The default version corresponds to the :latest
image tag - the most recent stable release.
This documentation provides you not only with the basic setup and configuration of DMS but also with advanced configuration, elaborate usage scenarios, detailed examples, hints and more.
"},{"location":"#about","title":"About","text":"docker-mailserver
, or DMS for short, is a production-ready fullstack but simple mail server (SMTP, IMAP, LDAP, Anti-spam, Anti-virus, etc.). It employs only configuration files, no SQL database. The image is focused around the slogan \"Keep it simple and versioned\".
If you're completely new to mail servers or you want to read up on them, check out our Introduction page. If you're new to DMS as a mail server appliance, make sure to read the Usage chapter first. If you want to look at examples for Docker Compose, we have an Examples page.
There is also a script - setup.sh
- supplied with this project. It supports you in configuring and administrating your server. Information on how to get it and how to use it is available on a dedicated page.
We have a dedicated configuration page. It contains most of the configuration and explanation you need to setup your mail server properly. Be aware that advanced tasks may still require reading through all parts of this documentation; it may also involve inspecting your running container for debugging purposes. After all, a mail server is a complex arrangement of various programs.
Important
If you'd like to change, patch or alter files or behavior of DMS, you can use a script. Just place a script called user-patches.sh
in your ./docker-data/dms/config/
folder volume (which is mounted to /tmp/docker-mailserver/
inside the container) and it will be run on container startup. See the 'Modifications via Script' page for additional documentation and an example.
You might also want to check out:
Tip
Definitely check out the FAQ for more information and tips! Please do not open an issue before you have checked our documentation for answers, including the FAQ!
"},{"location":"#tests","title":"Tests","text":"DMS employs a variety of tests. If you want to know more about our test suite, view our testing docs.
"},{"location":"#contributing","title":"Contributing","text":"We are always happy to welcome new contributors. For guidelines and entrypoints please have a look at the Contributing section.
"},{"location":"faq/","title":"FAQ","text":""},{"location":"faq/#what-kind-of-database-are-you-using","title":"What kind of database are you using?","text":"None! No database is required. The filesystem is the database. This image is based on config files that can be persisted using bind mounts (default) or Docker volumes, and as such versioned, backed up and so forth.
"},{"location":"faq/#where-are-emails-stored","title":"Where are emails stored?","text":"Mails are stored in /var/mail/${domain}/${username}
. Since v9.0.0
it is possible to add custom user_attributes
for each accounts to have a different mailbox configuration (See #1792).
INBOX
is setup by default with the special IMAP folders Drafts
, Sent
, Junk
and Trash
. You can learn how to modify or add your own folders (including additional special folders like Archive
) by visiting our docs page Customizing IMAP Folders for more information.
Make sure to read the CHANGELOG before updating to new versions, to be prepared for possible breaking changes.
Then, run the following commands:
docker compose pull\ndocker compose down\ndocker compose up -d\n
You should see the new version number on startup, for example: [ INF ] Welcome to docker-mailserver 11.3.1
. And you're done! Don't forget to have a look at the remaining functions of the setup.sh
script with ./setup.sh help
.
As you'll realistically be deploying to production on a Linux host, if you are on Windows or macOS and want to run the image locally first, it's advised to do so via a VM guest running Linux if you have issues running DMS on your host system.
"},{"location":"faq/#what-are-the-system-requirements","title":"What are the system requirements?","text":""},{"location":"faq/#recommended","title":"Recommended","text":"Warning
ClamAV can consume a lot of memory, as it reads the entire signature database into RAM.
Current figure is about 850M and growing. If you get errors about ClamAV or amavis failing to allocate memory you need more RAM or more swap and of course docker must be allowed to use swap (not always the case). If you can't use swap at all you may need 3G RAM.
"},{"location":"faq/#how-to-alter-a-running-dms-instance-without-relaunching-the-container","title":"How to alter a running DMS instance without relaunching the container?","text":"DMS aggregates multiple \"sub-services\", such as Postfix, Dovecot, Fail2ban, SpamAssassin, etc. In many cases, one may edit a sub-service's config and reload that very sub-service, without stopping and relaunching the whole mail server.
In order to do so, you'll probably want to push your config updates to your server through a Docker volume (these docs use: ./docker-data/dms/config/:/tmp/docker-mailserver/
), then restart the sub-service to apply your changes, using supervisorctl
. For instance, after editing fail2ban's config: supervisorctl restart fail2ban
.
See the documentation for supervisorctl
.
Tip
To add, update or delete an email account; there is no need to restart postfix / dovecot service inside the container after using setup.sh
script.
For more information, see #1639.
"},{"location":"faq/#how-can-i-sync-the-container-and-host-datetime","title":"How can I sync the container and host date/time?","text":"Share the host's /etc/localtime
with the container, e.g. by using a bind mount:
volumes:\n - /etc/localtime:/etc/localtime:ro\n
Optionally, you can set the TZ
ENV variable; e.g. TZ=Europe/Berlin
. Check this list for which values are allowed.
Properly working DNS servers are crucial for differentiating spam from legitimate e-mails. Records like SPF
, DKIM
and DMARC
records, as well as working name (resolving A
records) and reverse name (resolving PTR
records) resolution ensures legitimate e-mails arrive while e-mails that are more likely phishing and spam do not.
Anti-spam measures (like SpamAssassin or Rspamd) make use of DNS block lists. To learn more check out our Rspamd documentation on this topic. In case you want to utilize RBL/DNSBLs, you need a recursive DNS resolver (not big custom resolvers like Cloudflare, Quad9, Google, etc.).
DMS does not integrate support for an internal DNS service as this is a responsibility that is sensitive to the host environment. You can configure internal services within DMS to use your own managed DNS server, or configure for such at the host or container level (such as with compose.yaml
).
All files are using the Unix format with LF
line endings. Please do not use CRLF
.
DMS supports multiple domains out of the box, so you can do this:
./setup.sh email add user1@example.com\n./setup.sh email add user1@example.de\n./setup.sh email add user1@server.example.org\n
"},{"location":"faq/#what-about-backups","title":"What about backups?","text":""},{"location":"faq/#bind-mounts-default","title":"Bind mounts (default)","text":"From the location of your compose.yaml
, create a compressed archive of your docker-data/dms/config/
and docker-data/dms/mail-*
folders:
tar --gzip -cf \"backup-$(date +%F).tar.gz\" ./docker-data/dms\n
Then to restore docker-data/dms/config/
and docker-data/dms/mail-*
folders from your backup file:
tar --gzip -xf backup-date.tar.gz\n
"},{"location":"faq/#volumes","title":"Volumes","text":"Assuming that you use docker-compose
and data volumes, you can backup the configuration, emails and logs like this:
# create backup\ndocker run --rm -it \\\n -v \"${PWD}/docker-data/dms/config/:/tmp/docker-mailserver/\" \\\n -v \"${PWD}/docker-data/dms-backups/:/backup/\" \\\n --volumes-from mailserver \\\n alpine:latest \\\n tar czf \"/backup/mail-$(date +%F).tar.gz\" /var/mail /var/mail-state /var/log/mail /tmp/docker-mailserver\n\n# delete backups older than 30 days\nfind \"${PWD}/docker-data/dms-backups/\" -type f -mtime +30 -delete\n
"},{"location":"faq/#i-want-to-know-more-about-the-ports","title":"I Want to Know More About the Ports","text":"See this part of the documentation for further details and best practice advice, especially regarding security concerns.
"},{"location":"faq/#how-can-i-configure-my-email-client","title":"How can I configure my email client?","text":"Login is full email address (<user>@<domain>
).
# IMAP\nusername: <user1@example.com>\npassword: <mypassword>\nserver: <mail.example.com>\nimap port: 143 or 993 with STARTTLS/SSL (recommended)\nimap path prefix: INBOX\n\n# SMTP\nsmtp port: 587 or 465 with STARTTLS/SSL (recommended)\nusername: <user1@example.com>\npassword: <mypassword>\n
DMS is properly configured for port 587, if possible, we recommend using port 465 for SMTP though. See this section to learn more about ports.
"},{"location":"faq/#can-i-use-a-nakedbare-domain-ie-no-hostname","title":"Can I use a naked/bare domain (i.e. no hostname)?","text":"Yes, but not without some configuration changes. Normally it is assumed that DMS runs on a host with a name, so the fully qualified host name might be mail.example.com
with the domain example.com
. The MX records point to mail.example.com
.
To use a bare domain (where the host name is example.com
and the domain is also example.com
), change mydestination
:
mydestination = $myhostname, localhost.$mydomain, localhost
mydestination = localhost.$mydomain, localhost
Add the latter line to docker-data/dms/config/postfix-main.cf
. If that doesn't work, make sure that OVERRIDE_HOSTNAME
is blank in your mailserver.env
file. Without these changes there will be warnings in the logs like:
warning: do not list domain example.com in BOTH mydestination and virtual_mailbox_domains\n
Plus of course mail delivery fails.
Also you need to define hostname: example.com
in your compose.yaml
.
You might not want a bare domain
We encourage you to consider using a subdomain where possible.
user@example.com
, that is distinct from your hostname which is identified by a DNS MX record.Considering you want to redirect all incoming e-mails for the domain example.com
to user1@example.com
, add the following line to docker-data/dms/config/postfix-virtual.cf
:
@example.com user1@example.com\n
"},{"location":"faq/#how-can-i-delete-all-the-emails-for-a-specific-user","title":"How can I delete all the emails for a specific user?","text":"First of all, create a special alias named devnull
by editing docker-data/dms/config/postfix-aliases.cf
:
devnull: /dev/null\n
Considering you want to delete all the e-mails received for baduser@example.com
, add the following line to docker-data/dms/config/postfix-virtual.cf
:
baduser@example.com devnull\n
Important
If you use a catch-all rule for the main/sub domain, you need another entry in docker-data/dms/config/postfix-virtual.cf
:
@mail.example.com hello@example.com\nbaduser@example.com devnull\ndevnull@mail.example.com devnull\n
"},{"location":"faq/#what-kind-of-ssl-certificates-can-i-use","title":"What kind of SSL certificates can I use?","text":"Both RSA and ECDSA certs are supported. You can provide your own cert files manually, or mount a letsencrypt
generated directory (with alternative support for Traefik's acme.json
). Check out the SSL_TYPE
documentation for more details.
If this migration implies a DNS modification, be sure to wait for DNS propagation before opening an issue. Few examples of symptoms can be found here or here.
This could be related to a modification of your MX
record, or the IP mapped to mail.example.com
. Additionally, validate your DNS configuration.
If everything is OK regarding DNS, please provide formatted logs and config files. This will allow us to help you.
If we're blind, we won't be able to do anything.
"},{"location":"faq/#connection-refused-or-no-response-at-all","title":"Connection refused or No response at all","text":"You see errors like \"Connection Refused\" and \"Connection closed by foreign host\", or you cannot connect at all? You may not be able to connect with your mail client (MUA)? Make sure to check Fail2Ban did not ban you (for exceeding the number of tried logins for example)! You can run
docker exec <CONTAINER NAME> setup fail2ban\n
and check whether your IP address appears. Use
docker exec <CONTAINER NAME> setup fail2ban unban <YOUR IP>\n
to unban the IP address.
"},{"location":"faq/#how-can-i-authenticate-users-with-smtp_only1","title":"How can I authenticate users withSMTP_ONLY=1
?","text":"See #1247 for an example.
Todo
Write a How-to / Use-Case / Tutorial about authentication with SMTP_ONLY
.
hostname
","text":"Normally you will assign DMS a hostname
such as mail.example.com
. If you instead use a bare domain (such as example.com
) or add an alias / account with the same value as your hostname
, this can cause a conflict for mail addressed to @hostname
as Postfix gets confused where to deliver the mail (hostname
is configured for only system accounts via the Postfix main.cf
setting mydestination
).
When this conflict is detected you'll find logs similar to this:
warning: do not list domain mail.example.com in BOTH mydestination and virtual_mailbox_domains\n...\nNOQUEUE: reject: RCPT from HOST[IP]: 550 5.1.1 <RECIPIENT>: Recipient address rejected: User unknown in local recipient table; ...\n
Opt-out of mail being directed to services by excluding $myhostname
as a destination with a postfix-main.cf
override config:
mydestination = localhost.$mydomain, localhost\n
Tip
You may want to configure a postmaster
alias via setup alias add
to receive system notifications.
Warning
Internal mail destined for root
, amavis
or other accounts will now no longer be received without an alias or account created for them.
Using user-patches.sh
, update the container file /etc/postfix/main.cf
to include:
proxy_interfaces = X.X.X.X (your public IP)\n
For reverse proxy support you will want to view our dedicated guide.
"},{"location":"faq/#how-to-restrict-login-by-ip","title":"How to restrict login by IP?","text":"There are a few ways you could approach this, see this discussion answer for advice.
"},{"location":"faq/#how-to-adjust-settings-with-the-user-patchessh-script","title":"How to adjust settings with theuser-patches.sh
script","text":"Suppose you want to change a number of settings that are not listed as variables or add things to the server that are not included?
DMS has a built-in way to do post-install processes. If you place a script called user-patches.sh
in the config directory it will be run after all configuration files are set up, but before the postfix, amavis and other daemons are started.
It is common to use a local directory for config added to docker-mailsever
via a volume mount in your compose.yaml
(eg: ./docker-data/dms/config/:/tmp/docker-mailserver/
).
Add or create the script file to your config directory:
cd ./docker-data/dms/config\ntouch user-patches.sh\nchmod +x user-patches.sh\n
Then fill user-patches.sh
with suitable code.
If you want to test it you can move into the running container, run it and see if it does what you want. For instance:
# start shell in container\n./setup.sh debug login\n\n# check the file\ncat /tmp/docker-mailserver/user-patches.sh\n\n# run the script\n/tmp/docker-mailserver/user-patches.sh\n\n# exit the container shell back to the host shell\nexit\n
You can do a lot of things with such a script. You can find an example user-patches.sh
script here: example user-patches.sh
script.
We also have a very similar docs page specifically about this feature!
Special use-case - patching the supervisord
configuration
It seems worth noting, that the user-patches.sh
gets executed through supervisord
. If you need to patch some supervisord config (e.g. /etc/supervisor/conf.d/saslauth.conf
), the patching happens too late.
An easy workaround is to make the user-patches.sh
reload the supervisord config after patching it:
#!/bin/bash\nsed -i 's/rimap -r/rimap/' /etc/supervisor/conf.d/saslauth.conf\nsupervisorctl update\n
"},{"location":"faq/#how-to-ban-custom-ip-addresses-with-fail2ban","title":"How to ban custom IP addresses with Fail2ban","text":"Use the following command:
./setup.sh fail2ban ban <IP>\n
The default bantime is 180 days. This value can be customized.
"},{"location":"faq/#what-to-do-in-case-of-spfforwarding-problems","title":"What to do in case of SPF/Forwarding problems","text":"If you got any problems with SPF and/or forwarding mails, give SRS a try. You enable SRS by setting ENABLE_SRS=1
. See the variable description for further information.
There are many reasons why email might be rejected, common causes are:
DMS does not manage those concerns, verify they are not causing your delivery problems before reporting a bug on our issue tracker. Resources that can help you troubleshoot:
Anti-spam rules are managed in docker-data/dms/config/spamassassin-rules.cf
.
x-headers
not inserted into my subdomain.example.com
subdomain emails?","text":"In the default setup, amavis only applies SpamAssassin x-headers into domains matching the template listed in the config file (05-domain_id
in the amavis defaults).
The default setup @local_domains_acl = ( \".$mydomain\" );
does not match subdomains. To match subdomains, you can override the @local_domains_acl
directive in the amavis user config file 50-user
with @local_domains_maps = (\".\");
to match any sort of domain template.
Put received spams in .Junk/
imap folder using SPAMASSASSIN_SPAM_TO_INBOX=1
and MOVE_SPAM_TO_JUNK=1
and add a user cron like the following:
Example
NOTE: This example assumes you have a /var/mail-state
volume mounted.
# m h dom mon dow command\n# Everyday 2:00AM, learn spam from a specific user\n0 2 * * * docker exec mailserver sa-learn --spam /var/mail/example.com/username/.Junk --dbpath /var/mail-state/lib-amavis/.spamassassin\n
With docker-compose
you can more easily use the internal instance of cron
within DMS. This is less problematic than the simple solution shown above, because it decouples the learning from the host on which DMS is running, and avoids errors if the mail server is not running.
The following configuration works nicely:
ExampleNOTE: This example assumes you have a /var/mail-state
volume mounted.
Create a system cron file:
# in the compose.yaml root directory\nmkdir -p ./docker-data/dms/cron\ntouch ./docker-data/dms/cron/sa-learn\nchown root:root ./docker-data/dms/cron/sa-learn\nchmod 0644 ./docker-data/dms/cron/sa-learn\n
Edit the system cron file nano ./docker-data/dms/cron/sa-learn
, and set an appropriate configuration:
# '> /dev/null' to send error notifications from 'stderr' to 'postmaster@example.com'\n#\n# m h dom mon dow user command\n#\n# Everyday 2:00AM, learn spam from a specific user\n# spam: junk directory\n0 2 * * * root sa-learn --spam /var/mail/example.com/username/.Junk --dbpath /var/mail-state/lib-amavis/.spamassassin > /dev/null\n# ham: archive directories\n15 2 * * * root sa-learn --ham /var/mail/example.com/username/.Archive* --dbpath /var/mail-state/lib-amavis/.spamassassin > /dev/null\n# ham: inbox subdirectories\n30 2 * * * root sa-learn --ham /var/mail/example.com/username/cur* --dbpath /var/mail-state/lib-amavis/.spamassassin > /dev/null\n#\n# Everyday 3:00AM, learn spam from all users of a domain\n# spam: junk directory\n0 3 * * * root sa-learn --spam /var/mail/not-example.com/*/.Junk --dbpath /var/mail-state/lib-amavis/.spamassassin > /dev/null\n# ham: archive directories\n15 3 * * * root sa-learn --ham /var/mail/not-example.com/*/.Archive* --dbpath /var/mail-state/lib-amavis/.spamassassin > /dev/null\n# ham: inbox subdirectories\n30 3 * * * root sa-learn --ham /var/mail/not-example.com/*/cur* --dbpath /var/mail-state/lib-amavis/.spamassassin > /dev/null\n
Then with compose.yaml
:
services:\n mailserver:\n image: ghcr.io/docker-mailserver/docker-mailserver:latest\n volumes:\n - ./docker-data/dms/cron/sa-learn:/etc/cron.d/sa-learn\n
Or with Docker Swarm:
services:\n mailserver:\n image: ghcr.io/docker-mailserver/docker-mailserver:latest\n # ...\n configs:\n - source: my_sa_crontab\n target: /etc/cron.d/sa-learn\n\nconfigs:\n my_sa_crontab:\n file: ./docker-data/dms/cron/sa-learn\n
With the default settings, SpamAssassin will require 200 mails trained for spam (for example with the method explained above) and 200 mails trained for ham (using the same command as above but using --ham
and providing it with some ham mails).
This is related to Amavis processing the mail after SpamAssassin has analyzed it and assigned a spam score.
docker-data/dms/config/amavis.cf
.SPAM and INFECTED emails that reach the SA_KILL
threshold are archived into quarantine.
Instead of a quarantine folder, you can use a dedicated mailbox instead. Create an account like quarantine@example.com
and create docker-data/dms/config/amavis.cf
:
$clean_quarantine_to = \"quarantine\\@example.com\";\n$virus_quarantine_to = \"quarantine\\@example.com\";\n$banned_quarantine_to = \"quarantine\\@example.com\";\n$bad_header_quarantine_to = \"quarantine\\@example.com\";\n$spam_quarantine_to = \"quarantine\\@example.com\";\n
"},{"location":"introduction/","title":"An Overview of Mail Server Infrastructure","text":"This article answers the question \"What is a mail server, and how does it perform its duty?\" and it gives the reader an introduction to the field that covers everything you need to know to get started with DMS.
"},{"location":"introduction/#the-anatomy-of-a-mail-server","title":"The Anatomy of a Mail Server","text":"A mail server is only a part of a client-server relationship aimed at exchanging information in the form of emails. Exchanging emails requires using specific means (programs and protocols).
DMS provides you with the server portion, whereas the client can be anything from a terminal via text-based software (eg. Mutt) to a fully-fledged desktop application (eg. Mozilla Thunderbird, Microsoft Outlook\u2026), to a web interface, etc.
Unlike the client-side where usually a single program is used to perform retrieval and viewing of emails, the server-side is composed of many specialized components. The mail server is capable of accepting, forwarding, delivering, storing and overall exchanging messages, but each one of those tasks is actually handled by a specific piece of software. All of these \"agents\" must be integrated with one another for the exchange to take place.
DMS has made informed choices about those components and their (default) configuration. It offers a comprehensive platform to run a fully featured mail server in no time!
"},{"location":"introduction/#components","title":"Components","text":"The following components are required to create a complete delivery chain:
Here's a schematic view of mail delivery:
Sending an email: MUA ----> MTA ----> (MTA relays) ----> MDA\nFetching an email: MUA <--------------------------------- MDA\n
There may be other moving parts or sub-divisions (for instance, at several points along the chain, specialized programs may be analyzing, filtering, bouncing, editing\u2026 the exchanged emails).
In a nutshell, DMS provides you with the following components:
Here's where DMS's toolchain fits within the delivery chain:
docker-mailserver is here:\n \u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513\nSending an email: MUA ---> MTA ---> (MTA relays) ---> \u252b MTA \u256e \u2503\nFetching an email: MUA <------------------------------ \u252b MDA \u256f \u2503\n \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b\n
An Example Let's say Alice owns a Gmail account, alice@gmail.com
; and Bob owns an account on a DMS instance, bob@dms.io
.
Make sure not to conflate these two very different scenarios: A) Alice sends an email to bob@dms.io
=> the email is first submitted to MTA smtp.gmail.com
, then relayed to MTA smtp.dms.io
where it is then delivered into Bob's mailbox. B) Bob sends an email to alice@gmail.com
=> the email is first submitted to MTA smtp.dms.io
, then relayed to MTA smtp.gmail.com
and eventually delivered into Alice's mailbox.
In scenario A the email leaves Gmail's premises, that email's initial submission is not handled by your DMS instance(MTA); it merely receives the email after it has been relayed by Gmail's MTA. In scenario B, the DMS instance(MTA) handles the submission, prior to relaying.
The main takeaway is that when a third-party sends an email to a DMS instance(MTA) (or any MTA for that matter), it does not establish a direct connection with that MTA. Email submission first goes through the sender's MTA, then some relaying between at least two MTAs is required to deliver the email. That will prove very important when it comes to security management.
One important thing to note is that MTA and MDA programs may actually handle multiple tasks (which is the case with DMS's Postfix and Dovecot).
For instance, Postfix is both an SMTP server (accepting emails) and a relaying MTA (transferring, ie. sending emails to other MTA/MDA); Dovecot is both an MDA (delivering emails in mailboxes) and an IMAP server (allowing MUAs to fetch emails from the mail server). On top of that, Postfix may rely on Dovecot's authentication capabilities.
The exact relationship between all the components and their respective (sometimes shared) responsibilities is beyond the scope of this document. Please explore this wiki & the web to get more insights about DMS's toolchain.
"},{"location":"introduction/#about-security-ports","title":"About Security & Ports","text":""},{"location":"introduction/#introduction","title":"Introduction","text":"In the previous section, three components were outlined. Each one of those is responsible for a specific task when it comes to exchanging emails:
Postfix handles Submission (and may handle Relay), whereas Dovecot handles Retrieval. They both need to be accessible by MUAs in order to act as servers, therefore they expose public endpoints on specific TCP ports. Those endpoints may be secured, using an encryption scheme and TLS certificates.
When it comes to the specifics of email exchange, we have to look at protocols and ports enabled to support all the identified purposes. There are several valid options and they've been evolving overtime.
"},{"location":"introduction/#overview","title":"Overview","text":"The following picture gives a visualization of the interplay of all components and their respective ports:
\u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501 Submission \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513\u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501 Transfer/Relay \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513\n\n \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u250c\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2510\nMUA ----- STARTTLS -------> \u2524(587) MTA \u256e (25)\u251c <-- cleartext ---> \u250a Third-party MTA \u250a\n ----- implicit TLS ---> \u2524(465) \u2502 | \u2514\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2518\n ----- cleartext ------> \u2524(25) \u2502 |\n |\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504|\nMUA <---- STARTTLS -------- \u2524(143) MDA \u256f |\n <---- implicit TLS ---- \u2524(993) |\n \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n\n \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501 Retrieval \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b\n
If you're new to email infrastructure, both that table and the schema may be confusing. Read on to expand your understanding and learn about DMS's configuration, including how you can customize it.
"},{"location":"introduction/#submission-smtp","title":"Submission - SMTP","text":"For a MUA to send an email to an MTA, it needs to establish a connection with that server, then push data packets over a network that both the MUA (client) and the MTA (server) are connected to. The server implements the SMTP protocol, which makes it capable of handling Submission.
In the case of DMS, the MTA (SMTP server) is Postfix. The MUA (client) may vary, yet its Submission request is performed as TCP packets sent over the public internet. This exchange of information may be secured in order to counter eavesdropping.
Now let's say I own an account on a DMS instance, me@dms.io
. There are two very different use-cases for Submission:
In the first scenario, I will be submitting my email directly to my DMS instance/MTA (Postfix), which will then relay the email to its recipient's MTA for final delivery. In this case, Submission is first handled by establishing a direct connection to my own MTA-so at least for this portion of the delivery chain, I'll be able to ensure security/confidentiality. Not so much for what comes next, ie. relaying between MTAs and final delivery.
In the second scenario, a third-party email account owner will be first submitting an email to some third-party MTA. I have no control over this initial portion of the delivery chain, nor do I have control over the relaying that comes next. My MTA will merely accept a relayed email coming \"out of the blue\".
My MTA will thus have to support two kinds of Submission:
\u250f\u2501\u2501\u2501 Outbound Submission \u2501\u2501\u2501\u2513\n\n \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u250c\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2510\nMe ---------------> \u2524 \u251c -----------------> \u250a \u250a\n \u2502 My MTA \u2502 \u250a Third-party MTA \u250a\n \u2502 \u251c <----------------- \u250a \u250a\n \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2514\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2518\n\n \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501 Inbound Submission \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b\n
"},{"location":"introduction/#outbound-submission","title":"Outbound Submission","text":"When it comes to securing Outbound Submission you should prefer to use Implicit TLS connection via ESMTP on port 465 (see RFC 8314). Please read our article about Understanding the Ports for more details!
Warning
This Submission setup is sometimes referred to as SMTPS. Long story short: this is incorrect and should be avoided.
Although a very satisfactory setup, Implicit TLS on port 465 is somewhat \"cutting edge\". There exists another well established mail Submission setup that must be supported as well, SMTP+STARTTLS on port 587. It uses Explicit TLS: the client starts with a cleartext connection, then the server informs a TLS-encrypted \"upgraded\" connection may be established, and the client may eventually decide to establish it prior to the Submission. Basically it's an opportunistic, opt-in TLS upgrade of the connection between the client and the server, at the client's discretion, using a mechanism known as STARTTLS that both ends need to implement.
In many implementations, the mail server doesn't enforce TLS encryption, for backwards compatibility. Clients are thus free to deny the TLS-upgrade proposal (or misled by a hacker about STARTTLS not being available), and the server accepts unencrypted (cleartext) mail exchange, which poses a confidentiality threat and, to some extent, spam issues. RFC 8314 (section 3.3) recommends for a mail server to support both Implicit and Explicit TLS for Submission, and to enforce TLS-encryption on ports 587 (Explicit TLS) and 465 (Implicit TLS). That's exactly DMS's default configuration: abiding by RFC 8314, it enforces a strict (encrypt
) STARTTLS policy, where a denied TLS upgrade terminates the connection thus (hopefully but at the client's discretion) preventing unencrypted (cleartext) Submission.
A final Outbound Submission setup exists and is akin SMTP+STARTTLS on port 587, but on port 25. That port has historically been reserved specifically for unencrypted (cleartext) mail exchange though, making STARTTLS a bit wrong to use. As is expected by RFC 5321, DMS uses port 25 for unencrypted Submission in order to support older clients, but most importantly for unencrypted Transfer/Relay between MTAs.
Granted it's still very difficult enforcing encryption between MTAs (Transfer/Relay) without risking dropping emails (when relayed by MTAs not supporting TLS-encryption), Inbound Submission is to be handled in cleartext on port 25 by default.
Overall, DMS's default configuration for SMTP looks like this:
\u250f\u2501\u2501\u2501 Outbound Submission \u2501\u2501\u2501\u2513\n\n \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u250c\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2510\nMe -- cleartext --> \u2524(25) (25)\u251c --- cleartext ---> \u250a \u250a\nMe -- TLS ---> \u2524(465) My MTA \u2502 \u250a Third-party MTA \u250a\nMe -- STARTTLS ---> \u2524(587) \u2502 \u250a \u250a\n \u2502 (25)\u251c <---cleartext ---- \u250a \u250a\n \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2514\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2518\n\n \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501 Inbound Submission \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b\n
"},{"location":"introduction/#retrieval-imap","title":"Retrieval - IMAP","text":"A MUA willing to fetch an email from a mail server will most likely communicate with its IMAP server. As with SMTP described earlier, communication will take place in the form of data packets exchanged over a network that both the client and the server are connected to. The IMAP protocol makes the server capable of handling Retrieval.
In the case of DMS, the IMAP server is Dovecot. The MUA (client) may vary, yet its Retrieval request is performed as TCP packets sent over the public internet. This exchange of information may be secured in order to counter eavesdropping.
Again, as with SMTP described earlier, the IMAP protocol may be secured with either Implicit TLS (aka. IMAPS / IMAP4S) or Explicit TLS (using STARTTLS).
The best practice as of 2020 is to enforce IMAPS on port 993, rather than IMAP+STARTTLS on port 143 (see RFC 8314); yet the latter is usually provided for backwards compatibility.
DMS's default configuration enables both Implicit and Explicit TLS for Retrievial, on ports 993 and 143 respectively.
"},{"location":"introduction/#retrieval-pop3","title":"Retrieval - POP3","text":"Similarly to IMAP, the older POP3 protocol may be secured with either Implicit or Explicit TLS.
The best practice as of 2020 would be POP3S on port 995, rather than POP3+STARTTLS on port 110 (see RFC 8314).
DMS's default configuration disables POP3 altogether. One should expect MUAs to use TLS-encrypted IMAP for Retrieval.
"},{"location":"introduction/#how-does-dms-help-with-setting-everything-up","title":"How Does DMS Help With Setting Everything Up?","text":"As a batteries included container image, DMS provides you with all the required components and a default configuration to run a decent and secure mail server. One may then customize all aspects of its internal components.
Eventually, it is up to you deciding exactly what kind of transportation/encryption to use and/or enforce, and to customize your instance accordingly (with looser or stricter security). Be also aware that protocols and ports on your server can only go so far with security; third-party MTAs might relay your emails on insecure connections, man-in-the-middle attacks might still prove effective, etc. Advanced counter-measure such as DANE, MTA-STS and/or full body encryption (eg. PGP) should be considered as well for increased confidentiality, but ideally without compromising backwards compatibility so as to not block emails.
"},{"location":"usage/","title":"Usage","text":"This page explains how to get started with DMS. The guide uses Docker Compose as a reference. In our examples, a volume mounts the host location docker-data/dms/config/
to /tmp/docker-mailserver/
inside the container.
Before you can get started with deploying your own mail server, there are some requirements to be met:
There are a few requirements for a suitable host system:
PTR
record for your host; security-hardened mail servers might otherwise reject your mail server as the IP address of your host does not resolve correctly/at all to the DNS name of your server.About the Container Runtime
On the host, you need to have a suitable container runtime (like Docker or Podman) installed. We assume Docker Compose is installed. We have aligned file names and configuration conventions with the latest Docker Compose (currently V2) specification.
If you're using podman, make sure to read the related documentation.
"},{"location":"usage/#minimal-dns-setup","title":"Minimal DNS Setup","text":"The DNS setup is a big and essential part of the whole setup. There is a lot of confusion for newcomers and people starting out when setting up DNS. This section provides an example configuration and supplementary explanation. We expect you to be at least a bit familiar with DNS, what it does and what the individual record types are.
Now let's say you just bought example.com
and you want to be able to send and receive e-mails for the address test@example.com
. On the most basic level, you will need to
MX
record for your domain example.com
- in our example, the MX record contains mail.example.com
A
record that resolves the name of your mail server - in our example, the A record contains 11.22.33.44
PTR
record that resolves the IP of your mail server - in our example, the PTR contains mail.example.com
We will later dig into DKIM, DMARC & SPF, but for now, these are the records that suffice in getting you up and running. Here is a short explanation of what the records do:
mail.example.com
. This does not imply your e-mails will look like test@mail.example.com
, the DNS name of your mail server is decoupled of the domain it serves e-mails for. Your mail server could also handle emails for test@some-other-domain.com
, if the MX record for some-other-domain.com
points to mail.example.com
.mail.example.com
resolves to.11.22.33.44
resolves to.About The Mail Server's Fully Qualified Domain Name
The mail server's fully qualified domain name (FQDN) in our example above is mail.example.com
. Please note though that this is more of a convention, and not due to technical restrictions. One could also run the mail server
foo.example.com
: you would just need to change your MX
record;example.com
directly: you would need to change your MX
record and probably read our docs on bare domain setups, as these setups are called \"bare domain\" setups.The FQDN is what is relevant for TLS certificates, it has no (inherent/technical) relation to the email addresses and accounts DMS manages. That is to say: even though DMS runs on mail.example.com
, or foo.example.com
, or example.com
, there is nothing that prevents it from managing mail for barbaz.org
- barbaz.org
will just need to set its MX
record to mail.example.com
(or foo.example.com
or example.com
).
If you setup everything, it should roughly look like this:
$ dig @1.1.1.1 +short MX example.com\nmail.example.com\n$ dig @1.1.1.1 +short A mail.example.com\n11.22.33.44\n$ dig @1.1.1.1 +short -x 11.22.33.44\nmail.example.com\n
"},{"location":"usage/#deploying-the-actual-image","title":"Deploying the Actual Image","text":""},{"location":"usage/#tagging-convention","title":"Tagging Convention","text":"To understand which tags you should use, read this section carefully. Our CI will automatically build, test and push new images to the following container registries:
docker.io/mailserver/docker-mailserver
)ghcr.io/docker-mailserver/docker-mailserver
)All workflows are using the tagging convention listed below. It is subsequently applied to all images.
Event Image Tagspush
on master
edge
push
a tag (v1.2.3
) 1.2.3
, 1.2
, 1
, latest
"},{"location":"usage/#get-all-files","title":"Get All Files","text":"Issue the following commands to acquire the necessary files:
DMS_GITHUB_URL=\"https://raw.githubusercontent.com/docker-mailserver/docker-mailserver/master\"\nwget \"${DMS_GITHUB_URL}/compose.yaml\"\nwget \"${DMS_GITHUB_URL}/mailserver.env\"\n
"},{"location":"usage/#configuration-steps","title":"Configuration Steps","text":"compose.yaml
to your likingmail.example.com
according to your FQDN../docker-data/dms/config/:/tmp/docker-mailserver/
mount, append -z
or -Z
.mailserver.env
, but keep in mind that:VAR=VAL
is supportedOVERRIDE_HOSTNAME=$HOSTNAME.$DOMAINNAME
does not workUsing the Correct Commands For Stopping and Starting DMS
Use docker compose up / down
, not docker compose start / stop
. Otherwise, the container is not properly destroyed and you may experience problems during startup because of inconsistent state.
Using Ctrl+C
is not supported either!
For an overview of commands to manage DMS config, run: docker exec -it <CONTAINER NAME> setup help
.
setup.sh
when no DMS Container Is Running We encourage you to directly use setup
inside the container (like shown above). If you still want to use setup.sh
, here's some information about it.
If no DMS container is running, any ./setup.sh
command will check online for the :latest
image tag (the current stable release), performing a docker pull ...
if necessary followed by running the command in a temporary container:
$ ./setup.sh help\nImage 'ghcr.io/docker-mailserver/docker-mailserver:latest' not found. Pulling ...\nSETUP(1)\n\nNAME\n setup - 'docker-mailserver' Administration & Configuration script\n...\n\n$ docker run --rm ghcr.io/docker-mailserver/docker-mailserver:latest setup help\nSETUP(1)\n\nNAME\n setup - 'docker-mailserver' Administration & Configuration script\n...\n
On first start, you will need to add at least one email account (unless you're using LDAP). You have two minutes to do so, otherwise DMS will shutdown and restart. You can add accounts by running docker exec -ti <CONTAINER NAME> setup email add user@example.com
. That's it! It really is that easy.
You definitely want to setup TLS. Please refer to our documentation about TLS.
"},{"location":"usage/#aliases","title":"Aliases","text":"You should add at least one alias, the postmaster alias. This is a common convention, but not strictly required.
docker exec -ti <CONTAINER NAME> setup alias add postmaster@example.com user@example.com\n
"},{"location":"usage/#advanced-dns-setup-dkim-dmarc-spf","title":"Advanced DNS Setup - DKIM, DMARC & SPF","text":"You will very likely want to configure your DNS with these TXT records: SPF, DKIM, and DMARC. We also ship a dedicated page in our documentation about the setup of DKIM, DMARC & SPF.
"},{"location":"usage/#custom-user-changes-patches","title":"Custom User Changes & Patches","text":"If you'd like to change, patch or alter files or behavior of DMS, you can use a script. See this part of our documentation for a detailed explanation.
"},{"location":"usage/#testing","title":"Testing","text":"Here are some tools you can use to verify your configuration:
This page contains valuable information when it comes to resolving issues you encounter.
Contributions Welcome!
Please consider contributing solutions to the FAQ
"},{"location":"config/debugging/#preliminary-checks","title":"Preliminary Checks","text":"Correctly starting DMS
Use the --force-recreate
option to avoid configuration mishaps: docker compose up --force-recreate
Alternatively, always use docker compose down
to stop DMS. Do not rely on CTRL + C
, docker compose stop
, or docker compose restart
.
DMS setup scripts are run when a container starts, but may fail to work properly if you do the following:
docker stop
or docker compose up
stopped via CTRL + C
instead of docker compose down
.Volumes persist data across container instances, however the same container instance will keep internal changes not stored in a volume until the container is removed.
Due to this, DMS setup scripts may modify configuration it has already modified in the past.
compose.yaml
are expected to persist any important data. Thus it should be safe to throwaway the container created each time, avoiding this config problem.Some service providers block outbound traffic on port 25. Common hosting providers known to have this issue:
These links may advise how the provider can unblock the port through additional services offered, or via a support ticket request.
"},{"location":"config/debugging/#mail-sent-to-dms-does-not-get-delivered-to-user","title":"Mail sent to DMS does not get delivered to user","text":"Common logs related to this are:
warning: do not list domain domain.fr in BOTH mydestination and virtual_mailbox_domains
Recipient address rejected: User unknown in local recipient table
If your logs look like this, you likely have assigned the same FQDN to the DMS hostname
and your mail accounts which is not supported by default. You can either adjust your DMS hostname
or follow this FAQ advice
It is also possible that DMS services are temporarily unavailable when configuration changes are detected, producing the 2nd error. Certificate updates may be a less obvious trigger.
"},{"location":"config/debugging/#steps-for-debugging-dms","title":"Steps for Debugging DMS","text":"LOG_LEVEL
to debug
or trace
.docker log <CONTAINER NAME>
(or docker logs -f <CONTAINER NAME>
if you want to follow the log).To get a shell inside the container run: docker exec -it <CONTAINER NAME> bash
. To install additional software, run:
apt-get update
to update repository metadata.apt-get install <PACKAGE>
to install a package, e.g., apt-get install neovim
if you want to use NeoVim instead of nano
(which is shipped by default).If you need more flexibility than what the docker logs
command offers, then the most useful locations to get relevant DMS logs within the container are:
/var/log/mail/<SERVICE>.log
/var/log/supervisor/<SERVICE>.log
You may use nano
(a text editor) to edit files, while less
(a file viewer) and tail
/cat
are useful tools to inspect the contents of logs.
It's possible that the issue you're experiencing is due to a compatibility conflict.
This could be from outdated software, or running a system that isn't able to provide you newer software and kernels. You may want to verify if you can reproduce the issue on a system that is not affected by these concerns.
"},{"location":"config/debugging/#network","title":"Network","text":"userland-proxy
: Prior to Docker v23
, changing the userland-proxy
setting did not reliably remove NAT rules.iptables
/ nftables
:iptables
, relying on compatibility shims for supporting the successor nftables
. Internally DMS expects nftables
support on the host kernel for services like Fail2Ban to function correctly.nftables
. Other software outside of DMS may also manipulate these rules, such as firewall frontends.v23
(2023Q1).v20.10.0
(2020Q4).volumes
config in compose.yaml
. You may have luck trying gRPC FUSE
as the file sharing implementation; VirtioFS
is the successor but presently appears incompatible with DMS.unified
hierarchy. Not meeting this baseline may influence the behavior of your DMS container, even with the latest Docker Engine installed.Info
Values in bold are the default values. If an option doesn't work as documented here, check if you are running the latest image. The current master
branch corresponds to the image ghcr.io/docker-mailserver/docker-mailserver:edge
.
If you can't set your hostname (eg: you're in a container platform that doesn't let you) specify it via this environment variable. It will have priority over docker run --hostname
, or the equivalent hostname:
field in compose.yaml
.
hostname -f
command to get canonical hostname for DMS to use.Set the log level for DMS. This is mostly relevant for container startup scripts and change detection event feedback.
Valid values (in order of increasing verbosity) are: error
, warn
, info
, debug
and trace
. The default log level is info
.
Here you can adjust the log-level for Supervisor. Possible values are
The log-level will show everything in its class and above.
"},{"location":"config/environment/#dms_vmail_uid","title":"DMS_VMAIL_UID","text":"Default: 5000
The User ID assigned to the static vmail user for /var/mail
(Mail storage managed by Dovecot).
Incompatible UID values
0
(root) is not compatible.uid
for the docker
user (/etc/passwd
), hence the error emitted to logs if the UID is already assigned to another user./etc/passwd
, even though Dovecot by default has a setting for the minimum UID as 500
.Default: 5000
The Group ID assigned to the static vmail group for /var/mail
(Mail storage managed by Dovecot).
Configures the provisioning source of user accounts (including aliases) for user queries and authentication by services managed by DMS (Postfix and Dovecot).
LDAP requires an external service (e.g. bitnami/openldap
).
Set different options for mynetworks option (can be overwrite in postfix-main.cf) WARNING: Adding the docker network's gateway to the list of trusted hosts, e.g. using the network
or connected-networks
option, can create an open relay, for instance if IPv6 is enabled on the host machine but not in Docker.
docker-compose
might use others (e.g. 192.168.0.0/16) use PERMIT_DOCKER=connected-networks
in this case.Note: you probably want to set POSTFIX_INET_PROTOCOLS=ipv4
to make it work fine with Docker.
Set the timezone. If this variable is unset, the container runtime will try to detect the time using /etc/localtime
, which you can alternatively mount into the container. The value of this variable must follow the pattern AREA/ZONE
, i.e. of you want to use Germany's time zone, use Europe/Berlin
. You can lookup all available timezones here.
Amavis content filter (used for ClamAV & SpamAssassin)
This page provides information on Amavis' logging statistics.
This enables DNS block lists in Postscreen. If you want to know which lists we are using, have a look at the default main.cf
for Postfix we provide and search for postscreen_dnsbl_sites
.
A Warning On DNS Block Lists
Make sure your DNS queries are properly resolved, i.e. you will most likely not want to use a public DNS resolver as these queries do not return meaningful results. We try our best to only evaluate proper return codes - this is not a guarantee that all codes are handled fine though.
Note that emails will be rejected if they don't pass the block list checks!
Enables MTA-STS support for outbound mail.
See MTA-STS for further explanation.
"},{"location":"config/environment/#enable_opendkim","title":"ENABLE_OPENDKIM","text":"Enables the OpenDKIM service.
Enables the OpenDMARC service.
Enabled policyd-spf
in Postfix's configuration. You will likely want to set this to 0
in case you're using Rspamd (ENABLE_RSPAMD=1
).
If you enable Fail2Ban, don't forget to add the following lines to your compose.yaml
:
cap_add:\n - NET_ADMIN\n
Otherwise, nftables
won't be able to ban IPs.
In the majority of cases, you want letsencrypt
or manual
.
self-signed
can be used for testing SSL until you provide a valid certificate, note that third-parties cannot trust self-signed
certificates, do not use this type in production. custom
is a temporary workaround that is not officially supported.
SSL_CERT_PATH
and SSL_KEY_PATH
ENV vars to be set to the location of the files within the container.SSL_ALT_CERT_PATH
and SSL_ALT_KEY_PATH
allow providing a 2nd certificate as a fallback for dual (aka hybrid) certificate support. Useful for ECDSA with an RSA fallback. Presently only manual
mode supports this feature.None
)Please read the SSL page in the documentation for more information.
"},{"location":"config/environment/#tls_level","title":"TLS_LEVEL","text":"Configures the handling of creating mails with forged sender addresses.
Enables the Sender Rewriting Scheme. SRS is needed if DMS acts as forwarder. See postsrsd for further explanation.
In case your network interface differs from eth0
, e.g. when you are using HostNetworking in Kubernetes, you can set this to whatever interface you want. This interface will then be used.
eth0
Set how many days a virusmail will stay on the server before being deleted
Configure Postfix virtual_transport
to deliver mail to a different LMTP client (default is a unix socket to dovecot).
Provide any valid URI. Examples:
lmtp:unix:/var/run/dovecot/lmtp
(default, configured in Postfix main.cf
)lmtp:unix:private/dovecot-lmtp
(use socket)lmtps:inet:<host>:<port>
(secure lmtp with starttls)lmtp:<kopano-host>:2003
(use kopano as mailstore)Set the mailbox size limit for all users. If set to zero, the size will be unlimited (default). Size is in bytes.
See mailbox quota.
"},{"location":"config/environment/#postfix_message_size_limit","title":"POSTFIX_MESSAGE_SIZE_LIMIT","text":"Set the message size limit for all users. If set to zero, the size will be unlimited (not recommended!). Size is in bytes.
Mails larger than this limit won't be scanned. ClamAV must be enabled (ENABLE_CLAMAV=1) for this.
Check for updates on container start and then once a day. If an update is available, a mail is send to POSTMASTER_ADDRESS.
Customize the update check interval. Number + Suffix. Suffix must be 's' for seconds, 'm' for minutes, 'h' for hours or 'd' for days.
This option has been added in November 2019. Using other format than Maildir is considered as experimental in docker-mailserver and should only be used for testing purpose. For more details, please refer to Dovecot Documentation.
"},{"location":"config/environment/#postfix_reject_unknown_client_hostname","title":"POSTFIX_REJECT_UNKNOWN_CLIENT_HOSTNAME","text":"If enabled, employs reject_unknown_client_hostname
to sender restrictions in Postfix's configuration.
Note: More details at http://www.postfix.org/postconf.5.html#inet_protocols
"},{"location":"config/environment/#dovecot_inet_protocols","title":"DOVECOT_INET_PROTOCOLS","text":"Note: More information at https://dovecot.org/doc/dovecot-example.conf
"},{"location":"config/environment/#move_spam_to_junk","title":"MOVE_SPAM_TO_JUNK","text":"Routes mail identified as spam into the recipient(s) Junk mailbox (a specialized folder for junk associated to the special-use flag \\Junk
, handled via a Dovecot sieve script internally).
Info
Mail is received as spam when it has been marked with either header:
X-Spam: Yes
(added by Rspamd)X-Spam-Flag: YES
(added by SpamAssassin - requires SPAMASSASSIN_SPAM_TO_INBOX=1
)Enable to treat received spam as \"read\" (avoids notification to MUA client of new mail).
Info
Mail is received as spam when it has been marked with either header:
X-Spam: Yes
(added by Rspamd)X-Spam-Flag: YES
(added by SpamAssassin - requires SPAMASSASSIN_SPAM_TO_INBOX=1
)This variable defines a prefix for e-mails tagged with the X-Spam: Yes
(Rspamd) or X-Spam-Flag: YES
(SpamAssassin/Amavis) header.
Default: empty (no prefix will be added to e-mails)
Including trailing white-spaceAdd trailing white-space by quote wrapping the value: SPAM_SUBJECT='[SPAM] '
Enable or disable Rspamd.
Explicit control over running a Redis instance within the container. By default, this value will match what is set for ENABLE_RSPAMD
.
The purpose of this setting is to opt-out of starting an internal Redis instance when enabling Rspamd, replacing it with your own external instance.
Configuring Rspamd for an external Redis instanceYou will need to provide configuration at /etc/rspamd/local.d/redis.conf
similar to:
servers = \"redis.example.test:6379\";\nexpand_keys = true;\n
This settings controls whether checks should be performed on emails coming from authenticated users (i.e. most likely outgoing emails). The default value is 0
in order to align better with SpamAssassin. We recommend reading through the Rspamd documentation on scanning outbound emails though to decide for yourself whether you need and want this feature.
Not all checks and actions are disabled
DKIM signing of e-mails will still happen.
Controls whether the Rspamd Greylisting module is enabled. This module can further assist in avoiding spam emails by greylisting e-mails with a certain spam score.
When enabled,
Junk
folder (learning this email as spam);Junk
folder into the INBOX
(learning this email as ham).Attention
As of now, the spam learning database is global (i.e. available to all users). If one user deliberately trains it with malicious data, then it will ruin your detection rate.
This feature is suitably only for users who can tell ham from spam and users that can be trusted.
Can be used to enable or disable the Hfilter group module. This is used by DMS to adjust the HFILTER_HOSTNAME_UNKNOWN
symbol, increasing its default weight to act similar to Postfix's reject_unknown_client_hostname
, without the need to outright reject a message.
Can be used to control the score when the HFILTER_HOSTNAME_UNKNOWN
symbol applies. A higher score is more punishing. Setting it to 15 (the default score for rejecting an e-mail) is equivalent to rejecting the email when the check fails.
Default: 6 (which corresponds to the add_header
action)
Can be used to enable or disable the Neural network module. This is an experimental anti-spam weigh method using three neural networks in the configuration added here. As far as we can tell it trains itself by using other modules to find out what spam is. It will take a while (a week or more) to train its first neural network. The config trains new networks all the time and discards old networks. Since it is experimental, it is switched off by default.
Enables regular Postfix log summary (\"pflogsumm\") mail reports.
This is a new option. The old REPORT options are still supported for backwards compatibility. If this is not set and reports are enabled with the old options, logrotate will be used.
"},{"location":"config/environment/#pflogsumm_recipient","title":"PFLOGSUMM_RECIPIENT","text":"Recipient address for Postfix log summary reports.
Sender address (FROM
) for pflogsumm reports (if Postfix log summary reports are enabled).
Interval for logwatch report.
Recipient address for logwatch reports if they are enabled.
Sender address (FROM
) for logwatch reports if logwatch reports are enabled.
Defines who receives reports (if they are enabled).
Defines who sends reports (if they are enabled).
mailserver-report@<YOUR DOMAIN>
Changes the interval in which log files are rotated.
Note
LOGROTATE_INTERVAL
only manages logrotate
within the container for services we manage internally.
The entire log output for the container is still available via docker logs mailserver
(or your respective container name). If you want to configure external log rotation for that container output as well, : Docker Logging Drivers.
By default, the logs are lost when the container is destroyed (eg: re-creating via docker compose down && docker compose up -d
). To keep the logs, mount a volume (to /var/log/mail/
).
Note
This variable can also determine the interval for Postfix's log summary reports, see PFLOGSUMM_TRIGGER
.
Defines how many files are kept by logrotate.
Integration with Amavis involves processing mail based on the assigned spam score via SA_TAG
, SA_TAG2
and SA_KILL
.
These settings have equivalent ENV supported by DMS for easy adjustments, as documented below.
"},{"location":"config/environment/#enable_spamassassin_kam","title":"ENABLE_SPAMASSASSIN_KAM","text":"KAM is a 3rd party SpamAssassin ruleset, provided by the McGrail Foundation. If SpamAssassin is enabled, KAM can be used in addition to the default ruleset.
"},{"location":"config/environment/#spamassassin_spam_to_inbox","title":"SPAMASSASSIN_SPAM_TO_INBOX","text":"D_BOUNCE
): Spam messages will be bounced (rejected) without any notification (dangerous).D_PASS
): Spam messages will be delivered to the inbox.Note
The Amavis action configured by this setting:
SA_KILL
setting.$final_spam_destiny
and $final_bad_header_destiny
.This ENV setting is related to
MOVE_SPAM_TO_JUNK=1
MARK_SPAM_AS_READ=1
SPAM_SUBJECT
Mail is not yet considered spam at this spam score, but for purposes like diagnostics it can be useful to identify mail with a spam score at a lower bound than SA_TAG2
.
X-Spam
headers appended to mail Send a simple mail to a local DMS account hello@example.com
:
docker exec dms swaks --server 0.0.0.0 --to hello@example.com --body 'spam'\n
Inspecting the raw mail you will notice several X-Spam
headers were added to the mail like this:
X-Spam-Flag: NO\nX-Spam-Score: 4.162\nX-Spam-Level: ****\nX-Spam-Status: No, score=4.162 tagged_above=2 required=4\n tests=[BODY_SINGLE_WORD=1, DKIM_ADSP_NXDOMAIN=0.8,\n NO_DNS_FOR_FROM=0.379, NO_RECEIVED=-0.001, NO_RELAYS=-0.001,\n PYZOR_CHECK=1.985] autolearn=no autolearn_force=no\n
The X-Spam-Score
is 4.162
High enough for SA_TAG
to trigger adding these headers, but not high enough for SA_TAG2
(which would set X-Spam-Flag: YES
instead).
When a spam score is high enough, mark mail as spam (Appends the mail header: X-Spam-Flag: YES
).
Interaction with other ENV
SPAM_SUBJECT
modifies the mail subject to better communicate spam mail to the user.MOVE_SPAM_TO_JUNK=1
: The mail is still delivered, but to the recipient(s) junk folder instead. This feature reduces the usefulness of SPAM_SUBJECT
.Controls the spam score threshold for triggering an action on mail that has a high spam score.
Choosing an appropriateSA_KILL
value The value should be high enough to be represent confidence in mail as spam:
SA_KILL
trigger (how to treat mail with high confidence that it is actually spam).Experiences from DMS users with these settings has been collected here, along with some direct configuration guides (under \"Resources for references\").
Trigger actionDMS will configure Amavis with either of these actions based on the DMS SPAMASSASSIN_SPAM_TO_INBOX
ENV setting:
D_PASS
(default):SA_KILL
threshold that won't accidentally discard / bounce legitimate mail users are expecting to arrive but is detected as spam.D_BOUNCE
:$sa_dsn_cutoff_level
config setting (default: 10
). With the DMS SA_KILL
default also being 10
, no DSN will ever be sent.D_REJECT
/ D_DISCARD
:When mail has a spam score that reaches the SA_KILL
threshold:
SA_KILL
action to perform.D_PASS
the delivered mail also appends an X-Quarantine-ID
mail header. The ID value of this header is part of the quarantined file name.If emails are quarantined, they are compressed and stored at a location:
/var/lib/amavis/virusmails/
/var/mail-state/
volume is present: /var/mail-state/lib-amavis/virusmails/
Tip
Easily list mail stored in quarantine with find
and the quarantine path:
find /var/lib/amavis/virusmails -type f\n
"},{"location":"config/environment/#sa_shortcircuit_bayes_spam","title":"SA_SHORTCIRCUIT_BAYES_SPAM","text":"This will uncomment the respective line in /etc/spamassasin/local.cf
Warning
Activate this only if you are confident in your bayes database for identifying spam.
"},{"location":"config/environment/#sa_shortcircuit_bayes_ham","title":"SA_SHORTCIRCUIT_BAYES_HAM","text":"This will uncomment the respective line in /etc/spamassasin/local.cf
Warning
Activate this only if you are confident in your bayes database for identifying ham.
"},{"location":"config/environment/#fetchmail","title":"Fetchmail","text":""},{"location":"config/environment/#enable_fetchmail","title":"ENABLE_FETCHMAIL","text":"fetchmail
disabledfetchmail
enabledfetchmail
The number of seconds for the intervalfetchmail
runs with a single config file /etc/fetchmailrc
/etc/fetchmailrc
is split per poll entry. For every poll entry a separate fetchmail instance is started to allow having multiple imap idle connections per server (when poll entries reference the same IMAP server).Note: The defaults of your fetchmailrc file need to be at the top of the file. Otherwise it won't be added correctly to all separate fetchmail
instances.
Enable or disable getmail
.
getmail
The number of minutes for the interval. Min: 1; Default: 5.https://oauth2.example.com/userinfo/
)<dns-name>
/ <ip-address>
where the LDAP server is reachable via a URI like: ldaps://mail.example.com
.ldap://
, ldaps://
, ldapi://
).(&(mail=%s)(mailEnabled=TRUE))
(&(mailGroupMember=%s)(mailEnabled=TRUE))
(&(mailAlias=%s)(mailEnabled=TRUE))
(&(|(mail=*@%s)(mailalias=*@%s)(mailGroupMember=*@%s))(mailEnabled=TRUE))
(|($LDAP_QUERY_FILTER_USER)($LDAP_QUERY_FILTER_ALIAS)($LDAP_QUERY_FILTER_GROUP))
The following variables overwrite the default values for /etc/dovecot/dovecot-ldap.conf.ext
.
LDAP_SEARCH_BASE
ou=people,dc=domain,dc=com
)SSHA
LDAP_BIND_DN
cn=admin,dc=domain,dc=com
)LDAP_BIND_PW
DOVECOT_DN
.LDAP_SERVER_HOST
ldap://
, ldaps://
, ldapi://
).(&(objectClass=PostfixBookMailAccount)(uniqueIdentifier=%n))
homeDirectory=home,qmailUID=uid,qmailGID=gid,mailMessageStore=mail
(&(objectClass=PostfixBookMailAccount)(uniqueIdentifier=%n))
DOVECOT_USER_FILTER
uid=user,userPassword=password
postgrey
is disabledpostgrey
is enabledNote: This postgrey setting needs ENABLE_POSTGREY=1
Note: This postgrey setting needs ENABLE_POSTGREY=1
Note: This postgrey setting needs ENABLE_POSTGREY=1
Note: This postgrey setting needs ENABLE_POSTGREY=1
saslauthd
is disabledsaslauthd
is enabledDMS only implements support for these mechanisms:
ldap
=> Authenticate against an LDAP serverrimap
=> Authenticate against an IMAP serverInfo
With SASLAUTHD_MECHANISMS=rimap
you need to specify the ip-address / servername of the IMAP server, such as SASLAUTHD_MECH_OPTIONS=127.0.0.1
.
LDAP_SERVER_HOST
Note
You must include the desired URI scheme (ldap://
, ldaps://
, ldapi://
).
no
yes
=> Enable ldap_start_tls
optionno
yes
=> Enable ldap_tls_check_peer
optionPath to directory with CA (Certificate Authority) certificates.
ldap_tls_cacert_dir
optionFile containing CA (Certificate Authority) certificate(s).
ldap_tls_cacert_file
optionLDAP_BIND_DN
LDAP_BIND_PW
LDAP_SEARCH_BASE
(&(uniqueIdentifier=%u)(mailEnabled=TRUE))
(&(sAMAccountName=%U)(objectClass=person))
(&(uid=%U)(objectClass=person))
Specify what password attribute to use for password verification.
userPassword
by default.ldap_password_attr
optionbind
will be used as a default valuefastbind
=> The fastbind method is usedcustom
=> The custom method uses userPassword attribute to verify the passwordSpecify the authentication mechanism for SASL bind.
ldap_mech
optionAn email has an \"envelope\" sender (indicating the sending server) and a \"header\" sender (indicating who sent it). More strict SPF policies may require you to replace both instead of just the envelope sender.
More info.
dd if=/dev/urandom bs=24 count=1 2>/dev/null | base64
OVERRIDE_HOSTNAME
, $DOMAINNAME
(internal), or the container's hostnameSupported ENV for the Relay Host feature.
Prefer DEFAULT_RELAY_HOST
instead of RELAY_HOST
This is advised unless you need support for sender domain opt-out (via setup relay exclude-domain
).
The implementation for RELAY_HOST
is not compatible with LDAP.
Opt-in for relay host support
Enable relaying only for specific sender domains instead by using setup relay add-domain
.
NOTE: Presently there is a caveat when relay host credentials are configured (which is incompatible with opt-in).
"},{"location":"config/environment/#default_relay_host","title":"DEFAULT_RELAY_HOST","text":"Configures a default relay host.
Info
[mail.example.com]:587
vs example.com:587
Technical Details
This ENV internally configures the Postfix main.cf
setting: relayhost
Configures a default relay host.
Note
Expects a value like mail.example.com
. Internally this will be wrapped to [mail.example.com]
, so it should resolve to the MTA directly.
Do not use with DEFAULT_RELAY_HOST
RELAY_HOST
has precedence as it is configured with sender_dependent_relayhost_maps
.
Info
postfix-relaymap.cf
to work.DEFAULT_RELAY_HOST
.Technical Details
This feature is configured internally using the:
sender_dependent_relayhost_maps = texthash:/etc/postfix/relayhost_map
postfix-relaymap.cf
(generates /etc/postfix/relayhost_map
)All known mail domains managed by DMS will be configured to relay outbound mail to RELAY_HOST
by adding them implicitly to /etc/postfix/relayhost_map
, except for domains using the opt-out feature of postfix-relaymap.cf
.
Default => 25
Support for configuring a different port than 25 for RELAY_HOST
to use.
Note
Requires RELAY_HOST
.
Configuring relay host credentials enforces outbound authentication
Presently when RELAY_USER
+ RELAY_PASSWORD
or postfix-sasl-password.cf
are configured, all outbound mail traffic is configured to require a secure connection established and forbids the omission of credentials.
Additional feature work is required to only enforce these requirements on mail sent through a configured relay host.
"},{"location":"config/environment/#relay_user","title":"RELAY_USER","text":""},{"location":"config/environment/#relay_password","title":"RELAY_PASSWORD","text":"Provide the credentials to use with RELAY_HOST
or DEFAULT_RELAY_HOST
.
Alternative credentials config
You may prefer to use setup relay add-auth
to avoid risking ENV exposing secrets.
postfix-sasl-password.cf
with the correct relayhost entry (DEFAULT_RELAY_HOST
value, or as defined in /etc/postfix/relayhost_map
) to provide credentials per relayhost configured.Technical Details
Credentials for relay hosts are configured internally using the:
smtp_sasl_password_maps = texthash:/etc/postfix/sasl_passwd
postfix-sasl-password.cf
(generates /etc/postfix/sasl_passwd
)When postfix-sasl-password.cf
is present, DMS will copy it internally to /etc/postfix/sasl_passwd
.
setup relay add-auth
(creates / updates postfix-sasl-password.cf
).host:port
/ [host]:port
) from the generated /etc/postfix/relayhost_map
, or main.cf:relayhost
(DEFAULT_RELAY_HOST
).setup relay ...
is missing support, you must instead add these manually to postfix-sasl-password.cf
.If you want to use POP3(S), you have to add the ports 110 and/or 995 (TLS secured) and the environment variable ENABLE_POP3
to your compose.yaml
:
mailserver:\n ports:\n - \"25:25\" # SMTP (explicit TLS => STARTTLS)\n - \"143:143\" # IMAP4 (explicit TLS => STARTTLS)\n - \"465:465\" # ESMTP (implicit TLS)\n - \"587:587\" # ESMTP (explicit TLS => STARTTLS)\n - \"993:993\" # IMAP4 (implicit TLS)\n - \"110:110\" # POP3\n - \"995:995\" # POP3 (with TLS)\n environment:\n - ENABLE_POP3=1\n
"},{"location":"config/setup.sh/","title":"About setup.sh","text":"Note
setup.sh
is not required. We encourage you to use docker exec -ti <CONTAINER NAME> setup
instead.
Warning
This script assumes Docker or Podman is used. You will not be able to use setup.sh
with other container orchestration tools.
setup.sh
is a script that is complimentary to the internal setup
command in DMS.
It mostly provides the convenience of aliasing docker exec -ti <CONTAINER NAME> setup
, inferring the container name of a running DMS instance or running a new instance and bind mounting necessary volumes implicitly.
It is intended to be run from the host machine, not from inside your running container. The latest version of the script is included in the DMS repository. You may retrieve it at any time by running this command in your console:
wget https://raw.githubusercontent.com/docker-mailserver/docker-mailserver/master/setup.sh\nchmod a+x ./setup.sh\n
For more information on using the script run: ./setup.sh help
.
This page provides a technical reference for account management in DMS.
Account provisioners and alternative authentication support
Each ACCOUNT_PROVISIONER
has a separate page for configuration guidance and caveats:
FILE
provisioner docsLDAP
provisioner docsAuthentication from the provisioner can be supplemented with additional methods:
For custom authentication requirements, you could implement this with Lua.
"},{"location":"config/account-management/overview/#accounts","title":"Accounts","text":"Info
To receive or send mail, you'll need to provision user accounts into DMS (as each provisioner page documents).
A DMS account represents a user with their login username + password, and optional config like aliases and quota.
The email address associated to an account creates a mailbox. This address is relevant:
SPOOF_PROTECTION=1
restricts the sender address to the DMS account email address (unless additional sender addresses have been permitted via supported config).SPOOF_PROTECTION=0
allows DMS accounts to use any sender address (only a single DMS account is necessary to send mail with different sender addresses).For more details, see the Technical Overview section.
Support for multiple mail domainsNo extra configuration in DMS is required after provisioning an account with an email address.
hostname
setting).An email address should conform to the standard permitted charset and format (local-part@domain-part
).
DMS has features that need to reserve special characters to work correctly. Ensure those characters are not present in email addresses you configure for DMS, otherwise disable / opt-out of the feature.
+
as the tag delimiter. The tag can be changed, feature opt-out when the tag is explicitly unset.Info
Aliases allow receiving mail:
@gmail.com
.Aliases are managed through Postfix which supports local and virtual aliases:
local
delivery agent (see associated alias config format)root
).postmaster
may be a local alias to root
, and root
to a virtual alias or real email address.local
delivery agent will not be delivered to an inbox managed by Dovecot (unless you have configured a local alias to redirect mail to a valid address or alias).hostname: mail.example.com
, thus user@mail.example.com
). Technically there is no domain-part at this point, that context is used when routing delivery, the local delivery agent only knows of the local-part (an alias or unix account).virtual
delivery agent (see associated alias config format)user@example.com
).Verify alias resolves correctly
You can run postmap -q <alias> <table>
in the container to verify an alias resolves to the expected target. If the target is also an alias, the command will not expand that alias to resolve the actual recipient(s).
For the FILE
provisioner, an example would be: postmap -q alias1@example.com /etc/postfix/virtual
. For the LDAP
provisioner you'd need to adjust the table path.
Side effect - Dovecot Quotas (ENABLE_QUOTAS=1
)
As a side effect of the alias workaround for the FILE
provisioner with this feature, aliases can be used for account login. This is not intentional.
Info
Enables mail clients with the capability to query a mailbox for disk-space used and capacity limit.
ENABLE_QUOTAS=0
Without quota limits for disk storage, a mailbox could fill up the available storage which would cause delivery failures to all mailboxes.
Quotas help by preventing that abuse, so that only a mailbox exceeding the assigned quota experiences a delivery failure instead of negatively impacting others (provided disk space is available).
Technical DetailsThe Dovecot Quotas feature is configured by enabling the Dovecot imap-quota
plugin and using the count
quota backend.
Dovecot workaround for Postfix aliases
When mail is delivered to DMS, Postfix will query Dovecot with the recipient(s) to verify quota has not been exceeded.
This allows early rejection of mail arriving to DMS, preventing a spammer from taking advantage of a backscatter source if the mail was accepted by Postfix, only to later be rejected by Dovecot for storage when the quota limit was already reached.
However, Postfix does not resolve aliases until after the incoming mail is accepted.
check_policy_service
restriction tied to the Dovecot quota-status
service) with the recipient (the alias).dovecot: auth: passwd-file(alias@example.com): unknown user
is logged, Postfix is then informed that the recipient mailbox is not full even if it actually was (since no such user exists in the Dovecot UserDB).As a workaround to this problem with the ENABLE_QUOTAS=1
feature, DMS will add aliases as fake users into Dovecot UserDB (that are configured with the same data as the real address the alias would resolve to, thus sharing the same mailbox location and quota limit). This allows Postfix to properly be aware of an aliased mailbox having exceeded the allowed quota.
NOTE: This workaround only supports aliases to a single target recipient of a real account address / mailbox.
quota-status
service and returning that response to Postfix.Info
Subaddressing (aka Plus Addressing or Address Tags) is a feature that allows you to receive mail to an address which includes a tag appended to the local-part
of a valid account address.
+
), followed by the tag: <local-part>+<tag>@<domain-part>
user+github@example.com
would deliver mail to the same mailbox as user@example.com
.+
and @
is understood as the tag, no additional configuration required.A common use-case is to use a unique tag for each service you register your email address with.
Add recipient_delimiter = +
to these config override files (replacing +
with your preferred delimiter):
docker-data/dms/config/postfix-main.cf
docker-data/dms/config/dovecot.cf
Follow the advice to change the tag delimiter, but instead set an empty value (recipient_delimiter =
).
Do not attempt to send mail from these tagged addresses, they are not equivalent to aliases.
This feature is only intended to be used when a mail client sends to a DMS managed recipient address. While DMS does not restrict the sender address you choose to send mail from (provided SPOOF_PROTECTION
has not been enabled), it is often forbidden by mail services.
The configured tag delimiter (+
) allows both Postfix and Dovecot to recognize subaddresses. Without this feature configured, the subaddresses would be considered as separate mail accounts rather than routed to a common account address.
Internally DMS has the tag delimiter configured by:
main.cf
setting: recipient_delimiter = +
+
by default: recipient_delimiter = +
Info
This section provides insight for understanding how Postfix and Dovecot services are involved. It is intended as a reference for maintainers and contributors.
Postfix needs to know how to handle inbound and outbound mail by asking these queries:
InboundOutboundSPOOF_PROTECTION=1
, how should DMS restrict the sender address? (eg: Users may only send mail from their associated mailbox address)Dovecot additionally handles authenticating user accounts for sending and retrieving mail:
Dovecot splits all authentication lookups into two categories:
setup
CLI","text":"The best way to manage DMS accounts and related config files is through our setup
CLI provided within the container.
Using the setup
CLI
Try the following within the DMS container (docker exec -it <CONTAINER NAME> bash
):
setup email add <EMAIL ADDRESS>
setup alias add <FROM ALIAS> <TO TARGET ADDRESS>
setup help
# Starts a basic DMS instance and then shells into the container to use the `setup` CLI:\ndocker run --rm -itd --name dms --hostname mail.example.com mailserver/docker-mailserver\ndocker exec -it dms bash\n\n# Create an account:\nsetup email add hello@example.com your-password-here\n\n# Create an alias:\nsetup alias add your-alias-here@example.com hello@example.com\n\n# Limit the mailbox capacity to 10 MiB:\nsetup quota set hello@example.com 10M\n
Secure password input When you don't provide a password to the command, you will be prompted for one. This avoids the password being captured in your shell history.
# As you input your password it will not update.\n# Press the ENTER key to apply the hidden password input.\n$ setup email add hello@example.com\nEnter Password:\nConfirm Password:\n
Account removal via setup email del
When you remove a DMS account with this command, it will also remove any associated aliases and quota.
The command will also prompt for deleting the account mailbox from disk, or can be forced with the -y
flag.
These config files belong to the Config Volume.
"},{"location":"config/account-management/provisioner/file/#accounts","title":"Accounts","text":"Info
Config file: docker-data/dms/config/postfix-accounts.cf
The config format is line-based with two fields separated by the delimiter |
:
secret
).setup email add
command A compatible password hash can be generated with:
doveadm pw -s SHA512-CRYPT -u hello@example.com -p secret\n
postfix-accounts.cf
config file
In this example DMS manages mail for the domain example.com
:
hello@example.com|{SHA512-CRYPT}$6$W4rxRQwI6HNMt9n3$riCi5/OqUxnU8eZsOlZwoCnrNgu1gBGPkJc.ER.LhJCu7sOg9i1kBrRIistlBIp938GdBgMlYuoXYUU5A4Qiv0\n
Dovecot \"extra fields\"
Appending a third column will customize \"extra fields\" when converting account data into a Dovecot UserDB entry.
DMS is not aware of these customizations beyond carrying them over, expect potential for bugs when this feature breaks any assumed conventions used in the scripts (such as changing the mailbox path or type).
Note
Account creation will normalize the provided email address to lowercase, as DMS does not support multiple case-sensitive address variants.
The email address chosen will also represent the login username credential for mail clients to authenticate with.
"},{"location":"config/account-management/provisioner/file/#aliases","title":"Aliases","text":"Info
Config file: docker-data/dms/config/postfix-virtual.cf
The config format is line-based with key value pairs (alias --> target address), with white-space as a delimiter.
postfix-virtual.cf
config file
In this example DMS manages mail for the domain example.com
:
# Alias delivers to an existing account:\nalias1@example.com hello@example.com\n\n# Alias forwards to an external email address:\nalias2@example.com external-account@gmail.com\n
Known Issues setup
CLI prevents an alias and account sharing an address:
You cannot presently add a new account (setup email add
) or alias (setup alias add
) with an address which already exists as an alias or account in DMS.
This restriction was enforced due to problems it could cause, although there are use-cases where you may legitimately require this functionality.
For now you must manually edit the postfix-virtual.cf
file as a workaround. There are no run-time checks outside of the setup
CLI related to this restriction.
Wildcard catch-all support (@example.com
):
While this type of alias without a local-part is supported, you must keep in mind that aliases in Postfix have a higher precedence than a real address associated to a DMS account.
As a result, the wildcard is matched first and will direct mail for that entire domain to the alias target address. To work around this, you will need an alias for each non-alias address of that domain.
Additionally, Postfix will read the alias config and choose the alias value that matches the recipient address first. Ensure your more specific aliases for the domain are declared above the wildcard alias in the config file.
Aliasing to another alias or multiple recipients:
While aliasing to multiple recipients is possible, DMS does not officially support that.
setup alias add
.Info
Config file: docker-data/dms/config/postfix-regexp.cf
This config file is similar to the above postfix-virtual.cf
, but the alias value is instead configured with a regex pattern.
There is no setup
CLI support for this feature, it is config only.
postfix-regexp.cf
config file
Deliver all mail for test
users to qa@example.com
instead:
# Remember to escape regex tokens like `.` => `\\.`, otherwise\n# your alias pattern may be more permissive than you intended:\n/^test[0-9][0-9]*@example\\.com/ qa@example.com\n
Technical Details postfix-virtual.cf
has precedence, postfix-regexp.cf
will only be checked if no alias match was found in postfix-virtual.cf
.
These files are both copied internally to /etc/postfix/
and configured in main.cf
for the virtual_alias_maps
setting. As postfix-virtual.cf
is declared first for that setting, it will be processed before using postfix-regexp.cf
as a fallback.
Info
Config file: docker-data/dms/config/dovecot-quotas.cf
The config format is line-based with two fields separated by the delimiter :
:
postfix-accounts.cf
.M
=> MiB
, G
=> GiB
).dovecot-quotas.cf
config file
For the account with the mailbox address of hello@example.com
, it may not exceed 5 GiB in storage:
hello@example.com:5G\n
"},{"location":"config/account-management/provisioner/ldap/","title":"Account Management | Provisioner (LDAP)","text":""},{"location":"config/account-management/provisioner/ldap/#introduction","title":"Introduction","text":"Getting started with ldap and DMS we need to take 3 parts in account:
postfix
for incoming & outgoing emaildovecot
for accessing mailboxessaslauthd
for SMTP authentication (this can also be delegated to dovecot)Have a look at the ENV page for information on the default values.
"},{"location":"config/account-management/provisioner/ldap/#ldap_query_filter_","title":"LDAP_QUERY_FILTER_*
","text":"Those variables contain the LDAP lookup filters for postfix, using %s
as the placeholder for the domain or email address in question. This means that...
DOMAIN
filter (see virtual_alias_domains
).USER
, ALIAS
and GROUP
filters.USER
filter specifies personal mailboxes, for which only one should exist per address, for example (mail=%s)
(also see virtual_mailbox_maps
)ALIAS
filter specifies aliases for mailboxes, using virtual_alias_maps
, for example (mailAlias=%s)
GROUP
filter specifies the personal mailboxes in a group (for emails that multiple people shall receive), using virtual_alias_maps
, for example (mailGroupMember=%s)
.ALIAS
and GROUP
, but ideally you should use ALIAS
for personal aliases for a singular person (like ceo@example.org
) and GROUP
for multiple people (like hr@example.org
).SENDERS
filter, and only if the authenticated user is one of the returned entries, the email can be sent.SPOOF_PROTECTION=1
.SENDERS
filter is missing, the USER
, ALIAS
and GROUP
filters will be used in a disjunction (OR).admin
group to spoof any sender email address, and to force everyone else to only use their personal mailbox address for outgoing email, you can use something like this: (|(memberOf=cn=admin,*)(mail=%s))
A really simple LDAP_QUERY_FILTER
configuration, using only the user filter and allowing only admin@*
to spoof any sender addresses.
- LDAP_START_TLS=yes\n- ACCOUNT_PROVISIONER=LDAP\n- LDAP_SERVER_HOST=ldap.example.org\n- LDAP_SEARCH_BASE=dc=example,dc=org\"\n- LDAP_BIND_DN=cn=admin,dc=example,dc=org\n- LDAP_BIND_PW=mypassword\n- SPOOF_PROTECTION=1\n\n- LDAP_QUERY_FILTER_DOMAIN=(mail=*@%s)\n- LDAP_QUERY_FILTER_USER=(mail=%s)\n- LDAP_QUERY_FILTER_ALIAS=(|) # doesn't match anything\n- LDAP_QUERY_FILTER_GROUP=(|) # doesn't match anything\n- LDAP_QUERY_FILTER_SENDERS=(|(mail=%s)(mail=admin@*))\n
"},{"location":"config/account-management/provisioner/ldap/#dovecot__filter-dovecot__attrs","title":"DOVECOT_*_FILTER
& DOVECOT_*_ATTRS
","text":"These variables specify the LDAP filters that dovecot uses to determine if a user can log in to their IMAP account, and which mailbox is responsible to receive email for a specific postfix user.
This is split into the following two lookups, both using %u
as the placeholder for the full login name (see dovecot documentation for a full list of placeholders). Usually you only need to set DOVECOT_USER_FILTER
, in which case it will be used for both filters.
DOVECOT_USER_FILTER
is used to get the account details (uid, gid, home directory, quota, ...) of a user.DOVECOT_PASS_FILTER
is used to get the password information of the user, and is in pretty much all cases identical to DOVECOT_USER_FILTER
(which is the default behavior if left away).If your directory doesn't have the postfix-book schema installed, then you must change the internal attribute handling for dovecot. For this you have to change the pass_attr
and the user_attr
mapping, as shown in the example below:
- DOVECOT_PASS_ATTRS=<YOUR_USER_IDENTIFIER_ATTRIBUTE>=user,<YOUR_USER_PASSWORD_ATTRIBUTE>=password\n- DOVECOT_USER_ATTRS=<YOUR_USER_HOME_DIRECTORY_ATTRIBUTE>=home,<YOUR_USER_MAILSTORE_ATTRIBUTE>=mail,<YOUR_USER_MAIL_UID_ATTRIBUTE>=uid,<YOUR_USER_MAIL_GID_ATTRIBUTE>=gid\n
Note
For DOVECOT_*_ATTRS
, you can replace ldapAttr=dovecotAttr
with =dovecotAttr=%{ldap:ldapAttr}
for more flexibility, like for example =home=/var/mail/%{ldap:uid}
or just =uid=5000
.
A list of dovecot attributes can be found in the dovecot documentation.
Defaults- DOVECOT_USER_ATTRS=mailHomeDirectory=home,mailUidNumber=uid,mailGidNumber=gid,mailStorageDirectory=mail\n- DOVECOT_PASS_ATTRS=uniqueIdentifier=user,userPassword=password\n- DOVECOT_USER_FILTER=(&(objectClass=PostfixBookMailAccount)(uniqueIdentifier=%n))\n
Example Setup for a directory that has the qmail-schema installed and uses uid
:
- DOVECOT_PASS_ATTRS=uid=user,userPassword=password\n- DOVECOT_USER_ATTRS=homeDirectory=home,qmailUID=uid,qmailGID=gid,mailMessageStore=mail\n- DOVECOT_USER_FILTER=(&(objectClass=qmailUser)(uid=%u)(accountStatus=active))\n
The LDAP server configuration for dovecot will be taken mostly from postfix, other options can be found in the environment section in the docs.
"},{"location":"config/account-management/provisioner/ldap/#dovecot_auth_bind","title":"DOVECOT_AUTH_BIND
","text":"Set this to yes
to enable authentication binds (more details in the dovecot documentation). Currently, only DN lookup is supported without further changes to the configuration files, so this is only useful when you want to bind as a readonly user without the permission to read passwords.
SASLAUTHD_LDAP_FILTER
","text":"This filter is used for saslauthd
, which is called by postfix when someone is authenticating through SMTP (assuming that SASLAUTHD_MECHANISMS=ldap
is being used). Note that you'll need to set up the LDAP server for saslauthd separately from postfix.
The filter variables are explained in detail in the LDAP_SASLAUTHD
file, but unfortunately, this method doesn't really support domains right now - that means that %U
is the only token that makes sense in this variable.
When to use this and how to avoid it
Using a separate filter for SMTP authentication allows you to for example allow noreply@example.org
to send email, but not log in to IMAP or receive email: (&(mail=%U@example.org)(|(memberOf=cn=email,*)(mail=noreply@example.org)))
If you don't want to use a separate filter for SMTP authentication, you can set SASLAUTHD_MECHANISMS=rimap
and SASLAUTHD_MECH_OPTIONS=127.0.0.1
to authenticate against dovecot instead - this means that the DOVECOT_USER_FILTER
and DOVECOT_PASS_FILTER
will be used for SMTP authentication as well.
saslauthd
- ENABLE_SASLAUTHD=1\n- SASLAUTHD_MECHANISMS=ldap\n- SASLAUTHD_LDAP_FILTER=(mail=%U@example.org)\n
"},{"location":"config/account-management/provisioner/ldap/#secure-connection-with-ldaps-or-starttls","title":"Secure Connection with LDAPS or StartTLS","text":"To enable LDAPS, all you need to do is to add the protocol to LDAP_SERVER_HOST
, for example ldaps://example.org:636
.
To enable LDAP over StartTLS (on port 389), you need to set the following environment variables instead (the protocol must not be ldaps://
in this case!):
- LDAP_START_TLS=yes\n- DOVECOT_TLS=yes\n- SASLAUTHD_LDAP_START_TLS=yes\n
"},{"location":"config/account-management/provisioner/ldap/#active-directory-configurations-tested-with-samba4-ad-implementation","title":"Active Directory Configurations (Tested with Samba4 AD Implementation)","text":"In addition to LDAP explanation above, when Docker Mailserver is intended to be used with Active Directory (or the equivalent implementations like Samba4 AD DC) the following points should be taken into consideration:
sAMAccountName
.proxyAddresses
can be used to store email aliases of single users. The convention is to prefix the email aliases with smtp:
(e.g: smtp:some.name@example.com
).uidNumber
, gidNumber
instead of the typical uid
. Assigning different owner to email folders can also be done in this approach, nevertheless there is a bug at the moment in Docker Mailserver that overwrites all permissions when starting the container. Either a manual fix is necessary now, or a temporary workaround to use a hard-coded ldap:uidNumber
that equals to 5000
until this issue is fixed.user-patches.sh
script to modify ldap-groups.cf
so that it includes leaf_result_attribute = mail
and special_result_attribute = member
. This can be achieved simply by:The configuration shown to get the Group to work is from here and here.
# user-patches.sh\n\n...\ngrep -q '^leaf_result_attribute = mail$' /etc/postfix/ldap-groups.cf || echo \"leaf_result_attribute = mail\" >> /etc/postfix/ldap-groups.cf\ngrep -q '^special_result_attribute = member$' /etc/postfix/ldap-groups.cf || echo \"special_result_attribute = member\" >> /etc/postfix/ldap-groups.cf\n...\n
/etc/ldap/ldap.conf
, if the TLS_REQCERT
is demand
/ hard
(default), the CA certificate used to verify the LDAP server certificate must be recognized as a trusted CA. This can be done by volume mounting the ca.crt
file and updating the trust store via a user-patches.sh
script:# user-patches.sh\n\n...\ncp /MOUNTED_FOLDER/ca.crt /usr/local/share/ca-certificates/\nupdate-ca-certificates\n...\n
The changes on the configurations necessary to work with Active Directory (only changes are listed, the rest of the LDAP configuration can be taken from the other examples shown in this documentation):
# If StartTLS is the chosen method to establish a secure connection with Active Directory.\n- LDAP_START_TLS=yes\n- SASLAUTHD_LDAP_START_TLS=yes\n- DOVECOT_TLS=yes\n\n- LDAP_QUERY_FILTER_USER=(&(objectclass=person)(mail=%s))\n- LDAP_QUERY_FILTER_ALIAS=(&(objectclass=person)(proxyAddresses=smtp:%s))\n# Filters Active Directory groups (mail lists). Additional changes on ldap-groups.cf are also required as shown above.\n- LDAP_QUERY_FILTER_GROUP=(&(objectClass=group)(mail=%s))\n- LDAP_QUERY_FILTER_DOMAIN=(mail=*@%s)\n# Allows only Domain admins to send any sender email address, otherwise the sender address must match the LDAP attribute `mail`.\n- SPOOF_PROTECTION=1\n- LDAP_QUERY_FILTER_SENDERS=(|(mail=%s)(proxyAddresses=smtp:%s)(memberOf=cn=Domain Admins,cn=Users,dc=*))\n\n- DOVECOT_USER_FILTER=(&(objectclass=person)(sAMAccountName=%n))\n# At the moment to be able to use %{ldap:uidNumber}, a manual bug fix as described above must be used. Otherwise %{ldap:uidNumber} %{ldap:uidNumber} must be replaced by the hard-coded value 5000.\n- DOVECOT_USER_ATTRS==uid=%{ldap:uidNumber},=gid=5000,=home=/var/mail/%Ln,=mail=maildir:~/Maildir\n- DOVECOT_PASS_ATTRS=sAMAccountName=user,userPassword=password\n- SASLAUTHD_LDAP_FILTER=(&(sAMAccountName=%U)(objectClass=person))\n
"},{"location":"config/account-management/provisioner/ldap/#ldap-setup-examples","title":"LDAP Setup Examples","text":"Basic Setup services:\n mailserver:\n image: ghcr.io/docker-mailserver/docker-mailserver:latest\n container_name: mailserver\n hostname: mail.example.com\n\n ports:\n - \"25:25\"\n - \"143:143\"\n - \"587:587\"\n - \"993:993\"\n\n volumes:\n - ./docker-data/dms/mail-data/:/var/mail/\n - ./docker-data/dms/mail-state/:/var/mail-state/\n - ./docker-data/dms/mail-logs/:/var/log/mail/\n - ./docker-data/dms/config/:/tmp/docker-mailserver/\n - /etc/localtime:/etc/localtime:ro\n\n environment:\n - ENABLE_SPAMASSASSIN=1\n - ENABLE_CLAMAV=1\n - ENABLE_FAIL2BAN=1\n - ENABLE_POSTGREY=1\n\n # >>> Postfix LDAP Integration\n - ACCOUNT_PROVISIONER=LDAP\n - LDAP_SERVER_HOST=ldap.example.org\n - LDAP_BIND_DN=cn=admin,ou=users,dc=example,dc=org\n - LDAP_BIND_PW=mypassword\n - LDAP_SEARCH_BASE=dc=example,dc=org\n - LDAP_QUERY_FILTER_DOMAIN=(|(mail=*@%s)(mailAlias=*@%s)(mailGroupMember=*@%s))\n - LDAP_QUERY_FILTER_USER=(&(objectClass=inetOrgPerson)(mail=%s))\n - LDAP_QUERY_FILTER_ALIAS=(&(objectClass=inetOrgPerson)(mailAlias=%s))\n - LDAP_QUERY_FILTER_GROUP=(&(objectClass=inetOrgPerson)(mailGroupMember=%s))\n - LDAP_QUERY_FILTER_SENDERS=(&(objectClass=inetOrgPerson)(|(mail=%s)(mailAlias=%s)(mailGroupMember=%s)))\n - SPOOF_PROTECTION=1\n # <<< Postfix LDAP Integration\n\n # >>> Dovecot LDAP Integration\n - DOVECOT_USER_FILTER=(&(objectClass=inetOrgPerson)(mail=%u))\n - DOVECOT_PASS_ATTRS=uid=user,userPassword=password\n - DOVECOT_USER_ATTRS==home=/var/mail/%{ldap:uid},=mail=maildir:~/Maildir,uidNumber=uid,gidNumber=gid\n # <<< Dovecot LDAP Integration\n\n # >>> SASL LDAP Authentication\n - ENABLE_SASLAUTHD=1\n - SASLAUTHD_MECHANISMS=ldap\n - SASLAUTHD_LDAP_FILTER=(&(mail=%U@example.org)(objectClass=inetOrgPerson))\n # <<< SASL LDAP Authentication\n\n - SSL_TYPE=letsencrypt\n - PERMIT_DOCKER=host\n\n cap_add:\n - NET_ADMIN\n
Kopano / Zarafa services:\n mailserver:\n image: ghcr.io/docker-mailserver/docker-mailserver:latest\n container_name: mailserver\n hostname: mail.example.com\n\n ports:\n - \"25:25\"\n - \"143:143\"\n - \"587:587\"\n - \"993:993\"\n\n volumes:\n - ./docker-data/dms/mail-data/:/var/mail/\n - ./docker-data/dms/mail-state/:/var/mail-state/\n - ./docker-data/dms/config/:/tmp/docker-mailserver/\n\n environment:\n # We are not using dovecot here\n - SMTP_ONLY=1\n - ENABLE_SPAMASSASSIN=1\n - ENABLE_CLAMAV=1\n - ENABLE_FAIL2BAN=1\n - ENABLE_POSTGREY=1\n - SASLAUTHD_PASSWD=\n\n # >>> SASL Authentication\n - ENABLE_SASLAUTHD=1\n - SASLAUTHD_LDAP_FILTER=(&(sAMAccountName=%U)(objectClass=person))\n - SASLAUTHD_MECHANISMS=ldap\n # <<< SASL Authentication\n\n # >>> Postfix Ldap Integration\n - ACCOUNT_PROVISIONER=LDAP\n - LDAP_SERVER_HOST=<yourLdapContainer/yourLdapServer>\n - LDAP_SEARCH_BASE=dc=mydomain,dc=loc\n - LDAP_BIND_DN=cn=Administrator,cn=Users,dc=mydomain,dc=loc\n - LDAP_BIND_PW=mypassword\n - LDAP_QUERY_FILTER_USER=(&(objectClass=user)(mail=%s))\n - LDAP_QUERY_FILTER_GROUP=(&(objectclass=group)(mail=%s))\n - LDAP_QUERY_FILTER_ALIAS=(&(objectClass=user)(otherMailbox=%s))\n - LDAP_QUERY_FILTER_DOMAIN=(&(|(mail=*@%s)(mailalias=*@%s)(mailGroupMember=*@%s))(mailEnabled=TRUE))\n # <<< Postfix Ldap Integration\n\n # >>> Kopano Integration\n - POSTFIX_DAGENT=lmtp:kopano:2003\n # <<< Kopano Integration\n\n - SSL_TYPE=letsencrypt\n - PERMIT_DOCKER=host\n\n cap_add:\n - NET_ADMIN\n
"},{"location":"config/account-management/supplementary/master-accounts/","title":"Account Management | Master Accounts (Dovecot)","text":"This feature is useful for administrative tasks like hot backups.
Note
This feature is presently not supported with ACCOUNT_PROVISIONER=LDAP
.
Info
A Master Account:
setup
CLI support
Use the setup dovecot-master <add|update|del|list>
commands. These are roughly equivalent to the setup email
subcommands.
Config file: docker-data/dms/config/dovecot-masters.cf
The config format is the same as postfix-accounts.cf
for ACCOUNT_PROVISIONER=FILE
.
The only difference is the account field has no @domain-part
suffix, it is only a username.
The Master Accounts feature in DMS configures the Dovecot Master Users feature with the Dovecot setting auth_master_user_separator
(where the default value is *
).
Info
To login as another DMS account (user@example.com
) with POP3 or IMAP, use the following credentials format:
<LOGIN USERNAME>*<MASTER USER>
(user@example.com*admin
)<MASTER PASSWORD>
Verify login functionality
In the DMS container, you can verify with the testsaslauthd
command:
# Prerequisites:\n# A regular DMS account to test login through a Master Account:\nsetup email add user@example.com secret\n# Add a new Master Account:\nsetup dovecot-master add admin top-secret\n
# Login with credentials format as described earlier:\ntestsaslauthd -u 'user@example.com*admin' -p 'top-secret'\n
Alternatively, any mail client should be able to login the equivalent credentials.
"},{"location":"config/account-management/supplementary/oauth2/","title":"Authentication - OAuth2 / OIDC","text":"This feature enables support for delegating DMS account authentication through to an external Identity Provider (IdP).
Receiving mail requires a DMS account to exist
If you expect DMS to receive mail, you must provision an account into DMS in advance. Otherwise DMS has no awareness of your externally manmaged users and will reject delivery.
There are plans to implement support to provision users through a SCIM 2.0 API. An IdP that can operate as a SCIM Client (eg: Authentik) would then integrate with DMS for user provisioning. Until then you must keep your user accounts in sync manually via your configured ACCOUNT_PROVISIONER
.
XOAUTH2
or OAUTHBEARER
.XOAUTH2 (Googles widely adopted implementation) and OAUTHBEARER (the newer variant standardized by RFC 7628 in 2015) are supported as standards for verifying that a OAuth Bearer Token (RFC 6750 from 2012) is valid at the identity provider that created the token. The token itself in both cases is expected to be can an opaque Access Token, but it is possible to use a JWT ID Token (which encodes additional information into the token itself).
A mail client like Thunderbird has limited OAuth2 / OIDC support. The software maintains a hard-coded list of providers supported. Roundcube is a webmail client that does have support for generic providers, allowing you to integrate with a broader range of IdP services.
Documentation for this feature is WIP
See the initial feature support and existing issues for guidance that has not yet been documented officially.
Verify authentication worksIf you have a compatible mail client you can verify login through that.
CLI - Verify withcurl
# Shell into your DMS container:\ndocker exec -it dms bash\n\n# Adjust these variables for the methods below to use:\nexport AUTH_METHOD='OAUTHBEARER' USER_ACCOUNT='hello@example.com' ACCESS_TOKEN='DMS_YWNjZXNzX3Rva2Vu'\n\n# Authenticate via IMAP (Dovecot):\ncurl --silent --url 'imap://localhost:143' \\\n --login-options \"AUTH=${AUTH_METHOD}\" --user \"${USER_ACCOUNT}\" --oauth2-bearer \"${ACCESS_TOKEN}\" \\\n --request 'LOGOUT' \\\n && grep \"dovecot: imap-login: Login: user=<${USER_ACCOUNT}>, method=${AUTH_METHOD}\" /var/log/mail/mail.log\n\n# Authenticate via SMTP (Postfix), sending a mail with the same sender(from) and recipient(to) address:\n# NOTE: `curl` seems to require `--upload-file` with some mail content provided to test SMTP auth.\ncurl --silent --url 'smtp://localhost:587' \\\n --login-options \"AUTH=${AUTH_METHOD}\" --user \"${USER_ACCOUNT}\" --oauth2-bearer \"${ACCESS_TOKEN}\" \\\n --mail-from \"${USER_ACCOUNT}\" --mail-rcpt \"${USER_ACCOUNT}\" --upload-file - <<< 'RFC 5322 content - not important' \\\n && grep \"postfix/submission/smtpd.*, sasl_method=${AUTH_METHOD}, sasl_username=${USER_ACCOUNT}\" /var/log/mail/mail.log\n
Troubleshooting:
--verbose
to the curl options. This will output the protocol exchange which includes if authentication was successful or failed.curl
commands with grep
on DMS logs (for Dovecot and Postfix services). When not running curl
from the DMS container, ensure you check the logs correctly, or inspect the --verbose
output instead.curl
bug with XOAUTH2
Older releases of curl
have a bug with XOAUTH2
support since 7.80.0
(Nov 2021) but fixed from 8.6.0
(Jan 2024). It treats XOAUTH2
as OAUTHBEARER
.
If you use docker exec
to run curl
from within DMS, the current DMS v14 release (Debian 12 with curl 7.88.1
) is affected by this bug.
This example assumes you have already set up:
Setup Instructions
1. Docker Mailserver2. Authentik3. RoundcubeUpdate your Docker Compose ENV config to include:
compose.yamlservices:\n mailserver:\n env:\n # Enable the feature:\n - ENABLE_OAUTH2=1\n # Specify the user info endpoint URL of the oauth2 server for token inspection:\n - OAUTH2_INTROSPECTION_URL=https://authentik.example.com/application/o/userinfo/\n
https://roundcube.example.com/index.php/login/oauth
for your RoundCube instance.Add the following to oauth2.inc.php
(documentation):
$config['oauth_provider'] = 'generic';\n$config['oauth_provider_name'] = 'Authentik';\n$config['oauth_client_id'] = '<insert client id here>';\n$config['oauth_client_secret'] = '<insert client secret here>';\n$config['oauth_auth_uri'] = 'https://authentik.example.com/application/o/authorize/';\n$config['oauth_token_uri'] = 'https://authentik.example.com/application/o/token/';\n$config['oauth_identity_uri'] = 'https://authentik.example.com/application/o/userinfo/';\n\n// Optional: disable SSL certificate check on HTTP requests to OAuth server. For possible values, see:\n// http://docs.guzzlephp.org/en/stable/request-options.html#verify\n$config['oauth_verify_peer'] = false;\n\n$config['oauth_scope'] = 'email openid profile';\n$config['oauth_identity_fields'] = ['email'];\n\n// Boolean: automatically redirect to OAuth login when opening Roundcube without a valid session\n$config['oauth_login_redirect'] = false;\n
"},{"location":"config/advanced/full-text-search/","title":"Advanced | Full-Text Search","text":""},{"location":"config/advanced/full-text-search/#overview","title":"Overview","text":"Full-text search allows all messages to be indexed, so that mail clients can quickly and efficiently search messages by their full text content. Dovecot supports a variety of community supported FTS indexing backends.
DMS comes pre-installed with two plugins that can be enabled with a dovecot config file.
Please be aware that indexing consumes memory and takes up additional disk space.
"},{"location":"config/advanced/full-text-search/#xapian","title":"Xapian","text":"The dovecot-fts-xapian plugin makes use of Xapian. Xapian enables embedding an FTS engine without the need for additional backends.
The indexes will be stored as a subfolder named xapian-indexes
inside your local mail-data
folder (/var/mail
internally). With the default settings, 10GB of email data may generate around 4GB of indexed data.
While indexing is memory intensive, you can configure the plugin to limit the amount of memory consumed by the index workers. With Xapian being small and fast, this plugin is a good choice for low memory environments (2GB).
"},{"location":"config/advanced/full-text-search/#setup","title":"Setup","text":"To configure fts-xapian
as a dovecot plugin, create a file at docker-data/dms/config/dovecot/fts-xapian-plugin.conf
and place the following in it:
mail_plugins = $mail_plugins fts fts_xapian\n\nplugin {\n fts = xapian\n fts_xapian = partial=3 full=20 verbose=0\n\n fts_autoindex = yes\n fts_enforced = yes\n\n # disable indexing of folders\n # fts_autoindex_exclude = \\Trash\n\n # Index attachements\n # fts_decoder = decode2text\n}\n\nservice indexer-worker {\n # limit size of indexer-worker RAM usage, ex: 512MB, 1GB, 2GB\n vsz_limit = 1GB\n}\n\n# service decode2text {\n# executable = script /usr/libexec/dovecot/decode2text.sh\n# user = dovecot\n# unix_listener decode2text {\n# mode = 0666\n# }\n# }\n
adjust the settings to tune for your desired memory limits, exclude folders and enable searching text inside of attachments
Update compose.yaml
to load the previously created dovecot plugin config file:
services:\n mailserver:\n image: ghcr.io/docker-mailserver/docker-mailserver:latest\n container_name: mailserver\n hostname: mail.example.com\n env_file: mailserver.env\n ports:\n - \"25:25\" # SMTP (explicit TLS => STARTTLS)\n - \"143:143\" # IMAP4 (explicit TLS => STARTTLS)\n - \"465:465\" # ESMTP (implicit TLS)\n - \"587:587\" # ESMTP (explicit TLS => STARTTLS)\n - \"993:993\" # IMAP4 (implicit TLS)\n volumes:\n - ./docker-data/dms/mail-data/:/var/mail/\n - ./docker-data/dms/mail-state/:/var/mail-state/\n - ./docker-data/dms/mail-logs/:/var/log/mail/\n - ./docker-data/dms/config/:/tmp/docker-mailserver/\n - ./docker-data/dms/config/dovecot/fts-xapian-plugin.conf:/etc/dovecot/conf.d/10-plugin.conf:ro\n - /etc/localtime:/etc/localtime:ro\n restart: always\n stop_grace_period: 1m\n cap_add:\n - NET_ADMIN\n
Recreate containers:
docker compose down\ndocker compose up -d\n
Initialize indexing on all users for all mail:
docker compose exec mailserver doveadm index -A -q \\*\n
Run the following command in a daily cron job:
docker compose exec mailserver doveadm fts optimize -A\n
Or like the Spamassassin example shows, you can instead use cron
from within DMS to avoid potential errors if the mail server is not running: Create a system cron file:
# in the compose.yaml root directory\nmkdir -p ./docker-data/dms/cron # if you didn't have this folder before\ntouch ./docker-data/dms/cron/fts_xapian\nchown root:root ./docker-data/dms/cron/fts_xapian\nchmod 0644 ./docker-data/dms/cron/fts_xapian\n
Edit the system cron file nano ./docker-data/dms/cron/fts_xapian
, and set an appropriate configuration:
# Adding `MAILTO=\"\"` prevents cron emailing notifications of the task outcome each run\nMAILTO=\"\"\n#\n# m h dom mon dow user command\n#\n# Everyday 4:00AM, optimize index files\n0 4 * * * root doveadm fts optimize -A\n
Then with compose.yaml
:
services:\n mailserver:\n image: ghcr.io/docker-mailserver/docker-mailserver:latest\n volumes:\n - ./docker-data/dms/cron/fts_xapian:/etc/cron.d/fts_xapian\n
"},{"location":"config/advanced/full-text-search/#further-discussion","title":"Further Discussion","text":"See #905
"},{"location":"config/advanced/ipv6/","title":"Advanced | IPv6","text":"Ample Opportunities for Issues
Numerous bug reports have been raised in the past about IPv6. Please make sure your setup around DMS is correct when using IPv6!
"},{"location":"config/advanced/ipv6/#ipv6-networking-problems-with-docker-defaults","title":"IPv6 networking problems with Docker defaults","text":""},{"location":"config/advanced/ipv6/#what-can-go-wrong","title":"What can go wrong?","text":"If your host system supports IPv6 and an AAAA
DNS record exists to direct IPv6 traffic to DMS, you may experience issues when an IPv6 connection is made:
The impact of losing the real IP of the client connection can negatively affect DMS:
When the host network receives a connection to a containers published port, it is routed to the containers internal network managed by Docker (typically a bridge network).
By default, the Docker daemon only assigns IPv4 addresses to containers, thus it will only accept IPv4 connections (unless a docker-proxy
process is listening, which the default daemon setting userland-proxy: true
enables). With the daemon setting userland-proxy: true
(default), IPv6 connections from the host can also be accepted and routed to containers (even when they only have IPv4 addresses assigned). userland-proxy: false
will require the container to have atleast an IPv6 address assigned.
This can be problematic for IPv6 host connections when internally the container is no longer aware of the original client IPv6 address, as it has been proxied through the IPv4 or IPv6 gateway address of it's connected network (eg: 172.17.0.1
- Docker allocates networks from a set of default subnets).
This can be fixed by enabling a Docker network to assign IPv6 addresses to containers, along with some additional configuration. Alternatively you could configure the opposite to prevent IPv6 connections being made.
"},{"location":"config/advanced/ipv6/#prevent-ipv6-connections","title":"Prevent IPv6 connections","text":"AAAA
DNS record for your DMS FQDN would prevent resolving an IPv6 address to connect to.userland-proxy: false
, which will fail to establish a remote connection to DMS (provided no IPv6 address was assigned).With UFW or Firewalld
When one of these firewall frontends are active, remote clients should fail to connect instead of being masqueraded as the docker network gateway IP. Keep in mind that this only affects remote clients, it does not affect local IPv6 connections originating within the same host.
"},{"location":"config/advanced/ipv6/#enable-proper-ipv6-support","title":"Enable proper IPv6 support","text":"You can enable IPv6 support in Docker for container networks, however compatibility concerns may affect your success.
The official Docker documentation on enabling IPv6 has been improving and is a good resource to reference.
Enable ip6tables
support so that Docker will manage IPv6 networking rules as well. This will allow for IPv6 NAT to work like the existing IPv4 NAT already does for your containers, avoiding the above issue with external connections having their IP address seen as the container network gateway IP (provided an IPv6 address is also assigned to the container).
Configure the following in /etc/docker/daemon.json
{\n \"ip6tables\": true,\n \"experimental\" : true,\n \"userland-proxy\": true\n}\n
experimental: true
is currently required for ip6tables: true
to work.userland-proxy
setting can potentially affect connection behavior for local connections.Now restart the daemon if it's running: systemctl restart docker
.
Next, configure a network with an IPv6 subnet for your container with any of these examples:
Create an IPv6 ULA subnet About these examplesThese examples are focused on a IPv6 ULA subnet which is suitable for most users as described in the next section.
/64
(eg: /112
, which still provides over 65k IPv6 addresses), especially if instead configuring for an IPv6 GUA subnet.default-address-pools
).The preferred approach is with user-defined networks via compose.yaml
(recommended) or CLI with docker network create
:
Create the network in compose.yaml
and attach a service to it:
services:\n mailserver:\n networks:\n - dms-ipv6\n\nnetworks:\n dms-ipv6:\n enable_ipv6: true\n ipam:\n config:\n - subnet: fd00:cafe:face:feed::/64\n
Override the implicit default
network You can optionally avoid the service assignment by overriding the default
user-defined network that Docker Compose generates. Just replace dms-ipv6
with default
.
The Docker Compose default
bridge is not affected by settings for the default bridge
(aka docker0
) in /etc/docker/daemon.json
.
compose.yaml
To reference this network externally (from other compose files or docker run
), assign the networks name
key in compose.yaml
.
Create the network via a CLI command (which can then be used with docker run --network dms-ipv6
):
docker network create --ipv6 --subnet fd00:cafe:face:feed::/64 dms-ipv6\n
Optionally reference it from one or more compose.yaml
files:
services:\n mailserver:\n networks:\n - dms-ipv6\n\nnetworks:\n dms-ipv6:\n external: true\n
This approach is discouraged
The bridge
network is considered legacy.
Add these two extra IPv6 settings to your daemon config. They only apply to the default bridge
docker network aka docker0
(which containers are attached to by default when using docker run
).
{\n \"ipv6\": true,\n \"fixed-cidr-v6\": \"fd00:cafe:face:feed::/64\",\n}\n
Compose projects can also use this network via network_mode
:
services:\n mailserver:\n network_mode: bridge\n
Do not use 2001:db8:1::/64
for your private subnet
The 2001:db8
address prefix is reserved for documentation. Avoid creating a subnet with this prefix.
Presently this is used in examples for Dockers IPv6 docs as a placeholder, while mixed in with private IPv4 addresses which can be misleading.
"},{"location":"config/advanced/ipv6/#configuring-an-ipv6-subnet","title":"Configuring an IPv6 subnet","text":"If you've configured IPv6 address pools in /etc/docker/daemon.json
, you do not need to specify a subnet explicitly. Otherwise if you're unsure what value to provide, here's a quick guide (Tip: Prefer IPv6 ULA, it's the least hassle):
fd00:cafe:face:feed::/64
is an example of a IPv6 ULA subnet. ULA addresses are akin to the private IPv4 subnets you may already be familiar with. You can use that example, or choose your own ULA address. This is a good choice for getting Docker containers to their have networks support IPv6 via NAT like they already do by default with IPv4./64
block assigned to your host, but this varies by provider.ip6tables: true
is not required), you will want a firewall configured to manage which ports are accessible instead as no NAT is involved. Note that this may not be desired if the container should also be reachable via the host IPv4 public address./64
into smaller subnets for Docker to use only portions of the /64
. This can reduce some routing features, and require additional setup / management via a NDP Proxy for your public interface to know of IPv6 assignments managed by Docker and accept external traffic.With Docker CLI or Docker Compose, run a traefik/whoami
container with your IPv6 docker network and port 80 published. You can then send a curl request (or via address in the browser) from another host (as your remote client) with an IPv6 network, the RemoteAddr
value returned should match your client IPv6 address.
docker run --rm -d --network dms-ipv6 -p 80:80 traefik/whoami\n# On a different host, replace `2001:db8::1` with your DMS host IPv6 address\ncurl --max-time 5 http://[2001:db8::1]:80\n
IPv6 gateway IP
If instead of the remote IPv6 address, you may notice the gateway IP for the IPv6 subnet your DMS container belongs to.
This will happen when DMS has an IPv6 IP address assigned, for the same reason as with IPv4, userland-proxy: true
. It indicates that your daemon.json
has not been configured correctly or had the updated config applied for ip6tables :true
+ experimental: true
. Make sure you used systemctl restart docker
after updating daemon.json
.
IPv6 ULA address priority
DNS lookups that have records for both IPv4 and IPv6 addresses (eg: localhost
) may prefer IPv4 over IPv6 (ULA) for private addresses, whereas for public addresses IPv6 has priority. This shouldn't be anything to worry about, but can come across as a surprise when testing your IPv6 setup on the same host instead of from a remote client.
The preference can be controlled with /etc/gai.conf
, and appears was configured this way based on the assumption that IPv6 ULA would never be used with NAT. It should only affect the destination resolved for outgoing connections, which for IPv6 ULA should only really affect connections between your containers / host. In future IPv6 ULA may also be prioritized.
This article describes how to deploy DMS to Kubernetes. We highly recommend everyone to use our community DMS Helm chart.
Requirements
docker run
or Docker Compose).Limited Support
DMS does not officially support Kubernetes. This content is entirely community-supported. If you find errors, please open an issue and raise a PR.
"},{"location":"config/advanced/kubernetes/#manually-writing-manifests","title":"Manually Writing Manifests","text":"If using our Helm chart is not viable for you, here is some guidance to start with your own manifests.
ConfigMap
PersistentVolumeClaim
Service
Certificate
Deployment
Provide the basic configuration via environment variables with a ConfigMap
.
Example
Below is only an example configuration, adjust the ConfigMap
to your own needs.
---\napiVersion: v1\nkind: ConfigMap\n\nmetadata:\n name: mailserver.environment\n\nimmutable: false\n\ndata:\n TLS_LEVEL: modern\n POSTSCREEN_ACTION: drop\n OVERRIDE_HOSTNAME: mail.example.com\n FAIL2BAN_BLOCKTYPE: drop\n POSTMASTER_ADDRESS: postmaster@example.com\n UPDATE_CHECK_INTERVAL: 10d\n POSTFIX_INET_PROTOCOLS: ipv4\n ENABLE_CLAMAV: '1'\n ENABLE_POSTGREY: '0'\n ENABLE_FAIL2BAN: '1'\n AMAVIS_LOGLEVEL: '-1'\n SPOOF_PROTECTION: '1'\n MOVE_SPAM_TO_JUNK: '1'\n ENABLE_UPDATE_CHECK: '1'\n ENABLE_SPAMASSASSIN: '1'\n SUPERVISOR_LOGLEVEL: warn\n SPAMASSASSIN_SPAM_TO_INBOX: '1'\n\n # here, we provide an example for the SSL configuration\n SSL_TYPE: manual\n SSL_CERT_PATH: /secrets/ssl/rsa/tls.crt\n SSL_KEY_PATH: /secrets/ssl/rsa/tls.key\n
You can also make use of user-provided configuration files (e.g. user-patches.sh
, postfix-accounts.cf
, etc), to customize DMS to your needs.
Here is a minimal example that supplies a postfix-accounts.cf
file inline with two users:
---\napiVersion: v1\nkind: ConfigMap\n\nmetadata:\n name: mailserver.files\n\ndata:\n postfix-accounts.cf: |\n test@example.com|{SHA512-CRYPT}$6$someHashValueHere\n other@example.com|{SHA512-CRYPT}$6$someOtherHashValueHere\n
Static Configuration
The inline postfix-accounts.cf
config example above provides file content that is static. It is mounted as read-only at runtime, thus cannot support modifications.
For production deployments, use persistent volumes instead (via PersistentVolumeClaim
). That will enable files like postfix-account.cf
to add and remove accounts, while also persisting those changes externally from the container.
Modularize your ConfigMap
Kustomize can be a useful tool as it supports creating a ConfigMap
from multiple files.
To persist data externally from the DMS container, configure a PersistentVolumeClaim
(PVC).
Make sure you have a storage system (like Longhorn, Rook, etc.) and that you choose the correct storageClassName
(according to your storage system).
Example
---\napiVersion: v1\nkind: PersistentVolumeClaim\n\nmetadata:\n name: data\n\nspec:\n storageClassName: local-path\n accessModes:\n - ReadWriteOnce\n resources:\n requests:\n storage: 25Gi\n
A Service
is required for getting the traffic to the pod itself. It configures a load balancer with the ports you'll need.
The configuration for a Service
affects if the original IP from a connecting client is preserved (this is important). More about this further down below.
Example
---\napiVersion: v1\nkind: Service\n\nmetadata:\n name: mailserver\n labels:\n app: mailserver\n\nspec:\n # `Local` is most likely required, otherwise every incoming request would be identified by the external IP,\n # which will get banned by Fail2Ban when monitored services are not configured for PROXY protocol\n externalTrafficPolicy: Local\n type: LoadBalancer\n\n selector:\n app: mailserver\n\n ports:\n # smtp\n - name: smtp\n port: 25\n targetPort: smtp\n protocol: TCP\n # submissions (ESMTP with implicit TLS)\n - name: submissions\n port: 465\n targetPort: submissions\n protocol: TCP\n # submission (ESMTP with explicit TLS)\n - name: submission\n port: 587\n targetPort: submission\n protocol: TCP\n # imaps (implicit TLS)\n - name: imaps\n port: 993\n targetPort: imaps\n protocol: TCP\n
Using cert-manager
to supply TLS certificates
---\napiVersion: cert-manager.io/v1\nkind: Certificate\n\nmetadata:\n name: mail-tls-certificate-rsa\n\nspec:\n secretName: mail-tls-certificate-rsa\n isCA: false\n privateKey:\n algorithm: RSA\n encoding: PKCS1\n size: 2048\n dnsNames: [mail.example.com]\n issuerRef:\n name: mail-issuer\n kind: Issuer\n
The TLS docs page provides guidance when it comes to certificates and transport layer security.
ECDSA + RSA (fallback)
You could supply RSA certificates as fallback certificates instead, with ECDSA as the primary. DMS supports dual certificates via the ENV SSL_ALT_CERT_PATH
and SSL_ALT_KEY_PATH
.
Always provide sensitive information via a Secret
For storing OpenDKIM keys, TLS certificates, or any sort of sensitive data - you should be using Secret
s.
A Secret
is similar to ConfigMap
, it can be used and mounted as a volume as demonstrated in the Deployment
manifest tab.
The Deployment
config is the most complex component.
ConfigMap
s, persisted storage, etc.---\napiVersion: apps/v1\nkind: Deployment\n\nmetadata:\n name: mailserver\n\n annotations:\n ignore-check.kube-linter.io/run-as-non-root: >-\n 'mailserver' needs to run as root\n ignore-check.kube-linter.io/privileged-ports: >-\n 'mailserver' needs privileged ports\n ignore-check.kube-linter.io/no-read-only-root-fs: >-\n There are too many files written to make the root FS read-only\n\nspec:\n replicas: 1\n selector:\n matchLabels:\n app: mailserver\n\n template:\n metadata:\n labels:\n app: mailserver\n\n annotations:\n container.apparmor.security.beta.kubernetes.io/mailserver: runtime/default\n\n spec:\n hostname: mail\n containers:\n - name: mailserver\n image: ghcr.io/docker-mailserver/docker-mailserver:latest\n imagePullPolicy: IfNotPresent\n\n securityContext:\n # `allowPrivilegeEscalation: true` is required to support SGID via the `postdrop`\n # executable in `/var/mail-state` for Postfix (maildrop + public dirs):\n # https://github.com/docker-mailserver/docker-mailserver/pull/3625\n allowPrivilegeEscalation: true\n readOnlyRootFilesystem: false\n runAsUser: 0\n runAsGroup: 0\n runAsNonRoot: false\n privileged: false\n capabilities:\n add:\n # file permission capabilities\n - CHOWN\n - FOWNER\n - MKNOD\n - SETGID\n - SETUID\n - DAC_OVERRIDE\n # network capabilities\n - NET_ADMIN # needed for F2B\n - NET_RAW # needed for F2B\n - NET_BIND_SERVICE\n # miscellaneous capabilities\n - SYS_CHROOT\n - KILL\n drop: [ALL]\n seccompProfile:\n type: RuntimeDefault\n\n # Tune this to your needs.\n # If you disable ClamAV, you can use less RAM and CPU.\n # This becomes important in case you're low on resources\n # and Kubernetes refuses to schedule new pods.\n resources:\n limits:\n memory: 4Gi\n cpu: 1500m\n requests:\n memory: 2Gi\n cpu: 600m\n\n volumeMounts:\n - name: files\n subPath: postfix-accounts.cf\n mountPath: /tmp/docker-mailserver/postfix-accounts.cf\n readOnly: true\n\n # PVCs\n - name: data\n mountPath: /var/mail\n subPath: data\n readOnly: false\n - name: data\n mountPath: /var/mail-state\n subPath: state\n readOnly: false\n - name: data\n mountPath: /var/log/mail\n subPath: log\n readOnly: false\n\n # certificates\n - name: certificates-rsa\n mountPath: /secrets/ssl/rsa/\n readOnly: true\n\n ports:\n - name: smtp\n containerPort: 25\n protocol: TCP\n - name: submissions\n containerPort: 465\n protocol: TCP\n - name: submission\n containerPort: 587\n - name: imaps\n containerPort: 993\n protocol: TCP\n\n envFrom:\n - configMapRef:\n name: mailserver.environment\n\n restartPolicy: Always\n\n volumes:\n # configuration files\n - name: files\n configMap:\n name: mailserver.files\n\n # PVCs\n - name: data\n persistentVolumeClaim:\n claimName: data\n\n # certificates\n - name: certificates-rsa\n secret:\n secretName: mail-tls-certificate-rsa\n items:\n - key: tls.key\n path: tls.key\n - key: tls.crt\n path: tls.crt\n
"},{"location":"config/advanced/kubernetes/#exposing-your-mail-server-to-the-outside-world","title":"Exposing your Mail Server to the Outside World","text":"The more difficult part with Kubernetes is to expose a deployed DMS instance to the outside world.
The major problem with exposing DMS to the outside world in Kubernetes is to preserve the real client IP. The real client IP is required by DMS for performing IP-based DNS and spam checks.
Kubernetes provides multiple ways to address this; each has its upsides and downsides.
Configure IP ManuallyHost networkUsing the PROXY Protocol Advantages / DisadvantagesRequirements
Service
.A
and PTR
records (which other mail servers will use to verify trust when they receive mail sent from your DMS instance).Example
Assign the DMS Service
an external IP directly, or delegate an LB to assign the IP on your behalf.
The DMS Service
is configured with an \"external IP\" manually. Append your externally reachable IP address to spec.externalIPs
.
---\napiVersion: v1\nkind: Service\n\nmetadata:\n name: mailserver\n labels:\n app: mailserver\n\nspec:\n selector:\n app: mailserver\n ports:\n - name: smtp\n port: 25\n targetPort: smtp\n # ...\n\n externalIPs:\n - 10.20.30.40\n
The config differs depending on your choice of load balancer. This example uses MetalLB.
---\napiVersion: v1\nkind: Service\n\nmetadata:\n name: mailserver\n labels:\n app: mailserver\n annotations:\n metallb.universe.tf/address-pool: mailserver\n\n# ...\n\n---\napiVersion: metallb.io/v1beta1\nkind: IPAddressPool\n\nmetadata:\n name: mail\n namespace: metallb-system\n\nspec:\n addresses: [ <YOUR PUBLIC DEDICATED IP IN CIDR NOTATION> ]\n autoAssign: true\n\n---\napiVersion: metallb.io/v1beta1\nkind: L2Advertisement\n\nmetadata:\n name: mail\n namespace: metallb-system\n\nspec:\n ipAddressPools: [ mailserver ]\n
Advantages / Disadvantages Example
Using hostPort
and hostNetwork: true
is a similar approach to network_mode: host
with Docker Compose.
---\napiVersion: apps/v1\nkind: Deployment\n\nmetadata:\n name: mailserver\n\n# ...\n spec:\n hostNetwork: true\n # ...\n containers:\n # ...\n ports:\n - name: smtp\n containerPort: 25\n hostPort: 25\n - name: submissions\n containerPort: 465\n hostPort: 465\n - name: submission\n containerPort: 587\n hostPort: 587\n - name: imaps\n containerPort: 993\n hostPort: 993\n
Advantages / Disadvantages Service
PROXY protocol is a network protocol for preserving a client\u2019s IP address when the client\u2019s TCP connection passes through a proxy.
It is a common feature supported among reverse-proxy services (NGINX, HAProxy, Traefik), which you may already have handling ingress traffic for your cluster.
flowchart LR\n A(External Mail Server) -->|Incoming connection| B\n subgraph cluster\n B(\"Ingress Acting as a Proxy\") -->|PROXY protocol connection| C(DMS)\n end
For more information on the PROXY protocol, refer to our dedicated docs page on the topic.
Configure the Ingress Controller TraefikNGINXOn Traefik's side, the configuration is very simple.
IngressRouteTCP
that routes to the equivalent internal DMS Service
port which supports PROXY protocol connections.The below snippet demonstrates an example for two entrypoints, submissions
(port 465) and imaps
(port 993).
---\napiVersion: v1\nkind: Service\n\nmetadata:\n name: mailserver\n\nspec:\n # This an optimization to get rid of additional routing steps.\n # Previously \"type: LoadBalancer\"\n type: ClusterIP\n\n---\napiVersion: traefik.io/v1alpha1\nkind: IngressRouteTCP\n\nmetadata:\n name: smtp\n\nspec:\n entryPoints: [ submissions ]\n routes:\n - match: HostSNI(`*`)\n services:\n - name: mailserver\n namespace: mail\n port: subs-proxy # note the 15 character limit here\n proxyProtocol:\n version: 2\n\n---\napiVersion: traefik.io/v1alpha1\nkind: IngressRouteTCP\n\nmetadata:\n name: imaps\n\nspec:\n entryPoints: [ imaps ]\n routes:\n - match: HostSNI(`*`)\n services:\n - name: mailserver\n namespace: mail\n port: imaps-proxy\n proxyProtocol:\n version: 2\n
*-proxy
port name suffix
The IngressRouteTCP
example configs above reference ports with a *-proxy
suffix.
Deployment
manifest, and are scoped to the mailserver
service (via spec.routes.services.name
).With an NGINX ingress controller, add the following to the TCP services config map (as described here):
25: \"mailserver/mailserver:25::PROXY\"\n465: \"mailserver/mailserver:465::PROXY\"\n587: \"mailserver/mailserver:587::PROXY\"\n993: \"mailserver/mailserver:993::PROXY\"\n
Adjust DMS config for Dovecot + Postfix Only ingress should connect to DMS with PROXY protocol While Dovecot will restrict connections via PROXY protocol to only clients trusted configured via haproxy_trusted_networks
, Postfix does not have an equivalent setting. Public clients should always route through ingress to establish a PROXY protocol connection.
You are responsible for properly managing traffic inside your cluster and to ensure that only trustworthy entities can connect to the designated PROXY protocol ports.
With Kubernetes, this is usually the task of the CNI (container network interface).
Advised approach
The \"Separate PROXY protocol ports\" tab below introduces a little more complexity, but provides better compatibility for internal connections to DMS.
Only accept connections with PROXY protocolSeparate PROXY protocol ports for ingressConnections to DMS within the internal cluster will be rejected
The services for these ports can only enable PROXY protocol support by mandating the protocol on all connections for these ports.
This can be problematic when you also need to support internal cluster traffic directly to DMS (instead of routing indirectly through the ingress controller).
Here is an example configuration for Postfix, Dovecot, and the required adjustments for the Deployment
manifest. The port names are adjusted here only to convey the additional context described earlier.
kind: ConfigMap\napiVersion: v1\nmetadata:\n name: mailserver-extra-config\n labels:\n app: mailserver\ndata:\n postfix-main.cf: |\n postscreen_upstream_proxy_protocol = haproxy\n postfix-master.cf: |\n smtp/inet/postscreen_upstream_proxy_protocol=haproxy\n submission/inet/smtpd_upstream_proxy_protocol=haproxy\n submissions/inet/smtpd_upstream_proxy_protocol=haproxy\n dovecot.cf: |\n haproxy_trusted_networks = <YOUR POD CIDR>\n service imap-login {\n inet_listener imap {\n haproxy = yes\n }\n inet_listener imaps {\n haproxy = yes\n }\n }\n# ...\n\n---\nkind: Deployment\napiVersion: apps/v1\nmetadata:\n name: mailserver\nspec:\n template:\n spec:\n containers:\n - name: docker-mailserver\n # ...\n ports:\n - name: smtp-proxy\n containerPort: 25\n protocol: TCP\n - name: imap-proxy\n containerPort: 143\n protocol: TCP\n - name: subs-proxy\n containerPort: 465\n protocol: TCP\n - name: sub-proxy\n containerPort: 587\n protocol: TCP\n - name: imaps-proxy\n containerPort: 993\n protocol: TCP\n # ...\n volumeMounts:\n - name: config\n subPath: postfix-main.cf\n mountPath: /tmp/docker-mailserver/postfix-main.cf\n readOnly: true\n - name: config\n subPath: postfix-master.cf\n mountPath: /tmp/docker-mailserver/postfix-master.cf\n readOnly: true\n - name: config\n subPath: dovecot.cf\n mountPath: /tmp/docker-mailserver/dovecot.cf\n readOnly: true\n
Info
Supporting internal cluster connections to DMS without using PROXY protocol requires both Postfix and Dovecot to be configured with alternative ports for each service port (which only differ by enforcing PROXY protocol connections).
*-proxy
variants).In this example we'll create a copy of the original service ports with PROXY protocol enabled, and increment the port number assigned by 10000
.
Create a user-patches.sh
file to apply these config changes during container startup:
#!/bin/bash\n\n# Duplicate the config for the submission(s) service ports (587 / 465) with adjustments for the PROXY ports (10587 / 10465) and `syslog_name` setting:\npostconf -Mf submission/inet | sed -e s/^submission/10587/ -e 's/submission/submission-proxyprotocol/' >> /etc/postfix/master.cf\npostconf -Mf submissions/inet | sed -e s/^submissions/10465/ -e 's/submissions/submissions-proxyprotocol/' >> /etc/postfix/master.cf\n# Enable PROXY Protocol support for these new service variants:\npostconf -P 10587/inet/smtpd_upstream_proxy_protocol=haproxy\npostconf -P 10465/inet/smtpd_upstream_proxy_protocol=haproxy\n\n# Create a variant for port 25 too (NOTE: Port 10025 is already assigned in DMS to Amavis):\npostconf -Mf smtp/inet | sed -e s/^smtp/12525/ >> /etc/postfix/master.cf\n# Enable PROXY Protocol support (different setting as port 25 is handled via postscreen), optionally configure a `syslog_name` to distinguish in logs:\npostconf -P 12525/inet/postscreen_upstream_proxy_protocol=haproxy 12525/inet/syslog_name=smtp-proxyprotocol\n
For Dovecot, you can configure dovecot.cf
to look like this:
haproxy_trusted_networks = <YOUR POD CIDR>\n\nservice imap-login {\n inet_listener imap-proxied {\n haproxy = yes\n port = 10143\n }\n\n inet_listener imaps-proxied {\n haproxy = yes\n port = 10993\n ssl = yes\n }\n}\n
Update the Deployment
manifest ports
section by appending these new ports:
- name: smtp-proxy\n # not 10025 in this example due to a possible clash with Amavis\n containerPort: 12525\n protocol: TCP\n- name: imap-proxy\n containerPort: 10143\n protocol: TCP\n- name: subs-proxy\n containerPort: 10465\n protocol: TCP\n- name: sub-proxy\n containerPort: 10587\n protocol: TCP\n- name: imaps-proxy\n containerPort: 10993\n protocol: TCP\n
Note
If you use other Dovecot ports (110, 995, 4190), you may want to configure those similar to above. The dovecot.cf
config for these ports is documented here (in the equivalent section of that page).
To enable the fetchmail service to retrieve e-mails, set the environment variable ENABLE_FETCHMAIL
to 1
. Your compose.yaml
file should look like following snippet:
environment:\n - ENABLE_FETCHMAIL=1\n - FETCHMAIL_POLL=300\n
Generate a file called fetchmail.cf
and place it in the docker-data/dms/config/
folder. Your DMS folder should look like this example:
\u251c\u2500\u2500 docker-data/dms/config\n\u2502\u00a0\u00a0 \u251c\u2500\u2500 dovecot.cf\n\u2502\u00a0\u00a0 \u251c\u2500\u2500 fetchmail.cf\n\u2502\u00a0\u00a0 \u251c\u2500\u2500 postfix-accounts.cf\n\u2502\u00a0\u00a0 \u2514\u2500\u2500 postfix-virtual.cf\n\u2514\u2500\u2500 compose.yaml\n
"},{"location":"config/advanced/mail-fetchmail/#configuration","title":"Configuration","text":"Configuration options for fetchmail.cf
are covered at the official fetchmail docs (see the section \"The run control file\" and the table with \"keyword\" column for all settings).
Basic fetchmail.cf
configuration
Retrieve mail from remote-user@somewhere.com
and deliver it to dms-user@example.com
:
poll 'mail.somewhere.com'\nproto imap\nuser 'remote-user'\npass 'secret'\nis 'dms-user@example.com'\n
poll
sets the remote mail server to connect to retrieve mail from.proto
lets you connect via IMAP or POP3.user
and pass
provide the login credentials for the remote mail service account to access.is
configures where the fetched mail will be sent to (eg: your local DMS account in docker-data/dms/config/postfix-accounts.cf
).proto imap
will still delete remote mail once fetched This is due to a separate default setting no keep
. Adding the setting keep
to your config on a new line will prevent deleting the remote copy.
The official docs config examples show a common convention to indent settings on subsequent lines for visually grouping per server.
Minimal syntaxWith optional syntaxpoll 'mail.somewhere.com' proto imap\n user 'john.doe' pass 'secret' is 'johnny@example.com'\n user 'jane.doe' pass 'secret' is 'jane@example.com'\n\npoll 'mail.somewhere-else.com' proto pop3\n user 'john.doe@somewhere-else.com' pass 'secret' is 'johnny@example.com'\n
#
for adding comments.# Retrieve mail for users `john.doe` and `jane.doe` via IMAP at this remote mail server:\npoll 'mail.somewhere.com' with proto imap wants:\n user 'john.doe' with pass 'secret', is 'johnny@example.com' here\n user 'jane.doe' with pass 'secret', is 'jane@example.com' here\n\n# Also retrieve mail from this mail server (but via POP3).\n# NOTE: This could also be all on a single line, or with each key + value as a separate line.\n# Notice how the remote username includes a full email address,\n# Some mail servers like DMS use the full email address as the username:\npoll 'mail.somewhere-else.com' with proto pop3 wants:\n user 'john.doe@somewhere-else.com' with pass 'secret', is 'johnny@example.com' here\n
FETCHMAIL_POLL
ENV: Override default polling interval
By default the fetchmail service will check every 5 minutes for new mail at the configured mail accounts.
environment:\n # The fetchmail polling interval in seconds:\n FETCHMAIL_POLL: 60\n
"},{"location":"config/advanced/mail-fetchmail/#debugging","title":"Debugging","text":"To debug your fetchmail.cf
configuration run this setup debug
command:
docker exec -it dms-container-name setup debug fetchmail\n
Sample output of setup debug fetchmail
fetchmail: 6.3.26 querying outlook.office365.com (protocol POP3) at Mon Aug 29 22:11:09 2016: poll started\nTrying to connect to 132.245.48.18/995...connected.\nfetchmail: Server certificate:\nfetchmail: Issuer Organization: Microsoft Corporation\nfetchmail: Issuer CommonName: Microsoft IT SSL SHA2\nfetchmail: Subject CommonName: outlook.com\nfetchmail: Subject Alternative Name: outlook.com\nfetchmail: Subject Alternative Name: *.outlook.com\nfetchmail: Subject Alternative Name: office365.com\nfetchmail: Subject Alternative Name: *.office365.com\nfetchmail: Subject Alternative Name: *.live.com\nfetchmail: Subject Alternative Name: *.internal.outlook.com\nfetchmail: Subject Alternative Name: *.outlook.office365.com\nfetchmail: Subject Alternative Name: outlook.office.com\nfetchmail: Subject Alternative Name: attachment.outlook.office.net\nfetchmail: Subject Alternative Name: attachment.outlook.officeppe.net\nfetchmail: Subject Alternative Name: *.office.com\nfetchmail: outlook.office365.com key fingerprint: 3A:A4:58:42:56:CD:BD:11:19:5B:CF:1E:85:16:8E:4D\nfetchmail: POP3< +OK The Microsoft Exchange POP3 service is ready. [SABFADEAUABSADAAMQBDAEEAMAAwADAANwAuAGUAdQByAHAAcgBkADAAMQAuAHAAcgBvAGQALgBlAHgAYwBoAGEAbgBnAGUAbABhAGIAcwAuAGMAbwBtAA==]\nfetchmail: POP3> CAPA\nfetchmail: POP3< +OK\nfetchmail: POP3< TOP\nfetchmail: POP3< UIDL\nfetchmail: POP3< SASL PLAIN\nfetchmail: POP3< USER\nfetchmail: POP3< .\nfetchmail: POP3> USER user1@outlook.com\nfetchmail: POP3< +OK\nfetchmail: POP3> PASS *\nfetchmail: POP3< +OK User successfully logged on.\nfetchmail: POP3> STAT\nfetchmail: POP3< +OK 0 0\nfetchmail: No mail for user1@outlook.com at outlook.office365.com\nfetchmail: POP3> QUIT\nfetchmail: POP3< +OK Microsoft Exchange Server 2016 POP3 server signing off.\nfetchmail: 6.3.26 querying outlook.office365.com (protocol POP3) at Mon Aug 29 22:11:11 2016: poll completed\nfetchmail: normal termination, status 1\n
Troubleshoot with this reference compose.yaml
A minimal compose.yaml
example demonstrates how to run two instances of DMS locally, with one instance configured with fetchmail.cf
and the other to simulate a remote mail server to fetch from.
To enable the getmail service to retrieve e-mails set the environment variable ENABLE_GETMAIL
to 1
. Your compose.yaml
file should include the following:
environment:\n - ENABLE_GETMAIL=1\n - GETMAIL_POLL=5\n
In your DMS config volume (eg: docker-data/dms/config/
), add a subdirectory getmail/
for including your getmail config files (eg: imap-example.cf
) for each remote account that you want to retrieve mail from and deliver to the mailbox of a DMS account.
The content of these config files is documented in the next section with an IMAP and POP3 example to reference.
The directory structure should look similar to this:
\u251c\u2500\u2500 docker-data/dms/config\n\u2502\u00a0\u00a0 \u251c\u2500\u2500 dovecot.cf\n\u2502 \u251c\u2500\u2500 getmail\n\u2502\u00a0\u00a0 \u2502 \u251c\u2500\u2500 getmailrc_general.cf\n\u2502\u00a0\u00a0 \u2502 \u251c\u2500\u2500 remote-account1.cf\n\u2502\u00a0\u00a0 \u2502 \u251c\u2500\u2500 remote-account2.cf\n\u2502\u00a0\u00a0 \u251c\u2500\u2500 postfix-accounts.cf\n\u2502\u00a0\u00a0 \u2514\u2500\u2500 postfix-virtual.cf\n\u251c\u2500\u2500 docker-compose.yml\n\u2514\u2500\u2500 README.md\n
"},{"location":"config/advanced/mail-getmail/#configuration","title":"Configuration","text":"A detailed description of the configuration options can be found in the online version of the manual page.
"},{"location":"config/advanced/mail-getmail/#common-options","title":"Common Options","text":"The default options added to each getmail
config are:
[options]\nverbose = 0\nread_all = false\ndelete = false\nmax_messages_per_session = 500\nreceived = false\ndelivered_to = false\n
The DMS integration for Getmail generates a getmailrc
config that prepends the common options of the base config to each remote account config file (*.cf
) found in the DMS Config Volume getmail/
directory.
Change the base options
Add your own base config as getmail/getmailrc_general.cf
into the DMS Config Volume. It will replace the DMS defaults shown above.
This example will:
alice
with password notsecure
.user1@example.com
account that DMS manages.[retriever]\ntype = SimpleIMAPSSLRetriever\nserver = imap.gmail.com\nusername = alice\npassword = notsecure\n[destination]\ntype = MDA_external\npath = /usr/lib/dovecot/deliver\nallow_root_commands = true\narguments =(\"-d\",\"user1@example.com\")\n
POP3 Configuration Just like the IMAP example above, but instead via POP3 protocol if you prefer that over IMAP.
[retriever]\ntype = SimplePOP3SSLRetriever\nserver = pop3.gmail.com\nusername = alice\npassword = notsecure\n[destination]\ntype = MDA_external\npath = /usr/lib/dovecot/deliver\nallow_root_commands = true\narguments =(\"-d\",\"user1@example.com\")\n
"},{"location":"config/advanced/mail-getmail/#polling-interval","title":"Polling Interval","text":"By default the getmail
service checks external mail accounts for new mail every 5 minutes. That polling interval is configurable via the GETMAIL_POLL
ENV variable, with a value in minutes (default: 5, min: 1):
environment:\n - GETMAIL_POLL=1\n
"},{"location":"config/advanced/mail-getmail/#xoauth2-authentication","title":"XOAUTH2 Authentication","text":"It is possible to utilize the getmail-gmail-xoauth-tokens
helper to provide authentication using xoauth2
for gmail (example 12) or Microsoft Office 365 (example 13)
Advice may be outdated
This section was contributed by the community some time ago and some configuration examples may be outdated.
Sieve allows to specify filtering rules for incoming emails that allow for example sorting mails into different folders depending on the title of an email.
Global vs User order
There are global and user specific filters which are filtering the incoming emails in the following order:
Global-before -> User specific -> Global-after
Global filters are applied to EVERY incoming mail for EVERY email address.
docker-data/dms/config/before.dovecot.sieve
or a docker-data/dms/config/after.dovecot.sieve
file with your filter rules.To specify a user-defined Sieve filter place a .dovecot.sieve
file into a virtual user's mail folder (e.g. /var/mail/example.com/user1/home/.dovecot.sieve
). If this file exists dovecot will apply the filtering rules.
It's even possible to install a user provided Sieve filter at startup during users setup: simply include a Sieve file in the docker-data/dms/config/
path for each user login that needs a filter. The file name provided should be in the form <user_login>.dovecot.sieve
, so for example for user1@example.com
you should provide a Sieve file named docker-data/dms/config/user1@example.com.dovecot.sieve
.
An example of a sieve filter that moves mails to a folder INBOX/spam
depending on the sender address:
Example
require [\"fileinto\", \"reject\"];\n\nif address :contains [\"From\"] \"spam@spam.com\" {\n fileinto \"INBOX.spam\";\n} else {\n keep;\n}\n
Warning
That folders have to exist beforehand if sieve should move them.
Another example of a sieve filter that forward mails to a different address:
Example
require [\"copy\"];\n\nredirect :copy \"user2@not-example.com\";\n
Just forward all incoming emails and do not save them locally:
Example
redirect \"user2@not-example.com\";\n
You can also use external programs to filter or pipe (process) messages by adding executable scripts in docker-data/dms/config/sieve-pipe
or docker-data/dms/config/sieve-filter
.
This can be used in lieu of a local alias file, for instance to forward an email to a webservice.
require [\"vnd.dovecot.pipe\"];\npipe \"external-program\";\n
For more examples or a detailed description of the Sieve language have a look at the official site. Other resources are available on the internet where you can find several examples.
"},{"location":"config/advanced/mail-sieve/#subaddress-mailbox-routing","title":"Automatic Sorting Based on Sub-addresses","text":"When mail is delivered to your account, it is possible to organize storing mail into folders by the subaddress (tag) used.
Example: user+<tag>@example.com
to INBOX/<Tag>
This example sorts mail into inbox folders by their tag:
docker-data/dms/config/user@example.com.dovecot.sieverequire [\"envelope\", \"fileinto\", \"mailbox\", \"subaddress\", \"variables\"];\n\n# Check if the mail recipient address has a tag (:detail)\nif envelope :detail :matches \"to\" \"*\" {\n # Create a variable `tag`, with the the captured `to` value normalized (SoCIAL => Social)\n set :lower :upperfirst \"tag\" \"${1}\";\n\n # Store the mail into a folder with the tag name, nested under your inbox folder:\n if mailboxexists \"INBOX.${tag}\" {\n fileinto \"INBOX.${tag}\";\n } else {\n fileinto :create \"INBOX.${tag}\";\n }\n}\n
When receiving mail for user+social@example.com
it would be delivered into the INBOX/Social
folder.
If you want to only handle specific tags, you could replace the envelope condition and tag assignment from the prior example with:
docker-data/dms/config/user@example.com.dovecot.sieve# Instead of `:matches`, use the default comparator `:is` (exact match)\nif envelope :detail \"to\" \"social\" {\n set \"tag\" \"Social\";\n
docker-data/dms/config/user@example.com.dovecot.sieve# Alternatively you can also provide a list of values to match:\nif envelope :detail \"to\" [\"azure\", \"aws\"] {\n set \"tag\" \"Cloud\";\n
docker-data/dms/config/user@example.com.dovecot.sieve# Similar to `:matches`, except `:regex` provides enhanced pattern matching.\n# NOTE: This example needs you to `require` the \"regex\" extension\nif envelope :detail :regex \"to\" \"^cloud-(azure|aws)$\" {\n # Normalize the captured azure/aws tag as the resolved value is no longer fixed:\n set :lower :upperfirst \"vendor\" \"${1}\";\n # If a `.` exists in the tag, it will create nested folders:\n set \"tag\" \"Cloud.${vendor}\";\n
NOTE: There is no need to lowercase the tag in the conditional as the to
value is a case-insensitive check.
recipient_delimiter
(default: +
) configures the tag delimiter. This is where the local-part
of the recipient address will split at, providing the :detail
(tag) value for Sieve.INBOX
is the default namespace configured by Dovecot.
INBOX.
prefix from the sieve script above, the mailbox (folder) for that tag is created at the top-level alongside your Trash and Junk folders..
between INBOX
and ${tag}
is important as a separator to distinguish mailbox names. This can vary by mailbox format or configuration. DMS uses Maildir
by default, which uses .
as the separator.lmtp_save_to_detail_mailbox = yes
can be set in /etc/dovecot/conf.d/20-lmtp.conf
:INBOX.
prefix parts of the example script.The Manage Sieve extension allows users to modify their Sieve script by themselves. The authentication mechanisms are the same as for the main dovecot service. ManageSieve runs on port 4190
and needs to be enabled using the ENABLE_MANAGESIEVE=1
environment variable.
Example
compose.yamlports:\n - \"4190:4190\"\nenvironment:\n - ENABLE_MANAGESIEVE=1\n
All user defined sieve scripts that are managed by ManageSieve are stored in the user's home folder in /var/mail/example.com/user1/home/sieve
. Just one Sieve script might be active for a user and is sym-linked to /var/mail/example.com/user1/home/.dovecot.sieve
automatically.
Note
ManageSieve makes sure to not overwrite an existing .dovecot.sieve
file. If a user activates a new sieve script the old one is backed up and moved to the sieve
folder.
The extension is known to work with the following ManageSieve clients:
DMS has several locations in the container which may be worth persisting externally via Docker Volumes.
docker-data/dms/
for grouping these related volumes.Reference - Volmes for DMS
Our docs may refer to these DMS specific volumes only by name, or the host/container path for brevity.
docker-data/dms/config/
=> /tmp/docker-mailserver/
docker-data/dms/mail-data/
=> /var/mail/
docker-data/dms/mail-state/
=> /var/mail-state/
docker-data/dms/mail-logs/
=> /var/log/mail/
This is the location where mail is delivered to your mailboxes.
"},{"location":"config/advanced/optional-config/#volumes-state","title":"State Volume","text":"Run-time specific state lives here, but so does some data you may want to keep if a failure event occurs (crash, power loss).
Examples of relevant data
When a volume is mounted to /var/mail-state/
/var/mail-state/
directory. Otherwise the original locations vary and would need to be mounted individually./var/mail-state/
(eg: /var/lib/redis
=> /var/mail-state/lib-redis/
).Supported services: Postfix, Dovecot, Fail2Ban, Amavis, PostGrey, ClamAV, SpamAssassin, Rspamd & Redis, Fetchmail, Getmail, LogRotate, PostSRSd, MTA-STS.
Tip
Sometimes it is helpful to disable this volume when troubleshooting to verify if the data stored here is in a bad state (eg: caused by a failure event).
"},{"location":"config/advanced/optional-config/#volumes-log","title":"Logs Volume","text":"This can be a useful volume to persist for troubleshooting needs for the full set of log files.
"},{"location":"config/advanced/optional-config/#volumes-config","title":"Config Volume","text":"Most configuration files for Postfix, Dovecot, etc. are persisted here.
This is a list of all configuration files and directories which are optional, automatically generated / updated by our setup
CLI, or other internal scripts.
setup.sh config dkim
. (Docs: DKIM)SSL_TYPE
is set to self-signed
or custom
. (Docs: SSL)${login}.dovecot.sieve
filter. (Docs: Sieve)${login}.dovecot.sieve
filter. (Docs: Sieve)setup.sh email
script.setup.sh email restrict
.setup.sh email restrict
.setup.sh alias
.<username>:<password>
. Modify via setup.sh relay add-auth <domain> <username> [<password>]
. (Docs: Relay-Hosts Auth)setup.sh relay add-domain
and setup.sh relay exclude-domain
. (Docs: Relay-Hosts Senders)virtual_mailbox_maps
. See the setup-stack.sh
script.virtual_alias_maps
. See the setup-stack.sh
script.virtual_alias_maps
. See the setup-stack.sh
script.virtual_mailbox_domains
. See the setup-stack.sh
script.fail2ban.cf
. (Docs: Fail2Ban)/etc/amavis/conf.d/50-user
file/etc/dovecot/local.conf
. (Docs: Override Dovecot Defaults)user-patches.sh
script)Podman is a daemonless container engine for developing, managing, and running OCI Containers on your Linux System.
About Support for Podman
Please note that Podman is not officially supported as DMS is built and verified on top of the Docker Engine. This content is entirely community supported. If you find errors, please open an issue and provide a PR.
About this Guide
This guide was tested with Fedora 34 using systemd
and firewalld
. Moreover, it requires Podman version >= 3.2. You may be able to substitute dnf
- Fedora's package manager - with others such as apt
.
About Security
Running podman in rootless mode requires additional modifications in order to keep your mailserver secure. Make sure to read the related documentation.
"},{"location":"config/advanced/podman/#installation-in-rootfull-mode","title":"Installation in Rootfull Mode","text":"While using Podman, you can just manage docker-mailserver as what you did with Docker. Your best friend setup.sh
includes the minimum code in order to support Podman since it's 100% compatible with the Docker CLI.
The installation is basically the same. Podman v3.2 introduced a RESTful API that is 100% compatible with the Docker API, so you can use Docker Compose with Podman easily. Install Podman and Docker Compose with your package manager first.
sudo dnf install podman docker-compose\n
Then enable podman.socket
using systemctl
.
systemctl enable --now podman.socket\n
This will create a unix socket locate under /run/podman/podman.sock
, which is the entrypoint of Podman's API. Now, configure docker-mailserver and start it.
export DOCKER_HOST=\"unix:///run/podman/podman.sock\"\ndocker compose up -d mailserver\ndocker compose ps\n
You should see that docker-mailserver is running now.
"},{"location":"config/advanced/podman/#self-start-in-rootfull-mode","title":"Self-start in Rootfull Mode","text":"Podman is daemonless, that means if you want docker-mailserver self-start while boot up the system, you have to generate a systemd file with Podman CLI.
podman generate systemd mailserver > /etc/systemd/system/mailserver.service\nsystemctl daemon-reload\nsystemctl enable --now mailserver.service\n
"},{"location":"config/advanced/podman/#installation-in-rootless-mode","title":"Installation in Rootless Mode","text":"Running rootless containers is one of Podman's major features. But due to some restrictions, deploying docker-mailserver in rootless mode is not as easy compared to rootfull mode.
~/.config
Also notice that Podman's rootless mode is not about running as a non-root user inside the container, but about the mapping of (normal, non-root) host users to root inside the container.
Warning
In order to make rootless DMS work we must modify some settings in the Linux system, it requires some basic linux server knowledge so don't follow this guide if you not sure what this guide is talking about. Podman rootfull mode and Docker are still good and security enough for normal daily usage.
First, enable podman.socket
in systemd's userspace with a non-root user.
systemctl enable --now --user podman.socket\n
The socket file should be located at /var/run/user/$(id -u)/podman/podman.sock
. Then, modify compose.yaml
to make sure all ports are bindings are on non-privileged ports.
services:\n mailserver:\n ports:\n - \"10025:25\" # SMTP (explicit TLS => STARTTLS)\n - \"10143:143\" # IMAP4 (explicit TLS => STARTTLS)\n - \"10465:465\" # ESMTP (implicit TLS)\n - \"10587:587\" # ESMTP (explicit TLS => STARTTLS)\n - \"10993:993\" # IMAP4 (implicit TLS)\n
Then, setup your mailserver.env
file follow the documentation and use Docker Compose to start the container.
export DOCKER_HOST=\"unix:///var/run/user/$(id -u)/podman/podman.sock\"\ndocker compose up -d mailserver\ndocker compose ps\n
"},{"location":"config/advanced/podman/#security-in-rootless-mode","title":"Security in Rootless Mode","text":"In rootless mode, podman resolves all incoming IPs as localhost, which results in an open gateway in the default configuration. There are two workarounds to fix this problem, both of which have their own drawbacks.
"},{"location":"config/advanced/podman/#enforce-authentication-from-localhost","title":"Enforce authentication from localhost","text":"The PERMIT_DOCKER
variable in the mailserver.env
file allows to specify trusted networks that do not need to authenticate. If the variable is left empty, only requests from localhost and the container IP are allowed, but in the case of rootless podman any IP will be resolved as localhost. Setting PERMIT_DOCKER=none
enforces authentication also from localhost, which prevents sending unauthenticated emails.
The second workaround is slightly more complicated because the compose.yaml
has to be modified. As shown in the fail2ban section the slirp4netns
network driver has to be enabled. This network driver enables podman to correctly resolve IP addresses but it is not compatible with user defined networks which might be a problem depending on your setup.
Rootless Podman requires adding the value slirp4netns:port_handler=slirp4netns
to the --network
CLI option, or network_mode
setting in your compose.yaml
.
You must also add the ENV NETWORK_INTERFACE=tap0
, because Podman uses a hard-coded interface name for slirp4netns
.
Example
services:\n mailserver:\n network_mode: \"slirp4netns:port_handler=slirp4netns\"\n environment:\n - NETWORK_INTERFACE=tap0\n ...\n
Note
podman-compose
is not compatible with this configuration.
Generate a systemd file with the Podman CLI.
podman generate systemd mailserver > ~/.config/systemd/user/mailserver.service\nsystemctl --user daemon-reload\nsystemctl enable --user --now mailserver.service\n
Systemd's user space service is only started when a specific user logs in and stops when you log out. In order to make it to start with the system, we need to enable linger with loginctl
loginctl enable-linger <username>\n
Remember to run this command as root user.
"},{"location":"config/advanced/podman/#port-forwarding","title":"Port Forwarding","text":"When it comes to forwarding ports using firewalld
, see these port forwarding docs for more information.
firewall-cmd --permanent --add-forward-port=port=<25|143|465|587|993>:proto=<tcp>:toport=<10025|10143|10465|10587|10993>\n...\n\n# After you set all ports up.\nfirewall-cmd --reload\n
Notice that this will only open the access to the external client. If you want to access privileges port in your server, do this:
firewall-cmd --permanent --direct --add-rule <ipv4|ipv6> nat OUTPUT 0 -p <tcp|udp> -o lo --dport <25|143|465|587|993> -j REDIRECT --to-ports <10025|10143|10465|10587|10993>\n...\n# After you set all ports up.\nfirewall-cmd --reload\n
Just map all the privilege port with non-privilege port you set in compose.yaml before as root user.
"},{"location":"config/advanced/mail-forwarding/aws-ses/","title":"Mail Forwarding | AWS SES","text":"Amazon SES (Simple Email Service) provides a simple way for cloud based applications to send and receive email.
Configuration via ENV
Configure a relay host in DMS to forward all your mail through AWS SES:
RELAY_HOST
should match your AWS SES region.RELAY_PORT
should be set to one of the supported AWS SES SMTP ports (eg: 587 for STARTTLS).RELAY_USER
and RELAY_PASSWORD
should be set to your Amazon SES SMTP credentials.RELAY_HOST=email-smtp.us-west-2.amazonaws.com\nRELAY_PORT=587\n# Alternative to RELAY_HOST + RELAY_PORT which is compatible with LDAP:\nDEFAULT_RELAY_HOST=[email-smtp.us-west-2.amazonaws.com]:587\n\nRELAY_USER=aws-user\nRELAY_PASSWORD=secret\n
Tip
If you have set up AWS Easy DKIM, you can safely skip setting up DKIM as AWS SES will take care of signing your outbound mail.
Verify the relay host is configured correctly
To verify proper operation, send an email to some external account of yours and inspect the mail headers.
You will also see the connection to SES in the mail logs:
postfix/smtp[692]: Trusted TLS connection established to email-smtp.us-west-1.amazonaws.com[107.20.142.169]:25:\n TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)\npostfix/smtp[692]: 8C82A7E7: to=<someone@example.com>, relay=email-smtp.us-west-1.amazonaws.com[107.20.142.169]:25,\n delay=0.35, delays=0/0.02/0.13/0.2, dsn=2.0.0, status=sent (250 Ok 01000154dc729264-93fdd7ea-f039-43d6-91ed-653e8547867c-000000)\n
"},{"location":"config/advanced/mail-forwarding/gmail-smtp/","title":"Mail Forwarding | Configure Gmail as a relay host","text":"This page provides a guide for configuring DMS to use GMAIL as an SMTP relay host.
Configuration via ENV
Configure a relay host in DMS. This example shows how the related ENV settings map to the Gmail service config:
RELAY_HOST
should be configured as advised by Gmail, there are two SMTP endpoints to choose:smtp.gmail.com
(for a personal Gmail account)smtp-relay.gmail.com
(when using Google Workspace)RELAY_PORT
should be set to one of the supported Gmail SMTP ports (eg: 587 for STARTTLS).RELAY_USER
should be your gmail address (user@gmail.com
).RELAY_PASSWORD
should be your App Password, not your personal gmail account password.RELAY_HOST=smtp.gmail.com\nRELAY_PORT=587\n# Alternative to RELAY_HOST + RELAY_PORT which is compatible with LDAP:\nDEFAULT_RELAY_HOST=[smtp.gmail.com]:587\n\nRELAY_USER=username@gmail.com\nRELAY_PASSWORD=secret\n
Tip
setup relay add-auth
instead of the RELAY_USER
+ RELAY_PASSWORD
ENV.smtp-relay.gmail.com
, the DEFAULT_RELAY_HOST
ENV should be all you need as shown in the above example. Credentials can be optional when using Google Workspace (smtp-relay.gmail.com
), which supports restricting connections to trusted IP addresses.Verify the relay host is configured correctly
To verify proper operation, send an email to an external account of yours and inspect the mail headers.
You will also see the connection to the Gmail relay host (smtp.gmail.com
) in the mail logs:
postfix/smtp[910]: Trusted TLS connection established to smtp.gmail.com[64.233.188.109]:587:\n TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits)\npostfix/smtp[910]: 4BCB547D9D: to=<username@gmail.com>, relay=smtp.gmail.com[64.233.188.109]:587,\n delay=2.9, delays=0.01/0.02/1.7/1.2, dsn=2.0.0, status=sent (250 2.0.0 OK 17... - gsmtp)\n
"},{"location":"config/advanced/mail-forwarding/relay-hosts/","title":"Mail Forwarding | Relay Hosts","text":""},{"location":"config/advanced/mail-forwarding/relay-hosts/#what-is-a-relay-host","title":"What is a Relay Host?","text":"An SMTP relay service (aka relay host / smarthost) is an MTA that relays (forwards) mail on behalf of third-parties (it does not manage the mail domains).
When can a relay service can be helpful?
All mail sent outbound from DMS (where the sender address is a DMS account or a virtual alias) will be relayed through the configured relay host.
Configuration via ENV
Configure the default relayhost with either of these ENV:
DEFAULT_RELAY_HOST
(eg: [mail.relay-service.com]:25
)RELAY_HOST
(eg: mail.relay-service.com
) + RELAY_PORT
(default: 25)Most relay services also require authentication configured:
RELAY_USER
+ RELAY_PASSWORD
provides credentials for authenticating with the default relayhost.Providing secrets via ENV
While ENV is convenient, the risk of exposing secrets is higher.
setup relay add-auth
is a better alternative, which manages the credentials via a config file.
You can opt-out with: setup relay exclude-domain <domain>
Outbound mail from senders of that domain will be sent normally (instead of through the configured RELAY_HOST
).
When any relay host credentials are configured
It will still be expected that mail is sent over a secure connection with credentials provided.
Thus this opt-out feature is rarely practical.
"},{"location":"config/advanced/mail-forwarding/relay-hosts/#advanced-configuration","title":"Advanced Configuration","text":"When mail is sent, there is support to change the relay service or the credentials configured based on the sender address domain used.
We provide this support via two config files:
docker-data/dms/config/postfix-relaymap.cf
docker-data/dms/config/postfix-sasl-password.cf
Configure with our setup relay
commands
While you can edit those configs directly, DMS provides these helpful config management commands:
# Configure a sender domain to use a specific relay host:\nsetup relay add-domain <domain> <host> [<port>]\n\n# Configure relay host credentials for a sender domain to use:\nsetup relay add-auth <domain> <username> [<password>]\n\n# Optionally avoid relaying from senders of this domain:\n# NOTE: Only supported when configured with the `RELAY_HOST` ENV!\nsetup relay exclude-domain <domain>\n
Config file: postfix-sasl-password.cf
@domain1.com mailgun-user:secret\n@domain2.com sendgrid-user:secret\n\n# NOTE: This must have an exact match with the relay host in `postfix-relaymap.cf`,\n# `/etc/postfix/relayhost_map`, or the `DEFAULT_RELAY_HOST` ENV.\n# NOTE: Not supported via our setup CLI, but valid config for Postfix.\n[email-smtp.us-west-2.amazonaws.com]:2587 aws-user:secret\n
When Postfix needs to lookup credentials for mail sent outbound, the above config will:
mailgun-user
for mail sent with a sender belonging to @domain1.com
sendgrid-user
for mail sent with a sender belonging to @domain2.com
aws-user
for mail sent through a configured AWS SES relay host (any sender domain).Config file: postfix-relaymap.cf
@domain1.com [smtp.mailgun.org]:587\n@domain2.com [smtp.sendgrid.net]:2525\n\n# Opt-out of relaying:\n@domain3.com\n
When Postfix sends mail outbound from these sender domains, the above config will:
[smtp.mailgun.org]:587
when mail is sent from a sender of @domain1.com
[smtp.sendgrid.net]:2525
when mail is sent from a sender of @domain1.com
@domain3.com
is not sent through a relay (Only applicable when using RELAY_HOST
)compose.yaml
config with several DMS instances demonstrates this feature for local testing.Postfix Settings
Internally this feature is implemented in DMS by relay.sh
.
The relay.sh
script manages configuring these Postfix settings:
# Send all outbound mail through this relay service:\nrelayhost = [smtp.relay-service.com]:587\n\n# Credentials to use:\nsmtp_sasl_password_maps = texthash:/etc/postfix/sasl_passwd\n# Alternative table type examples which do not require a separate file:\n#smtp_sasl_password_maps = static:john.doe@relay-service.com:secret\n#smtp_sasl_password_maps = inline:{ [smtp.relay-service.com]:587=john.doe@relay-service.com:secret }\n\n## Authentication support:\n# Required to provide credentials to the relay service:\nsmtp_sasl_auth_enable = yes\n# Enforces requiring credentials when sending mail outbound:\nsmtp_sasl_security_options = noanonymous\n# Enforces a secure connection (TLS required) to the relay service:\nsmtp_tls_security_level = encrypt\n\n## Support for advanced requirements:\n# Relay service(s) to use instead of direct delivery for specific sender domains:\nsender_dependent_relayhost_maps = texthash:/etc/postfix/relayhost_map\n# Support credentials to a relay service(s) that vary by relay host used or sender domain:\nsmtp_sender_dependent_authentication = yes\n
"},{"location":"config/advanced/maintenance/update-and-cleanup/","title":"Maintenance | Update and Cleanup","text":"containrrr/watchtower
is a service that monitors Docker images for updates, automatically applying them to running containers.
Automatic image updates + cleanup
Run a watchtower
container with access to docker.sock
, enabling the service to manage Docker:
services:\n watchtower:\n image: containrrr/watchtower:latest\n # Automatic cleanup (removes older image pulls from wasting disk space):\n environment:\n - WATCHTOWER_CLEANUP=true\n volumes:\n - /var/run/docker.sock:/var/run/docker.sock\n
The image tag used for a container is monitored for updates (eg: :latest
, :edge
, :13
)
The automatic update support is only for updates to that specific image tag.
:latest
).13
will represent the latest minor + patch release of v13
).Updating only specific containers
By default the watchtower
service will check every 24 hours for new image updates to pull, based on currently running containers (not restricted to only those running within your compose.yaml
).
Images eligible for updates can configured with a custom command
that provides a list of container names, or via other supported options (eg: labels). This configuration is detailed in the watchtower
docs.
Manual cleanup
watchtower
also supports running on-demand with docker run
or compose.yaml
via the --run-once
option.
You can alternatively invoke cleanup of Docker storage directly with:
docker image prune --all
docker system prune --all
(also removes unused containers, networks, build cache).If you omit the --all
option, this will instead only remove \"dangling\" content (eg: Orphaned images).
The Dovecot default configuration can easily be extended providing a docker-data/dms/config/dovecot.cf
file. Dovecot documentation remains the best place to find configuration options.
Your DMS folder structure should look like this example:
\u251c\u2500\u2500 docker-data/dms/config\n\u2502 \u251c\u2500\u2500 dovecot.cf\n\u2502 \u251c\u2500\u2500 postfix-accounts.cf\n\u2502 \u2514\u2500\u2500 postfix-virtual.cf\n\u251c\u2500\u2500 compose.yaml\n\u2514\u2500\u2500 README.md\n
One common option to change is the maximum number of connections per user:
mail_max_userip_connections = 100\n
Another important option is the default_process_limit
(defaults to 100
). If high-security mode is enabled you'll need to make sure this count is higher than the maximum number of users that can be logged in simultaneously.
This limit is quickly reached if users connect to DMS with multiple end devices.
"},{"location":"config/advanced/override-defaults/dovecot/#override-configuration","title":"Override Configuration","text":"For major configuration changes it\u2019s best to override the dovecot configuration files. For each configuration file you want to override, add a list entry under the volumes
key.
services:\n mailserver:\n volumes:\n - ./docker-data/dms/mail-data/:/var/mail/\n - ./docker-data/dms/config/dovecot/10-master.conf:/etc/dovecot/conf.d/10-master.conf\n
You will first need to obtain the configuration from the running container (where mailserver
is the container name):
mkdir -p ./docker-data/dms/config/dovecot\ndocker cp mailserver:/etc/dovecot/conf.d/10-master.conf ./docker-data/dms/config/dovecot/10-master.conf\n
"},{"location":"config/advanced/override-defaults/dovecot/#debugging","title":"Debugging","text":"To debug your dovecot configuration you can use:
./setup.sh debug login doveconf | grep <some-keyword>
docker exec -it mailserver doveconf | grep <some-keyword>
Note
setup.sh
is included in the DMS repository. Make sure to use the one matching your image version release.
The file docker-data/dms/config/dovecot.cf
is copied internally to /etc/dovecot/local.conf
. To verify the file content, run:
docker exec -it mailserver cat /etc/dovecot/local.conf\n
"},{"location":"config/advanced/override-defaults/postfix/","title":"Override the Default Configs | Postfix","text":"Our default Postfix configuration can easily be extended to add parameters or modify existing ones by providing a docker-data/dms/config/postfix-main.cf
. This file uses the same format as Postfix main.cf
does (See official docs for all parameters and syntax rules).
Example
One can easily increase the backwards-compatibility level and set new Postscreen options:
# increase the compatibility level from 2 (default) to 3\ncompatibility_level = 3\n# set a threshold value for Spam detection\npostscreen_dnsbl_threshold = 4\n
How are your changes applied?
The custom configuration you supply is appended to the default configuration located at /etc/postfix/main.cf
, and then postconf -nf
is run to remove earlier duplicate entries that have since been replaced. This happens early during container startup before Postfix is started.
Similarly, it is possible to add a custom docker-data/dms/config/postfix-master.cf
file that will override the standard master.cf
. Note: Each line in this file will be passed to postconf -P
, i.e. the file is not appended as a whole to /etc/postfix/master.cf
like docker-data/dms/config/postfix-main.cf
! The expected format is <service_name>/<type>/<parameter>
, for example:
# adjust the submission \"reject_unlisted_recipient\" option\nsubmission/inet/smtpd_reject_unlisted_recipient=no\n
Attention
There should be no space between the parameter and the value.
Run postconf -Mf
in the container without arguments to see the active master options.
If you'd like to change, patch or alter files or behavior of DMS, you can use a script.
In case you cloned this repository, you can copy the file user-patches.sh.dist
(under config/
) with cp config/user-patches.sh.dist docker-data/dms/config/user-patches.sh
in order to create the user-patches.sh
script.
If you are managing your directory structure yourself, create a docker-data/dms/config/
directory and add the user-patches.sh
file yourself.
# 1. Either create the docker-data/dms/config/ directory yourself\n# or let docker-mailserver create it on initial startup\n/tmp $ mkdir -p docker-data/dms/config/ && cd docker-data/dms/config/\n\n# 2. Create the user-patches.sh file and edit it\n/tmp/docker-data/dms/config $ touch user-patches.sh\n/tmp/docker-data/dms/config $ nano user-patches.sh\n
The contents could look like this:
#!/bin/bash\n\ncat >/etc/amavis/conf.d/50-user << \"END\"\nuse strict;\n\n$undecipherable_subject_tag = undef;\n$admin_maps_by_ccat{+CC_UNCHECKED} = undef;\n\n#------------ Do not modify anything below this line -------------\n1; # ensure a defined return\nEND\n
And you're done. The user patches script runs right before starting daemons. That means, all the other configuration is in place, so the script can make final adjustments.
Note
Many \"patches\" can already be done with the Docker Compose-/Stack-file. Adding hostnames to /etc/hosts
is done with the extra_hosts:
section, sysctl
commands can be managed with the sysctls:
section, etc.
Email auto-discovery means a client email is able to automagically find out about what ports and security options to use, based on the mail server URI. It can help simplify the tedious / confusing task of adding own's email account for non-tech savvy users.
Email clients will search for auto-discoverable settings and prefill almost everything when a user enters its email address
There exists autodiscover-email-settings on which provides IMAP/POP/SMTP/LDAP autodiscover capabilities on Microsoft Outlook/Apple Mail, autoconfig capabilities for Thunderbird or kmail and configuration profiles for iOS/Apple Mail.
"},{"location":"config/best-practices/dkim_dmarc_spf/","title":"DKIM, DMARC & SPF","text":"Cloudflare has written an article about DKIM, DMARC and SPF that we highly recommend you to read to get acquainted with the topic.
Rspamd vs Individual validators
With v12.0.0, Rspamd was integrated into DMS. It can perform validations for DKIM, DMARC and SPF as part of the spam-score-calculation
for an email. DMS provides individual alternatives for each validation that can be used instead of deferring to Rspamd:
opendkim
is used as a milter (like Rspamd)opendmarc
is used as a milter (like Rspamd)policyd-spf
is used in Postfix's smtpd_recipient_restrictions
In a future release Rspamd will become the default for these validations, with a deprecation notice issued prior to the removal of the above alternatives.
We encourage everyone to prefer Rspamd via ENABLE_RSPAMD=1
.
DNS Caches & Propagation
While modern DNS providers are quick, it may take minutes or even hours for new DNS records to become available / propagate.
"},{"location":"config/best-practices/dkim_dmarc_spf/#dkim","title":"DKIM","text":"What is DKIM
DomainKeys Identified Mail (DKIM) is an email authentication method designed to detect forged sender addresses in email (email spoofing), a technique often used in phishing and email spam.
Source
When DKIM is enabled:
DKIM requires a public/private key pair to enable signing (via private key) your outgoing mail, while the receiving end must query DNS to verify (via public key) that the signature is trustworthy.
Verification expiryUnlike your TLS certificate, your DKIM keypair does not have a fixed expiry associated to it.
Instead, an expiry may be included in your DKIM signature for each mail sent, where a receiver will refuse to validate the signature for an email after that expiry date. This is an added precaution to mitigate malicious activity like \"DKIM replay attacks\", where an already delivered email from a third-party with a trustworthy DKIM signature is leveraged by a spammer when sending mail to an MTA which verifies the DKIM signature successfully, enabling the spammer to bypass spam protections.
Unlike a TLS handshake where you are authenticating trust with future communications, with DKIM once the mail has been received and trust of the signature has been verified, the value of verifying the signature again at a later date is less meaningful since the signature was to ensure no tampering had occurred during delivery through the network.
DKIM key rotationYou can rotate your DKIM keypair by switching to a new DKIM selector (and DNS updates), while the previous key and selector remains valid for verification until the last mail signed with that key reaches it's expiry.
DMS does not provide any automation or support for key rotation, nor is it likely to provide a notable security benefit to the typical small scale DMS deployment.
"},{"location":"config/best-practices/dkim_dmarc_spf/#generating-keys","title":"Generating Keys","text":"You'll need to repeat this process if you add any new domains.
You should have:
Creating DKIM Keys
DKIM keys can be generated with good defaults by running:
docker exec -it <CONTAINER NAME> setup config dkim\n
If you need to generate your keys with different settings, check the help
output for supported config options and examples:
docker exec -it <CONTAINER NAME> setup config dkim help\n
As described by the help output, you may need to use the domain
option explicitly when you're using LDAP or Rspamd.
The keypair generated for using with DKIM presently defaults to RSA-2048. This is a good size but you can lower the security to 1024-bit
, or increase it to 4096-bit
(discouraged as that is excessive).
To generate a key with different size (for RSA 1024-bit) run:
setup config dkim keysize 1024\n
RSA Key Sizes >= 4096 Bit
According to RFC 8301, keys are preferably between 1024 and 2048 bits. Keys of size 4096-bit or larger may not be compatible to all systems your mail is intended for.
You should not need a key length beyond 2048-bit. If 2048-bit does not meet your security needs, you may want to instead consider adopting key rotation or switching from RSA to ECC keys for DKIM.
You may need to specify mail domains explicitlyRequired when using LDAP and Rspamd.
setup config dkim
will generate DKIM keys for what is assumed as the primary mail domain (derived from the FQDN assigned to DMS, minus any subdomain).
When the DMS FQDN is mail.example.com
or example.com
, by default this command will generate DKIM keys for example.com
as the primary domain for your users mail accounts (eg: hello@example.com
).
The DKIM generation does not have support to query LDAP for additional mail domains it should know about. If the primary mail domain is not sufficient, then you must explicitly specify any extra domains via the domain
option:
# ENABLE_OPENDKIM=1 (default):\nsetup config dkim domain 'example.com,another-example.com'\n\n# ENABLE_RSPAMD=1 + ENABLE_OPENDKIM=0:\nsetup config dkim domain example.com\nsetup config dkim domain another-example.com\n
OpenDKIM with ACCOUNT_PROVISIONER=FILE
When DMS uses this configuration, it will by default also detect mail domains (from accounts added via setup email add
), generating additional DKIM keys.
DKIM is currently supported by either OpenDKIM or Rspamd:
OpenDKIMRspamdOpenDKIM is currently enabled by default.
After running setup config dkim
, your new DKIM key files (and OpenDKIM config) have been added to /tmp/docker-mailserver/opendkim/
.
Restart required
After restarting DMS, outgoing mail will now be signed with your new DKIM key(s)
Requires opt-in via ENABLE_RSPAMD=1
(and disable the default OpenDKIM: ENABLE_OPENDKIM=0
).
Rspamd provides DKIM support through two separate modules:
If you have multiple domains, you need to:
docker exec -it <CONTAINER NAME> setup config dkim domain <DOMAIN>
for each domain DMS should sign outgoing mail for.dkim_signing.conf
(for which an example is shown below), as the default config only supports one domain.About the Helper Script
The script will persist the keys in /tmp/docker-mailserver/rspamd/dkim/
. Hence, if you are already using the default volume mounts, the keys are persisted in a volume. The script also restarts Rspamd directly, so changes take effect without restarting DMS.
The script provides you with log messages along the way of creating keys. In case you want to read the complete log, use -v
(verbose) or -vv
(very verbose).
In case you have not already provided a default DKIM signing configuration, the script will create one and write it to /etc/rspamd/override.d/dkim_signing.conf
. If this file already exists, it will not be overwritten.
When you're already using the rspamd/override.d/
directory, the file is created inside your volume and therefore persisted correctly. If you are not using rspamd/override.d/
, you will need to persist the file yourself (otherwise it is lost on container restart).
An example of what a default configuration file for DKIM signing looks like can be found by expanding the example below.
DKIM Signing Module Configuration ExamplesA simple configuration could look like this:
# documentation: https://rspamd.com/doc/modules/dkim_signing.html\n\nenabled = true;\n\nsign_authenticated = true;\nsign_local = true;\n\nuse_domain = \"header\";\nuse_redis = false; # don't change unless Redis also provides the DKIM keys\nuse_esld = true;\ncheck_pubkey = true; # you want to use this in the beginning\n\nselector = \"mail\";\n# The path location is searched for a DKIM key with these variables:\n# - `$domain` is sourced from the MIME mail message `From` header\n# - `$selector` is configured for `mail` (as a default fallback)\npath = \"/tmp/docker-mailserver/dkim/keys/$domain/$selector.private\";\n\n# domain specific configurations can be provided below:\ndomain {\n example.com {\n path = \"/tmp/docker-mailserver/rspamd/dkim/mail.private\";\n selector = \"mail\";\n }\n}\n
As shown next:
domain { ... }
section (in the following example: example.com
and example.org
).selectors [ ... ]
array (in the following example, this is done for example.org
).# ...\n\ndomain {\n example.com {\n path = /tmp/docker-mailserver/rspamd/example.com/ed25519.private\";\n selector = \"dkim-ed25519\";\n }\n example.org {\n selectors [\n {\n path = \"/tmp/docker-mailserver/rspamd/dkim/example.org/rsa.private\";\n selector = \"dkim-rsa\";\n },\n {\n path = \"/tmp/docker-mailserver/rspamd/dkim/example.org/ed25519.private\";\n selector = \"dkim-ed25519\";\n }\n ]\n }\n}\n
Support for DKIM Keys using ED25519 This modern elliptic curve is supported by Rspamd, but support by third-parties for verifying Ed25519 DKIM signatures is unreliable.
If you sign your mail with this key type, you should include RSA as a fallback, like shown in the above example.
Let Rspamd Check Your KeysWhen check_pubkey = true;
is set, Rspamd will query the DNS record for each DKIM selector, verifying each public key matches the private key configured.
If there is a mismatch, a warning will be emitted to the Rspamd log /var/log/mail/rspamd.log
.
When mail signed with your DKIM key is sent from your mail server, the receiver needs to check a DNS TXT
record to verify the DKIM signature is trustworthy.
Configuring DNS - DKIM record
When you generated your key in the previous step, the DNS data was saved into a file <selector>.txt
(default: mail.txt
). Use this content to update your DNS via Web Interface or directly edit your DNS Zone file:
Create a new record:
Field Value TypeTXT
Name <selector>._domainkey
(default: mail._domainkey
) TTL Use the default (otherwise 3600 seconds is appropriate) Data File content within ( ... )
(formatted as advised below) When using Rspamd, the helper script has already provided you with the contents (the \"Data\" field) of the DNS record you need to create - you can just copy-paste this text.
<selector>.txt
is already formatted as a snippet for adding to your DNS Zone file.
Just copy/paste the file contents into your existing DNS zone. The TXT
value has been split into separate strings every 255 characters for compatibility.
<selector>.txt
- Formatting the TXT
record value correctly This file was generated for use within a DNS zone file. The file name uses the DKIM selector it was generated with (default DKIM selector is mail
, which creates mail.txt
_).
For your DNS setup, DKIM support needs to create a TXT
record to store the public key for mail clients to use. TXT
records with values that are longer than 255 characters need to be split into multiple parts. This is why the generated <selector>.txt
file (containing your public key for use with DKIM) has multiple value parts wrapped within double-quotes between (
and )
.
A DNS web-interface may handle this separation internally instead, and could expect the value provided all as a single line instead of split. When that is required, you'll need to manually format the value as described below.
Your generated DNS record file (<selector>.txt
) should look similar to this:
mail._domainkey IN TXT ( \"v=DKIM1; k=rsa; \"\n\"p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqQMMqhb1S52Rg7VFS3EC6JQIMxNDdiBmOKZvY5fiVtD3Z+yd9ZV+V8e4IARVoMXWcJWSR6xkloitzfrRtJRwOYvmrcgugOalkmM0V4Gy/2aXeamuiBuUc4esDQEI3egmtAsHcVY1XCoYfs+9VqoHEq3vdr3UQ8zP/l+FP5UfcaJFCK/ZllqcO2P1GjIDVSHLdPpRHbMP/tU1a9mNZ\"\n\"5QMZBJ/JuJK/s+2bp8gpxKn8rh1akSQjlynlV9NI+7J3CC7CUf3bGvoXIrb37C/lpJehS39KNtcGdaRufKauSfqx/7SxA0zyZC+r13f7ASbMaQFzm+/RRusTqozY/p/MsWx8QIDAQAB\"\n) ;\n
Take the content between ( ... )
, and combine all the quote wrapped content and remove the double-quotes including the white-space between them. That is your TXT
record value, the above example would become this:
v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqQMMqhb1S52Rg7VFS3EC6JQIMxNDdiBmOKZvY5fiVtD3Z+yd9ZV+V8e4IARVoMXWcJWSR6xkloitzfrRtJRwOYvmrcgugOalkmM0V4Gy/2aXeamuiBuUc4esDQEI3egmtAsHcVY1XCoYfs+9VqoHEq3vdr3UQ8zP/l+FP5UfcaJFCK/ZllqcO2P1GjIDVSHLdPpRHbMP/tU1a9mNZ5QMZBJ/JuJK/s+2bp8gpxKn8rh1akSQjlynlV9NI+7J3CC7CUf3bGvoXIrb37C/lpJehS39KNtcGdaRufKauSfqx/7SxA0zyZC+r13f7ASbMaQFzm+/RRusTqozY/p/MsWx8QIDAQAB\n
To test that your new DKIM record is correct, query it with the dig
command. The TXT
value response should be a single line split into multiple parts wrapped in double-quotes:
$ dig +short TXT mail._domainkey.example.com\n\"v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqQMMqhb1S52Rg7VFS3EC6JQIMxNDdiBmOKZvY5fiVtD3Z+yd9ZV+V8e4IARVoMXWcJWSR6xkloitzfrRtJRwOYvmrcgugOalkmM0V4Gy/2aXeamuiBuUc4esDQEI3egmtAsHcVY1XCoYfs+9VqoHEq3vdr3UQ8zP/l+FP5UfcaJFCK/ZllqcO2P1GjIDVSHLdPpRHbMP/tU1a9mNZ5QMZBJ/JuJK/s+2bp8gpxKn8rh1akSQjlynlV9NI+7J3CC7CUf3bGvoXIrb37C/lpJehS39\" \"KNtcGdaRufKauSfqx/7SxA0zyZC+r13f7ASbMaQFzm+/RRusTqozY/p/MsWx8QIDAQAB\"\n
"},{"location":"config/best-practices/dkim_dmarc_spf/#dkim-debug","title":"Troubleshooting","text":"MxToolbox has a DKIM Verifier that you can use to check your DKIM DNS record(s).
When using Rspamd, we recommend you turn on check_pubkey = true;
in dkim_signing.conf
. Rspamd will then check whether your private key matches your public key, and you can check possible mismatches by looking at /var/log/mail/rspamd.log
.
With DMS, DMARC is pre-configured out of the box. You may disable extra and excessive DMARC checks when using Rspamd via ENABLE_OPENDMARC=0
.
The only thing you need to do in order to enable DMARC on a \"DNS-level\" is to add new TXT
. In contrast to DKIM, DMARC DNS entries do not require any keys, but merely setting the configuration values. You can either handcraft the entry by yourself or use one of available generators (like this one).
Typically something like this should be good to start with:
_dmarc.example.com. IN TXT \"v=DMARC1; p=none; sp=none; fo=0; adkim=r; aspf=r; pct=100; rf=afrf; ri=86400; rua=mailto:dmarc.report@example.com; ruf=mailto:dmarc.report@example.com\"\n
Or a bit more strict policies (mind p=quarantine
and sp=quarantine
):
_dmarc.example.com. IN TXT \"v=DMARC1; p=quarantine; sp=quarantine; fo=0; adkim=r; aspf=r; pct=100; rf=afrf; ri=86400; rua=mailto:dmarc.report@example.com; ruf=mailto:dmarc.report@example.com\"\n
The DMARC status may not be displayed instantly due to delays in DNS (caches). Dmarcian has a few tools you can use to verify your DNS records.
"},{"location":"config/best-practices/dkim_dmarc_spf/#spf","title":"SPF","text":"What is SPF
Sender Policy Framework (SPF) is a simple email-validation system designed to detect email spoofing by providing a mechanism to allow receiving mail exchangers to check that incoming mail from a domain comes from a host authorized by that domain's administrators.
Source
Disabling the default SPF service policy-spf
Set ENABLE_POLICYD_SPF=0
to opt-out of the default SPF service. Advised when Rspamd is configured to handle SPF instead.
To add a SPF record in your DNS, insert the following line in your DNS zone:
example.com. IN TXT \"v=spf1 mx ~all\"\n
This enables the Softfail mode for SPF. You could first add this SPF record with a very low TTL. SoftFail is a good setting for getting started and testing, as it lets all email through, with spams tagged as such in the mailbox.
After verification, you might want to change your SPF record to v=spf1 mx -all
so as to enforce the HardFail policy. See http://www.open-spf.org/SPF_Record_Syntax for more details about SPF policies.
In any case, increment the SPF record's TTL to its final value.
"},{"location":"config/best-practices/dkim_dmarc_spf/#backup-mx-secondary-mx-for-policyd-spf","title":"Backup MX & Secondary MX forpolicyd-spf
","text":"For whitelisting an IP Address from the SPF test, you can create a config file (see policyd-spf.conf
) and mount that file into /etc/postfix-policyd-spf-python/policyd-spf.conf
.
Example: Create and edit a policyd-spf.conf
file at docker-data/dms/config/postfix-policyd-spf.conf
:
debugLevel = 1\n#0(only errors)-4(complete data received)\n\nskip_addresses = 127.0.0.0/8,::ffff:127.0.0.0/104,::1\n\n# Preferably use IP-Addresses for whitelist lookups:\nWhitelist = 192.168.0.0/31,192.168.1.0/30\n# Domain_Whitelist = mx1.not-example.com,mx2.not-example.com\n
Then add this line to compose.yaml
:
volumes:\n - ./docker-data/dms/config/postfix-policyd-spf.conf:/etc/postfix-policyd-spf-python/policyd-spf.conf\n
"},{"location":"config/best-practices/mta-sts/","title":"Best practices | MTA-STS","text":"MTA-STS is an optional mechanism for a domain to signal support for STARTTLS.
Enable this feature via the ENV setting ENABLE_MTA_STS=1
.
If you have configured DANE
Enabling MTA-STS will by default override DANE if both are configured for a domain.
This can be partially addressed by configuring a dane-only policy resolver before the MTA-STS entry in smtp_tls_policy_maps
. See the postfix-mta-sts-resolver
documentation for further details.
While this feature in DMS supports ensuring STARTTLS is used when mail is sent to another mail server, you may setup similar for mail servers sending mail to DMS.
This requires configuring your DNS and hosting the MTA-STS policy file via a webserver. A good introduction can be found on dmarcian.com.
"},{"location":"config/security/fail2ban/","title":"Security | Fail2Ban","text":"What is Fail2Ban (F2B)?
Fail2ban is an intrusion prevention software framework. Written in the Python programming language, it is designed to prevent against brute-force attacks. It is able to run on POSIX systems that have an interface to a packet-control system or firewall installed locally, such as [NFTables] or TCP Wrapper.
Source
"},{"location":"config/security/fail2ban/#configuration","title":"Configuration","text":"Warning
DMS must be launched with the NET_ADMIN
capability in order to be able to install the NFTables rules that actually ban IP addresses. Thus, either include --cap-add=NET_ADMIN
in the docker run
command, or the equivalent in the compose.yaml
:
cap_add:\n - NET_ADMIN\n
Running Fail2Ban on Older Kernels
DMS configures F2B to use NFTables, not IPTables (legacy). We have observed that older systems, for example NAS systems, do not support the modern NFTables rules. You will need to configure F2B to use legacy IPTables again, for example with the fail2ban-jail.cf
, see the section on configuration further down below.
DMS will automatically ban IP addresses of hosts that have generated 6 failed attempts over the course of the last week. The bans themselves last for one week. The Postfix jail is configured to use mode = extra
in DMS.
What is docker-data/dms/config/
?
This following configuration files inside the docker-data/dms/config/
volume will be copied inside the container during startup
fail2ban-jail.cf
is copied to /etc/fail2ban/jail.d/user-jail.local
fail2ban-fail2ban.cf
is copied to /etc/fail2ban/fail2ban.local
When just running
setup fail2ban\n
the script will show all banned IP addresses.
To get a more detailed status
view, run
setup fail2ban status\n
"},{"location":"config/security/fail2ban/#managing-bans","title":"Managing Bans","text":"You can manage F2B with the setup
script. The usage looks like this:
docker exec <CONTAINER NAME> setup fail2ban [<ban|unban> <IP>]\n
"},{"location":"config/security/fail2ban/#viewing-the-log-file","title":"Viewing the Log File","text":"docker exec <CONTAINER NAME> setup fail2ban log\n
"},{"location":"config/security/fail2ban/#rootless-container","title":"Running Inside A Rootless Container","text":"RootlessKit
is the fakeroot implementation for supporting rootless mode in Docker and Podman. By default, RootlessKit uses the builtin
port forwarding driver, which does not propagate source IP addresses.
It is necessary for F2B to have access to the real source IP addresses in order to correctly identify clients. This is achieved by changing the port forwarding driver to slirp4netns
, which is slower than the builtin driver but does preserve the real source IPs.
For rootless mode in Docker, create ~/.config/systemd/user/docker.service.d/override.conf
with the following content:
Danger
This changes the port driver for all rootless containers managed by Docker. Per container configuration is not supported, if you need that consider Podman instead.
[Service]\nEnvironment=\"DOCKERD_ROOTLESS_ROOTLESSKIT_PORT_DRIVER=slirp4netns\"\n
And then restart the daemon:
$ systemctl --user daemon-reload\n$ systemctl --user restart docker\n
Rootless Podman requires adding the value slirp4netns:port_handler=slirp4netns
to the --network
CLI option, or network_mode
setting in your compose.yaml
:
Example
services:\n mailserver:\n network_mode: \"slirp4netns:port_handler=slirp4netns\"\n environment:\n - ENABLE_FAIL2BAN=1\n - NETWORK_INTERFACE=tap0\n ...\n
You must also add the ENV NETWORK_INTERFACE=tap0
, because Podman uses a hard-coded interface name for slirp4netns
. slirp4netns
is not compatible with user-defined networks!
Info
The Mail crypt plugin is used to secure email messages stored in a Dovecot system. Messages are encrypted before written to storage and decrypted after reading. Both operations are transparent to the user.
In case of unauthorized access to the storage backend, the messages will, without access to the decryption keys, be unreadable to the offending party.
There can be a single encryption key for the whole system or each user can have a key of their own. The used cryptographical methods are widely used standards and keys are stored in portable formats, when possible.
Official Dovecot documentation: https://doc.dovecot.org/configuration_manual/mail_crypt_plugin/
"},{"location":"config/security/mail_crypt/#single-encryption-key-global-method","title":"Single Encryption Key / Global Method","text":"Create 10-custom.conf
and populate it with the following:
# Enables mail_crypt for all services (imap, pop3, etc)\nmail_plugins = $mail_plugins mail_crypt\nplugin {\n mail_crypt_global_private_key = </certs/ecprivkey.pem\n mail_crypt_global_public_key = </certs/ecpubkey.pem\n mail_crypt_save_version = 2\n}\n
Shutdown your mailserver (docker compose down
)
You then need to generate your global EC key. We named them /certs/ecprivkey.pem
and /certs/ecpubkey.pem
in step #1.
The EC key needs to be available in the container. I prefer to mount a /certs directory into the container:
services:\n mailserver:\n image: ghcr.io/docker-mailserver/docker-mailserver:latest\n volumes:\n . . .\n - ./certs/:/certs\n . . .\n
While you're editing the compose.yaml
, add the configuration file:
services:\n mailserver:\n image: ghcr.io/docker-mailserver/docker-mailserver:latest\n volumes:\n . . .\n - ./config/dovecot/10-custom.conf:/etc/dovecot/conf.d/10-custom.conf\n - ./certs/:/certs\n . . .\n
Start the container, monitor the logs for any errors, send yourself a message, and then confirm the file on disk is encrypted:
[root@ip-XXXXXXXXXX ~]# cat -A /mnt/efs-us-west-2/maildata/awesomesite.com/me/cur/1623989305.M6v\ufffdz\ufffd@\ufffd\ufffd m}\ufffd\ufffd,\ufffd\ufffd9\ufffd\ufffd\ufffd\ufffdB*\ufffd247.us-west-2.compute.inE\ufffd\ufffd\\Ck*\ufffd@7795,W=7947:2,\nT\ufffd9\ufffd8t\ufffd6\ufffd\ufffd t\ufffd\ufffd\ufffde\ufffdW\ufffd\ufffdS `\ufffdH\ufffd\ufffdC\ufffd\u06a4 \ufffdyeY\ufffd\ufffdXZ\ufffd\ufffd^\ufffdd\ufffd/\ufffd\ufffd+\ufffdA\n
This should be the minimum required for encryption of the mail while in storage.
"},{"location":"config/security/rspamd/","title":"Security | Rspamd","text":""},{"location":"config/security/rspamd/#about","title":"About","text":"Rspamd is a \"fast, free and open-source spam filtering system\". DMS integrates Rspamd like any other service. We provide a basic but easy to maintain setup of Rspamd.
If you want to take a look at the default configuration files for Rspamd that DMS packs, navigate to target/rspamd/
inside the repository. Please consult the section \"The Default Configuration\" section down below for a written overview.
The following environment variables are related to Rspamd:
ENABLE_RSPAMD
ENABLE_RSPAMD_REDIS
RSPAMD_CHECK_AUTHENTICATED
RSPAMD_GREYLISTING
RSPAMD_HFILTER
RSPAMD_HFILTER_HOSTNAME_UNKNOWN_SCORE
RSPAMD_LEARN
SPAM_SUBJECT
MOVE_SPAM_TO_JUNK
MARK_SPAM_AS_READ
With these variables, you can enable Rspamd itself, and you can enable / disable certain features related to Rspamd.
"},{"location":"config/security/rspamd/#the-default-configuration","title":"The Default Configuration","text":""},{"location":"config/security/rspamd/#other-anti-spam-services","title":"Other Anti-Spam-Services","text":"DMS packs other anti-spam services, like SpamAssassin or Amavis, next to Rspamd. There exist services, like ClamAV (ENABLE_CLAMAV
), that Rspamd can utilize to improve the scanning. Except for ClamAV, we recommend disabling all other anti-spam services when using Rspamd. The basic configuration shown below provides a good starting point.
Attention
Read this section carefully if you want to understand how Rspamd is integrated into DMS and how it works (on a surface level).
Rspamd is integrated as a milter into DMS. When enabled, Postfix's main.cf
configuration file includes the parameter rspamd_milter = inet:localhost:11332
, which is added to smtpd_milters
. As a milter, Rspamd can inspect incoming and outgoing e-mails.
Each mail is assigned what Rspamd calls symbols: when an e-mail matches a specific criterion, the e-mail receives a symbol. Afterward, Rspamd applies a spam score (as usual with anti-spam software) to the e-mail.
Rspamd then adds (a few) headers to the e-mail based on the spam score. Most important is X-Spamd-Result
, which contains an overview of which symbols were applied. It could look like this:
X-Spamd-Result default: False [-2.80 / 11.00]; R_SPF_NA(1.50)[no SPF record]; R_DKIM_ALLOW(-1.00)[example.com:s=dtag1]; DWL_DNSWL_LOW(-1.00)[example.com:dkim]; RWL_AMI_LASTHOP(-1.00)[192.0.2.42:from]; DMARC_POLICY_ALLOW(-1.00)[example.com,none]; RWL_MAILSPIKE_EXCELLENT(-0.40)[192.0.2.42:from]; FORGED_SENDER(0.30)[noreply@example.com,some-reply-address@bounce.example.com]; RCVD_IN_DNSWL_LOW(-0.10)[192.0.2.42:from]; MIME_GOOD(-0.10)[multipart/mixed,multipart/related,multipart/alternative,text/plain]; MIME_TRACE(0.00)[0:+,1:+,2:+,3:+,4:~,5:~,6:~]; RCVD_COUNT_THREE(0.00)[3]; RCPT_COUNT_ONE(0.00)[1]; REPLYTO_DN_EQ_FROM_DN(0.00)[]; ARC_NA(0.00)[]; TO_MATCH_ENVRCPT_ALL(0.00)[]; RCVD_TLS_LAST(0.00)[]; DKIM_TRACE(0.00)[example.com:+]; HAS_ATTACHMENT(0.00)[]; TO_DN_NONE(0.00)[]; FROM_NEQ_ENVFROM(0.00)[noreply@example.com,some-reply-address@bounce.example.com]; FROM_HAS_DN(0.00)[]; REPLYTO_DOM_NEQ_FROM_DOM(0.00)[]; PREVIOUSLY_DELIVERED(0.00)[receiver@anotherexample.com]; ASN(0.00)[asn:3320, ipnet:192.0.2.0/24, country:DE]; MID_RHS_MATCH_FROM(0.00)[]; MISSING_XM_UA(0.00)[]; HAS_REPLYTO(0.00)[some-reply-address@dms-reply.example.com]\n
And then there is a corresponding X-Rspamd-Action
header, which shows the overall result and the action that is taken. In our example, it would be:
X-Rspamd-Action no action\n
Since the score is -2.80
, nothing will happen and the e-mail is not classified as spam. Our custom actions.conf
defines what to do at certain scores:
X-Spam: Yes
);There is more to spam analysis than meets the eye: we have not covered the Bayes training and filters here, nor have we discussed Sieve rules for e-mails that are marked as spam.
Even the calculation of the score with the individual symbols has been presented to you in a simplified manner. But with the knowledge from above, you're equipped to read on and use Rspamd confidently. Keep on reading to understand the integration even better - you will want to know about your anti-spam software, not only to keep the bad e-mail out, but also to make sure the good e-mail arrive properly!
"},{"location":"config/security/rspamd/#workers","title":"Workers","text":"The proxy worker operates in self-scan mode. This simplifies the setup as we do not require a normal worker. You can easily change this though by overriding the configuration by DMS.
DMS does not set a default password for the controller worker. You may want to do that yourself. In setups where you already have an authentication provider in front of the Rspamd webpage, you may want to set the secure_ip
option to \"0.0.0.0/0\"
for the controller worker to disable password authentication inside Rspamd completely.
When Rspamd is enabled, we implicitly also start an instance of Redis in the container:
/var/lib/redis
(or the /var/mail-state/
volume when present).Redis uses /etc/redis/redis.conf
for configuration:
ENABLE_RSPAMD_REDIS=0
(link also details required changes to the DMS Rspamd config).Rspamd provides a web interface, which contains statistics and data Rspamd collects. The interface is enabled by default and reachable on port 11334.
To use the web interface you will need to configure a password, otherwise you won't be able to log in.
Set a custom passwordAdd this line to your rspamd custom-commands.conf
config which sets the password
option of the controller worker:
set-option-for-controller password \"your hashed password here\"\n
The password hash can be generated via the rspamadm pw
command:
docker exec -it <CONTAINER_NAME> rspamadm pw\n
"},{"location":"config/security/rspamd/#dns","title":"DNS","text":"DMS does not supply custom values for DNS servers (to Rspamd). If you need to use custom DNS servers, which could be required when using DNS-based deny/allowlists, you need to adjust options.inc
yourself. Make sure to also read our FAQ page on DNS servers.
Warning
Rspamd heavily relies on a properly working DNS server that it can use to resolve DNS queries. If your DNS server is misconfigured, you will encounter issues when Rspamd queries DNS to assess if mail is spam. Legitimate mail is then unintentionally marked as spam or worse, rejected entirely.
When Rspamd is deciding if mail is spam, it will check DNS records for SPF, DKIM and DMARC. Each of those has an associated symbol for DNS temporary errors with a non-zero weight assigned. That weight contributes towards the spam score assessed by Rspamd which is normally desirable - provided your network DNS is functioning correctly, otherwise when DNS is broken all mail is biased towards spam due to these failed DNS lookups.
Danger
While we do not provide values for custom DNS servers by default, we set soft_reject_on_timeout = true;
by default. This setting will cause a soft reject if a task (presumably a DNS request) timeout takes place.
This setting is enabled to not allow spam to proceed just because DNS requests did not succeed. It could deny legitimate e-mails to pass though too in case your DNS setup is incorrect or not functioning properly.
"},{"location":"config/security/rspamd/#logs","title":"Logs","text":"You can find the Rspamd logs at /var/log/mail/rspamd.log
, and the corresponding logs for Redis, if it is enabled, at /var/log/supervisor/rspamd-redis.log
. We recommend inspecting these logs (with docker exec -it <CONTAINER NAME> less /var/log/mail/rspamd.log
) in case Rspamd does not work as expected.
You can find a list of all Rspamd modules on their website.
"},{"location":"config/security/rspamd/#disabled-by-default","title":"Disabled By Default","text":"DMS disables certain modules (clickhouse
, elastic
, neural
, reputation
, spamassassin
, url_redirector
, metric_exporter
) by default. We believe these are not required in a standard setup, and they would otherwise needlessly use system resources.
You can choose to enable ClamAV, and Rspamd will then use it to check for viruses. Just set the environment variable ENABLE_CLAMAV=1
.
The RBL module is enabled by default. As a consequence, Rspamd will perform DNS lookups to various blacklists. Whether an RBL or a DNSBL is queried depends on where the domain name was obtained: RBL servers are queried with IP addresses extracted from message headers, DNSBL server are queried with domains and IP addresses extracted from the message body [source].
Rspamd and DNS Block Lists
When the RBL module is enabled, Rspamd will do a variety of DNS requests to (amongst other things) DNSBLs. There are a variety of issues involved when using DNSBLs. Rspamd will try to mitigate some of them by properly evaluating all return codes. This evaluation is a best effort though, so if the DNSBL operators change or add return codes, it may take a while for Rspamd to adjust as well.
If you want to use DNSBLs, try to use your own DNS resolver and make sure it is set up correctly, i.e. it should be a non-public & recursive resolver. Otherwise, you might not be able (see this Spamhaus post) to make use of the block lists.
"},{"location":"config/security/rspamd/#providing-custom-settings-overriding-settings","title":"Providing Custom Settings & Overriding Settings","text":"DMS brings sane default settings for Rspamd. They are located at /etc/rspamd/local.d/
inside the container (or target/rspamd/local.d/
in the repository).
What is docker-data/dms/config/
?
If you want to overwrite the default settings or provide your settings, you can place files at docker-data/dms/config/rspamd/override.d/
. Files from this directory are copied to /etc/rspamd/override.d/
during startup. These files forcibly override Rspamd and DMS default settings.
What is the local.d
directory and how does it compare to override.d
?
Clashing Overrides
Note that when also using the custom-commands.conf
file, files in override.d
may be overwritten in case you adjust them manually and with the help of the file.
DMS provides the ability to do simple adjustments to Rspamd modules with the help of a single file. Just place a file called custom-commands.conf
into docker-data/dms/config/rspamd/
. If this file is present, DMS will evaluate it. The structure is simple, as each line in the file looks like this:
COMMAND ARGUMENT1 ARGUMENT2 ARGUMENT3\n
where COMMAND
can be:
disable-module
: disables the module with name ARGUMENT1
enable-module
: explicitly enables the module with name ARGUMENT1
set-option-for-module
: sets the value for option ARGUMENT2
to ARGUMENT3
inside module ARGUMENT1
set-option-for-controller
: set the value of option ARGUMENT1
to ARGUMENT2
for the controller workerset-option-for-proxy
: set the value of option ARGUMENT1
to ARGUMENT2
for the proxy workerset-common-option
: set the option ARGUMENT1
that defines basic Rspamd behavior to value ARGUMENT2
add-line
: this will add the complete line after ARGUMENT1
(with all characters) to the file /etc/rspamd/override.d/<ARGUMENT1>
An Example Is Shown Down Below
File Names & Extensions
For command 1 - 3, we append the .conf
suffix to the module name to get the correct file name automatically. For commands 4 - 6, the file name is fixed (you don't even need to provide it). For command 7, you will need to provide the whole file name (including the suffix) yourself!
You can also have comments (the line starts with #
) and blank lines in custom-commands.conf
- they are properly handled and not evaluated.
Adjusting Modules This Way
These simple commands are meant to give users the ability to easily alter modules and their options. As a consequence, they are not powerful enough to enable multi-line adjustments. If you need to do something more complex, we advise to do that manually!
"},{"location":"config/security/rspamd/#examples-advanced-configuration","title":"Examples & Advanced Configuration","text":""},{"location":"config/security/rspamd/#a-very-basic-configuration","title":"A Very Basic Configuration","text":"Do you want to start using Rspamd? Rspamd is disabled by default, so you need to set the following environment variables:
ENABLE_RSPAMD=1\n# ClamAV is compatible with Rspamd. Optionally enable it for anti-virus support:\nENABLE_CLAMAV=1\n\n# Rspamd replaces the functionality of all these anti-spam services, disable them:\nENABLE_OPENDKIM=0\nENABLE_OPENDMARC=0\nENABLE_POLICYD_SPF=0\nENABLE_AMAVIS=0\nENABLE_SPAMASSASSIN=0\n\n# Provided you've set `RSPAMD_GREYLISTING=1`, also disable Postgrey:\nENABLE_POSTGREY=0\n
This will enable Rspamd and disable services you don't need when using Rspamd.
"},{"location":"config/security/rspamd/#adjusting-and-extending-the-very-basic-configuration","title":"Adjusting and Extending The Very Basic Configuration","text":"Rspamd is running, but you want or need to adjust it? First, create a file named custom-commands.conf
under docker-data/dms/config/rspamd
(which translates to /tmp/docker-mailserver/rspamd/
inside the container). Then add your changes:
set-option-for-controller secure_ip \"0.0.0.0/0\"
.set-option-for-module classifier-bayes autolearn true
.disable-module chartable
.Here is what the file looks like in the end:
# See 1.\n# ATTENTION: this disables authentication on the website - make sure you know what you're doing!\nset-option-for-controller secure_ip \"0.0.0.0/0\"\n\n# See 2.\nset-option-for-module classifier-bayes autolearn true\n\n# See 3.\ndisable-module chartable\n
"},{"location":"config/security/rspamd/#dkim-signing","title":"DKIM Signing","text":"There is a dedicated section for setting up DKIM with Rspamd in our documentation.
"},{"location":"config/security/rspamd/#abusix-integration","title":"Abusix Integration","text":"This subsection provides information about the integration of Abusix, \"a set of blocklists that work as an additional email security layer for your existing mail environment\". The setup is straight-forward and well documented:
<APIKEY>
to your private API keyWe recommend mounting the files directly into the container, as they are rather big and not manageable with our custom-command.conf
script. If mounted to the correct location, Rspamd will automatically pick them up.
While Abusix can be integrated into Postfix, Postscreen and a multitude of other software, we recommend integrating Abusix only into a single piece of software running in your mail server - everything else would be excessive and wasting queries. Moreover, we recommend the integration into suitable filtering software and not Postfix itself, as software like Postscreen or Rspamd can properly evaluate the return codes and other configuration.
"},{"location":"config/security/ssl/","title":"Security | TLS (aka SSL)","text":"There are multiple options to enable SSL (via SSL_TYPE
):
After installation, you can test your setup with:
checktls.com
testssl.sh
Exposure of DNS labels through Certificate Transparency
All public Certificate Authorities (CAs) are required to log certificates they issue publicly via Certificate Transparency. This helps to better establish trust.
When using a public CA for certificates used in private networks, be aware that the associated DNS labels in the certificate are logged publicly and easily searchable. These logs are append only, you cannot redact this information.
You could use a wildcard certificate. This avoids accidentally leaking information to the internet, but keep in mind the potential security risks of wildcard certs.
"},{"location":"config/security/ssl/#the-fqdn","title":"The FQDN","text":"An FQDN (Fully Qualified Domain Name) such as mail.example.com
is required for DMS to function correctly, especially for looking up the correct SSL certificate to use.
mail.example.com
will still use user@example.com
as the mail address. You do not need a bare domain for that.example.com
is also supported.hostname -f
will be used to retrieve the FQDN as configured in the below examples.*.example.com
) are supported for SSL_TYPE=letsencrypt
. Your configured FQDN below may be mail.example.com
, and your wildcard certificate provisioned to /etc/letsencrypt/live/example.com
which will be checked as a fallback FQDN by DMS.Setting the hostname correctly
Change mail.example.com
below to your own FQDN.
# CLI:\ndocker run --hostname mail.example.com\n
or
# compose.yaml\nservices:\n mailserver:\n hostname: mail.example.com\n
"},{"location":"config/security/ssl/#provisioning-methods","title":"Provisioning methods","text":""},{"location":"config/security/ssl/#lets-encrypt-recommended","title":"Let's Encrypt (Recommended)","text":"To enable Let's Encrypt for DMS, you have to:
For your DMS container:
SSL_TYPE=letsencrypt
.letsencrypt
folder as a volume to /etc/letsencrypt
.You don't have to do anything else. Enjoy!
Note
/etc/letsencrypt/live
stores provisioned certificates in individual folders named by their FQDN.
Make sure that the entire folder is mounted to DMS as there are typically symlinks from /etc/letsencrypt/live/mail.example.com
to /etc/letsencrypt/archive
.
Example
Add these additions to the mailserver
service in your compose.yaml
:
services:\n mailserver:\n hostname: mail.example.com\n environment:\n - SSL_TYPE=letsencrypt\n volumes:\n - /etc/letsencrypt:/etc/letsencrypt\n
"},{"location":"config/security/ssl/#example-using-docker-for-lets-encrypt","title":"Example using Docker for Let's Encrypt","text":"Certbot provisions certificates to /etc/letsencrypt
. Add a volume to store these, so that they can later be accessed by DMS container. You may also want to persist Certbot logs, just in case you need to troubleshoot.
Getting a certificate is this simple! (Referencing: Certbot docker instructions and certonly --standalone
mode):
# Requires access to port 80 from the internet, adjust your firewall if needed.\ndocker run --rm -it \\\n -v \"${PWD}/docker-data/certbot/certs/:/etc/letsencrypt/\" \\\n -v \"${PWD}/docker-data/certbot/logs/:/var/log/letsencrypt/\" \\\n -p 80:80 \\\n certbot/certbot certonly --standalone -d mail.example.com\n
Add a volume for DMS that maps the local certbot/certs/
folder to the container path /etc/letsencrypt/
.
Example
Add these additions to the mailserver
service in your compose.yaml
:
services:\n mailserver:\n hostname: mail.example.com\n environment:\n - SSL_TYPE=letsencrypt\n volumes:\n - ./docker-data/certbot/certs/:/etc/letsencrypt\n
The certificate setup is complete, but remember it will expire. Consider automating renewals.
Renewing Certificates
When running the above certonly --standalone
snippet again, the existing certificate is renewed if it would expire within 30 days.
Alternatively, Certbot can look at all the certificates it manages, and only renew those nearing their expiry via the renew
command:
# This will need access to port 443 from the internet, adjust your firewall if needed.\ndocker run --rm -it \\\n -v \"${PWD}/docker-data/certbot/certs/:/etc/letsencrypt/\" \\\n -v \"${PWD}/docker-data/certbot/logs/:/var/log/letsencrypt/\" \\\n -p 80:80 \\\n -p 443:443 \\\n certbot/certbot renew\n
This process can also be automated via cron or systemd timers.
Using a different ACME CA
Certbot does support alternative certificate providers via the --server
option. In most cases you'll want to use the default Let's Encrypt.
certbot-dns-cloudflare
with Docker","text":"If you are unable get a certificate via the HTTP-01
(port 80) or TLS-ALPN-01
(port 443) challenge types, the DNS-01
challenge can be useful (this challenge can additionally issue wildcard certificates). This guide shows how to use the DNS-01
challenge with Cloudflare as your DNS provider.
Obtain a Cloudflare API token:
Click \"Create Token\", and choose the Edit zone DNS
template (Certbot requires the ZONE:DNS:Edit
permission).
Only include the necessary Zone resource configuration
Be sure to configure \"Zone Resources\" section on this page to Include -> Specific zone -> <your zone here>
.
This restricts the API token to only this zone (domain) which is an important security measure.
Store the API token you received in a file cloudflare.ini
with content:
dns_cloudflare_api_token = YOUR_CLOUDFLARE_TOKEN_HERE\n
chmod 600
and chown 0:0
.docker-data/certbot/secrets/
.Your compose.yaml
should include the following:
services:\n mailserver:\n environments:\n # Set SSL certificate type.\n - SSL_TYPE=letsencrypt\n volumes:\n # Mount the cert folder generated by Certbot:\n - ./docker-data/certbot/certs/:/etc/letsencrypt/:ro\n\n certbot-cloudflare:\n image: certbot/dns-cloudflare:latest\n command: certonly --dns-cloudflare --dns-cloudflare-credentials /run/secrets/cloudflare-api-token -d mail.example.com\n volumes:\n - ./docker-data/certbot/certs/:/etc/letsencrypt/\n - ./docker-data/certbot/logs/:/var/log/letsencrypt/\n secrets:\n - cloudflare-api-token\n\n# Docs: https://docs.docker.com/engine/swarm/secrets/#use-secrets-in-compose\n# WARNING: In compose configs without swarm, the long syntax options have no effect,\n# Ensure that you properly `chmod 600` and `chown 0:0` the file on disk. Effectively treated as a bind mount.\nsecrets:\n cloudflare-api-token:\n file: ./docker-data/certbot/secrets/cloudflare.ini\n
Alternative using the docker run
command (secrets
feature is not available):
docker run \\\n --volume \"${PWD}/docker-data/certbot/certs/:/etc/letsencrypt/\" \\\n --volume \"${PWD}/docker-data/certbot/logs/:/var/log/letsencrypt/\" \\\n --volume \"${PWD}/docker-data/certbot/secrets/:/tmp/secrets/certbot/\"\n certbot/dns-cloudflare \\\n certonly --dns-cloudflare --dns-cloudflare-credentials /tmp/secrets/certbot/cloudflare.ini -d mail.example.com\n
Run the service to provision a certificate:
docker compose run certbot-cloudflare\n
You should see the following log output:
Saving debug log to /var/log/letsencrypt/letsencrypt. log | Requesting a certificate for mail.example.com\nWaiting 10 seconds for DNS changes to propagate\nSuccessfully received certificate.\nCertificate is saved at: /etc/letsencrypt/live/mail.example.com/fullchain.pem\nKey is saved at: /etc/letsencrypt/live/mail.example.com/privkey.pem\nThis certificate expires on YYYY-MM-DD.\nThese files will be updated when the certificate renews.\nNEXT STEPS:\n- The certificate will need to be renewed before it expires. Certbot can automatically renew the certificate in background, but you may need to take steps to enable that functionality. See https://certbot.org/renewal instructions.\n
After completing the steps above, your certificate should be ready to use.
Renewing a certificate (Optional)We've only demonstrated how to provision a certificate, but it will expire in 90 days and need to be renewed before then.
In the following example, add a new service (certbot-cloudflare-renew
) into compose.yaml
that will handle certificate renewals:
services:\n certbot-cloudflare-renew:\n image: certbot/dns-cloudflare:latest\n command: renew --dns-cloudflare --dns-cloudflare-credentials /run/secrets/cloudflare-api-token\n volumes:\n - ./docker-data/certbot/certs/:/etc/letsencrypt/\n - ./docker-data/certbot/logs/:/var/log/letsencrypt/\n secrets:\n - cloudflare-api-token\n
You can manually run this service to renew the cert within 90 days:
docker compose run certbot-cloudflare-renew\n
You should see the following output (The following log was generated with --dry-run
options)
Saving debug log to /var/log/letsencrypt/letsencrypt.log\n\n- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -\nProcessing /etc/letsencrypt/renewal/mail.example.com.conf\n- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -\nAccount registered.\nSimulating renewal of an existing certificate for mail.example.com\nWaiting 10 seconds for DNS changes to propagate\n\n- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -\nCongratulations, all simulated renewals succeeded:\n /etc/letsencrypt/live/mail.example.com/fullchain.pem (success)\n- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -\n
It is recommended to automate this renewal via a task scheduler like a systemd timer or in crontab
(crontab
example: Checks every day if the certificate should be renewed)
0 0 * * * docker compose -f PATH_TO_YOUR_DOCKER_COMPOSE_YML up certbot-cloudflare-renew\n
"},{"location":"config/security/ssl/#example-using-nginx-proxy-and-acme-companion-with-docker","title":"Example using nginx-proxy
and acme-companion
with Docker","text":"If you are running a web server already, port 80 will be in use which Certbot requires. You could use the Certbot --webroot
feature, but it is more common to leverage a reverse proxy that manages the provisioning and renewal of certificates for your services automatically.
In the following example, we show how DMS can be run alongside the docker containers nginx-proxy
and acme-companion
(Referencing: acme-companion
documentation):
Start the reverse proxy (nginx-proxy
):
docker run --detach \\\n --name nginx-proxy \\\n --restart always \\\n --publish 80:80 \\\n --publish 443:443 \\\n --volume \"${PWD}/docker-data/nginx-proxy/html/:/usr/share/nginx/html/\" \\\n --volume \"${PWD}/docker-data/nginx-proxy/vhost.d/:/etc/nginx/vhost.d/\" \\\n --volume \"${PWD}/docker-data/acme-companion/certs/:/etc/nginx/certs/:ro\" \\\n --volume '/var/run/docker.sock:/tmp/docker.sock:ro' \\\n nginxproxy/nginx-proxy\n
Then start the certificate provisioner (acme-companion
), which will provide certificates to nginx-proxy
:
# Inherit `nginx-proxy` volumes via `--volumes-from`, but make `certs/` writeable:\ndocker run --detach \\\n --name nginx-proxy-acme \\\n --restart always \\\n --volumes-from nginx-proxy \\\n --volume \"${PWD}/docker-data/acme-companion/certs/:/etc/nginx/certs/:rw\" \\\n --volume \"${PWD}/docker-data/acme-companion/acme-state/:/etc/acme.sh/\" \\\n --volume '/var/run/docker.sock:/var/run/docker.sock:ro' \\\n --env 'DEFAULT_EMAIL=admin@example.com' \\\n nginxproxy/acme-companion\n
Start the rest of your web server containers as usual.
Start a dummy container to provision certificates for your FQDN (eg: mail.example.com
). acme-companion
will detect the container and generate a Let's Encrypt certificate for your domain, which can be used by DMS:
docker run --detach \\\n --name webmail \\\n --env 'VIRTUAL_HOST=mail.example.com' \\\n --env 'LETSENCRYPT_HOST=mail.example.com' \\\n --env 'LETSENCRYPT_EMAIL=admin@example.com' \\\n nginx\n
You may want to add --env LETSENCRYPT_TEST=true
to the above while testing, to avoid the Let's Encrypt certificate generation rate limits.
Make sure your mount path to the letsencrypt
certificates directory is correct. Edit your compose.yaml
for the mailserver
service to have volumes added like below:
volumes:\n - ./docker-data/dms/mail-data/:/var/mail/\n - ./docker-data/dms/mail-state/:/var/mail-state/\n - ./docker-data/dms/config/:/tmp/docker-mailserver/\n - ./docker-data/acme-companion/certs/:/etc/letsencrypt/live/:ro\n
Then from the compose.yaml
project directory, run: docker compose up -d mailserver
.
nginx-proxy
and acme-companion
with docker-compose
","text":"The following example is the basic setup you need for using nginx-proxy
and acme-companion
with DMS (Referencing: acme-companion
documentation):
compose.yaml
You should have an existing compose.yaml
with a mailserver
service. Below are the modifications to add for integrating with nginx-proxy
and acme-companion
services:
services:\n # Add the following `environment` and `volumes` to your existing `mailserver` service:\n mailserver:\n environment:\n # SSL_TYPE: Uses the `letsencrypt` method to find mounted certificates.\n # VIRTUAL_HOST: The FQDN that `nginx-proxy` will configure itself to handle for HTTP[S] connections.\n # LETSENCRYPT_HOST: The FQDN for a certificate that `acme-companion` will provision and renew.\n - SSL_TYPE=letsencrypt\n - VIRTUAL_HOST=mail.example.com\n - LETSENCRYPT_HOST=mail.example.com\n volumes:\n - ./docker-data/acme-companion/certs/:/etc/letsencrypt/live/:ro\n\n # If you don't yet have your own `nginx-proxy` and `acme-companion` setup,\n # here is an example you can use:\n reverse-proxy:\n image: nginxproxy/nginx-proxy\n container_name: nginx-proxy\n restart: always\n ports:\n # Port 80: Required for HTTP-01 challenges to `acme-companion`.\n # Port 443: Only required for containers that need access over HTTPS. TLS-ALPN-01 challenge not supported.\n - \"80:80\"\n - \"443:443\"\n volumes:\n # `certs/`: Managed by the `acme-companion` container (_read-only_).\n # `docker.sock`: Required to interact with containers via the Docker API.\n - ./docker-data/nginx-proxy/html/:/usr/share/nginx/html/\n - ./docker-data/nginx-proxy/vhost.d/:/etc/nginx/vhost.d/\n - ./docker-data/acme-companion/certs/:/etc/nginx/certs/:ro\n - /var/run/docker.sock:/tmp/docker.sock:ro\n\n acme-companion:\n image: nginxproxy/acme-companion\n container_name: nginx-proxy-acme\n restart: always\n environment:\n # When `volumes_from: [nginx-proxy]` is not supported,\n # reference the _reverse-proxy_ `container_name` here:\n - NGINX_PROXY_CONTAINER=nginx-proxy\n volumes:\n # `html/`: Write ACME HTTP-01 challenge files that `nginx-proxy` will serve.\n # `vhost.d/`: To enable web access via `nginx-proxy` to HTTP-01 challenge files.\n # `certs/`: To store certificates and private keys.\n # `acme-state/`: To persist config and state for the ACME provisioner (`acme.sh`).\n # `docker.sock`: Required to interact with containers via the Docker API.\n - ./docker-data/nginx-proxy/html/:/usr/share/nginx/html/\n - ./docker-data/nginx-proxy/vhost.d/:/etc/nginx/vhost.d/\n - ./docker-data/acme-companion/certs/:/etc/nginx/certs/:rw\n - ./docker-data/acme-companion/acme-state/:/etc/acme.sh/\n - /var/run/docker.sock:/var/run/docker.sock:ro\n
Optional ENV vars worth knowing about
Per container ENV that acme-companion
will detect to override default provisioning settings:
LETSENCRYPT_TEST=true
: Recommended during initial setup. Otherwise the default production endpoint has a rate limit of 5 duplicate certificates per week. Overrides ACME_CA_URI
to use the Let's Encrypt staging endpoint.LETSENCRYPT_EMAIL
: For when you don't use DEFAULT_EMAIL
on acme-companion
, or want to assign a different email contact for this container.LETSENCRYPT_KEYSIZE
: Allows you to configure the type (RSA or ECDSA) and size of the private key for your certificate. Default is RSA 4096, but RSA 2048 is recommended.LETSENCRYPT_RESTART_CONTAINER=true
: When the certificate is renewed, the entire container will be restarted to ensure the new certificate is used.acme-companion
ENV for default settings that apply to all containers using LETSENCRYPT_HOST
:
DEFAULT_EMAIL
: An email address that the CA (eg: Let's Encrypt) can contact you about expiring certificates, failed renewals, or for account recovery. You may want to use an email address not handled by your mail server to ensure deliverability in the event your mail server breaks.CERTS_UPDATE_INTERVAL
: If you need to adjust the frequency to check for renewals. 3600 seconds (1 hour) by default.DEBUG=1
: Should be helpful when troubleshooting provisioning issues from acme-companion
logs.ACME_CA_URI
: Useful in combination with CA_BUNDLE
to use a private CA. To change the default Let's Encrypt endpoint to the staging endpoint, use https://acme-staging-v02.api.letsencrypt.org/directory
.CA_BUNDLE
: If you want to use a private CA instead of Let's Encrypt.Alternative to required ENV on mailserver
service
While you will still need both nginx-proxy
and acme-companion
containers, you can manage certificates without adding ENV vars to containers. Instead the ENV is moved into a file and uses the acme-companion
feature Standalone certificates.
This requires adding another shared volume between nginx-proxy
and acme-companion
:
services:\n reverse-proxy:\n volumes:\n - ./docker-data/nginx-proxy/conf.d/:/etc/nginx/conf.d/\n\n acme-companion:\n volumes:\n - ./docker-data/nginx-proxy/conf.d/:/etc/nginx/conf.d/\n - ./docker-data/acme-companion/standalone.sh:/app/letsencrypt_user_data:ro\n
acme-companion
mounts a shell script (standalone.sh
), which defines variables to customize certificate provisioning:
# A list IDs for certificates to provision:\nLETSENCRYPT_STANDALONE_CERTS=('mail')\n\n# Each ID inserts itself into the standard `acme-companion` supported container ENV vars below.\n# The LETSENCRYPT_<ID>_HOST var is a list of FQDNs to provision a certificate for as the SAN field:\nLETSENCRYPT_mail_HOST=('mail.example.com')\n\n# Optional variables:\nLETSENCRYPT_mail_TEST=true\nLETSENCRYPT_mail_EMAIL='admin@example.com'\n# Supported values are `2048`, `3072` and `4096` for RSA keys, and `ec-256` or `ec-384` for elliptic curve keys.\nLETSENCRYPT_mail_KEYSIZE=2048\n
Unlike with the equivalent ENV for containers, changes to this file will not be detected automatically. You would need to wait until the next renewal check by acme-companion
(every hour by default), restart acme-companion
, or manually invoke the service loop:
docker exec nginx-proxy-acme /app/signal_le_service
Version 6.2 and later of the Synology NAS DSM OS now come with an interface to generate and renew letencrypt certificates. Navigation into your DSM control panel and go to Security, then click on the tab Certificate to generate and manage letsencrypt certificates.
Amongst other things, you can use these to secure your mail server. DSM locates the generated certificates in a folder below /usr/syno/etc/certificate/_archive/
.
Navigate to that folder and note the 6 character random folder name of the certificate you'd like to use. Then, add the following to your compose.yaml
declaration file:
volumes:\n - /usr/syno/etc/certificate/_archive/<your-folder>/:/tmp/dms/custom-certs/\nenvironment:\n - SSL_TYPE=manual\n - SSL_CERT_PATH=/tmp/dms/custom-certs/fullchain.pem\n - SSL_KEY_PATH=/tmp/dms/custom-certs/privkey.pem\n
DSM-generated letsencrypt certificates get auto-renewed every three months.
"},{"location":"config/security/ssl/#caddy","title":"Caddy","text":"Caddy is an open-source web server with built-in TLS certificate generation. You can use the official Docker image and write your own Caddyfile
.
Example
compose.yamlservices:\n # Basic Caddy service to provision certs:\n reverse-proxy:\n image: caddy:2.7\n ports:\n - 80:80\n - 443:443\n volumes:\n - ./Caddyfile:/etc/caddy/Caddyfile:ro\n - ${CADDY_DATA_DIR}:/data\n\n # Share the Caddy data volume for certs and configure SSL_TYPE to `letsencrypt`\n mailserver:\n image: ghcr.io/docker-mailserver/docker-mailserver:latest\n hostname: mail.example.com\n environment:\n SSL_TYPE: letsencrypt\n # While you could use a named data volume instead of a bind mount volume, it would require the long-syntax to rename cert files:\n # https://docs.docker.com/compose/compose-file/05-services/#volumes\n volumes:\n - ${CADDY_DATA_DIR}/certificates/acme-v02.api.letsencrypt.org-directory/mail.example.com/mail.example.com.crt:/etc/letsencrypt/live/mail.example.com/fullchain.pem\n - ${CADDY_DATA_DIR}/certificates/acme-v02.api.letsencrypt.org-directory/mail.example.com/mail.example.com.key:/etc/letsencrypt/live/mail.example.com/privkey.pem\n
Caddyfilemail.example.com {\n tls internal {\n key_type rsa2048\n }\n\n # Optional, can be useful for troubleshooting\n # connection to Caddy with correct certificate:\n respond \"Hello DMS\"\n}\n
While DMS does not need a webserver to work, this workaround will provision a TLS certificate for DMS to use.
tls internal
will create a local self-signed cert for testing. This targets only the site-address, unlike the global local_certs
option.key_type
can be used in the tls
block if you need to enforce RSA as the key type for certificates provisioned. The default is currently ECDSA (P-256).caddy-docker-proxy
Using lucaslorentz/caddy-docker-proxy
allows you to generate a Caddyfile
by adding labels to your services in compose.yaml
:
services:\n reverse-proxy:\n image: lucaslorentz/caddy-docker-proxy:2.8\n ports:\n - 80:80\n - 443:443\n volumes:\n - /var/run/docker.sock:/var/run/docker.sock\n - ${CADDY_DATA_DIR}:/data\n labels:\n # Set global config here, this option has an empty value to enable self-signed certs for local testing:\n # NOTE: Remove this label when going to production.\n caddy.local_certs: \"\"\n\n # Use labels to configure Caddy to provision DMS certs\n mailserver:\n image: ghcr.io/docker-mailserver/docker-mailserver:latest\n hostname: mail.example.com\n environment:\n SSL_TYPE: letsencrypt\n volumes:\n - ${CADDY_DATA_DIR}/certificates/acme-v02.api.letsencrypt.org-directory/mail.example.com/mail.example.com.crt:/etc/letsencrypt/live/mail.example.com/fullchain.pem\n - ${CADDY_DATA_DIR}/certificates/acme-v02.api.letsencrypt.org-directory/mail.example.com/mail.example.com.key:/etc/letsencrypt/live/mail.example.com/privkey.pem\n labels:\n # Set your DMS FQDN here to add the site-address into the generated Caddyfile:\n caddy_0: mail.example.com\n # Add a dummy directive is required:\n caddy_0.respond: \"Hello DMS\"\n # Uncomment to make a proxy for Rspamd\n # caddy_1: rspamd.example.com\n # caddy_1.reverse_proxy: \"{{upstreams 11334}}\"\n
Caddy certificate location varies
The path contains the certificate provisioner used. This path may be different from the example above for you and may change over time when multiple ACME provisioner services are used.
This can make the volume mounting for DMS to find the certificates non-deterministic, but you can restrict provisioning to single service via the acme_ca
setting.
Traefik is an open-source application proxy using the ACME protocol. Traefik can request certificates for domains and subdomains, and it will take care of renewals, challenge negotiations, etc.
Traefik's storage format is natively supported if the acme.json
store is mounted into the container at /etc/letsencrypt/acme.json
. The file is also monitored for changes and will trigger a reload of the mail services (Postfix and Dovecot).
DMS will select it's certificate from acme.json
prioritizing a match for the DMS FQDN (hostname), while also checking one DNS level up (eg: mail.example.com
=> example.com
). Wildcard certificates are supported.
This setup only comes with one caveat - The domain has to be configured on another service for Traefik to actually request it from Let's Encrypt (i.e. Traefik will not issue a certificate without a service / router demanding it).
Example CodeHere is an example setup for Docker Compose
:
services:\n mailserver:\n image: ghcr.io/docker-mailserver/docker-mailserver:latest\n container_name: mailserver\n hostname: mail.example.com\n volumes:\n - ./docker-data/traefik/acme.json:/etc/letsencrypt/acme.json:ro\n environment:\n SSL_TYPE: letsencrypt\n SSL_DOMAIN: mail.example.com\n # for a wildcard certificate, use\n # SSL_DOMAIN: example.com\n\n reverse-proxy:\n image: docker.io/traefik:latest #v2.5\n container_name: docker-traefik\n ports:\n - \"80:80\"\n - \"443:443\"\n command:\n - --providers.docker\n - --entrypoints.http.address=:80\n - --entrypoints.http.http.redirections.entryPoint.to=https\n - --entrypoints.http.http.redirections.entryPoint.scheme=https\n - --entrypoints.https.address=:443\n - --entrypoints.https.http.tls.certResolver=letsencrypt\n - --certificatesresolvers.letsencrypt.acme.email=admin@example.com\n - --certificatesresolvers.letsencrypt.acme.storage=/acme.json\n - --certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=http\n volumes:\n - ./docker-data/traefik/acme.json:/acme.json\n - /var/run/docker.sock:/var/run/docker.sock:ro\n\n whoami:\n image: docker.io/traefik/whoami:latest\n labels:\n - \"traefik.http.routers.whoami.rule=Host(`mail.example.com`)\"\n
"},{"location":"config/security/ssl/#self-signed-certificates","title":"Self-Signed Certificates","text":"Warning
Use self-signed certificates only for testing purposes!
This feature requires you to provide the following files into your docker-data/dms/config/ssl/
directory (internal location: /tmp/docker-mailserver/ssl/
):
<FQDN>-key.pem
<FQDN>-cert.pem
demoCA/cacert.pem
Where <FQDN>
is the FQDN you've configured for your DMS container.
Add SSL_TYPE=self-signed
to your DMS environment variables. Postfix and Dovecot will be configured to use the provided certificate (.pem
files above) during container startup.
One way to generate self-signed certificates is with Smallstep's step
CLI. This is exactly what DMS does for creating test certificates.
For example with the FQDN mail.example.test
, you can generate the required files by running:
#! /bin/sh\nmkdir -p demoCA\n\nstep certificate create \"Smallstep Root CA\" \"demoCA/cacert.pem\" \"demoCA/cakey.pem\" \\\n --no-password --insecure \\\n --profile root-ca \\\n --not-before \"2021-01-01T00:00:00+00:00\" \\\n --not-after \"2031-01-01T00:00:00+00:00\" \\\n --san \"example.test\" \\\n --san \"mail.example.test\" \\\n --kty RSA --size 2048\n\nstep certificate create \"Smallstep Leaf\" mail.example.test-cert.pem mail.example.test-key.pem \\\n --no-password --insecure \\\n --profile leaf \\\n --ca \"demoCA/cacert.pem\" \\\n --ca-key \"demoCA/cakey.pem\" \\\n --not-before \"2021-01-01T00:00:00+00:00\" \\\n --not-after \"2031-01-01T00:00:00+00:00\" \\\n --san \"example.test\" \\\n --san \"mail.example.test\" \\\n --kty RSA --size 2048\n
If you'd rather not install the CLI tool locally to run the step
commands above; you can save the script above to a file such as generate-certs.sh
(and make it executable chmod +x generate-certs.sh
) in a directory that you want the certs to be placed (eg: docker-data/dms/custom-certs/
), then use docker to run that script in a container:
# '--user' is to keep ownership of the files written to\n# the local volume to use your systems User and Group ID values.\ndocker run --rm -it \\\n --user \"$(id -u):$(id -g)\" \\\n --volume \"${PWD}/docker-data/dms/custom-certs/:/tmp/step-ca/\" \\\n --workdir \"/tmp/step-ca/\" \\\n --entrypoint \"/tmp/step-ca/generate-certs.sh\" \\\n smallstep/step-ca\n
"},{"location":"config/security/ssl/#bring-your-own-certificates","title":"Bring Your Own Certificates","text":"You can also provide your own certificate files. Add these entries to your compose.yaml
:
volumes:\n - ./docker-data/dms/custom-certs/:/tmp/dms/custom-certs/:ro\nenvironment:\n - SSL_TYPE=manual\n # Values should match the file paths inside the container:\n - SSL_CERT_PATH=/tmp/dms/custom-certs/public.crt\n - SSL_KEY_PATH=/tmp/dms/custom-certs/private.key\n
This will mount the path where your certificate files reside locally into the read-only container folder: /tmp/dms/custom-certs
.
The local and internal paths may be whatever you prefer, so long as both SSL_CERT_PATH
and SSL_KEY_PATH
point to the correct internal file paths. The certificate files may also be named to your preference, but should be PEM encoded.
SSL_ALT_CERT_PATH
and SSL_ALT_KEY_PATH
are additional ENV vars to support a 2nd certificate as a fallback. Commonly known as hybrid or dual certificate support. This is useful for using a modern ECDSA as your primary certificate, and RSA as your fallback for older connections. They work in the same manner as the non-ALT
versions.
Info
You may have to restart DMS once the certificates change.
"},{"location":"config/security/ssl/#testing-a-certificate-is-valid","title":"Testing a Certificate is Valid","text":"Connect to DMS on port 25
docker exec mailserver openssl s_client \\\n -connect 0.0.0.0:25 \\\n -starttls smtp \\\n -CApath /etc/ssl/certs/\n
The response should show the certificate chain with a line further down: Verify return code: 0 (ok)
This example runs within the DMS container itself to verify the cert is working locally.
-connect
IP if testing externally from another system. Additionally testing for port 143 (Dovecot IMAP) is encouraged (change the protocol for -starttls
from smtp
to imap
).-CApath
will help verify the certificate chain, provided the location contains the root CA that signed your TLS cert for DMS.docker exec mailserver openssl s_client \\\n -connect 0.0.0.0:25 \\\n -starttls smtp \\\n -CApath /etc/ssl/certs/ \\\n 2>/dev/null | openssl x509 -noout -dates\n
Testing and troubleshooting
If you need to test a connection without resolving DNS, curl
can connect with --resolve
option to map an FQDN + Port to an IP address, instead of the request address provided.
# NOTE: You may want to use `--insecure` if the cert was provisioned with a private CA not present on the curl client:\n# Use `--verbose` for additional insights on the connection.\ncurl --resolve mail.example.com:443:127.0.0.1 https://mail.example.com\n
Similarly with openssl
you can connect to an IP as shown previously, but provide an explicit SNI if necessary with -servername mail.example.com
.
Both curl
and openssl
also support -4
and -6
for enforcing IPv4 or IPv6 lookup.
This can be useful, such as when DNS resolves the IP to different servers leading to different certificates returned. As shown in that link, step certificate inspect
is also handy for viewing details of the cert returned or on disk.
Warning
Not recommended for purposes other than testing.
Add this to docker-data/dms/config/dovecot.cf
:
ssl = yes\ndisable_plaintext_auth=no\n
These options in conjunction mean:
If you have another source for SSL/TLS certificates you can import them into the server via an external script. The external script can be found here: external certificate import script.
This is a community contributed script, and in most cases you will have better support via our Change Detection service (automatic for SSL_TYPE
of manual
and letsencrypt
) - Unless you're using LDAP which disables the service.
Script Compatibility
/etc/dms/tls/cert
and /etc/dms/tls/key
intended for internal use only.fullchain.key
+ privkey.pem
as your mounted file names. That may not align with your provisioning method.ALT
fallback certificates (for supporting dual/hybrid, RSA + ECDSA).The steps to follow are these:
./docker-data/dms/custom-certs/
(volume mounted to: /tmp/ssl/
)fullchain.key
and privkey.pem
./docker-data/dms/config/
(volume mounted to: /tmp/docker-mailserver/
)chmod +x tomav-renew-certs.sh
)docker exec mailserver /tmp/docker-mailserver/tomav-renew-certs.sh
If an error occurs the script will inform you. If not you will see both postfix and dovecot restart.
After the certificates have been loaded you can check the certificate:
openssl s_client \\\n -servername mail.example.com \\\n -connect 192.168.0.72:465 \\\n 2>/dev/null | openssl x509\n\n# or\n\nopenssl s_client \\\n -servername mail.example.com \\\n -connect mail.example.com:465 \\\n 2>/dev/null | openssl x509\n
Or you can check how long the new certificate is valid with commands like:
export SITE_URL=\"mail.example.com\"\nexport SITE_IP_URL=\"192.168.0.72\" # can also use `mail.example.com`\nexport SITE_SSL_PORT=\"993\" # imap port dovecot\n\n##works: check if certificate will expire in two weeks\n#2 weeks is 1209600 seconds\n#3 weeks is 1814400\n#12 weeks is 7257600\n#15 weeks is 9072000\n\ncertcheck_2weeks=`openssl s_client -connect ${SITE_IP_URL}:${SITE_SSL_PORT} \\\n -servername ${SITE_URL} 2> /dev/null | openssl x509 -noout -checkend 1209600`\n\n####################################\n#notes: output could be either:\n#Certificate will not expire\n#Certificate will expire\n####################\n
What does the script that imports the certificates do:
/tmp/ssl
.You can of course run the script by cron once a week or something. In that way you could automate cert renewal. If you do so it is probably wise to run an automated check on certificate expiry as well. Such a check could look something like this:
# This script is run inside docker-mailserver via 'docker exec ...', using the 'mail' command to send alerts.\n## code below will alert if certificate expires in less than two weeks\n## please adjust variables!\n## make sure the 'mail -s' command works! Test!\n\nexport SITE_URL=\"mail.example.com\"\nexport SITE_IP_URL=\"192.168.2.72\" # can also use `mail.example.com`\nexport SITE_SSL_PORT=\"993\" # imap port dovecot\n# Below can be from a different domain; like your personal email, not handled by this docker-mailserver:\nexport ALERT_EMAIL_ADDR=\"external-account@gmail.com\"\n\ncertcheck_2weeks=`openssl s_client -connect ${SITE_IP_URL}:${SITE_SSL_PORT} \\\n -servername ${SITE_URL} 2> /dev/null | openssl x509 -noout -checkend 1209600`\n\n####################################\n#notes: output can be\n#Certificate will not expire\n#Certificate will expire\n####################\n\n#echo \"certcheck 2 weeks gives $certcheck_2weeks\"\n\n##automated check you might run by cron or something\n## does the certificate expire within two weeks?\n\nif [ \"$certcheck_2weeks\" = \"Certificate will not expire\" ]; then\n echo \"all is well, certwatch 2 weeks says $certcheck_2weeks\"\n else\n echo \"Cert seems to be expiring pretty soon, within two weeks: $certcheck_2weeks\"\n echo \"we will send an alert email and log as well\"\n logger Certwatch: cert $SITE_URL will expire in two weeks\n echo \"Certwatch: cert $SITE_URL will expire in two weeks\" | mail -s \"cert $SITE_URL expires in two weeks \" $ALERT_EMAIL_ADDR\nfi\n
"},{"location":"config/security/ssl/#custom-dh-parameters","title":"Custom DH Parameters","text":"By default DMS uses ffdhe4096
from IETF RFC 7919. These are standardized pre-defined DH groups and the only available DH groups for TLS 1.3. It is discouraged to generate your own DH parameters as it is often less secure.
Despite this, if you must use non-standard DH parameters or you would like to swap ffdhe4096
for a different group (eg ffdhe2048
); Add your own PEM encoded DH params file via a volume to /tmp/docker-mailserver/dhparams.pem
. This will replace DH params for both Dovecot and Postfix services during container startup.
Prefer ports with Implicit TLS ports, they're more secure than ports using Explicit TLS, and if you use a Reverse Proxy should be less hassle.
"},{"location":"config/security/understanding-the-ports/#overview-of-email-ports","title":"Overview of Email Ports","text":"Protocol Explicit TLS1 Implicit TLS Purpose Enabled by Default ESMTP 25 N/A Transfer2 Yes ESMTP 587 4653 Submission Yes POP3 110 995 Retrieval No IMAP4 143 993 Retrieval YesSTARTTLS
. On ports 110, 143 and 587, DMS will reject a connection that cannot be secured. Port 25 is required to support insecure connections.There is a common misconception of this port due to it's history detailed by various communities and blogs articles on the topic (including by popular mail relay services).
Port 465 was briefly assigned the role of SMTPS in 1997 as an secure alternative to Port 25 between MTA exchanges. Then RFC 2487 (STARTTLS
) while still in a draft status in late 1998 had IANA revoke the SMTPS assignment. The draft history was modified to exclude all mention of port 465 and SMTPS.
In 2018 RFC 8314 was published which revives Port 465 as an Implicit TLS alternative to Port 587 for mail submission. It details very clearly that gaining adoption of 465 as the preferred port will take time. IANA reassigned port 465 as the submissions
service. Any unofficial usage as SMTPS is legacy and has been for over two decades.
Understand that port 587 is more broadly supported due to this history and that lots of software in that time has been built or configured with that port in mind. STARTTLS
is known to have various CVEs discovered even in recent years, do not be misled by any advice implying it should be preferred over implicit TLS. Trust in more official sources, such as the config Postfix has which acknowledges the submissions
port (465).
flowchart LR\n subgraph your-server [\"Your Server\"]\n in_25(25) --> server\n in_465(465) --> server\n server((\"docker-mailserver<br/>hello@world.com\"))\n server --- out_25(25)\n server --- out_465(465)\n end\n\n third-party(\"Third-party<br/>(sending you email)\") ---|\"Receive email for<br/>hello@world.com\"| in_25\n\n subgraph clients [\"Clients (MUA)\"]\n mua-client(Thunderbird,<br/>Webmail,<br/>Mutt,<br/>etc)\n mua-service(Backend software<br/>on another server)\n end\n clients ---|\"Send email as<br/>hello@world.com\"| in_465\n\n out_25(25) -->|\"Direct<br/>Delivery\"| tin_25\n out_465(465) --> relay(\"MTA<br/>Relay Server\") --> tin_25(25)\n\n subgraph third-party-server[\"Third-party Server\"]\n third-party-mta(\"MTA<br/>friend@example.com\")\n tin_25(25) --> third-party-mta\n end
"},{"location":"config/security/understanding-the-ports/#inbound-traffic-on-the-left","title":"Inbound Traffic (On the left)","text":"Mail arriving at your server will be processed and stored in a mailbox, or sent outbound to another mail server.
Note
When submitting mail (inbound) to be sent (outbound), this involves two separate connections to negotiate and secure. There may be additional intermediary connections which DMS is not involved in, and thus unable to ensure encrypted transit throughout delivery.
"},{"location":"config/security/understanding-the-ports/#outbound-traffic-on-the-right","title":"Outbound Traffic (On the Right)","text":"Mail being sent from your server is either being relayed through another MTA (eg: SendGrid), or direct to an MTA responsible for an email address (eg: Gmail).
Tip
DMS can function as a relay too, but professional relay services have a trusted reputation (which increases success of delivery).
An MTA with low reputation can affect if mail is treated as junk, or even rejected.
Note
At best, you can only ensure a secure connection between the MTA you directly connect to. The receiving MTA may relay that mail to another MTA (and so forth), each connection may not be enforcing TLS.
"},{"location":"config/security/understanding-the-ports/#explicit-vs-implicit-tls","title":"Explicit vs Implicit TLS","text":""},{"location":"config/security/understanding-the-ports/#explicit-tls-aka-opportunistic-tls-opt-in-encryption","title":"Explicit TLS (aka Opportunistic TLS) - Opt-in Encryption","text":"Communication on these ports begin in cleartext. Upgrading to an encrypted connection must be requested explicitly through the STARTTLS
protocol and successfully negotiated.
Sometimes a reverse-proxy is involved, but is misconfigured or lacks support for the STARTTLS
negotiation to succeed.
Note
STARTTLS
is unsuccessful, mail can be received over an unencrypted connection. You can better secure this port between trusted parties with the addition of MTA-STS, STARTTLS Policy List, DNSSEC and DANE.Warning
STARTTLS
continues to have vulnerabilities found (Nov 2021 article), as per RFC 8314 (Section 4.1) you are encouraged to prefer Implicit TLS where possible.
Support for STARTTLS
is not always implemented correctly, which can lead to leaking credentials (like a client sending too early) prior to a TLS connection being established. Third-parties such as some ISPs have also been known to intercept the STARTTLS
exchange, modifying network traffic to prevent establishing a secure connection.
Communication on these ports are always encrypted (enforced, thus implicit), avoiding the potential risks with STARTTLS
(Explicit TLS).
While Explicit TLS can provide the same benefit (when STARTTLS
is successfully negotiated), Implicit TLS more reliably avoids concerns with connection manipulation and compatibility.
Todo
This section should provide any related configuration advice, and probably expand on and link to resources about DANE, DNSSEC, MTA-STS and STARTTLS Policy list, with advice on how to configure/setup these added security layers.
Todo
A related section or page on ciphers used may be useful, although less important for users to be concerned about.
"},{"location":"config/security/understanding-the-ports/#tls-connections-for-a-mail-server-compared-to-web-browsers","title":"TLS connections for a Mail Server, compared to web browsers","text":"Unlike with HTTP where a web browser client communicates directly with the server providing a website, a secure TLS connection as discussed below does not provide the equivalent safety that HTTPS does when the transit of email (receiving or sending) is sent through third-parties, as the secure connection is only between two machines, any additional machines (MTAs) between the MUA and the MDA depends on them establishing secure connections between one another successfully.
Other machines that facilitate a connection that generally aren't taken into account can exist between a client and server, such as those where your connection passes through your ISP provider are capable of compromising a cleartext
connection through interception.
When refactoring, writing or altering scripts or other files, adhere to these rules:
shellcheck
to check your scripts! Your contributions are checked by GitHub Actions too, so you will need to do this. You can lint your work with make lint
to check against all targets..editorconfig
file./bin/bash
instead of /bin/sh
in scriptsMake sure to select edge
in the dropdown menu at the top. Navigate to the page you would like to edit and click the edit button in the top right. This allows you to make changes and create a pull-request.
Alternatively you can make the changes locally. For that you'll need to have Docker installed. Navigate into the docs/
directory. Then run:
docker run --rm -it -p 8000:8000 -v \"${PWD}:/docs\" squidfunk/mkdocs-material\n
This serves the documentation on your local machine on port 8000
. Each change will be hot-reloaded onto the page you view, just edit, save and look at the result.
This project is Open Source. That means that you can contribute on enhancements, bug fixing or improving the documentation.
"},{"location":"contributing/issues-and-pull-requests/#opening-an-issue","title":"Opening an Issue","text":"Attention
Before opening an issue, read the README
carefully, study the docs for your version (maybe latest), the Postfix/Dovecot documentation and your search engine you trust. The issue tracker is not meant to be used for unrelated questions!
When opening an issue, please provide details use case to let the community reproduce your problem. Please start DMS with the environment variable LOG_LEVEL
set to debug
or trace
and paste the output into the issue.
Attention
Use the issue templates to provide the necessary information. Issues which do not use these templates are not worked on and closed.
By raising issues, I agree to these terms and I understand, that the rules set for the issue tracker will help both maintainers as well as everyone to find a solution.
Maintainers take the time to improve on this project and help by solving issues together. It is therefore expected from others to make an effort and comply with the rules.
"},{"location":"contributing/issues-and-pull-requests/#filing-a-bug-report","title":"Filing a Bug Report","text":"Thank you for participating in this project and reporting a bug. Docker Mail Server (DMS) is a community-driven project, and each contribution counts!
Maintainers and moderators are volunteers. We greatly appreciate reports that take the time to provide detailed information via the template, enabling us to help you in the best and quickest way. Ignoring the template provided may seem easier, but discourages receiving any support (via assignment of the label meta/no template - no support
).
Markdown formatting can be used in almost all text fields (unless stated otherwise in the description).
Be as precise as possible, and if in doubt, it's best to add more information that too few.
When an option is marked with \"not officially supported\" / \"unsupported\", then support is dependent on availability from specific maintainers.
"},{"location":"contributing/issues-and-pull-requests/#pull-requests","title":"Pull Requests","text":"Motivation
You want to add a feature? Feel free to start creating an issue explaining what you want to do and how you're thinking doing it. Other users may have the same need and collaboration may lead to better results.
"},{"location":"contributing/issues-and-pull-requests/#submit-a-pull-request","title":"Submit a Pull-Request","text":"The development workflow is the following:
git clone --recurse-submodules ...
or run git submodule update --init --recursive
after you cloned your forkmaster
. Please use the pull-request template to provide a minimum of contextual information and make sure to meet the requirements of the checklist.Pull requests are automatically tested against the CI and will be reviewed when tests pass. When your changes are validated, your branch is merged. CI builds the new :edge
image immediately and your changes will be includes in the next version release.
Program testing can be used to show the presence of bugs, but never to show their absence!
\u2013 Edsger Wybe Dijkstra
"},{"location":"contributing/tests/#introduction","title":"Introduction","text":"DMS employs a variety of unit and integration tests. All tests and associated configuration is stored in the test/
directory. If you want to change existing functionality or integrate a new feature into DMS, you will probably need to work with our test suite.
Can I use macOS?
We do not support running linting, tests, etc. on macOS at this time. Please use a Linux VM, Debian/Ubuntu is recommended.
"},{"location":"contributing/tests/#about","title":"About","text":"We use BATS (Bash Automated Testing System) and additional support libraries. BATS is very similar to Bash, and one can easily and quickly get an understanding of how tests in a single file are run. A test file template provides a minimal working example for newcomers to look at.
"},{"location":"contributing/tests/#structure","title":"Structure","text":"The test/
directory contains multiple directories. Among them is the bats/
directory (which is the BATS git submodule) and the helper/
directory. The latter is especially interesting because it contains common support functionality used in almost every test. Actual tests are located in test/tests/
.
Test suite undergoing refactoring
We are currently in the process of restructuring all of our tests. Tests will be moved into test/tests/parallel/
and new tests should be placed there as well.
There are many functions that aid in writing tests. We urge you to use them! They will not only ease writing a test but they will also do their best to ensure there are no race conditions or other unwanted side effects. To learn about the functions we provide, you can:
test/helper/
directory which contains all files that can (and will) be loaded in test filesWe encourage you to try both of the approaches mentioned above. To make understanding and using the helper functions easy, every function contains detailed documentation comments. Read them carefully!
"},{"location":"contributing/tests/#how-are-tests-run","title":"How Are Tests Run?","text":"Tests are split into two categories:
test/tests/parallel/
: Multiple test files are run concurrently to reduce the required time to complete the test suite. A test file will presently run it's own defined test-cases in a sequential order.test/tests/serial/
: Each test file is queued up to run sequentially. Tests that are unable to support running concurrently belong here.Parallel tests are further partitioned into smaller sets. If your system has the resources to support running more than one of those sets at a time this is perfectly ok (our CI runs tests by distributing the sets across multiple test runners). Care must be taken not to mix running the serial tests while a parallel set is also running; this is handled for you when using make tests
.
To run the test suite, you will need to:
jq
, (GNU) parallel
and file
(under Ubuntu, use sudo apt-get -y install jq parallel file
)git submodule update --init --recursive
if you haven't already initialized the git submodulesWe use make
to run commands.
make build
to create or update the local mailserver-testing:ci
Docker image (using the projects Dockerfile
)make clean tests
make clean generate-accounts test/<TEST NAME WITHOUT .bats SUFFIX>
make clean generate-accounts test/<TEST NAME WITHOUT .bats SUFFIX>,<TEST NAME WITHOUT .bats SUFFIX>
(just add a ,
and then immediately write the new test name)make clean generate-accounts tests/parallel/setX
where X
is the number of the set or make clean generate-accounts tests/serial
If your machine is capable, you can increase the amount of tests that are run simultaneously by prepending the make clean all
command with BATS_PARALLEL_JOBS=X
(i.e. BATS_PARALLEL_JOBS=X make clean all
). This wil speed up the test procedure. You can also run all tests in serial by setting BATS_PARALLEL_JOBS=1
this way.
The default value of BATS_PARALLEL_JOBS
is 2. This can be increased if your system has the resources spare to support running more jobs at once to complete the test suite sooner.
Test Output when Running in Parallel
When running tests in parallel (with make clean generate-accounts tests/parallel/setX
), BATS will delay outputting the results until completing all test cases within a file.
This likewise delays the reporting of test-case failures. When troubleshooting parallel set tests, you may prefer to run specific tests you're working on serially (as demonstrated in the example below).
When writing tests, ensure that parallel set tests still pass when run in parallel. You need to account for other tests running in parallel that may interfere with your own tests logic.
Tip
You may use make run-local-instance
to run a version of the image built locally to test and edit your changes in a running DMS instance.
In this example, you've made a change to the Rspamd feature support (or adjusted it's tests). First verify no regressions have been introduced by running it's specific test file:
$ make clean generate-accounts test/rspamd\nrspamd.bats\n \u2713 [Rspamd] Postfix's main.cf was adjusted [12]\n \u2713 [Rspamd] normal mail passes fine [44]\n \u2713 [Rspamd] detects and rejects spam [122]\n \u2713 [Rspamd] detects and rejects virus [189]\n
As your feature work progresses your change for Rspamd also affects ClamAV. As your change now spans more than just the Rspamd test file, you could run multiple test files serially:
$ make clean generate-accounts test/rspamd,clamav\nrspamd.bats\n \u2713 [Rspamd] Postfix's main.cf was adjusted [12]\n \u2713 [Rspamd] normal mail passes fine [44]\n \u2713 [Rspamd] detects and rejects spam [122]\n \u2713 [Rspamd] detects and rejects virus [189]\n\nclamav.bats\n \u2713 [ClamAV] log files exist at /var/log/mail directory [68]\n \u2713 [ClamAV] should be identified by Amavis [67]\n \u2713 [ClamAV] freshclam cron is enabled [76]\n \u2713 [ClamAV] env CLAMAV_MESSAGE_SIZE_LIMIT is set correctly [63]\n \u2713 [ClamAV] rejects virus [60]\n
You're almost finished with your change before submitting it as a PR. It's a good idea to run the full parallel set those individual tests belong to (especially if you've modified any tests):
$ make clean generate-accounts tests/parallel/set1\ndefault_relay_host.bats\n \u2713 [Relay] (ENV) 'DEFAULT_RELAY_HOST' should configure 'main.cf:relayhost' [88]\n\nspam_virus/amavis.bats\n \u2713 [Amavis] SpamAssassin integration should be active [1165]\n\nspam_virus/clamav.bats\n \u2713 [ClamAV] log files exist at /var/log/mail directory [73]\n \u2713 [ClamAV] should be identified by Amavis [67]\n \u2713 [ClamAV] freshclam cron is enabled [76]\n...\n
Even better, before opening a PR run the full test suite:
$ make clean tests\n
"},{"location":"examples/tutorials/basic-installation/","title":"Tutorials | Basic Installation","text":""},{"location":"examples/tutorials/basic-installation/#a-basic-example-with-relevant-environmental-variables","title":"A Basic Example With Relevant Environmental Variables","text":"This example provides you only with a basic example of what a minimal setup could look like. We strongly recommend that you go through the configuration file yourself and adjust everything to your needs. The default compose.yaml can be used for the purpose out-of-the-box, see the Usage chapter.
services:\n mailserver:\n image: ghcr.io/docker-mailserver/docker-mailserver:latest\n container_name: mailserver\n # Provide the FQDN of your mail server here (Your DNS MX record should point to this value)\n hostname: mail.example.com\n ports:\n - \"25:25\"\n - \"465:465\"\n - \"587:587\"\n - \"993:993\"\n volumes:\n - ./docker-data/dms/mail-data/:/var/mail/\n - ./docker-data/dms/mail-state/:/var/mail-state/\n - ./docker-data/dms/mail-logs/:/var/log/mail/\n - ./docker-data/dms/config/:/tmp/docker-mailserver/\n - /etc/localtime:/etc/localtime:ro\n environment:\n - ENABLE_RSPAMD=1\n - ENABLE_CLAMAV=1\n - ENABLE_FAIL2BAN=1\n cap_add:\n - NET_ADMIN # For Fail2Ban to work\n restart: always\n
"},{"location":"examples/tutorials/basic-installation/#a-basic-ldap-setup","title":"A Basic LDAP Setup","text":"Note There are currently no LDAP maintainers. If you encounter issues, please raise them in the issue tracker, but be aware that the core maintainers team will most likely not be able to help you. We would appreciate and we encourage everyone to actively participate in maintaining LDAP-related code by becoming a maintainer!
services:\n mailserver:\n image: ghcr.io/docker-mailserver/docker-mailserver:latest\n container_name: mailserver\n # Provide the FQDN of your mail server here (Your DNS MX record should point to this value)\n hostname: mail.example.com\n ports:\n - \"25:25\"\n - \"465:465\"\n - \"587:587\"\n - \"993:993\"\n volumes:\n - ./docker-data/dms/mail-data/:/var/mail/\n - ./docker-data/dms/mail-state/:/var/mail-state/\n - ./docker-data/dms/mail-logs/:/var/log/mail/\n - ./docker-data/dms/config/:/tmp/docker-mailserver/\n - /etc/localtime:/etc/localtime:ro\n environment:\n - ACCOUNT_PROVISIONER=LDAP\n - LDAP_SERVER_HOST=ldap # your ldap container/IP/ServerName\n - LDAP_SEARCH_BASE=ou=people,dc=localhost,dc=localdomain\n - LDAP_BIND_DN=cn=admin,dc=localhost,dc=localdomain\n - LDAP_BIND_PW=admin\n - LDAP_QUERY_FILTER_USER=(&(mail=%s)(mailEnabled=TRUE))\n - LDAP_QUERY_FILTER_GROUP=(&(mailGroupMember=%s)(mailEnabled=TRUE))\n - LDAP_QUERY_FILTER_ALIAS=(|(&(mailAlias=%s)(objectClass=PostfixBookMailForward))(&(mailAlias=%s)(objectClass=PostfixBookMailAccount)(mailEnabled=TRUE)))\n - LDAP_QUERY_FILTER_DOMAIN=(|(&(mail=*@%s)(objectClass=PostfixBookMailAccount)(mailEnabled=TRUE))(&(mailGroupMember=*@%s)(objectClass=PostfixBookMailAccount)(mailEnabled=TRUE))(&(mailalias=*@%s)(objectClass=PostfixBookMailForward)))\n - DOVECOT_PASS_FILTER=(&(objectClass=PostfixBookMailAccount)(uniqueIdentifier=%n))\n - DOVECOT_USER_FILTER=(&(objectClass=PostfixBookMailAccount)(uniqueIdentifier=%n))\n - ENABLE_SASLAUTHD=1\n - SASLAUTHD_MECHANISMS=ldap\n - SASLAUTHD_LDAP_SERVER=ldap\n - SASLAUTHD_LDAP_BIND_DN=cn=admin,dc=localhost,dc=localdomain\n - SASLAUTHD_LDAP_PASSWORD=admin\n - SASLAUTHD_LDAP_SEARCH_BASE=ou=people,dc=localhost,dc=localdomain\n - SASLAUTHD_LDAP_FILTER=(&(objectClass=PostfixBookMailAccount)(uniqueIdentifier=%U))\n - POSTMASTER_ADDRESS=postmaster@localhost.localdomain\n restart: always\n
"},{"location":"examples/tutorials/basic-installation/#using-dms-as-a-local-mail-relay-for-containers","title":"Using DMS as a local mail relay for containers","text":"Info
This was originally a community contributed guide. Please let us know via a Github Issue if you're having any difficulty following the guide so that we can update it.
This guide is focused on only using SMTP ports (not POP3 and IMAP) with the intent to relay mail received from another service to an external email address (eg: user@gmail.com
). It is not intended for mailbox storage of real users.
In this setup DMS is not intended to receive email from the outside world, so no anti-spam or anti-virus software is needed, making the service lighter to run.
setup
The setup
command used below is to be run inside the container.
Open Relays
Adding the docker network's gateway to the list of trusted hosts (eg: using the network
or connected-networks
option), can create an open relay. For instance if IPv6 is enabled on the host machine, but not in Docker.
Create the file compose.yaml
with a content like this:
Example
services:\n mailserver:\n image: ghcr.io/docker-mailserver/docker-mailserver:latest\n container_name: mailserver\n # Provide the FQDN of your mail server here (Your DNS MX record should point to this value)\n hostname: mail.example.com\n ports:\n - \"25:25\"\n - \"587:587\"\n - \"465:465\"\n volumes:\n - ./docker-data/dms/mail-data/:/var/mail/\n - ./docker-data/dms/mail-state/:/var/mail-state/\n - ./docker-data/dms/mail-logs/:/var/log/mail/\n - ./docker-data/dms/config/:/tmp/docker-mailserver/\n - /etc/localtime:/etc/localtime:ro\n environment:\n - ENABLE_FAIL2BAN=1\n # Using letsencrypt for SSL/TLS certificates:\n - SSL_TYPE=letsencrypt\n # Allow sending emails from other docker containers:\n # Beware creating an Open Relay: https://docker-mailserver.github.io/docker-mailserver/latest/config/environment/#permit_docker\n - PERMIT_DOCKER=network\n # You may want to enable this: https://docker-mailserver.github.io/docker-mailserver/latest/config/environment/#spoof_protection\n # See step 6 below, which demonstrates setup with enabled/disabled SPOOF_PROTECTION:\n - SPOOF_PROTECTION=0\n cap_add:\n - NET_ADMIN # For Fail2Ban to work\n restart: always\n
The docs have a detailed page on Environment Variables for reference.
Firewalled portsIf you have a firewall running, you may need to open ports 25
, 587
and 465
.
For example, with the firewall ufw
, run:
ufw allow 25\nufw allow 587\nufw allow 465\n
Caution: This may not be sound advice.
Configure your DNS service to use an MX record for the hostname (eg: mail
) you configured in the previous step and add the SPF TXT record.
If you manually manage the DNS zone file for the domain
It would look something like this:
$ORIGIN example.com\n@ IN A 10.11.12.13\nmail IN A 10.11.12.13\n\n; mail server for example.com\n@ IN MX 10 mail.example.com.\n\n; Add SPF record\n@ IN TXT \"v=spf1 mx -all\"\n
Then don't forget to change the SOA
serial number, and to restart the service.
Generate DKIM keys for your domain via setup config dkim
.
Copy the content of the file docker-data/dms/config/opendkim/keys/example.com/mail.txt
and add it to your DNS records as a TXT like SPF was handled above.
I use bind9 for managing my domains, so I just paste it on example.com.db
:
mail._domainkey IN TXT ( \"v=DKIM1; h=sha256; k=rsa; \"\n \"p=MIIBIjANBgkqhkiG9w0BAQEFACAQ8AMIIBCgKCAQEAaH5KuPYPSF3Ppkt466BDMAFGOA4mgqn4oPjZ5BbFlYA9l5jU3bgzRj3l6/Q1n5a9lQs5fNZ7A/HtY0aMvs3nGE4oi+LTejt1jblMhV/OfJyRCunQBIGp0s8G9kIUBzyKJpDayk2+KJSJt/lxL9Iiy0DE5hIv62ZPP6AaTdHBAsJosLFeAzuLFHQ6USyQRojefqFQtgYqWQ2JiZQ3\"\n \"iqq3bD/BVlwKRp5gH6TEYEmx8EBJUuDxrJhkWRUk2VDl1fqhVBy8A9O7Ah+85nMrlOHIFsTaYo9o6+cDJ6t1i6G1gu+bZD0d3/3bqGLPBQV9LyEL1Rona5V7TJBGg099NQkTz1IwIDAQAB\" ) ; ----- DKIM key mail for example.com\n
Get an SSL certificate, we have a guide for you here (Let's Encrypt is a popular service to get free SSL certificates).
Start DMS and check the terminal output for any errors: docker compose up
.
Create email accounts and aliases:
With SPOOF_PROTECTION=0
setup email add admin@example.com passwd123\nsetup email add info@example.com passwd123\nsetup alias add admin@example.com external-account@gmail.com\nsetup alias add info@example.com external-account@gmail.com\nsetup email list\nsetup alias list\n
Aliases make sure that any email that comes to these accounts is forwarded to your third-party email address (external-account@gmail.com
), where they are retrieved (eg: via third-party web or mobile app), instead of connecting directly to docker-mailserer
with POP3 / IMAP.
With SPOOF_PROTECTION=1
setup email add admin.gmail@example.com passwd123\nsetup email add info.gmail@example.com passwd123\nsetup alias add admin@example.com admin.gmail@example.com\nsetup alias add info@example.com info.gmail@example.com\nsetup alias add admin.gmail@example.com external-account@gmail.com\nsetup alias add info.gmail@example.com external-account@gmail.com\nsetup email list\nsetup alias list\n
This extra step is required to avoid the 553 5.7.1 Sender address rejected: not owned by user
error (the accounts used for submitting mail to Gmail are admin.gmail@example.com
and info.gmail@example.com
)
Send some test emails to these addresses and make other tests. Once everything is working well, stop the container with ctrl+c
and start it again as a daemon: docker compose up -d
.
This site lists blog entries that write about the project. If you blogged about DMS let us know so we can add it here!
What is Crowdsec?
Crowdsec is an open source software that detects and blocks attackers using log analysis. It has access to a global community-wide IP reputation database.
Source
"},{"location":"examples/tutorials/crowdsec/#installation","title":"Installation","text":"Crowdsec supports multiple installation methods, however this page will use the docker installation.
"},{"location":"examples/tutorials/crowdsec/#docker-mailserver","title":"Docker mailserver","text":"In your compose.yaml
for the DMS service, add a bind mount volume for /var/log/mail
. This is to share the DMS logs to a separate crowdsec container.
Example
services:\n mailserver:\n - /docker-data/dms/mail-logs/:/var/log/mail/\n
"},{"location":"examples/tutorials/crowdsec/#crowdsec","title":"Crowdsec","text":"The crowdsec container should also bind mount the same host path for the DMS logs that was added in the DMS example above.
services:\n image: crowdsecurity/crowdsec\n restart: unless-stopped\n ports:\n - \"8080:8080\"\n - \"6060:6060\"\n volumes:\n - /docker-data/dms/mail-logs/:/var/log/dms:ro\n - ./acquis.d:/etc/crowdsec/acquis.d\n - crowdsec-db:/var/lib/crowdsec/data/\n environment:\n # These collection contains parsers and scenarios for postfix and dovecot\n COLLECTIONS: crowdsecurity/postfix crowdsecurity/dovecot\n TZ: Europe/Paris\nvolumes:\n crowdsec-db:\n
"},{"location":"examples/tutorials/crowdsec/#configuration","title":"Configuration","text":"Configure crowdsec to read and parse DMS logs file.
Example
Create the file dms.yml
in ./acquis.d/
---\nsource: file\nfilenames:\n - /var/log/dms/mail.log\nlabels:\n type: syslog\n
Warning
Crowdsec on its own is just a detection software, the remediation is done by components called bouncers. This page does not explain how to install or configure a bouncer. It can be found in crowdsec documentation.
"},{"location":"examples/tutorials/docker-build/","title":"Tutorials | Docker Build","text":""},{"location":"examples/tutorials/docker-build/#building-your-own-docker-image","title":"Building your own Docker image","text":""},{"location":"examples/tutorials/docker-build/#submodules","title":"Submodules","text":"You'll need to retrieve the git submodules prior to building your own Docker image. From within your copy of the git repo run the following to retrieve the submodules and build the Docker image:
git submodule update --init --recursive\ndocker build --tag <YOUR CUSTOM IMAGE NAME> .\n
Or, you can clone and retrieve the submodules in one command:
git clone --recurse-submodules https://github.com/docker-mailserver/docker-mailserver\n
"},{"location":"examples/tutorials/docker-build/#about-docker","title":"About Docker","text":""},{"location":"examples/tutorials/docker-build/#minimum-supported-version","title":"Minimum supported version","text":"We make use of build features that require a recent version of Docker. v23.0 or newer is advised, but earlier releases may work.
DOCKER_BUILDKIT=1
.The Dockerfile
includes several build ARG
instructions that can be configured:
DOVECOT_COMMUNITY_REPO
: Install Dovecot from the community repo instead of from Debian (default = 0) DMS_RELEASE
: The image version (default = edge)VCS_REVISION
: The git commit hash used for the build (default = unknown)Note
DMS_RELEASE
(when not edge
) will be used to check for updates from our GH releases page at runtime due to the default feature ENABLE_UPDATE_CHECK=1
.DMS_RELEASE
and VCS_REVISION
are also used with opencontainers
metadata LABEL
instructions.Dovecot supports several FTS backends for providing fast and efficient full text searching of e-mails directly from the IMAP server.
As the size of your mail storage grows, the benefits of FTS are especially notable:
This is a community contributed guide
It extends our official docs for Dovecot FTS with a focus on Apache Solr. DMS does not officially support this integration.
"},{"location":"examples/tutorials/dovecot-solr/#setup-solr-for-dms","title":"Setup Solr for DMS","text":"An FTS backend supported by Dovecot is Apache Solr, a fast and efficient multi-purpose search indexer.
"},{"location":"examples/tutorials/dovecot-solr/#add-the-required-dovecot-solr-package","title":"Add the requireddovecot-solr
package","text":"As the official DMS image does not provide dovecot-solr
, you'll need to include the package in your own image (extending a DMS release as a base image), or via our user-patches.sh
feature:
user-patches.sh
compose.yaml
If you'd prefer to avoid a custom image build. This approach is simpler but with the caveat that any time the container is restarted, you'll have a delay as the package is installed each time.
#!/bin/bash\n\napt-get update && apt-get install dovecot-solr\n
A custom DMS image does not add much friction. You do not need a separate Dockerfile
as Docker Compose supports building from an inline Dockerfile
in your compose.yaml
.
The image
key of the service is swapped for the build
key instead, as shown below:
services:\n mailserver:\n hostname: mail.example.com\n # The `image` setting now represents the tag for the local build configured below:\n image: local/dms:14.0\n # Local build (no need to try pull `image` remotely):\n pull_policy: build\n # Add this `build` section to your real `compose.yaml` for your DMS service:\n build:\n dockerfile_inline: |\n FROM docker.io/mailserver/docker-mailserver:14.0\n RUN apt-get update && apt-get install dovecot-solr\n
docker compose up
and it will pull DMS and build your custom image to run a container.Why doesn't DMS include dovecot-solr
?
This integration is not officially supported in DMS as no maintainer is able to provide troubleshooting support.
Prior to v14, the package was included but the community contributed guide had been outdated for several years that it was non-functional. It was decided that it was better to drop support and docs, however some DMS users voiced active use of Solr and it's benefits over Xapian for FTS which led to these revised docs.
ARM64 builds do not have support for dovecot-solr
. Additionally the user demand for including dovecot-solr
is presently too low to justify vs the minimal effort to add additional packages as shown above.
compose.yaml
config","text":"Firstly you need a working Solr container, for this the official docker image will do:
services:\n solr:\n image: solr:latest\n container_name: dms-solr\n environment:\n # As Solr can be quite resource hungry, raise the memory limit to 2GB.\n # The default is 512MB, which may be exhausted quickly.\n SOLR_JAVA_MEM: \"-Xms2g -Xmx2g\"\n volumes:\n - ./docker-data/solr:/var/solr\n restart: always\n
DMS will connect internally to the solr
service above. Either have both services in the same compose.yaml
file, or ensure that the containers are connected to the same docker network.
Once the Solr container is started, you need to configure a \"Solr core\" for Dovecot:
docker exec -it dms-solr /bin/sh\nsolr create -c dovecot\ncp -R /opt/solr/contrib/analysis-extras/lib /var/solr/data/dovecot\n
Stop the dms-solr
container and you should now have a ./data/dovecot
folder in the local bind mount volume.
Solr needs a schema that is specifically tailored for Dovecot FTS.
As of writing of this guide, Solr 9 is the current release. Dovecot provides the required schema configs for Solr, copy the following two v9 config files to ./data/dovecot
and rename them accordingly:
solr-config-9.xml
(rename to solrconfig.xml
)solr-schema-9.xml
(rename to schema.xml
)Additionally, remove the managed-schema.xml
file from ./data/dovecot
and ensure the two files you copied have a UID and GID of 8983
assigned.
Start the Solr container once again, you should now have a working Solr core specifically for Dovecot FTS.
Configure Dovecot in DMS to connect to this Solr core:
Create a 10-plugin.conf
file in your ./config/dovecot
folder with this contents:
mail_plugins = $mail_plugins fts fts_solr\n\nplugin {\n fts = solr\n fts_autoindex = yes\n fts_solr = url=http://dms-solr:8983/solr/dovecot/\n}\n
Add a volume mount for that config to your DMS service in compose.yaml
:
services:\n mailserver:\n volumes:\n - ./docker-data/config/dovecot/10-plugin.conf:/etc/dovecot/conf.d/10-plugin.conf:ro\n
After following the previous steps, restart DMS and run this command to have Dovecot re-index all mail:
docker compose exec mailserver doveadm fts rescan -A\n
Indexing will take a while depending on how large your mail folders
Usually within 15 minutes or so, you should be able to search your mail using the Dovecot FTS feature!
"},{"location":"examples/tutorials/mailserver-behind-proxy/","title":"Tutorials | Mail Server behind a Proxy","text":""},{"location":"examples/tutorials/mailserver-behind-proxy/#using-a-reverse-proxy","title":"Using a Reverse Proxy","text":"Guidance is provided via a Traefik config example, however if you're only familiar with configuring a reverse proxy for web services there are some differences to keep in mind.
This reduces many of the benefits for why you might use a reverse proxy, but they can still be useful.
Some deployments may require a service to route traffic (kubernetes) when deploying, in which case the below advice is important to understand well.
The guide here has also been adapted for our Kubernetes docs.
"},{"location":"examples/tutorials/mailserver-behind-proxy/#what-can-go-wrong","title":"What can go wrong?","text":"Without a reverse proxy involved, a service is typically aware of the client IP for a connection.
However when a reverse proxy routes the connection this information can be lost, and the proxied service mistakenly treats the client IP as the reverse proxy handling the connection.
A key difference for how the network is proxied relates to the OSI Model:
When working with Layer 7 and a protocol like HTTP, it is possible to inspect a protocol header like Forwarded
(or it's predecessor: X-Forwarded-For
). At a lower level with Layer 4, that information is not available and we are routing traffic agnostic to the application protocol being proxied.
A proxy can prepend the PROXY protocol header to the TCP/UDP connection as it is routed to the service, which must be configured to be compatible with PROXY protocol (often this adds a restriction that connections must provide the header, otherwise they're rejected).
Beyond your own proxy, traffic may be routed in the network by other means that would also rewrite this information such as Docker's own network management via iptables
and userland-proxy
(NAT). The PROXY header ensures the original source and destination IP addresses, along with their ports is preserved across transit.
The below guidance is focused on configuring Traefik, but the advice should be roughly applicable elsewhere (eg: NGINX, Caddy).
The Traefik service config is fairly standard, just define the necessary entrypoints:
compose.yamlservices:\n reverse-proxy:\n image: docker.io/traefik:latest # 2.10 / 3.0\n # CAUTION: In production you should configure the Docker API endpoint securely:\n # https://doc.traefik.io/traefik/providers/docker/#docker-api-access\n volumes:\n - /var/run/docker.sock:/var/run/docker.sock\n command:\n # Docker provider config:\n - --providers.docker=true\n - --providers.docker.exposedbydefault=false\n # DMS ports you want to proxy:\n - --entryPoints.mail-smtp.address=:25\n - --entryPoints.mail-submission.address=:587\n - --entryPoints.mail-submissions.address=:465\n - --entryPoints.mail-imap.address=:143\n - --entryPoints.mail-imaps.address=:993\n - --entryPoints.mail-pop3.address=:110\n - --entryPoints.mail-pop3s.address=:995\n - --entryPoints.mail-managesieve.address=:4190\n # Publish external access ports mapped to traefik entrypoint ports:\n ports:\n - \"25:25\"\n - \"587:587\"\n - \"465:465\"\n - \"143:143\"\n - \"993:993\"\n - \"110:110\"\n - \"995:995\"\n - \"4190:4190\"\n # An IP is assigned here for other services (Dovecot) to trust for PROXY protocol:\n networks:\n default:\n ipv4_address: 172.16.42.2\n\n# Specifying a subnet to assign a fixed container IP to the reverse proxy:\nnetworks:\n default:\n name: my-network\n ipam:\n config:\n - subnet: \"172.16.42.0/24\"\n
Extra considerations
--providers.docker.network=my-network
is useful when there is more than one network to consider.services:\n dms:\n image: ghcr.io/docker-mailserver/docker-mailserver:latest\n hostname: mail.example.com\n labels:\n - traefik.enable=true\n\n # These are examples, configure the equivalent for any additional ports you proxy.\n # Explicit TLS (STARTTLS):\n - traefik.tcp.routers.mail-smtp.rule=HostSNI(`*`)\n - traefik.tcp.routers.mail-smtp.entrypoints=smtp\n - traefik.tcp.routers.mail-smtp.service=smtp\n - traefik.tcp.services.mail-smtp.loadbalancer.server.port=25\n - traefik.tcp.services.mail-smtp.loadbalancer.proxyProtocol.version=2\n\n # Implicit TLS is no different, except for optional HostSNI support:\n - traefik.tcp.routers.mail-submissions.rule=HostSNI(`*`)\n - traefik.tcp.routers.mail-submissions.entrypoints=smtp-submissions\n - traefik.tcp.routers.mail-submissions.service=smtp-submissions\n - traefik.tcp.services.mail-submissions.loadbalancer.server.port=465\n - traefik.tcp.services.mail-submissions.loadbalancer.proxyProtocol.version=2\n # NOTE: Optionally match by SNI rule, this requires TLS passthrough (not compatible with STARTTLS):\n #- traefik.tcp.routers.mail-submissions.rule=HostSNI(`mail.example.com`)\n #- traefik.tcp.routers.mail-submissions.tls.passthrough=true\n
PROXY protocol compatibility
Only TCP routers support enabling PROXY Protocol (via proxyProtocol.version=2
)
Postfix and Dovecot are both compatible with PROXY protocol v1 and v2.
"},{"location":"examples/tutorials/mailserver-behind-proxy/#ports","title":"Ports","text":"Technical Details - Ports (Traefik config)Explicit TLS (STARTTLS)
Service Ports: mail-smtp
(25), mail-submission
(587), mail-imap
(143), mail-pop3
(110), mail-managesieve
(4190)
HostSNI
router rule is not usable (see \"HostSNI & TLS\"). This limits routing flexibility for these ports (eg: routing these ports by the FQDN to different DMS containers).Implicit TLS
Service Ports: mail-submissions
(465), mail-imaps
(993), mail-pop3s
(995)
The HostSNI
router rule could specify the DMS FQDN instead of *
:
*
when the client does not provide a server name. Some clients must explicitly opt-in, such as CLI clients openssl
(-servername
) and swaks
(--tls-sni
).tls.passthrough=true
to the router (this implicitly enables TLS).acme.json
from Traefik).Unlike proxying HTTPS (port 443) to a container via HTTP (port 80), the equivalent for DMS service ports is not supported:
tls.passthrough.true
would not be required, additionally port 25 would always be unencrypted (if the proxy exclusively manages TLS/certs), or unreachable by public MTAs attempting delivery if the proxy enables implicit TLS for this port.This can be handled via our config override support.
Postfix via postfix-master.cf
:
smtp/inet/postscreen_upstream_proxy_protocol=haproxy\nsubmission/inet/smtpd_upstream_proxy_protocol=haproxy\nsubmissions/inet/smtpd_upstream_proxy_protocol=haproxy\n
postscreen_upstream_proxy_protocol
and smtpd_upstream_proxy_protocol
both specify the protocol type used by a proxy. haproxy
represents the PROXY protocol.
Dovecot via dovecot.cf
:
haproxy_trusted_networks = 172.16.42.2\n\nservice imap-login {\n inet_listener imap {\n haproxy = yes\n }\n\n inet_listener imaps {\n haproxy = yes\n }\n}\n\nservice pop3-login {\n inet_listener pop3 {\n haproxy = yes\n }\n\n inet_listener pop3s {\n haproxy = yes\n }\n}\n\nservice managesieve-login {\n inet_listener sieve {\n haproxy = yes\n }\n}\n
haproxy_trusted_networks
must reference the reverse proxy IP, or a wider subnet using CIDR notation.haproxy = yes
for the TCP listeners of each login service.Internal traffic (within the network or DMS itself)
A solution is to configure alternative service ports that offer PROXY protocol support (as shown next).
Alternatively routing connections to DMS through the local reverse proxy via DNS query rewriting can work too.
Configuring services with separate ports for PROXY protocolIn this example we'll take the original service ports and add 10000
for the new PROXY protocol service ports.
Traefik labels will need to update their service ports accordingly (eg: .loadbalancer.server.port=10465
).
Postfix config now requires our user-patches.sh
support to add new services in /etc/postfix/master.cf
:
#!/bin/bash\n\n# Duplicate the config for the submission(s) service ports (587 / 465) with adjustments for the PROXY ports (10587 / 10465) and `syslog_name` setting:\npostconf -Mf submission/inet | sed -e s/^submission/10587/ -e 's/submission/submission-proxyprotocol/' >> /etc/postfix/master.cf\npostconf -Mf submissions/inet | sed -e s/^submissions/10465/ -e 's/submissions/submissions-proxyprotocol/' >> /etc/postfix/master.cf\n# Enable PROXY Protocol support for these new service variants:\npostconf -P 10587/inet/smtpd_upstream_proxy_protocol=haproxy\npostconf -P 10465/inet/smtpd_upstream_proxy_protocol=haproxy\n\n# Create a variant for port 25 too (NOTE: Port 10025 is already assigned in DMS to Amavis):\npostconf -Mf smtp/inet | sed -e s/^smtp/12525/ >> /etc/postfix/master.cf\n# Enable PROXY Protocol support (different setting as port 25 is handled via postscreen), optionally configure a `syslog_name` to distinguish in logs:\npostconf -P 12525/inet/postscreen_upstream_proxy_protocol=haproxy 12525/inet/syslog_name=smtp-proxyprotocol\n
Supporting port 25 with an additional PROXY protocol port will also require a postfix-main.cf
override line for postscreen
to work correctly:
postscreen_cache_map = proxy:btree:$data_directory/postscreen_cache\n
Dovecot is mostly the same as before:
ssl = yes
when implicit TLS is needed.haproxy_trusted_networks = 172.16.42.2\n\nservice imap-login {\n inet_listener imap-proxied {\n haproxy = yes\n port = 10143\n }\n\n inet_listener imaps-proxied {\n haproxy = yes\n port = 10993\n ssl = yes\n }\n}\n\nservice pop3-login {\n inet_listener pop3-proxied {\n haproxy = yes\n port = 10110\n }\n\n inet_listener pop3s-proxied {\n haproxy = yes\n port = 10995\n ssl = yes\n }\n}\n\nservice managesieve-login {\n inet_listener sieve-proxied {\n haproxy = yes\n port = 14190\n }\n}\n
"},{"location":"examples/tutorials/mailserver-behind-proxy/#verification","title":"Verification","text":"Send an email through the reverse proxy. If you do not use the DNS query rewriting approach, you'll need to do this from an external client.
Sending a generic test mail throughswaks
CLI Run a swaks
command and then check your DMS logs for the expected client IP, it should no longer be using the reverse proxy IP.
# NOTE: It is common to find port 25 is blocked from outbound connections, you may only be able to test the submission(s) ports.\nswaks --helo not-relevant.test --server mail.example.com --port 25 -tls --from hello@not-relevant.test --to user@example.com\n
--server
as the DMS FQDN or an IP address, where either should connect to the reverse proxy service.not-relevant.test
technically may be subject to some tests, at least for port 25. With the submission(s) ports those should be exempt.-tls
will use STARTTLS on port 25, you can exclude it to send unencrypted, but it would still go through the same port/route being tested.--port 587 -tls
or --port 465 -tlsc
with your credentials --auth-user user@example.com --auth-password secret
--tls-sni mail.example.com
if you have configured HostSNI
in Traefik router rules (SNI routing is only valid for implicit TLS ports).Testing from the Docker host technically works, however the IP is likely subject to more manipulation via iptables
than an external client.
The IP will likely appear as from the gateway IP of the Docker network associated to the reverse proxy, where that gateway IP then becomes the client IP when writing the PROXY protocol header.
"},{"location":"examples/tutorials/mailserver-behind-proxy/#security-concerns","title":"Security concerns","text":""},{"location":"examples/tutorials/mailserver-behind-proxy/#forgery","title":"Forgery","text":"Since the PROXY protocol sends a header with the client IP rewritten for software to use instead, this could be abused by bad actors.
Software on the receiving end of the connection often supports configuring an IP or CIDR range of clients to trust receiving the PROXY protocol header from.
Risk exposureIf you trust more than the reverse proxy IP, you must consider the risk exposure:
127.0.0.1
, or a private subnet 172.16.0.0/12
).iptables
with the kernel tunable sysctl net.ipv4.ip_forward=1
(enabled implicitly). Port access is via HOST:CONTAINER
ports published to their respective interface(s), that includes the container IP + port.While some concerns raised above are rather specific, these type of issues aren't exclusive to Docker and difficult to keep on top of as software is constantly changing. Limit the trusted networks where possible.
Postfix has no concept of trusted proxiesPostfix does not appear to have a way to configure trusted proxies like Dovecot does (haproxy_trusted_networks
).
postscreen_access_list
(or smtpd_client_restrictions
with check_client_access
for ports 587/465) can both restrict access by IP via a CIDR lookup table, however the client IP is already rewritten at this point via PROXY protocol.
Thus those settings cannot be used for restricting access to only trusted proxies, only to the actual clients.
A similar setting mynetworks
/ PERMIT_DOCKER
manages elevated trust for bypassing security restrictions. While it is intended for trusted clients, it has no relevance to trusting proxies for the same reasons.
While PROXY protocol works well with the reverse proxy, you may have some containers internally that interact with DMS on behalf of multiple clients.
Roundcube + Fail2BanYou may have other services with functionality like an API to send mail through DMS that likewise delegates credentials through DMS.
Roundcube is an example of this where authentication is delegated to DMS, which introduces the same concern with loss of client IP.
You should adjust configuration of these monitoring services to monitor for auth failures from those services directly instead, adding an exclusion for that service IP from any DMS logs monitored (but be mindful of PROXY header forgery risks).
"},{"location":"examples/use-cases/auth-lua/","title":"Examples | Use Cases | Lua Authentication","text":""},{"location":"examples/use-cases/auth-lua/#introduction","title":"Introduction","text":"Dovecot has the ability to let users create their own custom user provisioning and authentication providers in Lua. This allows any data source that can be approached from Lua to be used for authentication, including web servers. It is possible to do more with Dovecot and Lua, but other use cases fall outside of the scope of this documentation page.
Community contributed guide
Dovecot authentication via Lua scripting is not officially supported in DMS. No assistance will be provided should you encounter any issues.
DMS provides the required packages to support this guide. Note that these packages will be removed should they introduce any future maintenance burden.
The example in this guide relies on the current way in which DMS works with Dovecot configuration files. Changes to this to accommodate new authentication methods such as OpenID Connect will likely break this example in the future. This guide is updated on a best-effort base.
Dovecot's Lua support can be used for user provisioning (userdb functionality) and/or password verification (passdb functionality). Consider using other userdb and passdb options before considering Lua, since Lua does require the use of additional (unsupported) program code that might require maintenance when updating DMS.
Each implementation of Lua-based authentication is custom. Therefore it is impossible to write documentation that covers every scenario. Instead, this page describes a single example scenario. If that scenario is followed, you will learn vital aspects that are necessary to kickstart your own Lua development:
This scenario starts with DMS being configured to use LDAP for mailbox identification, user authorization and user authentication. In this scenario, Nextcloud is also a service that uses the same LDAP server for user identification, authorization and authentication.
The goal of this scenario is to have Dovecot not authenticate the user against LDAP, but against Nextcloud using an application password. The idea behind this is that a compromised mailbox password does not compromise the user's account entirely. To make this work, Nextcloud is configured to deny the use of account passwords by clients and to disable account password reset through mail verification.
If the application password is configured correctly, an adversary can only use it to access the user's mailbox on DMS, and CalDAV and CardDAV data on Nextcloud. File access through WebDAV can be disabled for the application password used to access mail. Having CalDAV and CardDAV compromised by the same password is a minor setback. If an adversary gets access to a Nextcloud application password through a device of the user, it is likely that the adversary also gets access to the user's calendars and contact lists anyway (locally or through the same account settings used for mail and CalDAV/CardDAV synchronization). The user's stored files in Nextcloud, the LDAP account password and any other services that rely on it would still be protected. A bonus is that a user is able to revoke and renew the mailbox password in Nextcloud for whatever reason, through a friendly user interface with all the security measures with which the Nextcloud instance is configured (e.g. verification of the current account password).
A drawback of this method is that any (compromised) Nextcloud application password can be used to access the user's mailbox. This introduces a risk that a Nextcloud application password used for something else (e.g. WebDAV file access) is compromised and used to access the user's mailbox. Discussion of that risk and possible mitigations fall outside of the scope of this scenario.
To answer the questions asked earlier for this specific scenario:
While it is possible to extend the authentication methods which Nextcloud can facilitate with Nextcloud apps, there is currently a mismatch between what DMS supports and what Nextcloud applications can provide. This might change in the future. For now, Lua will be used to bridge the gap between DMS and Nextcloud for authentication only (Dovecot passdb), while LDAP will still be used to identify mailboxes and verify authorization (Dovecot userdb).
"},{"location":"examples/use-cases/auth-lua/#modify-dovecots-configuration","title":"Modify Dovecot's configuration","text":"Add to DMS volumes incompose.yaml
# All new volumes are marked :ro to configure them as read-only, since their contents are not changed from inside the container\n volumes:\n # Configuration override to disable LDAP authentication\n - ./docker-data/dms/config/dovecot/auth-ldap.conf.ext:/etc/dovecot/conf.d/auth-ldap.conf.ext:ro\n # Configuration addition to enable Lua authentication\n - ./docker-data/dms/config/dovecot/auth-lua-httpbasic.conf:/etc/dovecot/conf.d/auth-lua-httpbasic.conf:ro\n # Directory containing Lua scripts\n - ./docker-data/dms/config/dovecot/lua/:/etc/dovecot/lua/:ro\n
Create a directory for Lua scripts:
mkdir -p ./docker-data/dms/config/dovecot/lua\n
Create configuration file ./docker-data/dms/config/dovecot/auth-ldap.conf.ext
for LDAP user provisioning:
userdb {\n driver = ldap\n args = /etc/dovecot/dovecot-ldap.conf.ext\n}\n
Create configuration file ./docker-data/dms/config/dovecot/auth-lua-httpbasic.conf
for Lua user authentication:
passdb {\n driver = lua\n args = file=/etc/dovecot/lua/auth-httpbasic.lua blocking=yes\n}\n
That is all for configuring Dovecot.
"},{"location":"examples/use-cases/auth-lua/#create-the-lua-script","title":"Create the Lua script","text":"Create Lua file ./docker-data/dms/config/dovecot/lua/auth-httpbasic.lua
with contents:
local http_url = \"https://nextcloud.example.com/remote.php/dav/\"\nlocal http_method = \"PROPFIND\"\nlocal http_status_ok = 207\nlocal http_status_failure = 401\nlocal http_header_forwarded_for = \"X-Forwarded-For\"\n\npackage.path = package.path .. \";/etc/dovecot/lua/?.lua\"\nlocal base64 = require(\"base64\")\n\nlocal http_client = dovecot.http.client {\n timeout = 1000;\n max_attempts = 1;\n debug = false;\n}\n\nfunction script_init()\n return 0\nend\n\nfunction script_deinit()\nend\n\nfunction auth_passdb_lookup(req)\n local auth_request = http_client:request {\n url = http_url;\n method = http_method;\n }\n auth_request:add_header(\"Authorization\", \"Basic \" .. (base64.encode(req.user .. \":\" .. req.password)))\n auth_request:add_header(http_header_forwarded_for, req.remote_ip)\n local auth_response = auth_request:submit()\n local resp_status = auth_response:status()\n local reason = auth_response:reason()\n\n local returnStatus = dovecot.auth.PASSDB_RESULT_INTERNAL_FAILURE\n local returnDesc = http_method .. \" - \" .. http_url .. \" - \" .. resp_status .. \" \" .. reason\n if resp_status == http_status_ok\n then\n returnStatus = dovecot.auth.PASSDB_RESULT_OK\n returnDesc = \"nopassword=y\"\n elseif resp_status == http_status_failure\n then\n returnStatus = dovecot.auth.PASSDB_RESULT_PASSWORD_MISMATCH\n returnDesc = \"\"\n end\n return returnStatus, returnDesc\nend\n
Replace the hostname in the URL to the actual hostname of Nextcloud.
Dovecot provides an HTTP client for use in Lua. Aside of that, Lua by itself is pretty barebones. It chooses library compactness over included functionality. You can see that in that a separate library is referenced to add support for Base64 encoding, which is required for HTTP basic access authentication. This library (also a Lua script) is not included. It must be downloaded and stored in the same directory:
cd ./docker-data/dms/config/dovecot/lua\ncurl -JLO https://raw.githubusercontent.com/iskolbin/lbase64/master/base64.lua\n
Only use native (pure Lua) libraries as dependencies if possible, such as base64.lua
from the example. This ensures maximum compatibility. Performance is less of an issue since Lua scripts written for Dovecot probably won't be long or complex, and there won't be a lot of data processing by Lua itself.
To see which Lua version is used by Dovecot if you plan to do something that is version dependent, run:
docker exec CONTAINER_NAME strings /usr/lib/dovecot/libdovecot-lua.so|grep '^LUA_'\n
While Dovecot logs the status of authentication attempts for any passdb backend, Dovecot will also log Lua scripting errors and messages sent to Dovecot's Lua API log functions. The combined DMS log (including that of Dovecot) can be viewed using docker logs CONTAINER_NAME
. If the log is too noisy (due to other processes in the container also logging to it), docker exec CONTAINER_NAME cat /var/log/mail/mail.log
can be used to view the log of Dovecot and Postfix specifically.
If working with HTTP in Lua, setting debug = true;
when initiating dovecot.http.client
will create debug log messages for every HTTP request and response.
Note that Lua runs compiled bytecode, and that scripts will be compiled when they are initially started. Once compiled, the bytecode is cached and changes in the Lua script will not be processed automatically. Dovecot will reload its configuration and clear its cached Lua bytecode when running docker exec CONTAINER_NAME dovecot reload
. A (changed) Lua script will be compiled to bytecode the next time it is executed after running the Dovecot reload command.
Advice not extensively tested
This configuration advice is a community contribution which has only been verified as a solution when using network: host
, where you have direct access to the host interfaces.
It may be applicable in other network modes if the container has control of the outbound IPs to bind to. This is not the case with bridge networks that typically bind to a private range network for containers which are bridged to a public interface via Docker.
If your Docker host is running multiple IPv4 and IPv6 IP-addresses, it may be beneficial to bind outgoing SMTP connections to specific IP-address / interface.
PTR
record for that IP resolves an address back to the same IP.reject_unknown_sender
).This can be configured by overriding the default Postfix configurations DMS provides. Create postfix-master.cf
and postfix-main.cf
files for your config volume (docker-data/dms/config
).
In postfix-main.cf
you'll have to set the smtp_bind_address
and smtp_bind_address6
to the respective IP-address on the server you want to use.
Example
Contributed solutionAlternative (unverified) postfix-main.cfsmtp_bind_address = 198.51.100.42\nsmtp_bind_address6 = 2001:DB8::42\n
Inheriting the bind from main.cf
can misconfigure services
One problem when setting smtp_bind_address
in main.cf
is that it will be inherited by any services in master.cf
that extend the smtp
transport. One of these is smtp-amavis
, which is explicitly configured to listen / connect via loopback (localhost / 127.0.0.1
).
A postfix-master.cf
override can workaround that issue by ensuring smtp-amavis
binds to the expected internal IP:
smtp-amavis/unix/smtp_bind_address=127.0.0.1\nsmtp-amavis/unix/smtp_bind_address6=::1\n
A potentially better solution might be to instead explicitly set the smtp_bind_address
override on the smtp
transport service:
smtp/inet/smtp_bind_address = 198.51.100.42\nsmtp/inet/smtp_bind_address6 = 2001:DB8::42\n
If that avoids the concern with smtp-amavis
, you may still need to additionally override for the relay
transport as well if you have configured DMS to relay mail.
IP addresses for documentation
IP addresses shown in above examples are placeholders, they are IP addresses reserved for documentation by IANA (RFC-5737 (IPv4) and RFC-3849 (IPv6)). Replace them with the IP addresses you want DMS to send mail through.
"},{"location":"examples/use-cases/external-relay-only-mailserver/","title":"Use Cases | Relay inbound and outbound mail for an internal DMS","text":""},{"location":"examples/use-cases/external-relay-only-mailserver/#introduction","title":"Introduction","text":"Community contributed guide
Adapted into a guide from this discussion.
Requirements:
mynetworks
setting).The guide below will assume the VPN is setup on 192.168.2.0/24
with:
192.168.2.2
192.168.2.3
The goal of this guide is to configure a public server that can receive inbound mail and relay that over to DMS on a private server, which can likewise submit mail outbound through a public server or service.
The primary motivation is to keep your mail storage private instead of storing it to disk unencrypted on a VPS host.
"},{"location":"examples/use-cases/external-relay-only-mailserver/#dns-setup","title":"DNS setup","text":"Follow our standard guidance for DNS setup.
Set your A, MX and PTR records for the public server as if it were running DMS.
DNS Zone file example
For this guide, we assume DNS is configured with:
11.22.33.44
@example.com
addresses must have an MX record pointing to mail.example.com
.mail.example.com
pointing to the IP address of your public server.$ORIGIN example.com\n@ IN A 11.22.33.44\nmail IN A 11.22.33.44\n\n; mail server for example.com\n@ IN MX 10 mail.example.com.\n
SPF records should also be set up as you normally would for mail.example.com
.
You will need to install Postfix on your public server. The functionality that is needed for this setup is not yet implemented in DMS, so a vanilla Postfix will probably be easier to work with, especially since this server will only be used as an inbound and outbound relay.
It's necessary to adjust some settings afterwards.
Postfix main configRoute outbound mail through a separate transportConfigure recipient domains to relay mail Create or replace/etc/postfix/main.cf
# See /usr/share/postfix/main.cf.dist for a commented, more complete version\n\nsmtpd_banner = $myhostname ESMTP $mail_name (Debian/GNU)\nbiff = no\n\n# appending .domain is the MUA's job.\nappend_dot_mydomain = no\n\n# Uncomment the next line to generate \"delayed mail\" warnings\n#delay_warning_time = 4h\n\n# See http://www.postfix.org/COMPATIBILITY_README.html -- default to 3.6 on\n# fresh installs.\ncompatibility_level = 3.6\n\n# TLS parameters\nsmtpd_tls_cert_file=/etc/postfix/certificates/mail.example.com.crt\nsmtpd_tls_key_file=/etc/postfix/certificates/mail.example.com.key\nsmtpd_tls_security_level=may\nsmtp_tls_CApath=/etc/ssl/certs\nsmtp_tls_security_level=may\nsmtp_tls_session_cache_database = btree:${data_directory}/smtp_scache\n\nalias_database = hash:/etc/aliases\nalias_maps = hash:/etc/aliases\nmaillog_file = /var/log/postfix.log\nmailbox_size_limit = 0\ninet_interfaces = all\ninet_protocols = ipv4\nreadme_directory = no\nrecipient_delimiter = +\n\n# Customizations relevant to this guide:\nmyhostname = mail.example.com\nmyorigin = example.com\nmydestination = localhost\nmynetworks = 127.0.0.0/8 [::ffff:127.0.0.0]/104 [::1]/128 192.168.2.0/24\nsmtpd_relay_restrictions = permit_mynetworks permit_sasl_authenticated defer_unauth_destination\ntransport_maps = hash:/etc/postfix/transport\nrelay_domains = $mydestination, hash:/etc/postfix/relay\n\n# Disable local system accounts and delivery:\nlocal_recipient_maps =\nlocal_transport = error:local mail delivery is disabled\n
Let's highlight some of the important parts:
mail.example.com
in mydestination
, in fact you can just set localhost
or nothing at all here as we want all mail to be relayed to our private server (DMS).mynetworks
should contain your VPN network (eg: 192.168.2.0/24
subnet).transport_maps = hash:/etc/postfix/transport
and relay_domains = $mydestination, hash:/etc/postfix/relay
, with their file contents covered below.local_recipient_maps
.mail.example.com
.Open relay
Please be aware that setting mynetworks
to a public CIDR will leave you with an open relay. Only set it to the CIDR of your VPN beyond the localhost ranges.
When mail arrives to the public server for an @example.com
address, we want to send it via the relay
transport to our private server over port 25 for delivery to DMS.
transport_maps
is configured with a transport
table file that matches recipient addresses and assigns a non-default transport. This setting has priority over relay_transport
.
Create /etc/postfix/transport
example.com relay:[192.168.2.3]:25\n
Other considerations:
* relay:[X.X.X.X]:port
to the bottom (eg: * relay:[relay1.org]:587
), which will relay everything outbound via this relay host.Tip
Instead of a file, you could alternatively configure main.cf
with transport_maps = inline:{ example.com=relay:[192.168.2.3]:25 }
We want example.com
to be relayed inbound and everything else relayed outbound.
relay_domains
is configured with a file with a list of domains that should be relayed (one per line), the 2nd value is required but can be anything.
Create /etc/postfix/relay
example.com OK\n
Tip
Instead of a file, you could alternatively configure main.cf
with relay_domains = example.com
.
Files configured with hash:
table type must run postmap
to apply changes
Run postmap /etc/postfix/transport
and postmap /etc/postfix/relay
after creating or updating either of these files, this processes them into a separate file for Postfix to use.
You can set up your DMS instance as you normally would.
mail.example.com
. Instead, use internal-mail.example.com
or something similar.mail.example.com
.Next, we need to configure our private server to relay all outbound mail through the public server (or a separate smarthost service). The setup is similar to the default relay setup.
Configure the relay hostTrust the public serverCreate postfix-relaymap.cf
@example.com [192.168.2.2]:25\n
Meaning all mail sent outbound from @example.com
addresses will be relayed through the public server at that VPN IP.
The public server mynetworks
setting from earlier trusts any mail received on port 25 from the VPN network, which is what allows the mail to be sent outbound when it'd otherwise be denied.
Create postfix-main.cf
mynetworks = 192.168.2.0/24\n
This will trust any connection from the VPN network to DMS, such as from the public server when relaying mail over to DMS at the private server.
This step is necessary to skip some security measures that DMS normally checks for, like verifying DNS records like SPF are valid. As the mail is being relayed, those checks would fail otherwise as the IP of your public server would not be authorized to send mail on behalf of the sender address in mail being relayed.
Alternative tomynetworks
setting Instead of trusting connections by their IP with the mynetworks
setting, those same security measures can be skipped for any authenticated deliveries to DMS over port 587 instead.
This is a bit more work. mynetworks
on the public server main.cf
Postfix config is for trusting DMS when it sends mail from the private server, thus you'll need to have that public Postfix service configured with a login account that DMS can use.
On the private server, DMS needs to know the credentials for that login account, that is handled with postfix-sasl-password.cf
:
@example.com user:secret\n
You could also relay mail through SendGrid, AWS SES or similar instead of the public server you're running to receive mail from. Login credentials for those relay services are provided via the same postfix-sasl-password.cf
file.
Likewise for the public server to send mail to DMS, it would need to be configured to relay mail with credentials too, removing the need for mynetworks
on the DMS postfix-main.cf
config.
The extra effort to require authentication instead of blind trust of your private subnet can be beneficial at reducing the impact of a compromised system or service on that network that wasn't expected to be permitted to send mail.
"},{"location":"examples/use-cases/external-relay-only-mailserver/#imap-pop3","title":"IMAP / POP3","text":"IMAP and POP3 need to point towards your private server, since that is where the mailboxes are located, which means you need to have a way for your MUA to connect to it.
"},{"location":"examples/use-cases/forward-only-mailserver-with-ldap-authentication/","title":"Use Cases | Forward-Only Mail Server with LDAP","text":""},{"location":"examples/use-cases/forward-only-mailserver-with-ldap-authentication/#building-a-forward-only-mail-server","title":"Building a Forward-Only Mail Server","text":"A forward-only mail server does not have any local mailboxes. Instead, it has only aliases that forward emails to external email accounts (for example to a Gmail account). You can also send email from the localhost (the computer where DMS is installed), using as sender any of the alias addresses.
The important settings for this setup (on mailserver.env
) are these:
PERMIT_DOCKER=host\nENABLE_POP3=\nENABLE_CLAMAV=0\nSMTP_ONLY=1\nENABLE_SPAMASSASSIN=0\nENABLE_FETCHMAIL=0\n
Since there are no local mailboxes, we use SMTP_ONLY=1
to disable dovecot
. We disable as well the other services that are related to local mailboxes (POP3
, ClamAV
, SpamAssassin
, etc.)
We can create aliases with ./setup.sh
, like this:
./setup.sh alias add <alias-address> <external-email-account>\n
"},{"location":"examples/use-cases/forward-only-mailserver-with-ldap-authentication/#authenticating-with-ldap","title":"Authenticating with LDAP","text":"If you want to send emails from outside the mail server you have to authenticate somehow (with a username and password). One way of doing it is described in this discussion. However if there are many user accounts, it is better to use authentication with LDAP. The settings for this on mailserver.env
are:
ACCOUNT_PROVISIONER=LDAP\nLDAP_START_TLS=yes\nLDAP_SERVER_HOST=ldap.example.org\nLDAP_SEARCH_BASE=ou=users,dc=example,dc=org\nLDAP_BIND_DN=cn=mailserver,dc=example,dc=org\nLDAP_BIND_PW=pass1234\n\nENABLE_SASLAUTHD=1\nSASLAUTHD_MECHANISMS=ldap\nSASLAUTHD_LDAP_SERVER=ldap.example.org\nSASLAUTHD_LDAP_START_TLS=yes\nSASLAUTHD_LDAP_BIND_DN=cn=mailserver,dc=example,dc=org\nSASLAUTHD_LDAP_PASSWORD=pass1234\nSASLAUTHD_LDAP_SEARCH_BASE=ou=users,dc=example,dc=org\nSASLAUTHD_LDAP_FILTER=(&(uid=%U)(objectClass=inetOrgPerson))\n
My LDAP data structure is very basic, containing only the username, password, and the external email address where to forward emails for this user. An entry looks like this:
add uid=username,ou=users,dc=example,dc=org\nuid: username\nobjectClass: inetOrgPerson\nsn: username\ncn: username\nuserPassword: {SSHA}abcdefghi123456789\nemail: external-account@gmail.com\n
This structure is different from what is expected/assumed from the configuration scripts of DMS, so it doesn't work just by using the LDAP_QUERY_FILTER_...
settings. Instead, I had to use a custom configuration (via user-patches.sh
). I created the script docker-data/dms/config/user-patches.sh
, with content like this:
#!/bin/bash\n\nrm -f /etc/postfix/{ldap-groups.cf,ldap-domains.cf}\n\npostconf \\\n \"virtual_mailbox_domains = /etc/postfix/vhost\" \\\n \"virtual_alias_maps = ldap:/etc/postfix/ldap-aliases.cf texthash:/etc/postfix/virtual\" \\\n \"smtpd_sender_login_maps = ldap:/etc/postfix/ldap-users.cf\"\n\nsed -i /etc/postfix/ldap-users.cf \\\n -e '/query_filter/d' \\\n -e '/result_attribute/d' \\\n -e '/result_format/d'\ncat <<EOF >> /etc/postfix/ldap-users.cf\nquery_filter = (uid=%u)\nresult_attribute = uid\nresult_format = %s@example.org\nEOF\n\nsed -i /etc/postfix/ldap-aliases.cf \\\n -e '/domain/d' \\\n -e '/query_filter/d' \\\n -e '/result_attribute/d'\ncat <<EOF >> /etc/postfix/ldap-aliases.cf\ndomain = example.org\nquery_filter = (uid=%u)\nresult_attribute = mail\nEOF\n\npostfix reload\n
You see that besides query_filter
, I had to customize as well result_attribute
and result_format
.
See also
For more details about using LDAP see: LDAP managed mail server with Postfix and Dovecot for multiple domains
Note
Another solution that serves as a forward-only mail server is this.
"},{"location":"examples/use-cases/imap-folders/","title":"Mailboxes (aka IMAP Folders)","text":"INBOX
is setup as the private inbox
namespace. By default target/dovecot/15-mailboxes.conf
configures the special IMAP folders Drafts
, Sent
, Junk
and Trash
to be automatically created and subscribed. They are all assigned to the private inbox
namespace (which implicitly provides the INBOX
folder).
These IMAP folders are considered special because they add a \"SPECIAL-USE\" attribute, which is a standardized way to communicate to mail clients that the folder serves a purpose like storing spam/junk mail (\\Junk
) or deleted mail (\\Trash
). This differentiates them from regular mail folders that you may use for organizing.
See target/dovecot/15-mailboxes.conf
for existing mailbox folders which you can modify or uncomment to enable some other common mailboxes. For more information try the official Dovecot documentation.
The Archive
special IMAP folder may be useful to enable. To do so, make a copy of target/dovecot/15-mailboxes.conf
and uncomment the Archive
mailbox definition. Mail clients should understand that this folder is intended for archiving mail due to the \\Archive
\"SPECIAL-USE\" attribute.
With the provided compose.yaml example, a volume bind mounts the host directory docker-data/dms/config/
to the container location /tmp/docker-mailserver/
. Config file overrides should instead be mounted to a different location as described in Overriding Configuration for Dovecot:
volumes:\n - ./docker-data/dms/config/dovecot/15-mailboxes.conf:/etc/dovecot/conf.d/15-mailboxes.conf:ro\n
"},{"location":"examples/use-cases/imap-folders/#caution","title":"Caution","text":""},{"location":"examples/use-cases/imap-folders/#adding-folders-to-an-existing-setup","title":"Adding folders to an existing setup","text":"Handling of newly added mailbox folders can be inconsistent across mail clients:
SPECIAL-USE
attributes","text":"Not all mail clients support the SPECIAL-USE
attribute for mailboxes (defined in RFC 6154). These clients will treat the mailbox folder as any other, using the name assigned to it instead.
Some clients may still know to treat these folders for their intended purpose if the mailbox name matches the common names that the SPECIAL-USE
attributes represent (eg Sent
as the mailbox name for \\Sent
).
Usually the mail client will know via context such as the SPECIAL-USE
attribute or common English mailbox names, to provide a localized label for the users preferred language.
Take care to test localized names work well as well.
"},{"location":"examples/use-cases/imap-folders/#email-clients-support","title":"Email Clients Support","text":"SPECIAL-USE
attribute enabled for archives:Archives
folder on the server.Archive
.SPECIAL-USE
attribute is enabled for archives:Windows Mail
Windows Mail has been said to ignore SPECIAL-USE
attribute and look only at the mailbox folder name assigned.
Needs citation
This information is provided by the community.
It presently lacks references to confirm the behavior. If any information is incorrect please let us know!
"},{"location":"examples/use-cases/ios-mail-push-support/","title":"Advanced | iOS Mail Push Support","text":""},{"location":"examples/use-cases/ios-mail-push-support/#introduction","title":"Introduction","text":"iOS Mail currently does not support the IMAP idle extension. Therefore users can only either check manually or configure intervals for fetching mails in their mail account preferences when using the default configuration.
To support mail push Dovecot needs to advertise the XAPPLEPUSHSERVICE
IMAP extension as well as sending the actual push notifications to the Apple Push Notification service (APNs) which will forward them to the device.
This can be done with two components:
dovecot-xaps-plugin
) which is triggered whenever a mail is created or moved from/to a mail folder.dovecot-xaps-daemon
) that manages both the device registrations as well as sending notifications to the APNs.docker-mailserver
image.Both components will be built using Docker and included into a custom docker-mailserver
image. Afterwards the required configuration is added to docker-data/dms/config
. The registration data is stored in /var/mail-state/lib-xapsd
.
Create a Dockerfile to build a docker-mailserver
image that includes the dovecot-xaps-plugin
as well as the dovecot-xaps-daemon
. This is required to ensure that the Dovecot plugin is built against the same Dovecot version. The :edge
tag is used here, but you might want to use a released version instead.
FROM mailserver/docker-mailserver:edge AS dovecot-plugin-xaps\nWORKDIR /tmp/dovecot-xaps-plugin\nRUN <<EOF\n apt-get update\n apt-get -y --no-install-recommends install git cmake make build-essential dovecot-dev\n git clone --single-branch --depth=1 https://github.com/freswa/dovecot-xaps-plugin.git .\n mkdir build && cd build\n cmake .. -DCMAKE_BUILD_TYPE=Release\n make install\nEOF\n\n# Use an older Go version as Go >= 1.20 causes this issue: https://github.com/freswa/dovecot-xaps-daemon/issues/24#issuecomment-1483876081\n# Note that the underlying issue are non-standard-compliant Apple http servers which might get fixed at some point\nFROM golang:1.19-alpine AS dovecot-xaps-daemon\nENV GOPROXY=https://proxy.golang.org,direct\nENV CGO_ENABLED=0\nWORKDIR /go/dovecot-xaps-daemon\nRUN <<EOF\n apk add --no-cache --virtual build-dependencies git\n git clone --single-branch --depth=1 https://github.com/freswa/dovecot-xaps-daemon .\n go build ./cmd/xapsd\nEOF\n\nFROM mailserver/docker-mailserver:edge\nCOPY --from=dovecot-plugin-xaps /usr/lib/dovecot/modules/*_xaps_* /usr/lib/dovecot/modules/\nCOPY --from=dovecot-xaps-daemon /go/dovecot-xaps-daemon/xapsd /usr/bin/xapsd\n\n# create a non-root user for the daemon process as well as configuration and run state directories\nRUN <<EOF\n adduser --quiet --system --group --disabled-password --home /var/mail-state/lib-xapsd --no-create-home xapsd\n mkdir -p /var/run/xapsd /etc/xapsd\nEOF\n
Build the new image:
docker build -t yourname/docker-mailserver .\n
Modify your compose.yaml
to use the newly created image:
services:\n mailserver:\n image: yourname/docker-mailserver:latest\n
Recreate the container:
docker compose down\ndocker compose up -d\n
Create a hash of your Apple developer account password using the provided xapsd -pass
command:
docker exec -it mailserver xapsd -pass\n
Add configuration for both components:
Create a folder named xaps
in docker-data/dms/config
.
Create a file named xapsd.yaml
in docker-data/dms/config/xaps
.
appleId
and appleIdHashedPassword
with your actual credentials. For reference see also here.# set the loglevel to either\n# trace, debug, error, fatal, info, panic or warn\n# Default: info\nloglevel: info\n\n# xapsd creates a json file to store the registration persistent on disk.\n# This sets the location of the file.\ndatabaseFile: /var/mail-state/lib-xapsd/database.json\n\n# xapsd listens on a socket for http/https requests from the dovecot plugin.\n# This sets the address and port number of the listen socket.\nlistenAddr: '127.0.0.1'\nport: 11619\n\n# xapsd is able to listen on a HTTPS Socket to allow HTTP/2 to be used\n# SSL is enabled implicitly when certfile and keyfile exist\n# !!! only use HTTPS for connection pooling with a proxy e.g. nginx or HaProxy\n# !!! direct usage with the plugin is discouraged and unsupported\ntlsCertfile:\ntlsKeyfile:\ntlsListenAddr:\ntlsPort: 11620\n\n# Notifications that are not initiated by new messages are not sent immediately for two reasons:\n# 1. When you move/copy/delete messages you most likely move/copy/delete more messages within a short period of time.\n# 2. You don't need your mailboxes to synchronize immediately since they are automatically synchronized when opening\n# the app\n# If a new message comes and the move/copy/delete notification is still on hold it will be sent with the notification\n# for the new message.\n# This sets the interval to check for delayed messages.\ncheckInterval: 20\n\n# Set the time how long notifications for not-new messages should be delayed until they are sent.\n# Whenever checkInterval runs, it checks if \"delay\" <= \"waiting time\" and sends the notification if the expression is\n# true.\ndelay: 30\n\n# To retrieve certificates from Apple, we need to login with a valid Apple ID\n# The accounts email must be given in cleartext, but the password has to\n# be hashed before sending it. To not leak working credentials on running servers,\n# we do not accept the cleartext password here.\nappleId: foo@example.com\n\n# use `xaps -pass` to calculate the hash of the apple id password\nappleIdHashedPassword: bar\n
Create a file named 95-xaps.conf
in docker-data/dms/config/xaps
. For reference see also here. 95-xaps.conf
protocol imap {\n mail_plugins = $mail_plugins notify push_notification xaps_push_notification xaps_imap\n}\n\nprotocol lda {\n mail_plugins = $mail_plugins notify push_notification xaps_push_notification\n}\n\nprotocol lmtp {\n mail_plugins = $mail_plugins notify push_notification xaps_push_notification\n}\n\nplugin {\n # xaps_config contains xaps specific configuration parameters\n # url: protocol, hostname and port under which xapsd listens\n # user_lookup: Use if you want to determine the username used for PNs from environment variables provided by\n # login mechanism. Value is variable name to look up.\n # max_retries: maximum num of retries the http client connects to the xaps daemon\n # timeout_msecs http timeout of the http connection\n xaps_config = url=http://127.0.0.1:11619 user_lookup=theattribute max_retries=6 timeout_msecs=5000\n push_notification_driver = xaps\n}\n
Create a supervisord file named xapsd.conf
in docker-data/dms/config/xaps
with the following content: xapsd.conf
[program:xapsd]\nstartsecs=0\nautostart=false\nautorestart=true\nstdout_logfile=/var/log/supervisor/%(program_name)s.log\nstderr_logfile=/var/log/supervisor/%(program_name)s.log\nuser=xapsd\ncommand=/usr/bin/xapsd\npidfile=/var/run/xapsd/xapsd.pid\n
Create or update your user-patches.sh
in docker-data/dms/config
to move the files to their final location as well as starting the daemon service: user-patches.sh
#!/bin/bash\n\n# Copy the configs to internal locations:\ncp /tmp/docker-mailserver/xaps/95-xaps.conf /etc/dovecot/conf.d/95-xaps.conf\ncp /tmp/docker-mailserver/xaps/xapsd.yaml /etc/xapsd/xapsd.yaml\ncp /tmp/docker-mailserver/xaps/xapsd.conf /etc/supervisor/conf.d/xapsd.conf\n\n# Setup data persistence and ensure ownership is always for xapsd:\nmkdir -p /var/mail-state/lib-xapsd\nchown -R xapsd:xapsd /var/mail-state/lib-xapsd\n\n# Start the xaps daemon:\nsupervisorctl update\nsupervisorctl start xapsd\n
Recreate the container again to apply the new configuration:
docker compose down\ndocker compose up -d\n
Recreate your mail account on your iOS device and check the logs in /var/log/supervisor/dovecot.log
and /var/log/supervisor/xapsd.log
for any errors.
Both device registration and notifications send a username to the daemon to lookup the device. While the registration and other IMAP operations in Dovecot will send the Dovecot username, LMTP will send the provided authentication username.
The format of that username is specified by the auth_username_format
Dovecot setting. If you are not using mail addresses as Dovecot usernames - e.g. when using LDAP - you can either change the auth_username_format
or add the mail address as property to the user account and use the lookup feature (see below).
sed -i -r \"s|^#?(auth_username_format =).*|\\1 %Ln|\" /etc/dovecot/conf.d/10-auth.conf\n
You can also use notifications for Dovecot alias mailboxes. Depending on your server configuration, this might require to add the original Dovecot username as Dovecot attribute to the login user as well as changing the user_lookup=theattribute
in 95-xaps.conf
to perform the lookup of that attribute.
docker-mailserver
!","text":"This Documentation is Versioned
Make sure to select the correct version of this documentation! It should match the version of the image you are using. The default version corresponds to the :latest
image tag - the most recent stable release.
This documentation provides you not only with the basic setup and configuration of DMS but also with advanced configuration, elaborate usage scenarios, detailed examples, hints and more.
"},{"location":"#about","title":"About","text":"docker-mailserver
, or DMS for short, is a production-ready fullstack but simple mail server (SMTP, IMAP, LDAP, Anti-spam, Anti-virus, etc.). It employs only configuration files, no SQL database. The image is focused around the slogan \"Keep it simple and versioned\".
If you're completely new to mail servers or you want to read up on them, check out our Introduction page. If you're new to DMS as a mail server appliance, make sure to read the Usage chapter first. If you want to look at examples for Docker Compose, we have an Examples page.
There is also a script - setup.sh
- supplied with this project. It supports you in configuring and administrating your server. Information on how to get it and how to use it is available on a dedicated page.
We have a dedicated configuration page. It contains most of the configuration and explanation you need to setup your mail server properly. Be aware that advanced tasks may still require reading through all parts of this documentation; it may also involve inspecting your running container for debugging purposes. After all, a mail server is a complex arrangement of various programs.
Important
If you'd like to change, patch or alter files or behavior of DMS, you can use a script. Just place a script called user-patches.sh
in your ./docker-data/dms/config/
folder volume (which is mounted to /tmp/docker-mailserver/
inside the container) and it will be run on container startup. See the 'Modifications via Script' page for additional documentation and an example.
You might also want to check out:
Tip
Definitely check out the FAQ for more information and tips! Please do not open an issue before you have checked our documentation for answers, including the FAQ!
"},{"location":"#tests","title":"Tests","text":"DMS employs a variety of tests. If you want to know more about our test suite, view our testing docs.
"},{"location":"#contributing","title":"Contributing","text":"We are always happy to welcome new contributors. For guidelines and entrypoints please have a look at the Contributing section.
"},{"location":"faq/","title":"FAQ","text":""},{"location":"faq/#what-kind-of-database-are-you-using","title":"What kind of database are you using?","text":"None! No database is required. The filesystem is the database. This image is based on config files that can be persisted using bind mounts (default) or Docker volumes, and as such versioned, backed up and so forth.
"},{"location":"faq/#where-are-emails-stored","title":"Where are emails stored?","text":"Mails are stored in /var/mail/${domain}/${username}
. Since v9.0.0
it is possible to add custom user_attributes
for each accounts to have a different mailbox configuration (See #1792).
INBOX
is setup by default with the special IMAP folders Drafts
, Sent
, Junk
and Trash
. You can learn how to modify or add your own folders (including additional special folders like Archive
) by visiting our docs page Customizing IMAP Folders for more information.
Make sure to read the CHANGELOG before updating to new versions, to be prepared for possible breaking changes.
Then, run the following commands:
docker compose pull\ndocker compose down\ndocker compose up -d\n
You should see the new version number on startup, for example: [ INF ] Welcome to docker-mailserver 11.3.1
. And you're done! Don't forget to have a look at the remaining functions of the setup.sh
script with ./setup.sh help
.
As you'll realistically be deploying to production on a Linux host, if you are on Windows or macOS and want to run the image locally first, it's advised to do so via a VM guest running Linux if you have issues running DMS on your host system.
"},{"location":"faq/#what-are-the-system-requirements","title":"What are the system requirements?","text":""},{"location":"faq/#recommended","title":"Recommended","text":"Warning
ClamAV can consume a lot of memory, as it reads the entire signature database into RAM.
Current figure is about 850M and growing. If you get errors about ClamAV or amavis failing to allocate memory you need more RAM or more swap and of course docker must be allowed to use swap (not always the case). If you can't use swap at all you may need 3G RAM.
"},{"location":"faq/#how-to-alter-a-running-dms-instance-without-relaunching-the-container","title":"How to alter a running DMS instance without relaunching the container?","text":"DMS aggregates multiple \"sub-services\", such as Postfix, Dovecot, Fail2ban, SpamAssassin, etc. In many cases, one may edit a sub-service's config and reload that very sub-service, without stopping and relaunching the whole mail server.
In order to do so, you'll probably want to push your config updates to your server through a Docker volume (these docs use: ./docker-data/dms/config/:/tmp/docker-mailserver/
), then restart the sub-service to apply your changes, using supervisorctl
. For instance, after editing fail2ban's config: supervisorctl restart fail2ban
.
See the documentation for supervisorctl
.
Tip
To add, update or delete an email account; there is no need to restart postfix / dovecot service inside the container after using setup.sh
script.
For more information, see #1639.
"},{"location":"faq/#how-can-i-sync-the-container-and-host-datetime","title":"How can I sync the container and host date/time?","text":"Share the host's /etc/localtime
with the container, e.g. by using a bind mount:
volumes:\n - /etc/localtime:/etc/localtime:ro\n
Optionally, you can set the TZ
ENV variable; e.g. TZ=Europe/Berlin
. Check this list for which values are allowed.
Properly working DNS servers are crucial for differentiating spam from legitimate e-mails. Records like SPF
, DKIM
and DMARC
records, as well as working name (resolving A
records) and reverse name (resolving PTR
records) resolution ensures legitimate e-mails arrive while e-mails that are more likely phishing and spam do not.
Anti-spam measures (like SpamAssassin or Rspamd) make use of DNS block lists. To learn more check out our Rspamd documentation on this topic. In case you want to utilize RBL/DNSBLs, you need a recursive DNS resolver (not big custom resolvers like Cloudflare, Quad9, Google, etc.).
DMS does not integrate support for an internal DNS service as this is a responsibility that is sensitive to the host environment. You can configure internal services within DMS to use your own managed DNS server, or configure for such at the host or container level (such as with compose.yaml
).
All files are using the Unix format with LF
line endings. Please do not use CRLF
.
DMS supports multiple domains out of the box, so you can do this:
./setup.sh email add user1@example.com\n./setup.sh email add user1@example.de\n./setup.sh email add user1@server.example.org\n
"},{"location":"faq/#what-about-backups","title":"What about backups?","text":""},{"location":"faq/#bind-mounts-default","title":"Bind mounts (default)","text":"From the location of your compose.yaml
, create a compressed archive of your docker-data/dms/config/
and docker-data/dms/mail-*
folders:
tar --gzip -cf \"backup-$(date +%F).tar.gz\" ./docker-data/dms\n
Then to restore docker-data/dms/config/
and docker-data/dms/mail-*
folders from your backup file:
tar --gzip -xf backup-date.tar.gz\n
"},{"location":"faq/#volumes","title":"Volumes","text":"Assuming that you use docker-compose
and data volumes, you can backup the configuration, emails and logs like this:
# create backup\ndocker run --rm -it \\\n -v \"${PWD}/docker-data/dms/config/:/tmp/docker-mailserver/\" \\\n -v \"${PWD}/docker-data/dms-backups/:/backup/\" \\\n --volumes-from mailserver \\\n alpine:latest \\\n tar czf \"/backup/mail-$(date +%F).tar.gz\" /var/mail /var/mail-state /var/log/mail /tmp/docker-mailserver\n\n# delete backups older than 30 days\nfind \"${PWD}/docker-data/dms-backups/\" -type f -mtime +30 -delete\n
"},{"location":"faq/#i-want-to-know-more-about-the-ports","title":"I Want to Know More About the Ports","text":"See this part of the documentation for further details and best practice advice, especially regarding security concerns.
"},{"location":"faq/#how-can-i-configure-my-email-client","title":"How can I configure my email client?","text":"Login is full email address (<user>@<domain>
).
# IMAP\nusername: <user1@example.com>\npassword: <mypassword>\nserver: <mail.example.com>\nimap port: 143 or 993 with STARTTLS/SSL (recommended)\nimap path prefix: INBOX\n\n# SMTP\nsmtp port: 587 or 465 with STARTTLS/SSL (recommended)\nusername: <user1@example.com>\npassword: <mypassword>\n
DMS is properly configured for port 587, if possible, we recommend using port 465 for SMTP though. See this section to learn more about ports.
"},{"location":"faq/#can-i-use-a-nakedbare-domain-ie-no-hostname","title":"Can I use a naked/bare domain (i.e. no hostname)?","text":"Yes, but not without some configuration changes. Normally it is assumed that DMS runs on a host with a name, so the fully qualified host name might be mail.example.com
with the domain example.com
. The MX records point to mail.example.com
.
To use a bare domain (where the host name is example.com
and the domain is also example.com
), change mydestination
:
mydestination = $myhostname, localhost.$mydomain, localhost
mydestination = localhost.$mydomain, localhost
Add the latter line to docker-data/dms/config/postfix-main.cf
. If that doesn't work, make sure that OVERRIDE_HOSTNAME
is blank in your mailserver.env
file. Without these changes there will be warnings in the logs like:
warning: do not list domain example.com in BOTH mydestination and virtual_mailbox_domains\n
Plus of course mail delivery fails.
Also you need to define hostname: example.com
in your compose.yaml
.
You might not want a bare domain
We encourage you to consider using a subdomain where possible.
user@example.com
, that is distinct from your hostname which is identified by a DNS MX record.Considering you want to redirect all incoming e-mails for the domain example.com
to user1@example.com
, add the following line to docker-data/dms/config/postfix-virtual.cf
:
@example.com user1@example.com\n
"},{"location":"faq/#how-can-i-delete-all-the-emails-for-a-specific-user","title":"How can I delete all the emails for a specific user?","text":"First of all, create a special alias named devnull
by editing docker-data/dms/config/postfix-aliases.cf
:
devnull: /dev/null\n
Considering you want to delete all the e-mails received for baduser@example.com
, add the following line to docker-data/dms/config/postfix-virtual.cf
:
baduser@example.com devnull\n
Important
If you use a catch-all rule for the main/sub domain, you need another entry in docker-data/dms/config/postfix-virtual.cf
:
@mail.example.com hello@example.com\nbaduser@example.com devnull\ndevnull@mail.example.com devnull\n
"},{"location":"faq/#what-kind-of-ssl-certificates-can-i-use","title":"What kind of SSL certificates can I use?","text":"Both RSA and ECDSA certs are supported. You can provide your own cert files manually, or mount a letsencrypt
generated directory (with alternative support for Traefik's acme.json
). Check out the SSL_TYPE
documentation for more details.
If this migration implies a DNS modification, be sure to wait for DNS propagation before opening an issue. Few examples of symptoms can be found here or here.
This could be related to a modification of your MX
record, or the IP mapped to mail.example.com
. Additionally, validate your DNS configuration.
If everything is OK regarding DNS, please provide formatted logs and config files. This will allow us to help you.
If we're blind, we won't be able to do anything.
"},{"location":"faq/#connection-refused-or-no-response-at-all","title":"Connection refused or No response at all","text":"You see errors like \"Connection Refused\" and \"Connection closed by foreign host\", or you cannot connect at all? You may not be able to connect with your mail client (MUA)? Make sure to check Fail2Ban did not ban you (for exceeding the number of tried logins for example)! You can run
docker exec <CONTAINER NAME> setup fail2ban\n
and check whether your IP address appears. Use
docker exec <CONTAINER NAME> setup fail2ban unban <YOUR IP>\n
to unban the IP address.
"},{"location":"faq/#how-can-i-authenticate-users-with-smtp_only1","title":"How can I authenticate users withSMTP_ONLY=1
?","text":"See #1247 for an example.
Todo
Write a How-to / Use-Case / Tutorial about authentication with SMTP_ONLY
.
hostname
","text":"Normally you will assign DMS a hostname
such as mail.example.com
. If you instead use a bare domain (such as example.com
) or add an alias / account with the same value as your hostname
, this can cause a conflict for mail addressed to @hostname
as Postfix gets confused where to deliver the mail (hostname
is configured for only system accounts via the Postfix main.cf
setting mydestination
).
When this conflict is detected you'll find logs similar to this:
warning: do not list domain mail.example.com in BOTH mydestination and virtual_mailbox_domains\n...\nNOQUEUE: reject: RCPT from HOST[IP]: 550 5.1.1 <RECIPIENT>: Recipient address rejected: User unknown in local recipient table; ...\n
Opt-out of mail being directed to services by excluding $myhostname
as a destination with a postfix-main.cf
override config:
mydestination = localhost.$mydomain, localhost\n
Tip
You may want to configure a postmaster
alias via setup alias add
to receive system notifications.
Warning
Internal mail destined for root
, amavis
or other accounts will now no longer be received without an alias or account created for them.
Using user-patches.sh
, update the container file /etc/postfix/main.cf
to include:
proxy_interfaces = X.X.X.X (your public IP)\n
For reverse proxy support you will want to view our dedicated guide.
"},{"location":"faq/#how-to-restrict-login-by-ip","title":"How to restrict login by IP?","text":"There are a few ways you could approach this, see this discussion answer for advice.
"},{"location":"faq/#how-to-adjust-settings-with-the-user-patchessh-script","title":"How to adjust settings with theuser-patches.sh
script","text":"Suppose you want to change a number of settings that are not listed as variables or add things to the server that are not included?
DMS has a built-in way to do post-install processes. If you place a script called user-patches.sh
in the config directory it will be run after all configuration files are set up, but before the postfix, amavis and other daemons are started.
It is common to use a local directory for config added to docker-mailsever
via a volume mount in your compose.yaml
(eg: ./docker-data/dms/config/:/tmp/docker-mailserver/
).
Add or create the script file to your config directory:
cd ./docker-data/dms/config\ntouch user-patches.sh\nchmod +x user-patches.sh\n
Then fill user-patches.sh
with suitable code.
If you want to test it you can move into the running container, run it and see if it does what you want. For instance:
# start shell in container\n./setup.sh debug login\n\n# check the file\ncat /tmp/docker-mailserver/user-patches.sh\n\n# run the script\n/tmp/docker-mailserver/user-patches.sh\n\n# exit the container shell back to the host shell\nexit\n
You can do a lot of things with such a script. You can find an example user-patches.sh
script here: example user-patches.sh
script.
We also have a very similar docs page specifically about this feature!
Special use-case - patching the supervisord
configuration
It seems worth noting, that the user-patches.sh
gets executed through supervisord
. If you need to patch some supervisord config (e.g. /etc/supervisor/conf.d/saslauth.conf
), the patching happens too late.
An easy workaround is to make the user-patches.sh
reload the supervisord config after patching it:
#!/bin/bash\nsed -i 's/rimap -r/rimap/' /etc/supervisor/conf.d/saslauth.conf\nsupervisorctl update\n
"},{"location":"faq/#how-to-ban-custom-ip-addresses-with-fail2ban","title":"How to ban custom IP addresses with Fail2ban","text":"Use the following command:
./setup.sh fail2ban ban <IP>\n
The default bantime is 180 days. This value can be customized.
"},{"location":"faq/#what-to-do-in-case-of-spfforwarding-problems","title":"What to do in case of SPF/Forwarding problems","text":"If you got any problems with SPF and/or forwarding mails, give SRS a try. You enable SRS by setting ENABLE_SRS=1
. See the variable description for further information.
There are many reasons why email might be rejected, common causes are:
DMS does not manage those concerns, verify they are not causing your delivery problems before reporting a bug on our issue tracker. Resources that can help you troubleshoot:
Anti-spam rules are managed in docker-data/dms/config/spamassassin-rules.cf
.
x-headers
not inserted into my subdomain.example.com
subdomain emails?","text":"In the default setup, amavis only applies SpamAssassin x-headers into domains matching the template listed in the config file (05-domain_id
in the amavis defaults).
The default setup @local_domains_acl = ( \".$mydomain\" );
does not match subdomains. To match subdomains, you can override the @local_domains_acl
directive in the amavis user config file 50-user
with @local_domains_maps = (\".\");
to match any sort of domain template.
Put received spams in .Junk/
imap folder using SPAMASSASSIN_SPAM_TO_INBOX=1
and MOVE_SPAM_TO_JUNK=1
and add a user cron like the following:
Example
NOTE: This example assumes you have a /var/mail-state
volume mounted.
# m h dom mon dow command\n# Everyday 2:00AM, learn spam from a specific user\n0 2 * * * docker exec mailserver sa-learn --spam /var/mail/example.com/username/.Junk --dbpath /var/mail-state/lib-amavis/.spamassassin\n
With docker-compose
you can more easily use the internal instance of cron
within DMS. This is less problematic than the simple solution shown above, because it decouples the learning from the host on which DMS is running, and avoids errors if the mail server is not running.
The following configuration works nicely:
ExampleNOTE: This example assumes you have a /var/mail-state
volume mounted.
Create a system cron file:
# in the compose.yaml root directory\nmkdir -p ./docker-data/dms/cron\ntouch ./docker-data/dms/cron/sa-learn\nchown root:root ./docker-data/dms/cron/sa-learn\nchmod 0644 ./docker-data/dms/cron/sa-learn\n
Edit the system cron file nano ./docker-data/dms/cron/sa-learn
, and set an appropriate configuration:
# '> /dev/null' to send error notifications from 'stderr' to 'postmaster@example.com'\n#\n# m h dom mon dow user command\n#\n# Everyday 2:00AM, learn spam from a specific user\n# spam: junk directory\n0 2 * * * root sa-learn --spam /var/mail/example.com/username/.Junk --dbpath /var/mail-state/lib-amavis/.spamassassin > /dev/null\n# ham: archive directories\n15 2 * * * root sa-learn --ham /var/mail/example.com/username/.Archive* --dbpath /var/mail-state/lib-amavis/.spamassassin > /dev/null\n# ham: inbox subdirectories\n30 2 * * * root sa-learn --ham /var/mail/example.com/username/cur* --dbpath /var/mail-state/lib-amavis/.spamassassin > /dev/null\n#\n# Everyday 3:00AM, learn spam from all users of a domain\n# spam: junk directory\n0 3 * * * root sa-learn --spam /var/mail/not-example.com/*/.Junk --dbpath /var/mail-state/lib-amavis/.spamassassin > /dev/null\n# ham: archive directories\n15 3 * * * root sa-learn --ham /var/mail/not-example.com/*/.Archive* --dbpath /var/mail-state/lib-amavis/.spamassassin > /dev/null\n# ham: inbox subdirectories\n30 3 * * * root sa-learn --ham /var/mail/not-example.com/*/cur* --dbpath /var/mail-state/lib-amavis/.spamassassin > /dev/null\n
Then with compose.yaml
:
services:\n mailserver:\n image: ghcr.io/docker-mailserver/docker-mailserver:latest\n volumes:\n - ./docker-data/dms/cron/sa-learn:/etc/cron.d/sa-learn\n
Or with Docker Swarm:
services:\n mailserver:\n image: ghcr.io/docker-mailserver/docker-mailserver:latest\n # ...\n configs:\n - source: my_sa_crontab\n target: /etc/cron.d/sa-learn\n\nconfigs:\n my_sa_crontab:\n file: ./docker-data/dms/cron/sa-learn\n
With the default settings, SpamAssassin will require 200 mails trained for spam (for example with the method explained above) and 200 mails trained for ham (using the same command as above but using --ham
and providing it with some ham mails).
This is related to Amavis processing the mail after SpamAssassin has analyzed it and assigned a spam score.
docker-data/dms/config/amavis.cf
.SPAM and INFECTED emails that reach the SA_KILL
threshold are archived into quarantine.
Instead of a quarantine folder, you can use a dedicated mailbox instead. Create an account like quarantine@example.com
and create docker-data/dms/config/amavis.cf
:
$clean_quarantine_to = \"quarantine\\@example.com\";\n$virus_quarantine_to = \"quarantine\\@example.com\";\n$banned_quarantine_to = \"quarantine\\@example.com\";\n$bad_header_quarantine_to = \"quarantine\\@example.com\";\n$spam_quarantine_to = \"quarantine\\@example.com\";\n
"},{"location":"introduction/","title":"An Overview of Mail Server Infrastructure","text":"This article answers the question \"What is a mail server, and how does it perform its duty?\" and it gives the reader an introduction to the field that covers everything you need to know to get started with DMS.
"},{"location":"introduction/#the-anatomy-of-a-mail-server","title":"The Anatomy of a Mail Server","text":"A mail server is only a part of a client-server relationship aimed at exchanging information in the form of emails. Exchanging emails requires using specific means (programs and protocols).
DMS provides you with the server portion, whereas the client can be anything from a terminal via text-based software (eg. Mutt) to a fully-fledged desktop application (eg. Mozilla Thunderbird, Microsoft Outlook\u2026), to a web interface, etc.
Unlike the client-side where usually a single program is used to perform retrieval and viewing of emails, the server-side is composed of many specialized components. The mail server is capable of accepting, forwarding, delivering, storing and overall exchanging messages, but each one of those tasks is actually handled by a specific piece of software. All of these \"agents\" must be integrated with one another for the exchange to take place.
DMS has made informed choices about those components and their (default) configuration. It offers a comprehensive platform to run a fully featured mail server in no time!
"},{"location":"introduction/#components","title":"Components","text":"The following components are required to create a complete delivery chain:
Here's a schematic view of mail delivery:
Sending an email: MUA ----> MTA ----> (MTA relays) ----> MDA\nFetching an email: MUA <--------------------------------- MDA\n
There may be other moving parts or sub-divisions (for instance, at several points along the chain, specialized programs may be analyzing, filtering, bouncing, editing\u2026 the exchanged emails).
In a nutshell, DMS provides you with the following components:
Here's where DMS's toolchain fits within the delivery chain:
docker-mailserver is here:\n \u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513\nSending an email: MUA ---> MTA ---> (MTA relays) ---> \u252b MTA \u256e \u2503\nFetching an email: MUA <------------------------------ \u252b MDA \u256f \u2503\n \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b\n
An Example Let's say Alice owns a Gmail account, alice@gmail.com
; and Bob owns an account on a DMS instance, bob@dms.io
.
Make sure not to conflate these two very different scenarios: A) Alice sends an email to bob@dms.io
=> the email is first submitted to MTA smtp.gmail.com
, then relayed to MTA smtp.dms.io
where it is then delivered into Bob's mailbox. B) Bob sends an email to alice@gmail.com
=> the email is first submitted to MTA smtp.dms.io
, then relayed to MTA smtp.gmail.com
and eventually delivered into Alice's mailbox.
In scenario A the email leaves Gmail's premises, that email's initial submission is not handled by your DMS instance(MTA); it merely receives the email after it has been relayed by Gmail's MTA. In scenario B, the DMS instance(MTA) handles the submission, prior to relaying.
The main takeaway is that when a third-party sends an email to a DMS instance(MTA) (or any MTA for that matter), it does not establish a direct connection with that MTA. Email submission first goes through the sender's MTA, then some relaying between at least two MTAs is required to deliver the email. That will prove very important when it comes to security management.
One important thing to note is that MTA and MDA programs may actually handle multiple tasks (which is the case with DMS's Postfix and Dovecot).
For instance, Postfix is both an SMTP server (accepting emails) and a relaying MTA (transferring, ie. sending emails to other MTA/MDA); Dovecot is both an MDA (delivering emails in mailboxes) and an IMAP server (allowing MUAs to fetch emails from the mail server). On top of that, Postfix may rely on Dovecot's authentication capabilities.
The exact relationship between all the components and their respective (sometimes shared) responsibilities is beyond the scope of this document. Please explore this wiki & the web to get more insights about DMS's toolchain.
"},{"location":"introduction/#about-security-ports","title":"About Security & Ports","text":""},{"location":"introduction/#introduction","title":"Introduction","text":"In the previous section, three components were outlined. Each one of those is responsible for a specific task when it comes to exchanging emails:
Postfix handles Submission (and may handle Relay), whereas Dovecot handles Retrieval. They both need to be accessible by MUAs in order to act as servers, therefore they expose public endpoints on specific TCP ports. Those endpoints may be secured, using an encryption scheme and TLS certificates.
When it comes to the specifics of email exchange, we have to look at protocols and ports enabled to support all the identified purposes. There are several valid options and they've been evolving overtime.
"},{"location":"introduction/#overview","title":"Overview","text":"The following picture gives a visualization of the interplay of all components and their respective ports:
\u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501 Submission \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513\u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501 Transfer/Relay \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513\n\n \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u250c\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2510\nMUA ----- STARTTLS -------> \u2524(587) MTA \u256e (25)\u251c <-- cleartext ---> \u250a Third-party MTA \u250a\n ----- implicit TLS ---> \u2524(465) \u2502 | \u2514\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2518\n ----- cleartext ------> \u2524(25) \u2502 |\n |\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504|\nMUA <---- STARTTLS -------- \u2524(143) MDA \u256f |\n <---- implicit TLS ---- \u2524(993) |\n \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n\n \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501 Retrieval \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b\n
If you're new to email infrastructure, both that table and the schema may be confusing. Read on to expand your understanding and learn about DMS's configuration, including how you can customize it.
"},{"location":"introduction/#submission-smtp","title":"Submission - SMTP","text":"For a MUA to send an email to an MTA, it needs to establish a connection with that server, then push data packets over a network that both the MUA (client) and the MTA (server) are connected to. The server implements the SMTP protocol, which makes it capable of handling Submission.
In the case of DMS, the MTA (SMTP server) is Postfix. The MUA (client) may vary, yet its Submission request is performed as TCP packets sent over the public internet. This exchange of information may be secured in order to counter eavesdropping.
Now let's say I own an account on a DMS instance, me@dms.io
. There are two very different use-cases for Submission:
In the first scenario, I will be submitting my email directly to my DMS instance/MTA (Postfix), which will then relay the email to its recipient's MTA for final delivery. In this case, Submission is first handled by establishing a direct connection to my own MTA-so at least for this portion of the delivery chain, I'll be able to ensure security/confidentiality. Not so much for what comes next, ie. relaying between MTAs and final delivery.
In the second scenario, a third-party email account owner will be first submitting an email to some third-party MTA. I have no control over this initial portion of the delivery chain, nor do I have control over the relaying that comes next. My MTA will merely accept a relayed email coming \"out of the blue\".
My MTA will thus have to support two kinds of Submission:
\u250f\u2501\u2501\u2501 Outbound Submission \u2501\u2501\u2501\u2513\n\n \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u250c\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2510\nMe ---------------> \u2524 \u251c -----------------> \u250a \u250a\n \u2502 My MTA \u2502 \u250a Third-party MTA \u250a\n \u2502 \u251c <----------------- \u250a \u250a\n \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2514\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2518\n\n \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501 Inbound Submission \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b\n
"},{"location":"introduction/#outbound-submission","title":"Outbound Submission","text":"When it comes to securing Outbound Submission you should prefer to use Implicit TLS connection via ESMTP on port 465 (see RFC 8314). Please read our article about Understanding the Ports for more details!
Warning
This Submission setup is sometimes referred to as SMTPS. Long story short: this is incorrect and should be avoided.
Although a very satisfactory setup, Implicit TLS on port 465 is somewhat \"cutting edge\". There exists another well established mail Submission setup that must be supported as well, SMTP+STARTTLS on port 587. It uses Explicit TLS: the client starts with a cleartext connection, then the server informs a TLS-encrypted \"upgraded\" connection may be established, and the client may eventually decide to establish it prior to the Submission. Basically it's an opportunistic, opt-in TLS upgrade of the connection between the client and the server, at the client's discretion, using a mechanism known as STARTTLS that both ends need to implement.
In many implementations, the mail server doesn't enforce TLS encryption, for backwards compatibility. Clients are thus free to deny the TLS-upgrade proposal (or misled by a hacker about STARTTLS not being available), and the server accepts unencrypted (cleartext) mail exchange, which poses a confidentiality threat and, to some extent, spam issues. RFC 8314 (section 3.3) recommends for a mail server to support both Implicit and Explicit TLS for Submission, and to enforce TLS-encryption on ports 587 (Explicit TLS) and 465 (Implicit TLS). That's exactly DMS's default configuration: abiding by RFC 8314, it enforces a strict (encrypt
) STARTTLS policy, where a denied TLS upgrade terminates the connection thus (hopefully but at the client's discretion) preventing unencrypted (cleartext) Submission.
A final Outbound Submission setup exists and is akin SMTP+STARTTLS on port 587, but on port 25. That port has historically been reserved specifically for unencrypted (cleartext) mail exchange though, making STARTTLS a bit wrong to use. As is expected by RFC 5321, DMS uses port 25 for unencrypted Submission in order to support older clients, but most importantly for unencrypted Transfer/Relay between MTAs.
Granted it's still very difficult enforcing encryption between MTAs (Transfer/Relay) without risking dropping emails (when relayed by MTAs not supporting TLS-encryption), Inbound Submission is to be handled in cleartext on port 25 by default.
Overall, DMS's default configuration for SMTP looks like this:
\u250f\u2501\u2501\u2501 Outbound Submission \u2501\u2501\u2501\u2513\n\n \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u250c\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2510\nMe -- cleartext --> \u2524(25) (25)\u251c --- cleartext ---> \u250a \u250a\nMe -- TLS ---> \u2524(465) My MTA \u2502 \u250a Third-party MTA \u250a\nMe -- STARTTLS ---> \u2524(587) \u2502 \u250a \u250a\n \u2502 (25)\u251c <---cleartext ---- \u250a \u250a\n \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2514\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2518\n\n \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501 Inbound Submission \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b\n
"},{"location":"introduction/#retrieval-imap","title":"Retrieval - IMAP","text":"A MUA willing to fetch an email from a mail server will most likely communicate with its IMAP server. As with SMTP described earlier, communication will take place in the form of data packets exchanged over a network that both the client and the server are connected to. The IMAP protocol makes the server capable of handling Retrieval.
In the case of DMS, the IMAP server is Dovecot. The MUA (client) may vary, yet its Retrieval request is performed as TCP packets sent over the public internet. This exchange of information may be secured in order to counter eavesdropping.
Again, as with SMTP described earlier, the IMAP protocol may be secured with either Implicit TLS (aka. IMAPS / IMAP4S) or Explicit TLS (using STARTTLS).
The best practice as of 2020 is to enforce IMAPS on port 993, rather than IMAP+STARTTLS on port 143 (see RFC 8314); yet the latter is usually provided for backwards compatibility.
DMS's default configuration enables both Implicit and Explicit TLS for Retrievial, on ports 993 and 143 respectively.
"},{"location":"introduction/#retrieval-pop3","title":"Retrieval - POP3","text":"Similarly to IMAP, the older POP3 protocol may be secured with either Implicit or Explicit TLS.
The best practice as of 2020 would be POP3S on port 995, rather than POP3+STARTTLS on port 110 (see RFC 8314).
DMS's default configuration disables POP3 altogether. One should expect MUAs to use TLS-encrypted IMAP for Retrieval.
"},{"location":"introduction/#how-does-dms-help-with-setting-everything-up","title":"How Does DMS Help With Setting Everything Up?","text":"As a batteries included container image, DMS provides you with all the required components and a default configuration to run a decent and secure mail server. One may then customize all aspects of its internal components.
Eventually, it is up to you deciding exactly what kind of transportation/encryption to use and/or enforce, and to customize your instance accordingly (with looser or stricter security). Be also aware that protocols and ports on your server can only go so far with security; third-party MTAs might relay your emails on insecure connections, man-in-the-middle attacks might still prove effective, etc. Advanced counter-measure such as DANE, MTA-STS and/or full body encryption (eg. PGP) should be considered as well for increased confidentiality, but ideally without compromising backwards compatibility so as to not block emails.
"},{"location":"usage/","title":"Usage","text":"This page explains how to get started with DMS. The guide uses Docker Compose as a reference. In our examples, a volume mounts the host location docker-data/dms/config/
to /tmp/docker-mailserver/
inside the container.
Before you can get started with deploying your own mail server, there are some requirements to be met:
There are a few requirements for a suitable host system:
PTR
record for your host; security-hardened mail servers might otherwise reject your mail server as the IP address of your host does not resolve correctly/at all to the DNS name of your server.About the Container Runtime
On the host, you need to have a suitable container runtime (like Docker or Podman) installed. We assume Docker Compose is installed. We have aligned file names and configuration conventions with the latest Docker Compose (currently V2) specification.
If you're using podman, make sure to read the related documentation.
"},{"location":"usage/#minimal-dns-setup","title":"Minimal DNS Setup","text":"The DNS setup is a big and essential part of the whole setup. There is a lot of confusion for newcomers and people starting out when setting up DNS. This section provides an example configuration and supplementary explanation. We expect you to be at least a bit familiar with DNS, what it does and what the individual record types are.
Now let's say you just bought example.com
and you want to be able to send and receive e-mails for the address test@example.com
. On the most basic level, you will need to
MX
record for your domain example.com
- in our example, the MX record contains mail.example.com
A
record that resolves the name of your mail server - in our example, the A record contains 11.22.33.44
PTR
record that resolves the IP of your mail server - in our example, the PTR contains mail.example.com
We will later dig into DKIM, DMARC & SPF, but for now, these are the records that suffice in getting you up and running. Here is a short explanation of what the records do:
mail.example.com
. This does not imply your e-mails will look like test@mail.example.com
, the DNS name of your mail server is decoupled of the domain it serves e-mails for. Your mail server could also handle emails for test@some-other-domain.com
, if the MX record for some-other-domain.com
points to mail.example.com
.mail.example.com
resolves to.11.22.33.44
resolves to.About The Mail Server's Fully Qualified Domain Name
The mail server's fully qualified domain name (FQDN) in our example above is mail.example.com
. Please note though that this is more of a convention, and not due to technical restrictions. One could also run the mail server
foo.example.com
: you would just need to change your MX
record;example.com
directly: you would need to change your MX
record and probably read our docs on bare domain setups, as these setups are called \"bare domain\" setups.The FQDN is what is relevant for TLS certificates, it has no (inherent/technical) relation to the email addresses and accounts DMS manages. That is to say: even though DMS runs on mail.example.com
, or foo.example.com
, or example.com
, there is nothing that prevents it from managing mail for barbaz.org
- barbaz.org
will just need to set its MX
record to mail.example.com
(or foo.example.com
or example.com
).
If you setup everything, it should roughly look like this:
$ dig @1.1.1.1 +short MX example.com\nmail.example.com\n$ dig @1.1.1.1 +short A mail.example.com\n11.22.33.44\n$ dig @1.1.1.1 +short -x 11.22.33.44\nmail.example.com\n
"},{"location":"usage/#deploying-the-actual-image","title":"Deploying the Actual Image","text":""},{"location":"usage/#tagging-convention","title":"Tagging Convention","text":"To understand which tags you should use, read this section carefully. Our CI will automatically build, test and push new images to the following container registries:
docker.io/mailserver/docker-mailserver
)ghcr.io/docker-mailserver/docker-mailserver
)All workflows are using the tagging convention listed below. It is subsequently applied to all images.
Event Image Tagspush
on master
edge
push
a tag (v1.2.3
) 1.2.3
, 1.2
, 1
, latest
"},{"location":"usage/#get-all-files","title":"Get All Files","text":"Issue the following commands to acquire the necessary files:
DMS_GITHUB_URL=\"https://raw.githubusercontent.com/docker-mailserver/docker-mailserver/master\"\nwget \"${DMS_GITHUB_URL}/compose.yaml\"\nwget \"${DMS_GITHUB_URL}/mailserver.env\"\n
"},{"location":"usage/#configuration-steps","title":"Configuration Steps","text":"compose.yaml
to your likingmail.example.com
according to your FQDN../docker-data/dms/config/:/tmp/docker-mailserver/
mount, append -z
or -Z
.mailserver.env
, but keep in mind that:VAR=VAL
is supportedOVERRIDE_HOSTNAME=$HOSTNAME.$DOMAINNAME
does not workUsing the Correct Commands For Stopping and Starting DMS
Use docker compose up / down
, not docker compose start / stop
. Otherwise, the container is not properly destroyed and you may experience problems during startup because of inconsistent state.
Using Ctrl+C
is not supported either!
For an overview of commands to manage DMS config, run: docker exec -it <CONTAINER NAME> setup help
.
setup.sh
when no DMS Container Is Running We encourage you to directly use setup
inside the container (like shown above). If you still want to use setup.sh
, here's some information about it.
If no DMS container is running, any ./setup.sh
command will check online for the :latest
image tag (the current stable release), performing a docker pull ...
if necessary followed by running the command in a temporary container:
$ ./setup.sh help\nImage 'ghcr.io/docker-mailserver/docker-mailserver:latest' not found. Pulling ...\nSETUP(1)\n\nNAME\n setup - 'docker-mailserver' Administration & Configuration script\n...\n\n$ docker run --rm ghcr.io/docker-mailserver/docker-mailserver:latest setup help\nSETUP(1)\n\nNAME\n setup - 'docker-mailserver' Administration & Configuration script\n...\n
On first start, you will need to add at least one email account (unless you're using LDAP). You have two minutes to do so, otherwise DMS will shutdown and restart. You can add accounts by running docker exec -ti <CONTAINER NAME> setup email add user@example.com
. That's it! It really is that easy.
You definitely want to setup TLS. Please refer to our documentation about TLS.
"},{"location":"usage/#aliases","title":"Aliases","text":"You should add at least one alias, the postmaster alias. This is a common convention, but not strictly required.
docker exec -ti <CONTAINER NAME> setup alias add postmaster@example.com user@example.com\n
"},{"location":"usage/#advanced-dns-setup-dkim-dmarc-spf","title":"Advanced DNS Setup - DKIM, DMARC & SPF","text":"You will very likely want to configure your DNS with these TXT records: SPF, DKIM, and DMARC. We also ship a dedicated page in our documentation about the setup of DKIM, DMARC & SPF.
"},{"location":"usage/#custom-user-changes-patches","title":"Custom User Changes & Patches","text":"If you'd like to change, patch or alter files or behavior of DMS, you can use a script. See this part of our documentation for a detailed explanation.
"},{"location":"usage/#testing","title":"Testing","text":"Here are some tools you can use to verify your configuration:
This page contains valuable information when it comes to resolving issues you encounter.
Contributions Welcome!
Please consider contributing solutions to the FAQ
"},{"location":"config/debugging/#preliminary-checks","title":"Preliminary Checks","text":"Correctly starting DMS
Use the --force-recreate
option to avoid configuration mishaps: docker compose up --force-recreate
Alternatively, always use docker compose down
to stop DMS. Do not rely on CTRL + C
, docker compose stop
, or docker compose restart
.
DMS setup scripts are run when a container starts, but may fail to work properly if you do the following:
docker stop
or docker compose up
stopped via CTRL + C
instead of docker compose down
.Volumes persist data across container instances, however the same container instance will keep internal changes not stored in a volume until the container is removed.
Due to this, DMS setup scripts may modify configuration it has already modified in the past.
compose.yaml
are expected to persist any important data. Thus it should be safe to throwaway the container created each time, avoiding this config problem.Some service providers block outbound traffic on port 25. Common hosting providers known to have this issue:
These links may advise how the provider can unblock the port through additional services offered, or via a support ticket request.
"},{"location":"config/debugging/#mail-sent-to-dms-does-not-get-delivered-to-user","title":"Mail sent to DMS does not get delivered to user","text":"Common logs related to this are:
warning: do not list domain domain.fr in BOTH mydestination and virtual_mailbox_domains
Recipient address rejected: User unknown in local recipient table
If your logs look like this, you likely have assigned the same FQDN to the DMS hostname
and your mail accounts which is not supported by default. You can either adjust your DMS hostname
or follow this FAQ advice
It is also possible that DMS services are temporarily unavailable when configuration changes are detected, producing the 2nd error. Certificate updates may be a less obvious trigger.
"},{"location":"config/debugging/#steps-for-debugging-dms","title":"Steps for Debugging DMS","text":"LOG_LEVEL
to debug
or trace
.docker log <CONTAINER NAME>
(or docker logs -f <CONTAINER NAME>
if you want to follow the log).To get a shell inside the container run: docker exec -it <CONTAINER NAME> bash
. To install additional software, run:
apt-get update
to update repository metadata.apt-get install <PACKAGE>
to install a package, e.g., apt-get install neovim
if you want to use NeoVim instead of nano
(which is shipped by default).If you need more flexibility than what the docker logs
command offers, then the most useful locations to get relevant DMS logs within the container are:
/var/log/mail/<SERVICE>.log
/var/log/supervisor/<SERVICE>.log
You may use nano
(a text editor) to edit files, while less
(a file viewer) and tail
/cat
are useful tools to inspect the contents of logs.
It's possible that the issue you're experiencing is due to a compatibility conflict.
This could be from outdated software, or running a system that isn't able to provide you newer software and kernels. You may want to verify if you can reproduce the issue on a system that is not affected by these concerns.
"},{"location":"config/debugging/#network","title":"Network","text":"userland-proxy
: Prior to Docker v23
, changing the userland-proxy
setting did not reliably remove NAT rules.iptables
/ nftables
:iptables
, relying on compatibility shims for supporting the successor nftables
. Internally DMS expects nftables
support on the host kernel for services like Fail2Ban to function correctly.nftables
. Other software outside of DMS may also manipulate these rules, such as firewall frontends.v23
(2023Q1).v20.10.0
(2020Q4).volumes
config in compose.yaml
. You may have luck trying gRPC FUSE
as the file sharing implementation; VirtioFS
is the successor but presently appears incompatible with DMS.unified
hierarchy. Not meeting this baseline may influence the behavior of your DMS container, even with the latest Docker Engine installed.Info
Values in bold are the default values. If an option doesn't work as documented here, check if you are running the latest image. The current master
branch corresponds to the image ghcr.io/docker-mailserver/docker-mailserver:edge
.
If you can't set your hostname (eg: you're in a container platform that doesn't let you) specify it via this environment variable. It will have priority over docker run --hostname
, or the equivalent hostname:
field in compose.yaml
.
hostname -f
command to get canonical hostname for DMS to use.Set the log level for DMS. This is mostly relevant for container startup scripts and change detection event feedback.
Valid values (in order of increasing verbosity) are: error
, warn
, info
, debug
and trace
. The default log level is info
.
Here you can adjust the log-level for Supervisor. Possible values are
The log-level will show everything in its class and above.
"},{"location":"config/environment/#dms_vmail_uid","title":"DMS_VMAIL_UID","text":"Default: 5000
The User ID assigned to the static vmail user for /var/mail
(Mail storage managed by Dovecot).
Incompatible UID values
0
(root) is not compatible.uid
for the docker
user (/etc/passwd
), hence the error emitted to logs if the UID is already assigned to another user./etc/passwd
, even though Dovecot by default has a setting for the minimum UID as 500
.Default: 5000
The Group ID assigned to the static vmail group for /var/mail
(Mail storage managed by Dovecot).
Configures the provisioning source of user accounts (including aliases) for user queries and authentication by services managed by DMS (Postfix and Dovecot).
LDAP requires an external service (e.g. bitnami/openldap
).
Set different options for mynetworks option (can be overwrite in postfix-main.cf) WARNING: Adding the docker network's gateway to the list of trusted hosts, e.g. using the network
or connected-networks
option, can create an open relay, for instance if IPv6 is enabled on the host machine but not in Docker.
docker-compose
might use others (e.g. 192.168.0.0/16) use PERMIT_DOCKER=connected-networks
in this case.Note: you probably want to set POSTFIX_INET_PROTOCOLS=ipv4
to make it work fine with Docker.
Set the timezone. If this variable is unset, the container runtime will try to detect the time using /etc/localtime
, which you can alternatively mount into the container. The value of this variable must follow the pattern AREA/ZONE
, i.e. of you want to use Germany's time zone, use Europe/Berlin
. You can lookup all available timezones here.
Amavis content filter (used for ClamAV & SpamAssassin)
This page provides information on Amavis' logging statistics.
This enables DNS block lists in Postscreen. If you want to know which lists we are using, have a look at the default main.cf
for Postfix we provide and search for postscreen_dnsbl_sites
.
A Warning On DNS Block Lists
Make sure your DNS queries are properly resolved, i.e. you will most likely not want to use a public DNS resolver as these queries do not return meaningful results. We try our best to only evaluate proper return codes - this is not a guarantee that all codes are handled fine though.
Note that emails will be rejected if they don't pass the block list checks!
Enables MTA-STS support for outbound mail.
See MTA-STS for further explanation.
"},{"location":"config/environment/#enable_opendkim","title":"ENABLE_OPENDKIM","text":"Enables the OpenDKIM service.
Enables the OpenDMARC service.
Enabled policyd-spf
in Postfix's configuration. You will likely want to set this to 0
in case you're using Rspamd (ENABLE_RSPAMD=1
).
If you enable Fail2Ban, don't forget to add the following lines to your compose.yaml
:
cap_add:\n - NET_ADMIN\n
Otherwise, nftables
won't be able to ban IPs.
In the majority of cases, you want letsencrypt
or manual
.
self-signed
can be used for testing SSL until you provide a valid certificate, note that third-parties cannot trust self-signed
certificates, do not use this type in production. custom
is a temporary workaround that is not officially supported.
SSL_CERT_PATH
and SSL_KEY_PATH
ENV vars to be set to the location of the files within the container.SSL_ALT_CERT_PATH
and SSL_ALT_KEY_PATH
allow providing a 2nd certificate as a fallback for dual (aka hybrid) certificate support. Useful for ECDSA with an RSA fallback. Presently only manual
mode supports this feature.None
)Please read the SSL page in the documentation for more information.
"},{"location":"config/environment/#tls_level","title":"TLS_LEVEL","text":"Configures the handling of creating mails with forged sender addresses.
Enables the Sender Rewriting Scheme. SRS is needed if DMS acts as forwarder. See postsrsd for further explanation.
In case your network interface differs from eth0
, e.g. when you are using HostNetworking in Kubernetes, you can set this to whatever interface you want. This interface will then be used.
eth0
Set how many days a virusmail will stay on the server before being deleted
Configure Postfix virtual_transport
to deliver mail to a different LMTP client (default is a unix socket to dovecot).
Provide any valid URI. Examples:
lmtp:unix:/var/run/dovecot/lmtp
(default, configured in Postfix main.cf
)lmtp:unix:private/dovecot-lmtp
(use socket)lmtps:inet:<host>:<port>
(secure lmtp with starttls)lmtp:<kopano-host>:2003
(use kopano as mailstore)Set the mailbox size limit for all users. If set to zero, the size will be unlimited (default). Size is in bytes.
See mailbox quota.
"},{"location":"config/environment/#postfix_message_size_limit","title":"POSTFIX_MESSAGE_SIZE_LIMIT","text":"Set the message size limit for all users. If set to zero, the size will be unlimited (not recommended!). Size is in bytes.
Mails larger than this limit won't be scanned. ClamAV must be enabled (ENABLE_CLAMAV=1) for this.
Check for updates on container start and then once a day. If an update is available, a mail is send to POSTMASTER_ADDRESS.
Customize the update check interval. Number + Suffix. Suffix must be 's' for seconds, 'm' for minutes, 'h' for hours or 'd' for days.
This option has been added in November 2019. Using other format than Maildir is considered as experimental in docker-mailserver and should only be used for testing purpose. For more details, please refer to Dovecot Documentation.
"},{"location":"config/environment/#postfix_reject_unknown_client_hostname","title":"POSTFIX_REJECT_UNKNOWN_CLIENT_HOSTNAME","text":"If enabled, employs reject_unknown_client_hostname
to sender restrictions in Postfix's configuration.
Note: More details at http://www.postfix.org/postconf.5.html#inet_protocols
"},{"location":"config/environment/#dovecot_inet_protocols","title":"DOVECOT_INET_PROTOCOLS","text":"Note: More information at https://dovecot.org/doc/dovecot-example.conf
"},{"location":"config/environment/#move_spam_to_junk","title":"MOVE_SPAM_TO_JUNK","text":"Routes mail identified as spam into the recipient(s) Junk mailbox (a specialized folder for junk associated to the special-use flag \\Junk
, handled via a Dovecot sieve script internally).
Info
Mail is received as spam when it has been marked with either header:
X-Spam: Yes
(added by Rspamd)X-Spam-Flag: YES
(added by SpamAssassin - requires SPAMASSASSIN_SPAM_TO_INBOX=1
)Enable to treat received spam as \"read\" (avoids notification to MUA client of new mail).
Info
Mail is received as spam when it has been marked with either header:
X-Spam: Yes
(added by Rspamd)X-Spam-Flag: YES
(added by SpamAssassin - requires SPAMASSASSIN_SPAM_TO_INBOX=1
)This variable defines a prefix for e-mails tagged with the X-Spam: Yes
(Rspamd) or X-Spam-Flag: YES
(SpamAssassin/Amavis) header.
Default: empty (no prefix will be added to e-mails)
Including trailing white-spaceAdd trailing white-space by quote wrapping the value: SPAM_SUBJECT='[SPAM] '
Enable or disable Rspamd.
Explicit control over running a Redis instance within the container. By default, this value will match what is set for ENABLE_RSPAMD
.
The purpose of this setting is to opt-out of starting an internal Redis instance when enabling Rspamd, replacing it with your own external instance.
Configuring Rspamd for an external Redis instanceYou will need to provide configuration at /etc/rspamd/local.d/redis.conf
similar to:
servers = \"redis.example.test:6379\";\nexpand_keys = true;\n
This settings controls whether checks should be performed on emails coming from authenticated users (i.e. most likely outgoing emails). The default value is 0
in order to align better with SpamAssassin. We recommend reading through the Rspamd documentation on scanning outbound emails though to decide for yourself whether you need and want this feature.
Not all checks and actions are disabled
DKIM signing of e-mails will still happen.
Controls whether the Rspamd Greylisting module is enabled. This module can further assist in avoiding spam emails by greylisting e-mails with a certain spam score.
When enabled,
Junk
folder (learning this email as spam);Junk
folder into the INBOX
(learning this email as ham).Attention
As of now, the spam learning database is global (i.e. available to all users). If one user deliberately trains it with malicious data, then it will ruin your detection rate.
This feature is suitably only for users who can tell ham from spam and users that can be trusted.
Can be used to enable or disable the Hfilter group module. This is used by DMS to adjust the HFILTER_HOSTNAME_UNKNOWN
symbol, increasing its default weight to act similar to Postfix's reject_unknown_client_hostname
, without the need to outright reject a message.
Can be used to control the score when the HFILTER_HOSTNAME_UNKNOWN
symbol applies. A higher score is more punishing. Setting it to 15 (the default score for rejecting an e-mail) is equivalent to rejecting the email when the check fails.
Default: 6 (which corresponds to the add_header
action)
Can be used to enable or disable the Neural network module. This is an experimental anti-spam weigh method using three neural networks in the configuration added here. As far as we can tell it trains itself by using other modules to find out what spam is. It will take a while (a week or more) to train its first neural network. The config trains new networks all the time and discards old networks. Since it is experimental, it is switched off by default.
Enables regular Postfix log summary (\"pflogsumm\") mail reports.
This is a new option. The old REPORT options are still supported for backwards compatibility. If this is not set and reports are enabled with the old options, logrotate will be used.
"},{"location":"config/environment/#pflogsumm_recipient","title":"PFLOGSUMM_RECIPIENT","text":"Recipient address for Postfix log summary reports.
Sender address (FROM
) for pflogsumm reports (if Postfix log summary reports are enabled).
Interval for logwatch report.
Recipient address for logwatch reports if they are enabled.
Sender address (FROM
) for logwatch reports if logwatch reports are enabled.
Defines who receives reports (if they are enabled).
Defines who sends reports (if they are enabled).
mailserver-report@<YOUR DOMAIN>
Changes the interval in which log files are rotated.
Note
LOGROTATE_INTERVAL
only manages logrotate
within the container for services we manage internally.
The entire log output for the container is still available via docker logs mailserver
(or your respective container name). If you want to configure external log rotation for that container output as well, : Docker Logging Drivers.
By default, the logs are lost when the container is destroyed (eg: re-creating via docker compose down && docker compose up -d
). To keep the logs, mount a volume (to /var/log/mail/
).
Note
This variable can also determine the interval for Postfix's log summary reports, see PFLOGSUMM_TRIGGER
.
Defines how many files are kept by logrotate.
Integration with Amavis involves processing mail based on the assigned spam score via SA_TAG
, SA_TAG2
and SA_KILL
.
These settings have equivalent ENV supported by DMS for easy adjustments, as documented below.
"},{"location":"config/environment/#enable_spamassassin_kam","title":"ENABLE_SPAMASSASSIN_KAM","text":"KAM is a 3rd party SpamAssassin ruleset, provided by the McGrail Foundation. If SpamAssassin is enabled, KAM can be used in addition to the default ruleset.
"},{"location":"config/environment/#spamassassin_spam_to_inbox","title":"SPAMASSASSIN_SPAM_TO_INBOX","text":"D_BOUNCE
): Spam messages will be bounced (rejected) without any notification (dangerous).D_PASS
): Spam messages will be delivered to the inbox.Note
The Amavis action configured by this setting:
SA_KILL
setting.$final_spam_destiny
and $final_bad_header_destiny
.This ENV setting is related to
MOVE_SPAM_TO_JUNK=1
MARK_SPAM_AS_READ=1
SPAM_SUBJECT
Mail is not yet considered spam at this spam score, but for purposes like diagnostics it can be useful to identify mail with a spam score at a lower bound than SA_TAG2
.
X-Spam
headers appended to mail Send a simple mail to a local DMS account hello@example.com
:
docker exec dms swaks --server 0.0.0.0 --to hello@example.com --body 'spam'\n
Inspecting the raw mail you will notice several X-Spam
headers were added to the mail like this:
X-Spam-Flag: NO\nX-Spam-Score: 4.162\nX-Spam-Level: ****\nX-Spam-Status: No, score=4.162 tagged_above=2 required=4\n tests=[BODY_SINGLE_WORD=1, DKIM_ADSP_NXDOMAIN=0.8,\n NO_DNS_FOR_FROM=0.379, NO_RECEIVED=-0.001, NO_RELAYS=-0.001,\n PYZOR_CHECK=1.985] autolearn=no autolearn_force=no\n
The X-Spam-Score
is 4.162
High enough for SA_TAG
to trigger adding these headers, but not high enough for SA_TAG2
(which would set X-Spam-Flag: YES
instead).
When a spam score is high enough, mark mail as spam (Appends the mail header: X-Spam-Flag: YES
).
Interaction with other ENV
SPAM_SUBJECT
modifies the mail subject to better communicate spam mail to the user.MOVE_SPAM_TO_JUNK=1
: The mail is still delivered, but to the recipient(s) junk folder instead. This feature reduces the usefulness of SPAM_SUBJECT
.Controls the spam score threshold for triggering an action on mail that has a high spam score.
Choosing an appropriateSA_KILL
value The value should be high enough to be represent confidence in mail as spam:
SA_KILL
trigger (how to treat mail with high confidence that it is actually spam).Experiences from DMS users with these settings has been collected here, along with some direct configuration guides (under \"Resources for references\").
Trigger actionDMS will configure Amavis with either of these actions based on the DMS SPAMASSASSIN_SPAM_TO_INBOX
ENV setting:
D_PASS
(default):SA_KILL
threshold that won't accidentally discard / bounce legitimate mail users are expecting to arrive but is detected as spam.D_BOUNCE
:$sa_dsn_cutoff_level
config setting (default: 10
). With the DMS SA_KILL
default also being 10
, no DSN will ever be sent.D_REJECT
/ D_DISCARD
:When mail has a spam score that reaches the SA_KILL
threshold:
SA_KILL
action to perform.D_PASS
the delivered mail also appends an X-Quarantine-ID
mail header. The ID value of this header is part of the quarantined file name.If emails are quarantined, they are compressed and stored at a location:
/var/lib/amavis/virusmails/
/var/mail-state/
volume is present: /var/mail-state/lib-amavis/virusmails/
Tip
Easily list mail stored in quarantine with find
and the quarantine path:
find /var/lib/amavis/virusmails -type f\n
"},{"location":"config/environment/#sa_shortcircuit_bayes_spam","title":"SA_SHORTCIRCUIT_BAYES_SPAM","text":"This will uncomment the respective line in /etc/spamassasin/local.cf
Warning
Activate this only if you are confident in your bayes database for identifying spam.
"},{"location":"config/environment/#sa_shortcircuit_bayes_ham","title":"SA_SHORTCIRCUIT_BAYES_HAM","text":"This will uncomment the respective line in /etc/spamassasin/local.cf
Warning
Activate this only if you are confident in your bayes database for identifying ham.
"},{"location":"config/environment/#fetchmail","title":"Fetchmail","text":""},{"location":"config/environment/#enable_fetchmail","title":"ENABLE_FETCHMAIL","text":"fetchmail
disabledfetchmail
enabledfetchmail
The number of seconds for the intervalfetchmail
runs with a single config file /etc/fetchmailrc
/etc/fetchmailrc
is split per poll entry. For every poll entry a separate fetchmail instance is started to allow having multiple imap idle connections per server (when poll entries reference the same IMAP server).Note: The defaults of your fetchmailrc file need to be at the top of the file. Otherwise it won't be added correctly to all separate fetchmail
instances.
Enable or disable getmail
.
getmail
The number of minutes for the interval. Min: 1; Default: 5.https://oauth2.example.com/userinfo/
)<dns-name>
/ <ip-address>
where the LDAP server is reachable via a URI like: ldaps://mail.example.com
.ldap://
, ldaps://
, ldapi://
).(&(mail=%s)(mailEnabled=TRUE))
(&(mailGroupMember=%s)(mailEnabled=TRUE))
(&(mailAlias=%s)(mailEnabled=TRUE))
(&(|(mail=*@%s)(mailalias=*@%s)(mailGroupMember=*@%s))(mailEnabled=TRUE))
(|($LDAP_QUERY_FILTER_USER)($LDAP_QUERY_FILTER_ALIAS)($LDAP_QUERY_FILTER_GROUP))
The following variables overwrite the default values for /etc/dovecot/dovecot-ldap.conf.ext
.
LDAP_SEARCH_BASE
ou=people,dc=domain,dc=com
)SSHA
LDAP_BIND_DN
cn=admin,dc=domain,dc=com
)LDAP_BIND_PW
DOVECOT_DN
.LDAP_SERVER_HOST
ldap://
, ldaps://
, ldapi://
).(&(objectClass=PostfixBookMailAccount)(uniqueIdentifier=%n))
homeDirectory=home,qmailUID=uid,qmailGID=gid,mailMessageStore=mail
(&(objectClass=PostfixBookMailAccount)(uniqueIdentifier=%n))
DOVECOT_USER_FILTER
uid=user,userPassword=password
postgrey
is disabledpostgrey
is enabledNote: This postgrey setting needs ENABLE_POSTGREY=1
Note: This postgrey setting needs ENABLE_POSTGREY=1
Note: This postgrey setting needs ENABLE_POSTGREY=1
Note: This postgrey setting needs ENABLE_POSTGREY=1
saslauthd
is disabledsaslauthd
is enabledDMS only implements support for these mechanisms:
ldap
=> Authenticate against an LDAP serverrimap
=> Authenticate against an IMAP serverInfo
With SASLAUTHD_MECHANISMS=rimap
you need to specify the ip-address / servername of the IMAP server, such as SASLAUTHD_MECH_OPTIONS=127.0.0.1
.
LDAP_SERVER_HOST
Note
You must include the desired URI scheme (ldap://
, ldaps://
, ldapi://
).
no
yes
=> Enable ldap_start_tls
optionno
yes
=> Enable ldap_tls_check_peer
optionPath to directory with CA (Certificate Authority) certificates.
ldap_tls_cacert_dir
optionFile containing CA (Certificate Authority) certificate(s).
ldap_tls_cacert_file
optionLDAP_BIND_DN
LDAP_BIND_PW
LDAP_SEARCH_BASE
(&(uniqueIdentifier=%u)(mailEnabled=TRUE))
(&(sAMAccountName=%U)(objectClass=person))
(&(uid=%U)(objectClass=person))
Specify what password attribute to use for password verification.
userPassword
by default.ldap_password_attr
optionbind
will be used as a default valuefastbind
=> The fastbind method is usedcustom
=> The custom method uses userPassword attribute to verify the passwordSpecify the authentication mechanism for SASL bind.
ldap_mech
optionAn email has an \"envelope\" sender (indicating the sending server) and a \"header\" sender (indicating who sent it). More strict SPF policies may require you to replace both instead of just the envelope sender.
More info.
dd if=/dev/urandom bs=24 count=1 2>/dev/null | base64
OVERRIDE_HOSTNAME
, $DOMAINNAME
(internal), or the container's hostnameSupported ENV for the Relay Host feature.
Prefer DEFAULT_RELAY_HOST
instead of RELAY_HOST
This is advised unless you need support for sender domain opt-out (via setup relay exclude-domain
).
The implementation for RELAY_HOST
is not compatible with LDAP.
Opt-in for relay host support
Enable relaying only for specific sender domains instead by using setup relay add-domain
.
NOTE: Presently there is a caveat when relay host credentials are configured (which is incompatible with opt-in).
"},{"location":"config/environment/#default_relay_host","title":"DEFAULT_RELAY_HOST","text":"Configures a default relay host.
Info
[mail.example.com]:587
vs example.com:587
Technical Details
This ENV internally configures the Postfix main.cf
setting: relayhost
Configures a default relay host.
Note
Expects a value like mail.example.com
. Internally this will be wrapped to [mail.example.com]
, so it should resolve to the MTA directly.
Do not use with DEFAULT_RELAY_HOST
RELAY_HOST
has precedence as it is configured with sender_dependent_relayhost_maps
.
Info
postfix-relaymap.cf
to work.DEFAULT_RELAY_HOST
.Technical Details
This feature is configured internally using the:
sender_dependent_relayhost_maps = texthash:/etc/postfix/relayhost_map
postfix-relaymap.cf
(generates /etc/postfix/relayhost_map
)All known mail domains managed by DMS will be configured to relay outbound mail to RELAY_HOST
by adding them implicitly to /etc/postfix/relayhost_map
, except for domains using the opt-out feature of postfix-relaymap.cf
.
Default => 25
Support for configuring a different port than 25 for RELAY_HOST
to use.
Note
Requires RELAY_HOST
.
Configuring relay host credentials enforces outbound authentication
Presently when RELAY_USER
+ RELAY_PASSWORD
or postfix-sasl-password.cf
are configured, all outbound mail traffic is configured to require a secure connection established and forbids the omission of credentials.
Additional feature work is required to only enforce these requirements on mail sent through a configured relay host.
"},{"location":"config/environment/#relay_user","title":"RELAY_USER","text":""},{"location":"config/environment/#relay_password","title":"RELAY_PASSWORD","text":"Provide the credentials to use with RELAY_HOST
or DEFAULT_RELAY_HOST
.
Alternative credentials config
You may prefer to use setup relay add-auth
to avoid risking ENV exposing secrets.
postfix-sasl-password.cf
with the correct relayhost entry (DEFAULT_RELAY_HOST
value, or as defined in /etc/postfix/relayhost_map
) to provide credentials per relayhost configured.Technical Details
Credentials for relay hosts are configured internally using the:
smtp_sasl_password_maps = texthash:/etc/postfix/sasl_passwd
postfix-sasl-password.cf
(generates /etc/postfix/sasl_passwd
)When postfix-sasl-password.cf
is present, DMS will copy it internally to /etc/postfix/sasl_passwd
.
setup relay add-auth
(creates / updates postfix-sasl-password.cf
).host:port
/ [host]:port
) from the generated /etc/postfix/relayhost_map
, or main.cf:relayhost
(DEFAULT_RELAY_HOST
).setup relay ...
is missing support, you must instead add these manually to postfix-sasl-password.cf
.If you want to use POP3(S), you have to add the ports 110 and/or 995 (TLS secured) and the environment variable ENABLE_POP3
to your compose.yaml
:
mailserver:\n ports:\n - \"25:25\" # SMTP (explicit TLS => STARTTLS)\n - \"143:143\" # IMAP4 (explicit TLS => STARTTLS)\n - \"465:465\" # ESMTP (implicit TLS)\n - \"587:587\" # ESMTP (explicit TLS => STARTTLS)\n - \"993:993\" # IMAP4 (implicit TLS)\n - \"110:110\" # POP3\n - \"995:995\" # POP3 (with TLS)\n environment:\n - ENABLE_POP3=1\n
"},{"location":"config/setup.sh/","title":"About setup.sh","text":"Note
setup.sh
is not required. We encourage you to use docker exec -ti <CONTAINER NAME> setup
instead.
Warning
This script assumes Docker or Podman is used. You will not be able to use setup.sh
with other container orchestration tools.
setup.sh
is a script that is complimentary to the internal setup
command in DMS.
It mostly provides the convenience of aliasing docker exec -ti <CONTAINER NAME> setup
, inferring the container name of a running DMS instance or running a new instance and bind mounting necessary volumes implicitly.
It is intended to be run from the host machine, not from inside your running container. The latest version of the script is included in the DMS repository. You may retrieve it at any time by running this command in your console:
wget https://raw.githubusercontent.com/docker-mailserver/docker-mailserver/master/setup.sh\nchmod a+x ./setup.sh\n
For more information on using the script run: ./setup.sh help
.
This page provides a technical reference for account management in DMS.
Account provisioners and alternative authentication support
Each ACCOUNT_PROVISIONER
has a separate page for configuration guidance and caveats:
FILE
provisioner docsLDAP
provisioner docsAuthentication from the provisioner can be supplemented with additional methods:
For custom authentication requirements, you could implement this with Lua.
"},{"location":"config/account-management/overview/#accounts","title":"Accounts","text":"Info
To receive or send mail, you'll need to provision user accounts into DMS (as each provisioner page documents).
A DMS account represents a user with their login username + password, and optional config like aliases and quota.
The email address associated to an account creates a mailbox. This address is relevant:
SPOOF_PROTECTION=1
restricts the sender address to the DMS account email address (unless additional sender addresses have been permitted via supported config).SPOOF_PROTECTION=0
allows DMS accounts to use any sender address (only a single DMS account is necessary to send mail with different sender addresses).For more details, see the Technical Overview section.
Support for multiple mail domainsNo extra configuration in DMS is required after provisioning an account with an email address.
hostname
setting).An email address should conform to the standard permitted charset and format (local-part@domain-part
).
DMS has features that need to reserve special characters to work correctly. Ensure those characters are not present in email addresses you configure for DMS, otherwise disable / opt-out of the feature.
+
as the tag delimiter. The tag can be changed, feature opt-out when the tag is explicitly unset.Info
Aliases allow receiving mail:
@gmail.com
.Aliases are managed through Postfix which supports local and virtual aliases:
local
delivery agent (see associated alias config format)root
).postmaster
may be a local alias to root
, and root
to a virtual alias or real email address.local
delivery agent will not be delivered to an inbox managed by Dovecot (unless you have configured a local alias to redirect mail to a valid address or alias).hostname: mail.example.com
, thus user@mail.example.com
). Technically there is no domain-part at this point, that context is used when routing delivery, the local delivery agent only knows of the local-part (an alias or unix account).virtual
delivery agent (see associated alias config format)user@example.com
).Verify alias resolves correctly
You can run postmap -q <alias> <table>
in the container to verify an alias resolves to the expected target. If the target is also an alias, the command will not expand that alias to resolve the actual recipient(s).
For the FILE
provisioner, an example would be: postmap -q alias1@example.com /etc/postfix/virtual
. For the LDAP
provisioner you'd need to adjust the table path.
Side effect - Dovecot Quotas (ENABLE_QUOTAS=1
)
As a side effect of the alias workaround for the FILE
provisioner with this feature, aliases can be used for account login. This is not intentional.
Info
Enables mail clients with the capability to query a mailbox for disk-space used and capacity limit.
ENABLE_QUOTAS=0
Without quota limits for disk storage, a mailbox could fill up the available storage which would cause delivery failures to all mailboxes.
Quotas help by preventing that abuse, so that only a mailbox exceeding the assigned quota experiences a delivery failure instead of negatively impacting others (provided disk space is available).
Technical DetailsThe Dovecot Quotas feature is configured by enabling the Dovecot imap-quota
plugin and using the count
quota backend.
Dovecot workaround for Postfix aliases
When mail is delivered to DMS, Postfix will query Dovecot with the recipient(s) to verify quota has not been exceeded.
This allows early rejection of mail arriving to DMS, preventing a spammer from taking advantage of a backscatter source if the mail was accepted by Postfix, only to later be rejected by Dovecot for storage when the quota limit was already reached.
However, Postfix does not resolve aliases until after the incoming mail is accepted.
check_policy_service
restriction tied to the Dovecot quota-status
service) with the recipient (the alias).dovecot: auth: passwd-file(alias@example.com): unknown user
is logged, Postfix is then informed that the recipient mailbox is not full even if it actually was (since no such user exists in the Dovecot UserDB).As a workaround to this problem with the ENABLE_QUOTAS=1
feature, DMS will add aliases as fake users into Dovecot UserDB (that are configured with the same data as the real address the alias would resolve to, thus sharing the same mailbox location and quota limit). This allows Postfix to properly be aware of an aliased mailbox having exceeded the allowed quota.
NOTE: This workaround only supports aliases to a single target recipient of a real account address / mailbox.
quota-status
service and returning that response to Postfix.Info
Subaddressing (aka Plus Addressing or Address Tags) is a feature that allows you to receive mail to an address which includes a tag appended to the local-part
of a valid account address.
+
), followed by the tag: <local-part>+<tag>@<domain-part>
user+github@example.com
would deliver mail to the same mailbox as user@example.com
.+
and @
is understood as the tag, no additional configuration required.A common use-case is to use a unique tag for each service you register your email address with.
Add recipient_delimiter = +
to these config override files (replacing +
with your preferred delimiter):
docker-data/dms/config/postfix-main.cf
docker-data/dms/config/dovecot.cf
Follow the advice to change the tag delimiter, but instead set an empty value (recipient_delimiter =
).
Do not attempt to send mail from these tagged addresses, they are not equivalent to aliases.
This feature is only intended to be used when a mail client sends to a DMS managed recipient address. While DMS does not restrict the sender address you choose to send mail from (provided SPOOF_PROTECTION
has not been enabled), it is often forbidden by mail services.
The configured tag delimiter (+
) allows both Postfix and Dovecot to recognize subaddresses. Without this feature configured, the subaddresses would be considered as separate mail accounts rather than routed to a common account address.
Internally DMS has the tag delimiter configured by:
main.cf
setting: recipient_delimiter = +
+
by default: recipient_delimiter = +
Info
This section provides insight for understanding how Postfix and Dovecot services are involved. It is intended as a reference for maintainers and contributors.
Postfix needs to know how to handle inbound and outbound mail by asking these queries:
InboundOutboundSPOOF_PROTECTION=1
, how should DMS restrict the sender address? (eg: Users may only send mail from their associated mailbox address)Dovecot additionally handles authenticating user accounts for sending and retrieving mail:
Dovecot splits all authentication lookups into two categories:
setup
CLI","text":"The best way to manage DMS accounts and related config files is through our setup
CLI provided within the container.
Using the setup
CLI
Try the following within the DMS container (docker exec -it <CONTAINER NAME> bash
):
setup email add <EMAIL ADDRESS>
setup alias add <FROM ALIAS> <TO TARGET ADDRESS>
setup help
# Starts a basic DMS instance and then shells into the container to use the `setup` CLI:\ndocker run --rm -itd --name dms --hostname mail.example.com mailserver/docker-mailserver\ndocker exec -it dms bash\n\n# Create an account:\nsetup email add hello@example.com your-password-here\n\n# Create an alias:\nsetup alias add your-alias-here@example.com hello@example.com\n\n# Limit the mailbox capacity to 10 MiB:\nsetup quota set hello@example.com 10M\n
Secure password input When you don't provide a password to the command, you will be prompted for one. This avoids the password being captured in your shell history.
# As you input your password it will not update.\n# Press the ENTER key to apply the hidden password input.\n$ setup email add hello@example.com\nEnter Password:\nConfirm Password:\n
Account removal via setup email del
When you remove a DMS account with this command, it will also remove any associated aliases and quota.
The command will also prompt for deleting the account mailbox from disk, or can be forced with the -y
flag.
These config files belong to the Config Volume.
"},{"location":"config/account-management/provisioner/file/#accounts","title":"Accounts","text":"Info
Config file: docker-data/dms/config/postfix-accounts.cf
The config format is line-based with two fields separated by the delimiter |
:
secret
).setup email add
command A compatible password hash can be generated with:
doveadm pw -s SHA512-CRYPT -u hello@example.com -p secret\n
postfix-accounts.cf
config file
In this example DMS manages mail for the domain example.com
:
hello@example.com|{SHA512-CRYPT}$6$W4rxRQwI6HNMt9n3$riCi5/OqUxnU8eZsOlZwoCnrNgu1gBGPkJc.ER.LhJCu7sOg9i1kBrRIistlBIp938GdBgMlYuoXYUU5A4Qiv0\n
Dovecot \"extra fields\"
Appending a third column will customize \"extra fields\" when converting account data into a Dovecot UserDB entry.
DMS is not aware of these customizations beyond carrying them over, expect potential for bugs when this feature breaks any assumed conventions used in the scripts (such as changing the mailbox path or type).
Note
Account creation will normalize the provided email address to lowercase, as DMS does not support multiple case-sensitive address variants.
The email address chosen will also represent the login username credential for mail clients to authenticate with.
"},{"location":"config/account-management/provisioner/file/#aliases","title":"Aliases","text":"Info
Config file: docker-data/dms/config/postfix-virtual.cf
The config format is line-based with key value pairs (alias --> target address), with white-space as a delimiter.
postfix-virtual.cf
config file
In this example DMS manages mail for the domain example.com
:
# Alias delivers to an existing account:\nalias1@example.com hello@example.com\n\n# Alias forwards to an external email address:\nalias2@example.com external-account@gmail.com\n
Known Issues setup
CLI prevents an alias and account sharing an address:
You cannot presently add a new account (setup email add
) or alias (setup alias add
) with an address which already exists as an alias or account in DMS.
This restriction was enforced due to problems it could cause, although there are use-cases where you may legitimately require this functionality.
For now you must manually edit the postfix-virtual.cf
file as a workaround. There are no run-time checks outside of the setup
CLI related to this restriction.
Wildcard catch-all support (@example.com
):
While this type of alias without a local-part is supported, you must keep in mind that aliases in Postfix have a higher precedence than a real address associated to a DMS account.
As a result, the wildcard is matched first and will direct mail for that entire domain to the alias target address. To work around this, you will need an alias for each non-alias address of that domain.
Additionally, Postfix will read the alias config and choose the alias value that matches the recipient address first. Ensure your more specific aliases for the domain are declared above the wildcard alias in the config file.
Aliasing to another alias or multiple recipients:
While aliasing to multiple recipients is possible, DMS does not officially support that.
setup alias add
.Info
Config file: docker-data/dms/config/postfix-regexp.cf
This config file is similar to the above postfix-virtual.cf
, but the alias value is instead configured with a regex pattern.
There is no setup
CLI support for this feature, it is config only.
postfix-regexp.cf
config file
Deliver all mail for test
users to qa@example.com
instead:
# Remember to escape regex tokens like `.` => `\\.`, otherwise\n# your alias pattern may be more permissive than you intended:\n/^test[0-9][0-9]*@example\\.com/ qa@example.com\n
Technical Details postfix-virtual.cf
has precedence, postfix-regexp.cf
will only be checked if no alias match was found in postfix-virtual.cf
.
These files are both copied internally to /etc/postfix/
and configured in main.cf
for the virtual_alias_maps
setting. As postfix-virtual.cf
is declared first for that setting, it will be processed before using postfix-regexp.cf
as a fallback.
Info
Config file: docker-data/dms/config/dovecot-quotas.cf
The config format is line-based with two fields separated by the delimiter :
:
postfix-accounts.cf
.M
=> MiB
, G
=> GiB
).dovecot-quotas.cf
config file
For the account with the mailbox address of hello@example.com
, it may not exceed 5 GiB in storage:
hello@example.com:5G\n
"},{"location":"config/account-management/provisioner/ldap/","title":"Account Management | Provisioner (LDAP)","text":""},{"location":"config/account-management/provisioner/ldap/#introduction","title":"Introduction","text":"Getting started with ldap and DMS we need to take 3 parts in account:
postfix
for incoming & outgoing emaildovecot
for accessing mailboxessaslauthd
for SMTP authentication (this can also be delegated to dovecot)Have a look at the ENV page for information on the default values.
"},{"location":"config/account-management/provisioner/ldap/#ldap_query_filter_","title":"LDAP_QUERY_FILTER_*
","text":"Those variables contain the LDAP lookup filters for postfix, using %s
as the placeholder for the domain or email address in question. This means that...
DOMAIN
filter (see virtual_alias_domains
).USER
, ALIAS
and GROUP
filters.USER
filter specifies personal mailboxes, for which only one should exist per address, for example (mail=%s)
(also see virtual_mailbox_maps
)ALIAS
filter specifies aliases for mailboxes, using virtual_alias_maps
, for example (mailAlias=%s)
GROUP
filter specifies the personal mailboxes in a group (for emails that multiple people shall receive), using virtual_alias_maps
, for example (mailGroupMember=%s)
.ALIAS
and GROUP
, but ideally you should use ALIAS
for personal aliases for a singular person (like ceo@example.org
) and GROUP
for multiple people (like hr@example.org
).SENDERS
filter, and only if the authenticated user is one of the returned entries, the email can be sent.SPOOF_PROTECTION=1
.SENDERS
filter is missing, the USER
, ALIAS
and GROUP
filters will be used in a disjunction (OR).admin
group to spoof any sender email address, and to force everyone else to only use their personal mailbox address for outgoing email, you can use something like this: (|(memberOf=cn=admin,*)(mail=%s))
A really simple LDAP_QUERY_FILTER
configuration, using only the user filter and allowing only admin@*
to spoof any sender addresses.
- LDAP_START_TLS=yes\n- ACCOUNT_PROVISIONER=LDAP\n- LDAP_SERVER_HOST=ldap.example.org\n- LDAP_SEARCH_BASE=dc=example,dc=org\"\n- LDAP_BIND_DN=cn=admin,dc=example,dc=org\n- LDAP_BIND_PW=mypassword\n- SPOOF_PROTECTION=1\n\n- LDAP_QUERY_FILTER_DOMAIN=(mail=*@%s)\n- LDAP_QUERY_FILTER_USER=(mail=%s)\n- LDAP_QUERY_FILTER_ALIAS=(|) # doesn't match anything\n- LDAP_QUERY_FILTER_GROUP=(|) # doesn't match anything\n- LDAP_QUERY_FILTER_SENDERS=(|(mail=%s)(mail=admin@*))\n
"},{"location":"config/account-management/provisioner/ldap/#dovecot__filter-dovecot__attrs","title":"DOVECOT_*_FILTER
& DOVECOT_*_ATTRS
","text":"These variables specify the LDAP filters that dovecot uses to determine if a user can log in to their IMAP account, and which mailbox is responsible to receive email for a specific postfix user.
This is split into the following two lookups, both using %u
as the placeholder for the full login name (see dovecot documentation for a full list of placeholders). Usually you only need to set DOVECOT_USER_FILTER
, in which case it will be used for both filters.
DOVECOT_USER_FILTER
is used to get the account details (uid, gid, home directory, quota, ...) of a user.DOVECOT_PASS_FILTER
is used to get the password information of the user, and is in pretty much all cases identical to DOVECOT_USER_FILTER
(which is the default behavior if left away).If your directory doesn't have the postfix-book schema installed, then you must change the internal attribute handling for dovecot. For this you have to change the pass_attr
and the user_attr
mapping, as shown in the example below:
- DOVECOT_PASS_ATTRS=<YOUR_USER_IDENTIFIER_ATTRIBUTE>=user,<YOUR_USER_PASSWORD_ATTRIBUTE>=password\n- DOVECOT_USER_ATTRS=<YOUR_USER_HOME_DIRECTORY_ATTRIBUTE>=home,<YOUR_USER_MAILSTORE_ATTRIBUTE>=mail,<YOUR_USER_MAIL_UID_ATTRIBUTE>=uid,<YOUR_USER_MAIL_GID_ATTRIBUTE>=gid\n
Note
For DOVECOT_*_ATTRS
, you can replace ldapAttr=dovecotAttr
with =dovecotAttr=%{ldap:ldapAttr}
for more flexibility, like for example =home=/var/mail/%{ldap:uid}
or just =uid=5000
.
A list of dovecot attributes can be found in the dovecot documentation.
Defaults- DOVECOT_USER_ATTRS=mailHomeDirectory=home,mailUidNumber=uid,mailGidNumber=gid,mailStorageDirectory=mail\n- DOVECOT_PASS_ATTRS=uniqueIdentifier=user,userPassword=password\n- DOVECOT_USER_FILTER=(&(objectClass=PostfixBookMailAccount)(uniqueIdentifier=%n))\n
Example Setup for a directory that has the qmail-schema installed and uses uid
:
- DOVECOT_PASS_ATTRS=uid=user,userPassword=password\n- DOVECOT_USER_ATTRS=homeDirectory=home,qmailUID=uid,qmailGID=gid,mailMessageStore=mail\n- DOVECOT_USER_FILTER=(&(objectClass=qmailUser)(uid=%u)(accountStatus=active))\n
The LDAP server configuration for dovecot will be taken mostly from postfix, other options can be found in the environment section in the docs.
"},{"location":"config/account-management/provisioner/ldap/#dovecot_auth_bind","title":"DOVECOT_AUTH_BIND
","text":"Set this to yes
to enable authentication binds (more details in the dovecot documentation). Currently, only DN lookup is supported without further changes to the configuration files, so this is only useful when you want to bind as a readonly user without the permission to read passwords.
SASLAUTHD_LDAP_FILTER
","text":"This filter is used for saslauthd
, which is called by postfix when someone is authenticating through SMTP (assuming that SASLAUTHD_MECHANISMS=ldap
is being used). Note that you'll need to set up the LDAP server for saslauthd separately from postfix.
The filter variables are explained in detail in the LDAP_SASLAUTHD
file, but unfortunately, this method doesn't really support domains right now - that means that %U
is the only token that makes sense in this variable.
When to use this and how to avoid it
Using a separate filter for SMTP authentication allows you to for example allow noreply@example.org
to send email, but not log in to IMAP or receive email: (&(mail=%U@example.org)(|(memberOf=cn=email,*)(mail=noreply@example.org)))
If you don't want to use a separate filter for SMTP authentication, you can set SASLAUTHD_MECHANISMS=rimap
and SASLAUTHD_MECH_OPTIONS=127.0.0.1
to authenticate against dovecot instead - this means that the DOVECOT_USER_FILTER
and DOVECOT_PASS_FILTER
will be used for SMTP authentication as well.
saslauthd
- ENABLE_SASLAUTHD=1\n- SASLAUTHD_MECHANISMS=ldap\n- SASLAUTHD_LDAP_FILTER=(mail=%U@example.org)\n
"},{"location":"config/account-management/provisioner/ldap/#secure-connection-with-ldaps-or-starttls","title":"Secure Connection with LDAPS or StartTLS","text":"To enable LDAPS, all you need to do is to add the protocol to LDAP_SERVER_HOST
, for example ldaps://example.org:636
.
To enable LDAP over StartTLS (on port 389), you need to set the following environment variables instead (the protocol must not be ldaps://
in this case!):
- LDAP_START_TLS=yes\n- DOVECOT_TLS=yes\n- SASLAUTHD_LDAP_START_TLS=yes\n
"},{"location":"config/account-management/provisioner/ldap/#active-directory-configurations-tested-with-samba4-ad-implementation","title":"Active Directory Configurations (Tested with Samba4 AD Implementation)","text":"In addition to LDAP explanation above, when Docker Mailserver is intended to be used with Active Directory (or the equivalent implementations like Samba4 AD DC) the following points should be taken into consideration:
sAMAccountName
.proxyAddresses
can be used to store email aliases of single users. The convention is to prefix the email aliases with smtp:
(e.g: smtp:some.name@example.com
).uidNumber
, gidNumber
instead of the typical uid
. Assigning different owner to email folders can also be done in this approach, nevertheless there is a bug at the moment in Docker Mailserver that overwrites all permissions when starting the container. Either a manual fix is necessary now, or a temporary workaround to use a hard-coded ldap:uidNumber
that equals to 5000
until this issue is fixed.user-patches.sh
script to modify ldap-groups.cf
so that it includes leaf_result_attribute = mail
and special_result_attribute = member
. This can be achieved simply by:The configuration shown to get the Group to work is from here and here.
# user-patches.sh\n\n...\ngrep -q '^leaf_result_attribute = mail$' /etc/postfix/ldap-groups.cf || echo \"leaf_result_attribute = mail\" >> /etc/postfix/ldap-groups.cf\ngrep -q '^special_result_attribute = member$' /etc/postfix/ldap-groups.cf || echo \"special_result_attribute = member\" >> /etc/postfix/ldap-groups.cf\n...\n
/etc/ldap/ldap.conf
, if the TLS_REQCERT
is demand
/ hard
(default), the CA certificate used to verify the LDAP server certificate must be recognized as a trusted CA. This can be done by volume mounting the ca.crt
file and updating the trust store via a user-patches.sh
script:# user-patches.sh\n\n...\ncp /MOUNTED_FOLDER/ca.crt /usr/local/share/ca-certificates/\nupdate-ca-certificates\n...\n
The changes on the configurations necessary to work with Active Directory (only changes are listed, the rest of the LDAP configuration can be taken from the other examples shown in this documentation):
# If StartTLS is the chosen method to establish a secure connection with Active Directory.\n- LDAP_START_TLS=yes\n- SASLAUTHD_LDAP_START_TLS=yes\n- DOVECOT_TLS=yes\n\n- LDAP_QUERY_FILTER_USER=(&(objectclass=person)(mail=%s))\n- LDAP_QUERY_FILTER_ALIAS=(&(objectclass=person)(proxyAddresses=smtp:%s))\n# Filters Active Directory groups (mail lists). Additional changes on ldap-groups.cf are also required as shown above.\n- LDAP_QUERY_FILTER_GROUP=(&(objectClass=group)(mail=%s))\n- LDAP_QUERY_FILTER_DOMAIN=(mail=*@%s)\n# Allows only Domain admins to send any sender email address, otherwise the sender address must match the LDAP attribute `mail`.\n- SPOOF_PROTECTION=1\n- LDAP_QUERY_FILTER_SENDERS=(|(mail=%s)(proxyAddresses=smtp:%s)(memberOf=cn=Domain Admins,cn=Users,dc=*))\n\n- DOVECOT_USER_FILTER=(&(objectclass=person)(sAMAccountName=%n))\n# At the moment to be able to use %{ldap:uidNumber}, a manual bug fix as described above must be used. Otherwise %{ldap:uidNumber} %{ldap:uidNumber} must be replaced by the hard-coded value 5000.\n- DOVECOT_USER_ATTRS==uid=%{ldap:uidNumber},=gid=5000,=home=/var/mail/%Ln,=mail=maildir:~/Maildir\n- DOVECOT_PASS_ATTRS=sAMAccountName=user,userPassword=password\n- SASLAUTHD_LDAP_FILTER=(&(sAMAccountName=%U)(objectClass=person))\n
"},{"location":"config/account-management/provisioner/ldap/#ldap-setup-examples","title":"LDAP Setup Examples","text":"Basic Setup services:\n mailserver:\n image: ghcr.io/docker-mailserver/docker-mailserver:latest\n container_name: mailserver\n hostname: mail.example.com\n\n ports:\n - \"25:25\"\n - \"143:143\"\n - \"587:587\"\n - \"993:993\"\n\n volumes:\n - ./docker-data/dms/mail-data/:/var/mail/\n - ./docker-data/dms/mail-state/:/var/mail-state/\n - ./docker-data/dms/mail-logs/:/var/log/mail/\n - ./docker-data/dms/config/:/tmp/docker-mailserver/\n - /etc/localtime:/etc/localtime:ro\n\n environment:\n - ENABLE_SPAMASSASSIN=1\n - ENABLE_CLAMAV=1\n - ENABLE_FAIL2BAN=1\n - ENABLE_POSTGREY=1\n\n # >>> Postfix LDAP Integration\n - ACCOUNT_PROVISIONER=LDAP\n - LDAP_SERVER_HOST=ldap.example.org\n - LDAP_BIND_DN=cn=admin,ou=users,dc=example,dc=org\n - LDAP_BIND_PW=mypassword\n - LDAP_SEARCH_BASE=dc=example,dc=org\n - LDAP_QUERY_FILTER_DOMAIN=(|(mail=*@%s)(mailAlias=*@%s)(mailGroupMember=*@%s))\n - LDAP_QUERY_FILTER_USER=(&(objectClass=inetOrgPerson)(mail=%s))\n - LDAP_QUERY_FILTER_ALIAS=(&(objectClass=inetOrgPerson)(mailAlias=%s))\n - LDAP_QUERY_FILTER_GROUP=(&(objectClass=inetOrgPerson)(mailGroupMember=%s))\n - LDAP_QUERY_FILTER_SENDERS=(&(objectClass=inetOrgPerson)(|(mail=%s)(mailAlias=%s)(mailGroupMember=%s)))\n - SPOOF_PROTECTION=1\n # <<< Postfix LDAP Integration\n\n # >>> Dovecot LDAP Integration\n - DOVECOT_USER_FILTER=(&(objectClass=inetOrgPerson)(mail=%u))\n - DOVECOT_PASS_ATTRS=uid=user,userPassword=password\n - DOVECOT_USER_ATTRS==home=/var/mail/%{ldap:uid},=mail=maildir:~/Maildir,uidNumber=uid,gidNumber=gid\n # <<< Dovecot LDAP Integration\n\n # >>> SASL LDAP Authentication\n - ENABLE_SASLAUTHD=1\n - SASLAUTHD_MECHANISMS=ldap\n - SASLAUTHD_LDAP_FILTER=(&(mail=%U@example.org)(objectClass=inetOrgPerson))\n # <<< SASL LDAP Authentication\n\n - SSL_TYPE=letsencrypt\n - PERMIT_DOCKER=host\n\n cap_add:\n - NET_ADMIN\n
Kopano / Zarafa services:\n mailserver:\n image: ghcr.io/docker-mailserver/docker-mailserver:latest\n container_name: mailserver\n hostname: mail.example.com\n\n ports:\n - \"25:25\"\n - \"143:143\"\n - \"587:587\"\n - \"993:993\"\n\n volumes:\n - ./docker-data/dms/mail-data/:/var/mail/\n - ./docker-data/dms/mail-state/:/var/mail-state/\n - ./docker-data/dms/config/:/tmp/docker-mailserver/\n\n environment:\n # We are not using dovecot here\n - SMTP_ONLY=1\n - ENABLE_SPAMASSASSIN=1\n - ENABLE_CLAMAV=1\n - ENABLE_FAIL2BAN=1\n - ENABLE_POSTGREY=1\n - SASLAUTHD_PASSWD=\n\n # >>> SASL Authentication\n - ENABLE_SASLAUTHD=1\n - SASLAUTHD_LDAP_FILTER=(&(sAMAccountName=%U)(objectClass=person))\n - SASLAUTHD_MECHANISMS=ldap\n # <<< SASL Authentication\n\n # >>> Postfix Ldap Integration\n - ACCOUNT_PROVISIONER=LDAP\n - LDAP_SERVER_HOST=<yourLdapContainer/yourLdapServer>\n - LDAP_SEARCH_BASE=dc=mydomain,dc=loc\n - LDAP_BIND_DN=cn=Administrator,cn=Users,dc=mydomain,dc=loc\n - LDAP_BIND_PW=mypassword\n - LDAP_QUERY_FILTER_USER=(&(objectClass=user)(mail=%s))\n - LDAP_QUERY_FILTER_GROUP=(&(objectclass=group)(mail=%s))\n - LDAP_QUERY_FILTER_ALIAS=(&(objectClass=user)(otherMailbox=%s))\n - LDAP_QUERY_FILTER_DOMAIN=(&(|(mail=*@%s)(mailalias=*@%s)(mailGroupMember=*@%s))(mailEnabled=TRUE))\n # <<< Postfix Ldap Integration\n\n # >>> Kopano Integration\n - POSTFIX_DAGENT=lmtp:kopano:2003\n # <<< Kopano Integration\n\n - SSL_TYPE=letsencrypt\n - PERMIT_DOCKER=host\n\n cap_add:\n - NET_ADMIN\n
"},{"location":"config/account-management/supplementary/master-accounts/","title":"Account Management | Master Accounts (Dovecot)","text":"This feature is useful for administrative tasks like hot backups.
Note
This feature is presently not supported with ACCOUNT_PROVISIONER=LDAP
.
Info
A Master Account:
setup
CLI support
Use the setup dovecot-master <add|update|del|list>
commands. These are roughly equivalent to the setup email
subcommands.
Config file: docker-data/dms/config/dovecot-masters.cf
The config format is the same as postfix-accounts.cf
for ACCOUNT_PROVISIONER=FILE
.
The only difference is the account field has no @domain-part
suffix, it is only a username.
The Master Accounts feature in DMS configures the Dovecot Master Users feature with the Dovecot setting auth_master_user_separator
(where the default value is *
).
Info
To login as another DMS account (user@example.com
) with POP3 or IMAP, use the following credentials format:
<LOGIN USERNAME>*<MASTER USER>
(user@example.com*admin
)<MASTER PASSWORD>
Verify login functionality
In the DMS container, you can verify with the testsaslauthd
command:
# Prerequisites:\n# A regular DMS account to test login through a Master Account:\nsetup email add user@example.com secret\n# Add a new Master Account:\nsetup dovecot-master add admin top-secret\n
# Login with credentials format as described earlier:\ntestsaslauthd -u 'user@example.com*admin' -p 'top-secret'\n
Alternatively, any mail client should be able to login the equivalent credentials.
"},{"location":"config/account-management/supplementary/oauth2/","title":"Authentication - OAuth2 / OIDC","text":"This feature enables support for delegating DMS account authentication through to an external Identity Provider (IdP).
Receiving mail requires a DMS account to exist
If you expect DMS to receive mail, you must provision an account into DMS in advance. Otherwise DMS has no awareness of your externally manmaged users and will reject delivery.
There are plans to implement support to provision users through a SCIM 2.0 API. An IdP that can operate as a SCIM Client (eg: Authentik) would then integrate with DMS for user provisioning. Until then you must keep your user accounts in sync manually via your configured ACCOUNT_PROVISIONER
.
XOAUTH2
or OAUTHBEARER
.XOAUTH2 (Googles widely adopted implementation) and OAUTHBEARER (the newer variant standardized by RFC 7628 in 2015) are supported as standards for verifying that a OAuth Bearer Token (RFC 6750 from 2012) is valid at the identity provider that created the token. The token itself in both cases is expected to be can an opaque Access Token, but it is possible to use a JWT ID Token (which encodes additional information into the token itself).
A mail client like Thunderbird has limited OAuth2 / OIDC support. The software maintains a hard-coded list of providers supported. Roundcube is a webmail client that does have support for generic providers, allowing you to integrate with a broader range of IdP services.
Documentation for this feature is WIP
See the initial feature support and existing issues for guidance that has not yet been documented officially.
Verify authentication worksIf you have a compatible mail client you can verify login through that.
CLI - Verify withcurl
# Shell into your DMS container:\ndocker exec -it dms bash\n\n# Adjust these variables for the methods below to use:\nexport AUTH_METHOD='OAUTHBEARER' USER_ACCOUNT='hello@example.com' ACCESS_TOKEN='DMS_YWNjZXNzX3Rva2Vu'\n\n# Authenticate via IMAP (Dovecot):\ncurl --silent --url 'imap://localhost:143' \\\n --login-options \"AUTH=${AUTH_METHOD}\" --user \"${USER_ACCOUNT}\" --oauth2-bearer \"${ACCESS_TOKEN}\" \\\n --request 'LOGOUT' \\\n && grep \"dovecot: imap-login: Login: user=<${USER_ACCOUNT}>, method=${AUTH_METHOD}\" /var/log/mail/mail.log\n\n# Authenticate via SMTP (Postfix), sending a mail with the same sender(from) and recipient(to) address:\n# NOTE: `curl` seems to require `--upload-file` with some mail content provided to test SMTP auth.\ncurl --silent --url 'smtp://localhost:587' \\\n --login-options \"AUTH=${AUTH_METHOD}\" --user \"${USER_ACCOUNT}\" --oauth2-bearer \"${ACCESS_TOKEN}\" \\\n --mail-from \"${USER_ACCOUNT}\" --mail-rcpt \"${USER_ACCOUNT}\" --upload-file - <<< 'RFC 5322 content - not important' \\\n && grep \"postfix/submission/smtpd.*, sasl_method=${AUTH_METHOD}, sasl_username=${USER_ACCOUNT}\" /var/log/mail/mail.log\n
Troubleshooting:
--verbose
to the curl options. This will output the protocol exchange which includes if authentication was successful or failed.curl
commands with grep
on DMS logs (for Dovecot and Postfix services). When not running curl
from the DMS container, ensure you check the logs correctly, or inspect the --verbose
output instead.curl
bug with XOAUTH2
Older releases of curl
have a bug with XOAUTH2
support since 7.80.0
(Nov 2021) but fixed from 8.6.0
(Jan 2024). It treats XOAUTH2
as OAUTHBEARER
.
If you use docker exec
to run curl
from within DMS, the current DMS v14 release (Debian 12 with curl 7.88.1
) is affected by this bug.
This example assumes you have already set up:
Setup Instructions
1. Docker Mailserver2. Authentik3. RoundcubeUpdate your Docker Compose ENV config to include:
compose.yamlservices:\n mailserver:\n env:\n # Enable the feature:\n - ENABLE_OAUTH2=1\n # Specify the user info endpoint URL of the oauth2 server for token inspection:\n - OAUTH2_INTROSPECTION_URL=https://authentik.example.com/application/o/userinfo/\n
https://roundcube.example.com/index.php/login/oauth
for your RoundCube instance.Add the following to oauth2.inc.php
(documentation):
$config['oauth_provider'] = 'generic';\n$config['oauth_provider_name'] = 'Authentik';\n$config['oauth_client_id'] = '<insert client id here>';\n$config['oauth_client_secret'] = '<insert client secret here>';\n$config['oauth_auth_uri'] = 'https://authentik.example.com/application/o/authorize/';\n$config['oauth_token_uri'] = 'https://authentik.example.com/application/o/token/';\n$config['oauth_identity_uri'] = 'https://authentik.example.com/application/o/userinfo/';\n\n// Optional: disable SSL certificate check on HTTP requests to OAuth server. For possible values, see:\n// http://docs.guzzlephp.org/en/stable/request-options.html#verify\n$config['oauth_verify_peer'] = false;\n\n$config['oauth_scope'] = 'email openid profile';\n$config['oauth_identity_fields'] = ['email'];\n\n// Boolean: automatically redirect to OAuth login when opening Roundcube without a valid session\n$config['oauth_login_redirect'] = false;\n
"},{"location":"config/advanced/full-text-search/","title":"Advanced | Full-Text Search","text":""},{"location":"config/advanced/full-text-search/#overview","title":"Overview","text":"Full-text search allows all messages to be indexed, so that mail clients can quickly and efficiently search messages by their full text content. Dovecot supports a variety of community supported FTS indexing backends.
DMS comes pre-installed with two plugins that can be enabled with a dovecot config file.
Please be aware that indexing consumes memory and takes up additional disk space.
"},{"location":"config/advanced/full-text-search/#xapian","title":"Xapian","text":"The dovecot-fts-xapian plugin makes use of Xapian. Xapian enables embedding an FTS engine without the need for additional backends.
The indexes will be stored as a subfolder named xapian-indexes
inside your local mail-data
folder (/var/mail
internally). With the default settings, 10GB of email data may generate around 4GB of indexed data.
While indexing is memory intensive, you can configure the plugin to limit the amount of memory consumed by the index workers. With Xapian being small and fast, this plugin is a good choice for low memory environments (2GB).
"},{"location":"config/advanced/full-text-search/#setup","title":"Setup","text":"To configure fts-xapian
as a dovecot plugin, create a file at docker-data/dms/config/dovecot/fts-xapian-plugin.conf
and place the following in it:
mail_plugins = $mail_plugins fts fts_xapian\n\nplugin {\n fts = xapian\n fts_xapian = partial=3 full=20 verbose=0\n\n fts_autoindex = yes\n fts_enforced = yes\n\n # disable indexing of folders\n # fts_autoindex_exclude = \\Trash\n\n # Index attachements\n # fts_decoder = decode2text\n}\n\nservice indexer-worker {\n # limit size of indexer-worker RAM usage, ex: 512MB, 1GB, 2GB\n vsz_limit = 1GB\n}\n\n# service decode2text {\n# executable = script /usr/libexec/dovecot/decode2text.sh\n# user = dovecot\n# unix_listener decode2text {\n# mode = 0666\n# }\n# }\n
adjust the settings to tune for your desired memory limits, exclude folders and enable searching text inside of attachments
Update compose.yaml
to load the previously created dovecot plugin config file:
services:\n mailserver:\n image: ghcr.io/docker-mailserver/docker-mailserver:latest\n container_name: mailserver\n hostname: mail.example.com\n env_file: mailserver.env\n ports:\n - \"25:25\" # SMTP (explicit TLS => STARTTLS)\n - \"143:143\" # IMAP4 (explicit TLS => STARTTLS)\n - \"465:465\" # ESMTP (implicit TLS)\n - \"587:587\" # ESMTP (explicit TLS => STARTTLS)\n - \"993:993\" # IMAP4 (implicit TLS)\n volumes:\n - ./docker-data/dms/mail-data/:/var/mail/\n - ./docker-data/dms/mail-state/:/var/mail-state/\n - ./docker-data/dms/mail-logs/:/var/log/mail/\n - ./docker-data/dms/config/:/tmp/docker-mailserver/\n - ./docker-data/dms/config/dovecot/fts-xapian-plugin.conf:/etc/dovecot/conf.d/10-plugin.conf:ro\n - /etc/localtime:/etc/localtime:ro\n restart: always\n stop_grace_period: 1m\n cap_add:\n - NET_ADMIN\n
Recreate containers:
docker compose down\ndocker compose up -d\n
Initialize indexing on all users for all mail:
docker compose exec mailserver doveadm index -A -q \\*\n
Run the following command in a daily cron job:
docker compose exec mailserver doveadm fts optimize -A\n
Or like the Spamassassin example shows, you can instead use cron
from within DMS to avoid potential errors if the mail server is not running: Create a system cron file:
# in the compose.yaml root directory\nmkdir -p ./docker-data/dms/cron # if you didn't have this folder before\ntouch ./docker-data/dms/cron/fts_xapian\nchown root:root ./docker-data/dms/cron/fts_xapian\nchmod 0644 ./docker-data/dms/cron/fts_xapian\n
Edit the system cron file nano ./docker-data/dms/cron/fts_xapian
, and set an appropriate configuration:
# Adding `MAILTO=\"\"` prevents cron emailing notifications of the task outcome each run\nMAILTO=\"\"\n#\n# m h dom mon dow user command\n#\n# Everyday 4:00AM, optimize index files\n0 4 * * * root doveadm fts optimize -A\n
Then with compose.yaml
:
services:\n mailserver:\n image: ghcr.io/docker-mailserver/docker-mailserver:latest\n volumes:\n - ./docker-data/dms/cron/fts_xapian:/etc/cron.d/fts_xapian\n
"},{"location":"config/advanced/full-text-search/#further-discussion","title":"Further Discussion","text":"See #905
"},{"location":"config/advanced/ipv6/","title":"Advanced | IPv6","text":"Ample Opportunities for Issues
Numerous bug reports have been raised in the past about IPv6. Please make sure your setup around DMS is correct when using IPv6!
"},{"location":"config/advanced/ipv6/#ipv6-networking-problems-with-docker-defaults","title":"IPv6 networking problems with Docker defaults","text":""},{"location":"config/advanced/ipv6/#what-can-go-wrong","title":"What can go wrong?","text":"If your host system supports IPv6 and an AAAA
DNS record exists to direct IPv6 traffic to DMS, you may experience issues when an IPv6 connection is made:
The impact of losing the real IP of the client connection can negatively affect DMS:
When the host network receives a connection to a containers published port, it is routed to the containers internal network managed by Docker (typically a bridge network).
By default, the Docker daemon only assigns IPv4 addresses to containers, thus it will only accept IPv4 connections (unless a docker-proxy
process is listening, which the default daemon setting userland-proxy: true
enables). With the daemon setting userland-proxy: true
(default), IPv6 connections from the host can also be accepted and routed to containers (even when they only have IPv4 addresses assigned). userland-proxy: false
will require the container to have atleast an IPv6 address assigned.
This can be problematic for IPv6 host connections when internally the container is no longer aware of the original client IPv6 address, as it has been proxied through the IPv4 or IPv6 gateway address of it's connected network (eg: 172.17.0.1
- Docker allocates networks from a set of default subnets).
This can be fixed by enabling a Docker network to assign IPv6 addresses to containers, along with some additional configuration. Alternatively you could configure the opposite to prevent IPv6 connections being made.
"},{"location":"config/advanced/ipv6/#prevent-ipv6-connections","title":"Prevent IPv6 connections","text":"AAAA
DNS record for your DMS FQDN would prevent resolving an IPv6 address to connect to.userland-proxy: false
, which will fail to establish a remote connection to DMS (provided no IPv6 address was assigned).With UFW or Firewalld
When one of these firewall frontends are active, remote clients should fail to connect instead of being masqueraded as the docker network gateway IP. Keep in mind that this only affects remote clients, it does not affect local IPv6 connections originating within the same host.
"},{"location":"config/advanced/ipv6/#enable-proper-ipv6-support","title":"Enable proper IPv6 support","text":"You can enable IPv6 support in Docker for container networks, however compatibility concerns may affect your success.
The official Docker documentation on enabling IPv6 has been improving and is a good resource to reference.
Enable ip6tables
support so that Docker will manage IPv6 networking rules as well. This will allow for IPv6 NAT to work like the existing IPv4 NAT already does for your containers, avoiding the above issue with external connections having their IP address seen as the container network gateway IP (provided an IPv6 address is also assigned to the container).
Configure the following in /etc/docker/daemon.json
{\n \"ip6tables\": true,\n \"experimental\" : true,\n \"userland-proxy\": true\n}\n
experimental: true
is currently required for ip6tables: true
to work.userland-proxy
setting can potentially affect connection behavior for local connections.Now restart the daemon if it's running: systemctl restart docker
.
Next, configure a network with an IPv6 subnet for your container with any of these examples:
Create an IPv6 ULA subnet About these examplesThese examples are focused on a IPv6 ULA subnet which is suitable for most users as described in the next section.
/64
(eg: /112
, which still provides over 65k IPv6 addresses), especially if instead configuring for an IPv6 GUA subnet.default-address-pools
).The preferred approach is with user-defined networks via compose.yaml
(recommended) or CLI with docker network create
:
Create the network in compose.yaml
and attach a service to it:
services:\n mailserver:\n networks:\n - dms-ipv6\n\nnetworks:\n dms-ipv6:\n enable_ipv6: true\n ipam:\n config:\n - subnet: fd00:cafe:face:feed::/64\n
Override the implicit default
network You can optionally avoid the service assignment by overriding the default
user-defined network that Docker Compose generates. Just replace dms-ipv6
with default
.
The Docker Compose default
bridge is not affected by settings for the default bridge
(aka docker0
) in /etc/docker/daemon.json
.
compose.yaml
To reference this network externally (from other compose files or docker run
), assign the networks name
key in compose.yaml
.
Create the network via a CLI command (which can then be used with docker run --network dms-ipv6
):
docker network create --ipv6 --subnet fd00:cafe:face:feed::/64 dms-ipv6\n
Optionally reference it from one or more compose.yaml
files:
services:\n mailserver:\n networks:\n - dms-ipv6\n\nnetworks:\n dms-ipv6:\n external: true\n
This approach is discouraged
The bridge
network is considered legacy.
Add these two extra IPv6 settings to your daemon config. They only apply to the default bridge
docker network aka docker0
(which containers are attached to by default when using docker run
).
{\n \"ipv6\": true,\n \"fixed-cidr-v6\": \"fd00:cafe:face:feed::/64\",\n}\n
Compose projects can also use this network via network_mode
:
services:\n mailserver:\n network_mode: bridge\n
Do not use 2001:db8:1::/64
for your private subnet
The 2001:db8
address prefix is reserved for documentation. Avoid creating a subnet with this prefix.
Presently this is used in examples for Dockers IPv6 docs as a placeholder, while mixed in with private IPv4 addresses which can be misleading.
"},{"location":"config/advanced/ipv6/#configuring-an-ipv6-subnet","title":"Configuring an IPv6 subnet","text":"If you've configured IPv6 address pools in /etc/docker/daemon.json
, you do not need to specify a subnet explicitly. Otherwise if you're unsure what value to provide, here's a quick guide (Tip: Prefer IPv6 ULA, it's the least hassle):
fd00:cafe:face:feed::/64
is an example of a IPv6 ULA subnet. ULA addresses are akin to the private IPv4 subnets you may already be familiar with. You can use that example, or choose your own ULA address. This is a good choice for getting Docker containers to their have networks support IPv6 via NAT like they already do by default with IPv4./64
block assigned to your host, but this varies by provider.ip6tables: true
is not required), you will want a firewall configured to manage which ports are accessible instead as no NAT is involved. Note that this may not be desired if the container should also be reachable via the host IPv4 public address./64
into smaller subnets for Docker to use only portions of the /64
. This can reduce some routing features, and require additional setup / management via a NDP Proxy for your public interface to know of IPv6 assignments managed by Docker and accept external traffic.With Docker CLI or Docker Compose, run a traefik/whoami
container with your IPv6 docker network and port 80 published. You can then send a curl request (or via address in the browser) from another host (as your remote client) with an IPv6 network, the RemoteAddr
value returned should match your client IPv6 address.
docker run --rm -d --network dms-ipv6 -p 80:80 traefik/whoami\n# On a different host, replace `2001:db8::1` with your DMS host IPv6 address\ncurl --max-time 5 http://[2001:db8::1]:80\n
IPv6 gateway IP
If instead of the remote IPv6 address, you may notice the gateway IP for the IPv6 subnet your DMS container belongs to.
This will happen when DMS has an IPv6 IP address assigned, for the same reason as with IPv4, userland-proxy: true
. It indicates that your daemon.json
has not been configured correctly or had the updated config applied for ip6tables :true
+ experimental: true
. Make sure you used systemctl restart docker
after updating daemon.json
.
IPv6 ULA address priority
DNS lookups that have records for both IPv4 and IPv6 addresses (eg: localhost
) may prefer IPv4 over IPv6 (ULA) for private addresses, whereas for public addresses IPv6 has priority. This shouldn't be anything to worry about, but can come across as a surprise when testing your IPv6 setup on the same host instead of from a remote client.
The preference can be controlled with /etc/gai.conf
, and appears was configured this way based on the assumption that IPv6 ULA would never be used with NAT. It should only affect the destination resolved for outgoing connections, which for IPv6 ULA should only really affect connections between your containers / host. In future IPv6 ULA may also be prioritized.
This article describes how to deploy DMS to Kubernetes. We highly recommend everyone to use our community DMS Helm chart.
Requirements
docker run
or Docker Compose).Limited Support
DMS does not officially support Kubernetes. This content is entirely community-supported. If you find errors, please open an issue and raise a PR.
"},{"location":"config/advanced/kubernetes/#manually-writing-manifests","title":"Manually Writing Manifests","text":"If using our Helm chart is not viable for you, here is some guidance to start with your own manifests.
ConfigMap
PersistentVolumeClaim
Service
Certificate
Deployment
Provide the basic configuration via environment variables with a ConfigMap
.
Example
Below is only an example configuration, adjust the ConfigMap
to your own needs.
---\napiVersion: v1\nkind: ConfigMap\n\nmetadata:\n name: mailserver.environment\n\nimmutable: false\n\ndata:\n TLS_LEVEL: modern\n POSTSCREEN_ACTION: drop\n OVERRIDE_HOSTNAME: mail.example.com\n FAIL2BAN_BLOCKTYPE: drop\n POSTMASTER_ADDRESS: postmaster@example.com\n UPDATE_CHECK_INTERVAL: 10d\n POSTFIX_INET_PROTOCOLS: ipv4\n ENABLE_CLAMAV: '1'\n ENABLE_POSTGREY: '0'\n ENABLE_FAIL2BAN: '1'\n AMAVIS_LOGLEVEL: '-1'\n SPOOF_PROTECTION: '1'\n MOVE_SPAM_TO_JUNK: '1'\n ENABLE_UPDATE_CHECK: '1'\n ENABLE_SPAMASSASSIN: '1'\n SUPERVISOR_LOGLEVEL: warn\n SPAMASSASSIN_SPAM_TO_INBOX: '1'\n\n # here, we provide an example for the SSL configuration\n SSL_TYPE: manual\n SSL_CERT_PATH: /secrets/ssl/rsa/tls.crt\n SSL_KEY_PATH: /secrets/ssl/rsa/tls.key\n
You can also make use of user-provided configuration files (e.g. user-patches.sh
, postfix-accounts.cf
, etc), to customize DMS to your needs.
Here is a minimal example that supplies a postfix-accounts.cf
file inline with two users:
---\napiVersion: v1\nkind: ConfigMap\n\nmetadata:\n name: mailserver.files\n\ndata:\n postfix-accounts.cf: |\n test@example.com|{SHA512-CRYPT}$6$someHashValueHere\n other@example.com|{SHA512-CRYPT}$6$someOtherHashValueHere\n
Static Configuration
The inline postfix-accounts.cf
config example above provides file content that is static. It is mounted as read-only at runtime, thus cannot support modifications.
For production deployments, use persistent volumes instead (via PersistentVolumeClaim
). That will enable files like postfix-account.cf
to add and remove accounts, while also persisting those changes externally from the container.
Modularize your ConfigMap
Kustomize can be a useful tool as it supports creating a ConfigMap
from multiple files.
To persist data externally from the DMS container, configure a PersistentVolumeClaim
(PVC).
Make sure you have a storage system (like Longhorn, Rook, etc.) and that you choose the correct storageClassName
(according to your storage system).
Example
---\napiVersion: v1\nkind: PersistentVolumeClaim\n\nmetadata:\n name: data\n\nspec:\n storageClassName: local-path\n accessModes:\n - ReadWriteOnce\n resources:\n requests:\n storage: 25Gi\n
A Service
is required for getting the traffic to the pod itself. It configures a load balancer with the ports you'll need.
The configuration for a Service
affects if the original IP from a connecting client is preserved (this is important). More about this further down below.
Example
---\napiVersion: v1\nkind: Service\n\nmetadata:\n name: mailserver\n labels:\n app: mailserver\n\nspec:\n # `Local` is most likely required, otherwise every incoming request would be identified by the external IP,\n # which will get banned by Fail2Ban when monitored services are not configured for PROXY protocol\n externalTrafficPolicy: Local\n type: LoadBalancer\n\n selector:\n app: mailserver\n\n ports:\n # smtp\n - name: smtp\n port: 25\n targetPort: smtp\n protocol: TCP\n # submissions (ESMTP with implicit TLS)\n - name: submissions\n port: 465\n targetPort: submissions\n protocol: TCP\n # submission (ESMTP with explicit TLS)\n - name: submission\n port: 587\n targetPort: submission\n protocol: TCP\n # imaps (implicit TLS)\n - name: imaps\n port: 993\n targetPort: imaps\n protocol: TCP\n
Using cert-manager
to supply TLS certificates
---\napiVersion: cert-manager.io/v1\nkind: Certificate\n\nmetadata:\n name: mail-tls-certificate-rsa\n\nspec:\n secretName: mail-tls-certificate-rsa\n isCA: false\n privateKey:\n algorithm: RSA\n encoding: PKCS1\n size: 2048\n dnsNames: [mail.example.com]\n issuerRef:\n name: mail-issuer\n kind: Issuer\n
The TLS docs page provides guidance when it comes to certificates and transport layer security.
ECDSA + RSA (fallback)
You could supply RSA certificates as fallback certificates instead, with ECDSA as the primary. DMS supports dual certificates via the ENV SSL_ALT_CERT_PATH
and SSL_ALT_KEY_PATH
.
Always provide sensitive information via a Secret
For storing OpenDKIM keys, TLS certificates, or any sort of sensitive data - you should be using Secret
s.
A Secret
is similar to ConfigMap
, it can be used and mounted as a volume as demonstrated in the Deployment
manifest tab.
The Deployment
config is the most complex component.
ConfigMap
s, persisted storage, etc.---\napiVersion: apps/v1\nkind: Deployment\n\nmetadata:\n name: mailserver\n\n annotations:\n ignore-check.kube-linter.io/run-as-non-root: >-\n 'mailserver' needs to run as root\n ignore-check.kube-linter.io/privileged-ports: >-\n 'mailserver' needs privileged ports\n ignore-check.kube-linter.io/no-read-only-root-fs: >-\n There are too many files written to make the root FS read-only\n\nspec:\n replicas: 1\n selector:\n matchLabels:\n app: mailserver\n\n template:\n metadata:\n labels:\n app: mailserver\n\n annotations:\n container.apparmor.security.beta.kubernetes.io/mailserver: runtime/default\n\n spec:\n hostname: mail\n containers:\n - name: mailserver\n image: ghcr.io/docker-mailserver/docker-mailserver:latest\n imagePullPolicy: IfNotPresent\n\n securityContext:\n # `allowPrivilegeEscalation: true` is required to support SGID via the `postdrop`\n # executable in `/var/mail-state` for Postfix (maildrop + public dirs):\n # https://github.com/docker-mailserver/docker-mailserver/pull/3625\n allowPrivilegeEscalation: true\n readOnlyRootFilesystem: false\n runAsUser: 0\n runAsGroup: 0\n runAsNonRoot: false\n privileged: false\n capabilities:\n add:\n # file permission capabilities\n - CHOWN\n - FOWNER\n - MKNOD\n - SETGID\n - SETUID\n - DAC_OVERRIDE\n # network capabilities\n - NET_ADMIN # needed for F2B\n - NET_RAW # needed for F2B\n - NET_BIND_SERVICE\n # miscellaneous capabilities\n - SYS_CHROOT\n - KILL\n drop: [ALL]\n seccompProfile:\n type: RuntimeDefault\n\n # Tune this to your needs.\n # If you disable ClamAV, you can use less RAM and CPU.\n # This becomes important in case you're low on resources\n # and Kubernetes refuses to schedule new pods.\n resources:\n limits:\n memory: 4Gi\n cpu: 1500m\n requests:\n memory: 2Gi\n cpu: 600m\n\n volumeMounts:\n - name: files\n subPath: postfix-accounts.cf\n mountPath: /tmp/docker-mailserver/postfix-accounts.cf\n readOnly: true\n\n # PVCs\n - name: data\n mountPath: /var/mail\n subPath: data\n readOnly: false\n - name: data\n mountPath: /var/mail-state\n subPath: state\n readOnly: false\n - name: data\n mountPath: /var/log/mail\n subPath: log\n readOnly: false\n\n # certificates\n - name: certificates-rsa\n mountPath: /secrets/ssl/rsa/\n readOnly: true\n\n ports:\n - name: smtp\n containerPort: 25\n protocol: TCP\n - name: submissions\n containerPort: 465\n protocol: TCP\n - name: submission\n containerPort: 587\n - name: imaps\n containerPort: 993\n protocol: TCP\n\n envFrom:\n - configMapRef:\n name: mailserver.environment\n\n restartPolicy: Always\n\n volumes:\n # configuration files\n - name: files\n configMap:\n name: mailserver.files\n\n # PVCs\n - name: data\n persistentVolumeClaim:\n claimName: data\n\n # certificates\n - name: certificates-rsa\n secret:\n secretName: mail-tls-certificate-rsa\n items:\n - key: tls.key\n path: tls.key\n - key: tls.crt\n path: tls.crt\n
"},{"location":"config/advanced/kubernetes/#exposing-your-mail-server-to-the-outside-world","title":"Exposing your Mail Server to the Outside World","text":"The more difficult part with Kubernetes is to expose a deployed DMS instance to the outside world.
The major problem with exposing DMS to the outside world in Kubernetes is to preserve the real client IP. The real client IP is required by DMS for performing IP-based DNS and spam checks.
Kubernetes provides multiple ways to address this; each has its upsides and downsides.
Configure IP ManuallyHost networkUsing the PROXY Protocol Advantages / DisadvantagesRequirements
Service
.A
and PTR
records (which other mail servers will use to verify trust when they receive mail sent from your DMS instance).Example
Assign the DMS Service
an external IP directly, or delegate an LB to assign the IP on your behalf.
The DMS Service
is configured with an \"external IP\" manually. Append your externally reachable IP address to spec.externalIPs
.
---\napiVersion: v1\nkind: Service\n\nmetadata:\n name: mailserver\n labels:\n app: mailserver\n\nspec:\n selector:\n app: mailserver\n ports:\n - name: smtp\n port: 25\n targetPort: smtp\n # ...\n\n externalIPs:\n - 10.20.30.40\n
The config differs depending on your choice of load balancer. This example uses MetalLB.
---\napiVersion: v1\nkind: Service\n\nmetadata:\n name: mailserver\n labels:\n app: mailserver\n annotations:\n metallb.universe.tf/address-pool: mailserver\n\n# ...\n\n---\napiVersion: metallb.io/v1beta1\nkind: IPAddressPool\n\nmetadata:\n name: mail\n namespace: metallb-system\n\nspec:\n addresses: [ <YOUR PUBLIC DEDICATED IP IN CIDR NOTATION> ]\n autoAssign: true\n\n---\napiVersion: metallb.io/v1beta1\nkind: L2Advertisement\n\nmetadata:\n name: mail\n namespace: metallb-system\n\nspec:\n ipAddressPools: [ mailserver ]\n
Advantages / Disadvantages Example
Using hostPort
and hostNetwork: true
is a similar approach to network_mode: host
with Docker Compose.
---\napiVersion: apps/v1\nkind: Deployment\n\nmetadata:\n name: mailserver\n\n# ...\n spec:\n hostNetwork: true\n # ...\n containers:\n # ...\n ports:\n - name: smtp\n containerPort: 25\n hostPort: 25\n - name: submissions\n containerPort: 465\n hostPort: 465\n - name: submission\n containerPort: 587\n hostPort: 587\n - name: imaps\n containerPort: 993\n hostPort: 993\n
Advantages / Disadvantages Service
PROXY protocol is a network protocol for preserving a client\u2019s IP address when the client\u2019s TCP connection passes through a proxy.
It is a common feature supported among reverse-proxy services (NGINX, HAProxy, Traefik), which you may already have handling ingress traffic for your cluster.
flowchart LR\n A(External Mail Server) -->|Incoming connection| B\n subgraph cluster\n B(\"Ingress Acting as a Proxy\") -->|PROXY protocol connection| C(DMS)\n end
For more information on the PROXY protocol, refer to our dedicated docs page on the topic.
Configure the Ingress Controller TraefikNGINXOn Traefik's side, the configuration is very simple.
IngressRouteTCP
that routes to the equivalent internal DMS Service
port which supports PROXY protocol connections.The below snippet demonstrates an example for two entrypoints, submissions
(port 465) and imaps
(port 993).
---\napiVersion: v1\nkind: Service\n\nmetadata:\n name: mailserver\n\nspec:\n # This an optimization to get rid of additional routing steps.\n # Previously \"type: LoadBalancer\"\n type: ClusterIP\n\n---\napiVersion: traefik.io/v1alpha1\nkind: IngressRouteTCP\n\nmetadata:\n name: smtp\n\nspec:\n entryPoints: [ submissions ]\n routes:\n - match: HostSNI(`*`)\n services:\n - name: mailserver\n namespace: mail\n port: subs-proxy # note the 15 character limit here\n proxyProtocol:\n version: 2\n\n---\napiVersion: traefik.io/v1alpha1\nkind: IngressRouteTCP\n\nmetadata:\n name: imaps\n\nspec:\n entryPoints: [ imaps ]\n routes:\n - match: HostSNI(`*`)\n services:\n - name: mailserver\n namespace: mail\n port: imaps-proxy\n proxyProtocol:\n version: 2\n
*-proxy
port name suffix
The IngressRouteTCP
example configs above reference ports with a *-proxy
suffix.
Deployment
manifest, and are scoped to the mailserver
service (via spec.routes.services.name
).With an NGINX ingress controller, add the following to the TCP services config map (as described here):
25: \"mailserver/mailserver:25::PROXY\"\n465: \"mailserver/mailserver:465::PROXY\"\n587: \"mailserver/mailserver:587::PROXY\"\n993: \"mailserver/mailserver:993::PROXY\"\n
Adjust DMS config for Dovecot + Postfix Only ingress should connect to DMS with PROXY protocol While Dovecot will restrict connections via PROXY protocol to only clients trusted configured via haproxy_trusted_networks
, Postfix does not have an equivalent setting. Public clients should always route through ingress to establish a PROXY protocol connection.
You are responsible for properly managing traffic inside your cluster and to ensure that only trustworthy entities can connect to the designated PROXY protocol ports.
With Kubernetes, this is usually the task of the CNI (container network interface).
Advised approach
The \"Separate PROXY protocol ports\" tab below introduces a little more complexity, but provides better compatibility for internal connections to DMS.
Only accept connections with PROXY protocolSeparate PROXY protocol ports for ingressConnections to DMS within the internal cluster will be rejected
The services for these ports can only enable PROXY protocol support by mandating the protocol on all connections for these ports.
This can be problematic when you also need to support internal cluster traffic directly to DMS (instead of routing indirectly through the ingress controller).
Here is an example configuration for Postfix, Dovecot, and the required adjustments for the Deployment
manifest. The port names are adjusted here only to convey the additional context described earlier.
kind: ConfigMap\napiVersion: v1\nmetadata:\n name: mailserver-extra-config\n labels:\n app: mailserver\ndata:\n postfix-main.cf: |\n postscreen_upstream_proxy_protocol = haproxy\n postfix-master.cf: |\n smtp/inet/postscreen_upstream_proxy_protocol=haproxy\n submission/inet/smtpd_upstream_proxy_protocol=haproxy\n submissions/inet/smtpd_upstream_proxy_protocol=haproxy\n dovecot.cf: |\n haproxy_trusted_networks = <YOUR POD CIDR>\n service imap-login {\n inet_listener imap {\n haproxy = yes\n }\n inet_listener imaps {\n haproxy = yes\n }\n }\n# ...\n\n---\nkind: Deployment\napiVersion: apps/v1\nmetadata:\n name: mailserver\nspec:\n template:\n spec:\n containers:\n - name: docker-mailserver\n # ...\n ports:\n - name: smtp-proxy\n containerPort: 25\n protocol: TCP\n - name: imap-proxy\n containerPort: 143\n protocol: TCP\n - name: subs-proxy\n containerPort: 465\n protocol: TCP\n - name: sub-proxy\n containerPort: 587\n protocol: TCP\n - name: imaps-proxy\n containerPort: 993\n protocol: TCP\n # ...\n volumeMounts:\n - name: config\n subPath: postfix-main.cf\n mountPath: /tmp/docker-mailserver/postfix-main.cf\n readOnly: true\n - name: config\n subPath: postfix-master.cf\n mountPath: /tmp/docker-mailserver/postfix-master.cf\n readOnly: true\n - name: config\n subPath: dovecot.cf\n mountPath: /tmp/docker-mailserver/dovecot.cf\n readOnly: true\n
Info
Supporting internal cluster connections to DMS without using PROXY protocol requires both Postfix and Dovecot to be configured with alternative ports for each service port (which only differ by enforcing PROXY protocol connections).
*-proxy
variants).In this example we'll create a copy of the original service ports with PROXY protocol enabled, and increment the port number assigned by 10000
.
Create a user-patches.sh
file to apply these config changes during container startup:
#!/bin/bash\n\n# Duplicate the config for the submission(s) service ports (587 / 465) with adjustments for the PROXY ports (10587 / 10465) and `syslog_name` setting:\npostconf -Mf submission/inet | sed -e s/^submission/10587/ -e 's/submission/submission-proxyprotocol/' >> /etc/postfix/master.cf\npostconf -Mf submissions/inet | sed -e s/^submissions/10465/ -e 's/submissions/submissions-proxyprotocol/' >> /etc/postfix/master.cf\n# Enable PROXY Protocol support for these new service variants:\npostconf -P 10587/inet/smtpd_upstream_proxy_protocol=haproxy\npostconf -P 10465/inet/smtpd_upstream_proxy_protocol=haproxy\n\n# Create a variant for port 25 too (NOTE: Port 10025 is already assigned in DMS to Amavis):\npostconf -Mf smtp/inet | sed -e s/^smtp/12525/ >> /etc/postfix/master.cf\n# Enable PROXY Protocol support (different setting as port 25 is handled via postscreen), optionally configure a `syslog_name` to distinguish in logs:\npostconf -P 12525/inet/postscreen_upstream_proxy_protocol=haproxy 12525/inet/syslog_name=smtp-proxyprotocol\n
For Dovecot, you can configure dovecot.cf
to look like this:
haproxy_trusted_networks = <YOUR POD CIDR>\n\nservice imap-login {\n inet_listener imap-proxied {\n haproxy = yes\n port = 10143\n }\n\n inet_listener imaps-proxied {\n haproxy = yes\n port = 10993\n ssl = yes\n }\n}\n
Update the Deployment
manifest ports
section by appending these new ports:
- name: smtp-proxy\n # not 10025 in this example due to a possible clash with Amavis\n containerPort: 12525\n protocol: TCP\n- name: imap-proxy\n containerPort: 10143\n protocol: TCP\n- name: subs-proxy\n containerPort: 10465\n protocol: TCP\n- name: sub-proxy\n containerPort: 10587\n protocol: TCP\n- name: imaps-proxy\n containerPort: 10993\n protocol: TCP\n
Note
If you use other Dovecot ports (110, 995, 4190), you may want to configure those similar to above. The dovecot.cf
config for these ports is documented here (in the equivalent section of that page).
To enable the fetchmail service to retrieve e-mails, set the environment variable ENABLE_FETCHMAIL
to 1
. Your compose.yaml
file should look like following snippet:
environment:\n - ENABLE_FETCHMAIL=1\n - FETCHMAIL_POLL=300\n
Generate a file called fetchmail.cf
and place it in the docker-data/dms/config/
folder. Your DMS folder should look like this example:
\u251c\u2500\u2500 docker-data/dms/config\n\u2502\u00a0\u00a0 \u251c\u2500\u2500 dovecot.cf\n\u2502\u00a0\u00a0 \u251c\u2500\u2500 fetchmail.cf\n\u2502\u00a0\u00a0 \u251c\u2500\u2500 postfix-accounts.cf\n\u2502\u00a0\u00a0 \u2514\u2500\u2500 postfix-virtual.cf\n\u2514\u2500\u2500 compose.yaml\n
"},{"location":"config/advanced/mail-fetchmail/#configuration","title":"Configuration","text":"Configuration options for fetchmail.cf
are covered at the official fetchmail docs (see the section \"The run control file\" and the table with \"keyword\" column for all settings).
Basic fetchmail.cf
configuration
Retrieve mail from remote-user@somewhere.com
and deliver it to dms-user@example.com
:
poll 'mail.somewhere.com'\nproto imap\nuser 'remote-user'\npass 'secret'\nis 'dms-user@example.com'\n
poll
sets the remote mail server to connect to retrieve mail from.proto
lets you connect via IMAP or POP3.user
and pass
provide the login credentials for the remote mail service account to access.is
configures where the fetched mail will be sent to (eg: your local DMS account in docker-data/dms/config/postfix-accounts.cf
).proto imap
will still delete remote mail once fetched This is due to a separate default setting no keep
. Adding the setting keep
to your config on a new line will prevent deleting the remote copy.
The official docs config examples show a common convention to indent settings on subsequent lines for visually grouping per server.
Minimal syntaxWith optional syntaxpoll 'mail.somewhere.com' proto imap\n user 'john.doe' pass 'secret' is 'johnny@example.com'\n user 'jane.doe' pass 'secret' is 'jane@example.com'\n\npoll 'mail.somewhere-else.com' proto pop3\n user 'john.doe@somewhere-else.com' pass 'secret' is 'johnny@example.com'\n
#
for adding comments.# Retrieve mail for users `john.doe` and `jane.doe` via IMAP at this remote mail server:\npoll 'mail.somewhere.com' with proto imap wants:\n user 'john.doe' with pass 'secret', is 'johnny@example.com' here\n user 'jane.doe' with pass 'secret', is 'jane@example.com' here\n\n# Also retrieve mail from this mail server (but via POP3).\n# NOTE: This could also be all on a single line, or with each key + value as a separate line.\n# Notice how the remote username includes a full email address,\n# Some mail servers like DMS use the full email address as the username:\npoll 'mail.somewhere-else.com' with proto pop3 wants:\n user 'john.doe@somewhere-else.com' with pass 'secret', is 'johnny@example.com' here\n
FETCHMAIL_POLL
ENV: Override default polling interval
By default the fetchmail service will check every 5 minutes for new mail at the configured mail accounts.
environment:\n # The fetchmail polling interval in seconds:\n FETCHMAIL_POLL: 60\n
"},{"location":"config/advanced/mail-fetchmail/#debugging","title":"Debugging","text":"To debug your fetchmail.cf
configuration run this setup debug
command:
docker exec -it dms-container-name setup debug fetchmail\n
Sample output of setup debug fetchmail
fetchmail: 6.3.26 querying outlook.office365.com (protocol POP3) at Mon Aug 29 22:11:09 2016: poll started\nTrying to connect to 132.245.48.18/995...connected.\nfetchmail: Server certificate:\nfetchmail: Issuer Organization: Microsoft Corporation\nfetchmail: Issuer CommonName: Microsoft IT SSL SHA2\nfetchmail: Subject CommonName: outlook.com\nfetchmail: Subject Alternative Name: outlook.com\nfetchmail: Subject Alternative Name: *.outlook.com\nfetchmail: Subject Alternative Name: office365.com\nfetchmail: Subject Alternative Name: *.office365.com\nfetchmail: Subject Alternative Name: *.live.com\nfetchmail: Subject Alternative Name: *.internal.outlook.com\nfetchmail: Subject Alternative Name: *.outlook.office365.com\nfetchmail: Subject Alternative Name: outlook.office.com\nfetchmail: Subject Alternative Name: attachment.outlook.office.net\nfetchmail: Subject Alternative Name: attachment.outlook.officeppe.net\nfetchmail: Subject Alternative Name: *.office.com\nfetchmail: outlook.office365.com key fingerprint: 3A:A4:58:42:56:CD:BD:11:19:5B:CF:1E:85:16:8E:4D\nfetchmail: POP3< +OK The Microsoft Exchange POP3 service is ready. [SABFADEAUABSADAAMQBDAEEAMAAwADAANwAuAGUAdQByAHAAcgBkADAAMQAuAHAAcgBvAGQALgBlAHgAYwBoAGEAbgBnAGUAbABhAGIAcwAuAGMAbwBtAA==]\nfetchmail: POP3> CAPA\nfetchmail: POP3< +OK\nfetchmail: POP3< TOP\nfetchmail: POP3< UIDL\nfetchmail: POP3< SASL PLAIN\nfetchmail: POP3< USER\nfetchmail: POP3< .\nfetchmail: POP3> USER user1@outlook.com\nfetchmail: POP3< +OK\nfetchmail: POP3> PASS *\nfetchmail: POP3< +OK User successfully logged on.\nfetchmail: POP3> STAT\nfetchmail: POP3< +OK 0 0\nfetchmail: No mail for user1@outlook.com at outlook.office365.com\nfetchmail: POP3> QUIT\nfetchmail: POP3< +OK Microsoft Exchange Server 2016 POP3 server signing off.\nfetchmail: 6.3.26 querying outlook.office365.com (protocol POP3) at Mon Aug 29 22:11:11 2016: poll completed\nfetchmail: normal termination, status 1\n
Troubleshoot with this reference compose.yaml
A minimal compose.yaml
example demonstrates how to run two instances of DMS locally, with one instance configured with fetchmail.cf
and the other to simulate a remote mail server to fetch from.
To enable the getmail service to retrieve e-mails set the environment variable ENABLE_GETMAIL
to 1
. Your compose.yaml
file should include the following:
environment:\n - ENABLE_GETMAIL=1\n - GETMAIL_POLL=5\n
In your DMS config volume (eg: docker-data/dms/config/
), add a subdirectory getmail/
for including your getmail config files (eg: imap-example.cf
) for each remote account that you want to retrieve mail from and deliver to the mailbox of a DMS account.
The content of these config files is documented in the next section with an IMAP and POP3 example to reference.
The directory structure should look similar to this:
\u251c\u2500\u2500 docker-data/dms/config\n\u2502\u00a0\u00a0 \u251c\u2500\u2500 dovecot.cf\n\u2502 \u251c\u2500\u2500 getmail\n\u2502\u00a0\u00a0 \u2502 \u251c\u2500\u2500 getmailrc_general.cf\n\u2502\u00a0\u00a0 \u2502 \u251c\u2500\u2500 remote-account1.cf\n\u2502\u00a0\u00a0 \u2502 \u251c\u2500\u2500 remote-account2.cf\n\u2502\u00a0\u00a0 \u251c\u2500\u2500 postfix-accounts.cf\n\u2502\u00a0\u00a0 \u2514\u2500\u2500 postfix-virtual.cf\n\u251c\u2500\u2500 docker-compose.yml\n\u2514\u2500\u2500 README.md\n
"},{"location":"config/advanced/mail-getmail/#configuration","title":"Configuration","text":"A detailed description of the configuration options can be found in the online version of the manual page.
"},{"location":"config/advanced/mail-getmail/#common-options","title":"Common Options","text":"The default options added to each getmail
config are:
[options]\nverbose = 0\nread_all = false\ndelete = false\nmax_messages_per_session = 500\nreceived = false\ndelivered_to = false\n
The DMS integration for Getmail generates a getmailrc
config that prepends the common options of the base config to each remote account config file (*.cf
) found in the DMS Config Volume getmail/
directory.
Change the base options
Add your own base config as getmail/getmailrc_general.cf
into the DMS Config Volume. It will replace the DMS defaults shown above.
This example will:
alice
with password notsecure
.user1@example.com
account that DMS manages.[retriever]\ntype = SimpleIMAPSSLRetriever\nserver = imap.gmail.com\nusername = alice\npassword = notsecure\n[destination]\ntype = MDA_external\npath = /usr/lib/dovecot/deliver\nallow_root_commands = true\narguments =(\"-d\",\"user1@example.com\")\n
POP3 Configuration Just like the IMAP example above, but instead via POP3 protocol if you prefer that over IMAP.
[retriever]\ntype = SimplePOP3SSLRetriever\nserver = pop3.gmail.com\nusername = alice\npassword = notsecure\n[destination]\ntype = MDA_external\npath = /usr/lib/dovecot/deliver\nallow_root_commands = true\narguments =(\"-d\",\"user1@example.com\")\n
"},{"location":"config/advanced/mail-getmail/#polling-interval","title":"Polling Interval","text":"By default the getmail
service checks external mail accounts for new mail every 5 minutes. That polling interval is configurable via the GETMAIL_POLL
ENV variable, with a value in minutes (default: 5, min: 1):
environment:\n - GETMAIL_POLL=1\n
"},{"location":"config/advanced/mail-getmail/#xoauth2-authentication","title":"XOAUTH2 Authentication","text":"It is possible to utilize the getmail-gmail-xoauth-tokens
helper to provide authentication using xoauth2
for gmail (example 12) or Microsoft Office 365 (example 13)
Advice may be outdated
This section was contributed by the community some time ago and some configuration examples may be outdated.
Sieve allows to specify filtering rules for incoming emails that allow for example sorting mails into different folders depending on the title of an email.
Global vs User order
There are global and user specific filters which are filtering the incoming emails in the following order:
Global-before -> User specific -> Global-after
Global filters are applied to EVERY incoming mail for EVERY email address.
docker-data/dms/config/before.dovecot.sieve
or a docker-data/dms/config/after.dovecot.sieve
file with your filter rules.To specify a user-defined Sieve filter place a .dovecot.sieve
file into a virtual user's mail folder (e.g. /var/mail/example.com/user1/home/.dovecot.sieve
). If this file exists dovecot will apply the filtering rules.
It's even possible to install a user provided Sieve filter at startup during users setup: simply include a Sieve file in the docker-data/dms/config/
path for each user login that needs a filter. The file name provided should be in the form <user_login>.dovecot.sieve
, so for example for user1@example.com
you should provide a Sieve file named docker-data/dms/config/user1@example.com.dovecot.sieve
.
An example of a sieve filter that moves mails to a folder INBOX/spam
depending on the sender address:
Example
require [\"fileinto\", \"reject\"];\n\nif address :contains [\"From\"] \"spam@spam.com\" {\n fileinto \"INBOX.spam\";\n} else {\n keep;\n}\n
Warning
That folders have to exist beforehand if sieve should move them.
Another example of a sieve filter that forward mails to a different address:
Example
require [\"copy\"];\n\nredirect :copy \"user2@not-example.com\";\n
Just forward all incoming emails and do not save them locally:
Example
redirect \"user2@not-example.com\";\n
You can also use external programs to filter or pipe (process) messages by adding executable scripts in docker-data/dms/config/sieve-pipe
or docker-data/dms/config/sieve-filter
.
This can be used in lieu of a local alias file, for instance to forward an email to a webservice.
require [\"vnd.dovecot.pipe\"];\npipe \"external-program\";\n
For more examples or a detailed description of the Sieve language have a look at the official site. Other resources are available on the internet where you can find several examples.
"},{"location":"config/advanced/mail-sieve/#subaddress-mailbox-routing","title":"Automatic Sorting Based on Sub-addresses","text":"When mail is delivered to your account, it is possible to organize storing mail into folders by the subaddress (tag) used.
Example: user+<tag>@example.com
to INBOX/<Tag>
This example sorts mail into inbox folders by their tag:
docker-data/dms/config/user@example.com.dovecot.sieverequire [\"envelope\", \"fileinto\", \"mailbox\", \"subaddress\", \"variables\"];\n\n# Check if the mail recipient address has a tag (:detail)\nif envelope :detail :matches \"to\" \"*\" {\n # Create a variable `tag`, with the the captured `to` value normalized (SoCIAL => Social)\n set :lower :upperfirst \"tag\" \"${1}\";\n\n # Store the mail into a folder with the tag name, nested under your inbox folder:\n if mailboxexists \"INBOX.${tag}\" {\n fileinto \"INBOX.${tag}\";\n } else {\n fileinto :create \"INBOX.${tag}\";\n }\n}\n
When receiving mail for user+social@example.com
it would be delivered into the INBOX/Social
folder.
If you want to only handle specific tags, you could replace the envelope condition and tag assignment from the prior example with:
docker-data/dms/config/user@example.com.dovecot.sieve# Instead of `:matches`, use the default comparator `:is` (exact match)\nif envelope :detail \"to\" \"social\" {\n set \"tag\" \"Social\";\n
docker-data/dms/config/user@example.com.dovecot.sieve# Alternatively you can also provide a list of values to match:\nif envelope :detail \"to\" [\"azure\", \"aws\"] {\n set \"tag\" \"Cloud\";\n
docker-data/dms/config/user@example.com.dovecot.sieve# Similar to `:matches`, except `:regex` provides enhanced pattern matching.\n# NOTE: This example needs you to `require` the \"regex\" extension\nif envelope :detail :regex \"to\" \"^cloud-(azure|aws)$\" {\n # Normalize the captured azure/aws tag as the resolved value is no longer fixed:\n set :lower :upperfirst \"vendor\" \"${1}\";\n # If a `.` exists in the tag, it will create nested folders:\n set \"tag\" \"Cloud.${vendor}\";\n
NOTE: There is no need to lowercase the tag in the conditional as the to
value is a case-insensitive check.
recipient_delimiter
(default: +
) configures the tag delimiter. This is where the local-part
of the recipient address will split at, providing the :detail
(tag) value for Sieve.INBOX
is the default namespace configured by Dovecot.
INBOX.
prefix from the sieve script above, the mailbox (folder) for that tag is created at the top-level alongside your Trash and Junk folders..
between INBOX
and ${tag}
is important as a separator to distinguish mailbox names. This can vary by mailbox format or configuration. DMS uses Maildir
by default, which uses .
as the separator.lmtp_save_to_detail_mailbox = yes
can be set in /etc/dovecot/conf.d/20-lmtp.conf
:INBOX.
prefix parts of the example script.The Manage Sieve extension allows users to modify their Sieve script by themselves. The authentication mechanisms are the same as for the main dovecot service. ManageSieve runs on port 4190
and needs to be enabled using the ENABLE_MANAGESIEVE=1
environment variable.
Example
compose.yamlports:\n - \"4190:4190\"\nenvironment:\n - ENABLE_MANAGESIEVE=1\n
All user defined sieve scripts that are managed by ManageSieve are stored in the user's home folder in /var/mail/example.com/user1/home/sieve
. Just one Sieve script might be active for a user and is sym-linked to /var/mail/example.com/user1/home/.dovecot.sieve
automatically.
Note
ManageSieve makes sure to not overwrite an existing .dovecot.sieve
file. If a user activates a new sieve script the old one is backed up and moved to the sieve
folder.
The extension is known to work with the following ManageSieve clients:
DMS has several locations in the container which may be worth persisting externally via Docker Volumes.
docker-data/dms/
for grouping these related volumes.Reference - Volmes for DMS
Our docs may refer to these DMS specific volumes only by name, or the host/container path for brevity.
docker-data/dms/config/
=> /tmp/docker-mailserver/
docker-data/dms/mail-data/
=> /var/mail/
docker-data/dms/mail-state/
=> /var/mail-state/
docker-data/dms/mail-logs/
=> /var/log/mail/
This is the location where mail is delivered to your mailboxes.
"},{"location":"config/advanced/optional-config/#volumes-state","title":"State Volume","text":"Run-time specific state lives here, but so does some data you may want to keep if a failure event occurs (crash, power loss).
Examples of relevant data
When a volume is mounted to /var/mail-state/
/var/mail-state/
directory. Otherwise the original locations vary and would need to be mounted individually./var/mail-state/
(eg: /var/lib/redis
=> /var/mail-state/lib-redis/
).Supported services: Postfix, Dovecot, Fail2Ban, Amavis, PostGrey, ClamAV, SpamAssassin, Rspamd & Redis, Fetchmail, Getmail, LogRotate, PostSRSd, MTA-STS.
Tip
Sometimes it is helpful to disable this volume when troubleshooting to verify if the data stored here is in a bad state (eg: caused by a failure event).
"},{"location":"config/advanced/optional-config/#volumes-log","title":"Logs Volume","text":"This can be a useful volume to persist for troubleshooting needs for the full set of log files.
"},{"location":"config/advanced/optional-config/#volumes-config","title":"Config Volume","text":"Most configuration files for Postfix, Dovecot, etc. are persisted here.
This is a list of all configuration files and directories which are optional, automatically generated / updated by our setup
CLI, or other internal scripts.
setup.sh config dkim
. (Docs: DKIM)SSL_TYPE
is set to self-signed
or custom
. (Docs: SSL)${login}.dovecot.sieve
filter. (Docs: Sieve)${login}.dovecot.sieve
filter. (Docs: Sieve)setup.sh email
script.setup.sh email restrict
.setup.sh email restrict
.setup.sh alias
.<username>:<password>
. Modify via setup.sh relay add-auth <domain> <username> [<password>]
. (Docs: Relay-Hosts Auth)setup.sh relay add-domain
and setup.sh relay exclude-domain
. (Docs: Relay-Hosts Senders)virtual_mailbox_maps
. See the setup-stack.sh
script.virtual_alias_maps
. See the setup-stack.sh
script.virtual_alias_maps
. See the setup-stack.sh
script.virtual_mailbox_domains
. See the setup-stack.sh
script.fail2ban.cf
. (Docs: Fail2Ban)/etc/amavis/conf.d/50-user
file/etc/dovecot/local.conf
. (Docs: Override Dovecot Defaults)user-patches.sh
script)Podman is a daemonless container engine for developing, managing, and running OCI Containers on your Linux System.
About Support for Podman
Please note that Podman is not officially supported as DMS is built and verified on top of the Docker Engine. This content is entirely community supported. If you find errors, please open an issue and provide a PR.
About this Guide
This guide was tested with Fedora 34 using systemd
and firewalld
. Moreover, it requires Podman version >= 3.2. You may be able to substitute dnf
- Fedora's package manager - with others such as apt
.
About Security
Running podman in rootless mode requires additional modifications in order to keep your mailserver secure. Make sure to read the related documentation.
"},{"location":"config/advanced/podman/#installation-in-rootfull-mode","title":"Installation in Rootfull Mode","text":"While using Podman, you can just manage docker-mailserver as what you did with Docker. Your best friend setup.sh
includes the minimum code in order to support Podman since it's 100% compatible with the Docker CLI.
The installation is basically the same. Podman v3.2 introduced a RESTful API that is 100% compatible with the Docker API, so you can use Docker Compose with Podman easily. Install Podman and Docker Compose with your package manager first.
sudo dnf install podman docker-compose\n
Then enable podman.socket
using systemctl
.
systemctl enable --now podman.socket\n
This will create a unix socket locate under /run/podman/podman.sock
, which is the entrypoint of Podman's API. Now, configure docker-mailserver and start it.
export DOCKER_HOST=\"unix:///run/podman/podman.sock\"\ndocker compose up -d mailserver\ndocker compose ps\n
You should see that docker-mailserver is running now.
"},{"location":"config/advanced/podman/#self-start-in-rootfull-mode","title":"Self-start in Rootfull Mode","text":"Podman is daemonless, that means if you want docker-mailserver self-start while boot up the system, you have to generate a systemd file with Podman CLI.
podman generate systemd mailserver > /etc/systemd/system/mailserver.service\nsystemctl daemon-reload\nsystemctl enable --now mailserver.service\n
"},{"location":"config/advanced/podman/#installation-in-rootless-mode","title":"Installation in Rootless Mode","text":"Running rootless containers is one of Podman's major features. But due to some restrictions, deploying docker-mailserver in rootless mode is not as easy compared to rootfull mode.
~/.config
Also notice that Podman's rootless mode is not about running as a non-root user inside the container, but about the mapping of (normal, non-root) host users to root inside the container.
Warning
In order to make rootless DMS work we must modify some settings in the Linux system, it requires some basic linux server knowledge so don't follow this guide if you not sure what this guide is talking about. Podman rootfull mode and Docker are still good and security enough for normal daily usage.
First, enable podman.socket
in systemd's userspace with a non-root user.
systemctl enable --now --user podman.socket\n
The socket file should be located at /var/run/user/$(id -u)/podman/podman.sock
. Then, modify compose.yaml
to make sure all ports are bindings are on non-privileged ports.
services:\n mailserver:\n ports:\n - \"10025:25\" # SMTP (explicit TLS => STARTTLS)\n - \"10143:143\" # IMAP4 (explicit TLS => STARTTLS)\n - \"10465:465\" # ESMTP (implicit TLS)\n - \"10587:587\" # ESMTP (explicit TLS => STARTTLS)\n - \"10993:993\" # IMAP4 (implicit TLS)\n
Then, setup your mailserver.env
file follow the documentation and use Docker Compose to start the container.
export DOCKER_HOST=\"unix:///var/run/user/$(id -u)/podman/podman.sock\"\ndocker compose up -d mailserver\ndocker compose ps\n
"},{"location":"config/advanced/podman/#security-in-rootless-mode","title":"Security in Rootless Mode","text":"In rootless mode, podman resolves all incoming IPs as localhost, which results in an open gateway in the default configuration. There are two workarounds to fix this problem, both of which have their own drawbacks.
"},{"location":"config/advanced/podman/#enforce-authentication-from-localhost","title":"Enforce authentication from localhost","text":"The PERMIT_DOCKER
variable in the mailserver.env
file allows to specify trusted networks that do not need to authenticate. If the variable is left empty, only requests from localhost and the container IP are allowed, but in the case of rootless podman any IP will be resolved as localhost. Setting PERMIT_DOCKER=none
enforces authentication also from localhost, which prevents sending unauthenticated emails.
The second workaround is slightly more complicated because the compose.yaml
has to be modified. As shown in the fail2ban section the slirp4netns
network driver has to be enabled. This network driver enables podman to correctly resolve IP addresses but it is not compatible with user defined networks which might be a problem depending on your setup.
Rootless Podman requires adding the value slirp4netns:port_handler=slirp4netns
to the --network
CLI option, or network_mode
setting in your compose.yaml
.
You must also add the ENV NETWORK_INTERFACE=tap0
, because Podman uses a hard-coded interface name for slirp4netns
.
Example
services:\n mailserver:\n network_mode: \"slirp4netns:port_handler=slirp4netns\"\n environment:\n - NETWORK_INTERFACE=tap0\n ...\n
Note
podman-compose
is not compatible with this configuration.
Generate a systemd file with the Podman CLI.
podman generate systemd mailserver > ~/.config/systemd/user/mailserver.service\nsystemctl --user daemon-reload\nsystemctl enable --user --now mailserver.service\n
Systemd's user space service is only started when a specific user logs in and stops when you log out. In order to make it to start with the system, we need to enable linger with loginctl
loginctl enable-linger <username>\n
Remember to run this command as root user.
"},{"location":"config/advanced/podman/#port-forwarding","title":"Port Forwarding","text":"When it comes to forwarding ports using firewalld
, see these port forwarding docs for more information.
firewall-cmd --permanent --add-forward-port=port=<25|143|465|587|993>:proto=<tcp>:toport=<10025|10143|10465|10587|10993>\n...\n\n# After you set all ports up.\nfirewall-cmd --reload\n
Notice that this will only open the access to the external client. If you want to access privileges port in your server, do this:
firewall-cmd --permanent --direct --add-rule <ipv4|ipv6> nat OUTPUT 0 -p <tcp|udp> -o lo --dport <25|143|465|587|993> -j REDIRECT --to-ports <10025|10143|10465|10587|10993>\n...\n# After you set all ports up.\nfirewall-cmd --reload\n
Just map all the privilege port with non-privilege port you set in compose.yaml before as root user.
"},{"location":"config/advanced/mail-forwarding/aws-ses/","title":"Mail Forwarding | AWS SES","text":"Amazon SES (Simple Email Service) provides a simple way for cloud based applications to send and receive email.
Configuration via ENV
Configure a relay host in DMS to forward all your mail through AWS SES:
RELAY_HOST
should match your AWS SES region.RELAY_PORT
should be set to one of the supported AWS SES SMTP ports (eg: 587 for STARTTLS).RELAY_USER
and RELAY_PASSWORD
should be set to your Amazon SES SMTP credentials.RELAY_HOST=email-smtp.us-west-2.amazonaws.com\nRELAY_PORT=587\n# Alternative to RELAY_HOST + RELAY_PORT which is compatible with LDAP:\nDEFAULT_RELAY_HOST=[email-smtp.us-west-2.amazonaws.com]:587\n\nRELAY_USER=aws-user\nRELAY_PASSWORD=secret\n
Tip
If you have set up AWS Easy DKIM, you can safely skip setting up DKIM as AWS SES will take care of signing your outbound mail.
Verify the relay host is configured correctly
To verify proper operation, send an email to some external account of yours and inspect the mail headers.
You will also see the connection to SES in the mail logs:
postfix/smtp[692]: Trusted TLS connection established to email-smtp.us-west-1.amazonaws.com[107.20.142.169]:25:\n TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)\npostfix/smtp[692]: 8C82A7E7: to=<someone@example.com>, relay=email-smtp.us-west-1.amazonaws.com[107.20.142.169]:25,\n delay=0.35, delays=0/0.02/0.13/0.2, dsn=2.0.0, status=sent (250 Ok 01000154dc729264-93fdd7ea-f039-43d6-91ed-653e8547867c-000000)\n
"},{"location":"config/advanced/mail-forwarding/gmail-smtp/","title":"Mail Forwarding | Configure Gmail as a relay host","text":"This page provides a guide for configuring DMS to use GMAIL as an SMTP relay host.
Configuration via ENV
Configure a relay host in DMS. This example shows how the related ENV settings map to the Gmail service config:
RELAY_HOST
should be configured as advised by Gmail, there are two SMTP endpoints to choose:smtp.gmail.com
(for a personal Gmail account)smtp-relay.gmail.com
(when using Google Workspace)RELAY_PORT
should be set to one of the supported Gmail SMTP ports (eg: 587 for STARTTLS).RELAY_USER
should be your gmail address (user@gmail.com
).RELAY_PASSWORD
should be your App Password, not your personal gmail account password.RELAY_HOST=smtp.gmail.com\nRELAY_PORT=587\n# Alternative to RELAY_HOST + RELAY_PORT which is compatible with LDAP:\nDEFAULT_RELAY_HOST=[smtp.gmail.com]:587\n\nRELAY_USER=username@gmail.com\nRELAY_PASSWORD=secret\n
Tip
setup relay add-auth
instead of the RELAY_USER
+ RELAY_PASSWORD
ENV.smtp-relay.gmail.com
, the DEFAULT_RELAY_HOST
ENV should be all you need as shown in the above example. Credentials can be optional when using Google Workspace (smtp-relay.gmail.com
), which supports restricting connections to trusted IP addresses.Verify the relay host is configured correctly
To verify proper operation, send an email to an external account of yours and inspect the mail headers.
You will also see the connection to the Gmail relay host (smtp.gmail.com
) in the mail logs:
postfix/smtp[910]: Trusted TLS connection established to smtp.gmail.com[64.233.188.109]:587:\n TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits)\npostfix/smtp[910]: 4BCB547D9D: to=<username@gmail.com>, relay=smtp.gmail.com[64.233.188.109]:587,\n delay=2.9, delays=0.01/0.02/1.7/1.2, dsn=2.0.0, status=sent (250 2.0.0 OK 17... - gsmtp)\n
"},{"location":"config/advanced/mail-forwarding/relay-hosts/","title":"Mail Forwarding | Relay Hosts","text":""},{"location":"config/advanced/mail-forwarding/relay-hosts/#what-is-a-relay-host","title":"What is a Relay Host?","text":"An SMTP relay service (aka relay host / smarthost) is an MTA that relays (forwards) mail on behalf of third-parties (it does not manage the mail domains).
When can a relay service can be helpful?
All mail sent outbound from DMS (where the sender address is a DMS account or a virtual alias) will be relayed through the configured relay host.
Configuration via ENV
Configure the default relayhost with either of these ENV:
DEFAULT_RELAY_HOST
(eg: [mail.relay-service.com]:25
)RELAY_HOST
(eg: mail.relay-service.com
) + RELAY_PORT
(default: 25)Most relay services also require authentication configured:
RELAY_USER
+ RELAY_PASSWORD
provides credentials for authenticating with the default relayhost.Providing secrets via ENV
While ENV is convenient, the risk of exposing secrets is higher.
setup relay add-auth
is a better alternative, which manages the credentials via a config file.
You can opt-out with: setup relay exclude-domain <domain>
Outbound mail from senders of that domain will be sent normally (instead of through the configured RELAY_HOST
).
When any relay host credentials are configured
It will still be expected that mail is sent over a secure connection with credentials provided.
Thus this opt-out feature is rarely practical.
"},{"location":"config/advanced/mail-forwarding/relay-hosts/#advanced-configuration","title":"Advanced Configuration","text":"When mail is sent, there is support to change the relay service or the credentials configured based on the sender address domain used.
We provide this support via two config files:
docker-data/dms/config/postfix-relaymap.cf
docker-data/dms/config/postfix-sasl-password.cf
Configure with our setup relay
commands
While you can edit those configs directly, DMS provides these helpful config management commands:
# Configure a sender domain to use a specific relay host:\nsetup relay add-domain <domain> <host> [<port>]\n\n# Configure relay host credentials for a sender domain to use:\nsetup relay add-auth <domain> <username> [<password>]\n\n# Optionally avoid relaying from senders of this domain:\n# NOTE: Only supported when configured with the `RELAY_HOST` ENV!\nsetup relay exclude-domain <domain>\n
Config file: postfix-sasl-password.cf
@domain1.com mailgun-user:secret\n@domain2.com sendgrid-user:secret\n\n# NOTE: This must have an exact match with the relay host in `postfix-relaymap.cf`,\n# `/etc/postfix/relayhost_map`, or the `DEFAULT_RELAY_HOST` ENV.\n# NOTE: Not supported via our setup CLI, but valid config for Postfix.\n[email-smtp.us-west-2.amazonaws.com]:2587 aws-user:secret\n
When Postfix needs to lookup credentials for mail sent outbound, the above config will:
mailgun-user
for mail sent with a sender belonging to @domain1.com
sendgrid-user
for mail sent with a sender belonging to @domain2.com
aws-user
for mail sent through a configured AWS SES relay host (any sender domain).Config file: postfix-relaymap.cf
@domain1.com [smtp.mailgun.org]:587\n@domain2.com [smtp.sendgrid.net]:2525\n\n# Opt-out of relaying:\n@domain3.com\n
When Postfix sends mail outbound from these sender domains, the above config will:
[smtp.mailgun.org]:587
when mail is sent from a sender of @domain1.com
[smtp.sendgrid.net]:2525
when mail is sent from a sender of @domain1.com
@domain3.com
is not sent through a relay (Only applicable when using RELAY_HOST
)compose.yaml
config with several DMS instances demonstrates this feature for local testing.Postfix Settings
Internally this feature is implemented in DMS by relay.sh
.
The relay.sh
script manages configuring these Postfix settings:
# Send all outbound mail through this relay service:\nrelayhost = [smtp.relay-service.com]:587\n\n# Credentials to use:\nsmtp_sasl_password_maps = texthash:/etc/postfix/sasl_passwd\n# Alternative table type examples which do not require a separate file:\n#smtp_sasl_password_maps = static:john.doe@relay-service.com:secret\n#smtp_sasl_password_maps = inline:{ [smtp.relay-service.com]:587=john.doe@relay-service.com:secret }\n\n## Authentication support:\n# Required to provide credentials to the relay service:\nsmtp_sasl_auth_enable = yes\n# Enforces requiring credentials when sending mail outbound:\nsmtp_sasl_security_options = noanonymous\n# Enforces a secure connection (TLS required) to the relay service:\nsmtp_tls_security_level = encrypt\n\n## Support for advanced requirements:\n# Relay service(s) to use instead of direct delivery for specific sender domains:\nsender_dependent_relayhost_maps = texthash:/etc/postfix/relayhost_map\n# Support credentials to a relay service(s) that vary by relay host used or sender domain:\nsmtp_sender_dependent_authentication = yes\n
"},{"location":"config/advanced/maintenance/update-and-cleanup/","title":"Maintenance | Update and Cleanup","text":"containrrr/watchtower
is a service that monitors Docker images for updates, automatically applying them to running containers.
Automatic image updates + cleanup
Run a watchtower
container with access to docker.sock
, enabling the service to manage Docker:
services:\n watchtower:\n image: containrrr/watchtower:latest\n # Automatic cleanup (removes older image pulls from wasting disk space):\n environment:\n - WATCHTOWER_CLEANUP=true\n volumes:\n - /var/run/docker.sock:/var/run/docker.sock\n
The image tag used for a container is monitored for updates (eg: :latest
, :edge
, :13
)
The automatic update support is only for updates to that specific image tag.
:latest
).13
will represent the latest minor + patch release of v13
).Updating only specific containers
By default the watchtower
service will check every 24 hours for new image updates to pull, based on currently running containers (not restricted to only those running within your compose.yaml
).
Images eligible for updates can configured with a custom command
that provides a list of container names, or via other supported options (eg: labels). This configuration is detailed in the watchtower
docs.
Manual cleanup
watchtower
also supports running on-demand with docker run
or compose.yaml
via the --run-once
option.
You can alternatively invoke cleanup of Docker storage directly with:
docker image prune --all
docker system prune --all
(also removes unused containers, networks, build cache).If you omit the --all
option, this will instead only remove \"dangling\" content (eg: Orphaned images).
The Dovecot default configuration can easily be extended providing a docker-data/dms/config/dovecot.cf
file. Dovecot documentation remains the best place to find configuration options.
Your DMS folder structure should look like this example:
\u251c\u2500\u2500 docker-data/dms/config\n\u2502 \u251c\u2500\u2500 dovecot.cf\n\u2502 \u251c\u2500\u2500 postfix-accounts.cf\n\u2502 \u2514\u2500\u2500 postfix-virtual.cf\n\u251c\u2500\u2500 compose.yaml\n\u2514\u2500\u2500 README.md\n
One common option to change is the maximum number of connections per user:
mail_max_userip_connections = 100\n
Another important option is the default_process_limit
(defaults to 100
). If high-security mode is enabled you'll need to make sure this count is higher than the maximum number of users that can be logged in simultaneously.
This limit is quickly reached if users connect to DMS with multiple end devices.
"},{"location":"config/advanced/override-defaults/dovecot/#override-configuration","title":"Override Configuration","text":"For major configuration changes it\u2019s best to override the dovecot configuration files. For each configuration file you want to override, add a list entry under the volumes
key.
services:\n mailserver:\n volumes:\n - ./docker-data/dms/mail-data/:/var/mail/\n - ./docker-data/dms/config/dovecot/10-master.conf:/etc/dovecot/conf.d/10-master.conf\n
You will first need to obtain the configuration from the running container (where mailserver
is the container name):
mkdir -p ./docker-data/dms/config/dovecot\ndocker cp mailserver:/etc/dovecot/conf.d/10-master.conf ./docker-data/dms/config/dovecot/10-master.conf\n
"},{"location":"config/advanced/override-defaults/dovecot/#debugging","title":"Debugging","text":"To debug your dovecot configuration you can use:
./setup.sh debug login doveconf | grep <some-keyword>
docker exec -it mailserver doveconf | grep <some-keyword>
Note
setup.sh
is included in the DMS repository. Make sure to use the one matching your image version release.
The file docker-data/dms/config/dovecot.cf
is copied internally to /etc/dovecot/local.conf
. To verify the file content, run:
docker exec -it mailserver cat /etc/dovecot/local.conf\n
"},{"location":"config/advanced/override-defaults/postfix/","title":"Override the Default Configs | Postfix","text":"Our default Postfix configuration can easily be extended to add parameters or modify existing ones by providing a docker-data/dms/config/postfix-main.cf
. This file uses the same format as Postfix main.cf
does (See official docs for all parameters and syntax rules).
Example
One can easily increase the backwards-compatibility level and set new Postscreen options:
# increase the compatibility level from 2 (default) to 3\ncompatibility_level = 3\n# set a threshold value for Spam detection\npostscreen_dnsbl_threshold = 4\n
How are your changes applied?
The custom configuration you supply is appended to the default configuration located at /etc/postfix/main.cf
, and then postconf -nf
is run to remove earlier duplicate entries that have since been replaced. This happens early during container startup before Postfix is started.
Similarly, it is possible to add a custom docker-data/dms/config/postfix-master.cf
file that will override the standard master.cf
. Note: Each line in this file will be passed to postconf -P
, i.e. the file is not appended as a whole to /etc/postfix/master.cf
like docker-data/dms/config/postfix-main.cf
! The expected format is <service_name>/<type>/<parameter>
, for example:
# adjust the submission \"reject_unlisted_recipient\" option\nsubmission/inet/smtpd_reject_unlisted_recipient=no\n
Attention
There should be no space between the parameter and the value.
Run postconf -Mf
in the container without arguments to see the active master options.
If you'd like to change, patch or alter files or behavior of DMS, you can use a script.
In case you cloned this repository, you can copy the file user-patches.sh.dist
(under config/
) with cp config/user-patches.sh.dist docker-data/dms/config/user-patches.sh
in order to create the user-patches.sh
script.
If you are managing your directory structure yourself, create a docker-data/dms/config/
directory and add the user-patches.sh
file yourself.
# 1. Either create the docker-data/dms/config/ directory yourself\n# or let docker-mailserver create it on initial startup\n/tmp $ mkdir -p docker-data/dms/config/ && cd docker-data/dms/config/\n\n# 2. Create the user-patches.sh file and edit it\n/tmp/docker-data/dms/config $ touch user-patches.sh\n/tmp/docker-data/dms/config $ nano user-patches.sh\n
The contents could look like this:
#!/bin/bash\n\ncat >/etc/amavis/conf.d/50-user << \"END\"\nuse strict;\n\n$undecipherable_subject_tag = undef;\n$admin_maps_by_ccat{+CC_UNCHECKED} = undef;\n\n#------------ Do not modify anything below this line -------------\n1; # ensure a defined return\nEND\n
And you're done. The user patches script runs right before starting daemons. That means, all the other configuration is in place, so the script can make final adjustments.
Note
Many \"patches\" can already be done with the Docker Compose-/Stack-file. Adding hostnames to /etc/hosts
is done with the extra_hosts:
section, sysctl
commands can be managed with the sysctls:
section, etc.
Email auto-discovery means a client email is able to automagically find out about what ports and security options to use, based on the mail server URI. It can help simplify the tedious / confusing task of adding own's email account for non-tech savvy users.
Email clients will search for auto-discoverable settings and prefill almost everything when a user enters its email address
There exists autodiscover-email-settings on which provides IMAP/POP/SMTP/LDAP autodiscover capabilities on Microsoft Outlook/Apple Mail, autoconfig capabilities for Thunderbird or kmail and configuration profiles for iOS/Apple Mail.
"},{"location":"config/best-practices/dkim_dmarc_spf/","title":"DKIM, DMARC & SPF","text":"Cloudflare has written an article about DKIM, DMARC and SPF that we highly recommend you to read to get acquainted with the topic.
Rspamd vs Individual validators
With v12.0.0, Rspamd was integrated into DMS. It can perform validations for DKIM, DMARC and SPF as part of the spam-score-calculation
for an email. DMS provides individual alternatives for each validation that can be used instead of deferring to Rspamd:
opendkim
is used as a milter (like Rspamd)opendmarc
is used as a milter (like Rspamd)policyd-spf
is used in Postfix's smtpd_recipient_restrictions
In a future release Rspamd will become the default for these validations, with a deprecation notice issued prior to the removal of the above alternatives.
We encourage everyone to prefer Rspamd via ENABLE_RSPAMD=1
.
DNS Caches & Propagation
While modern DNS providers are quick, it may take minutes or even hours for new DNS records to become available / propagate.
"},{"location":"config/best-practices/dkim_dmarc_spf/#dkim","title":"DKIM","text":"What is DKIM
DomainKeys Identified Mail (DKIM) is an email authentication method designed to detect forged sender addresses in email (email spoofing), a technique often used in phishing and email spam.
Source
When DKIM is enabled:
DKIM requires a public/private key pair to enable signing (via private key) your outgoing mail, while the receiving end must query DNS to verify (via public key) that the signature is trustworthy.
Verification expiryUnlike your TLS certificate, your DKIM keypair does not have a fixed expiry associated to it.
Instead, an expiry may be included in your DKIM signature for each mail sent, where a receiver will refuse to validate the signature for an email after that expiry date. This is an added precaution to mitigate malicious activity like \"DKIM replay attacks\", where an already delivered email from a third-party with a trustworthy DKIM signature is leveraged by a spammer when sending mail to an MTA which verifies the DKIM signature successfully, enabling the spammer to bypass spam protections.
Unlike a TLS handshake where you are authenticating trust with future communications, with DKIM once the mail has been received and trust of the signature has been verified, the value of verifying the signature again at a later date is less meaningful since the signature was to ensure no tampering had occurred during delivery through the network.
DKIM key rotationYou can rotate your DKIM keypair by switching to a new DKIM selector (and DNS updates), while the previous key and selector remains valid for verification until the last mail signed with that key reaches it's expiry.
DMS does not provide any automation or support for key rotation, nor is it likely to provide a notable security benefit to the typical small scale DMS deployment.
"},{"location":"config/best-practices/dkim_dmarc_spf/#generating-keys","title":"Generating Keys","text":"You'll need to repeat this process if you add any new domains.
You should have:
Creating DKIM Keys
DKIM keys can be generated with good defaults by running:
docker exec -it <CONTAINER NAME> setup config dkim\n
If you need to generate your keys with different settings, check the help
output for supported config options and examples:
docker exec -it <CONTAINER NAME> setup config dkim help\n
As described by the help output, you may need to use the domain
option explicitly when you're using LDAP or Rspamd.
The keypair generated for using with DKIM presently defaults to RSA-2048. This is a good size but you can lower the security to 1024-bit
, or increase it to 4096-bit
(discouraged as that is excessive).
To generate a key with different size (for RSA 1024-bit) run:
setup config dkim keysize 1024\n
RSA Key Sizes >= 4096 Bit
According to RFC 8301, keys are preferably between 1024 and 2048 bits. Keys of size 4096-bit or larger may not be compatible to all systems your mail is intended for.
You should not need a key length beyond 2048-bit. If 2048-bit does not meet your security needs, you may want to instead consider adopting key rotation or switching from RSA to ECC keys for DKIM.
You may need to specify mail domains explicitlyRequired when using LDAP and Rspamd.
setup config dkim
will generate DKIM keys for what is assumed as the primary mail domain (derived from the FQDN assigned to DMS, minus any subdomain).
When the DMS FQDN is mail.example.com
or example.com
, by default this command will generate DKIM keys for example.com
as the primary domain for your users mail accounts (eg: hello@example.com
).
The DKIM generation does not have support to query LDAP for additional mail domains it should know about. If the primary mail domain is not sufficient, then you must explicitly specify any extra domains via the domain
option:
# ENABLE_OPENDKIM=1 (default):\nsetup config dkim domain 'example.com,another-example.com'\n\n# ENABLE_RSPAMD=1 + ENABLE_OPENDKIM=0:\nsetup config dkim domain example.com\nsetup config dkim domain another-example.com\n
OpenDKIM with ACCOUNT_PROVISIONER=FILE
When DMS uses this configuration, it will by default also detect mail domains (from accounts added via setup email add
), generating additional DKIM keys.
DKIM is currently supported by either OpenDKIM or Rspamd:
OpenDKIMRspamdOpenDKIM is currently enabled by default.
After running setup config dkim
, your new DKIM key files (and OpenDKIM config) have been added to /tmp/docker-mailserver/opendkim/
.
Restart required
After restarting DMS, outgoing mail will now be signed with your new DKIM key(s)
Requires opt-in via ENABLE_RSPAMD=1
(and disable the default OpenDKIM: ENABLE_OPENDKIM=0
).
Rspamd provides DKIM support through two separate modules:
If you have multiple domains, you need to:
docker exec -it <CONTAINER NAME> setup config dkim domain <DOMAIN>
for each domain DMS should sign outgoing mail for.dkim_signing.conf
(for which an example is shown below), as the default config only supports one domain.About the Helper Script
The script will persist the keys in /tmp/docker-mailserver/rspamd/dkim/
. Hence, if you are already using the default volume mounts, the keys are persisted in a volume. The script also restarts Rspamd directly, so changes take effect without restarting DMS.
The script provides you with log messages along the way of creating keys. In case you want to read the complete log, use -v
(verbose) or -vv
(very verbose).
In case you have not already provided a default DKIM signing configuration, the script will create one and write it to /etc/rspamd/override.d/dkim_signing.conf
. If this file already exists, it will not be overwritten.
When you're already using the rspamd/override.d/
directory, the file is created inside your volume and therefore persisted correctly. If you are not using rspamd/override.d/
, you will need to persist the file yourself (otherwise it is lost on container restart).
An example of what a default configuration file for DKIM signing looks like can be found by expanding the example below.
DKIM Signing Module Configuration ExamplesA simple configuration could look like this:
# documentation: https://rspamd.com/doc/modules/dkim_signing.html\n\nenabled = true;\n\nsign_authenticated = true;\nsign_local = true;\n\nuse_domain = \"header\";\nuse_redis = false; # don't change unless Redis also provides the DKIM keys\nuse_esld = true;\ncheck_pubkey = true; # you want to use this in the beginning\n\nselector = \"mail\";\n# The path location is searched for a DKIM key with these variables:\n# - `$domain` is sourced from the MIME mail message `From` header\n# - `$selector` is configured for `mail` (as a default fallback)\npath = \"/tmp/docker-mailserver/dkim/keys/$domain/$selector.private\";\n\n# domain specific configurations can be provided below:\ndomain {\n example.com {\n path = \"/tmp/docker-mailserver/rspamd/dkim/mail.private\";\n selector = \"mail\";\n }\n}\n
As shown next:
domain { ... }
section (in the following example: example.com
and example.org
).selectors [ ... ]
array (in the following example, this is done for example.org
).# ...\n\ndomain {\n example.com {\n path = /tmp/docker-mailserver/rspamd/example.com/ed25519.private\";\n selector = \"dkim-ed25519\";\n }\n example.org {\n selectors [\n {\n path = \"/tmp/docker-mailserver/rspamd/dkim/example.org/rsa.private\";\n selector = \"dkim-rsa\";\n },\n {\n path = \"/tmp/docker-mailserver/rspamd/dkim/example.org/ed25519.private\";\n selector = \"dkim-ed25519\";\n }\n ]\n }\n}\n
Support for DKIM Keys using ED25519 This modern elliptic curve is supported by Rspamd, but support by third-parties for verifying Ed25519 DKIM signatures is unreliable.
If you sign your mail with this key type, you should include RSA as a fallback, like shown in the above example.
Let Rspamd Check Your KeysWhen check_pubkey = true;
is set, Rspamd will query the DNS record for each DKIM selector, verifying each public key matches the private key configured.
If there is a mismatch, a warning will be emitted to the Rspamd log /var/log/mail/rspamd.log
.
When mail signed with your DKIM key is sent from your mail server, the receiver needs to check a DNS TXT
record to verify the DKIM signature is trustworthy.
Configuring DNS - DKIM record
When you generated your key in the previous step, the DNS data was saved into a file <selector>.txt
(default: mail.txt
). Use this content to update your DNS via Web Interface or directly edit your DNS Zone file:
Create a new record:
Field Value TypeTXT
Name <selector>._domainkey
(default: mail._domainkey
) TTL Use the default (otherwise 3600 seconds is appropriate) Data File content within ( ... )
(formatted as advised below) When using Rspamd, the helper script has already provided you with the contents (the \"Data\" field) of the DNS record you need to create - you can just copy-paste this text.
<selector>.txt
is already formatted as a snippet for adding to your DNS Zone file.
Just copy/paste the file contents into your existing DNS zone. The TXT
value has been split into separate strings every 255 characters for compatibility.
<selector>.txt
- Formatting the TXT
record value correctly This file was generated for use within a DNS zone file. The file name uses the DKIM selector it was generated with (default DKIM selector is mail
, which creates mail.txt
_).
For your DNS setup, DKIM support needs to create a TXT
record to store the public key for mail clients to use. TXT
records with values that are longer than 255 characters need to be split into multiple parts. This is why the generated <selector>.txt
file (containing your public key for use with DKIM) has multiple value parts wrapped within double-quotes between (
and )
.
A DNS web-interface may handle this separation internally instead, and could expect the value provided all as a single line instead of split. When that is required, you'll need to manually format the value as described below.
Your generated DNS record file (<selector>.txt
) should look similar to this:
mail._domainkey IN TXT ( \"v=DKIM1; k=rsa; \"\n\"p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqQMMqhb1S52Rg7VFS3EC6JQIMxNDdiBmOKZvY5fiVtD3Z+yd9ZV+V8e4IARVoMXWcJWSR6xkloitzfrRtJRwOYvmrcgugOalkmM0V4Gy/2aXeamuiBuUc4esDQEI3egmtAsHcVY1XCoYfs+9VqoHEq3vdr3UQ8zP/l+FP5UfcaJFCK/ZllqcO2P1GjIDVSHLdPpRHbMP/tU1a9mNZ\"\n\"5QMZBJ/JuJK/s+2bp8gpxKn8rh1akSQjlynlV9NI+7J3CC7CUf3bGvoXIrb37C/lpJehS39KNtcGdaRufKauSfqx/7SxA0zyZC+r13f7ASbMaQFzm+/RRusTqozY/p/MsWx8QIDAQAB\"\n) ;\n
Take the content between ( ... )
, and combine all the quote wrapped content and remove the double-quotes including the white-space between them. That is your TXT
record value, the above example would become this:
v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqQMMqhb1S52Rg7VFS3EC6JQIMxNDdiBmOKZvY5fiVtD3Z+yd9ZV+V8e4IARVoMXWcJWSR6xkloitzfrRtJRwOYvmrcgugOalkmM0V4Gy/2aXeamuiBuUc4esDQEI3egmtAsHcVY1XCoYfs+9VqoHEq3vdr3UQ8zP/l+FP5UfcaJFCK/ZllqcO2P1GjIDVSHLdPpRHbMP/tU1a9mNZ5QMZBJ/JuJK/s+2bp8gpxKn8rh1akSQjlynlV9NI+7J3CC7CUf3bGvoXIrb37C/lpJehS39KNtcGdaRufKauSfqx/7SxA0zyZC+r13f7ASbMaQFzm+/RRusTqozY/p/MsWx8QIDAQAB\n
To test that your new DKIM record is correct, query it with the dig
command. The TXT
value response should be a single line split into multiple parts wrapped in double-quotes:
$ dig +short TXT mail._domainkey.example.com\n\"v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqQMMqhb1S52Rg7VFS3EC6JQIMxNDdiBmOKZvY5fiVtD3Z+yd9ZV+V8e4IARVoMXWcJWSR6xkloitzfrRtJRwOYvmrcgugOalkmM0V4Gy/2aXeamuiBuUc4esDQEI3egmtAsHcVY1XCoYfs+9VqoHEq3vdr3UQ8zP/l+FP5UfcaJFCK/ZllqcO2P1GjIDVSHLdPpRHbMP/tU1a9mNZ5QMZBJ/JuJK/s+2bp8gpxKn8rh1akSQjlynlV9NI+7J3CC7CUf3bGvoXIrb37C/lpJehS39\" \"KNtcGdaRufKauSfqx/7SxA0zyZC+r13f7ASbMaQFzm+/RRusTqozY/p/MsWx8QIDAQAB\"\n
"},{"location":"config/best-practices/dkim_dmarc_spf/#dkim-debug","title":"Troubleshooting","text":"MxToolbox has a DKIM Verifier that you can use to check your DKIM DNS record(s).
When using Rspamd, we recommend you turn on check_pubkey = true;
in dkim_signing.conf
. Rspamd will then check whether your private key matches your public key, and you can check possible mismatches by looking at /var/log/mail/rspamd.log
.
With DMS, DMARC is pre-configured out of the box. You may disable extra and excessive DMARC checks when using Rspamd via ENABLE_OPENDMARC=0
.
The only thing you need to do in order to enable DMARC on a \"DNS-level\" is to add new TXT
. In contrast to DKIM, DMARC DNS entries do not require any keys, but merely setting the configuration values. You can either handcraft the entry by yourself or use one of available generators (like this one).
Typically something like this should be good to start with:
_dmarc.example.com. IN TXT \"v=DMARC1; p=none; sp=none; fo=0; adkim=r; aspf=r; pct=100; rf=afrf; ri=86400; rua=mailto:dmarc.report@example.com; ruf=mailto:dmarc.report@example.com\"\n
Or a bit more strict policies (mind p=quarantine
and sp=quarantine
):
_dmarc.example.com. IN TXT \"v=DMARC1; p=quarantine; sp=quarantine; fo=0; adkim=r; aspf=r; pct=100; rf=afrf; ri=86400; rua=mailto:dmarc.report@example.com; ruf=mailto:dmarc.report@example.com\"\n
The DMARC status may not be displayed instantly due to delays in DNS (caches). Dmarcian has a few tools you can use to verify your DNS records.
"},{"location":"config/best-practices/dkim_dmarc_spf/#spf","title":"SPF","text":"What is SPF
Sender Policy Framework (SPF) is a simple email-validation system designed to detect email spoofing by providing a mechanism to allow receiving mail exchangers to check that incoming mail from a domain comes from a host authorized by that domain's administrators.
Source
Disabling the default SPF service policy-spf
Set ENABLE_POLICYD_SPF=0
to opt-out of the default SPF service. Advised when Rspamd is configured to handle SPF instead.
To add a SPF record in your DNS, insert the following line in your DNS zone:
example.com. IN TXT \"v=spf1 mx ~all\"\n
This enables the Softfail mode for SPF. You could first add this SPF record with a very low TTL. SoftFail is a good setting for getting started and testing, as it lets all email through, with spams tagged as such in the mailbox.
After verification, you might want to change your SPF record to v=spf1 mx -all
so as to enforce the HardFail policy. See http://www.open-spf.org/SPF_Record_Syntax for more details about SPF policies.
In any case, increment the SPF record's TTL to its final value.
"},{"location":"config/best-practices/dkim_dmarc_spf/#backup-mx-secondary-mx-for-policyd-spf","title":"Backup MX & Secondary MX forpolicyd-spf
","text":"For whitelisting an IP Address from the SPF test, you can create a config file (see policyd-spf.conf
) and mount that file into /etc/postfix-policyd-spf-python/policyd-spf.conf
.
Example: Create and edit a policyd-spf.conf
file at docker-data/dms/config/postfix-policyd-spf.conf
:
debugLevel = 1\n#0(only errors)-4(complete data received)\n\nskip_addresses = 127.0.0.0/8,::ffff:127.0.0.0/104,::1\n\n# Preferably use IP-Addresses for whitelist lookups:\nWhitelist = 192.168.0.0/31,192.168.1.0/30\n# Domain_Whitelist = mx1.not-example.com,mx2.not-example.com\n
Then add this line to compose.yaml
:
volumes:\n - ./docker-data/dms/config/postfix-policyd-spf.conf:/etc/postfix-policyd-spf-python/policyd-spf.conf\n
"},{"location":"config/best-practices/mta-sts/","title":"Best practices | MTA-STS","text":"MTA-STS is an optional mechanism for a domain to signal support for STARTTLS.
Enable this feature via the ENV setting ENABLE_MTA_STS=1
.
If you have configured DANE
Enabling MTA-STS will by default override DANE if both are configured for a domain.
This can be partially addressed by configuring a dane-only policy resolver before the MTA-STS entry in smtp_tls_policy_maps
. See the postfix-mta-sts-resolver
documentation for further details.
While this feature in DMS supports ensuring STARTTLS is used when mail is sent to another mail server, you may setup similar for mail servers sending mail to DMS.
This requires configuring your DNS and hosting the MTA-STS policy file via a webserver. A good introduction can be found on dmarcian.com.
"},{"location":"config/security/fail2ban/","title":"Security | Fail2Ban","text":"What is Fail2Ban (F2B)?
Fail2ban is an intrusion prevention software framework. Written in the Python programming language, it is designed to prevent against brute-force attacks. It is able to run on POSIX systems that have an interface to a packet-control system or firewall installed locally, such as [NFTables] or TCP Wrapper.
Source
"},{"location":"config/security/fail2ban/#configuration","title":"Configuration","text":"Warning
DMS must be launched with the NET_ADMIN
capability in order to be able to install the NFTables rules that actually ban IP addresses. Thus, either include --cap-add=NET_ADMIN
in the docker run
command, or the equivalent in the compose.yaml
:
cap_add:\n - NET_ADMIN\n
Running Fail2Ban on Older Kernels
DMS configures F2B to use NFTables, not IPTables (legacy). We have observed that older systems, for example NAS systems, do not support the modern NFTables rules. You will need to configure F2B to use legacy IPTables again, for example with the fail2ban-jail.cf
, see the section on configuration further down below.
DMS will automatically ban IP addresses of hosts that have generated 6 failed attempts over the course of the last week. The bans themselves last for one week. The Postfix jail is configured to use mode = extra
in DMS.
What is docker-data/dms/config/
?
This following configuration files inside the docker-data/dms/config/
volume will be copied inside the container during startup
fail2ban-jail.cf
is copied to /etc/fail2ban/jail.d/user-jail.local
fail2ban-fail2ban.cf
is copied to /etc/fail2ban/fail2ban.local
When just running
setup fail2ban\n
the script will show all banned IP addresses.
To get a more detailed status
view, run
setup fail2ban status\n
"},{"location":"config/security/fail2ban/#managing-bans","title":"Managing Bans","text":"You can manage F2B with the setup
script. The usage looks like this:
docker exec <CONTAINER NAME> setup fail2ban [<ban|unban> <IP>]\n
"},{"location":"config/security/fail2ban/#viewing-the-log-file","title":"Viewing the Log File","text":"docker exec <CONTAINER NAME> setup fail2ban log\n
"},{"location":"config/security/fail2ban/#rootless-container","title":"Running Inside A Rootless Container","text":"RootlessKit
is the fakeroot implementation for supporting rootless mode in Docker and Podman. By default, RootlessKit uses the builtin
port forwarding driver, which does not propagate source IP addresses.
It is necessary for F2B to have access to the real source IP addresses in order to correctly identify clients. This is achieved by changing the port forwarding driver to slirp4netns
, which is slower than the builtin driver but does preserve the real source IPs.
For rootless mode in Docker, create ~/.config/systemd/user/docker.service.d/override.conf
with the following content:
Danger
This changes the port driver for all rootless containers managed by Docker. Per container configuration is not supported, if you need that consider Podman instead.
[Service]\nEnvironment=\"DOCKERD_ROOTLESS_ROOTLESSKIT_PORT_DRIVER=slirp4netns\"\n
And then restart the daemon:
$ systemctl --user daemon-reload\n$ systemctl --user restart docker\n
Rootless Podman requires adding the value slirp4netns:port_handler=slirp4netns
to the --network
CLI option, or network_mode
setting in your compose.yaml
:
Example
services:\n mailserver:\n network_mode: \"slirp4netns:port_handler=slirp4netns\"\n environment:\n - ENABLE_FAIL2BAN=1\n - NETWORK_INTERFACE=tap0\n ...\n
You must also add the ENV NETWORK_INTERFACE=tap0
, because Podman uses a hard-coded interface name for slirp4netns
. slirp4netns
is not compatible with user-defined networks!
Info
The Mail crypt plugin is used to secure email messages stored in a Dovecot system. Messages are encrypted before written to storage and decrypted after reading. Both operations are transparent to the user.
In case of unauthorized access to the storage backend, the messages will, without access to the decryption keys, be unreadable to the offending party.
There can be a single encryption key for the whole system or each user can have a key of their own. The used cryptographical methods are widely used standards and keys are stored in portable formats, when possible.
Official Dovecot documentation: https://doc.dovecot.org/configuration_manual/mail_crypt_plugin/
"},{"location":"config/security/mail_crypt/#single-encryption-key-global-method","title":"Single Encryption Key / Global Method","text":"Create 10-custom.conf
and populate it with the following:
# Enables mail_crypt for all services (imap, pop3, etc)\nmail_plugins = $mail_plugins mail_crypt\nplugin {\n mail_crypt_global_private_key = </certs/ecprivkey.pem\n mail_crypt_global_public_key = </certs/ecpubkey.pem\n mail_crypt_save_version = 2\n}\n
Shutdown your mailserver (docker compose down
)
You then need to generate your global EC key. We named them /certs/ecprivkey.pem
and /certs/ecpubkey.pem
in step #1.
The EC key needs to be available in the container. I prefer to mount a /certs directory into the container:
services:\n mailserver:\n image: ghcr.io/docker-mailserver/docker-mailserver:latest\n volumes:\n . . .\n - ./certs/:/certs\n . . .\n
While you're editing the compose.yaml
, add the configuration file:
services:\n mailserver:\n image: ghcr.io/docker-mailserver/docker-mailserver:latest\n volumes:\n . . .\n - ./config/dovecot/10-custom.conf:/etc/dovecot/conf.d/10-custom.conf\n - ./certs/:/certs\n . . .\n
Start the container, monitor the logs for any errors, send yourself a message, and then confirm the file on disk is encrypted:
[root@ip-XXXXXXXXXX ~]# cat -A /mnt/efs-us-west-2/maildata/awesomesite.com/me/cur/1623989305.M6v\ufffdz\ufffd@\ufffd\ufffd m}\ufffd\ufffd,\ufffd\ufffd9\ufffd\ufffd\ufffd\ufffdB*\ufffd247.us-west-2.compute.inE\ufffd\ufffd\\Ck*\ufffd@7795,W=7947:2,\nT\ufffd9\ufffd8t\ufffd6\ufffd\ufffd t\ufffd\ufffd\ufffde\ufffdW\ufffd\ufffdS `\ufffdH\ufffd\ufffdC\ufffd\u06a4 \ufffdyeY\ufffd\ufffdXZ\ufffd\ufffd^\ufffdd\ufffd/\ufffd\ufffd+\ufffdA\n
This should be the minimum required for encryption of the mail while in storage.
"},{"location":"config/security/rspamd/","title":"Security | Rspamd","text":""},{"location":"config/security/rspamd/#about","title":"About","text":"Rspamd is a \"fast, free and open-source spam filtering system\". DMS integrates Rspamd like any other service. We provide a basic but easy to maintain setup of Rspamd.
If you want to take a look at the default configuration files for Rspamd that DMS packs, navigate to target/rspamd/
inside the repository. Please consult the section \"The Default Configuration\" section down below for a written overview.
The following environment variables are related to Rspamd:
ENABLE_RSPAMD
ENABLE_RSPAMD_REDIS
RSPAMD_CHECK_AUTHENTICATED
RSPAMD_GREYLISTING
RSPAMD_HFILTER
RSPAMD_HFILTER_HOSTNAME_UNKNOWN_SCORE
RSPAMD_LEARN
SPAM_SUBJECT
MOVE_SPAM_TO_JUNK
MARK_SPAM_AS_READ
With these variables, you can enable Rspamd itself, and you can enable / disable certain features related to Rspamd.
"},{"location":"config/security/rspamd/#the-default-configuration","title":"The Default Configuration","text":""},{"location":"config/security/rspamd/#other-anti-spam-services","title":"Other Anti-Spam-Services","text":"DMS packs other anti-spam services, like SpamAssassin or Amavis, next to Rspamd. There exist services, like ClamAV (ENABLE_CLAMAV
), that Rspamd can utilize to improve the scanning. Except for ClamAV, we recommend disabling all other anti-spam services when using Rspamd. The basic configuration shown below provides a good starting point.
Attention
Read this section carefully if you want to understand how Rspamd is integrated into DMS and how it works (on a surface level).
Rspamd is integrated as a milter into DMS. When enabled, Postfix's main.cf
configuration file includes the parameter rspamd_milter = inet:localhost:11332
, which is added to smtpd_milters
. As a milter, Rspamd can inspect incoming and outgoing e-mails.
Each mail is assigned what Rspamd calls symbols: when an e-mail matches a specific criterion, the e-mail receives a symbol. Afterward, Rspamd applies a spam score (as usual with anti-spam software) to the e-mail.
Rspamd then adds (a few) headers to the e-mail based on the spam score. Most important is X-Spamd-Result
, which contains an overview of which symbols were applied. It could look like this:
X-Spamd-Result default: False [-2.80 / 11.00]; R_SPF_NA(1.50)[no SPF record]; R_DKIM_ALLOW(-1.00)[example.com:s=dtag1]; DWL_DNSWL_LOW(-1.00)[example.com:dkim]; RWL_AMI_LASTHOP(-1.00)[192.0.2.42:from]; DMARC_POLICY_ALLOW(-1.00)[example.com,none]; RWL_MAILSPIKE_EXCELLENT(-0.40)[192.0.2.42:from]; FORGED_SENDER(0.30)[noreply@example.com,some-reply-address@bounce.example.com]; RCVD_IN_DNSWL_LOW(-0.10)[192.0.2.42:from]; MIME_GOOD(-0.10)[multipart/mixed,multipart/related,multipart/alternative,text/plain]; MIME_TRACE(0.00)[0:+,1:+,2:+,3:+,4:~,5:~,6:~]; RCVD_COUNT_THREE(0.00)[3]; RCPT_COUNT_ONE(0.00)[1]; REPLYTO_DN_EQ_FROM_DN(0.00)[]; ARC_NA(0.00)[]; TO_MATCH_ENVRCPT_ALL(0.00)[]; RCVD_TLS_LAST(0.00)[]; DKIM_TRACE(0.00)[example.com:+]; HAS_ATTACHMENT(0.00)[]; TO_DN_NONE(0.00)[]; FROM_NEQ_ENVFROM(0.00)[noreply@example.com,some-reply-address@bounce.example.com]; FROM_HAS_DN(0.00)[]; REPLYTO_DOM_NEQ_FROM_DOM(0.00)[]; PREVIOUSLY_DELIVERED(0.00)[receiver@anotherexample.com]; ASN(0.00)[asn:3320, ipnet:192.0.2.0/24, country:DE]; MID_RHS_MATCH_FROM(0.00)[]; MISSING_XM_UA(0.00)[]; HAS_REPLYTO(0.00)[some-reply-address@dms-reply.example.com]\n
And then there is a corresponding X-Rspamd-Action
header, which shows the overall result and the action that is taken. In our example, it would be:
X-Rspamd-Action no action\n
Since the score is -2.80
, nothing will happen and the e-mail is not classified as spam. Our custom actions.conf
defines what to do at certain scores:
X-Spam: Yes
);There is more to spam analysis than meets the eye: we have not covered the Bayes training and filters here, nor have we discussed Sieve rules for e-mails that are marked as spam.
Even the calculation of the score with the individual symbols has been presented to you in a simplified manner. But with the knowledge from above, you're equipped to read on and use Rspamd confidently. Keep on reading to understand the integration even better - you will want to know about your anti-spam software, not only to keep the bad e-mail out, but also to make sure the good e-mail arrive properly!
"},{"location":"config/security/rspamd/#workers","title":"Workers","text":"The proxy worker operates in self-scan mode. This simplifies the setup as we do not require a normal worker. You can easily change this though by overriding the configuration by DMS.
DMS does not set a default password for the controller worker. You may want to do that yourself. In setups where you already have an authentication provider in front of the Rspamd webpage, you may want to set the secure_ip
option to \"0.0.0.0/0\"
for the controller worker to disable password authentication inside Rspamd completely.
When Rspamd is enabled, we implicitly also start an instance of Redis in the container:
/var/lib/redis
(or the /var/mail-state/
volume when present).Redis uses /etc/redis/redis.conf
for configuration:
ENABLE_RSPAMD_REDIS=0
(link also details required changes to the DMS Rspamd config).Rspamd provides a web interface, which contains statistics and data Rspamd collects. The interface is enabled by default and reachable on port 11334.
To use the web interface you will need to configure a password, otherwise you won't be able to log in.
Set a custom passwordAdd this line to your rspamd custom-commands.conf
config which sets the password
option of the controller worker:
set-option-for-controller password \"your hashed password here\"\n
The password hash can be generated via the rspamadm pw
command:
docker exec -it <CONTAINER_NAME> rspamadm pw\n
"},{"location":"config/security/rspamd/#dns","title":"DNS","text":"DMS does not supply custom values for DNS servers (to Rspamd). If you need to use custom DNS servers, which could be required when using DNS-based deny/allowlists, you need to adjust options.inc
yourself. Make sure to also read our FAQ page on DNS servers.
Warning
Rspamd heavily relies on a properly working DNS server that it can use to resolve DNS queries. If your DNS server is misconfigured, you will encounter issues when Rspamd queries DNS to assess if mail is spam. Legitimate mail is then unintentionally marked as spam or worse, rejected entirely.
When Rspamd is deciding if mail is spam, it will check DNS records for SPF, DKIM and DMARC. Each of those has an associated symbol for DNS temporary errors with a non-zero weight assigned. That weight contributes towards the spam score assessed by Rspamd which is normally desirable - provided your network DNS is functioning correctly, otherwise when DNS is broken all mail is biased towards spam due to these failed DNS lookups.
Danger
While we do not provide values for custom DNS servers by default, we set soft_reject_on_timeout = true;
by default. This setting will cause a soft reject if a task (presumably a DNS request) timeout takes place.
This setting is enabled to not allow spam to proceed just because DNS requests did not succeed. It could deny legitimate e-mails to pass though too in case your DNS setup is incorrect or not functioning properly.
"},{"location":"config/security/rspamd/#logs","title":"Logs","text":"You can find the Rspamd logs at /var/log/mail/rspamd.log
, and the corresponding logs for Redis, if it is enabled, at /var/log/supervisor/rspamd-redis.log
. We recommend inspecting these logs (with docker exec -it <CONTAINER NAME> less /var/log/mail/rspamd.log
) in case Rspamd does not work as expected.
You can find a list of all Rspamd modules on their website.
"},{"location":"config/security/rspamd/#disabled-by-default","title":"Disabled By Default","text":"DMS disables certain modules (clickhouse
, elastic
, neural
, reputation
, spamassassin
, url_redirector
, metric_exporter
) by default. We believe these are not required in a standard setup, and they would otherwise needlessly use system resources.
You can choose to enable ClamAV, and Rspamd will then use it to check for viruses. Just set the environment variable ENABLE_CLAMAV=1
.
The RBL module is enabled by default. As a consequence, Rspamd will perform DNS lookups to various blacklists. Whether an RBL or a DNSBL is queried depends on where the domain name was obtained: RBL servers are queried with IP addresses extracted from message headers, DNSBL server are queried with domains and IP addresses extracted from the message body [source].
Rspamd and DNS Block Lists
When the RBL module is enabled, Rspamd will do a variety of DNS requests to (amongst other things) DNSBLs. There are a variety of issues involved when using DNSBLs. Rspamd will try to mitigate some of them by properly evaluating all return codes. This evaluation is a best effort though, so if the DNSBL operators change or add return codes, it may take a while for Rspamd to adjust as well.
If you want to use DNSBLs, try to use your own DNS resolver and make sure it is set up correctly, i.e. it should be a non-public & recursive resolver. Otherwise, you might not be able (see this Spamhaus post) to make use of the block lists.
"},{"location":"config/security/rspamd/#providing-custom-settings-overriding-settings","title":"Providing Custom Settings & Overriding Settings","text":"DMS brings sane default settings for Rspamd. They are located at /etc/rspamd/local.d/
inside the container (or target/rspamd/local.d/
in the repository).
What is docker-data/dms/config/
?
If you want to overwrite the default settings or provide your settings, you can place files at docker-data/dms/config/rspamd/override.d/
. Files from this directory are copied to /etc/rspamd/override.d/
during startup. These files forcibly override Rspamd and DMS default settings.
What is the local.d
directory and how does it compare to override.d
?
Clashing Overrides
Note that when also using the custom-commands.conf
file, files in override.d
may be overwritten in case you adjust them manually and with the help of the file.
DMS provides the ability to do simple adjustments to Rspamd modules with the help of a single file. Just place a file called custom-commands.conf
into docker-data/dms/config/rspamd/
. If this file is present, DMS will evaluate it. The structure is simple, as each line in the file looks like this:
COMMAND ARGUMENT1 ARGUMENT2 ARGUMENT3\n
where COMMAND
can be:
disable-module
: disables the module with name ARGUMENT1
enable-module
: explicitly enables the module with name ARGUMENT1
set-option-for-module
: sets the value for option ARGUMENT2
to ARGUMENT3
inside module ARGUMENT1
set-option-for-controller
: set the value of option ARGUMENT1
to ARGUMENT2
for the controller workerset-option-for-proxy
: set the value of option ARGUMENT1
to ARGUMENT2
for the proxy workerset-common-option
: set the option ARGUMENT1
that defines basic Rspamd behavior to value ARGUMENT2
add-line
: this will add the complete line after ARGUMENT1
(with all characters) to the file /etc/rspamd/override.d/<ARGUMENT1>
An Example Is Shown Down Below
File Names & Extensions
For command 1 - 3, we append the .conf
suffix to the module name to get the correct file name automatically. For commands 4 - 6, the file name is fixed (you don't even need to provide it). For command 7, you will need to provide the whole file name (including the suffix) yourself!
You can also have comments (the line starts with #
) and blank lines in custom-commands.conf
- they are properly handled and not evaluated.
Adjusting Modules This Way
These simple commands are meant to give users the ability to easily alter modules and their options. As a consequence, they are not powerful enough to enable multi-line adjustments. If you need to do something more complex, we advise to do that manually!
"},{"location":"config/security/rspamd/#examples-advanced-configuration","title":"Examples & Advanced Configuration","text":""},{"location":"config/security/rspamd/#a-very-basic-configuration","title":"A Very Basic Configuration","text":"Do you want to start using Rspamd? Rspamd is disabled by default, so you need to set the following environment variables:
ENABLE_RSPAMD=1\n# ClamAV is compatible with Rspamd. Optionally enable it for anti-virus support:\nENABLE_CLAMAV=1\n\n# Rspamd replaces the functionality of all these anti-spam services, disable them:\nENABLE_OPENDKIM=0\nENABLE_OPENDMARC=0\nENABLE_POLICYD_SPF=0\nENABLE_AMAVIS=0\nENABLE_SPAMASSASSIN=0\n\n# Provided you've set `RSPAMD_GREYLISTING=1`, also disable Postgrey:\nENABLE_POSTGREY=0\n
This will enable Rspamd and disable services you don't need when using Rspamd.
"},{"location":"config/security/rspamd/#adjusting-and-extending-the-very-basic-configuration","title":"Adjusting and Extending The Very Basic Configuration","text":"Rspamd is running, but you want or need to adjust it? First, create a file named custom-commands.conf
under docker-data/dms/config/rspamd
(which translates to /tmp/docker-mailserver/rspamd/
inside the container). Then add your changes:
set-option-for-controller secure_ip \"0.0.0.0/0\"
.set-option-for-module classifier-bayes autolearn true
.disable-module chartable
.Here is what the file looks like in the end:
# See 1.\n# ATTENTION: this disables authentication on the website - make sure you know what you're doing!\nset-option-for-controller secure_ip \"0.0.0.0/0\"\n\n# See 2.\nset-option-for-module classifier-bayes autolearn true\n\n# See 3.\ndisable-module chartable\n
"},{"location":"config/security/rspamd/#dkim-signing","title":"DKIM Signing","text":"There is a dedicated section for setting up DKIM with Rspamd in our documentation.
"},{"location":"config/security/rspamd/#abusix-integration","title":"Abusix Integration","text":"This subsection provides information about the integration of Abusix, \"a set of blocklists that work as an additional email security layer for your existing mail environment\". The setup is straight-forward and well documented:
<APIKEY>
to your private API keyWe recommend mounting the files directly into the container, as they are rather big and not manageable with our custom-command.conf
script. If mounted to the correct location, Rspamd will automatically pick them up.
While Abusix can be integrated into Postfix, Postscreen and a multitude of other software, we recommend integrating Abusix only into a single piece of software running in your mail server - everything else would be excessive and wasting queries. Moreover, we recommend the integration into suitable filtering software and not Postfix itself, as software like Postscreen or Rspamd can properly evaluate the return codes and other configuration.
"},{"location":"config/security/ssl/","title":"Security | TLS (aka SSL)","text":"There are multiple options to enable SSL (via SSL_TYPE
):
After installation, you can test your setup with:
checktls.com
testssl.sh
Exposure of DNS labels through Certificate Transparency
All public Certificate Authorities (CAs) are required to log certificates they issue publicly via Certificate Transparency. This helps to better establish trust.
When using a public CA for certificates used in private networks, be aware that the associated DNS labels in the certificate are logged publicly and easily searchable. These logs are append only, you cannot redact this information.
You could use a wildcard certificate. This avoids accidentally leaking information to the internet, but keep in mind the potential security risks of wildcard certs.
"},{"location":"config/security/ssl/#the-fqdn","title":"The FQDN","text":"An FQDN (Fully Qualified Domain Name) such as mail.example.com
is required for DMS to function correctly, especially for looking up the correct SSL certificate to use.
mail.example.com
will still use user@example.com
as the mail address. You do not need a bare domain for that.example.com
is also supported.hostname -f
will be used to retrieve the FQDN as configured in the below examples.*.example.com
) are supported for SSL_TYPE=letsencrypt
. Your configured FQDN below may be mail.example.com
, and your wildcard certificate provisioned to /etc/letsencrypt/live/example.com
which will be checked as a fallback FQDN by DMS.Setting the hostname correctly
Change mail.example.com
below to your own FQDN.
# CLI:\ndocker run --hostname mail.example.com\n
or
# compose.yaml\nservices:\n mailserver:\n hostname: mail.example.com\n
"},{"location":"config/security/ssl/#provisioning-methods","title":"Provisioning methods","text":""},{"location":"config/security/ssl/#lets-encrypt-recommended","title":"Let's Encrypt (Recommended)","text":"To enable Let's Encrypt for DMS, you have to:
For your DMS container:
SSL_TYPE=letsencrypt
.letsencrypt
folder as a volume to /etc/letsencrypt
.You don't have to do anything else. Enjoy!
Note
/etc/letsencrypt/live
stores provisioned certificates in individual folders named by their FQDN.
Make sure that the entire folder is mounted to DMS as there are typically symlinks from /etc/letsencrypt/live/mail.example.com
to /etc/letsencrypt/archive
.
Example
Add these additions to the mailserver
service in your compose.yaml
:
services:\n mailserver:\n hostname: mail.example.com\n environment:\n - SSL_TYPE=letsencrypt\n volumes:\n - /etc/letsencrypt:/etc/letsencrypt\n
"},{"location":"config/security/ssl/#example-using-docker-for-lets-encrypt","title":"Example using Docker for Let's Encrypt","text":"Certbot provisions certificates to /etc/letsencrypt
. Add a volume to store these, so that they can later be accessed by DMS container. You may also want to persist Certbot logs, just in case you need to troubleshoot.
Getting a certificate is this simple! (Referencing: Certbot docker instructions and certonly --standalone
mode):
# Requires access to port 80 from the internet, adjust your firewall if needed.\ndocker run --rm -it \\\n -v \"${PWD}/docker-data/certbot/certs/:/etc/letsencrypt/\" \\\n -v \"${PWD}/docker-data/certbot/logs/:/var/log/letsencrypt/\" \\\n -p 80:80 \\\n certbot/certbot certonly --standalone -d mail.example.com\n
Add a volume for DMS that maps the local certbot/certs/
folder to the container path /etc/letsencrypt/
.
Example
Add these additions to the mailserver
service in your compose.yaml
:
services:\n mailserver:\n hostname: mail.example.com\n environment:\n - SSL_TYPE=letsencrypt\n volumes:\n - ./docker-data/certbot/certs/:/etc/letsencrypt\n
The certificate setup is complete, but remember it will expire. Consider automating renewals.
Renewing Certificates
When running the above certonly --standalone
snippet again, the existing certificate is renewed if it would expire within 30 days.
Alternatively, Certbot can look at all the certificates it manages, and only renew those nearing their expiry via the renew
command:
# This will need access to port 443 from the internet, adjust your firewall if needed.\ndocker run --rm -it \\\n -v \"${PWD}/docker-data/certbot/certs/:/etc/letsencrypt/\" \\\n -v \"${PWD}/docker-data/certbot/logs/:/var/log/letsencrypt/\" \\\n -p 80:80 \\\n -p 443:443 \\\n certbot/certbot renew\n
This process can also be automated via cron or systemd timers.
Using a different ACME CA
Certbot does support alternative certificate providers via the --server
option. In most cases you'll want to use the default Let's Encrypt.
certbot-dns-cloudflare
with Docker","text":"If you are unable get a certificate via the HTTP-01
(port 80) or TLS-ALPN-01
(port 443) challenge types, the DNS-01
challenge can be useful (this challenge can additionally issue wildcard certificates). This guide shows how to use the DNS-01
challenge with Cloudflare as your DNS provider.
Obtain a Cloudflare API token:
Click \"Create Token\", and choose the Edit zone DNS
template (Certbot requires the ZONE:DNS:Edit
permission).
Only include the necessary Zone resource configuration
Be sure to configure \"Zone Resources\" section on this page to Include -> Specific zone -> <your zone here>
.
This restricts the API token to only this zone (domain) which is an important security measure.
Store the API token you received in a file cloudflare.ini
with content:
dns_cloudflare_api_token = YOUR_CLOUDFLARE_TOKEN_HERE\n
chmod 600
and chown 0:0
.docker-data/certbot/secrets/
.Your compose.yaml
should include the following:
services:\n mailserver:\n environments:\n # Set SSL certificate type.\n - SSL_TYPE=letsencrypt\n volumes:\n # Mount the cert folder generated by Certbot:\n - ./docker-data/certbot/certs/:/etc/letsencrypt/:ro\n\n certbot-cloudflare:\n image: certbot/dns-cloudflare:latest\n command: certonly --dns-cloudflare --dns-cloudflare-credentials /run/secrets/cloudflare-api-token -d mail.example.com\n volumes:\n - ./docker-data/certbot/certs/:/etc/letsencrypt/\n - ./docker-data/certbot/logs/:/var/log/letsencrypt/\n secrets:\n - cloudflare-api-token\n\n# Docs: https://docs.docker.com/engine/swarm/secrets/#use-secrets-in-compose\n# WARNING: In compose configs without swarm, the long syntax options have no effect,\n# Ensure that you properly `chmod 600` and `chown 0:0` the file on disk. Effectively treated as a bind mount.\nsecrets:\n cloudflare-api-token:\n file: ./docker-data/certbot/secrets/cloudflare.ini\n
Alternative using the docker run
command (secrets
feature is not available):
docker run \\\n --volume \"${PWD}/docker-data/certbot/certs/:/etc/letsencrypt/\" \\\n --volume \"${PWD}/docker-data/certbot/logs/:/var/log/letsencrypt/\" \\\n --volume \"${PWD}/docker-data/certbot/secrets/:/tmp/secrets/certbot/\"\n certbot/dns-cloudflare \\\n certonly --dns-cloudflare --dns-cloudflare-credentials /tmp/secrets/certbot/cloudflare.ini -d mail.example.com\n
Run the service to provision a certificate:
docker compose run certbot-cloudflare\n
You should see the following log output:
Saving debug log to /var/log/letsencrypt/letsencrypt. log | Requesting a certificate for mail.example.com\nWaiting 10 seconds for DNS changes to propagate\nSuccessfully received certificate.\nCertificate is saved at: /etc/letsencrypt/live/mail.example.com/fullchain.pem\nKey is saved at: /etc/letsencrypt/live/mail.example.com/privkey.pem\nThis certificate expires on YYYY-MM-DD.\nThese files will be updated when the certificate renews.\nNEXT STEPS:\n- The certificate will need to be renewed before it expires. Certbot can automatically renew the certificate in background, but you may need to take steps to enable that functionality. See https://certbot.org/renewal instructions.\n
After completing the steps above, your certificate should be ready to use.
Renewing a certificate (Optional)We've only demonstrated how to provision a certificate, but it will expire in 90 days and need to be renewed before then.
In the following example, add a new service (certbot-cloudflare-renew
) into compose.yaml
that will handle certificate renewals:
services:\n certbot-cloudflare-renew:\n image: certbot/dns-cloudflare:latest\n command: renew --dns-cloudflare --dns-cloudflare-credentials /run/secrets/cloudflare-api-token\n volumes:\n - ./docker-data/certbot/certs/:/etc/letsencrypt/\n - ./docker-data/certbot/logs/:/var/log/letsencrypt/\n secrets:\n - cloudflare-api-token\n
You can manually run this service to renew the cert within 90 days:
docker compose run certbot-cloudflare-renew\n
You should see the following output (The following log was generated with --dry-run
options)
Saving debug log to /var/log/letsencrypt/letsencrypt.log\n\n- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -\nProcessing /etc/letsencrypt/renewal/mail.example.com.conf\n- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -\nAccount registered.\nSimulating renewal of an existing certificate for mail.example.com\nWaiting 10 seconds for DNS changes to propagate\n\n- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -\nCongratulations, all simulated renewals succeeded:\n /etc/letsencrypt/live/mail.example.com/fullchain.pem (success)\n- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -\n
It is recommended to automate this renewal via a task scheduler like a systemd timer or in crontab
(crontab
example: Checks every day if the certificate should be renewed)
0 0 * * * docker compose -f PATH_TO_YOUR_DOCKER_COMPOSE_YML up certbot-cloudflare-renew\n
"},{"location":"config/security/ssl/#example-using-nginx-proxy-and-acme-companion-with-docker","title":"Example using nginx-proxy
and acme-companion
with Docker","text":"If you are running a web server already, port 80 will be in use which Certbot requires. You could use the Certbot --webroot
feature, but it is more common to leverage a reverse proxy that manages the provisioning and renewal of certificates for your services automatically.
In the following example, we show how DMS can be run alongside the docker containers nginx-proxy
and acme-companion
(Referencing: acme-companion
documentation):
Start the reverse proxy (nginx-proxy
):
docker run --detach \\\n --name nginx-proxy \\\n --restart always \\\n --publish 80:80 \\\n --publish 443:443 \\\n --volume \"${PWD}/docker-data/nginx-proxy/html/:/usr/share/nginx/html/\" \\\n --volume \"${PWD}/docker-data/nginx-proxy/vhost.d/:/etc/nginx/vhost.d/\" \\\n --volume \"${PWD}/docker-data/acme-companion/certs/:/etc/nginx/certs/:ro\" \\\n --volume '/var/run/docker.sock:/tmp/docker.sock:ro' \\\n nginxproxy/nginx-proxy\n
Then start the certificate provisioner (acme-companion
), which will provide certificates to nginx-proxy
:
# Inherit `nginx-proxy` volumes via `--volumes-from`, but make `certs/` writeable:\ndocker run --detach \\\n --name nginx-proxy-acme \\\n --restart always \\\n --volumes-from nginx-proxy \\\n --volume \"${PWD}/docker-data/acme-companion/certs/:/etc/nginx/certs/:rw\" \\\n --volume \"${PWD}/docker-data/acme-companion/acme-state/:/etc/acme.sh/\" \\\n --volume '/var/run/docker.sock:/var/run/docker.sock:ro' \\\n --env 'DEFAULT_EMAIL=admin@example.com' \\\n nginxproxy/acme-companion\n
Start the rest of your web server containers as usual.
Start a dummy container to provision certificates for your FQDN (eg: mail.example.com
). acme-companion
will detect the container and generate a Let's Encrypt certificate for your domain, which can be used by DMS:
docker run --detach \\\n --name webmail \\\n --env 'VIRTUAL_HOST=mail.example.com' \\\n --env 'LETSENCRYPT_HOST=mail.example.com' \\\n --env 'LETSENCRYPT_EMAIL=admin@example.com' \\\n nginx\n
You may want to add --env LETSENCRYPT_TEST=true
to the above while testing, to avoid the Let's Encrypt certificate generation rate limits.
Make sure your mount path to the letsencrypt
certificates directory is correct. Edit your compose.yaml
for the mailserver
service to have volumes added like below:
volumes:\n - ./docker-data/dms/mail-data/:/var/mail/\n - ./docker-data/dms/mail-state/:/var/mail-state/\n - ./docker-data/dms/config/:/tmp/docker-mailserver/\n - ./docker-data/acme-companion/certs/:/etc/letsencrypt/live/:ro\n
Then from the compose.yaml
project directory, run: docker compose up -d mailserver
.
nginx-proxy
and acme-companion
with docker-compose
","text":"The following example is the basic setup you need for using nginx-proxy
and acme-companion
with DMS (Referencing: acme-companion
documentation):
compose.yaml
You should have an existing compose.yaml
with a mailserver
service. Below are the modifications to add for integrating with nginx-proxy
and acme-companion
services:
services:\n # Add the following `environment` and `volumes` to your existing `mailserver` service:\n mailserver:\n environment:\n # SSL_TYPE: Uses the `letsencrypt` method to find mounted certificates.\n # VIRTUAL_HOST: The FQDN that `nginx-proxy` will configure itself to handle for HTTP[S] connections.\n # LETSENCRYPT_HOST: The FQDN for a certificate that `acme-companion` will provision and renew.\n - SSL_TYPE=letsencrypt\n - VIRTUAL_HOST=mail.example.com\n - LETSENCRYPT_HOST=mail.example.com\n volumes:\n - ./docker-data/acme-companion/certs/:/etc/letsencrypt/live/:ro\n\n # If you don't yet have your own `nginx-proxy` and `acme-companion` setup,\n # here is an example you can use:\n reverse-proxy:\n image: nginxproxy/nginx-proxy\n container_name: nginx-proxy\n restart: always\n ports:\n # Port 80: Required for HTTP-01 challenges to `acme-companion`.\n # Port 443: Only required for containers that need access over HTTPS. TLS-ALPN-01 challenge not supported.\n - \"80:80\"\n - \"443:443\"\n volumes:\n # `certs/`: Managed by the `acme-companion` container (_read-only_).\n # `docker.sock`: Required to interact with containers via the Docker API.\n - ./docker-data/nginx-proxy/html/:/usr/share/nginx/html/\n - ./docker-data/nginx-proxy/vhost.d/:/etc/nginx/vhost.d/\n - ./docker-data/acme-companion/certs/:/etc/nginx/certs/:ro\n - /var/run/docker.sock:/tmp/docker.sock:ro\n\n acme-companion:\n image: nginxproxy/acme-companion\n container_name: nginx-proxy-acme\n restart: always\n environment:\n # When `volumes_from: [nginx-proxy]` is not supported,\n # reference the _reverse-proxy_ `container_name` here:\n - NGINX_PROXY_CONTAINER=nginx-proxy\n volumes:\n # `html/`: Write ACME HTTP-01 challenge files that `nginx-proxy` will serve.\n # `vhost.d/`: To enable web access via `nginx-proxy` to HTTP-01 challenge files.\n # `certs/`: To store certificates and private keys.\n # `acme-state/`: To persist config and state for the ACME provisioner (`acme.sh`).\n # `docker.sock`: Required to interact with containers via the Docker API.\n - ./docker-data/nginx-proxy/html/:/usr/share/nginx/html/\n - ./docker-data/nginx-proxy/vhost.d/:/etc/nginx/vhost.d/\n - ./docker-data/acme-companion/certs/:/etc/nginx/certs/:rw\n - ./docker-data/acme-companion/acme-state/:/etc/acme.sh/\n - /var/run/docker.sock:/var/run/docker.sock:ro\n
Optional ENV vars worth knowing about
Per container ENV that acme-companion
will detect to override default provisioning settings:
LETSENCRYPT_TEST=true
: Recommended during initial setup. Otherwise the default production endpoint has a rate limit of 5 duplicate certificates per week. Overrides ACME_CA_URI
to use the Let's Encrypt staging endpoint.LETSENCRYPT_EMAIL
: For when you don't use DEFAULT_EMAIL
on acme-companion
, or want to assign a different email contact for this container.LETSENCRYPT_KEYSIZE
: Allows you to configure the type (RSA or ECDSA) and size of the private key for your certificate. Default is RSA 4096, but RSA 2048 is recommended.LETSENCRYPT_RESTART_CONTAINER=true
: When the certificate is renewed, the entire container will be restarted to ensure the new certificate is used.acme-companion
ENV for default settings that apply to all containers using LETSENCRYPT_HOST
:
DEFAULT_EMAIL
: An email address that the CA (eg: Let's Encrypt) can contact you about expiring certificates, failed renewals, or for account recovery. You may want to use an email address not handled by your mail server to ensure deliverability in the event your mail server breaks.CERTS_UPDATE_INTERVAL
: If you need to adjust the frequency to check for renewals. 3600 seconds (1 hour) by default.DEBUG=1
: Should be helpful when troubleshooting provisioning issues from acme-companion
logs.ACME_CA_URI
: Useful in combination with CA_BUNDLE
to use a private CA. To change the default Let's Encrypt endpoint to the staging endpoint, use https://acme-staging-v02.api.letsencrypt.org/directory
.CA_BUNDLE
: If you want to use a private CA instead of Let's Encrypt.Alternative to required ENV on mailserver
service
While you will still need both nginx-proxy
and acme-companion
containers, you can manage certificates without adding ENV vars to containers. Instead the ENV is moved into a file and uses the acme-companion
feature Standalone certificates.
This requires adding another shared volume between nginx-proxy
and acme-companion
:
services:\n reverse-proxy:\n volumes:\n - ./docker-data/nginx-proxy/conf.d/:/etc/nginx/conf.d/\n\n acme-companion:\n volumes:\n - ./docker-data/nginx-proxy/conf.d/:/etc/nginx/conf.d/\n - ./docker-data/acme-companion/standalone.sh:/app/letsencrypt_user_data:ro\n
acme-companion
mounts a shell script (standalone.sh
), which defines variables to customize certificate provisioning:
# A list IDs for certificates to provision:\nLETSENCRYPT_STANDALONE_CERTS=('mail')\n\n# Each ID inserts itself into the standard `acme-companion` supported container ENV vars below.\n# The LETSENCRYPT_<ID>_HOST var is a list of FQDNs to provision a certificate for as the SAN field:\nLETSENCRYPT_mail_HOST=('mail.example.com')\n\n# Optional variables:\nLETSENCRYPT_mail_TEST=true\nLETSENCRYPT_mail_EMAIL='admin@example.com'\n# Supported values are `2048`, `3072` and `4096` for RSA keys, and `ec-256` or `ec-384` for elliptic curve keys.\nLETSENCRYPT_mail_KEYSIZE=2048\n
Unlike with the equivalent ENV for containers, changes to this file will not be detected automatically. You would need to wait until the next renewal check by acme-companion
(every hour by default), restart acme-companion
, or manually invoke the service loop:
docker exec nginx-proxy-acme /app/signal_le_service
Version 6.2 and later of the Synology NAS DSM OS now come with an interface to generate and renew letencrypt certificates. Navigation into your DSM control panel and go to Security, then click on the tab Certificate to generate and manage letsencrypt certificates.
Amongst other things, you can use these to secure your mail server. DSM locates the generated certificates in a folder below /usr/syno/etc/certificate/_archive/
.
Navigate to that folder and note the 6 character random folder name of the certificate you'd like to use. Then, add the following to your compose.yaml
declaration file:
volumes:\n - /usr/syno/etc/certificate/_archive/<your-folder>/:/tmp/dms/custom-certs/\nenvironment:\n - SSL_TYPE=manual\n - SSL_CERT_PATH=/tmp/dms/custom-certs/fullchain.pem\n - SSL_KEY_PATH=/tmp/dms/custom-certs/privkey.pem\n
DSM-generated letsencrypt certificates get auto-renewed every three months.
"},{"location":"config/security/ssl/#caddy","title":"Caddy","text":"Caddy is an open-source web server with built-in TLS certificate generation. You can use the official Docker image and write your own Caddyfile
.
Example
While DMS does not need a webserver to work, this workaround will provision a TLS certificate for DMS to use by adding a dummy site block to trigger cert provisioning.
compose.yamlservices:\n # Basic Caddy service to provision certs:\n reverse-proxy:\n image: caddy:2.7\n ports:\n - 80:80\n - 443:443\n volumes:\n - ./Caddyfile:/etc/caddy/Caddyfile:ro\n - ${CADDY_DATA_DIR}:/data\n\n # Share the Caddy data volume for certs and configure SSL_TYPE to `letsencrypt`\n mailserver:\n image: ghcr.io/docker-mailserver/docker-mailserver:latest\n hostname: mail.example.com\n environment:\n SSL_TYPE: letsencrypt\n # While you could use a named data volume instead of a bind mount volume, it would require the long-syntax to rename cert files:\n # https://docs.docker.com/compose/compose-file/05-services/#volumes\n volumes:\n - ${CADDY_DATA_DIR}/certificates/acme-v02.api.letsencrypt.org-directory/mail.example.com/mail.example.com.crt:/etc/letsencrypt/live/mail.example.com/fullchain.pem\n - ${CADDY_DATA_DIR}/certificates/acme-v02.api.letsencrypt.org-directory/mail.example.com/mail.example.com.key:/etc/letsencrypt/live/mail.example.com/privkey.pem\n
An explicit entry in your Caddyfile
config will have Caddy provision and renew a certificate for your DMS FQDN:
mail.example.com {\n # Optionally provision RSA 2048-bit certificate instead of ECDSA P-256:\n tls {\n key_type rsa2048\n }\n\n # Optional, can be useful for troubleshooting\n # connection to Caddy with correct certificate:\n respond \"Hello DMS\"\n}\n
Info
An explicit tls
directive affects only the site-address block it's used in:
tls internal { ... }
if wanting to create a local self-signed cert, which may be useful for testing. This allows opt-in to use self-signed certs unlike the global local_certs
option.key_type
can be used in the tls
block if you need to enforce RSA as the key type for certificates provisioned. The default is currently ECDSA (P-256). This may improve compatibility with legacy clients.caddy-docker-proxy
Using lucaslorentz/caddy-docker-proxy
allows you to generate a Caddyfile
by adding labels to your services in compose.yaml
:
services:\n reverse-proxy:\n image: lucaslorentz/caddy-docker-proxy:2.8\n ports:\n - 80:80\n - 443:443\n volumes:\n - /var/run/docker.sock:/var/run/docker.sock\n - ${CADDY_DATA_DIR}:/data\n labels:\n # Set global config here, this option has an empty value to enable self-signed certs for local testing:\n # NOTE: Remove this label when going to production.\n caddy.local_certs: \"\"\n\n # Use labels to configure Caddy to provision DMS certs\n mailserver:\n image: ghcr.io/docker-mailserver/docker-mailserver:latest\n hostname: mail.example.com\n environment:\n SSL_TYPE: letsencrypt\n volumes:\n - ${CADDY_DATA_DIR}/certificates/acme-v02.api.letsencrypt.org-directory/mail.example.com/mail.example.com.crt:/etc/letsencrypt/live/mail.example.com/fullchain.pem\n - ${CADDY_DATA_DIR}/certificates/acme-v02.api.letsencrypt.org-directory/mail.example.com/mail.example.com.key:/etc/letsencrypt/live/mail.example.com/privkey.pem\n labels:\n # Set your DMS FQDN here to add the site-address into the generated Caddyfile:\n caddy_0: mail.example.com\n # Adding a dummy directive is required:\n caddy_0.respond: \"Hello DMS\"\n # Uncomment to make a proxy for Rspamd:\n # caddy_1: rspamd.example.com\n # caddy_1.reverse_proxy: \"{{upstreams 11334}}\"\n
Caddy certificate location varies
The path contains the certificate provisioner used. This path may be different from the example above for you and may change over time when multiple ACME provisioner services are used.
This can make the volume mounting for DMS to find the certificates non-deterministic, but you can restrict provisioning to single service via the acme_ca
setting.
NOTE: Bind mounting a file directly instead of a directory will mount by inode. If the file is updated at renewal and this modifies the inode on the host system, then the container will still point to the old certificate.
If this happens, consider using our manual TLS type instead:
compose.yamlservices:\n mailserver:\n environment:\n SSL_TYPE: manual\n SSL_CERT_PATH: /srv/tls/mail.example.com/mail.example.com.crt\n SSL_KEY_PATH: /srv/tls/mail.example.com/mail.example.com.key\n volumes:\n - ${CADDY_DATA_DIR}/certificates/acme-v02.api.letsencrypt.org-directory/mail.example.com/:/srv/tls/mail.example.com/:ro\n
"},{"location":"config/security/ssl/#traefik","title":"Traefik","text":"Traefik is an open-source application proxy using the ACME protocol. Traefik can request certificates for domains and subdomains, and it will take care of renewals, challenge negotiations, etc.
Traefik's storage format is natively supported if the acme.json
store is mounted into the container at /etc/letsencrypt/acme.json
. The file is also monitored for changes and will trigger a reload of the mail services (Postfix and Dovecot).
DMS will select it's certificate from acme.json
prioritizing a match for the DMS FQDN (hostname), while also checking one DNS level up (eg: mail.example.com
=> example.com
). Wildcard certificates are supported.
This setup only comes with one caveat - The domain has to be configured on another service for Traefik to actually request it from Let's Encrypt (i.e. Traefik will not issue a certificate without a service / router demanding it).
Example CodeHere is an example setup for Docker Compose
:
services:\n mailserver:\n image: ghcr.io/docker-mailserver/docker-mailserver:latest\n container_name: mailserver\n hostname: mail.example.com\n volumes:\n - ./docker-data/traefik/acme.json:/etc/letsencrypt/acme.json:ro\n environment:\n SSL_TYPE: letsencrypt\n SSL_DOMAIN: mail.example.com\n # for a wildcard certificate, use\n # SSL_DOMAIN: example.com\n\n reverse-proxy:\n image: docker.io/traefik:latest #v2.5\n container_name: docker-traefik\n ports:\n - \"80:80\"\n - \"443:443\"\n command:\n - --providers.docker\n - --entrypoints.http.address=:80\n - --entrypoints.http.http.redirections.entryPoint.to=https\n - --entrypoints.http.http.redirections.entryPoint.scheme=https\n - --entrypoints.https.address=:443\n - --entrypoints.https.http.tls.certResolver=letsencrypt\n - --certificatesresolvers.letsencrypt.acme.email=admin@example.com\n - --certificatesresolvers.letsencrypt.acme.storage=/acme.json\n - --certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=http\n volumes:\n - ./docker-data/traefik/acme.json:/acme.json\n - /var/run/docker.sock:/var/run/docker.sock:ro\n\n whoami:\n image: docker.io/traefik/whoami:latest\n labels:\n - \"traefik.http.routers.whoami.rule=Host(`mail.example.com`)\"\n
"},{"location":"config/security/ssl/#self-signed-certificates","title":"Self-Signed Certificates","text":"Warning
Use self-signed certificates only for testing purposes!
This feature requires you to provide the following files into your docker-data/dms/config/ssl/
directory (internal location: /tmp/docker-mailserver/ssl/
):
<FQDN>-key.pem
<FQDN>-cert.pem
demoCA/cacert.pem
Where <FQDN>
is the FQDN you've configured for your DMS container.
Add SSL_TYPE=self-signed
to your DMS environment variables. Postfix and Dovecot will be configured to use the provided certificate (.pem
files above) during container startup.
One way to generate self-signed certificates is with Smallstep's step
CLI. This is exactly what DMS does for creating test certificates.
For example with the FQDN mail.example.test
, you can generate the required files by running:
#! /bin/sh\nmkdir -p demoCA\n\nstep certificate create \"Smallstep Root CA\" \"demoCA/cacert.pem\" \"demoCA/cakey.pem\" \\\n --no-password --insecure \\\n --profile root-ca \\\n --not-before \"2021-01-01T00:00:00+00:00\" \\\n --not-after \"2031-01-01T00:00:00+00:00\" \\\n --san \"example.test\" \\\n --san \"mail.example.test\" \\\n --kty RSA --size 2048\n\nstep certificate create \"Smallstep Leaf\" mail.example.test-cert.pem mail.example.test-key.pem \\\n --no-password --insecure \\\n --profile leaf \\\n --ca \"demoCA/cacert.pem\" \\\n --ca-key \"demoCA/cakey.pem\" \\\n --not-before \"2021-01-01T00:00:00+00:00\" \\\n --not-after \"2031-01-01T00:00:00+00:00\" \\\n --san \"example.test\" \\\n --san \"mail.example.test\" \\\n --kty RSA --size 2048\n
If you'd rather not install the CLI tool locally to run the step
commands above; you can save the script above to a file such as generate-certs.sh
(and make it executable chmod +x generate-certs.sh
) in a directory that you want the certs to be placed (eg: docker-data/dms/custom-certs/
), then use docker to run that script in a container:
# '--user' is to keep ownership of the files written to\n# the local volume to use your systems User and Group ID values.\ndocker run --rm -it \\\n --user \"$(id -u):$(id -g)\" \\\n --volume \"${PWD}/docker-data/dms/custom-certs/:/tmp/step-ca/\" \\\n --workdir \"/tmp/step-ca/\" \\\n --entrypoint \"/tmp/step-ca/generate-certs.sh\" \\\n smallstep/step-ca\n
"},{"location":"config/security/ssl/#bring-your-own-certificates","title":"Bring Your Own Certificates","text":"You can also provide your own certificate files. Add these entries to your compose.yaml
:
volumes:\n - ./docker-data/dms/custom-certs/:/tmp/dms/custom-certs/:ro\nenvironment:\n - SSL_TYPE=manual\n # Values should match the file paths inside the container:\n - SSL_CERT_PATH=/tmp/dms/custom-certs/public.crt\n - SSL_KEY_PATH=/tmp/dms/custom-certs/private.key\n
This will mount the path where your certificate files reside locally into the read-only container folder: /tmp/dms/custom-certs
.
The local and internal paths may be whatever you prefer, so long as both SSL_CERT_PATH
and SSL_KEY_PATH
point to the correct internal file paths. The certificate files may also be named to your preference, but should be PEM encoded.
SSL_ALT_CERT_PATH
and SSL_ALT_KEY_PATH
are additional ENV vars to support a 2nd certificate as a fallback. Commonly known as hybrid or dual certificate support. This is useful for using a modern ECDSA as your primary certificate, and RSA as your fallback for older connections. They work in the same manner as the non-ALT
versions.
Info
You may have to restart DMS once the certificates change.
"},{"location":"config/security/ssl/#testing-a-certificate-is-valid","title":"Testing a Certificate is Valid","text":"Connect to DMS on port 25
docker exec mailserver openssl s_client \\\n -connect 0.0.0.0:25 \\\n -starttls smtp \\\n -CApath /etc/ssl/certs/\n
The response should show the certificate chain with a line further down: Verify return code: 0 (ok)
This example runs within the DMS container itself to verify the cert is working locally.
-connect
IP if testing externally from another system. Additionally testing for port 143 (Dovecot IMAP) is encouraged (change the protocol for -starttls
from smtp
to imap
).-CApath
will help verify the certificate chain, provided the location contains the root CA that signed your TLS cert for DMS.docker exec mailserver openssl s_client \\\n -connect 0.0.0.0:25 \\\n -starttls smtp \\\n -CApath /etc/ssl/certs/ \\\n 2>/dev/null | openssl x509 -noout -dates\n
Testing and troubleshooting
If you need to test a connection without resolving DNS, curl
can connect with --resolve
option to map an FQDN + Port to an IP address, instead of the request address provided.
# NOTE: You may want to use `--insecure` if the cert was provisioned with a private CA not present on the curl client:\n# Use `--verbose` for additional insights on the connection.\ncurl --resolve mail.example.com:443:127.0.0.1 https://mail.example.com\n
Similarly with openssl
you can connect to an IP as shown previously, but provide an explicit SNI if necessary with -servername mail.example.com
.
Both curl
and openssl
also support -4
and -6
for enforcing IPv4 or IPv6 lookup.
This can be useful, such as when DNS resolves the IP to different servers leading to different certificates returned. As shown in that link, step certificate inspect
is also handy for viewing details of the cert returned or on disk.
Warning
Not recommended for purposes other than testing.
Add this to docker-data/dms/config/dovecot.cf
:
ssl = yes\ndisable_plaintext_auth=no\n
These options in conjunction mean:
If you have another source for SSL/TLS certificates you can import them into the server via an external script. The external script can be found here: external certificate import script.
This is a community contributed script, and in most cases you will have better support via our Change Detection service (automatic for SSL_TYPE
of manual
and letsencrypt
) - Unless you're using LDAP which disables the service.
Script Compatibility
/etc/dms/tls/cert
and /etc/dms/tls/key
intended for internal use only.fullchain.key
+ privkey.pem
as your mounted file names. That may not align with your provisioning method.ALT
fallback certificates (for supporting dual/hybrid, RSA + ECDSA).The steps to follow are these:
./docker-data/dms/custom-certs/
(volume mounted to: /tmp/ssl/
)fullchain.key
and privkey.pem
./docker-data/dms/config/
(volume mounted to: /tmp/docker-mailserver/
)chmod +x tomav-renew-certs.sh
)docker exec mailserver /tmp/docker-mailserver/tomav-renew-certs.sh
If an error occurs the script will inform you. If not you will see both postfix and dovecot restart.
After the certificates have been loaded you can check the certificate:
openssl s_client \\\n -servername mail.example.com \\\n -connect 192.168.0.72:465 \\\n 2>/dev/null | openssl x509\n\n# or\n\nopenssl s_client \\\n -servername mail.example.com \\\n -connect mail.example.com:465 \\\n 2>/dev/null | openssl x509\n
Or you can check how long the new certificate is valid with commands like:
export SITE_URL=\"mail.example.com\"\nexport SITE_IP_URL=\"192.168.0.72\" # can also use `mail.example.com`\nexport SITE_SSL_PORT=\"993\" # imap port dovecot\n\n##works: check if certificate will expire in two weeks\n#2 weeks is 1209600 seconds\n#3 weeks is 1814400\n#12 weeks is 7257600\n#15 weeks is 9072000\n\ncertcheck_2weeks=`openssl s_client -connect ${SITE_IP_URL}:${SITE_SSL_PORT} \\\n -servername ${SITE_URL} 2> /dev/null | openssl x509 -noout -checkend 1209600`\n\n####################################\n#notes: output could be either:\n#Certificate will not expire\n#Certificate will expire\n####################\n
What does the script that imports the certificates do:
/tmp/ssl
.You can of course run the script by cron once a week or something. In that way you could automate cert renewal. If you do so it is probably wise to run an automated check on certificate expiry as well. Such a check could look something like this:
# This script is run inside docker-mailserver via 'docker exec ...', using the 'mail' command to send alerts.\n## code below will alert if certificate expires in less than two weeks\n## please adjust variables!\n## make sure the 'mail -s' command works! Test!\n\nexport SITE_URL=\"mail.example.com\"\nexport SITE_IP_URL=\"192.168.2.72\" # can also use `mail.example.com`\nexport SITE_SSL_PORT=\"993\" # imap port dovecot\n# Below can be from a different domain; like your personal email, not handled by this docker-mailserver:\nexport ALERT_EMAIL_ADDR=\"external-account@gmail.com\"\n\ncertcheck_2weeks=`openssl s_client -connect ${SITE_IP_URL}:${SITE_SSL_PORT} \\\n -servername ${SITE_URL} 2> /dev/null | openssl x509 -noout -checkend 1209600`\n\n####################################\n#notes: output can be\n#Certificate will not expire\n#Certificate will expire\n####################\n\n#echo \"certcheck 2 weeks gives $certcheck_2weeks\"\n\n##automated check you might run by cron or something\n## does the certificate expire within two weeks?\n\nif [ \"$certcheck_2weeks\" = \"Certificate will not expire\" ]; then\n echo \"all is well, certwatch 2 weeks says $certcheck_2weeks\"\n else\n echo \"Cert seems to be expiring pretty soon, within two weeks: $certcheck_2weeks\"\n echo \"we will send an alert email and log as well\"\n logger Certwatch: cert $SITE_URL will expire in two weeks\n echo \"Certwatch: cert $SITE_URL will expire in two weeks\" | mail -s \"cert $SITE_URL expires in two weeks \" $ALERT_EMAIL_ADDR\nfi\n
"},{"location":"config/security/ssl/#custom-dh-parameters","title":"Custom DH Parameters","text":"By default DMS uses ffdhe4096
from IETF RFC 7919. These are standardized pre-defined DH groups and the only available DH groups for TLS 1.3. It is discouraged to generate your own DH parameters as it is often less secure.
Despite this, if you must use non-standard DH parameters or you would like to swap ffdhe4096
for a different group (eg ffdhe2048
); Add your own PEM encoded DH params file via a volume to /tmp/docker-mailserver/dhparams.pem
. This will replace DH params for both Dovecot and Postfix services during container startup.
Prefer ports with Implicit TLS ports, they're more secure than ports using Explicit TLS, and if you use a Reverse Proxy should be less hassle.
"},{"location":"config/security/understanding-the-ports/#overview-of-email-ports","title":"Overview of Email Ports","text":"Protocol Explicit TLS1 Implicit TLS Purpose Enabled by Default ESMTP 25 N/A Transfer2 Yes ESMTP 587 4653 Submission Yes POP3 110 995 Retrieval No IMAP4 143 993 Retrieval YesSTARTTLS
. On ports 110, 143 and 587, DMS will reject a connection that cannot be secured. Port 25 is required to support insecure connections.There is a common misconception of this port due to it's history detailed by various communities and blogs articles on the topic (including by popular mail relay services).
Port 465 was briefly assigned the role of SMTPS in 1997 as an secure alternative to Port 25 between MTA exchanges. Then RFC 2487 (STARTTLS
) while still in a draft status in late 1998 had IANA revoke the SMTPS assignment. The draft history was modified to exclude all mention of port 465 and SMTPS.
In 2018 RFC 8314 was published which revives Port 465 as an Implicit TLS alternative to Port 587 for mail submission. It details very clearly that gaining adoption of 465 as the preferred port will take time. IANA reassigned port 465 as the submissions
service. Any unofficial usage as SMTPS is legacy and has been for over two decades.
Understand that port 587 is more broadly supported due to this history and that lots of software in that time has been built or configured with that port in mind. STARTTLS
is known to have various CVEs discovered even in recent years, do not be misled by any advice implying it should be preferred over implicit TLS. Trust in more official sources, such as the config Postfix has which acknowledges the submissions
port (465).
flowchart LR\n subgraph your-server [\"Your Server\"]\n in_25(25) --> server\n in_465(465) --> server\n server((\"docker-mailserver<br/>hello@world.com\"))\n server --- out_25(25)\n server --- out_465(465)\n end\n\n third-party(\"Third-party<br/>(sending you email)\") ---|\"Receive email for<br/>hello@world.com\"| in_25\n\n subgraph clients [\"Clients (MUA)\"]\n mua-client(Thunderbird,<br/>Webmail,<br/>Mutt,<br/>etc)\n mua-service(Backend software<br/>on another server)\n end\n clients ---|\"Send email as<br/>hello@world.com\"| in_465\n\n out_25(25) -->|\"Direct<br/>Delivery\"| tin_25\n out_465(465) --> relay(\"MTA<br/>Relay Server\") --> tin_25(25)\n\n subgraph third-party-server[\"Third-party Server\"]\n third-party-mta(\"MTA<br/>friend@example.com\")\n tin_25(25) --> third-party-mta\n end
"},{"location":"config/security/understanding-the-ports/#inbound-traffic-on-the-left","title":"Inbound Traffic (On the left)","text":"Mail arriving at your server will be processed and stored in a mailbox, or sent outbound to another mail server.
Note
When submitting mail (inbound) to be sent (outbound), this involves two separate connections to negotiate and secure. There may be additional intermediary connections which DMS is not involved in, and thus unable to ensure encrypted transit throughout delivery.
"},{"location":"config/security/understanding-the-ports/#outbound-traffic-on-the-right","title":"Outbound Traffic (On the Right)","text":"Mail being sent from your server is either being relayed through another MTA (eg: SendGrid), or direct to an MTA responsible for an email address (eg: Gmail).
Tip
DMS can function as a relay too, but professional relay services have a trusted reputation (which increases success of delivery).
An MTA with low reputation can affect if mail is treated as junk, or even rejected.
Note
At best, you can only ensure a secure connection between the MTA you directly connect to. The receiving MTA may relay that mail to another MTA (and so forth), each connection may not be enforcing TLS.
"},{"location":"config/security/understanding-the-ports/#explicit-vs-implicit-tls","title":"Explicit vs Implicit TLS","text":""},{"location":"config/security/understanding-the-ports/#explicit-tls-aka-opportunistic-tls-opt-in-encryption","title":"Explicit TLS (aka Opportunistic TLS) - Opt-in Encryption","text":"Communication on these ports begin in cleartext. Upgrading to an encrypted connection must be requested explicitly through the STARTTLS
protocol and successfully negotiated.
Sometimes a reverse-proxy is involved, but is misconfigured or lacks support for the STARTTLS
negotiation to succeed.
Note
STARTTLS
is unsuccessful, mail can be received over an unencrypted connection. You can better secure this port between trusted parties with the addition of MTA-STS, STARTTLS Policy List, DNSSEC and DANE.Warning
STARTTLS
continues to have vulnerabilities found (Nov 2021 article), as per RFC 8314 (Section 4.1) you are encouraged to prefer Implicit TLS where possible.
Support for STARTTLS
is not always implemented correctly, which can lead to leaking credentials (like a client sending too early) prior to a TLS connection being established. Third-parties such as some ISPs have also been known to intercept the STARTTLS
exchange, modifying network traffic to prevent establishing a secure connection.
Communication on these ports are always encrypted (enforced, thus implicit), avoiding the potential risks with STARTTLS
(Explicit TLS).
While Explicit TLS can provide the same benefit (when STARTTLS
is successfully negotiated), Implicit TLS more reliably avoids concerns with connection manipulation and compatibility.
Todo
This section should provide any related configuration advice, and probably expand on and link to resources about DANE, DNSSEC, MTA-STS and STARTTLS Policy list, with advice on how to configure/setup these added security layers.
Todo
A related section or page on ciphers used may be useful, although less important for users to be concerned about.
"},{"location":"config/security/understanding-the-ports/#tls-connections-for-a-mail-server-compared-to-web-browsers","title":"TLS connections for a Mail Server, compared to web browsers","text":"Unlike with HTTP where a web browser client communicates directly with the server providing a website, a secure TLS connection as discussed below does not provide the equivalent safety that HTTPS does when the transit of email (receiving or sending) is sent through third-parties, as the secure connection is only between two machines, any additional machines (MTAs) between the MUA and the MDA depends on them establishing secure connections between one another successfully.
Other machines that facilitate a connection that generally aren't taken into account can exist between a client and server, such as those where your connection passes through your ISP provider are capable of compromising a cleartext
connection through interception.
When refactoring, writing or altering scripts or other files, adhere to these rules:
shellcheck
to check your scripts! Your contributions are checked by GitHub Actions too, so you will need to do this. You can lint your work with make lint
to check against all targets..editorconfig
file./bin/bash
instead of /bin/sh
in scriptsMake sure to select edge
in the dropdown menu at the top. Navigate to the page you would like to edit and click the edit button in the top right. This allows you to make changes and create a pull-request.
Alternatively you can make the changes locally. For that you'll need to have Docker installed. Navigate into the docs/
directory. Then run:
docker run --rm -it -p 8000:8000 -v \"${PWD}:/docs\" squidfunk/mkdocs-material\n
This serves the documentation on your local machine on port 8000
. Each change will be hot-reloaded onto the page you view, just edit, save and look at the result.
This project is Open Source. That means that you can contribute on enhancements, bug fixing or improving the documentation.
"},{"location":"contributing/issues-and-pull-requests/#opening-an-issue","title":"Opening an Issue","text":"Attention
Before opening an issue, read the README
carefully, study the docs for your version (maybe latest), the Postfix/Dovecot documentation and your search engine you trust. The issue tracker is not meant to be used for unrelated questions!
When opening an issue, please provide details use case to let the community reproduce your problem. Please start DMS with the environment variable LOG_LEVEL
set to debug
or trace
and paste the output into the issue.
Attention
Use the issue templates to provide the necessary information. Issues which do not use these templates are not worked on and closed.
By raising issues, I agree to these terms and I understand, that the rules set for the issue tracker will help both maintainers as well as everyone to find a solution.
Maintainers take the time to improve on this project and help by solving issues together. It is therefore expected from others to make an effort and comply with the rules.
"},{"location":"contributing/issues-and-pull-requests/#filing-a-bug-report","title":"Filing a Bug Report","text":"Thank you for participating in this project and reporting a bug. Docker Mail Server (DMS) is a community-driven project, and each contribution counts!
Maintainers and moderators are volunteers. We greatly appreciate reports that take the time to provide detailed information via the template, enabling us to help you in the best and quickest way. Ignoring the template provided may seem easier, but discourages receiving any support (via assignment of the label meta/no template - no support
).
Markdown formatting can be used in almost all text fields (unless stated otherwise in the description).
Be as precise as possible, and if in doubt, it's best to add more information that too few.
When an option is marked with \"not officially supported\" / \"unsupported\", then support is dependent on availability from specific maintainers.
"},{"location":"contributing/issues-and-pull-requests/#pull-requests","title":"Pull Requests","text":"Motivation
You want to add a feature? Feel free to start creating an issue explaining what you want to do and how you're thinking doing it. Other users may have the same need and collaboration may lead to better results.
"},{"location":"contributing/issues-and-pull-requests/#submit-a-pull-request","title":"Submit a Pull-Request","text":"The development workflow is the following:
git clone --recurse-submodules ...
or run git submodule update --init --recursive
after you cloned your forkmaster
. Please use the pull-request template to provide a minimum of contextual information and make sure to meet the requirements of the checklist.Pull requests are automatically tested against the CI and will be reviewed when tests pass. When your changes are validated, your branch is merged. CI builds the new :edge
image immediately and your changes will be includes in the next version release.
Program testing can be used to show the presence of bugs, but never to show their absence!
\u2013 Edsger Wybe Dijkstra
"},{"location":"contributing/tests/#introduction","title":"Introduction","text":"DMS employs a variety of unit and integration tests. All tests and associated configuration is stored in the test/
directory. If you want to change existing functionality or integrate a new feature into DMS, you will probably need to work with our test suite.
Can I use macOS?
We do not support running linting, tests, etc. on macOS at this time. Please use a Linux VM, Debian/Ubuntu is recommended.
"},{"location":"contributing/tests/#about","title":"About","text":"We use BATS (Bash Automated Testing System) and additional support libraries. BATS is very similar to Bash, and one can easily and quickly get an understanding of how tests in a single file are run. A test file template provides a minimal working example for newcomers to look at.
"},{"location":"contributing/tests/#structure","title":"Structure","text":"The test/
directory contains multiple directories. Among them is the bats/
directory (which is the BATS git submodule) and the helper/
directory. The latter is especially interesting because it contains common support functionality used in almost every test. Actual tests are located in test/tests/
.
Test suite undergoing refactoring
We are currently in the process of restructuring all of our tests. Tests will be moved into test/tests/parallel/
and new tests should be placed there as well.
There are many functions that aid in writing tests. We urge you to use them! They will not only ease writing a test but they will also do their best to ensure there are no race conditions or other unwanted side effects. To learn about the functions we provide, you can:
test/helper/
directory which contains all files that can (and will) be loaded in test filesWe encourage you to try both of the approaches mentioned above. To make understanding and using the helper functions easy, every function contains detailed documentation comments. Read them carefully!
"},{"location":"contributing/tests/#how-are-tests-run","title":"How Are Tests Run?","text":"Tests are split into two categories:
test/tests/parallel/
: Multiple test files are run concurrently to reduce the required time to complete the test suite. A test file will presently run it's own defined test-cases in a sequential order.test/tests/serial/
: Each test file is queued up to run sequentially. Tests that are unable to support running concurrently belong here.Parallel tests are further partitioned into smaller sets. If your system has the resources to support running more than one of those sets at a time this is perfectly ok (our CI runs tests by distributing the sets across multiple test runners). Care must be taken not to mix running the serial tests while a parallel set is also running; this is handled for you when using make tests
.
To run the test suite, you will need to:
jq
, (GNU) parallel
and file
(under Ubuntu, use sudo apt-get -y install jq parallel file
)git submodule update --init --recursive
if you haven't already initialized the git submodulesWe use make
to run commands.
make build
to create or update the local mailserver-testing:ci
Docker image (using the projects Dockerfile
)make clean tests
make clean generate-accounts test/<TEST NAME WITHOUT .bats SUFFIX>
make clean generate-accounts test/<TEST NAME WITHOUT .bats SUFFIX>,<TEST NAME WITHOUT .bats SUFFIX>
(just add a ,
and then immediately write the new test name)make clean generate-accounts tests/parallel/setX
where X
is the number of the set or make clean generate-accounts tests/serial
If your machine is capable, you can increase the amount of tests that are run simultaneously by prepending the make clean all
command with BATS_PARALLEL_JOBS=X
(i.e. BATS_PARALLEL_JOBS=X make clean all
). This wil speed up the test procedure. You can also run all tests in serial by setting BATS_PARALLEL_JOBS=1
this way.
The default value of BATS_PARALLEL_JOBS
is 2. This can be increased if your system has the resources spare to support running more jobs at once to complete the test suite sooner.
Test Output when Running in Parallel
When running tests in parallel (with make clean generate-accounts tests/parallel/setX
), BATS will delay outputting the results until completing all test cases within a file.
This likewise delays the reporting of test-case failures. When troubleshooting parallel set tests, you may prefer to run specific tests you're working on serially (as demonstrated in the example below).
When writing tests, ensure that parallel set tests still pass when run in parallel. You need to account for other tests running in parallel that may interfere with your own tests logic.
Tip
You may use make run-local-instance
to run a version of the image built locally to test and edit your changes in a running DMS instance.
In this example, you've made a change to the Rspamd feature support (or adjusted it's tests). First verify no regressions have been introduced by running it's specific test file:
$ make clean generate-accounts test/rspamd\nrspamd.bats\n \u2713 [Rspamd] Postfix's main.cf was adjusted [12]\n \u2713 [Rspamd] normal mail passes fine [44]\n \u2713 [Rspamd] detects and rejects spam [122]\n \u2713 [Rspamd] detects and rejects virus [189]\n
As your feature work progresses your change for Rspamd also affects ClamAV. As your change now spans more than just the Rspamd test file, you could run multiple test files serially:
$ make clean generate-accounts test/rspamd,clamav\nrspamd.bats\n \u2713 [Rspamd] Postfix's main.cf was adjusted [12]\n \u2713 [Rspamd] normal mail passes fine [44]\n \u2713 [Rspamd] detects and rejects spam [122]\n \u2713 [Rspamd] detects and rejects virus [189]\n\nclamav.bats\n \u2713 [ClamAV] log files exist at /var/log/mail directory [68]\n \u2713 [ClamAV] should be identified by Amavis [67]\n \u2713 [ClamAV] freshclam cron is enabled [76]\n \u2713 [ClamAV] env CLAMAV_MESSAGE_SIZE_LIMIT is set correctly [63]\n \u2713 [ClamAV] rejects virus [60]\n
You're almost finished with your change before submitting it as a PR. It's a good idea to run the full parallel set those individual tests belong to (especially if you've modified any tests):
$ make clean generate-accounts tests/parallel/set1\ndefault_relay_host.bats\n \u2713 [Relay] (ENV) 'DEFAULT_RELAY_HOST' should configure 'main.cf:relayhost' [88]\n\nspam_virus/amavis.bats\n \u2713 [Amavis] SpamAssassin integration should be active [1165]\n\nspam_virus/clamav.bats\n \u2713 [ClamAV] log files exist at /var/log/mail directory [73]\n \u2713 [ClamAV] should be identified by Amavis [67]\n \u2713 [ClamAV] freshclam cron is enabled [76]\n...\n
Even better, before opening a PR run the full test suite:
$ make clean tests\n
"},{"location":"examples/tutorials/basic-installation/","title":"Tutorials | Basic Installation","text":""},{"location":"examples/tutorials/basic-installation/#a-basic-example-with-relevant-environmental-variables","title":"A Basic Example With Relevant Environmental Variables","text":"This example provides you only with a basic example of what a minimal setup could look like. We strongly recommend that you go through the configuration file yourself and adjust everything to your needs. The default compose.yaml can be used for the purpose out-of-the-box, see the Usage chapter.
services:\n mailserver:\n image: ghcr.io/docker-mailserver/docker-mailserver:latest\n container_name: mailserver\n # Provide the FQDN of your mail server here (Your DNS MX record should point to this value)\n hostname: mail.example.com\n ports:\n - \"25:25\"\n - \"465:465\"\n - \"587:587\"\n - \"993:993\"\n volumes:\n - ./docker-data/dms/mail-data/:/var/mail/\n - ./docker-data/dms/mail-state/:/var/mail-state/\n - ./docker-data/dms/mail-logs/:/var/log/mail/\n - ./docker-data/dms/config/:/tmp/docker-mailserver/\n - /etc/localtime:/etc/localtime:ro\n environment:\n - ENABLE_RSPAMD=1\n - ENABLE_CLAMAV=1\n - ENABLE_FAIL2BAN=1\n cap_add:\n - NET_ADMIN # For Fail2Ban to work\n restart: always\n
"},{"location":"examples/tutorials/basic-installation/#a-basic-ldap-setup","title":"A Basic LDAP Setup","text":"Note There are currently no LDAP maintainers. If you encounter issues, please raise them in the issue tracker, but be aware that the core maintainers team will most likely not be able to help you. We would appreciate and we encourage everyone to actively participate in maintaining LDAP-related code by becoming a maintainer!
services:\n mailserver:\n image: ghcr.io/docker-mailserver/docker-mailserver:latest\n container_name: mailserver\n # Provide the FQDN of your mail server here (Your DNS MX record should point to this value)\n hostname: mail.example.com\n ports:\n - \"25:25\"\n - \"465:465\"\n - \"587:587\"\n - \"993:993\"\n volumes:\n - ./docker-data/dms/mail-data/:/var/mail/\n - ./docker-data/dms/mail-state/:/var/mail-state/\n - ./docker-data/dms/mail-logs/:/var/log/mail/\n - ./docker-data/dms/config/:/tmp/docker-mailserver/\n - /etc/localtime:/etc/localtime:ro\n environment:\n - ACCOUNT_PROVISIONER=LDAP\n - LDAP_SERVER_HOST=ldap # your ldap container/IP/ServerName\n - LDAP_SEARCH_BASE=ou=people,dc=localhost,dc=localdomain\n - LDAP_BIND_DN=cn=admin,dc=localhost,dc=localdomain\n - LDAP_BIND_PW=admin\n - LDAP_QUERY_FILTER_USER=(&(mail=%s)(mailEnabled=TRUE))\n - LDAP_QUERY_FILTER_GROUP=(&(mailGroupMember=%s)(mailEnabled=TRUE))\n - LDAP_QUERY_FILTER_ALIAS=(|(&(mailAlias=%s)(objectClass=PostfixBookMailForward))(&(mailAlias=%s)(objectClass=PostfixBookMailAccount)(mailEnabled=TRUE)))\n - LDAP_QUERY_FILTER_DOMAIN=(|(&(mail=*@%s)(objectClass=PostfixBookMailAccount)(mailEnabled=TRUE))(&(mailGroupMember=*@%s)(objectClass=PostfixBookMailAccount)(mailEnabled=TRUE))(&(mailalias=*@%s)(objectClass=PostfixBookMailForward)))\n - DOVECOT_PASS_FILTER=(&(objectClass=PostfixBookMailAccount)(uniqueIdentifier=%n))\n - DOVECOT_USER_FILTER=(&(objectClass=PostfixBookMailAccount)(uniqueIdentifier=%n))\n - ENABLE_SASLAUTHD=1\n - SASLAUTHD_MECHANISMS=ldap\n - SASLAUTHD_LDAP_SERVER=ldap\n - SASLAUTHD_LDAP_BIND_DN=cn=admin,dc=localhost,dc=localdomain\n - SASLAUTHD_LDAP_PASSWORD=admin\n - SASLAUTHD_LDAP_SEARCH_BASE=ou=people,dc=localhost,dc=localdomain\n - SASLAUTHD_LDAP_FILTER=(&(objectClass=PostfixBookMailAccount)(uniqueIdentifier=%U))\n - POSTMASTER_ADDRESS=postmaster@localhost.localdomain\n restart: always\n
"},{"location":"examples/tutorials/basic-installation/#using-dms-as-a-local-mail-relay-for-containers","title":"Using DMS as a local mail relay for containers","text":"Info
This was originally a community contributed guide. Please let us know via a Github Issue if you're having any difficulty following the guide so that we can update it.
This guide is focused on only using SMTP ports (not POP3 and IMAP) with the intent to relay mail received from another service to an external email address (eg: user@gmail.com
). It is not intended for mailbox storage of real users.
In this setup DMS is not intended to receive email from the outside world, so no anti-spam or anti-virus software is needed, making the service lighter to run.
setup
The setup
command used below is to be run inside the container.
Open Relays
Adding the docker network's gateway to the list of trusted hosts (eg: using the network
or connected-networks
option), can create an open relay. For instance if IPv6 is enabled on the host machine, but not in Docker.
Create the file compose.yaml
with a content like this:
Example
services:\n mailserver:\n image: ghcr.io/docker-mailserver/docker-mailserver:latest\n container_name: mailserver\n # Provide the FQDN of your mail server here (Your DNS MX record should point to this value)\n hostname: mail.example.com\n ports:\n - \"25:25\"\n - \"587:587\"\n - \"465:465\"\n volumes:\n - ./docker-data/dms/mail-data/:/var/mail/\n - ./docker-data/dms/mail-state/:/var/mail-state/\n - ./docker-data/dms/mail-logs/:/var/log/mail/\n - ./docker-data/dms/config/:/tmp/docker-mailserver/\n - /etc/localtime:/etc/localtime:ro\n environment:\n - ENABLE_FAIL2BAN=1\n # Using letsencrypt for SSL/TLS certificates:\n - SSL_TYPE=letsencrypt\n # Allow sending emails from other docker containers:\n # Beware creating an Open Relay: https://docker-mailserver.github.io/docker-mailserver/latest/config/environment/#permit_docker\n - PERMIT_DOCKER=network\n # You may want to enable this: https://docker-mailserver.github.io/docker-mailserver/latest/config/environment/#spoof_protection\n # See step 6 below, which demonstrates setup with enabled/disabled SPOOF_PROTECTION:\n - SPOOF_PROTECTION=0\n cap_add:\n - NET_ADMIN # For Fail2Ban to work\n restart: always\n
The docs have a detailed page on Environment Variables for reference.
Firewalled portsIf you have a firewall running, you may need to open ports 25
, 587
and 465
.
For example, with the firewall ufw
, run:
ufw allow 25\nufw allow 587\nufw allow 465\n
Caution: This may not be sound advice.
Configure your DNS service to use an MX record for the hostname (eg: mail
) you configured in the previous step and add the SPF TXT record.
If you manually manage the DNS zone file for the domain
It would look something like this:
$ORIGIN example.com\n@ IN A 10.11.12.13\nmail IN A 10.11.12.13\n\n; mail server for example.com\n@ IN MX 10 mail.example.com.\n\n; Add SPF record\n@ IN TXT \"v=spf1 mx -all\"\n
Then don't forget to change the SOA
serial number, and to restart the service.
Generate DKIM keys for your domain via setup config dkim
.
Copy the content of the file docker-data/dms/config/opendkim/keys/example.com/mail.txt
and add it to your DNS records as a TXT like SPF was handled above.
I use bind9 for managing my domains, so I just paste it on example.com.db
:
mail._domainkey IN TXT ( \"v=DKIM1; h=sha256; k=rsa; \"\n \"p=MIIBIjANBgkqhkiG9w0BAQEFACAQ8AMIIBCgKCAQEAaH5KuPYPSF3Ppkt466BDMAFGOA4mgqn4oPjZ5BbFlYA9l5jU3bgzRj3l6/Q1n5a9lQs5fNZ7A/HtY0aMvs3nGE4oi+LTejt1jblMhV/OfJyRCunQBIGp0s8G9kIUBzyKJpDayk2+KJSJt/lxL9Iiy0DE5hIv62ZPP6AaTdHBAsJosLFeAzuLFHQ6USyQRojefqFQtgYqWQ2JiZQ3\"\n \"iqq3bD/BVlwKRp5gH6TEYEmx8EBJUuDxrJhkWRUk2VDl1fqhVBy8A9O7Ah+85nMrlOHIFsTaYo9o6+cDJ6t1i6G1gu+bZD0d3/3bqGLPBQV9LyEL1Rona5V7TJBGg099NQkTz1IwIDAQAB\" ) ; ----- DKIM key mail for example.com\n
Get an SSL certificate, we have a guide for you here (Let's Encrypt is a popular service to get free SSL certificates).
Start DMS and check the terminal output for any errors: docker compose up
.
Create email accounts and aliases:
With SPOOF_PROTECTION=0
setup email add admin@example.com passwd123\nsetup email add info@example.com passwd123\nsetup alias add admin@example.com external-account@gmail.com\nsetup alias add info@example.com external-account@gmail.com\nsetup email list\nsetup alias list\n
Aliases make sure that any email that comes to these accounts is forwarded to your third-party email address (external-account@gmail.com
), where they are retrieved (eg: via third-party web or mobile app), instead of connecting directly to docker-mailserer
with POP3 / IMAP.
With SPOOF_PROTECTION=1
setup email add admin.gmail@example.com passwd123\nsetup email add info.gmail@example.com passwd123\nsetup alias add admin@example.com admin.gmail@example.com\nsetup alias add info@example.com info.gmail@example.com\nsetup alias add admin.gmail@example.com external-account@gmail.com\nsetup alias add info.gmail@example.com external-account@gmail.com\nsetup email list\nsetup alias list\n
This extra step is required to avoid the 553 5.7.1 Sender address rejected: not owned by user
error (the accounts used for submitting mail to Gmail are admin.gmail@example.com
and info.gmail@example.com
)
Send some test emails to these addresses and make other tests. Once everything is working well, stop the container with ctrl+c
and start it again as a daemon: docker compose up -d
.
This site lists blog entries that write about the project. If you blogged about DMS let us know so we can add it here!
What is Crowdsec?
Crowdsec is an open source software that detects and blocks attackers using log analysis. It has access to a global community-wide IP reputation database.
Source
"},{"location":"examples/tutorials/crowdsec/#installation","title":"Installation","text":"Crowdsec supports multiple installation methods, however this page will use the docker installation.
"},{"location":"examples/tutorials/crowdsec/#docker-mailserver","title":"Docker mailserver","text":"In your compose.yaml
for the DMS service, add a bind mount volume for /var/log/mail
. This is to share the DMS logs to a separate crowdsec container.
Example
services:\n mailserver:\n - /docker-data/dms/mail-logs/:/var/log/mail/\n
"},{"location":"examples/tutorials/crowdsec/#crowdsec","title":"Crowdsec","text":"The crowdsec container should also bind mount the same host path for the DMS logs that was added in the DMS example above.
services:\n image: crowdsecurity/crowdsec\n restart: unless-stopped\n ports:\n - \"8080:8080\"\n - \"6060:6060\"\n volumes:\n - /docker-data/dms/mail-logs/:/var/log/dms:ro\n - ./acquis.d:/etc/crowdsec/acquis.d\n - crowdsec-db:/var/lib/crowdsec/data/\n environment:\n # These collection contains parsers and scenarios for postfix and dovecot\n COLLECTIONS: crowdsecurity/postfix crowdsecurity/dovecot\n TZ: Europe/Paris\nvolumes:\n crowdsec-db:\n
"},{"location":"examples/tutorials/crowdsec/#configuration","title":"Configuration","text":"Configure crowdsec to read and parse DMS logs file.
Example
Create the file dms.yml
in ./acquis.d/
---\nsource: file\nfilenames:\n - /var/log/dms/mail.log\nlabels:\n type: syslog\n
Warning
Crowdsec on its own is just a detection software, the remediation is done by components called bouncers. This page does not explain how to install or configure a bouncer. It can be found in crowdsec documentation.
"},{"location":"examples/tutorials/docker-build/","title":"Tutorials | Docker Build","text":""},{"location":"examples/tutorials/docker-build/#building-your-own-docker-image","title":"Building your own Docker image","text":""},{"location":"examples/tutorials/docker-build/#submodules","title":"Submodules","text":"You'll need to retrieve the git submodules prior to building your own Docker image. From within your copy of the git repo run the following to retrieve the submodules and build the Docker image:
git submodule update --init --recursive\ndocker build --tag <YOUR CUSTOM IMAGE NAME> .\n
Or, you can clone and retrieve the submodules in one command:
git clone --recurse-submodules https://github.com/docker-mailserver/docker-mailserver\n
"},{"location":"examples/tutorials/docker-build/#about-docker","title":"About Docker","text":""},{"location":"examples/tutorials/docker-build/#minimum-supported-version","title":"Minimum supported version","text":"We make use of build features that require a recent version of Docker. v23.0 or newer is advised, but earlier releases may work.
DOCKER_BUILDKIT=1
.The Dockerfile
includes several build ARG
instructions that can be configured:
DOVECOT_COMMUNITY_REPO
: Install Dovecot from the community repo instead of from Debian (default = 0) DMS_RELEASE
: The image version (default = edge)VCS_REVISION
: The git commit hash used for the build (default = unknown)Note
DMS_RELEASE
(when not edge
) will be used to check for updates from our GH releases page at runtime due to the default feature ENABLE_UPDATE_CHECK=1
.DMS_RELEASE
and VCS_REVISION
are also used with opencontainers
metadata LABEL
instructions.Dovecot supports several FTS backends for providing fast and efficient full text searching of e-mails directly from the IMAP server.
As the size of your mail storage grows, the benefits of FTS are especially notable:
This is a community contributed guide
It extends our official docs for Dovecot FTS with a focus on Apache Solr. DMS does not officially support this integration.
"},{"location":"examples/tutorials/dovecot-solr/#setup-solr-for-dms","title":"Setup Solr for DMS","text":"An FTS backend supported by Dovecot is Apache Solr, a fast and efficient multi-purpose search indexer.
"},{"location":"examples/tutorials/dovecot-solr/#add-the-required-dovecot-solr-package","title":"Add the requireddovecot-solr
package","text":"As the official DMS image does not provide dovecot-solr
, you'll need to include the package in your own image (extending a DMS release as a base image), or via our user-patches.sh
feature:
user-patches.sh
compose.yaml
If you'd prefer to avoid a custom image build. This approach is simpler but with the caveat that any time the container is restarted, you'll have a delay as the package is installed each time.
#!/bin/bash\n\napt-get update && apt-get install dovecot-solr\n
A custom DMS image does not add much friction. You do not need a separate Dockerfile
as Docker Compose supports building from an inline Dockerfile
in your compose.yaml
.
The image
key of the service is swapped for the build
key instead, as shown below:
services:\n mailserver:\n hostname: mail.example.com\n # The `image` setting now represents the tag for the local build configured below:\n image: local/dms:14.0\n # Local build (no need to try pull `image` remotely):\n pull_policy: build\n # Add this `build` section to your real `compose.yaml` for your DMS service:\n build:\n dockerfile_inline: |\n FROM docker.io/mailserver/docker-mailserver:14.0\n RUN apt-get update && apt-get install dovecot-solr\n
docker compose up
and it will pull DMS and build your custom image to run a container.Why doesn't DMS include dovecot-solr
?
This integration is not officially supported in DMS as no maintainer is able to provide troubleshooting support.
Prior to v14, the package was included but the community contributed guide had been outdated for several years that it was non-functional. It was decided that it was better to drop support and docs, however some DMS users voiced active use of Solr and it's benefits over Xapian for FTS which led to these revised docs.
ARM64 builds do not have support for dovecot-solr
. Additionally the user demand for including dovecot-solr
is presently too low to justify vs the minimal effort to add additional packages as shown above.
compose.yaml
config","text":"Firstly you need a working Solr container, for this the official docker image will do:
services:\n solr:\n image: solr:latest\n container_name: dms-solr\n environment:\n # As Solr can be quite resource hungry, raise the memory limit to 2GB.\n # The default is 512MB, which may be exhausted quickly.\n SOLR_JAVA_MEM: \"-Xms2g -Xmx2g\"\n volumes:\n - ./docker-data/solr:/var/solr\n restart: always\n
DMS will connect internally to the solr
service above. Either have both services in the same compose.yaml
file, or ensure that the containers are connected to the same docker network.
Once the Solr container is started, you need to configure a \"Solr core\" for Dovecot:
docker exec -it dms-solr /bin/sh\nsolr create -c dovecot\ncp -R /opt/solr/contrib/analysis-extras/lib /var/solr/data/dovecot\n
Stop the dms-solr
container and you should now have a ./data/dovecot
folder in the local bind mount volume.
Solr needs a schema that is specifically tailored for Dovecot FTS.
As of writing of this guide, Solr 9 is the current release. Dovecot provides the required schema configs for Solr, copy the following two v9 config files to ./data/dovecot
and rename them accordingly:
solr-config-9.xml
(rename to solrconfig.xml
)solr-schema-9.xml
(rename to schema.xml
)Additionally, remove the managed-schema.xml
file from ./data/dovecot
and ensure the two files you copied have a UID and GID of 8983
assigned.
Start the Solr container once again, you should now have a working Solr core specifically for Dovecot FTS.
Configure Dovecot in DMS to connect to this Solr core:
Create a 10-plugin.conf
file in your ./config/dovecot
folder with this contents:
mail_plugins = $mail_plugins fts fts_solr\n\nplugin {\n fts = solr\n fts_autoindex = yes\n fts_solr = url=http://dms-solr:8983/solr/dovecot/\n}\n
Add a volume mount for that config to your DMS service in compose.yaml
:
services:\n mailserver:\n volumes:\n - ./docker-data/config/dovecot/10-plugin.conf:/etc/dovecot/conf.d/10-plugin.conf:ro\n
After following the previous steps, restart DMS and run this command to have Dovecot re-index all mail:
docker compose exec mailserver doveadm fts rescan -A\n
Indexing will take a while depending on how large your mail folders
Usually within 15 minutes or so, you should be able to search your mail using the Dovecot FTS feature!
"},{"location":"examples/tutorials/mailserver-behind-proxy/","title":"Tutorials | Mail Server behind a Proxy","text":""},{"location":"examples/tutorials/mailserver-behind-proxy/#using-a-reverse-proxy","title":"Using a Reverse Proxy","text":"Guidance is provided via a Traefik config example, however if you're only familiar with configuring a reverse proxy for web services there are some differences to keep in mind.
This reduces many of the benefits for why you might use a reverse proxy, but they can still be useful.
Some deployments may require a service to route traffic (kubernetes) when deploying, in which case the below advice is important to understand well.
The guide here has also been adapted for our Kubernetes docs.
"},{"location":"examples/tutorials/mailserver-behind-proxy/#what-can-go-wrong","title":"What can go wrong?","text":"Without a reverse proxy involved, a service is typically aware of the client IP for a connection.
However when a reverse proxy routes the connection this information can be lost, and the proxied service mistakenly treats the client IP as the reverse proxy handling the connection.
A key difference for how the network is proxied relates to the OSI Model:
When working with Layer 7 and a protocol like HTTP, it is possible to inspect a protocol header like Forwarded
(or it's predecessor: X-Forwarded-For
). At a lower level with Layer 4, that information is not available and we are routing traffic agnostic to the application protocol being proxied.
A proxy can prepend the PROXY protocol header to the TCP/UDP connection as it is routed to the service, which must be configured to be compatible with PROXY protocol (often this adds a restriction that connections must provide the header, otherwise they're rejected).
Beyond your own proxy, traffic may be routed in the network by other means that would also rewrite this information such as Docker's own network management via iptables
and userland-proxy
(NAT). The PROXY header ensures the original source and destination IP addresses, along with their ports is preserved across transit.
The below guidance is focused on configuring Traefik, but the advice should be roughly applicable elsewhere (eg: NGINX, Caddy).
The Traefik service config is fairly standard, just define the necessary entrypoints:
compose.yamlservices:\n reverse-proxy:\n image: docker.io/traefik:latest # 2.10 / 3.0\n # CAUTION: In production you should configure the Docker API endpoint securely:\n # https://doc.traefik.io/traefik/providers/docker/#docker-api-access\n volumes:\n - /var/run/docker.sock:/var/run/docker.sock\n command:\n # Docker provider config:\n - --providers.docker=true\n - --providers.docker.exposedbydefault=false\n # DMS ports you want to proxy:\n - --entryPoints.mail-smtp.address=:25\n - --entryPoints.mail-submission.address=:587\n - --entryPoints.mail-submissions.address=:465\n - --entryPoints.mail-imap.address=:143\n - --entryPoints.mail-imaps.address=:993\n - --entryPoints.mail-pop3.address=:110\n - --entryPoints.mail-pop3s.address=:995\n - --entryPoints.mail-managesieve.address=:4190\n # Publish external access ports mapped to traefik entrypoint ports:\n ports:\n - \"25:25\"\n - \"587:587\"\n - \"465:465\"\n - \"143:143\"\n - \"993:993\"\n - \"110:110\"\n - \"995:995\"\n - \"4190:4190\"\n # An IP is assigned here for other services (Dovecot) to trust for PROXY protocol:\n networks:\n default:\n ipv4_address: 172.16.42.2\n\n# Specifying a subnet to assign a fixed container IP to the reverse proxy:\nnetworks:\n default:\n name: my-network\n ipam:\n config:\n - subnet: \"172.16.42.0/24\"\n
Extra considerations
--providers.docker.network=my-network
is useful when there is more than one network to consider.services:\n dms:\n image: ghcr.io/docker-mailserver/docker-mailserver:latest\n hostname: mail.example.com\n labels:\n - traefik.enable=true\n\n # These are examples, configure the equivalent for any additional ports you proxy.\n # Explicit TLS (STARTTLS):\n - traefik.tcp.routers.mail-smtp.rule=HostSNI(`*`)\n - traefik.tcp.routers.mail-smtp.entrypoints=smtp\n - traefik.tcp.routers.mail-smtp.service=smtp\n - traefik.tcp.services.mail-smtp.loadbalancer.server.port=25\n - traefik.tcp.services.mail-smtp.loadbalancer.proxyProtocol.version=2\n\n # Implicit TLS is no different, except for optional HostSNI support:\n - traefik.tcp.routers.mail-submissions.rule=HostSNI(`*`)\n - traefik.tcp.routers.mail-submissions.entrypoints=smtp-submissions\n - traefik.tcp.routers.mail-submissions.service=smtp-submissions\n - traefik.tcp.services.mail-submissions.loadbalancer.server.port=465\n - traefik.tcp.services.mail-submissions.loadbalancer.proxyProtocol.version=2\n # NOTE: Optionally match by SNI rule, this requires TLS passthrough (not compatible with STARTTLS):\n #- traefik.tcp.routers.mail-submissions.rule=HostSNI(`mail.example.com`)\n #- traefik.tcp.routers.mail-submissions.tls.passthrough=true\n
PROXY protocol compatibility
Only TCP routers support enabling PROXY Protocol (via proxyProtocol.version=2
)
Postfix and Dovecot are both compatible with PROXY protocol v1 and v2.
"},{"location":"examples/tutorials/mailserver-behind-proxy/#ports","title":"Ports","text":"Technical Details - Ports (Traefik config)Explicit TLS (STARTTLS)
Service Ports: mail-smtp
(25), mail-submission
(587), mail-imap
(143), mail-pop3
(110), mail-managesieve
(4190)
HostSNI
router rule is not usable (see \"HostSNI & TLS\"). This limits routing flexibility for these ports (eg: routing these ports by the FQDN to different DMS containers).Implicit TLS
Service Ports: mail-submissions
(465), mail-imaps
(993), mail-pop3s
(995)
The HostSNI
router rule could specify the DMS FQDN instead of *
:
*
when the client does not provide a server name. Some clients must explicitly opt-in, such as CLI clients openssl
(-servername
) and swaks
(--tls-sni
).tls.passthrough=true
to the router (this implicitly enables TLS).acme.json
from Traefik).Unlike proxying HTTPS (port 443) to a container via HTTP (port 80), the equivalent for DMS service ports is not supported:
tls.passthrough.true
would not be required, additionally port 25 would always be unencrypted (if the proxy exclusively manages TLS/certs), or unreachable by public MTAs attempting delivery if the proxy enables implicit TLS for this port.This can be handled via our config override support.
Postfix via postfix-master.cf
:
smtp/inet/postscreen_upstream_proxy_protocol=haproxy\nsubmission/inet/smtpd_upstream_proxy_protocol=haproxy\nsubmissions/inet/smtpd_upstream_proxy_protocol=haproxy\n
postscreen_upstream_proxy_protocol
and smtpd_upstream_proxy_protocol
both specify the protocol type used by a proxy. haproxy
represents the PROXY protocol.
Dovecot via dovecot.cf
:
haproxy_trusted_networks = 172.16.42.2\n\nservice imap-login {\n inet_listener imap {\n haproxy = yes\n }\n\n inet_listener imaps {\n haproxy = yes\n }\n}\n\nservice pop3-login {\n inet_listener pop3 {\n haproxy = yes\n }\n\n inet_listener pop3s {\n haproxy = yes\n }\n}\n\nservice managesieve-login {\n inet_listener sieve {\n haproxy = yes\n }\n}\n
haproxy_trusted_networks
must reference the reverse proxy IP, or a wider subnet using CIDR notation.haproxy = yes
for the TCP listeners of each login service.Internal traffic (within the network or DMS itself)
A solution is to configure alternative service ports that offer PROXY protocol support (as shown next).
Alternatively routing connections to DMS through the local reverse proxy via DNS query rewriting can work too.
Configuring services with separate ports for PROXY protocolIn this example we'll take the original service ports and add 10000
for the new PROXY protocol service ports.
Traefik labels will need to update their service ports accordingly (eg: .loadbalancer.server.port=10465
).
Postfix config now requires our user-patches.sh
support to add new services in /etc/postfix/master.cf
:
#!/bin/bash\n\n# Duplicate the config for the submission(s) service ports (587 / 465) with adjustments for the PROXY ports (10587 / 10465) and `syslog_name` setting:\npostconf -Mf submission/inet | sed -e s/^submission/10587/ -e 's/submission/submission-proxyprotocol/' >> /etc/postfix/master.cf\npostconf -Mf submissions/inet | sed -e s/^submissions/10465/ -e 's/submissions/submissions-proxyprotocol/' >> /etc/postfix/master.cf\n# Enable PROXY Protocol support for these new service variants:\npostconf -P 10587/inet/smtpd_upstream_proxy_protocol=haproxy\npostconf -P 10465/inet/smtpd_upstream_proxy_protocol=haproxy\n\n# Create a variant for port 25 too (NOTE: Port 10025 is already assigned in DMS to Amavis):\npostconf -Mf smtp/inet | sed -e s/^smtp/12525/ >> /etc/postfix/master.cf\n# Enable PROXY Protocol support (different setting as port 25 is handled via postscreen), optionally configure a `syslog_name` to distinguish in logs:\npostconf -P 12525/inet/postscreen_upstream_proxy_protocol=haproxy 12525/inet/syslog_name=smtp-proxyprotocol\n
Supporting port 25 with an additional PROXY protocol port will also require a postfix-main.cf
override line for postscreen
to work correctly:
postscreen_cache_map = proxy:btree:$data_directory/postscreen_cache\n
Dovecot is mostly the same as before:
ssl = yes
when implicit TLS is needed.haproxy_trusted_networks = 172.16.42.2\n\nservice imap-login {\n inet_listener imap-proxied {\n haproxy = yes\n port = 10143\n }\n\n inet_listener imaps-proxied {\n haproxy = yes\n port = 10993\n ssl = yes\n }\n}\n\nservice pop3-login {\n inet_listener pop3-proxied {\n haproxy = yes\n port = 10110\n }\n\n inet_listener pop3s-proxied {\n haproxy = yes\n port = 10995\n ssl = yes\n }\n}\n\nservice managesieve-login {\n inet_listener sieve-proxied {\n haproxy = yes\n port = 14190\n }\n}\n
"},{"location":"examples/tutorials/mailserver-behind-proxy/#verification","title":"Verification","text":"Send an email through the reverse proxy. If you do not use the DNS query rewriting approach, you'll need to do this from an external client.
Sending a generic test mail throughswaks
CLI Run a swaks
command and then check your DMS logs for the expected client IP, it should no longer be using the reverse proxy IP.
# NOTE: It is common to find port 25 is blocked from outbound connections, you may only be able to test the submission(s) ports.\nswaks --helo not-relevant.test --server mail.example.com --port 25 -tls --from hello@not-relevant.test --to user@example.com\n
--server
as the DMS FQDN or an IP address, where either should connect to the reverse proxy service.not-relevant.test
technically may be subject to some tests, at least for port 25. With the submission(s) ports those should be exempt.-tls
will use STARTTLS on port 25, you can exclude it to send unencrypted, but it would still go through the same port/route being tested.--port 587 -tls
or --port 465 -tlsc
with your credentials --auth-user user@example.com --auth-password secret
--tls-sni mail.example.com
if you have configured HostSNI
in Traefik router rules (SNI routing is only valid for implicit TLS ports).Testing from the Docker host technically works, however the IP is likely subject to more manipulation via iptables
than an external client.
The IP will likely appear as from the gateway IP of the Docker network associated to the reverse proxy, where that gateway IP then becomes the client IP when writing the PROXY protocol header.
"},{"location":"examples/tutorials/mailserver-behind-proxy/#security-concerns","title":"Security concerns","text":""},{"location":"examples/tutorials/mailserver-behind-proxy/#forgery","title":"Forgery","text":"Since the PROXY protocol sends a header with the client IP rewritten for software to use instead, this could be abused by bad actors.
Software on the receiving end of the connection often supports configuring an IP or CIDR range of clients to trust receiving the PROXY protocol header from.
Risk exposureIf you trust more than the reverse proxy IP, you must consider the risk exposure:
127.0.0.1
, or a private subnet 172.16.0.0/12
).iptables
with the kernel tunable sysctl net.ipv4.ip_forward=1
(enabled implicitly). Port access is via HOST:CONTAINER
ports published to their respective interface(s), that includes the container IP + port.While some concerns raised above are rather specific, these type of issues aren't exclusive to Docker and difficult to keep on top of as software is constantly changing. Limit the trusted networks where possible.
Postfix has no concept of trusted proxiesPostfix does not appear to have a way to configure trusted proxies like Dovecot does (haproxy_trusted_networks
).
postscreen_access_list
(or smtpd_client_restrictions
with check_client_access
for ports 587/465) can both restrict access by IP via a CIDR lookup table, however the client IP is already rewritten at this point via PROXY protocol.
Thus those settings cannot be used for restricting access to only trusted proxies, only to the actual clients.
A similar setting mynetworks
/ PERMIT_DOCKER
manages elevated trust for bypassing security restrictions. While it is intended for trusted clients, it has no relevance to trusting proxies for the same reasons.
While PROXY protocol works well with the reverse proxy, you may have some containers internally that interact with DMS on behalf of multiple clients.
Roundcube + Fail2BanYou may have other services with functionality like an API to send mail through DMS that likewise delegates credentials through DMS.
Roundcube is an example of this where authentication is delegated to DMS, which introduces the same concern with loss of client IP.
You should adjust configuration of these monitoring services to monitor for auth failures from those services directly instead, adding an exclusion for that service IP from any DMS logs monitored (but be mindful of PROXY header forgery risks).
"},{"location":"examples/use-cases/auth-lua/","title":"Examples | Use Cases | Lua Authentication","text":""},{"location":"examples/use-cases/auth-lua/#introduction","title":"Introduction","text":"Dovecot has the ability to let users create their own custom user provisioning and authentication providers in Lua. This allows any data source that can be approached from Lua to be used for authentication, including web servers. It is possible to do more with Dovecot and Lua, but other use cases fall outside of the scope of this documentation page.
Community contributed guide
Dovecot authentication via Lua scripting is not officially supported in DMS. No assistance will be provided should you encounter any issues.
DMS provides the required packages to support this guide. Note that these packages will be removed should they introduce any future maintenance burden.
The example in this guide relies on the current way in which DMS works with Dovecot configuration files. Changes to this to accommodate new authentication methods such as OpenID Connect will likely break this example in the future. This guide is updated on a best-effort base.
Dovecot's Lua support can be used for user provisioning (userdb functionality) and/or password verification (passdb functionality). Consider using other userdb and passdb options before considering Lua, since Lua does require the use of additional (unsupported) program code that might require maintenance when updating DMS.
Each implementation of Lua-based authentication is custom. Therefore it is impossible to write documentation that covers every scenario. Instead, this page describes a single example scenario. If that scenario is followed, you will learn vital aspects that are necessary to kickstart your own Lua development:
This scenario starts with DMS being configured to use LDAP for mailbox identification, user authorization and user authentication. In this scenario, Nextcloud is also a service that uses the same LDAP server for user identification, authorization and authentication.
The goal of this scenario is to have Dovecot not authenticate the user against LDAP, but against Nextcloud using an application password. The idea behind this is that a compromised mailbox password does not compromise the user's account entirely. To make this work, Nextcloud is configured to deny the use of account passwords by clients and to disable account password reset through mail verification.
If the application password is configured correctly, an adversary can only use it to access the user's mailbox on DMS, and CalDAV and CardDAV data on Nextcloud. File access through WebDAV can be disabled for the application password used to access mail. Having CalDAV and CardDAV compromised by the same password is a minor setback. If an adversary gets access to a Nextcloud application password through a device of the user, it is likely that the adversary also gets access to the user's calendars and contact lists anyway (locally or through the same account settings used for mail and CalDAV/CardDAV synchronization). The user's stored files in Nextcloud, the LDAP account password and any other services that rely on it would still be protected. A bonus is that a user is able to revoke and renew the mailbox password in Nextcloud for whatever reason, through a friendly user interface with all the security measures with which the Nextcloud instance is configured (e.g. verification of the current account password).
A drawback of this method is that any (compromised) Nextcloud application password can be used to access the user's mailbox. This introduces a risk that a Nextcloud application password used for something else (e.g. WebDAV file access) is compromised and used to access the user's mailbox. Discussion of that risk and possible mitigations fall outside of the scope of this scenario.
To answer the questions asked earlier for this specific scenario:
While it is possible to extend the authentication methods which Nextcloud can facilitate with Nextcloud apps, there is currently a mismatch between what DMS supports and what Nextcloud applications can provide. This might change in the future. For now, Lua will be used to bridge the gap between DMS and Nextcloud for authentication only (Dovecot passdb), while LDAP will still be used to identify mailboxes and verify authorization (Dovecot userdb).
"},{"location":"examples/use-cases/auth-lua/#modify-dovecots-configuration","title":"Modify Dovecot's configuration","text":"Add to DMS volumes incompose.yaml
# All new volumes are marked :ro to configure them as read-only, since their contents are not changed from inside the container\n volumes:\n # Configuration override to disable LDAP authentication\n - ./docker-data/dms/config/dovecot/auth-ldap.conf.ext:/etc/dovecot/conf.d/auth-ldap.conf.ext:ro\n # Configuration addition to enable Lua authentication\n - ./docker-data/dms/config/dovecot/auth-lua-httpbasic.conf:/etc/dovecot/conf.d/auth-lua-httpbasic.conf:ro\n # Directory containing Lua scripts\n - ./docker-data/dms/config/dovecot/lua/:/etc/dovecot/lua/:ro\n
Create a directory for Lua scripts:
mkdir -p ./docker-data/dms/config/dovecot/lua\n
Create configuration file ./docker-data/dms/config/dovecot/auth-ldap.conf.ext
for LDAP user provisioning:
userdb {\n driver = ldap\n args = /etc/dovecot/dovecot-ldap.conf.ext\n}\n
Create configuration file ./docker-data/dms/config/dovecot/auth-lua-httpbasic.conf
for Lua user authentication:
passdb {\n driver = lua\n args = file=/etc/dovecot/lua/auth-httpbasic.lua blocking=yes\n}\n
That is all for configuring Dovecot.
"},{"location":"examples/use-cases/auth-lua/#create-the-lua-script","title":"Create the Lua script","text":"Create Lua file ./docker-data/dms/config/dovecot/lua/auth-httpbasic.lua
with contents:
local http_url = \"https://nextcloud.example.com/remote.php/dav/\"\nlocal http_method = \"PROPFIND\"\nlocal http_status_ok = 207\nlocal http_status_failure = 401\nlocal http_header_forwarded_for = \"X-Forwarded-For\"\n\npackage.path = package.path .. \";/etc/dovecot/lua/?.lua\"\nlocal base64 = require(\"base64\")\n\nlocal http_client = dovecot.http.client {\n timeout = 1000;\n max_attempts = 1;\n debug = false;\n}\n\nfunction script_init()\n return 0\nend\n\nfunction script_deinit()\nend\n\nfunction auth_passdb_lookup(req)\n local auth_request = http_client:request {\n url = http_url;\n method = http_method;\n }\n auth_request:add_header(\"Authorization\", \"Basic \" .. (base64.encode(req.user .. \":\" .. req.password)))\n auth_request:add_header(http_header_forwarded_for, req.remote_ip)\n local auth_response = auth_request:submit()\n local resp_status = auth_response:status()\n local reason = auth_response:reason()\n\n local returnStatus = dovecot.auth.PASSDB_RESULT_INTERNAL_FAILURE\n local returnDesc = http_method .. \" - \" .. http_url .. \" - \" .. resp_status .. \" \" .. reason\n if resp_status == http_status_ok\n then\n returnStatus = dovecot.auth.PASSDB_RESULT_OK\n returnDesc = \"nopassword=y\"\n elseif resp_status == http_status_failure\n then\n returnStatus = dovecot.auth.PASSDB_RESULT_PASSWORD_MISMATCH\n returnDesc = \"\"\n end\n return returnStatus, returnDesc\nend\n
Replace the hostname in the URL to the actual hostname of Nextcloud.
Dovecot provides an HTTP client for use in Lua. Aside of that, Lua by itself is pretty barebones. It chooses library compactness over included functionality. You can see that in that a separate library is referenced to add support for Base64 encoding, which is required for HTTP basic access authentication. This library (also a Lua script) is not included. It must be downloaded and stored in the same directory:
cd ./docker-data/dms/config/dovecot/lua\ncurl -JLO https://raw.githubusercontent.com/iskolbin/lbase64/master/base64.lua\n
Only use native (pure Lua) libraries as dependencies if possible, such as base64.lua
from the example. This ensures maximum compatibility. Performance is less of an issue since Lua scripts written for Dovecot probably won't be long or complex, and there won't be a lot of data processing by Lua itself.
To see which Lua version is used by Dovecot if you plan to do something that is version dependent, run:
docker exec CONTAINER_NAME strings /usr/lib/dovecot/libdovecot-lua.so|grep '^LUA_'\n
While Dovecot logs the status of authentication attempts for any passdb backend, Dovecot will also log Lua scripting errors and messages sent to Dovecot's Lua API log functions. The combined DMS log (including that of Dovecot) can be viewed using docker logs CONTAINER_NAME
. If the log is too noisy (due to other processes in the container also logging to it), docker exec CONTAINER_NAME cat /var/log/mail/mail.log
can be used to view the log of Dovecot and Postfix specifically.
If working with HTTP in Lua, setting debug = true;
when initiating dovecot.http.client
will create debug log messages for every HTTP request and response.
Note that Lua runs compiled bytecode, and that scripts will be compiled when they are initially started. Once compiled, the bytecode is cached and changes in the Lua script will not be processed automatically. Dovecot will reload its configuration and clear its cached Lua bytecode when running docker exec CONTAINER_NAME dovecot reload
. A (changed) Lua script will be compiled to bytecode the next time it is executed after running the Dovecot reload command.
Advice not extensively tested
This configuration advice is a community contribution which has only been verified as a solution when using network: host
, where you have direct access to the host interfaces.
It may be applicable in other network modes if the container has control of the outbound IPs to bind to. This is not the case with bridge networks that typically bind to a private range network for containers which are bridged to a public interface via Docker.
If your Docker host is running multiple IPv4 and IPv6 IP-addresses, it may be beneficial to bind outgoing SMTP connections to specific IP-address / interface.
PTR
record for that IP resolves an address back to the same IP.reject_unknown_sender
).This can be configured by overriding the default Postfix configurations DMS provides. Create postfix-master.cf
and postfix-main.cf
files for your config volume (docker-data/dms/config
).
In postfix-main.cf
you'll have to set the smtp_bind_address
and smtp_bind_address6
to the respective IP-address on the server you want to use.
Example
Contributed solutionAlternative (unverified) postfix-main.cfsmtp_bind_address = 198.51.100.42\nsmtp_bind_address6 = 2001:DB8::42\n
Inheriting the bind from main.cf
can misconfigure services
One problem when setting smtp_bind_address
in main.cf
is that it will be inherited by any services in master.cf
that extend the smtp
transport. One of these is smtp-amavis
, which is explicitly configured to listen / connect via loopback (localhost / 127.0.0.1
).
A postfix-master.cf
override can workaround that issue by ensuring smtp-amavis
binds to the expected internal IP:
smtp-amavis/unix/smtp_bind_address=127.0.0.1\nsmtp-amavis/unix/smtp_bind_address6=::1\n
A potentially better solution might be to instead explicitly set the smtp_bind_address
override on the smtp
transport service:
smtp/inet/smtp_bind_address = 198.51.100.42\nsmtp/inet/smtp_bind_address6 = 2001:DB8::42\n
If that avoids the concern with smtp-amavis
, you may still need to additionally override for the relay
transport as well if you have configured DMS to relay mail.
IP addresses for documentation
IP addresses shown in above examples are placeholders, they are IP addresses reserved for documentation by IANA (RFC-5737 (IPv4) and RFC-3849 (IPv6)). Replace them with the IP addresses you want DMS to send mail through.
"},{"location":"examples/use-cases/external-relay-only-mailserver/","title":"Use Cases | Relay inbound and outbound mail for an internal DMS","text":""},{"location":"examples/use-cases/external-relay-only-mailserver/#introduction","title":"Introduction","text":"Community contributed guide
Adapted into a guide from this discussion.
Requirements:
mynetworks
setting).The guide below will assume the VPN is setup on 192.168.2.0/24
with:
192.168.2.2
192.168.2.3
The goal of this guide is to configure a public server that can receive inbound mail and relay that over to DMS on a private server, which can likewise submit mail outbound through a public server or service.
The primary motivation is to keep your mail storage private instead of storing it to disk unencrypted on a VPS host.
"},{"location":"examples/use-cases/external-relay-only-mailserver/#dns-setup","title":"DNS setup","text":"Follow our standard guidance for DNS setup.
Set your A, MX and PTR records for the public server as if it were running DMS.
DNS Zone file example
For this guide, we assume DNS is configured with:
11.22.33.44
@example.com
addresses must have an MX record pointing to mail.example.com
.mail.example.com
pointing to the IP address of your public server.$ORIGIN example.com\n@ IN A 11.22.33.44\nmail IN A 11.22.33.44\n\n; mail server for example.com\n@ IN MX 10 mail.example.com.\n
SPF records should also be set up as you normally would for mail.example.com
.
You will need to install Postfix on your public server. The functionality that is needed for this setup is not yet implemented in DMS, so a vanilla Postfix will probably be easier to work with, especially since this server will only be used as an inbound and outbound relay.
It's necessary to adjust some settings afterwards.
Postfix main configRoute outbound mail through a separate transportConfigure recipient domains to relay mail Create or replace/etc/postfix/main.cf
# See /usr/share/postfix/main.cf.dist for a commented, more complete version\n\nsmtpd_banner = $myhostname ESMTP $mail_name (Debian/GNU)\nbiff = no\n\n# appending .domain is the MUA's job.\nappend_dot_mydomain = no\n\n# Uncomment the next line to generate \"delayed mail\" warnings\n#delay_warning_time = 4h\n\n# See http://www.postfix.org/COMPATIBILITY_README.html -- default to 3.6 on\n# fresh installs.\ncompatibility_level = 3.6\n\n# TLS parameters\nsmtpd_tls_cert_file=/etc/postfix/certificates/mail.example.com.crt\nsmtpd_tls_key_file=/etc/postfix/certificates/mail.example.com.key\nsmtpd_tls_security_level=may\nsmtp_tls_CApath=/etc/ssl/certs\nsmtp_tls_security_level=may\nsmtp_tls_session_cache_database = btree:${data_directory}/smtp_scache\n\nalias_database = hash:/etc/aliases\nalias_maps = hash:/etc/aliases\nmaillog_file = /var/log/postfix.log\nmailbox_size_limit = 0\ninet_interfaces = all\ninet_protocols = ipv4\nreadme_directory = no\nrecipient_delimiter = +\n\n# Customizations relevant to this guide:\nmyhostname = mail.example.com\nmyorigin = example.com\nmydestination = localhost\nmynetworks = 127.0.0.0/8 [::ffff:127.0.0.0]/104 [::1]/128 192.168.2.0/24\nsmtpd_relay_restrictions = permit_mynetworks permit_sasl_authenticated defer_unauth_destination\ntransport_maps = hash:/etc/postfix/transport\nrelay_domains = $mydestination, hash:/etc/postfix/relay\n\n# Disable local system accounts and delivery:\nlocal_recipient_maps =\nlocal_transport = error:local mail delivery is disabled\n
Let's highlight some of the important parts:
mail.example.com
in mydestination
, in fact you can just set localhost
or nothing at all here as we want all mail to be relayed to our private server (DMS).mynetworks
should contain your VPN network (eg: 192.168.2.0/24
subnet).transport_maps = hash:/etc/postfix/transport
and relay_domains = $mydestination, hash:/etc/postfix/relay
, with their file contents covered below.local_recipient_maps
.mail.example.com
.Open relay
Please be aware that setting mynetworks
to a public CIDR will leave you with an open relay. Only set it to the CIDR of your VPN beyond the localhost ranges.
When mail arrives to the public server for an @example.com
address, we want to send it via the relay
transport to our private server over port 25 for delivery to DMS.
transport_maps
is configured with a transport
table file that matches recipient addresses and assigns a non-default transport. This setting has priority over relay_transport
.
Create /etc/postfix/transport
example.com relay:[192.168.2.3]:25\n
Other considerations:
* relay:[X.X.X.X]:port
to the bottom (eg: * relay:[relay1.org]:587
), which will relay everything outbound via this relay host.Tip
Instead of a file, you could alternatively configure main.cf
with transport_maps = inline:{ example.com=relay:[192.168.2.3]:25 }
We want example.com
to be relayed inbound and everything else relayed outbound.
relay_domains
is configured with a file with a list of domains that should be relayed (one per line), the 2nd value is required but can be anything.
Create /etc/postfix/relay
example.com OK\n
Tip
Instead of a file, you could alternatively configure main.cf
with relay_domains = example.com
.
Files configured with hash:
table type must run postmap
to apply changes
Run postmap /etc/postfix/transport
and postmap /etc/postfix/relay
after creating or updating either of these files, this processes them into a separate file for Postfix to use.
You can set up your DMS instance as you normally would.
mail.example.com
. Instead, use internal-mail.example.com
or something similar.mail.example.com
.Next, we need to configure our private server to relay all outbound mail through the public server (or a separate smarthost service). The setup is similar to the default relay setup.
Configure the relay hostTrust the public serverCreate postfix-relaymap.cf
@example.com [192.168.2.2]:25\n
Meaning all mail sent outbound from @example.com
addresses will be relayed through the public server at that VPN IP.
The public server mynetworks
setting from earlier trusts any mail received on port 25 from the VPN network, which is what allows the mail to be sent outbound when it'd otherwise be denied.
Create postfix-main.cf
mynetworks = 192.168.2.0/24\n
This will trust any connection from the VPN network to DMS, such as from the public server when relaying mail over to DMS at the private server.
This step is necessary to skip some security measures that DMS normally checks for, like verifying DNS records like SPF are valid. As the mail is being relayed, those checks would fail otherwise as the IP of your public server would not be authorized to send mail on behalf of the sender address in mail being relayed.
Alternative tomynetworks
setting Instead of trusting connections by their IP with the mynetworks
setting, those same security measures can be skipped for any authenticated deliveries to DMS over port 587 instead.
This is a bit more work. mynetworks
on the public server main.cf
Postfix config is for trusting DMS when it sends mail from the private server, thus you'll need to have that public Postfix service configured with a login account that DMS can use.
On the private server, DMS needs to know the credentials for that login account, that is handled with postfix-sasl-password.cf
:
@example.com user:secret\n
You could also relay mail through SendGrid, AWS SES or similar instead of the public server you're running to receive mail from. Login credentials for those relay services are provided via the same postfix-sasl-password.cf
file.
Likewise for the public server to send mail to DMS, it would need to be configured to relay mail with credentials too, removing the need for mynetworks
on the DMS postfix-main.cf
config.
The extra effort to require authentication instead of blind trust of your private subnet can be beneficial at reducing the impact of a compromised system or service on that network that wasn't expected to be permitted to send mail.
"},{"location":"examples/use-cases/external-relay-only-mailserver/#imap-pop3","title":"IMAP / POP3","text":"IMAP and POP3 need to point towards your private server, since that is where the mailboxes are located, which means you need to have a way for your MUA to connect to it.
"},{"location":"examples/use-cases/forward-only-mailserver-with-ldap-authentication/","title":"Use Cases | Forward-Only Mail Server with LDAP","text":""},{"location":"examples/use-cases/forward-only-mailserver-with-ldap-authentication/#building-a-forward-only-mail-server","title":"Building a Forward-Only Mail Server","text":"A forward-only mail server does not have any local mailboxes. Instead, it has only aliases that forward emails to external email accounts (for example to a Gmail account). You can also send email from the localhost (the computer where DMS is installed), using as sender any of the alias addresses.
The important settings for this setup (on mailserver.env
) are these:
PERMIT_DOCKER=host\nENABLE_POP3=\nENABLE_CLAMAV=0\nSMTP_ONLY=1\nENABLE_SPAMASSASSIN=0\nENABLE_FETCHMAIL=0\n
Since there are no local mailboxes, we use SMTP_ONLY=1
to disable dovecot
. We disable as well the other services that are related to local mailboxes (POP3
, ClamAV
, SpamAssassin
, etc.)
We can create aliases with ./setup.sh
, like this:
./setup.sh alias add <alias-address> <external-email-account>\n
"},{"location":"examples/use-cases/forward-only-mailserver-with-ldap-authentication/#authenticating-with-ldap","title":"Authenticating with LDAP","text":"If you want to send emails from outside the mail server you have to authenticate somehow (with a username and password). One way of doing it is described in this discussion. However if there are many user accounts, it is better to use authentication with LDAP. The settings for this on mailserver.env
are:
ACCOUNT_PROVISIONER=LDAP\nLDAP_START_TLS=yes\nLDAP_SERVER_HOST=ldap.example.org\nLDAP_SEARCH_BASE=ou=users,dc=example,dc=org\nLDAP_BIND_DN=cn=mailserver,dc=example,dc=org\nLDAP_BIND_PW=pass1234\n\nENABLE_SASLAUTHD=1\nSASLAUTHD_MECHANISMS=ldap\nSASLAUTHD_LDAP_SERVER=ldap.example.org\nSASLAUTHD_LDAP_START_TLS=yes\nSASLAUTHD_LDAP_BIND_DN=cn=mailserver,dc=example,dc=org\nSASLAUTHD_LDAP_PASSWORD=pass1234\nSASLAUTHD_LDAP_SEARCH_BASE=ou=users,dc=example,dc=org\nSASLAUTHD_LDAP_FILTER=(&(uid=%U)(objectClass=inetOrgPerson))\n
My LDAP data structure is very basic, containing only the username, password, and the external email address where to forward emails for this user. An entry looks like this:
add uid=username,ou=users,dc=example,dc=org\nuid: username\nobjectClass: inetOrgPerson\nsn: username\ncn: username\nuserPassword: {SSHA}abcdefghi123456789\nemail: external-account@gmail.com\n
This structure is different from what is expected/assumed from the configuration scripts of DMS, so it doesn't work just by using the LDAP_QUERY_FILTER_...
settings. Instead, I had to use a custom configuration (via user-patches.sh
). I created the script docker-data/dms/config/user-patches.sh
, with content like this:
#!/bin/bash\n\nrm -f /etc/postfix/{ldap-groups.cf,ldap-domains.cf}\n\npostconf \\\n \"virtual_mailbox_domains = /etc/postfix/vhost\" \\\n \"virtual_alias_maps = ldap:/etc/postfix/ldap-aliases.cf texthash:/etc/postfix/virtual\" \\\n \"smtpd_sender_login_maps = ldap:/etc/postfix/ldap-users.cf\"\n\nsed -i /etc/postfix/ldap-users.cf \\\n -e '/query_filter/d' \\\n -e '/result_attribute/d' \\\n -e '/result_format/d'\ncat <<EOF >> /etc/postfix/ldap-users.cf\nquery_filter = (uid=%u)\nresult_attribute = uid\nresult_format = %s@example.org\nEOF\n\nsed -i /etc/postfix/ldap-aliases.cf \\\n -e '/domain/d' \\\n -e '/query_filter/d' \\\n -e '/result_attribute/d'\ncat <<EOF >> /etc/postfix/ldap-aliases.cf\ndomain = example.org\nquery_filter = (uid=%u)\nresult_attribute = mail\nEOF\n\npostfix reload\n
You see that besides query_filter
, I had to customize as well result_attribute
and result_format
.
See also
For more details about using LDAP see: LDAP managed mail server with Postfix and Dovecot for multiple domains
Note
Another solution that serves as a forward-only mail server is this.
"},{"location":"examples/use-cases/imap-folders/","title":"Mailboxes (aka IMAP Folders)","text":"INBOX
is setup as the private inbox
namespace. By default target/dovecot/15-mailboxes.conf
configures the special IMAP folders Drafts
, Sent
, Junk
and Trash
to be automatically created and subscribed. They are all assigned to the private inbox
namespace (which implicitly provides the INBOX
folder).
These IMAP folders are considered special because they add a \"SPECIAL-USE\" attribute, which is a standardized way to communicate to mail clients that the folder serves a purpose like storing spam/junk mail (\\Junk
) or deleted mail (\\Trash
). This differentiates them from regular mail folders that you may use for organizing.
See target/dovecot/15-mailboxes.conf
for existing mailbox folders which you can modify or uncomment to enable some other common mailboxes. For more information try the official Dovecot documentation.
The Archive
special IMAP folder may be useful to enable. To do so, make a copy of target/dovecot/15-mailboxes.conf
and uncomment the Archive
mailbox definition. Mail clients should understand that this folder is intended for archiving mail due to the \\Archive
\"SPECIAL-USE\" attribute.
With the provided compose.yaml example, a volume bind mounts the host directory docker-data/dms/config/
to the container location /tmp/docker-mailserver/
. Config file overrides should instead be mounted to a different location as described in Overriding Configuration for Dovecot:
volumes:\n - ./docker-data/dms/config/dovecot/15-mailboxes.conf:/etc/dovecot/conf.d/15-mailboxes.conf:ro\n
"},{"location":"examples/use-cases/imap-folders/#caution","title":"Caution","text":""},{"location":"examples/use-cases/imap-folders/#adding-folders-to-an-existing-setup","title":"Adding folders to an existing setup","text":"Handling of newly added mailbox folders can be inconsistent across mail clients:
SPECIAL-USE
attributes","text":"Not all mail clients support the SPECIAL-USE
attribute for mailboxes (defined in RFC 6154). These clients will treat the mailbox folder as any other, using the name assigned to it instead.
Some clients may still know to treat these folders for their intended purpose if the mailbox name matches the common names that the SPECIAL-USE
attributes represent (eg Sent
as the mailbox name for \\Sent
).
Usually the mail client will know via context such as the SPECIAL-USE
attribute or common English mailbox names, to provide a localized label for the users preferred language.
Take care to test localized names work well as well.
"},{"location":"examples/use-cases/imap-folders/#email-clients-support","title":"Email Clients Support","text":"SPECIAL-USE
attribute enabled for archives:Archives
folder on the server.Archive
.SPECIAL-USE
attribute is enabled for archives:Windows Mail
Windows Mail has been said to ignore SPECIAL-USE
attribute and look only at the mailbox folder name assigned.
Needs citation
This information is provided by the community.
It presently lacks references to confirm the behavior. If any information is incorrect please let us know!
"},{"location":"examples/use-cases/ios-mail-push-support/","title":"Advanced | iOS Mail Push Support","text":""},{"location":"examples/use-cases/ios-mail-push-support/#introduction","title":"Introduction","text":"iOS Mail currently does not support the IMAP idle extension. Therefore users can only either check manually or configure intervals for fetching mails in their mail account preferences when using the default configuration.
To support mail push Dovecot needs to advertise the XAPPLEPUSHSERVICE
IMAP extension as well as sending the actual push notifications to the Apple Push Notification service (APNs) which will forward them to the device.
This can be done with two components:
dovecot-xaps-plugin
) which is triggered whenever a mail is created or moved from/to a mail folder.dovecot-xaps-daemon
) that manages both the device registrations as well as sending notifications to the APNs.docker-mailserver
image.Both components will be built using Docker and included into a custom docker-mailserver
image. Afterwards the required configuration is added to docker-data/dms/config
. The registration data is stored in /var/mail-state/lib-xapsd
.
Create a Dockerfile to build a docker-mailserver
image that includes the dovecot-xaps-plugin
as well as the dovecot-xaps-daemon
. This is required to ensure that the Dovecot plugin is built against the same Dovecot version. The :edge
tag is used here, but you might want to use a released version instead.
FROM mailserver/docker-mailserver:edge AS dovecot-plugin-xaps\nWORKDIR /tmp/dovecot-xaps-plugin\nRUN <<EOF\n apt-get update\n apt-get -y --no-install-recommends install git cmake make build-essential dovecot-dev\n git clone --single-branch --depth=1 https://github.com/freswa/dovecot-xaps-plugin.git .\n mkdir build && cd build\n cmake .. -DCMAKE_BUILD_TYPE=Release\n make install\nEOF\n\n# Use an older Go version as Go >= 1.20 causes this issue: https://github.com/freswa/dovecot-xaps-daemon/issues/24#issuecomment-1483876081\n# Note that the underlying issue are non-standard-compliant Apple http servers which might get fixed at some point\nFROM golang:1.19-alpine AS dovecot-xaps-daemon\nENV GOPROXY=https://proxy.golang.org,direct\nENV CGO_ENABLED=0\nWORKDIR /go/dovecot-xaps-daemon\nRUN <<EOF\n apk add --no-cache --virtual build-dependencies git\n git clone --single-branch --depth=1 https://github.com/freswa/dovecot-xaps-daemon .\n go build ./cmd/xapsd\nEOF\n\nFROM mailserver/docker-mailserver:edge\nCOPY --from=dovecot-plugin-xaps /usr/lib/dovecot/modules/*_xaps_* /usr/lib/dovecot/modules/\nCOPY --from=dovecot-xaps-daemon /go/dovecot-xaps-daemon/xapsd /usr/bin/xapsd\n\n# create a non-root user for the daemon process as well as configuration and run state directories\nRUN <<EOF\n adduser --quiet --system --group --disabled-password --home /var/mail-state/lib-xapsd --no-create-home xapsd\n mkdir -p /var/run/xapsd /etc/xapsd\nEOF\n
Build the new image:
docker build -t yourname/docker-mailserver .\n
Modify your compose.yaml
to use the newly created image:
services:\n mailserver:\n image: yourname/docker-mailserver:latest\n
Recreate the container:
docker compose down\ndocker compose up -d\n
Create a hash of your Apple developer account password using the provided xapsd -pass
command:
docker exec -it mailserver xapsd -pass\n
Add configuration for both components:
Create a folder named xaps
in docker-data/dms/config
.
Create a file named xapsd.yaml
in docker-data/dms/config/xaps
.
appleId
and appleIdHashedPassword
with your actual credentials. For reference see also here.# set the loglevel to either\n# trace, debug, error, fatal, info, panic or warn\n# Default: info\nloglevel: info\n\n# xapsd creates a json file to store the registration persistent on disk.\n# This sets the location of the file.\ndatabaseFile: /var/mail-state/lib-xapsd/database.json\n\n# xapsd listens on a socket for http/https requests from the dovecot plugin.\n# This sets the address and port number of the listen socket.\nlistenAddr: '127.0.0.1'\nport: 11619\n\n# xapsd is able to listen on a HTTPS Socket to allow HTTP/2 to be used\n# SSL is enabled implicitly when certfile and keyfile exist\n# !!! only use HTTPS for connection pooling with a proxy e.g. nginx or HaProxy\n# !!! direct usage with the plugin is discouraged and unsupported\ntlsCertfile:\ntlsKeyfile:\ntlsListenAddr:\ntlsPort: 11620\n\n# Notifications that are not initiated by new messages are not sent immediately for two reasons:\n# 1. When you move/copy/delete messages you most likely move/copy/delete more messages within a short period of time.\n# 2. You don't need your mailboxes to synchronize immediately since they are automatically synchronized when opening\n# the app\n# If a new message comes and the move/copy/delete notification is still on hold it will be sent with the notification\n# for the new message.\n# This sets the interval to check for delayed messages.\ncheckInterval: 20\n\n# Set the time how long notifications for not-new messages should be delayed until they are sent.\n# Whenever checkInterval runs, it checks if \"delay\" <= \"waiting time\" and sends the notification if the expression is\n# true.\ndelay: 30\n\n# To retrieve certificates from Apple, we need to login with a valid Apple ID\n# The accounts email must be given in cleartext, but the password has to\n# be hashed before sending it. To not leak working credentials on running servers,\n# we do not accept the cleartext password here.\nappleId: foo@example.com\n\n# use `xaps -pass` to calculate the hash of the apple id password\nappleIdHashedPassword: bar\n
Create a file named 95-xaps.conf
in docker-data/dms/config/xaps
. For reference see also here. 95-xaps.conf
protocol imap {\n mail_plugins = $mail_plugins notify push_notification xaps_push_notification xaps_imap\n}\n\nprotocol lda {\n mail_plugins = $mail_plugins notify push_notification xaps_push_notification\n}\n\nprotocol lmtp {\n mail_plugins = $mail_plugins notify push_notification xaps_push_notification\n}\n\nplugin {\n # xaps_config contains xaps specific configuration parameters\n # url: protocol, hostname and port under which xapsd listens\n # user_lookup: Use if you want to determine the username used for PNs from environment variables provided by\n # login mechanism. Value is variable name to look up.\n # max_retries: maximum num of retries the http client connects to the xaps daemon\n # timeout_msecs http timeout of the http connection\n xaps_config = url=http://127.0.0.1:11619 user_lookup=theattribute max_retries=6 timeout_msecs=5000\n push_notification_driver = xaps\n}\n
Create a supervisord file named xapsd.conf
in docker-data/dms/config/xaps
with the following content: xapsd.conf
[program:xapsd]\nstartsecs=0\nautostart=false\nautorestart=true\nstdout_logfile=/var/log/supervisor/%(program_name)s.log\nstderr_logfile=/var/log/supervisor/%(program_name)s.log\nuser=xapsd\ncommand=/usr/bin/xapsd\npidfile=/var/run/xapsd/xapsd.pid\n
Create or update your user-patches.sh
in docker-data/dms/config
to move the files to their final location as well as starting the daemon service: user-patches.sh
#!/bin/bash\n\n# Copy the configs to internal locations:\ncp /tmp/docker-mailserver/xaps/95-xaps.conf /etc/dovecot/conf.d/95-xaps.conf\ncp /tmp/docker-mailserver/xaps/xapsd.yaml /etc/xapsd/xapsd.yaml\ncp /tmp/docker-mailserver/xaps/xapsd.conf /etc/supervisor/conf.d/xapsd.conf\n\n# Setup data persistence and ensure ownership is always for xapsd:\nmkdir -p /var/mail-state/lib-xapsd\nchown -R xapsd:xapsd /var/mail-state/lib-xapsd\n\n# Start the xaps daemon:\nsupervisorctl update\nsupervisorctl start xapsd\n
Recreate the container again to apply the new configuration:
docker compose down\ndocker compose up -d\n
Recreate your mail account on your iOS device and check the logs in /var/log/supervisor/dovecot.log
and /var/log/supervisor/xapsd.log
for any errors.
Both device registration and notifications send a username to the daemon to lookup the device. While the registration and other IMAP operations in Dovecot will send the Dovecot username, LMTP will send the provided authentication username.
The format of that username is specified by the auth_username_format
Dovecot setting. If you are not using mail addresses as Dovecot usernames - e.g. when using LDAP - you can either change the auth_username_format
or add the mail address as property to the user account and use the lookup feature (see below).
sed -i -r \"s|^#?(auth_username_format =).*|\\1 %Ln|\" /etc/dovecot/conf.d/10-auth.conf\n
You can also use notifications for Dovecot alias mailboxes. Depending on your server configuration, this might require to add the original Dovecot username as Dovecot attribute to the login user as well as changing the user_lookup=theattribute
in 95-xaps.conf
to perform the lookup of that attribute.