summaryrefslogtreecommitdiff
path: root/playbooks/roles/mail
diff options
context:
space:
mode:
Diffstat (limited to 'playbooks/roles/mail')
-rw-r--r--playbooks/roles/mail/tasks/main.yml22
-rw-r--r--playbooks/roles/mail/templates/stacks/docker-compose.yml98
-rw-r--r--playbooks/roles/mail/templates/volumes/data/dms/config/dovecot.cf27
-rw-r--r--playbooks/roles/mail/templates/volumes/data/dms/config/postfix-master.cf3
-rwxr-xr-xplaybooks/roles/mail/templates/volumes/data/dms/config/user-patches.sh13
-rw-r--r--playbooks/roles/mail/templates/volumes/scripts/check-postfix-health.unused.sh24
-rw-r--r--playbooks/roles/mail/templates/volumes/scripts/wait-for-cert.sh23
-rw-r--r--playbooks/roles/mail/templates/volumes/scripts/wait-for-postfix.unused.sh27
8 files changed, 163 insertions, 74 deletions
diff --git a/playbooks/roles/mail/tasks/main.yml b/playbooks/roles/mail/tasks/main.yml
index 4576500..b2a7ea8 100644
--- a/playbooks/roles/mail/tasks/main.yml
+++ b/playbooks/roles/mail/tasks/main.yml
@@ -1,19 +1,9 @@
---
-- name: Build mail compose dirs
- ansible.builtin.file:
- state: directory
- dest: '{{ mail_base }}/{{ item.path }}'
- with_filetree: '../templates'
- when: item.state == 'directory'
+- name: Deploy mail
+ ansible.builtin.import_tasks: manage-docker-swarm-service.yml
+ vars:
+ service_name: mail
+ template_render_dir: "../templates"
+ service_destination_dir: "{{ mail_base }}"
-- name: Build mail compose files
- ansible.builtin.template:
- src: '{{ item.src }}'
- dest: '{{ mail_base }}/{{ item.path }}'
- with_filetree: '../templates'
- when: item.state == 'file'
-
-- name: Deploy mail stack
- ansible.builtin.command:
- cmd: 'docker stack deploy -c {{ mail_base }}/stacks/docker-compose.yml mail'
diff --git a/playbooks/roles/mail/templates/stacks/docker-compose.yml b/playbooks/roles/mail/templates/stacks/docker-compose.yml
index 50108c1..b4cc3e0 100644
--- a/playbooks/roles/mail/templates/stacks/docker-compose.yml
+++ b/playbooks/roles/mail/templates/stacks/docker-compose.yml
@@ -4,11 +4,13 @@ services:
restart: always
volumes:
- {{ mail_base }}/volumes/data/roundcube/db:/var/roundcube/db
- - {{ mail_base }}/volumes/data/roundcube/config:/var/roundcube/config
+ - {{ mail_base }}/volumes/data/roundcube/config:/var/roundcube/config/
environment:
+ - DEPLOYMENT_TIME={{ now() }}
- ROUNDCUBEMAIL_DB_TYPE=sqlite
- - ROUNDCUBEMAIL_SKIN=elastic
+ - ROUNDCUBEMAIL_SKIN={{ roundcube_skin | default('elastic') }}
- ROUNDCUBEMAIL_PLUGINS={{ roundcube_plugins }}
+ - ROUNDCUBEMAIL_COMPOSER_PLUGINS={{ roundcube_composer_plugins }}
- ROUNDCUBEMAIL_DEFAULT_HOST={{ roundcube_default_host }}
- ROUNDCUBEMAIL_DEFAULT_PORT={{ roundcube_default_port }}
- ROUNDCUBEMAIL_SMTP_SERVER={{ roundcube_smtp_host }}
@@ -16,6 +18,11 @@ services:
networks:
- proxy
- roundcube
+ healthcheck:
+ test: ["CMD", "curl", "--fail", "http://localhost:8000"]
+ timeout: 3s
+ interval: 30s
+ retries: 2
deploy:
mode: replicated
replicas: 1
@@ -31,70 +38,62 @@ services:
mailserver:
image: ghcr.io/docker-mailserver/docker-mailserver:latest
hostname: {{ mail_domain }}
+ command:
+ - /scripts/wait-for-cert.sh
{% if homelab_build %}
- command:
- - /bin/sh
- - -c
- - |
- [ ! -f "/etc/letsencrypt/live/{{ mail_domain }}" ] && sleep 60 # Sleep until certificate requested from traefik
- supervisord -c /etc/supervisor/supervisord.conf
healthcheck:
disable: true
+{% else %}
+ healthcheck:
+ test: ["CMD-SHELL", "ss --listening --tcp | grep -P :smtp"]
+ interval: 3s
+ timeout: 2s
+ retries: 3
{% endif %}
+ ports:
+ - '25:25'
+ - '587:587'
+ - '465:465'
+ - '143:143'
+ - '993:993'
+ - '4190:4190'
+ - '110:110'
+ - '995:995'
+ stop_grace_period: 30s
deploy:
mode: replicated
replicas: 1
- labels:
- - traefik.enable=true
- - traefik.swarm.network=proxy
- # ManageSieve
- - traefik.tcp.routers.sieve.tls.passthrough=true
- - traefik.tcp.routers.sieve.rule=HostSNI(`*`)
- - traefik.tcp.routers.sieve.entrypoints=sieve
- - traefik.tcp.routers.sieve.service=sieve
- - traefik.tcp.services.sieve.loadbalancer.server.port=4190
- # IMAP
- - traefik.tcp.routers.imap.tls.passthrough=true
- - traefik.tcp.routers.imap.rule=HostSNI(`*`)
- - traefik.tcp.routers.imap.entrypoints=imap
- - traefik.tcp.routers.imap.service=imap
- - traefik.tcp.services.imap.loadbalancer.server.port=993
- # SMTPS
- - traefik.tcp.routers.smtps.tls.passthrough=true
- - traefik.tcp.routers.smtps.rule=HostSNI(`*`)
- - traefik.tcp.routers.smtps.entrypoints=smtps
- - traefik.tcp.routers.smtps.service=smtps
- - traefik.tcp.services.smtps.loadbalancer.server.port=465
- # SMTP (StartTLS)
- - traefik.tcp.routers.smtptls.tls.passthrough=true
- - traefik.tcp.routers.smtptls.rule=HostSNI(`*`)
- - traefik.tcp.routers.smtptls.entrypoints=smtptls
- - traefik.tcp.routers.smtptls.service=smtptls
- - traefik.tcp.services.smtptls.loadbalancer.server.port=587
- # SMTP ("ye' old")
- - traefik.tcp.routers.smtp.tls.passthrough=true
- - traefik.tcp.routers.smtp.rule=HostSNI(`*`)
- - traefik.tcp.routers.smtp.entrypoints=smtp
- - traefik.tcp.routers.smtp.service=smtp
- - traefik.tcp.services.smtp.loadbalancer.server.port=25
+ update_config:
+ parallelism: 1
+ failure_action: rollback
+ # order: start-first
+ # We need to stop the old container first because it holds a lock on the
+ # Postfix mail queue. I don't believe there is a feasible way to solve
+ # this without either a tiny bit of downtime waiting for the lock to clear,
+ # or lost mail since we'd have to ignore the lock and thus two competing mailservers
+ # are accepting mail.
+ # One of these is more acceptable than the other haha.
+ # See stuff in scripts/ for the last attempt if interested.
+ order: stop-first
volumes:
- - {{ mail_base }}/volumes/data/dms/vmail:/var/mail/
- - {{ mail_base }}/volumes/data/dms/mail-state:/var/mail-state/
- - {{ mail_base }}/volumes/data/dms/mail-logs:/var/log/mail/
- - {{ mail_base }}/volumes/data/dms/config:/tmp/docker-mailserver/
+ - {{ mail_base }}/volumes/scripts/:/scripts/
+ - {{ mail_base }}/volumes/data/dms/vmail/:/var/mail/
+ - {{ mail_base }}/volumes/data/dms/mail-state/:/var/mail-state/
+ - {{ mail_base }}/volumes/data/dms/mail-logs/:/var/log/mail/
+ - {{ mail_base }}/volumes/data/dms/config/:/tmp/docker-mailserver/
- {{ mail_base }}/volumes/data/dms/config/dovecot-ldap.conf:/etc/dovecot/dovecot-ldap.conf.ext
- {{ letsencrypt_certs }}:/certs/:ro
- /etc/localtime:/etc/localtime:ro
environment:
+ - DEPLOYMENT_TIME={{ now() }}
- SSL_TYPE=manual
- SSL_CERT_PATH=/certs/{{ mail_domain }}.pem
- SSL_KEY_PATH=/certs/{{ mail_domain }}.key
- ENABLE_CLAMAV=0
- ENABLE_AMAVIS=0
- - ENABLE_FAIL2BAN=1
- ENABLE_SASLAUTHD=1
- ENABLE_MANAGESIEVE=1
- - ENABLE_POSTGREY=0
+ - ENABLE_POSTGREY=1
- SPOOF_PROTECTION=1
- ACCOUNT_PROVISIONER=LDAP
@@ -121,12 +120,7 @@ services:
- RELAY_USER={{ relay_user }}
- RELAY_PASSWORD={{ relay_password }}
- networks:
- - mailserver
- - proxy
-
networks:
- mailserver:
roundcube:
proxy:
external: true
diff --git a/playbooks/roles/mail/templates/volumes/data/dms/config/dovecot.cf b/playbooks/roles/mail/templates/volumes/data/dms/config/dovecot.cf
new file mode 100644
index 0000000..62d0550
--- /dev/null
+++ b/playbooks/roles/mail/templates/volumes/data/dms/config/dovecot.cf
@@ -0,0 +1,27 @@
+haproxy_trusted_networks = {{ homelab_network }}
+
+service imap-login {
+ inet_listener imap {
+ haproxy = yes
+ }
+
+ inet_listener imaps {
+ haproxy = yes
+ }
+}
+
+service pop3-login {
+ inet_listener pop3 {
+ haproxy = yes
+ }
+
+ inet_listener pop3s {
+ haproxy = yes
+ }
+}
+
+service managesieve-login {
+ inet_listener sieve {
+ haproxy = yes
+ }
+}
diff --git a/playbooks/roles/mail/templates/volumes/data/dms/config/postfix-master.cf b/playbooks/roles/mail/templates/volumes/data/dms/config/postfix-master.cf
new file mode 100644
index 0000000..1885f4d
--- /dev/null
+++ b/playbooks/roles/mail/templates/volumes/data/dms/config/postfix-master.cf
@@ -0,0 +1,3 @@
+smtp/inet/postscreen_upstream_proxy_protocol=haproxy
+submission/inet/smtpd_upstream_proxy_protocol=haproxy
+submissions/inet/smtpd_upstream_proxy_protocol=haproxy
diff --git a/playbooks/roles/mail/templates/volumes/data/dms/config/user-patches.sh b/playbooks/roles/mail/templates/volumes/data/dms/config/user-patches.sh
index c62753f..1749499 100755
--- a/playbooks/roles/mail/templates/volumes/data/dms/config/user-patches.sh
+++ b/playbooks/roles/mail/templates/volumes/data/dms/config/user-patches.sh
@@ -3,7 +3,13 @@
postconf -e 'smtpd_sasl_type = dovecot'
postconf -e 'smtpd_sasl_path = /dev/shm/sasl-auth.sock'
postconf -e 'smtpd_sasl_auth_enable = yes'
-postconf -e 'broken_sasl_auth_clients = yes'
+postconf -e 'broken_sasl_auth_clients = no'
+postconf -e 'smtpd_tls_auth_only = yes'
+postconf -e 'smtpd_tls_security_level = encrypt'
+
+postconf -e 'postscreen_bare_newline_enable = no'
+postconf -e 'postscreen_non_smtp_command_enable = no'
+postconf -e 'postscreen_pipelining_enable = no'
postconf -e 'smtp_tls_wrappermode = yes' # for relay
@@ -34,8 +40,3 @@ userdb {
args = username_format=%u uid=docker gid=docker home=/var/mail/%d/%u
default_fields = uid=docker gid=docker home=/var/mail/%d/%u
}" > /etc/dovecot/conf.d/auth-ldap.conf.ext
-
-#userdb {
-# driver = static
-# args = home=/var/mail/%u
-#}"
diff --git a/playbooks/roles/mail/templates/volumes/scripts/check-postfix-health.unused.sh b/playbooks/roles/mail/templates/volumes/scripts/check-postfix-health.unused.sh
new file mode 100644
index 0000000..198221a
--- /dev/null
+++ b/playbooks/roles/mail/templates/volumes/scripts/check-postfix-health.unused.sh
@@ -0,0 +1,24 @@
+#!/bin/bash
+
+LOCKFILE="/var/mail-state/lib-postfix/master.lock"
+
+function log() {
+ echo "[health] $@"
+}
+
+if [ -f "$LOCKFILE" ]; then
+ PID=$(cat "$LOCKFILE")
+ log "pid $PID"
+ if kill -0 "$PID" 2>/dev/null; then
+ if ss --listening --tcp | grep -P 'LISTEN.+:smtp' > /dev/null; then
+ log "successfully listening to smtp"
+ exit 0
+ fi
+ else
+ # Not our postfix lock.
+ exit 0
+ fi
+fi
+
+log "bad health state"
+exit 1
diff --git a/playbooks/roles/mail/templates/volumes/scripts/wait-for-cert.sh b/playbooks/roles/mail/templates/volumes/scripts/wait-for-cert.sh
new file mode 100644
index 0000000..0f8018c
--- /dev/null
+++ b/playbooks/roles/mail/templates/volumes/scripts/wait-for-cert.sh
@@ -0,0 +1,23 @@
+#!/bin/bash
+set -e
+
+function log() {
+ echo "[cert] $@"
+}
+
+CERT="/certs/{{ mail_domain }}.pem"
+MAX_TRIES=30
+TRY=0
+
+while [ ! -f "$CERT" ]; do
+ if [ "$TRY" -eq "$MAX_TRIES" ]; then
+ log "[$TRY/$MAX_TRIES] Max tries, failing."
+ exit 1
+ fi
+ log "[$TRY/$MAX_TRIES] Certificate nonexistant. Waiting..."
+ sleep 2
+ TRY=$((TRY + 1))
+done
+
+log "Cert check done. Starting DMS."
+supervisord -c /etc/supervisor/supervisord.conf
diff --git a/playbooks/roles/mail/templates/volumes/scripts/wait-for-postfix.unused.sh b/playbooks/roles/mail/templates/volumes/scripts/wait-for-postfix.unused.sh
new file mode 100644
index 0000000..3e8a8c5
--- /dev/null
+++ b/playbooks/roles/mail/templates/volumes/scripts/wait-for-postfix.unused.sh
@@ -0,0 +1,27 @@
+# This was an attempt to keep rolling updates with very little downtime.
+# I don't think it's worth it, and the nature of update_config provides
+# little flexibility to use here.
+
+#!/bin/bash
+set -e
+
+function log() {
+ echo "[startup] $@"
+}
+
+LOCKFILE="/var/mail-state/lib-postfix/master.lock"
+MAX_TRIES=30
+TRY=0
+
+while [ -f "$LOCKFILE" ]; do
+ if [ "$TRY" -eq "$MAX_TRIES" ]; then
+ log "[$TRY/$MAX_TRIES] Max tries, failing."
+ exit 1
+ fi
+ log "[$TRY/$MAX_TRIES] Lockfile exists, waiting for it to be cleaned up by previous container..."
+ sleep 2
+ TRY=$((TRY + 1))
+done
+
+log "Lock check done. Starting DMS."
+supervisord -c /etc/supervisor/supervisord.conf