diff options
author | Elizabeth Hunt <me@liz.coffee> | 2025-04-20 00:11:26 -0700 |
---|---|---|
committer | Elizabeth Hunt <me@liz.coffee> | 2025-04-20 00:11:26 -0700 |
commit | d85cfa1693068666512b183ce9437faddda7de87 (patch) | |
tree | f69222d1f044141eb2ae997c5c50144b9928066a | |
parent | d0eede8b6ce27d297879d9c56942d9053228acec (diff) | |
download | infra-d85cfa1693068666512b183ce9437faddda7de87.tar.gz infra-d85cfa1693068666512b183ce9437faddda7de87.zip |
Initiate operation email w SES
24 files changed, 352 insertions, 10 deletions
@@ -44,3 +44,6 @@ - name: bin ansible.builtin.import_playbook: playbooks/bin.yml + +- name: mail + ansible.builtin.import_playbook: playbooks/mail.yml diff --git a/group_vars/all.yml b/group_vars/all.yml index b0d197c..8b31f3f 100644 --- a/group_vars/all.yml +++ b/group_vars/all.yml @@ -1,6 +1,12 @@ --- +# first deployment? +homelab_build: false +#homelab_build: true + ansible_user: serve + +loadbalancer_ip: "10.128.0.200" rfc1918_cgnat_networks: - 10.0.0.0/8 - 172.16.0.0/12 @@ -8,13 +14,10 @@ rfc1918_cgnat_networks: - 100.64.0.0/10 timezone: "America/Los_Angeles" + domain: "liz.coffee" idm_domain: "idm.{{ domain }}" headscale_host: "vpn.{{ domain }}" -# super internal private servers -sips_domain: "sips.{{ domain }}" -# first deployment? -homelab_build: false - -loadbalancer_ip: "10.128.0.200" +traextor_base: "{{ swarm_base }}/traextor" +letsencrypt_certs: "{{ traextor_base }}/volumes/certs/letsencrypt" diff --git a/group_vars/mail.yml b/group_vars/mail.yml new file mode 100644 index 0000000..3cfbeb3 --- /dev/null +++ b/group_vars/mail.yml @@ -0,0 +1,44 @@ +--- + +relay_host: email-smtp.us-west-2.amazonaws.com +relay_port: 465 +relay_user: "{{ ses_smtp_user_name }}" +relay_password: "{{ ses_smtp_password }}" +default_relay_host: "[{{ relay_host }}]:{{ relay_port }}" + +mail_domain: "mail.{{ domain }}" +mail_base: "{{ swarm_base }}/mail" + +postmaster_email: "postmaster@{{ domain }}" + +ldap_server: "{{ idm_domain }}" +ldap_server_host: "ldaps://{{ ldap_server }}:3636" +ldap_search_base: "{{ 'dc=' ~ idm_domain | regex_replace('\\.', ',dc=') }}" +ldap_bind_dn: "dn=token" + +ldap_query_filter_user: "(&(class=account)(emailprimary=%s))" +ldap_query_filter_group: "(&(class=account)(emailprimary=%s))" +ldap_query_filter_alias: "(&(class=account)(emailalternative=%s))" +ldap_query_filter_domain: "(&(class=account)(emailprimary=%s))" +ldap_query_filter_senders: "(&(class=account)(emailprimary=%s))" + +sasl_ldap_filter: > + (&(|(name=%U)(emailprimary=%U))(class=account) + (memberOf=cn=mail,{{ ldap_search_base }})) + +dovecot_user_filter: > + (&(class=account)(name=%u) + (memberOf=cn=mail,{{ ldap_search_base }})) +dovecot_auth_bind_userdn: "name=%u,{{ ldap_search_base }}" + +roundcube_default_host: "ssl://{{ mail_domain }}" +roundcube_default_port: 993 +roundcube_smtp_host: "ssl://{{ mail_domain }}" +roundcube_smtp_port: 465 +roundcube_plugins: "archive,zipdownload,managesieve,markasjunk,enigma" + +roundcube_oauth2_auth_uri: "https://{{ idm_domain }}/ui/oauth2" +roundcube_oauth2_user_uri: > + https://{{ idm_domain }}/oauth2/openid/roundcube/userinfo +roundcube_oauth2_token_uri: "https://{{ idm_domain }}/oauth2/token" +roundcube_oauth2_client_id: "roundcube" diff --git a/group_vars/traextor.yml b/group_vars/traextor.yml deleted file mode 100644 index 0f463f7..0000000 --- a/group_vars/traextor.yml +++ /dev/null @@ -1,3 +0,0 @@ ---- - -traextor_base: "{{ swarm_base }}/traextor" @@ -52,3 +52,6 @@ swarm-one ansible_host=10.128.0.201 ansible_user=serve ansible_connection=ssh a [bin] swarm-one ansible_host=10.128.0.201 ansible_user=serve ansible_connection=ssh ansible_become_password='{{ swarm_become_password }}' +[mail] +swarm-one ansible_host=10.128.0.201 ansible_user=serve ansible_connection=ssh ansible_become_password='{{ swarm_become_password }}' + diff --git a/playbooks/mail.yml b/playbooks/mail.yml new file mode 100644 index 0000000..73c6d20 --- /dev/null +++ b/playbooks/mail.yml @@ -0,0 +1,7 @@ +--- + +- name: mail setup + hosts: mail + become: true + roles: + - mail diff --git a/playbooks/roles/kanidm/templates/stacks/docker-compose.yml b/playbooks/roles/kanidm/templates/stacks/docker-compose.yml index 7f568e8..7f8bfe2 100644 --- a/playbooks/roles/kanidm/templates/stacks/docker-compose.yml +++ b/playbooks/roles/kanidm/templates/stacks/docker-compose.yml @@ -3,7 +3,7 @@ services: image: kanidm/server volumes: - {{ kanidm_base }}/volumes/data:/data - - {{ traextor_base }}/volumes/certs/letsencrypt:/certs:ro + - {{ letsencrypt_certs }}:/certs:ro networks: - proxy {% if homelab_build %} diff --git a/playbooks/roles/mail/tasks/main.yml b/playbooks/roles/mail/tasks/main.yml new file mode 100644 index 0000000..12b789d --- /dev/null +++ b/playbooks/roles/mail/tasks/main.yml @@ -0,0 +1,67 @@ +--- + +- name: Build mail compose dirs + ansible.builtin.file: + state: directory + dest: '{{ mail_base }}/{{ item.path }}' + owner: 1000 + group: 1000 + mode: 0755 + with_filetree: '../templates' + when: item.state == 'directory' + +- name: Build mail compose files + ansible.builtin.template: + src: '{{ item.src }}' + dest: '{{ mail_base }}/{{ item.path }}' + owner: 1000 + group: 1000 + mode: 0755 + with_filetree: '../templates' + when: item.state == 'file' + +# https://github.com/docker-mailserver/docker-mailserver/blob/23bb1c8e50dad1462c645b8a9cf50aeee8bc2625/Dockerfile#L149C19-L149C20 +- name: Build DMS compose dirs + ansible.builtin.file: + state: directory + dest: '{{ mail_base }}/volumes/data/dms/{{ item.path }}' + owner: 5000 + group: 5000 + mode: 0755 + with_filetree: '../templates/volumes/data/dms' + when: item.state == 'directory' + +- name: Build DMS template files with correct UID for docker mailserver + ansible.builtin.template: + src: '{{ item.src }}' + dest: '{{ mail_base }}/volumes/data/dms/{{ item.path }}' + owner: 5000 + group: 5000 + mode: 0755 + with_filetree: '../templates/volumes/data/dms' + when: item.state == 'file' + +- name: Build Roundcube compose dirs + ansible.builtin.file: + state: directory + dest: '{{ mail_base }}/volumes/data/roundcube/{{ item.path }}' + mode: 0755 + # https://github.com/roundcube/roundcubemail-docker/blob/ef4b8cc59eecbf0e25c66c7f3c464594cc310761/apache/Dockerfile#L145 + owner: 33 + group: 33 + with_filetree: '../templates/volumes/data/roundcube' + when: item.state == 'directory' + +- name: Build Roundcube template files + ansible.builtin.template: + src: '{{ item.src }}' + dest: '{{ mail_base }}/volumes/data/roundcube/{{ item.path }}' + owner: 33 + group: 33 + mode: 0755 + with_filetree: '../templates/volumes/data/roundcube' + 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 new file mode 100644 index 0000000..654f264 --- /dev/null +++ b/playbooks/roles/mail/templates/stacks/docker-compose.yml @@ -0,0 +1,120 @@ +services: + roundcube: + image: roundcube/roundcubemail:latest-nonroot + restart: always + volumes: + - {{ mail_base }}/volumes/data/roundcube/db:/var/roundcube/db + - {{ mail_base }}/volumes/data/roundcube/config:/var/roundcube/config + environment: + - ROUNDCUBEMAIL_DB_TYPE=sqlite + - ROUNDCUBEMAIL_SKIN=elastic + - ROUNDCUBEMAIL_PLUGINS={{ roundcube_plugins }} + - ROUNDCUBEMAIL_DEFAULT_HOST={{ roundcube_default_host }} + - ROUNDCUBEMAIL_DEFAULT_PORT={{ roundcube_default_port }} + - ROUNDCUBEMAIL_SMTP_SERVER={{ roundcube_smtp_host }} + - ROUNDCUBEMAIL_SMTP_PORT={{ roundcube_smtp_port }} + networks: + - proxy + - roundcube + deploy: + mode: replicated + replicas: 1 + labels: + - traefik.enable=true + - traefik.swarm.network=proxy + - traefik.http.routers.mail.tls=true + - traefik.http.routers.mail.tls.certResolver=letsencrypt + - traefik.http.routers.mail.rule=Host(`{{ mail_domain }}`) + - traefik.http.routers.mail.entrypoints=websecure + - traefik.http.services.mail.loadbalancer.server.port=8000 + + mailserver: + image: ghcr.io/docker-mailserver/docker-mailserver:latest + hostname: {{ mail_domain }} +{% 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 +{% endif %} + 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 + # SMTP + - 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=465 + 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/data/dms/config/dovecot-ldap.conf:/etc/dovecot/dovecot-ldap.conf.ext + - {{ letsencrypt_certs }}:/certs/:ro + - /etc/localtime:/etc/localtime:ro + environment: + - 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 + + - SPOOF_PROTECTION=1 + - ACCOUNT_PROVISIONER=LDAP + - LDAP_SERVER_HOST={{ ldap_server_host }} + - LDAP_SEARCH_BASE={{ ldap_search_base }} + - LDAP_BIND_DN={{ ldap_bind_dn }} + - LDAP_BIND_PW={{ email_ldap_api_token }} + + - LDAP_QUERY_FILTER_USER={{ ldap_query_filter_user }} + - LDAP_QUERY_FILTER_GROUP={{ ldap_query_filter_group }} + - LDAP_QUERY_FILTER_ALIAS={{ ldap_query_filter_alias }} + - LDAP_QUERY_FILTER_DOMAIN={{ ldap_query_filter_domain }} + - LDAP_QUERY_FILTER_SENDERS={{ ldap_query_filter_senders }} + + - POSTMASTER_ADDRESS={{ postmaster_email }} + + - SASLAUTHD_MECHANISMS=ldap + - SASLAUTHD_LDAP_FILTER={{ sasl_ldap_filter }} + + - ENABLE_OAUTH2=1 + - OAUTH2_INTROSPECTION_URL={{ roundcube_oauth2_user_uri }} + + - DEFAULT_RELAY_HOST={{ default_relay_host }} + - 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/.gitkeep b/playbooks/roles/mail/templates/volumes/data/.gitkeep new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/playbooks/roles/mail/templates/volumes/data/.gitkeep diff --git a/playbooks/roles/mail/templates/volumes/data/dms/.gitkeep b/playbooks/roles/mail/templates/volumes/data/dms/.gitkeep new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/playbooks/roles/mail/templates/volumes/data/dms/.gitkeep diff --git a/playbooks/roles/mail/templates/volumes/data/dms/config/dovecot-ldap.conf b/playbooks/roles/mail/templates/volumes/data/dms/config/dovecot-ldap.conf new file mode 100644 index 0000000..956942c --- /dev/null +++ b/playbooks/roles/mail/templates/volumes/data/dms/config/dovecot-ldap.conf @@ -0,0 +1,11 @@ +base = {{ ldap_search_base }} +uris = {{ ldap_server_host }} +tls = no +ldap_version = 3 +default_pass_scheme = SSHA +dn = {{ ldap_bind_dn }} +dnpass = {{ email_ldap_api_token }} + +auth_bind = yes +auth_bind_userdn = {{ dovecot_auth_bind_userdn }} +user_filter = {{ dovecot_user_filter }} 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 new file mode 100755 index 0000000..c62753f --- /dev/null +++ b/playbooks/roles/mail/templates/volumes/data/dms/config/user-patches.sh @@ -0,0 +1,41 @@ +#!/bin/bash + +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 'smtp_tls_wrappermode = yes' # for relay + +# This is necessary for any users with multiple email addresses. Kanidm stores all email addresses in +# the mail attribute, which means that postfix clones emails to all addresses. Since dovecot only +# has a mailbox for the primary email address, the other addresses will bounce, and the sender +# will receive a bounce message. Kanidm provides the "primary" (first) email address in the +# emailprimary attribute, so we use that instead. +# +# We don't change the ldap-sender.cf or ldap-groups.cf files because we want people to be able to +# send mail from any of their email addresses, not just the primary one, and we want email to +# groups to be delivered regardless of which email it arrives at. +sed -i 's/result_attribute = mail/result_attribute = emailprimary/' /etc/postfix/ldap-aliases.cf +sed -i 's/result_attribute = mail/result_attribute = emailprimary/' /etc/postfix/ldap-domains.cf +sed -i 's/result_attribute = mail/result_attribute = emailprimary/' /etc/postfix/ldap-users.cf + +echo 'auth_username_format = %Ln' >> /etc/dovecot/conf.d/10-auth.conf + +echo 'username_format = %Ln' >> /etc/dovecot/dovecot-oauth2.conf.ext + +echo "passdb { + driver = ldap + args = /etc/dovecot/dovecot-ldap.conf.ext +} + +userdb { + driver = static + 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/data/dms/mail-logs/.gitkeep b/playbooks/roles/mail/templates/volumes/data/dms/mail-logs/.gitkeep new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/playbooks/roles/mail/templates/volumes/data/dms/mail-logs/.gitkeep diff --git a/playbooks/roles/mail/templates/volumes/data/dms/mail-state/.gitkeep b/playbooks/roles/mail/templates/volumes/data/dms/mail-state/.gitkeep new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/playbooks/roles/mail/templates/volumes/data/dms/mail-state/.gitkeep diff --git a/playbooks/roles/mail/templates/volumes/data/dms/vmail/.gitkeep b/playbooks/roles/mail/templates/volumes/data/dms/vmail/.gitkeep new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/playbooks/roles/mail/templates/volumes/data/dms/vmail/.gitkeep diff --git a/playbooks/roles/mail/templates/volumes/data/roundcube/.gitkeep b/playbooks/roles/mail/templates/volumes/data/roundcube/.gitkeep new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/playbooks/roles/mail/templates/volumes/data/roundcube/.gitkeep diff --git a/playbooks/roles/mail/templates/volumes/data/roundcube/config/.gitkeep b/playbooks/roles/mail/templates/volumes/data/roundcube/config/.gitkeep new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/playbooks/roles/mail/templates/volumes/data/roundcube/config/.gitkeep diff --git a/playbooks/roles/mail/templates/volumes/data/roundcube/config/oauth2.inc.php b/playbooks/roles/mail/templates/volumes/data/roundcube/config/oauth2.inc.php new file mode 100644 index 0000000..0284e9e --- /dev/null +++ b/playbooks/roles/mail/templates/volumes/data/roundcube/config/oauth2.inc.php @@ -0,0 +1,19 @@ +<?php + +$config['oauth_provider'] = 'generic'; +$config['oauth_provider_name'] = '{{ domain }} <3'; +$config['oauth_client_id'] = '{{ roundcube_oauth2_client_id }}'; +$config['oauth_client_secret'] = '{{ roundcube_oauth2_client_basic_secret }}'; +$config['oauth_auth_uri'] = '{{ roundcube_oauth2_auth_uri }}'; +$config['oauth_token_uri'] = '{{ roundcube_oauth2_token_uri }}'; +$config['oauth_identity_uri'] = '{{ roundcube_oauth2_user_uri }}'; + +$config['oauth_verify_peer'] = true; + +$config['oauth_scope'] = 'email openid profile'; +$config['oauth_identity_fields'] = ['email']; + +$config['oauth_login_redirect'] = false; + +$config['force_https'] = true; +$config['use_https'] = true; diff --git a/playbooks/roles/mail/templates/volumes/data/roundcube/config/sieve.inc.php b/playbooks/roles/mail/templates/volumes/data/roundcube/config/sieve.inc.php new file mode 100644 index 0000000..e7b08b0 --- /dev/null +++ b/playbooks/roles/mail/templates/volumes/data/roundcube/config/sieve.inc.php @@ -0,0 +1,4 @@ +<?php + +$config['managesieve_host'] = "tls://{{ mail_domain }}"; +$config['managesieve_auth_type'] = "PLAIN"; diff --git a/playbooks/roles/mail/templates/volumes/data/roundcube/db/.gitkeep b/playbooks/roles/mail/templates/volumes/data/roundcube/db/.gitkeep new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/playbooks/roles/mail/templates/volumes/data/roundcube/db/.gitkeep diff --git a/playbooks/roles/outbound/templates/proxy/sites-enabled/mail.conf b/playbooks/roles/outbound/templates/proxy/sites-enabled/mail.conf new file mode 100644 index 0000000..c810f5a --- /dev/null +++ b/playbooks/roles/outbound/templates/proxy/sites-enabled/mail.conf @@ -0,0 +1,15 @@ +server { + listen 80; + server_name mail.liz.coffee; + location / { + proxy_pass https://{{ loadbalancer_ip }}; + proxy_ssl_verify off; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + } +} diff --git a/playbooks/roles/traefik/templates/stacks/traefik.yml b/playbooks/roles/traefik/templates/stacks/traefik.yml index c4e2bd5..27af9a3 100644 --- a/playbooks/roles/traefik/templates/stacks/traefik.yml +++ b/playbooks/roles/traefik/templates/stacks/traefik.yml @@ -22,6 +22,12 @@ entryPoints: address: ":53/tcp" ldaps: address: ":3636/tcp" + sieve: + address: ":4190/tcp" + imap: + address: ":993/tcp" + smtp: + address: ":465/tcp" serversTransport: insecureSkipVerify: true providers: diff --git a/secrets.txt b/secrets.txt index 19108cb..1a2a469 100644 --- a/secrets.txt +++ b/secrets.txt @@ -12,3 +12,5 @@ kanboard_ldap_password silverbullet_password ses_smtp_user_name ses_smtp_password +email_ldap_api_token +roundcube_oauth2_client_basic_secret |