From 12edcdbe9cb890608cf06ef2863834102bf80291 Mon Sep 17 00:00:00 2001 From: Charlie Savage Date: Tue, 9 Jan 2024 18:53:31 -0800 Subject: [PATCH 01/66] Update Chart version and while at it the appVersion and docker-mailserver images to 13.2.0 --- charts/docker-mailserver/Chart.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/charts/docker-mailserver/Chart.yaml b/charts/docker-mailserver/Chart.yaml index 189ca58d..a0757488 100644 --- a/charts/docker-mailserver/Chart.yaml +++ b/charts/docker-mailserver/Chart.yaml @@ -2,7 +2,7 @@ apiVersion: v2 appVersion: "13.2.0" description: A fullstack but simple mailserver (smtp, imap, antispam, antivirus, ssl...) using Docker. name: docker-mailserver -version: 2.2.2 +version: 2.3.0 sources: - https://github.com/docker-mailserver/docker-mailserver-helm maintainers: From cd0ab07d636df0a8f48061f0f7b395751661fdb0 Mon Sep 17 00:00:00 2001 From: Charlie Savage Date: Wed, 10 Jan 2024 22:45:36 -0800 Subject: [PATCH 02/66] Sync to latest env values - copied from https://github.com/docker-mailserver/docker-mailserver/blob/master/mailserver.env --- .../templates/_upstream-env-variables.tpl | 164 ------------ charts/docker-mailserver/values.yaml | 233 ++++++++++-------- 2 files changed, 137 insertions(+), 260 deletions(-) delete mode 100644 charts/docker-mailserver/templates/_upstream-env-variables.tpl diff --git a/charts/docker-mailserver/templates/_upstream-env-variables.tpl b/charts/docker-mailserver/templates/_upstream-env-variables.tpl deleted file mode 100644 index dbeb0456..00000000 --- a/charts/docker-mailserver/templates/_upstream-env-variables.tpl +++ /dev/null @@ -1,164 +0,0 @@ -{{/* -There are a _lot_ of upstream env variables used to customize docker-mailserver. -We list them here (and include this template in deployment.yaml) to keep deployment.yaml neater -*/}} -{{- define "dockermailserver.upstream-env-variables" -}} -- name: OVERRIDE_HOSTNAME - value: {{ .Values.pod.dockermailserver.override_hostname | quote }} -- name: DMS_DEBUG - value: {{ .Values.pod.dockermailserver.dms_debug | quote }} -- name: ENABLE_CLAMAV - value: {{ .Values.pod.dockermailserver.enable_clamav | quote }} -- name: ONE_DIR - value: {{ .Values.pod.dockermailserver.one_dir | quote }} -- name: ENABLE_POP3 - value: {{ .Values.pod.dockermailserver.enable_pop3 | quote }} -- name: ENABLE_FAIL2BAN - value: {{ default false .Values.pod.dockermailserver.enable_fail2ban | quote }} -- name: SMTP_ONLY - value: {{ .Values.pod.dockermailserver.smtp_only | quote }} -- name: SSL_TYPE - value: {{ .Values.pod.dockermailserver.ssl_type | quote }} -- name: SSL_CERT_PATH - value: {{ default "/tmp/ssl/tls.crt" .Values.pod.dockermailserver.ssl_cert_path | quote }} -- name: SSL_KEY_PATH - value: {{ default "/tmp/ssl/tls.key" .Values.pod.dockermailserver.ssl_key_path | quote }} -- name: TLS_LEVEL - value: {{ .Values.pod.dockermailserver.tls_level | quote }} -- name: SPOOF_PROTECTION - value: {{ .Values.pod.dockermailserver.spoof_protection | quote }} -- name: ENABLE_SRS - value: {{ .Values.pod.dockermailserver.enable_srs | quote }} -- name: PERMIT_DOCKER - value: {{ .Values.pod.dockermailserver.permit_docker | quote }} -- name: VIRUSMAILS_DELETE_DELAY - value: {{ .Values.pod.dockermailserver.virusmails_delete_delay | quote }} -- name: ENABLE_POSTFIX_VIRTUAL_TRANSPORT - value: {{ .Values.pod.dockermailserver.enable_postfix_virtual_transport | quote }} -- name: POSTFIX_DAGENT - value: {{ .Values.pod.dockermailserver.postfix_dagent | quote }} -- name: POSTFIX_MAILBOX_SIZE_LIMIT - value: {{ .Values.pod.dockermailserver.postfix_mailbox_size_limit | quote }} -- name: POSTFIX_MESSAGE_SIZE_LIMIT - value: {{ .Values.pod.dockermailserver.postfix_message_size_limit | quote }} -- name: ENABLE_MANAGESIEVE - value: {{ .Values.pod.dockermailserver.enable_managesieve | quote }} -- name: POSTMASTER_ADDRESS - value: {{ .Values.pod.dockermailserver.postmaster_address | quote }} -- name: POSTSCREEN_ACTION - value: {{ .Values.pod.dockermailserver.postscreen_action | quote }} -- name: REPORT_RECIPIENT - value: {{ .Values.pod.dockermailserver.report_recipient | quote }} -- name: REPORT_SENDER - value: {{ .Values.pod.dockermailserver.report_sender | quote }} -- name: REPORT_INTERVAL - value: {{ .Values.pod.dockermailserver.report_interval | quote }} -- name: ENABLE_SPAMASSASSIN - value: {{ .Values.pod.dockermailserver.enable_spamassassin | quote }} -- name: SPAMASSASSIN_SPAM_TO_INBOX - value: {{ .Values.pod.dockermailserver.sa_spam_to_inbox | quote }} -- name: MOVE_SPAM_TO_JUNK - value: {{ .Values.pod.dockermailserver.sa_move_spam_to_junk | quote }} -- name: SA_TAG - value: {{ .Values.pod.dockermailserver.sa_tag | quote }} -- name: SA_TAG2 - value: {{ .Values.pod.dockermailserver.sa_tag2 | quote }} -- name: SA_KILL - value: {{ .Values.pod.dockermailserver.sa_kill | quote }} -- name: SA_SPAM_SUBJECT - value: {{ .Values.pod.dockermailserver.sa_spam_subject | quote }} -- name: ENABLE_FETCHMAIL - value: {{ .Values.pod.dockermailserver.enable_fetchmail | quote }} -- name: FETCHMAIL_POLL - value: {{ .Values.pod.dockermailserver.fetchmail_poll | quote }} -- name: ENABLE_LDAP - value: {{ .Values.pod.dockermailserver.enable_ldap | quote }} -- name: LDAP_START_TLS - value: {{ .Values.pod.dockermailserver.ldap_start_tls | quote }} -- name: LDAP_SERVER_HOST - value: {{ .Values.pod.dockermailserver.ldap_server_host | quote }} -- name: LDAP_SEARCH_BASE - value: {{ .Values.pod.dockermailserver.ldap_search_base | quote }} -- name: LDAP_BIND_DN - value: {{ .Values.pod.dockermailserver.ldap_bind_dn | quote }} -- name: LDAP_BIND_PW - value: {{ .Values.pod.dockermailserver.ldap_bind_pw | quote }} -- name: LDAP_QUERY_FILTER_USER - value: {{ .Values.pod.dockermailserver.ldap_query_filter_user | quote }} -- name: LDAP_QUERY_FILTER_GROUP - value: {{ .Values.pod.dockermailserver.ldap_query_filter_group | quote }} -- name: LDAP_QUERY_FILTER_ALIAS - value: {{ .Values.pod.dockermailserver.ldap_query_filter_alias | quote }} -- name: LDAP_QUERY_FILTER_DOMAIN - value: {{ .Values.pod.dockermailserver.ldap_query_filter_domain | quote }} -- name: LOG_LEVEL - value: {{ .Values.pod.dockermailserver.log_level | quote }} -- name: DOVECOT_TLS - value: {{ .Values.pod.dockermailserver.dovecot_tls | quote }} -- name: DOVECOT_LDAP_VERSION - value: {{ .Values.pod.dockermailserver.dovecot_ldap_version | quote }} -- name: DOVECOT_DEFAULT_PASS_SCHEME - value: {{ .Values.pod.dockermailserver.dovecot_default_pass_scheme | quote }} -- name: DOVECOT_AUTH_BIND - value: {{ .Values.pod.dockermailserver.dovecot_auth_bind | quote }} -- name: DOVECOT_USER_FILTER - value: {{ .Values.pod.dockermailserver.dovecot_user_filter | quote }} -- name: DOVECOT_USER_ATTRS - value: {{ .Values.pod.dockermailserver.dovecot_user_attrs | quote }} -- name: DOVECOT_PASS_FILTER - value: {{ .Values.pod.dockermailserver.dovecot_pass_filter | quote }} -- name: DOVECOT_PASS_ATTRS - value: {{ .Values.pod.dockermailserver.dovecot_pass_attrs | quote }} -- name: DOVECOT_MAILBOX_FORMAT - value: {{ .Values.pod.dockermailserver.dovecot_mailbox_format | quote }} -- name: ENABLE_POSTGREY - value: {{ .Values.pod.dockermailserver.enable_postgrey | quote }} -- name: POSTGREY_DELAY - value: {{ .Values.pod.dockermailserver.postgrey_delay | quote }} -- name: POSTGREY_MAX_AGE - value: {{ .Values.pod.dockermailserver.postgrey_max_age | quote }} -- name: POSTGREY_AUTO_WHITELIST_CLIENTS - value: {{ .Values.pod.dockermailserver.postgrey_auto_whitelist_clients | quote }} -- name: POSTGREY_TEXT - value: {{ .Values.pod.dockermailserver.postgrey_text | quote }} -- name: ENABLE_SASLAUTHD - value: {{ .Values.pod.dockermailserver.enable_saslauthd | quote }} -- name: SASLAUTHD_MECHANISMS - value: {{ .Values.pod.dockermailserver.saslauthd_mechanisms | quote }} -- name: SASLAUTHD_MECH_OPTIONS - value: {{ .Values.pod.dockermailserver.saslauthd_mech_options | quote }} -- name: SASLAUTHD_LDAP_SERVER - value: {{ .Values.pod.dockermailserver.saslauthd_ldap_server | quote }} -- name: SASLAUTHD_LDAP_SSL - value: {{ .Values.pod.dockermailserver.saslauthd_ldap_ssl | quote }} -- name: SASLAUTHD_LDAP_BIND_DN - value: {{ .Values.pod.dockermailserver.saslauthd_ldap_bind_dn | quote }} -- name: SASLAUTHD_LDAP_PASSWORD - value: {{ .Values.pod.dockermailserver.saslauthd_ldap_password | quote }} -- name: SASLAUTHD_LDAP_SEARCH_BASE - value: {{ .Values.pod.dockermailserver.saslauthd_ldap_search_base | quote }} -- name: SASLAUTHD_LDAP_FILTER - value: {{ .Values.pod.dockermailserver.saslauthd_ldap_filter | quote }} -- name: SASL_PASSWD - value: {{ .Values.pod.dockermailserver.sasl_passwd | quote }} -- name: SRS_EXCLUDE_DOMAINS - value: {{ .Values.pod.dockermailserver.srs_exclude_domains | quote }} -- name: SRS_SECRET - value: {{ .Values.pod.dockermailserver.srs_secret | quote }} -- name: SRS_DOMAINNAME - value: {{ .Values.pod.dockermailserver.srs_domainname | quote }} -- name: DEFAULT_RELAY_HOST - value: {{ .Values.pod.dockermailserver.default_relay_host | quote }} -- name: RELAY_HOST - value: {{ .Values.pod.dockermailserver.relay_host | quote }} -- name: RELAY_PORT - value: {{ .Values.pod.dockermailserver.relay_port | quote }} -- name: RELAY_USER - value: {{ .Values.pod.dockermailserver.relay_user | quote }} -- name: RELAY_PASSWORD - value: {{ .Values.pod.dockermailserver.relay_password | quote }} -- name: PFLOGSUMM_TRIGGER - value: {{ .Values.pod.dockermailserver.pflogsumm_trigger | quote }} -- name: PFLOGSUMM_RECIPIENT - value: {{ .Values.pod.dockermailserver.pflogsumm_recipient | quote }} -{{- end -}} \ No newline at end of file diff --git a/charts/docker-mailserver/values.yaml b/charts/docker-mailserver/values.yaml index 0e403c34..5aabc347 100644 --- a/charts/docker-mailserver/values.yaml +++ b/charts/docker-mailserver/values.yaml @@ -119,130 +119,171 @@ pod: type: "Recreate" ## The following variables affect the behaviour of docker-mailserver - ## See https://docker-mailserver.github.io/docker-mailserver/v11.0/config/environment/ for details + ## See https://docker-mailserver.github.io/docker-mailserver/latest/config/environment/ for details ## Note that an empty value indicates the default as described in the docs above env: - # General - OVERRIDE_HOSTNAME: "mail.batcave.org" + # ----------------------------------------------- + # --- Required Section --------------------------- + # ----------------------------------------------- + OVERRIDE_HOSTNAME: + + # ----------------------------------------------- + # --- General Section --------------------------- + # ----------------------------------------------- LOG_LEVEL: info + SUPERVISOR_LOGLEVEL: ONE_DIR: 1 - PERMIT_DOCKER: + DMS_VMAIL_UID: + DMS_VMAIL_GID: + ACCOUNT_PROVISIONER: + POSTMASTER_ADDRESS: + ENABLE_UPDATE_CHECK: 1 + UPDATE_CHECK_INTERVAL: 1d + PERMIT_DOCKER: none TZ: + NETWORK_INTERFACE: + TLS_LEVEL: + SPOOF_PROTECTION: + ENABLE_SRS: 0 + ENABLE_OPENDKIM: 1 + ENABLE_OPENDMARC: 1 + ENABLE_POLICYD_SPF: 1 + ENABLE_POP3: + ENABLE_IMAP: 1 + ENABLE_CLAMAV: 0 + ENABLE_RSPAMD: 1 + ENABLE_RSPAMD_REDIS: + RSPAMD_LEARN: 0 + RSPAMD_CHECK_AUTHENTICATED: 0 + RSPAMD_GREYLISTING: 0 + RSPAMD_HFILTER: 1 + RSPAMD_HFILTER_HOSTNAME_UNKNOWN_SCORE: 6 ENABLE_AMAVIS: 1 AMAVIS_LOGLEVEL: 0 ENABLE_DNSBL: 0 - ENABLE_CLAMAV: 0 - ENABLE_POP3: - ENABLE_FAIL2BAN: 0 # Setting this to 1 will add the NET_ADMIN cap to the deployment + ENABLE_FAIL2BAN: 0 FAIL2BAN_BLOCKTYPE: drop - SMTP_ONLY: - SSL_TYPE: - SSL_CERT_PATH: - SSL_KEY_PATH: - TLS_LEVEL: - SPOOF_PROTECTION: - ENABLE_SRS: 0 - NETWORK_INTERFACE: - VIRUSMAILS_DELETE_DELAY: - ENABLE_POSTFIX_VIRTUAL_TRANSPORT: + ENABLE_MANAGESIEVE: + POSTSCREEN_ACTION: enforce + SMTP_ONLY: + SSL_TYPE: + # These two values are automatically setup if SSL_TYPE is configured + # SSL_CERT_PATH: + # SSL_KEY_PATH: + SSL_ALT_CERT_PATH: + SSL_ALT_KEY_PATH: + VIRUSMAILS_DELETE_DELAY: POSTFIX_DAGENT: - POSTFIX_MAILBOX_SIZE_LIMIT: + POSTFIX_MAILBOX_SIZE_LIMIT: ENABLE_QUOTAS: 1 - POSTFIX_MESSAGE_SIZE_LIMIT: - CLAMAV_MESSAGE_SIZE_LIMIT: - ENABLE_MANAGESIEVE: - POSTMASTER_ADDRESS: - ENABLE_UPDATE_CHECK: 1 - UPDATE_CHECK_INTERVAL: 1d - POSTSCREEN_ACTION: enforce - DOVECOT_MAILBOX_FORMAT: maildir - POSTFIX_INET_PROTOCOLS: all - DOVECOT_INET_PROTOCOLS: all - # Reports - PFLOGSUMM_TRIGGER: + POSTFIX_MESSAGE_SIZE_LIMIT: + CLAMAV_MESSAGE_SIZE_LIMIT: + PFLOGSUMM_TRIGGER: PFLOGSUMM_RECIPIENT: - PFLOGSUMM_SENDER: + PFLOGSUMM_SENDER: LOGWATCH_INTERVAL: - LOGWATCH_RECIPIENT: - LOGWATCH_SENDER: - REPORT_RECIPIENT: - REPORT_SENDER: + LOGWATCH_RECIPIENT: + LOGWATCH_SENDER: + REPORT_RECIPIENT: + REPORT_SENDER: LOGROTATE_INTERVAL: weekly - # SpamAssassin + POSTFIX_REJECT_UNKNOWN_CLIENT_HOSTNAME: 0 + POSTFIX_INET_PROTOCOLS: all + DOVECOT_INET_PROTOCOLS: all + + # ----------------------------------------------- + # --- SpamAssassin Section ---------------------- + # ----------------------------------------------- ENABLE_SPAMASSASSIN: 0 - SPAMASSASSIN_SPAM_TO_INBOX: 1 ENABLE_SPAMASSASSIN_KAM: 0 + SPAMASSASSIN_SPAM_TO_INBOX: 1 MOVE_SPAM_TO_JUNK: 1 + MARK_SPAM_AS_READ: 0 SA_TAG: 2.0 SA_TAG2: 6.31 - SA_KILL: 6.31 - SA_SPAM_SUBJECT: "**SPAM**" - SA_SHORTCIRCUIT_BAYES_SPAM: 1 - SA_SHORTCIRCUIT_BAYES_HAM: 1 - # Fetchmail + SA_KILL: 10.0 + SA_SPAM_SUBJECT: '***SPAM*** ' + + # ----------------------------------------------- + # --- Fetchmail Section ------------------------- + # ----------------------------------------------- ENABLE_FETCHMAIL: 0 FETCHMAIL_POLL: 300 FETCHMAIL_PARALLEL: 0 - # LDAP - ENABLE_LDAP: + ENABLE_GETMAIL: 0 + GETMAIL_POLL: 5 + + # ----------------------------------------------- + # --- LDAP Section ------------------------------ + # ----------------------------------------------- LDAP_START_TLS: LDAP_SERVER_HOST: - LDAP_SEARCH_BASE: - LDAP_BIND_DN: - LDAP_BIND_PW: - LDAP_QUERY_FILTER_USER: - LDAP_QUERY_FILTER_GROUP: - LDAP_QUERY_FILTER_ALIAS: - LDAP_QUERY_FILTER_DOMAIN: - LDAP_QUERY_FILTER_SENDERS: - DOVECOT_TLS: - # Dovecot - DOVECOT_BASE: - DOVECOT_DEFAULT_PASS_SCHEME: - DOVECOT_DN: - DOVECOT_DNPASS: - DOVECOT_URIS: - DOVECOT_LDAP_VERSION: - DOVECOT_AUTH_BIND: - DOVECOT_USER_FILTER: - DOVECOT_USER_ATTRS: - DOVECOT_PASS_FILTER: - DOVECOT_PASS_ATTRS: - # Postgrey - ENABLE_POSTGREY: + LDAP_SEARCH_BASE: + LDAP_BIND_DN: + LDAP_BIND_PW: + LDAP_QUERY_FILTER_USER: + LDAP_QUERY_FILTER_GROUP: + LDAP_QUERY_FILTER_ALIAS: + LDAP_QUERY_FILTER_DOMAIN: + + # ----------------------------------------------- + # --- Dovecot Section --------------------------- + # ----------------------------------------------- + DOVECOT_TLS: + DOVECOT_USER_FILTER: + DOVECOT_PASS_FILTER: + DOVECOT_MAILBOX_FORMAT: maildir + DOVECOT_AUTH_BIND: + + # ----------------------------------------------- + # --- Postgrey Section -------------------------- + # ----------------------------------------------- + ENABLE_POSTGREY: 0 POSTGREY_DELAY: 300 POSTGREY_MAX_AGE: 35 + POSTGREY_TEXT: "Delayed by Postgrey" POSTGREY_AUTO_WHITELIST_CLIENTS: 5 - POSTGREY_TEXT: Delayed by Postgrey - # SASL Auth - ENABLE_SASLAUTHD: - SASLAUTHD_MECHANISMS: - SASLAUTHD_MECH_OPTIONS: - SASLAUTHD_LDAP_SERVER: - SASLAUTHD_LDAP_START_TLS: - SASLAUTHD_LDAP_TLS_CHECK_PEER: - SASLAUTHD_LDAP_TLS_CACERT_DIR: - SASLAUTHD_LDAP_TLS_CACERT_FILE: - SASLAUTHD_LDAP_BIND_DN: - SASLAUTHD_LDAP_PASSWORD: - SASLAUTHD_LDAP_SEARCH_BASE: - SASLAUTHD_LDAP_FILTER: - SASLAUTHD_LDAP_PASSWORD_ATTR: - SASL_PASSWD: - SASLAUTHD_LDAP_AUTH_METHOD: - SASLAUTHD_LDAP_MECH: - # SRS (Sender Rewriting Scheme) + + # ----------------------------------------------- + # --- SASL Section ------------------------------ + # ----------------------------------------------- + ENABLE_SASLAUTHD: 0 + SASLAUTHD_MECHANISMS: + SASLAUTHD_MECH_OPTIONS: + SASLAUTHD_LDAP_SERVER: + SASLAUTHD_LDAP_BIND_DN: + SASLAUTHD_LDAP_PASSWORD: + SASLAUTHD_LDAP_SEARCH_BASE: + SASLAUTHD_LDAP_FILTER: + SASLAUTHD_LDAP_START_TLS: + SASLAUTHD_LDAP_TLS_CHECK_PEER: + SASLAUTHD_LDAP_TLS_CACERT_FILE: + SASLAUTHD_LDAP_TLS_CACERT_DIR: + SASLAUTHD_LDAP_PASSWORD_ATTR: + SASLAUTHD_LDAP_AUTH_METHOD: + SASLAUTHD_LDAP_MECH: + + # ----------------------------------------------- + # --- SRS Section ------------------------------- + # ----------------------------------------------- SRS_SENDER_CLASSES: envelope_sender - SRS_EXCLUDE_DOMAINS: - SRS_SECRET: - SRS_DOMAINNAME: - # Default Relay Host - DEFAULT_RELAY_HOST: - # Multi-domain Relay Hosts - RELAY_HOST: - RELAY_PORT: - RELAY_USER: - RELAY_PASSWORD: + SRS_EXCLUDE_DOMAINS: + SRS_SECRET: + + # ----------------------------------------------- + # --- Default Relay Host Section ---------------- + # ----------------------------------------------- + + DEFAULT_RELAY_HOST: + + # ----------------------------------------------- + # --- Multi-Domain Relay Section ---------------- + # ----------------------------------------------- + + RELAY_HOST: + RELAY_PORT: 25 + RELAY_USER: + RELAY_PASSWORD: # Whether to enable dovecot replication. Allows the syncronization of a pair of dovecot servers # https://wiki.dovecot.org/Replication From 3ec5c88f25b2eb8e7200d8880e014ab190faa8f8 Mon Sep 17 00:00:00 2001 From: Charlie Savage Date: Thu, 11 Jan 2024 13:34:36 -0800 Subject: [PATCH 03/66] This commit change how config files are copied to the mailserver pod. Instead of dealing with individual files - all files under the config directory are read and either stored in a single configmap or a single secret. Files that have private or public in their name are considered secrets. The key for each file is its path (with / replaced by . to meet K8 rules) and then mounted in the container in the right location. In addition, config files are now treated as templates so can include HAML templates/functions/etc. Last, this eliminates the need for an initContainer. --- .../config/80-replication.conf | 33 +++++++ .../config/91-override-sieve.conf | 4 + .../docker-mailserver/config/am-i-healthy.sh | 7 ++ charts/docker-mailserver/config/dovecot.cf | 29 +++++- .../templates/configmap.yaml | 94 ++----------------- .../templates/deployment.yaml | 90 ++++++------------ .../docker-mailserver/templates/secret.yaml | 15 ++- charts/docker-mailserver/values.yaml | 22 ----- 8 files changed, 113 insertions(+), 181 deletions(-) create mode 100644 charts/docker-mailserver/config/80-replication.conf create mode 100644 charts/docker-mailserver/config/91-override-sieve.conf create mode 100644 charts/docker-mailserver/config/am-i-healthy.sh diff --git a/charts/docker-mailserver/config/80-replication.conf b/charts/docker-mailserver/config/80-replication.conf new file mode 100644 index 00000000..6b49010b --- /dev/null +++ b/charts/docker-mailserver/config/80-replication.conf @@ -0,0 +1,33 @@ +mail_plugins = $mail_plugins notify replication + +service replicator { + process_min_avail = 1 + unix_listener replicator-doveadm { + mode = 0600 + user = docker + } +} + +service aggregator { + fifo_listener replication-notify-fifo { + user = docker + } + unix_listener replication-notify { + user = docker + } +} + +doveadm_port = 4117 +doveadm_password = secret + +service doveadm { + inet_listener { + port = 4117 + ssl = yes + } +} + +plugin { + #mail_replica = tcp:anotherhost.example.com # use doveadm_port + #mail_replica = tcp:anotherhost.example.com:12345 # use port 12345 explicitly +} diff --git a/charts/docker-mailserver/config/91-override-sieve.conf b/charts/docker-mailserver/config/91-override-sieve.conf new file mode 100644 index 00000000..c5bec151 --- /dev/null +++ b/charts/docker-mailserver/config/91-override-sieve.conf @@ -0,0 +1,4 @@ +plugin { + sieve = /var/mail/sieve/%d/%n/.dovecot.sieve + sieve_dir = /var/mail/sieve/%d/%n/sieve +} diff --git a/charts/docker-mailserver/config/am-i-healthy.sh b/charts/docker-mailserver/config/am-i-healthy.sh new file mode 100644 index 00000000..3f1b8b0a --- /dev/null +++ b/charts/docker-mailserver/config/am-i-healthy.sh @@ -0,0 +1,7 @@ +#!/bin/bash +# this script is intended to be used by periodic kubernetes liveness probes to ensure that the container +# (and all its dependent services) is healthy +{{ range .Values.livenessTests.commands -}} +{{ . }} && \ +{{- end }} +echo "All healthy" diff --git a/charts/docker-mailserver/config/dovecot.cf b/charts/docker-mailserver/config/dovecot.cf index f1358884..641b1ba4 100644 --- a/charts/docker-mailserver/config/dovecot.cf +++ b/charts/docker-mailserver/config/dovecot.cf @@ -1,4 +1,25 @@ -# File for additional dovecot configurations. -# For more informations read http://wiki.dovecot.org/BasicConfiguration - -#mail_max_userip_connections = 50 +{{- if .Values.haproxy.enabled }} +haproxy_trusted_networks = {{ .Values.haproxy.trustedNetworks }} +{{- end -}} +service imap-login { + inet_listener imap { + {{ if .Values.haproxy.enabled -}}haproxy = yes{{ end }} + } + inet_listener imaps { + {{ if .Values.haproxy.enabled -}}haproxy = yes{{ end }} + } +{{- if .Values.rainloop.enabled }} + inet_listener imaps-rainloop { + port = 10993 + ssl = yes + } +{{- end }} +} +service pop3-login { + inet_listener pop3 { + {{ if .Values.haproxy.enabled -}}haproxy = yes{{ end }} + } + inet_listener pop3s { + {{ if .Values.haproxy.enabled -}}haproxy = yes{{ end }} + } +} diff --git a/charts/docker-mailserver/templates/configmap.yaml b/charts/docker-mailserver/templates/configmap.yaml index 4d43b3bd..d1ab9159 100644 --- a/charts/docker-mailserver/templates/configmap.yaml +++ b/charts/docker-mailserver/templates/configmap.yaml @@ -29,92 +29,12 @@ data: ### End demo mode data {{/* Use real data from "config" subdirectory if user is _not_ running in demo mode */}} {{ else -}} - postfix-main.cf: | - {{/* Enable proxy protocol for postscreen / dovecot */}} - {{- if .Values.haproxy.enabled }} # Necessary to permit proxy protocol from haproxy to postscreen - postscreen_upstream_proxy_protocol = haproxy - {{ end }} - {{ if not .Values.spfTestsDisabled }} - smtpd_recipient_restrictions = permit_sasl_authenticated, permit_mynetworks, reject_unauth_destination, reject_unauth_pipelining, reject_invalid_helo_hostname, reject_non_fqdn_helo_hostname, reject_unknown_recipient_domain, reject_rbl_client zen.spamhaus.org, reject_rbl_client bl.spamcop.net{{ range .Values.rblRejectDomains }}, reject_rbl_client {{ . }}{{ end }} - {{ end -}} - {{- (.Files.Glob "config/*").AsConfig | nindent 2 }} - {{- (.Files.Glob "config/opendkim/*").AsConfig | nindent 2 }} + {{- range $path, $content := .Files.Glob "config/**" }} + {{/* Skip empty files and public / private keys */}} + {{- if and (gt (len $content) 0) (not (contains "private" $path)) (not (contains "public" $path)) }} + {{ $path | replace "/" "." }}: | +{{ tpl ($.Files.Get $path) $ | indent 6 }} {{- end }} - dovecot-services.cf: | - {{- if .Values.haproxy.enabled }} - haproxy_trusted_networks = {{ .Values.haproxy.trustedNetworks }} - {{ end }} - service imap-login { - inet_listener imap { - {{ if .Values.haproxy.enabled -}}haproxy = yes{{ end }} - } - inet_listener imaps { - {{ if .Values.haproxy.enabled -}}haproxy = yes{{ end }} - } - {{- if .Values.rainloop.enabled }} - inet_listener imaps-rainloop { - port = 10993 - ssl = yes - } - {{ end }} - } - service pop3-login { - inet_listener pop3 { - {{ if .Values.haproxy.enabled -}}haproxy = yes{{ end }} - } - inet_listener pop3s { - {{ if .Values.haproxy.enabled -}}haproxy = yes{{ end }} - } - } - - - 80-replication.conf: | - mail_plugins = $mail_plugins notify replication - - service replicator { - process_min_avail = 1 - unix_listener replicator-doveadm { - mode = 0600 - user = docker - } - } - - service aggregator { - fifo_listener replication-notify-fifo { - user = docker - } - unix_listener replication-notify { - user = docker - } - } - - doveadm_port = 4117 - doveadm_password = secret - - service doveadm { - inet_listener { - port = 4117 - ssl = yes - } - } - - plugin { - #mail_replica = tcp:anotherhost.example.com # use doveadm_port - #mail_replica = tcp:anotherhost.example.com:12345 # use port 12345 explicitly - } - - 91-override-sieve.conf: | - plugin { - sieve = /var/mail/sieve/%d/%n/.dovecot.sieve - sieve_dir = /var/mail/sieve/%d/%n/sieve - } - - am-i-healthy.sh: | - #!/bin/bash - # this script is intended to be used by periodic kubernetes liveness probes to ensure that the container - # (and all its dependent services) is healthy - {{ range .Values.livenessTests.commands -}} - {{ . }} && \ - {{- end }} - echo "All healthy" + {{- end }} +{{- end }} {{- end -}} diff --git a/charts/docker-mailserver/templates/deployment.yaml b/charts/docker-mailserver/templates/deployment.yaml index 9a2645cf..901790b7 100644 --- a/charts/docker-mailserver/templates/deployment.yaml +++ b/charts/docker-mailserver/templates/deployment.yaml @@ -37,13 +37,13 @@ spec: volumes: - name: "data" persistentVolumeClaim: - claimName: {{ template "dockermailserver.pvcName" . }} + claimName: {{ template "dockermailserver.fullname" . }} - name: "config" emptyDir: {} - name: "configmap" configMap: name: {{ template "dockermailserver.fullname" . }}-configs - - name: "opendkim-keys" + - name: "secrets" secret: secretName: {{ template "dockermailserver.fullname" . }}-secrets {{- if and .Values.pod.dockermailserver.env.SSL_TYPE .Values.ssl.useExisting }} @@ -56,28 +56,21 @@ spec: {{- end }} - name: tmp emptyDir: {} - initContainers: - - name: prep-config - image: {{ .Values.initContainer.image.name }}:{{ .Values.initContainer.image.tag }} - imagePullPolicy: {{ .Values.initContainer.image.pullPolicy }} - command: [ 'sh','-c', 'cp /tmp/configmaps/* /tmp/docker-mailserver -rfpvL' ] - volumeMounts: - - name: configmap - mountPath: /tmp/configmaps - readOnly: true - - name: config - mountPath: /tmp/docker-mailserver/ - resources: -{{ toYaml .Values.initContainer.resources | indent 12 }} - securityContext: -{{ toYaml .Values.initContainer.containerSecurityContext | indent 12 }} containers: - name: docker-mailserver env: {{- range $pkey, $pval := .Values.pod.dockermailserver.env }} - name: {{ $pkey }} value: {{ quote $pval }} - {{- end }} + {{- end }} + + {{- if .Values.pod.dockermailserver.env.SSL_TYPE }} + - name: SSL_CERT_PATH + value: /tmp/dms/custom-certs/tls.crt + - name: SSL_KEY_PATH + value: /tmp/dms/custom-certs/tls.key + {{- end }} + image: {{ .Values.image.name }}:{{ .Values.image.tag }} imagePullPolicy: {{ .Values.image.pullPolicy }} resources: @@ -94,7 +87,7 @@ spec: mountPath: /tmp/docker-mailserver {{- if and .Values.pod.dockermailserver.env.SSL_TYPE .Values.ssl.useExisting }} - name: ssl-cert - mountPath: /tmp/ssl + mountPath: /tmp/dms/custom-certs readOnly: true {{- end }} - name: tmp @@ -110,52 +103,31 @@ spec: - name: data mountPath: /var/mail-state subPath: mail-state + + {{- range $path, $content := .Files.Glob "config/**" }} + {{- if and (gt (len $content) 0) (not (contains "private" $path)) (not (contains "public" $path)) }} - name: configmap - subPath: dovecot-services.cf - mountPath: /etc/dovecot/conf.d/services.cf - readOnly: true - - name: configmap - subPath: dovecot.cf - mountPath: /etc/dovecot/conf.d/zz-custom.cf - readOnly: true - - name: configmap - subPath: TrustedHosts - mountPath: /tmp/docker-mailserver/opendkim/TrustedHosts - readOnly: true - - name: configmap - subPath: SigningTable - mountPath: /tmp/docker-mailserver/opendkim/SigningTable - readOnly: true - - name: configmap - subPath: KeyTable - mountPath: /tmp/docker-mailserver/opendkim/KeyTable - readOnly: true - {{- if .Values.pod.dockermailserver.enable_dovecot_replication }} - - name: configmap - subPath: 80-replication.conf - mountPath: /etc/dovecot/conf.d/80-replication.conf + subPath: {{ $path | replace "/" "." }} + mountPath: /tmp/docker-mailserver/{{ $path | trimPrefix "config/" }} readOnly: true - - name: configmap - subPath: 91-override-sieve.conf - mountPath: /etc/dovecot/conf.d/91-override-sieve.conf + {{- end }} + {{- end }} + + {{- range $path, $content := .Files.Glob "config/**" }} + {{- if and (gt (len $content) 0) (or (contains "private" $path) (contains "public" $path)) }} + - name: secrets + subPath: {{ $path | replace "/" "." }} + mountPath: /tmp/docker-mailserver/{{ $path | trimPrefix "config/" }} readOnly: true - {{- end }} + {{- end }} + {{- end }} + {{- if .Values.demoMode.enabled }} - name: opendkim-keys mountPath: "/tmp/docker-mailserver/opendkim/keys/example.com/mail.private" subPath: "example.com-mail.private" readOnly: true - {{- else -}} - {{/* Mount a dkim key for every domain configured */}} - {{- range .Values.domains }} - {{- $path := printf "/tmp/docker-mailserver/opendkim/keys/%s/mail.private" . }} - {{- $name := printf "%s-mail.private" . }} - - name: opendkim-keys - mountPath: {{ $path }} - subPath: {{ $name }} - readOnly: true - {{- end }} - {{- end }} + {{- end }} {{- if .Values.additionalVolumeMounts }} {{ toYaml .Values.additionalVolumeMounts | indent 12 }} {{- end }} @@ -177,8 +149,7 @@ spec: initialDelaySeconds: 10 timeoutSeconds: 5 failureThreshold: 3 - -{{ if .Values.metrics.enabled }} +{{- if .Values.metrics.enabled }} - name: metrics-exporter image: {{ .Values.metrics.image.name }}:{{ .Values.metrics.image.tag }} imagePullPolicy: {{ .Values.metrics.image.pullPolicy }} @@ -209,5 +180,4 @@ spec: readOnly: true {{- end }} - restartPolicy: "Always" \ No newline at end of file diff --git a/charts/docker-mailserver/templates/secret.yaml b/charts/docker-mailserver/templates/secret.yaml index 1d912a57..ee79c630 100644 --- a/charts/docker-mailserver/templates/secret.yaml +++ b/charts/docker-mailserver/templates/secret.yaml @@ -11,7 +11,7 @@ metadata: release: "{{ .Release.Name }}" name: {{ template "dockermailserver.fullname" . }}-secrets data: - {{/* Import any files ending in .secret, as secrets */}} + {{- /* Import any files ending in .secret, as secrets */}} {{- $globdash := .Files.Glob "*.secret" }} {{- if $globdash -}} {{- (.Files.Glob "*.secret").AsSecrets | nindent 2 -}} @@ -21,12 +21,11 @@ data: example.com-mail.private: {{ .Files.Get "demo-mode-dkim-key-for-example.com.key" | b64enc }} {{/* If we're _not_ in demo mode, assume the user has created dkim keys for his domains, and import them */}} {{- else -}} - {{/* Import any opendkim keys for configured domains, as "-mail.private" */}} - {{- $files := .Files }} - {{ range .Values.domains }} - {{- $path := printf "config/opendkim/keys/%s/mail.private" . }} - {{- $name := printf "%s-mail.private" . }} - {{ $name }}: {{ $files.Get $path | b64enc }} - {{ end -}} + {{- range $path, $content := .Files.Glob "config/**" }} + {{- /* Skip empty files and contains public or private keys */}} + {{- if and (gt (len $content) 0) (or (contains "private" $path) (contains "public" $path)) }} + {{ $path | replace "/" "." }}: {{ tpl ($.Files.Get $path) $ | b64enc }} + {{- end }} + {{- end }} {{ end }} {{- end -}} \ No newline at end of file diff --git a/charts/docker-mailserver/values.yaml b/charts/docker-mailserver/values.yaml index 5aabc347..1ddf3a4d 100644 --- a/charts/docker-mailserver/values.yaml +++ b/charts/docker-mailserver/values.yaml @@ -6,28 +6,6 @@ image: tag: "13.2.0" pullPolicy: "IfNotPresent" -initContainer: - image: - name: "busybox" - tag: "stable" - pullPolicy: "IfNotPresent" - - # These resources refer specifically to the _init container_, which needs only _puny_ resources, - # since all it does is copy the config into place - resources: - requests: - cpu: "10m" - memory: "32Mi" - ephemeral-storage: "100Mi" - limits: - cpu: "50m" - memory: "64Mi" - ephemeral-storage: "100Mi" - - containerSecurityContext: - readOnlyRootFilesystem: true - privileged: false - ## Optionally specify a runtimeClassName for the deployment runtimeClassName: From 5fb95c135ea28381ef0b36dcf64257f4476bdd13 Mon Sep 17 00:00:00 2001 From: Charlie Savage Date: Fri, 12 Jan 2024 23:00:55 -0800 Subject: [PATCH 04/66] Remove rainloop which is no longer maintained and should be in a separate Helm chart anyway. --- charts/docker-mailserver/README.md | 18 ------ charts/docker-mailserver/ci/ci-rainloop.yaml | 6 -- charts/docker-mailserver/config/dovecot.cf | 6 -- charts/docker-mailserver/templates/NOTES.txt | 18 ------ .../docker-mailserver/templates/_helpers.tpl | 11 ---- .../templates/deployment-rainloop.yaml | 60 ------------------- .../templates/ingress-rainloop.yaml | 44 -------------- .../templates/pvc-rainloop.yaml | 29 --------- .../templates/service-rainloop.yaml | 32 ---------- .../docker-mailserver/templates/service.yaml | 5 -- .../templates/serviceaccount-rainloop.yaml | 11 ---- .../tests/configmap_test.yaml | 5 +- .../tests/deployment-rainloop_test.yaml | 22 ------- .../tests/ingress-rainloop_test.yaml | 22 ------- charts/docker-mailserver/tests/oobe_test.yaml | 4 +- .../tests/pvc-rainloop_test.yaml | 33 ---------- .../tests/service-rainloop_test.yaml | 22 ------- charts/docker-mailserver/values.yaml | 43 ------------- 18 files changed, 4 insertions(+), 387 deletions(-) delete mode 100644 charts/docker-mailserver/ci/ci-rainloop.yaml delete mode 100644 charts/docker-mailserver/templates/deployment-rainloop.yaml delete mode 100644 charts/docker-mailserver/templates/ingress-rainloop.yaml delete mode 100644 charts/docker-mailserver/templates/pvc-rainloop.yaml delete mode 100644 charts/docker-mailserver/templates/service-rainloop.yaml delete mode 100644 charts/docker-mailserver/templates/serviceaccount-rainloop.yaml delete mode 100644 charts/docker-mailserver/tests/deployment-rainloop_test.yaml delete mode 100644 charts/docker-mailserver/tests/ingress-rainloop_test.yaml delete mode 100644 charts/docker-mailserver/tests/pvc-rainloop_test.yaml delete mode 100644 charts/docker-mailserver/tests/service-rainloop_test.yaml diff --git a/charts/docker-mailserver/README.md b/charts/docker-mailserver/README.md index bf560fa0..2adb85e7 100644 --- a/charts/docker-mailserver/README.md +++ b/charts/docker-mailserver/README.md @@ -23,12 +23,10 @@ Kubernetes](https://github.com/docker-mailserver/docker-mailserver/wiki/Using-in - [Download setup.sh](#download-setupsh) - [Create / Update / Delete users](#create--update--delete-users) - [Setup OpenDKIM](#setup-opendkim) - - [Setup RainLoop](#setup-rainloop) - [Configuration](#docker-mailserver-configuration) - [Minimal configuration](#minimal-configuration) - [Chart Configuration](#chart-configuration) - [docker-mailserver Configuration](#docker-mailserver-configuration) - - [Rainloop Configuration](#rainloop-configuration) - [HA Proxy-Ingress Configuration](#ha-proxy-ingress-configuration) - [Development](#development) - [Testing](#testing) @@ -42,7 +40,6 @@ The chart includes the following features: - All configuration is done in values.yaml, or using the native "setup.sh" script (to create mailboxes or DKIM keys) - Avoids the [common problem of masking of source IP](https://kubernetes.io/docs/tutorials/services/source-ip/) by supporting haproxy's PROXY protocol (enabled by default) - Employs [cert-manager](https://github.com/jetstack/cert-manager) to automatically provide/renew SSL certificates -- Bundles in [RainLoop](https://www.rainloop.net) for webmail access (disabled by default) - Starts in "demo" mode, allowing the user to test core functionality before configuring for specific domains - CI/CD tested against Kubernetes 1.18,1.19, and 1.20 : ![Lint and Test Charts](https://github.com/funkypenguin/helm-docker-mailserver/workflows/Lint%20and%20Test%20Charts/badge.svg) @@ -163,12 +160,6 @@ Creating DKIM TrustedHosts [funkypenguin:~/demo] ``` -### Setup RainLoop - -If employing HAProxy with RainLoop, use port 10993 for your IMAPS server, as illustrated below: - -![Rainloop with HAProxy screenshot](rainloop_with_haproxy.png) - ### Docker Mailserver Configuration All configuration values are documented in values.yaml. Check that for references, default values etc. To modify a @@ -191,7 +182,6 @@ Most of the values recorded belowe are set to sensible default, butyou'll defina | Parameter | Description | Default | |------------------------------------------|-----------------------------------------------------------------------------------------------------------------------|------------------------| | `pod.dockermailserver.override_hostname` | The hostname to be presented on SMTP banners | `mail.batcave.org` | -| `rainloop.ingress.hosts` | The hostname(s) to be used via your ingress to access RainLoop | `rainloop.example.com` | | `demoMode.enabled` | Start the container with a demo "user@example.com" user (password is "password") | `true` | | `domains` | List of domains to be served | `[]` | | `ssl.issuer.name` | The name of the cert-manager issuer expected to issue certs | `letsencrypt-staging` | @@ -246,14 +236,6 @@ There are **many** environment variables which allow you to customize the behavi Every variable can be set using `values.yaml`, but note that docker-mailserver expects any true/false values to be set as binary numbers (1/0), rather than boolean (true/false). BadThings(tm) will happen if you try to pass an environment variable as "true" when [`start-mailserver.sh`](https://github.com/docker-mailserver/docker-mailserver/blob/master/target/start-mailserver.sh) is expecting a 1 or a 0! -#### Rainloop Configuration - -Values you'll definately want to pay attention to: - -| Parameter | Description | Default | -| -------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------- | -| `rainloop.ingress.hosts` | The hostname(s) to be used via your ingress to access RainLoop | `rainloop.example.com` | - #### HA Proxy-Ingress Configuration | Parameter | Description | Default | diff --git a/charts/docker-mailserver/ci/ci-rainloop.yaml b/charts/docker-mailserver/ci/ci-rainloop.yaml deleted file mode 100644 index 507b2376..00000000 --- a/charts/docker-mailserver/ci/ci-rainloop.yaml +++ /dev/null @@ -1,6 +0,0 @@ -# This file exists to facilitate testing various install scenarios using ct -rainloop: - enabled: true - -service: - type: ClusterIP \ No newline at end of file diff --git a/charts/docker-mailserver/config/dovecot.cf b/charts/docker-mailserver/config/dovecot.cf index 641b1ba4..2165ede6 100644 --- a/charts/docker-mailserver/config/dovecot.cf +++ b/charts/docker-mailserver/config/dovecot.cf @@ -8,12 +8,6 @@ service imap-login { inet_listener imaps { {{ if .Values.haproxy.enabled -}}haproxy = yes{{ end }} } -{{- if .Values.rainloop.enabled }} - inet_listener imaps-rainloop { - port = 10993 - ssl = yes - } -{{- end }} } service pop3-login { inet_listener pop3 { diff --git a/charts/docker-mailserver/templates/NOTES.txt b/charts/docker-mailserver/templates/NOTES.txt index 9320ec7d..0ac7af02 100644 --- a/charts/docker-mailserver/templates/NOTES.txt +++ b/charts/docker-mailserver/templates/NOTES.txt @@ -106,21 +106,3 @@ You've disabled haproxy support. This means that you'll need to MANUALLY configu to the internet. You may be running in host mode, or nodeport mode, or some other complex configuration. ------- {{ end }} - -{{- if .Values.rainloop.enabled -}} -{{- if .Values.rainloop.ingress.enabled -}} -{{ if .Values.haproxy.enabled -}} ---- -You've enabled RainLoop integration. Because you're using HAProxy, you'll need to configure RainLoop -to use an IMAPS server of {{ template "dockermailserver.fullname" . }} on port 10993 (with SSL), since port 993 -expects to receive a PROXY header from haproxy, for external IMAPS connections. Use port 465 (with SSL) for SMTP ---- -{{ else }} ---- -You've enabled RainLoop integration. You'll need to configure RainLoop -to use an IMAPS server of {{ template "dockermailserver.fullname" . }} on port 993 (with SSL), and an -SMTP server of {{ template "dockermailserver.fullname" . }} on port 465 (with SSL) ---- -{{ end }} -{{ end }} -{{ end }} \ No newline at end of file diff --git a/charts/docker-mailserver/templates/_helpers.tpl b/charts/docker-mailserver/templates/_helpers.tpl index 823299bc..0642ce66 100644 --- a/charts/docker-mailserver/templates/_helpers.tpl +++ b/charts/docker-mailserver/templates/_helpers.tpl @@ -46,14 +46,3 @@ Create the name of the controller service account to use {{ default "docker-mailserver" .Values.serviceAccount.name }} {{- end -}} {{- end -}} - -{{/* -Create the name of the controller service account to use -*/}} -{{- define "dockermailserver.rainloop.serviceAccountName" -}} -{{- if .Values.rainloop.serviceAccount.create -}} - {{ default (include "dockermailserver.fullname" .) .Values.rainloop.serviceAccount.name }} -{{- else -}} - {{ default "rainloop" .Values.rainloop.serviceAccount.name }} -{{- end -}} -{{- end -}} \ No newline at end of file diff --git a/charts/docker-mailserver/templates/deployment-rainloop.yaml b/charts/docker-mailserver/templates/deployment-rainloop.yaml deleted file mode 100644 index 3e37be64..00000000 --- a/charts/docker-mailserver/templates/deployment-rainloop.yaml +++ /dev/null @@ -1,60 +0,0 @@ -{{/* This whole template is only necessary if we're using rainloop */}} -{{ if .Values.rainloop.enabled -}} ---- -apiVersion: "apps/v1" -kind: "Deployment" -metadata: - labels: - app.kubernetes.io/name: {{ template "dockermailserver.fullname" . }} - chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" - heritage: "{{ .Release.Service }}" - release: "{{ .Release.Name }}" - name: {{ template "dockermailserver.fullname" . }}-rainloop -spec: - replicas: {{ default 2 .Values.deployment.replicas }} - selector: - matchLabels: - app.kubernetes.io/name: {{ template "dockermailserver.fullname" . }}-rainloop - release: "{{ .Release.Name }}" - strategy: {{- toYaml .Values.rainloop.strategy | nindent 4 }} - template: - metadata: - labels: - app.kubernetes.io/name: {{ template "dockermailserver.fullname" . }}-rainloop - release: "{{ .Release.Name }}" - {{- if .Values.rainloop.annotations }} - annotations: -{{ toYaml .Values.rainloop.annotations | indent 8 }} - {{ end }} - spec: - serviceAccountName: {{ template "dockermailserver.rainloop.serviceAccountName" . }} - containers: - - name: rainloop - image: "{{ .Values.rainloop.image.name }}:{{ .Values.rainloop.image.tag }}" - imagePullPolicy: {{ .Values.rainloop.image.pullPolicy }} - ports: - - name: http - containerPort: 8888 - protocol: TCP - livenessProbe: - httpGet: - path: / - port: http - readinessProbe: - httpGet: - path: / - port: http - volumeMounts: - - name: data - mountPath: "/rainloop/data" - subPath: "rainloop" - env: - - name: LOG_TO_STDOUT - value: "true" - - volumes: - - name: "data" - persistentVolumeClaim: - claimName: {{ template "dockermailserver.pvcName" . }}-rainloop - -{{ end }} \ No newline at end of file diff --git a/charts/docker-mailserver/templates/ingress-rainloop.yaml b/charts/docker-mailserver/templates/ingress-rainloop.yaml deleted file mode 100644 index 9f20ece5..00000000 --- a/charts/docker-mailserver/templates/ingress-rainloop.yaml +++ /dev/null @@ -1,44 +0,0 @@ -{{- if .Values.rainloop.enabled -}} -{{- if .Values.rainloop.ingress.enabled -}} -{{- $fullName := include "dockermailserver.fullname" . -}} -{{- $servicePort := .Values.rainloop.service.port -}} -{{- $ingressPath := .Values.rainloop.ingress.path -}} -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - labels: - app.kubernetes.io/name: {{ template "dockermailserver.fullname" . }} - chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" - heritage: "{{ .Release.Service }}" - release: "{{ .Release.Name }}" - name: {{ template "dockermailserver.fullname" . }} -{{- with .Values.rainloop.ingress.annotations }} - annotations: -{{ toYaml . | indent 4 }} -{{- end }} -spec: -{{- if .Values.rainloop.ingress.tls }} - tls: - {{- range .Values.rainloop.ingress.tls }} - - hosts: - {{- range .hosts }} - - {{ . }} - {{- end }} - secretName: {{ .secretName }} - {{- end }} -{{- end }} - rules: - {{- range .Values.rainloop.ingress.hosts }} - - host: {{ . }} - http: - paths: - - pathType: Prefix - path: {{ $ingressPath }} - backend: - service: - name: {{ $fullName }}-rainloop - port: - number: {{ $servicePort }} - {{- end }} -{{- end }} -{{- end }} diff --git a/charts/docker-mailserver/templates/pvc-rainloop.yaml b/charts/docker-mailserver/templates/pvc-rainloop.yaml deleted file mode 100644 index fe1897f1..00000000 --- a/charts/docker-mailserver/templates/pvc-rainloop.yaml +++ /dev/null @@ -1,29 +0,0 @@ -{{/* This whole template is only necessary if we're using rainloop */}} -{{- if .Values.rainloop.enabled -}} -{{- if not .Values.rainloop.persistence.existingClaim -}} ---- -kind: "PersistentVolumeClaim" -apiVersion: "v1" -metadata: - name: {{ template "dockermailserver.fullname" . }}-rainloop - annotations: - {{- if .Values.rainloop.persistence.storageClass }} - volume.beta.kubernetes.io/storage-class: {{ .Values.rainloop.persistence.storageClass | quote }} - {{- else }} - volume.alpha.kubernetes.io/storage-class: "generic" - {{- end }} - {{- if .Values.rainloop.persistence.annotations }} - {{ toYaml .Values.rainloop.persistence.annotations | indent 2 }} - {{ end }} -spec: - accessModes: - - {{ default "ReadWriteOnce" .Values.rainloop.persistence.accessMode | quote }} - resources: - requests: - storage: {{ .Values.rainloop.persistence.size | quote }} - {{- if .Values.rainloop.persistence.selector }} - selector: -{{ toYaml .Values.rainloop.persistence.selector | indent 4 }} - {{ end }} -{{- end }} -{{- end }} diff --git a/charts/docker-mailserver/templates/service-rainloop.yaml b/charts/docker-mailserver/templates/service-rainloop.yaml deleted file mode 100644 index 0ebbc5bb..00000000 --- a/charts/docker-mailserver/templates/service-rainloop.yaml +++ /dev/null @@ -1,32 +0,0 @@ -{{/* This whole template is only necessary if we're using rainloop */}} -{{- if .Values.rainloop.enabled -}} ---- -kind: "Service" -apiVersion: "v1" -metadata: - annotations: - ## These annontations mark the service as monitorable by Prometheus, both directly as a service level metric and - ## via the blackbox exporter. For more information, see - ## values.yaml - prometheus.io/scrape: {{ .Values.monitoring.service.scrape | quote }} - prometheus.io/probe: {{ .Values.monitoring.service.probe | quote }} - prometheus.io/path: {{ .Values.monitoring.service.path | quote }} - prometheus.io/port: {{ .Values.monitoring.service.port | quote }} - ## - {{- if eq .Values.service.type "LoadBalancer" }}service.beta.kubernetes.io/external-traffic: "OnlyLocal"{{ end }} - labels: - app.kubernetes.io/name: {{ template "dockermailserver.fullname" . }} - chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" - heritage: "{{ .Release.Service }}" - release: "{{ .Release.Name }}" - name: {{ template "dockermailserver.fullname" . }}-rainloop -spec: - selector: - app.kubernetes.io/name: {{ template "dockermailserver.fullname" . }}-rainloop - release: "{{ .Release.Name }}" - ports: - - port: {{ .Values.rainloop.service.port }} - targetPort: {{ .Values.rainloop.container.port }} - protocol: TCP - name: http -{{ end }} \ No newline at end of file diff --git a/charts/docker-mailserver/templates/service.yaml b/charts/docker-mailserver/templates/service.yaml index 042fedb1..404ebfe7 100644 --- a/charts/docker-mailserver/templates/service.yaml +++ b/charts/docker-mailserver/templates/service.yaml @@ -80,11 +80,6 @@ spec: - protocol: "TCP" name: "tcp-dsync" port: 4711 - {{- if .Values.rainloop.enabled }} - - protocol: "TCP" - name: "tcp-imaps-rainloop" - port: 10993 - {{ end }} {{- if .Values.metrics.enabled }} - name: tcp-metrics diff --git a/charts/docker-mailserver/templates/serviceaccount-rainloop.yaml b/charts/docker-mailserver/templates/serviceaccount-rainloop.yaml deleted file mode 100644 index 46242671..00000000 --- a/charts/docker-mailserver/templates/serviceaccount-rainloop.yaml +++ /dev/null @@ -1,11 +0,0 @@ -{{- if and .Values.rainloop.enabled .Values.serviceAccount.create -}} -apiVersion: v1 -kind: ServiceAccount -metadata: - labels: - app.kubernetes.io/name: {{ template "dockermailserver.fullname" . }} - chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" - heritage: {{ .Release.Service }} - release: {{ .Release.Name }} - name: {{ template "dockermailserver.rainloop.serviceAccountName" . }} -{{- end -}} diff --git a/charts/docker-mailserver/tests/configmap_test.yaml b/charts/docker-mailserver/tests/configmap_test.yaml index 1846c0d8..43309e91 100644 --- a/charts/docker-mailserver/tests/configmap_test.yaml +++ b/charts/docker-mailserver/tests/configmap_test.yaml @@ -12,14 +12,13 @@ tests: pattern: "dbpurgeage" - - it: should configure imaps port 10993 for rainloop if enabled (and haproxy enabled) + - it: should configure imaps port 10993 if haproxy enabled set: - rainloop.enabled: true haproxy.enabled: true asserts: - matchRegex: path: data.dovecot\.cf - pattern: rainloop + pattern: 10993 - it: manifest should match snapshot asserts: diff --git a/charts/docker-mailserver/tests/deployment-rainloop_test.yaml b/charts/docker-mailserver/tests/deployment-rainloop_test.yaml deleted file mode 100644 index 9c8787d1..00000000 --- a/charts/docker-mailserver/tests/deployment-rainloop_test.yaml +++ /dev/null @@ -1,22 +0,0 @@ -suite: deployment-rainloop -templates: - - deployment-rainloop.yaml -tests: - - - it: should create rainloop deployment if enabled - set: - rainloop.enabled: true - asserts: - - hasDocuments: - count: 1 - - - it: should not create rainloop deployment if disabled - set: - rainloop.enabled: false - asserts: - - hasDocuments: - count: 0 - - - it: manifest should match snapshot - asserts: - - matchSnapshot: {} \ No newline at end of file diff --git a/charts/docker-mailserver/tests/ingress-rainloop_test.yaml b/charts/docker-mailserver/tests/ingress-rainloop_test.yaml deleted file mode 100644 index e9682f91..00000000 --- a/charts/docker-mailserver/tests/ingress-rainloop_test.yaml +++ /dev/null @@ -1,22 +0,0 @@ -suite: ingress-rainloop -templates: - - ingress-rainloop.yaml -tests: - - - it: should create rainloop ingress if enabled - set: - rainloop.enabled: true - asserts: - - hasDocuments: - count: 1 - - - it: should not create rainloop ingress if disabled - set: - rainloop.enabled: false - asserts: - - hasDocuments: - count: 0 - - - it: manifest should match snapshot - asserts: - - matchSnapshot: {} \ No newline at end of file diff --git a/charts/docker-mailserver/tests/oobe_test.yaml b/charts/docker-mailserver/tests/oobe_test.yaml index 1bd0671e..5ed26ec7 100644 --- a/charts/docker-mailserver/tests/oobe_test.yaml +++ b/charts/docker-mailserver/tests/oobe_test.yaml @@ -46,12 +46,12 @@ tests: path: data.dovecot\.cf pattern: haproxy - - it: should configure imaps port 10993 for rainloop if enabled (and haproxy enabled) + - it: should configure imaps port 10993 if haproxy is enabled set: asserts: - matchRegex: path: data.dovecot\.cf - pattern: rainloop + pattern: 10993 - it: manifest should match snapshot asserts: diff --git a/charts/docker-mailserver/tests/pvc-rainloop_test.yaml b/charts/docker-mailserver/tests/pvc-rainloop_test.yaml deleted file mode 100644 index d78337c4..00000000 --- a/charts/docker-mailserver/tests/pvc-rainloop_test.yaml +++ /dev/null @@ -1,33 +0,0 @@ - -suite: pvc-rainloop -templates: - - pvc-rainloop.yaml -tests: - - - it: should apply annotations from persistence.annotations - set: - rainloop.persistence.annotations.backup\.banana\.io/deltas: pancakes - asserts: - - equal: - path: metadata.annotations.backup\.banana\.io/deltas - value: pancakes - - - it: should create rainloop pvc if enabled - set: - rainloop.enabled: true - rainloop.persistence.size: 1Pb - asserts: - - equal: - path: spec.resources.requests.storage - value: 1Pb - - - it: should not create rainloop pvc if disabled - set: - rainloop.enabled: false - asserts: - - hasDocuments: - count: 0 - - - it: manifest should match snapshot - asserts: - - matchSnapshot: {} \ No newline at end of file diff --git a/charts/docker-mailserver/tests/service-rainloop_test.yaml b/charts/docker-mailserver/tests/service-rainloop_test.yaml deleted file mode 100644 index 7063c58d..00000000 --- a/charts/docker-mailserver/tests/service-rainloop_test.yaml +++ /dev/null @@ -1,22 +0,0 @@ -suite: service-rainloop -templates: - - service-rainloop.yaml -tests: - - - it: should create rainloop service if enabled - set: - rainloop.enabled: true - asserts: - - hasDocuments: - count: 1 - - - it: should not create rainloop service if disabled - set: - rainloop.enabled: false - asserts: - - hasDocuments: - count: 0 - - - it: manifest should match snapshot - asserts: - - matchSnapshot: {} \ No newline at end of file diff --git a/charts/docker-mailserver/values.yaml b/charts/docker-mailserver/values.yaml index 1ddf3a4d..f27e1619 100644 --- a/charts/docker-mailserver/values.yaml +++ b/charts/docker-mailserver/values.yaml @@ -386,49 +386,6 @@ monitoring: ## Port on which HTTP server is served port: "9102" -# Values imported from https://github.com/t13a/helm-chart-rainloop/blob/master/values.yaml -rainloop: - # rainloop.enabled will include a rainloop (webmail) pod in the release - enabled: false - serviceAccount: - create: true - name: "rainloop" - image: - # rainloop.image.name is the docker container to use for the rainloop pod - name: "hardware/rainloop" - # rainloop.image.tag is the tag of the docker container to use for the rainloop pod - tag: "latest" - pullPolicy: "Always" - - ## Update strategy - only really applicable for deployments with RWO PVs attached - ## If replicas = 1, an update can get "stuck", as the previous pod remains attached to the - ## PV, and the "incoming" pod can never start. Setting the strategy to "Recreate" (our default) will - ## terminate the single previous pod, so that the new, incoming pod can attach to the PV. - strategy: - # rollingUpdate: - # maxSurge: 1 - # maxUnavailable: 1 - type: "Recreate" - - persistence: - size: "1Gi" - # Uncomment the backup.kubernetes.io/deltas annotation below if you use https://github.com/miracle2k/k8s-snapshots - annotations: {} - # backup.kubernetes.io/deltas: PT1H P2D P30D P180D - service: - port: 80 - container: - port: 8888 - ingress: - enabled: true - hosts: - - rainloop.example.com - annotations: {} - # kubernetes.io/ingress.class: nginx - # kubernetes.io/tls-acme: "true" - path: / - tls: [] - ## These values are for the haproxy sub-chart haproxy: # haproxy.enabled will deploy an haproxy sub-chart, configured for the TCP ports used by docker-mailserver From 76a6475db6facfe13a54c42b1ae9c361e7eccfb8 Mon Sep 17 00:00:00 2001 From: Charlie Savage Date: Fri, 12 Jan 2024 23:54:32 -0800 Subject: [PATCH 05/66] Add container ports to deployment, update Service to match, remove unneeded protocol field (default is TCP) and remove Dovecot dsync port since its not supported by docker-mailserver. --- .../templates/deployment.yaml | 32 ++++++ .../docker-mailserver/templates/service.yaml | 101 +++++++++--------- 2 files changed, 84 insertions(+), 49 deletions(-) diff --git a/charts/docker-mailserver/templates/deployment.yaml b/charts/docker-mailserver/templates/deployment.yaml index 901790b7..6552d4a5 100644 --- a/charts/docker-mailserver/templates/deployment.yaml +++ b/charts/docker-mailserver/templates/deployment.yaml @@ -149,6 +149,38 @@ spec: initialDelaySeconds: 10 timeoutSeconds: 5 failureThreshold: 3 + ports: + - name: "smtp" + containerPort: 25 + - name: "smtps" + containerPort: 465 + - name: "submission" + containerPort: 587 + + {{- if and (.Values.pod.dockermailserver.env.ENABLE_IMAP) (not .Values.pod.dockermailserver.env.SMTP_ONLY) }} + - name: "imap" + containerPort: 143 + - name: "imaps" + containerPort: 993 + {{- end }} + + {{- if and (.Values.pod.dockermailserver.env.ENABLE_POP) (not .Values.pod.dockermailserver.env.SMTP_ONLY) }} + - name: "pop3" + containerPort: 110 + - name: "pop3s" + containerPort: 995 + {{- end }} + + {{- if and (.Values.pod.dockermailserver.env.ENABLE_RSPAMD) (not .Values.pod.dockermailserver.env.SMTP_ONLY) }} + - name: "rspamd" + containerPort: 11334 + {{- end }} + + {{- if and (.Values.pod.dockermailserver.env.ENABLE_MANAGESIEVE) (not .Values.pod.dockermailserver.env.SMTP_ONLY) }} + - name: "managesieve" + containerPort: 4190 + {{- end }} + {{- if .Values.metrics.enabled }} - name: metrics-exporter image: {{ .Values.metrics.image.name }}:{{ .Values.metrics.image.tag }} diff --git a/charts/docker-mailserver/templates/service.yaml b/charts/docker-mailserver/templates/service.yaml index 404ebfe7..62b9141b 100644 --- a/charts/docker-mailserver/templates/service.yaml +++ b/charts/docker-mailserver/templates/service.yaml @@ -32,71 +32,74 @@ spec: app.kubernetes.io/name: {{ template "dockermailserver.fullname" . }} release: "{{ .Release.Name }}" ports: - - protocol: "TCP" - name: "tcp-smtp" + - name: smtp port: 25 - {{- if eq .Values.service.type "NodePort" }} + targetPort: smtp + {{- if eq .Values.service.type "NodePort" }} nodePort: {{ default "30025" .Values.service.nodePort.smtp }} - {{ end }} - - protocol: "TCP" - name: "tcp-pop3" - port: 110 - {{- if eq .Values.service.type "NodePort" }} - nodePort: {{ default "30110" .Values.service.nodePort.pop3 }} - {{ end }} - - protocol: "TCP" - name: "tcp-imap" - port: 143 - {{- if eq .Values.service.type "NodePort" }} - nodePort: {{ default "30143" .Values.service.nodePort.imap }} - {{ end }} - - protocol: "TCP" - name: "tcp-smtps" + {{- end }} + - name: smtps + targetPort: smtps port: 465 - {{- if eq .Values.service.type "NodePort" }} + {{- if eq .Values.service.type "NodePort" }} nodePort: {{ default "30465" .Values.service.nodePort.smtps }} - {{ end }} - - protocol: "TCP" - name: "tcp-submission" + {{- end }} + - name: submission + targetPort: submission port: 587 - {{- if eq .Values.service.type "NodePort" }} + {{- if eq .Values.service.type "NodePort" }} nodePort: {{ default "30587" .Values.service.nodePort.submission }} - {{ end }} - - protocol: "TCP" - name: "tcp-imaps" + {{ end }} + + {{- if and (.Values.pod.dockermailserver.env.ENABLE_IMAP) (not .Values.pod.dockermailserver.env.SMTP_ONLY) }} + - name: imap + targetPort: imap + port: 143 + {{- if eq .Values.service.type "NodePort" }} + nodePort: {{ default "30143" .Values.service.nodePort.imap }} + {{- end }} + - name: imaps + targetPort: imaps port: 993 - {{- if eq .Values.service.type "NodePort" }} + {{- if eq .Values.service.type "NodePort" }} nodePort: {{ default "30993" .Values.service.nodePort.imaps }} - {{ end }} - - protocol: "TCP" - name: "tcp-pop3s" + {{- end }} + {{- end }} + + {{- if and (.Values.pod.dockermailserver.env.ENABLE_POP) (not .Values.pod.dockermailserver.env.SMTP_ONLY) }} + - name: pop3 + targetPort: pop3 + port: 110 + {{- if eq .Values.service.type "NodePort" }} + nodePort: {{ default "30110" .Values.service.nodePort.pop3 }} + {{- end }} + - name: pop3s + targetPort: pop3s port: 995 - {{- if eq .Values.service.type "NodePort" }} + {{- if eq .Values.service.type "NodePort" }} nodePort: {{ default "30995" .Values.service.nodePort.pop3s }} - {{ end }} - - protocol: "TCP" - name: "tcp-managesieve" + {{- end }} + {{- end }} + + {{- if and (.Values.pod.dockermailserver.env.ENABLE_MANAGESIEVE) (not .Values.pod.dockermailserver.env.SMTP_ONLY) }} + - name: managesieve + targetPort: managesieve port: 4190 - - protocol: "TCP" - name: "tcp-dsync" - port: 4711 - - {{- if .Values.metrics.enabled }} - - name: tcp-metrics + {{- end }} + + {{- if .Values.metrics.enabled }} + - name: metrics port: 9154 - protocol: TCP targetPort: 9154 - {{ end }} - - + {{- end }} type: {{ default "ClusterIP" .Values.service.type }} {{ if eq .Values.service.type "LoadBalancer" -}} - {{ if .Values.service.loadBalancer.publicIp -}} + {{ if .Values.service.loadBalancer.publicIp -}} loadBalancerIP: {{ .Values.service.loadBalancer.publicIp }} - {{ if .Values.service.loadBalancer.allowedIps -}} + {{ if .Values.service.loadBalancer.allowedIps -}} loadBalancerSourceRanges: {{ .Values.service.loadBalancer.allowedIps | toYaml | indent 4 }} - {{ end -}} - {{ end -}} - {{ end }} + {{- end }} + {{- end }} + {{- end }} From 528d3075dc35ea17eaa16ee85662a164bd064ff0 Mon Sep 17 00:00:00 2001 From: Charlie Savage Date: Sat, 13 Jan 2024 00:03:28 -0800 Subject: [PATCH 06/66] Add ingress for rspamd if its enabled --- .../templates/ingress-rspamd.yaml | 35 +++++++++++++++++++ charts/docker-mailserver/values.yaml | 11 ++++++ 2 files changed, 46 insertions(+) create mode 100644 charts/docker-mailserver/templates/ingress-rspamd.yaml diff --git a/charts/docker-mailserver/templates/ingress-rspamd.yaml b/charts/docker-mailserver/templates/ingress-rspamd.yaml new file mode 100644 index 00000000..eab555fe --- /dev/null +++ b/charts/docker-mailserver/templates/ingress-rspamd.yaml @@ -0,0 +1,35 @@ +{{- if and (.Values.pod.dockermailserver.env.ENABLE_RSPAMD) (.Values.rspamd.ingress.enabled) -}} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + labels: + app.kubernetes.io/name: {{ template "dockermailserver.fullname" . }} + chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" + heritage: "{{ .Release.Service }}" + release: "{{ .Release.Name }}" + name: {{ template "dockermailserver.fullname" . }}-rspamd +{{- with .Values.rspamd.ingress.annotations }} + annotations: +{{ toYaml . | indent 4 }} +{{- end }} +spec: + ingressClassName: {{ .Values.rspamd.ingress.ingressClassName }} + rules: + - host: {{ .Values.rspamd.ingress.host }} + http: + paths: + - pathType: Prefix + path: {{ .Values.rspamd.ingress.path }} + backend: + service: + name: {{ template "dockermailserver.fullname" . }} + port: + name: rspamd + +{{ if .Values.rspamd.ingress.tls.enabled }} + tls: + - secretName: {{ .Values.ingress.tls.secret }} + hosts: + - {{ .Values.rspamd.ingress.host }} +{{- end }} +{{- end }} \ No newline at end of file diff --git a/charts/docker-mailserver/values.yaml b/charts/docker-mailserver/values.yaml index f27e1619..86defc0f 100644 --- a/charts/docker-mailserver/values.yaml +++ b/charts/docker-mailserver/values.yaml @@ -386,6 +386,17 @@ monitoring: ## Port on which HTTP server is served port: "9102" +rspamd: + ingress: + enabled: true + ingressClassName: nginx + annotations: {} + host: rspamd.example.com + path: / + tls: + enabled: false + secret: + ## These values are for the haproxy sub-chart haproxy: # haproxy.enabled will deploy an haproxy sub-chart, configured for the TCP ports used by docker-mailserver From 7552fa352220a37337e523f4d70fcbeceee8ae6d Mon Sep 17 00:00:00 2001 From: Charlie Savage Date: Sun, 14 Jan 2024 12:48:09 -0800 Subject: [PATCH 07/66] Large changeset that makes it easier for users to add their own custom configuration files and secrets. --- .../templates/configmap-user.yaml | 20 ++++++ .../templates/configmap.yaml | 25 ++++--- .../templates/deployment.yaml | 67 ++++++++++++++++--- .../templates/secret-user.yaml | 20 ++++++ .../docker-mailserver/templates/secret.yaml | 6 +- charts/docker-mailserver/values.yaml | 54 ++++++++++++--- 6 files changed, 153 insertions(+), 39 deletions(-) create mode 100644 charts/docker-mailserver/templates/configmap-user.yaml create mode 100644 charts/docker-mailserver/templates/secret-user.yaml diff --git a/charts/docker-mailserver/templates/configmap-user.yaml b/charts/docker-mailserver/templates/configmap-user.yaml new file mode 100644 index 00000000..0ae36386 --- /dev/null +++ b/charts/docker-mailserver/templates/configmap-user.yaml @@ -0,0 +1,20 @@ +{{- range $config := .Values.configs }} +{{- if $config.create }} +apiVersion: "v1" +kind: "ConfigMap" +metadata: + labels: + app.kubernetes.io/name: {{ template "dockermailserver.fullname" $ }} + chart: "{{ $.Chart.Name }}-{{ $.Chart.Version }}" + heritage: "{{ $.Release.Service }}" + release: "{{ $.Release.Name }}" + name: {{ $config.name }} +data: + {{- range $item := $config.data }} + {{ $item.subPath}}: | +{{ $item.content | indent 6 }} + {{- end }} +{{- end }} +--- +{{- end }} + diff --git a/charts/docker-mailserver/templates/configmap.yaml b/charts/docker-mailserver/templates/configmap.yaml index d1ab9159..25d76277 100644 --- a/charts/docker-mailserver/templates/configmap.yaml +++ b/charts/docker-mailserver/templates/configmap.yaml @@ -1,6 +1,3 @@ -{{/* This whole template is only necessary if we've enabled creation of the configmap (default true) */}} -{{- if not .Values.configMap.useExisting -}} ---- apiVersion: "v1" kind: "ConfigMap" metadata: @@ -10,9 +7,9 @@ metadata: heritage: "{{ .Release.Service }}" release: "{{ .Release.Name }}" name: {{ template "dockermailserver.fullname" . }}-configs -data: - {{/* Use sample data if user is running in demo mode */}} - {{- if .Values.demoMode.enabled -}} +data: +{{/* Use sample data if user is running in demo mode */}} +{{- if .Values.demoMode.enabled -}} ### We are in demo mode, so add in some sample data for quick testing postfix-accounts.cf: | # A sample user - the password is "password" @@ -27,14 +24,16 @@ data: 127.0.0.1 localhost ### End demo mode data - {{/* Use real data from "config" subdirectory if user is _not_ running in demo mode */}} - {{ else -}} + +{{/* Copy files from config subdirectory into a config map. The key for each file is its path + stripped of the leading "/config" and then base 64 encoded. */}} +{{ else -}} {{- range $path, $content := .Files.Glob "config/**" }} - {{/* Skip empty files and public / private keys */}} - {{- if and (gt (len $content) 0) (not (contains "private" $path)) (not (contains "public" $path)) }} - {{ $path | replace "/" "." }}: | + {{/* Skip empty files and public / private keys */}} + {{- if and (gt (len $content) 0) (not (contains "private" $path)) (not (contains "public" $path)) }} + # {{ $path }} + {{ regexReplaceAll "[^-_.\\w]" $path "." }}: | {{ tpl ($.Files.Get $path) $ | indent 6 }} - {{- end }} + {{- end }} {{- end }} {{- end }} -{{- end -}} diff --git a/charts/docker-mailserver/templates/deployment.yaml b/charts/docker-mailserver/templates/deployment.yaml index 6552d4a5..cde10d6b 100644 --- a/charts/docker-mailserver/templates/deployment.yaml +++ b/charts/docker-mailserver/templates/deployment.yaml @@ -40,13 +40,33 @@ spec: claimName: {{ template "dockermailserver.fullname" . }} - name: "config" emptyDir: {} - - name: "configmap" + + # Default ConfigMap + - name: {{ template "dockermailserver.fullname" . }}-configs configMap: name: {{ template "dockermailserver.fullname" . }}-configs + + # User ConfigMaps + {{- range $config := .Values.configs }} + - name: {{ regexReplaceAll "[.]" $config.name "-" }}-config + configMap: + name: {{ $config.name }} + {{- end }} + + # Default Secret - name: "secrets" secret: secretName: {{ template "dockermailserver.fullname" . }}-secrets -{{- if and .Values.pod.dockermailserver.env.SSL_TYPE .Values.ssl.useExisting }} + + # User Secrets + {{- range $secret := .Values.secrets }} + - name: {{ regexReplaceAll "[.]" $secret.name "-" }}-secret + secret: + secretName: {{ $secret.name }} + {{- end }} + + +{{- if .Values.pod.dockermailserver.env.SSL_TYPE }} - name: "ssl-cert" secret: secretName: {{ .Values.ssl.existingName }} @@ -104,24 +124,49 @@ spec: mountPath: /var/mail-state subPath: mail-state - {{- range $path, $content := .Files.Glob "config/**" }} - {{- if and (gt (len $content) 0) (not (contains "private" $path)) (not (contains "public" $path)) }} - - name: configmap - subPath: {{ $path | replace "/" "." }} + ### Default ConfigMap ### + {{- /* Mount configuration files stored in the built-in ConfigMap into the container */}} + {{- range $path, $content := .Files.Glob "config/**" }} + {{- if and (gt (len $content) 0) (not (contains "private" $path)) (not (contains "public" $path)) }} + # {{ $path }} + - name: {{ template "dockermailserver.fullname" $ }}-configs + subPath: {{ regexReplaceAll "[^-_.\\w]" $path "." }} mountPath: /tmp/docker-mailserver/{{ $path | trimPrefix "config/" }} readOnly: true - {{- end }} - {{- end }} - + {{- end }} + {{- end }} + + ### User ConfigMaps ### + {{- range $config := .Values.configs }} + {{- range $item := $config.data }} + # {{ $item.subPath }} + - name: {{ regexReplaceAll "[.]" $config.name "-" }}-config + subPath: {{ $item.subPath }} + mountPath: /tmp/docker-mailserver/{{ $item.mountPath }} + {{- end }} + {{- end }} + + ### Default Secret ### {{- range $path, $content := .Files.Glob "config/**" }} {{- if and (gt (len $content) 0) (or (contains "private" $path) (contains "public" $path)) }} + ### System defined secrets ### - name: secrets - subPath: {{ $path | replace "/" "." }} + subPath: {{ regexReplaceAll "[^-_.\\w]" $path "." }} mountPath: /tmp/docker-mailserver/{{ $path | trimPrefix "config/" }} readOnly: true {{- end }} {{- end }} + ### User Secrets ### + {{- range $secret := .Values.secrets }} + {{- range $item := $secret.data }} + # {{ $item.subPath }} + - name: {{ regexReplaceAll "[.]" $secret.name "-" }}-secret + subPath: {{ $item.subPath }} + mountPath: /tmp/docker-mailserver/{{ $item.mountPath }} + {{- end }} + {{- end }} + {{- if .Values.demoMode.enabled }} - name: opendkim-keys mountPath: "/tmp/docker-mailserver/opendkim/keys/example.com/mail.private" @@ -171,7 +216,7 @@ spec: containerPort: 995 {{- end }} - {{- if and (.Values.pod.dockermailserver.env.ENABLE_RSPAMD) (not .Values.pod.dockermailserver.env.SMTP_ONLY) }} + {{- if .Values.pod.dockermailserver.env.ENABLE_RSPAMD }} - name: "rspamd" containerPort: 11334 {{- end }} diff --git a/charts/docker-mailserver/templates/secret-user.yaml b/charts/docker-mailserver/templates/secret-user.yaml new file mode 100644 index 00000000..9ada2077 --- /dev/null +++ b/charts/docker-mailserver/templates/secret-user.yaml @@ -0,0 +1,20 @@ +{{- range $secret := .Values.secrets }} +{{- if $secret.create }} +apiVersion: "v1" +kind: "Secret" +metadata: + labels: + app.kubernetes.io/name: {{ template "dockermailserver.fullname" $ }} + chart: "{{ $.Chart.Name }}-{{ $.Chart.Version }}" + heritage: "{{ $.Release.Service }}" + release: "{{ $.Release.Name }}" + name: {{ $secret.name }} +data: + {{- range $item := $secret.data }} + {{ $item.subPath}}: | +{{ $item.content | indent 6 }} + {{- end }} +{{- end }} +--- +{{- end }} + diff --git a/charts/docker-mailserver/templates/secret.yaml b/charts/docker-mailserver/templates/secret.yaml index ee79c630..66743950 100644 --- a/charts/docker-mailserver/templates/secret.yaml +++ b/charts/docker-mailserver/templates/secret.yaml @@ -1,6 +1,3 @@ -{{/* This whole template is only necessary if we've enabled creation of the configmap (default true) */}} -{{- if not .Values.secret.useExisting -}} ---- apiVersion: "v1" kind: "Secret" metadata: @@ -27,5 +24,4 @@ data: {{ $path | replace "/" "." }}: {{ tpl ($.Files.Get $path) $ | b64enc }} {{- end }} {{- end }} - {{ end }} -{{- end -}} \ No newline at end of file + {{ end }} \ No newline at end of file diff --git a/charts/docker-mailserver/values.yaml b/charts/docker-mailserver/values.yaml index 86defc0f..dd24180e 100644 --- a/charts/docker-mailserver/values.yaml +++ b/charts/docker-mailserver/values.yaml @@ -15,16 +15,50 @@ priorityClassName: serviceAccount: create: "true" -## By default, the chart will create a configmap based on the contents of the config/ subdirectory -## If you want to create the configmap yourself, set useExisting to "true", and specify the name -## of the existing configmap. -## This is an advanced feature, allowing further customization / templating of config elements +## ConfigMaps (and Secrets) are used to copy docker-mailserver configuration files +## into running containers. This chart automatically sets up any config files that +## are stored in its chart/config directory. +## +## However, Helm does not provide a way too save external files to a ConfigMap or Secret. +## This is problem for docker-mailserver because you need to setup postfix acounts, +## dovecot accounts, etc. ## -configMap: - useExisting: false - -secret: - useExisting: false +## The configs and secrets keys solve this problem. They allow you to add additional config +## files by either referencing existing ConfigMaps (that you create before installing the Chart) +## or by creating new ones (set the create key to true). +## +## configs: +## - name: dovecot.example.com # This is the name of the ConfigMap +## create: true # If true, create a new ConfigMap +## data: +## - subPath: dovecot-masters.cf +## mountPath: dovecot-masters.cf # This is relative path to /tmp/docker-mailserver/ +## content: | # If create is true, then you must specify content +## someuser|{SHA512-CRYPT}... +## +## - name: postfix.example.com +## create: true +## data: +## - subPath: postfix-accounts.cf +## mountPath: postfix-accounts.cf +## content: | +## someuser|{SHA512-CRYPT}... +configs: [] + +## The secrets key is similar section allows you to add additional secrets +## +## secrets: +## - name: rspamd.example.com # This is the name of the Secret +## create: true # If true, create a new Secret +## data: +## - subPath: rspamd.dkim.rsa-2048-mail-example.com.private.txt +## mountPath: rspamd/dkim/rsa-2048-mail-example.com.private.txt +## content: abace # If create is true, then you must specify content. Must be base 64 encoded! +## +## - subPath: rspamd.dkim.rsa-2048-mail-example.com.public +## mountPath: rspamd/dkim/rsa-2048-mail-example.com.public +## content: abace # If create is true, then you must specify content. Must be base 64 encoded! +secrets: [] ## Specify an existing secret that contains TLS certificates. The easiest way to create a ## secret is by using cert-manager. @@ -388,7 +422,7 @@ monitoring: rspamd: ingress: - enabled: true + enabled: false ingressClassName: nginx annotations: {} host: rspamd.example.com From b19494458da22f6b492e75c9f6b58841ee878ced Mon Sep 17 00:00:00 2001 From: Charlie Savage Date: Sun, 14 Jan 2024 12:48:32 -0800 Subject: [PATCH 08/66] Add rspamd port so it can be exposed via an ingress --- charts/docker-mailserver/templates/service.yaml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/charts/docker-mailserver/templates/service.yaml b/charts/docker-mailserver/templates/service.yaml index 62b9141b..78456324 100644 --- a/charts/docker-mailserver/templates/service.yaml +++ b/charts/docker-mailserver/templates/service.yaml @@ -81,6 +81,12 @@ spec: {{- end }} {{- end }} + {{- if .Values.pod.dockermailserver.env.ENABLE_RSPAMD }} + - name: rspamd + targetPort: rspamd + port: 11334 + {{- end }} + {{- if and (.Values.pod.dockermailserver.env.ENABLE_MANAGESIEVE) (not .Values.pod.dockermailserver.env.SMTP_ONLY) }} - name: managesieve targetPort: managesieve From 5624f31f5ae4af0b95f716ecad1de0d51449f96b Mon Sep 17 00:00:00 2001 From: Charlie Savage Date: Sun, 14 Jan 2024 12:48:51 -0800 Subject: [PATCH 09/66] Add in support for proxy --- charts/docker-mailserver/config/postfix-main.cf | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/charts/docker-mailserver/config/postfix-main.cf b/charts/docker-mailserver/config/postfix-main.cf index e69de29b..96c4b14a 100644 --- a/charts/docker-mailserver/config/postfix-main.cf +++ b/charts/docker-mailserver/config/postfix-main.cf @@ -0,0 +1,7 @@ + {{- /* Enable proxy protocol for postscreen / dovecot */}} + {{- if .Values.haproxy.enabled -}} # Necessary to permit proxy protocol from haproxy to postscreen + postscreen_upstream_proxy_protocol = haproxy + {{- end }} + {{- if not .Values.spfTestsDisabled -}} + smtpd_recipient_restrictions = permit_sasl_authenticated, permit_mynetworks, reject_unauth_destination, reject_unauth_pipelining, reject_invalid_helo_hostname, reject_non_fqdn_helo_hostname, reject_unknown_recipient_domain, reject_rbl_client zen.spamhaus.org, reject_rbl_client bl.spamcop.net{{ range .Values.rblRejectDomains }}, reject_rbl_client {{ . }}{{ end }} + {{ end -}} From 4eb34e456357769307ceac2f3c2693102474aa6a Mon Sep 17 00:00:00 2001 From: Charlie Savage Date: Sun, 14 Jan 2024 13:25:29 -0800 Subject: [PATCH 10/66] Remove empty or all commented out config files - users can't override them in the chart (without checking it out). --- .../config/fail2ban-fail2ban.cf | 19 ------------------- .../docker-mailserver/config/fail2ban-jail.cf | 11 ----------- charts/docker-mailserver/config/fetchmail.cf | 13 ------------- .../config/postfix-accounts.cf | 0 .../config/postfix-accounts.cf.bak | 0 .../config/postfix-aliases.cf | 0 .../config/postfix-master.cf | 0 .../config/postfix-virtual.cf | 0 .../config/spamassassin-rules.cf | 0 9 files changed, 43 deletions(-) delete mode 100644 charts/docker-mailserver/config/fail2ban-fail2ban.cf delete mode 100644 charts/docker-mailserver/config/fail2ban-jail.cf delete mode 100644 charts/docker-mailserver/config/fetchmail.cf delete mode 100644 charts/docker-mailserver/config/postfix-accounts.cf delete mode 100644 charts/docker-mailserver/config/postfix-accounts.cf.bak delete mode 100644 charts/docker-mailserver/config/postfix-aliases.cf delete mode 100644 charts/docker-mailserver/config/postfix-master.cf delete mode 100644 charts/docker-mailserver/config/postfix-virtual.cf delete mode 100644 charts/docker-mailserver/config/spamassassin-rules.cf diff --git a/charts/docker-mailserver/config/fail2ban-fail2ban.cf b/charts/docker-mailserver/config/fail2ban-fail2ban.cf deleted file mode 100644 index 04b5b4e1..00000000 --- a/charts/docker-mailserver/config/fail2ban-fail2ban.cf +++ /dev/null @@ -1,19 +0,0 @@ -[Definition] - -# Option: loglevel -# Notes.: Set the log level output. -# CRITICAL -# ERROR -# WARNING -# NOTICE -# INFO -# DEBUG -# Values: [ LEVEL ] Default: ERROR -# - -# loglevel = INFO - -# Options: dbpurgeage -# Notes.: Sets age at which bans should be purged from the database -# Values: [ SECONDS ] Default: 86400 (24hours), 604800 (1week) -# dbpurgeage = 604800 diff --git a/charts/docker-mailserver/config/fail2ban-jail.cf b/charts/docker-mailserver/config/fail2ban-jail.cf deleted file mode 100644 index 7b426c4a..00000000 --- a/charts/docker-mailserver/config/fail2ban-jail.cf +++ /dev/null @@ -1,11 +0,0 @@ -[DEFAULT] - -# "bantime" is the number of seconds that a host is banned. -#bantime = 10800 - -# A host is banned if it has generated "maxretry" during the last "findtime" -# seconds. -#findtime = 600 - -# "maxretry" is the number of failures before a host get banned. -#maxretry = 3 diff --git a/charts/docker-mailserver/config/fetchmail.cf b/charts/docker-mailserver/config/fetchmail.cf deleted file mode 100644 index 3a7e0c34..00000000 --- a/charts/docker-mailserver/config/fetchmail.cf +++ /dev/null @@ -1,13 +0,0 @@ -## Example configuration: IMAP -#poll imap.example.com with proto IMAP -# user 'username' there with -# password 'secret' -# is 'user1@domain.tld' -# here ssl - -## Example configuration: POP3 -#poll pop3.example.com with proto POP3 -# user 'username' there with -# password 'secret' -# is 'user2@domain.tld' -# here options keep ssl diff --git a/charts/docker-mailserver/config/postfix-accounts.cf b/charts/docker-mailserver/config/postfix-accounts.cf deleted file mode 100644 index e69de29b..00000000 diff --git a/charts/docker-mailserver/config/postfix-accounts.cf.bak b/charts/docker-mailserver/config/postfix-accounts.cf.bak deleted file mode 100644 index e69de29b..00000000 diff --git a/charts/docker-mailserver/config/postfix-aliases.cf b/charts/docker-mailserver/config/postfix-aliases.cf deleted file mode 100644 index e69de29b..00000000 diff --git a/charts/docker-mailserver/config/postfix-master.cf b/charts/docker-mailserver/config/postfix-master.cf deleted file mode 100644 index e69de29b..00000000 diff --git a/charts/docker-mailserver/config/postfix-virtual.cf b/charts/docker-mailserver/config/postfix-virtual.cf deleted file mode 100644 index e69de29b..00000000 diff --git a/charts/docker-mailserver/config/spamassassin-rules.cf b/charts/docker-mailserver/config/spamassassin-rules.cf deleted file mode 100644 index e69de29b..00000000 From 8983accbf69f9be733c100e0d23e8da8eacca21e Mon Sep 17 00:00:00 2001 From: Charlie Savage Date: Sun, 14 Jan 2024 13:26:08 -0800 Subject: [PATCH 11/66] Make sure default values.yaml file correctly spins up a mailserver with demo user account --- .../templates/configmap.yaml | 25 ++------- .../templates/deployment.yaml | 7 --- .../docker-mailserver/templates/secret.yaml | 19 ++----- charts/docker-mailserver/values.yaml | 53 ++++++++++--------- 4 files changed, 34 insertions(+), 70 deletions(-) diff --git a/charts/docker-mailserver/templates/configmap.yaml b/charts/docker-mailserver/templates/configmap.yaml index 25d76277..36fd00f1 100644 --- a/charts/docker-mailserver/templates/configmap.yaml +++ b/charts/docker-mailserver/templates/configmap.yaml @@ -8,32 +8,13 @@ metadata: release: "{{ .Release.Name }}" name: {{ template "dockermailserver.fullname" . }}-configs data: -{{/* Use sample data if user is running in demo mode */}} -{{- if .Values.demoMode.enabled -}} - ### We are in demo mode, so add in some sample data for quick testing - postfix-accounts.cf: | - # A sample user - the password is "password" - user@example.com|{SHA512-CRYPT}$6$l4023rZnQEy/l0Rg$JeNjAAICB43VAX7GTJ9jeE7DR0LeyR5nU.ftq3c42T5E1IZSuRBqwM8erRh6t0CyIT6aYpBIAopzcQHNUvMV00 - postfix-virtual.cf: | - # Intentionally left empty - SigningTable: | - *@example.com mail._domainkey.example.com - KeyTable: | - mail._domainkey.example.com example.com:mail:/etc/opendkim/keys/example.com/mail.private - TrustedHosts: | - 127.0.0.1 - localhost - ### End demo mode data - {{/* Copy files from config subdirectory into a config map. The key for each file is its path stripped of the leading "/config" and then base 64 encoded. */}} -{{ else -}} - {{- range $path, $content := .Files.Glob "config/**" }} - {{/* Skip empty files and public / private keys */}} - {{- if and (gt (len $content) 0) (not (contains "private" $path)) (not (contains "public" $path)) }} +{{- range $path, $content := .Files.Glob "config/**" }} + {{/* Skip empty files and public / private keys */}} + {{- if and (gt (len $content) 0) (not (contains "private" $path)) (not (contains "public" $path)) }} # {{ $path }} {{ regexReplaceAll "[^-_.\\w]" $path "." }}: | {{ tpl ($.Files.Get $path) $ | indent 6 }} - {{- end }} {{- end }} {{- end }} diff --git a/charts/docker-mailserver/templates/deployment.yaml b/charts/docker-mailserver/templates/deployment.yaml index cde10d6b..d67dcb89 100644 --- a/charts/docker-mailserver/templates/deployment.yaml +++ b/charts/docker-mailserver/templates/deployment.yaml @@ -132,7 +132,6 @@ spec: - name: {{ template "dockermailserver.fullname" $ }}-configs subPath: {{ regexReplaceAll "[^-_.\\w]" $path "." }} mountPath: /tmp/docker-mailserver/{{ $path | trimPrefix "config/" }} - readOnly: true {{- end }} {{- end }} @@ -167,12 +166,6 @@ spec: {{- end }} {{- end }} - {{- if .Values.demoMode.enabled }} - - name: opendkim-keys - mountPath: "/tmp/docker-mailserver/opendkim/keys/example.com/mail.private" - subPath: "example.com-mail.private" - readOnly: true - {{- end }} {{- if .Values.additionalVolumeMounts }} {{ toYaml .Values.additionalVolumeMounts | indent 12 }} {{- end }} diff --git a/charts/docker-mailserver/templates/secret.yaml b/charts/docker-mailserver/templates/secret.yaml index 66743950..be5f19e5 100644 --- a/charts/docker-mailserver/templates/secret.yaml +++ b/charts/docker-mailserver/templates/secret.yaml @@ -8,20 +8,9 @@ metadata: release: "{{ .Release.Name }}" name: {{ template "dockermailserver.fullname" . }}-secrets data: - {{- /* Import any files ending in .secret, as secrets */}} - {{- $globdash := .Files.Glob "*.secret" }} - {{- if $globdash -}} - {{- (.Files.Glob "*.secret").AsSecrets | nindent 2 -}} - {{- end -}} - {{/* If we're in demo mode, just import a pre-created key for example.com */}} - {{- if .Values.demoMode.enabled -}} - example.com-mail.private: {{ .Files.Get "demo-mode-dkim-key-for-example.com.key" | b64enc }} - {{/* If we're _not_ in demo mode, assume the user has created dkim keys for his domains, and import them */}} - {{- else -}} - {{- range $path, $content := .Files.Glob "config/**" }} - {{- /* Skip empty files and contains public or private keys */}} - {{- if and (gt (len $content) 0) (or (contains "private" $path) (contains "public" $path)) }} +{{- range $path, $content := .Files.Glob "config/**" }} + {{- /* Process files that contains public or private or secret in their name */}} + {{- if or (contains "private" $path) (contains "public" $path) (contains ".secret" $path) }} {{ $path | replace "/" "." }}: {{ tpl ($.Files.Get $path) $ | b64enc }} {{- end }} - {{- end }} - {{ end }} \ No newline at end of file +{{ end }} \ No newline at end of file diff --git a/charts/docker-mailserver/values.yaml b/charts/docker-mailserver/values.yaml index dd24180e..d493ca83 100644 --- a/charts/docker-mailserver/values.yaml +++ b/charts/docker-mailserver/values.yaml @@ -15,6 +15,9 @@ priorityClassName: serviceAccount: create: "true" +demoMode: + enabled: true # You must OVERRIDE this! + ## ConfigMaps (and Secrets) are used to copy docker-mailserver configuration files ## into running containers. This chart automatically sets up any config files that ## are stored in its chart/config directory. @@ -27,23 +30,25 @@ serviceAccount: ## files by either referencing existing ConfigMaps (that you create before installing the Chart) ## or by creating new ones (set the create key to true). ## -## configs: -## - name: dovecot.example.com # This is the name of the ConfigMap -## create: true # If true, create a new ConfigMap -## data: -## - subPath: dovecot-masters.cf -## mountPath: dovecot-masters.cf # This is relative path to /tmp/docker-mailserver/ -## content: | # If create is true, then you must specify content -## someuser|{SHA512-CRYPT}... -## -## - name: postfix.example.com -## create: true -## data: -## - subPath: postfix-accounts.cf -## mountPath: postfix-accounts.cf -## content: | -## someuser|{SHA512-CRYPT}... -configs: [] +## This is a DEMO mode configuration - You must OVERRIDE this! +configs: +- name: dovecot.example.com # Name of the ConfigMap + create: true # Whether to create the ConfigMap (if true, content must be specified) + data: + - subPath: dovecot-masters.cf + mountPath: dovecot-masters.cf # Relative path to /tmp/docker-mailserver/ + content: | + # A sample user - the password is "password" + user@example.com|{SHA512-CRYPT}$6$l4023rZnQEy/l0Rg$JeNjAAICB43VAX7GTJ9jeE7DR0LeyR5nU.ftq3c42T5E1IZSuRBqwM8erRh6t0CyIT6aYpBIAopzcQHNUvMV00 + +- name: postfix.example.com + create: true + data: + - subPath: postfix-accounts.cf + mountPath: postfix-accounts.cf + content: | + # A sample user - the password is "password" + user@example.com|{SHA512-CRYPT}$6$l4023rZnQEy/l0Rg$JeNjAAICB43VAX7GTJ9jeE7DR0LeyR5nU.ftq3c42T5E1IZSuRBqwM8erRh6t0CyIT6aYpBIAopzcQHNUvMV00 ## The secrets key is similar section allows you to add additional secrets ## @@ -75,10 +80,6 @@ ssl: # - name: additional # mountPath: /additional -demoMode: - # demoMode.enabled ignores the contents of helm-chart/docker-mailserver/config and creates minimal static configuration to be used instead. A single account (user "user@example.com", password "password") will be configured for the purpose of validating core functions and connectivity, before applying user config - enabled: true - # If you choose _not_ to use haproxy, and you're not exposing your services with a load-balanced service # with an external traffic policy in "Local" mode, you risk having the source IP of incoming mail overwritten # with a local Kubernetes cluster IP, as part of the ingress routing of the connection. This, in turn, @@ -137,7 +138,7 @@ pod: # ----------------------------------------------- # --- Required Section --------------------------- # ----------------------------------------------- - OVERRIDE_HOSTNAME: + OVERRIDE_HOSTNAME: mail.example.com # You must OVERRIDE this! # ----------------------------------------------- # --- General Section --------------------------- @@ -157,14 +158,14 @@ pod: TLS_LEVEL: SPOOF_PROTECTION: ENABLE_SRS: 0 - ENABLE_OPENDKIM: 1 - ENABLE_OPENDMARC: 1 - ENABLE_POLICYD_SPF: 1 + ENABLE_OPENDKIM: 0 + ENABLE_OPENDMARC: 0 + ENABLE_POLICYD_SPF: 0 ENABLE_POP3: ENABLE_IMAP: 1 ENABLE_CLAMAV: 0 ENABLE_RSPAMD: 1 - ENABLE_RSPAMD_REDIS: + ENABLE_RSPAMD_REDIS: 1 RSPAMD_LEARN: 0 RSPAMD_CHECK_AUTHENTICATED: 0 RSPAMD_GREYLISTING: 0 From e4cb13324459f3cd866f01480ba243a02dd7a22b Mon Sep 17 00:00:00 2001 From: Charlie Savage Date: Sun, 14 Jan 2024 14:18:19 -0800 Subject: [PATCH 12/66] As opposed to making the user have to download setup.sh and use docker, instead provide directions on how to open a command prompt into the running container and run setup configuration utility. --- charts/docker-mailserver/templates/NOTES.txt | 120 +++++++------------ charts/docker-mailserver/values.yaml | 42 +++---- 2 files changed, 65 insertions(+), 97 deletions(-) diff --git a/charts/docker-mailserver/templates/NOTES.txt b/charts/docker-mailserver/templates/NOTES.txt index 0ac7af02..b550d56f 100644 --- a/charts/docker-mailserver/templates/NOTES.txt +++ b/charts/docker-mailserver/templates/NOTES.txt @@ -1,77 +1,49 @@ - - ^ - (CCCC%((((((((((%(/ - (GG@#O(%77777(((t((((/ // - G@#%%%77777ttttttttt(((////^ - %OOO77777ttttttttttttt(((((/// - /CG##@#O776tt((t6#66##6tttttt(((((//^ - CGC%%7777tt666ttt6###666tttttt(((////^ - (CGOO#####66#67(7#@##66667(tttttt((((//( - CC//((O######O/ (O###O6777ttttt(((((//^ - ((/(GGC/(C((GC/^/((^/C%(%OO77tttt((((///( - C((GGC//(GGG@C^/CC(^^CC((CO%(7((((((///( - C^ %%((CGGCCGG/^(C/^^(C((COO7tttt((((/( - /(/(OOO%(((%G%/ ^ ^((((C######6t((/(% - #O(77((7(((CCC(G@GG##O7777(76#####/ - O7(7(((777((%CGGGC%%%777((((76O#@@G - ^#676ttttt6#6666666777tttt((/(77OO(/% - (//7766####6t666tttttt((((((tt7666(7 - (/(66##O6677tt666tt(ttttttttttt6677% - ###6tttttttttt6#6ttttttttttt(767OO - (#7(t6tttttttttttt66ttttttttt(((6O% - C@O((tttttttttttttt6#6ttttttttttt6O( - (O#67(7ttt(((t6######66##6tt((((777667 - #OO###66tt66##66677tt666667((((((t#6(6O - ^/(((%O###67777((t((ttt(((((((((#% - /####6(tttttttttt6t(t676@@( - ^O#7tttt6ttt666((66O##OO/ - /%#67tt#6ttt66(7#####77% - #@#(##ttt####O%OO( (( - /@@#@@####O%%%OG/ %^ - G@@###OOO%(%%%G% /#% - /#@C((((/((((G#C (%GO - /GGC((((((CG#@#/ // /O( - ^CG@#GGGGGGCCGC ^/^ (G - CCGC%((((((CGC /( /#( - CCGC(((((((GG/ (^ %G - - - GOOD NEWS, EVERYONE! - -You've successfully launched docker-mailserver helm chart! - -{{- if .Values.demoMode.enabled -}} - -But wait, dear reader. You're still running in DEMO MODE. - -What does this mean? We set you up a demo user (user is "user@example.com", password is "password"), so that -you can confirm your services are setup correctly. Once you're ready to configure docker-mailserver _for realz_ -here's what you need to do: - -1. Update values.yaml, and set pod.demoMode.enabled to "false". Also set your domains under "domains", removing "example.com" -2. Download https://raw.githubusercontent.com/tomav/docker-mailserver/master/setup.sh to your current working directory -3. From your current working directory, use ./setup.sh to create your users and setup your DKIM key(s), as follows: - - ./setup.sh email add [password] - ./setup.sh config dkim - -NOTE: The above step is mandatory if you disable demo mode. Without valid DKIM keys, the chart WON'T DEPLOY - -4. Upgrade the helm chart to apply your changes, by running: - - helm upgrade {{ .Release.Namespace }} docker-mailserver - -{{- else }} - -You're not in demo mode! Assuming all has gone well, execute the commands below to retrieve the TXT records -to add to your DNS zones, to finish setting up DKIM: -{{/* Mount a dkim key for every domain configured */}} ---- -{{ range .Values.domains }} -{{- $path := printf "cat config/opendkim/keys/%s/mail.txt" . }} - {{ $path }} -{{ end }} ---- +GOOD NEWS! +==================== + +You've successfully launched the docker-mailserver helm chart! + +{{- if not .Values.configs -}} + +Initial Setup +------------ + +But wait, dear reader! You haven't configured your mail server yet! You'll need to quickly open a command +prompt into the running container (you have two minutes) and setup a first email account. + + kubectl exec -it --namespace mail deploy/docker-mailserver -- bash + + setup email add user@example.com password + +This will create a file: + + cat /tmp/docker-mailserver/postfix-accounts.cf + +Next, run the setup command to see your other options: + + setup + +As you run various setup commands, additional files will be generated in `/tmp/docker-mailserver`. + +Once you are done, you will want to copy them to your local machine (remember when the pod is terminated all of +these files will be deleted!) + + exit # To exit the bash prompt in the container + + mdkir /tmp/config + + cd /tmp/config + + podname=$(kubectl get pod --namespace mail -l app.kubernetes.io/name=docker-mailserver -o jsonpath="{.items[0].metadata.name}") + + kubectl cp mail/$podname:tmp/docker-mailserver /tmp/test + +Once you have copied these files locally, you then need to save them into configmaps or +secrets. Please see the `configs` and `secrets` key in the values.yaml file for more information. Once you have created your custom values.yaml file you +can then redeploy the chart + + helm upgrade --namespace mail {{ .Release.Namespace }} docker-mailserver --values + {{ end }} {{ if .Values.haproxy.enabled -}} diff --git a/charts/docker-mailserver/values.yaml b/charts/docker-mailserver/values.yaml index d493ca83..2ede792d 100644 --- a/charts/docker-mailserver/values.yaml +++ b/charts/docker-mailserver/values.yaml @@ -15,9 +15,6 @@ priorityClassName: serviceAccount: create: "true" -demoMode: - enabled: true # You must OVERRIDE this! - ## ConfigMaps (and Secrets) are used to copy docker-mailserver configuration files ## into running containers. This chart automatically sets up any config files that ## are stored in its chart/config directory. @@ -30,25 +27,24 @@ demoMode: ## files by either referencing existing ConfigMaps (that you create before installing the Chart) ## or by creating new ones (set the create key to true). ## -## This is a DEMO mode configuration - You must OVERRIDE this! -configs: -- name: dovecot.example.com # Name of the ConfigMap - create: true # Whether to create the ConfigMap (if true, content must be specified) - data: - - subPath: dovecot-masters.cf - mountPath: dovecot-masters.cf # Relative path to /tmp/docker-mailserver/ - content: | - # A sample user - the password is "password" - user@example.com|{SHA512-CRYPT}$6$l4023rZnQEy/l0Rg$JeNjAAICB43VAX7GTJ9jeE7DR0LeyR5nU.ftq3c42T5E1IZSuRBqwM8erRh6t0CyIT6aYpBIAopzcQHNUvMV00 - -- name: postfix.example.com - create: true - data: - - subPath: postfix-accounts.cf - mountPath: postfix-accounts.cf - content: | - # A sample user - the password is "password" - user@example.com|{SHA512-CRYPT}$6$l4023rZnQEy/l0Rg$JeNjAAICB43VAX7GTJ9jeE7DR0LeyR5nU.ftq3c42T5E1IZSuRBqwM8erRh6t0CyIT6aYpBIAopzcQHNUvMV00 +## configs: +## - name: postfix.example.com +## create: true +## data: +## - subPath: postfix-accounts.cf +## mountPath: postfix-accounts.cf +## content: | +## # A sample user - the password is "password" +## user@example.com|{SHA512-CRYPT}$6$l4023rZnQEy/l0Rg$JeNjAAICB43VAX7GTJ9jeE7DR0LeyR5nU.ftq3c42T5E1IZSuRBqwM8erRh6t0CyIT6aYpBIAopzcQHNUvMV00 +## +## - name: dovecot.example.com # Name of the ConfigMap +## create: true # Whether to create the ConfigMap (if true, content must be specified) +## data: +## - subPath: dovecot-masters.cf +## mountPath: dovecot-masters.cf # Relative path to /tmp/docker-mailserver/ +## content: | +## # A sample user - the password is "password" +## user@example.com|{SHA512-CRYPT}$6$l4023rZnQEy/l0Rg$JeNjAAICB43VAX7GTJ9jeE7DR0LeyR5nU.ftq3c42T5E1IZSuRBqwM8erRh6t0CyIT6aYpBIAopzcQHNUvMV00 ## The secrets key is similar section allows you to add additional secrets ## @@ -171,7 +167,7 @@ pod: RSPAMD_GREYLISTING: 0 RSPAMD_HFILTER: 1 RSPAMD_HFILTER_HOSTNAME_UNKNOWN_SCORE: 6 - ENABLE_AMAVIS: 1 + ENABLE_AMAVIS: 0 AMAVIS_LOGLEVEL: 0 ENABLE_DNSBL: 0 ENABLE_FAIL2BAN: 0 From 2b5934d8feb8677bf4a0bc1898ce657b4548a83a Mon Sep 17 00:00:00 2001 From: Charlie Savage Date: Sun, 14 Jan 2024 16:50:40 -0800 Subject: [PATCH 13/66] Lots of documentation updates --- charts/docker-mailserver/README.md | 183 +++++++++++------------------ 1 file changed, 66 insertions(+), 117 deletions(-) diff --git a/charts/docker-mailserver/README.md b/charts/docker-mailserver/README.md index 2adb85e7..ff0cffcf 100644 --- a/charts/docker-mailserver/README.md +++ b/charts/docker-mailserver/README.md @@ -2,21 +2,18 @@ This helm chart deploys [Docker Mailserver](https://github.com/docker-mailserver/docker-mailserver) into a -Kubernetes cluster, in a manner which retains compatibility with the upstream, -docker-specific version. +Kubernetes cluster. Docker Mailserver was originally intended to be run with Docker or Docker -Compose, it's been [adapted to -Kubernetes](https://github.com/docker-mailserver/docker-mailserver/wiki/Using-in-Kubernetes). +Compose, but it has been [adapted to Kubernetes](https://github.com/docker-mailserver/docker-mailserver/wiki/Using-in-Kubernetes). ## Contents - [Contents](#contents) - [Features](#features) - [Prerequisites](#prerequisites) -- [Architecture](#architecture) - [Getting Started](#getting-started) - - [Install Helm](#1-install-helm) + - [Install](install) - [Install Cert-manager](#2-install-cert-manager) - [Install Docker Mailserver](#install-docker-mailserver) - [Configuration and Operation](#configuration-and-operation) @@ -37,7 +34,7 @@ Kubernetes](https://github.com/docker-mailserver/docker-mailserver/wiki/Using-in The chart includes the following features: -- All configuration is done in values.yaml, or using the native "setup.sh" script (to create mailboxes or DKIM keys) +- Configuration is done in values.yaml and by using the setup script inside the container - Avoids the [common problem of masking of source IP](https://kubernetes.io/docs/tutorials/services/source-ip/) by supporting haproxy's PROXY protocol (enabled by default) - Employs [cert-manager](https://github.com/jetstack/cert-manager) to automatically provide/renew SSL certificates - Starts in "demo" mode, allowing the user to test core functionality before configuring for specific domains @@ -45,151 +42,112 @@ The chart includes the following features: ## Prerequisites -- Kubernetes 1.16+ (*CI validates against > 1.18.0*) -- To use HAProxy ingress, you'll need to deploying the chart to a cluster with a cloud provider capable of provisioning an -external load balancer (e.g. AWS, DO or GKE). (There is an [update planned](https://github.com/funkypenguin/docker-mailserver/issues/5) to support HA ingress on bare-metal deployments) -- You control DNS for the domain(s) you intend to route through Traefik +- A Kubernetes cluster +- Acquire a custom domain +- Configure a [DNS](https://docker-mailserver.github.io/docker-mailserver/latest/usage/#minimal-dns-setup) - __Suggested:__ PV provisioner support in the underlying infrastructure - [Cert-manager](https://github.com/jetstack/cert-manager/tree/master/deploy/charts/cert-manager) => 1.0 requires manual deployment into your cluster (details below) -- [Helm](https://helm.sh) >= 2.13.0 (*errors were encountered when testing with 2.11.0, so the chart has a minimum requirement of 2.13.0*) -- Access to a platform with Docker installed, in order to run [docker-mailserver's setup.sh binary](https://github.com/docker-mailserver/docker-mailserver/blob/master/setup.sh), which uses a docker container to setup dovecot password hashes and OpenDKIM keys - -## Architecture - -There are several ways you might deploy docker-mailserver. The most common would be: - -1. Within a cloud provider, utilizing a load balancer service from the cloud provider (i.e. GKE). This is an expensive option, since typically you'd pay for each individual port (25, 465, 993, etc) which gets load-balanced - -2. Either within a cloud provider, or in a private Kubernetes cluster, behind a non-integrated load-balancer such as haproxy. An example deployment might be something like [Funky Penguin's Poor Man's K8s Load Balancer](https://www.funkypenguin.co.nz/project/a-simple-free-load-balancer-for-your-kubernetes-cluster/), or even a manually configured haproxy instance/pair. +- [Helm](https://helm.sh) >= 3.0 ## Getting Started -### 1. Install helm - -You need helm, obviously. Instructions are [here](https://helm.sh/docs/intro/install/). - -### 2. Install cert-manager - -You need to install cert-manager, and [setup issuers](https://docs.cert-manager.io/en/latest/index.html). It's easy to install using helm (which you have anyway, right?). Cert-manager is what will request and renew SSL certificates required for `docker-mailserver` to work. The chart will assume that you've configured and tested certmanager. - -Here are the TL;DR steps for installing cert-manager: +### Install +First install docker-mailserver: ```console -# Install the CustomResourceDefinition resources separately -kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.9.1/cert-manager.yaml - -# Create the namespace for cert-manager -kubectl create namespace cert-manager - -# Label the cert-manager namespace to disable resource validation -kubectl label namespace cert-manager certmanager.k8s.io/disable-validation=true +helm upgrade --install docker-mailserver docker-mailserver --namespace mail --create-namespace +``` -# Add the Jetstack Helm repository -helm repo add jetstack https://charts.jetstack.io +### Create a User +Next you'll need to quickly open a command prompt into the running container (you have two minutes) and setup an email account. -# Update your local Helm chart repository cache -helm repo update +```console +kubectl exec -it --namespace mail deploy/docker-mailserver -- bash -# Install the cert-manager Helm chart -helm install \ - --name cert-manager \ - --namespace cert-manager \ - --version v1.9.1 \ - jetstack/cert-manager +setup email add user@example.com password ``` -### Install docker-mailserver - -You will either need a local clone of this repository or to add the docker-mailserver-helm helm chart repository to your helm configuration: +This will geneate a new new file: ```console -helm repo add docker-mailserver https://docker-mailserver.github.io/docker-mailserver-helm/ +cat /tmp/docker-mailserver/postfix-accounts.cf ``` -## Configuration and Operation +## Configuration +Assuming you still have a command prompt in the running container, run the setup command to see additional +configuration options: -### Install +```console +setup +```console -This command will install Docker Mailserver with default values. You probably want to read the below section for how to configure it before doing this. +For extensive configuration documentation, please refer to [configuration](https://docker-mailserver.github.io/docker-mailserver/latest/config/environment/). -```console -helm install --name docker-mailserver docker-mailserver -``` +As you run various setup commands, additional files will be generated in `/tmp/docker-mailserver`. -### Download setup.sh +Once you are done, you will want to copy them to your local machine (remember when the pod is terminated all of +these files will be deleted!) -Download the [upstream setup.sh](https://raw.githubusercontent.com/docker-mailserver/docker-mailserver/master/setup.sh) to a local folder (*ideally the same location you store your custom values.yaml*) +```console +exit # To exit the bash prompt in the container -Run `./setup.sh` without arguments for a list of full options +mdkir /tmp/config -### Create / Update / Delete users +cd /tmp/config -Run `./setup.sh ` to create the email addresses in `$PWD/config` +podname=$(kubectl get pod --namespace mail -l app.kubernetes.io/name=docker-mailserver -o jsonpath="{.items[0].metadata.name}") + +kubectl cp mail/$podname:tmp/docker-mailserver /tmp/test +``` -Example output: +### Using Docker To Generate Configuration Files +If you have docker or podman installed, you can run a docker-mailserver container and run the setup script. You'll want to mount a local volume into the container so that configuration files are save locally. -```console -[funkypenguin:~/demo] ./setup.sh email add david@kowalski.elpenguino.net -"docker inspect" requires at least 1 argument. -See 'docker inspect --help'. +To make this easier, the docker-mailserver project includes a [setup.sh] (https://docker-mailserver.github.io/docker-mailserver/latest/config/setup.sh/) script. -Usage: docker inspect [OPTIONS] NAME|ID [NAME|ID...] +### Create custom values.yaml +Once you have generated configuration files, you need to deploy them along with the Helm Chart. -Return low-level information on Docker objects -Enter Password: -[funkypenguin:~/demo] % -``` +Unfortunately, Helm does not provide a way too include external files in a deployment. Instead, +configuration files need to be stored in ConfigMaps and Secrets that are then mounted as volumes +into a container. -### Setup OpenDKIM +This can be done by via the `configs` and `secrets` key in the values.yaml file. Please see the comments +in (values.yaml)[./values.yaml] on how to setup these keys. -Example output: +Once you have created your own values.yaml files, then redeploy the chart like this: ```console -[funkypenguin:~/demo] ./setup.sh config dkim -"docker inspect" requires at least 1 argument. -See 'docker inspect --help'. - -Usage: docker inspect [OPTIONS] NAME|ID [NAME|ID...] - -Return low-level information on Docker objects -Creating DKIM private key /tmp/docker-mailserver/opendkim/keys/bob.com/mail.private -Creating DKIM KeyTable -Creating DKIM SigningTable -Creating DKIM private key /tmp/docker-mailserver/opendkim/keys/example.com/mail.private -Creating DKIM TrustedHosts -[funkypenguin:~/demo] +helm upgrade docker-mailserver docker-mailserver --namespace mail --values ``` -### Docker Mailserver Configuration - -All configuration values are documented in values.yaml. Check that for references, default values etc. To modify a -configuration value for a chart, you can either supply your own values.yaml overriding the default one in the repo: +You can also override individual configuration setting with `helm upgrade --set`, specifying each parameter using the `--set key=value[,key=value]` argument to `helm install`. For example: ```console -$ helm upgrade --install docker-mailserver docker-mailserver --values path/to/custom/values/file.yaml +$ helm upgrade docker-mailserver docker-mailserver --namespace mail --set pod.dockermailserver.image="your/image:1.0.0" ``` -Or, you can override an individual configuration setting with `helm upgrade --set`, specifying each parameter using the `--set key=value[,key=value]` argument to `helm install`. For example: +### Minimal configuration +There are various settings in `values.yaml` that you must override. -```console -$ helm upgrade --install docker-mailserver docker-mailserver --set pod.dockermailserver.image="your/image:1.0.0" -``` +| Parameter | Description | Default | +| -------------------------------------------- | --------------------------------------------------- | ---------------- | +| pod.dockermailserver.pod.OVERRIDE_HOSTNAME | The hostname to be presented on SMTP banners | mail.example.com | +| configs | Specify ConfigMaps that contain configuration files | [] | +| secrets | Specify Secrets that contain configuration files | [] | +| ssl.issuer.name | The name of the cert-manager issuer expected to issue certs | `letsencrypt-staging` | +| ssl.issuer.kind | Whether the issuer is namespaced (`Issuer`) on cluster-wide (`ClusterIssuer`) | ClusterIssuer | +| ssl.dnsname | DNS domain used for DNS01 validation | example.com | -#### Minimal configuration +### Environmental Variables +There are **many** environment variables which allow you to customize the behaviour of docker-mailserver. The function of each variable is described at https://github.com/docker-mailserver/docker-mailserver#environment-variables -Most of the values recorded belowe are set to sensible default, butyou'll definately want to pay attention to at least the following: +Every variable can be set using `values.yaml`, but note that docker-mailserver expects any true/false values to be set as binary numbers (1/0), rather than boolean (true/false). BadThings(tm) will happen if you try to pass an environment variable as "true" when [`start-mailserver.sh`](https://github.com/docker-mailserver/docker-mailserver/blob/master/target/start-mailserver.sh) is expecting a 1 or a 0! -| Parameter | Description | Default | -|------------------------------------------|-----------------------------------------------------------------------------------------------------------------------|------------------------| -| `pod.dockermailserver.override_hostname` | The hostname to be presented on SMTP banners | `mail.batcave.org` | -| `demoMode.enabled` | Start the container with a demo "user@example.com" user (password is "password") | `true` | -| `domains` | List of domains to be served | `[]` | -| `ssl.issuer.name` | The name of the cert-manager issuer expected to issue certs | `letsencrypt-staging` | -| `ssl.issuer.kind` | Whether the issuer is namespaced (`Issuer`) on cluster-wide (`ClusterIssuer`) | `ClusterIssuer` | -| `ssl.dnsname` | DNS domain used for DNS01 validation | `example.com` | +### Default Configuration +By default, the Chart enables `rspamd` and disables `opendkim`, `dmarc`, `policyd-spf` and `clamav`. This is the setup [recommended] (https://docker-mailserver.github.io/docker-mailserver/latest/config/best-practices/dkim_dmarc_spf/) by the docker-mailserver project. #### Chart Configuration - The following table lists the configurable parameters of the docker-mailserver chart and their default values. | Parameter | Description | Default | @@ -230,12 +188,6 @@ The following table lists the configurable parameters of the docker-mailserver c | `runtimeClassName` | Optionally, set the pod's [runtimeClass](https://kubernetes.io/docs/concepts/containers/runtime-class/) | `""` | | `priorityClassName` | Optionally, set the pod's [priorityClass](https://kubernetes.io/docs/concepts/scheduling-eviction/pod-priority-preemption/) | `""` | -#### docker-mailserver Configuration - -There are **many** environment variables which allow you to customize the behaviour of docker-mailserver. The function of each variable is described at https://github.com/docker-mailserver/docker-mailserver#environment-variables - -Every variable can be set using `values.yaml`, but note that docker-mailserver expects any true/false values to be set as binary numbers (1/0), rather than boolean (true/false). BadThings(tm) will happen if you try to pass an environment variable as "true" when [`start-mailserver.sh`](https://github.com/docker-mailserver/docker-mailserver/blob/master/target/start-mailserver.sh) is expecting a 1 or a 0! - #### HA Proxy-Ingress Configuration | Parameter | Description | Default | @@ -270,9 +222,6 @@ Every variable can be set using `values.yaml`, but note that docker-mailserver e | `metrics.serviceMonitor.scrapeInterval` | default scrape interval | `15s` | - - - ## Development ### Testing From 91c4f4b7aae6b3d529bb456384e3600b9468eb8e Mon Sep 17 00:00:00 2001 From: Charlie Savage Date: Sun, 14 Jan 2024 16:51:12 -0800 Subject: [PATCH 14/66] Remove domains which is no longer used and set default values for configs and secrets keys. --- charts/docker-mailserver/values.yaml | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/charts/docker-mailserver/values.yaml b/charts/docker-mailserver/values.yaml index 2ede792d..35dd496f 100644 --- a/charts/docker-mailserver/values.yaml +++ b/charts/docker-mailserver/values.yaml @@ -45,8 +45,15 @@ serviceAccount: ## content: | ## # A sample user - the password is "password" ## user@example.com|{SHA512-CRYPT}$6$l4023rZnQEy/l0Rg$JeNjAAICB43VAX7GTJ9jeE7DR0LeyR5nU.ftq3c42T5E1IZSuRBqwM8erRh6t0CyIT6aYpBIAopzcQHNUvMV00 +## +## If you set the create key to false, then you must manually create the ConfigMaps before deploying the chart. +## +## kubectl create configmap postfix.example.com --namespace mail --from-file=postfix-accounts.cf= +## +configs: [] -## The secrets key is similar section allows you to add additional secrets +## The secrets key works the same way as the configs key. Use secrets to store sensitive information, +## such as DKIM signing keys. ## ## secrets: ## - name: rspamd.example.com # This is the name of the Secret @@ -59,6 +66,10 @@ serviceAccount: ## - subPath: rspamd.dkim.rsa-2048-mail-example.com.public ## mountPath: rspamd/dkim/rsa-2048-mail-example.com.public ## content: abace # If create is true, then you must specify content. Must be base 64 encoded! +## +## If you set the create key to false, then you must manually create the ConfigMaps before deploying the chart. +## +## kubectl create secret rspamd.example.com --namespace mail --from-file=rspamd.dkim.rsa-2048-mail-example.com.private.txt= secrets: [] ## Specify an existing secret that contains TLS certificates. The easiest way to create a @@ -87,12 +98,6 @@ spfTestsDisabled: false # List extra RBL domains to use for hard reject filtering rblRejectDomains: [] -# List all the domains to be used below - this is necessary to correctly configure DKIM keys for email signing -# domains is the list of domains to be served by your docker-mailserver instance -domains: [] -# - your-first-domain.com -# - your-second-domain.com - livenessTests: # livenessTests.enabled will add a liveness test, which will classify a pod as 'unhealthy' if any livenessTests.commands (below) return non-zero enabled: true From 41155c8db60aa35dc61c4a0d23e85152c26a7b94 Mon Sep 17 00:00:00 2001 From: Charlie Savage Date: Sun, 14 Jan 2024 21:29:42 -0800 Subject: [PATCH 15/66] More documentation updates --- charts/docker-mailserver/README.md | 23 ++++++++--------------- 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/charts/docker-mailserver/README.md b/charts/docker-mailserver/README.md index ff0cffcf..f599656e 100644 --- a/charts/docker-mailserver/README.md +++ b/charts/docker-mailserver/README.md @@ -50,16 +50,16 @@ The chart includes the following features: - [Helm](https://helm.sh) >= 3.0 ## Getting Started +Setting up docker-mailserver requires generating a number of configuration (files)[https://docker-mailserver.github.io/docker-mailserver/latest/config/advanced/optional-config/]. Luckily, docker-mailserver ships with a helpful `setup` command that makes it easy to generate these files. It writes files to the `/tmp/docker-mailserver` directory inside a running container. -### Install -First install docker-mailserver: +First run docker-mailserver: ```console helm upgrade --install docker-mailserver docker-mailserver --namespace mail --create-namespace ``` ### Create a User -Next you'll need to quickly open a command prompt into the running container (you have two minutes) and setup an email account. +Next you'll need to quickly open a command prompt in the running container (you have two minutes) and setup an email account. ```console kubectl exec -it --namespace mail deploy/docker-mailserver -- bash @@ -67,15 +67,14 @@ kubectl exec -it --namespace mail deploy/docker-mailserver -- bash setup email add user@example.com password ``` -This will geneate a new new file: +This will geneate a new `postfix-accounts.cf` file: ```console cat /tmp/docker-mailserver/postfix-accounts.cf ``` -## Configuration -Assuming you still have a command prompt in the running container, run the setup command to see additional -configuration options: +### Create Additional Configuration Files +Assuming you still have a command prompt open in the running container, run the setup command to see additional configuration options: ```console setup @@ -85,8 +84,7 @@ For extensive configuration documentation, please refer to [configuration](https As you run various setup commands, additional files will be generated in `/tmp/docker-mailserver`. -Once you are done, you will want to copy them to your local machine (remember when the pod is terminated all of -these files will be deleted!) +Once you are done, copy the configuration files to your local machine (remember when the pod is terminated all of these files will be deleted!) ```console exit # To exit the bash prompt in the container @@ -100,12 +98,7 @@ podname=$(kubectl get pod --namespace mail -l app.kubernetes.io/name=docker-mail kubectl cp mail/$podname:tmp/docker-mailserver /tmp/test ``` -### Using Docker To Generate Configuration Files -If you have docker or podman installed, you can run a docker-mailserver container and run the setup script. You'll want to mount a local volume into the container so that configuration files are save locally. - -To make this easier, the docker-mailserver project includes a [setup.sh] (https://docker-mailserver.github.io/docker-mailserver/latest/config/setup.sh/) script. - -### Create custom values.yaml +## Customize values.yaml Once you have generated configuration files, you need to deploy them along with the Helm Chart. Unfortunately, Helm does not provide a way too include external files in a deployment. Instead, From 0712d4f6a1c2df5892623560791b2a20904befd2 Mon Sep 17 00:00:00 2001 From: Charlie Savage Date: Sun, 14 Jan 2024 21:29:58 -0800 Subject: [PATCH 16/66] Remove unneeded volumes and volume mounts --- .../templates/deployment.yaml | 19 ++++++++-------- charts/docker-mailserver/values.yaml | 22 +++++++------------ 2 files changed, 18 insertions(+), 23 deletions(-) diff --git a/charts/docker-mailserver/templates/deployment.yaml b/charts/docker-mailserver/templates/deployment.yaml index d67dcb89..61eeb222 100644 --- a/charts/docker-mailserver/templates/deployment.yaml +++ b/charts/docker-mailserver/templates/deployment.yaml @@ -66,16 +66,21 @@ spec: {{- end }} -{{- if .Values.pod.dockermailserver.env.SSL_TYPE }} + {{- if .Values.pod.dockermailserver.env.SSL_TYPE }} - name: "ssl-cert" secret: + {{- if .Values.ssl.useExisting }} secretName: {{ .Values.ssl.existingName }} -{{- end }} + {{- else }} + secretName: {{ template "dockermailserver.fullname" . }}-tls + {{- end }} + {{- end }} + {{- if .Values.additionalVolumes }} + # Additional Volumes {{ toYaml .Values.additionalVolumes | indent 8 }} {{- end }} - - name: tmp - emptyDir: {} + containers: - name: docker-mailserver env: @@ -103,15 +108,11 @@ spec: {{ end }} {{ toYaml .Values.pod.dockermailserver.containerSecurityContext | indent 12 }} volumeMounts: - - name: config - mountPath: /tmp/docker-mailserver -{{- if and .Values.pod.dockermailserver.env.SSL_TYPE .Values.ssl.useExisting }} +{{- if .Values.pod.dockermailserver.env.SSL_TYPE }} - name: ssl-cert mountPath: /tmp/dms/custom-certs readOnly: true {{- end }} - - name: tmp - mountPath: /var/tmp {{ if .Values.metrics.enabled }} - name: data mountPath: /var/log/mail diff --git a/charts/docker-mailserver/values.yaml b/charts/docker-mailserver/values.yaml index 35dd496f..ed69aabf 100644 --- a/charts/docker-mailserver/values.yaml +++ b/charts/docker-mailserver/values.yaml @@ -79,13 +79,16 @@ ssl: existingName: name-of-existing-secret ## Mount additional volumes into the container. Useful for persistent logs or -## injecting additional config files. +## creating or injecting additional config files. #additionalVolumes: -# - name: "additional" -# emptyDir: {} +#- name: host-config-files +# hostPath: +# path: /tmp/docker-mailserver-config # Directory on host +# type: Directory + #additionalVolumeMounts: -# - name: additional -# mountPath: /additional +#- name: host-config-files +# mountPath: /tmp/docker-mailserver # Directory in container # If you choose _not_ to use haproxy, and you're not exposing your services with a load-balanced service # with an external traffic policy in "Local" mode, you risk having the source IP of incoming mail overwritten @@ -382,15 +385,6 @@ persistence: annotations: {} # backup.kubernetes.io/deltas: PT1H P2D P30D P180D -additionalVolumeMounts: [] -# - name: backup -# mountPath: /scratch - -additionalVolumes: [] -# - name: backup -# emptyDir: -# sizeLimit: 10Gi - ## Monitoring adds the prometheus.io annotations to pods and services, so that the Prometheus Kubernetes SD mechanism ## as configured in the examples will automatically discover both the pods and the services to query. ## From e36a901ceb97e9d8ef44528e311c496c2dac8f0d Mon Sep 17 00:00:00 2001 From: Charlie Savage Date: Sun, 14 Jan 2024 21:53:02 -0800 Subject: [PATCH 17/66] Separate proxy_protocol setting from haproxy since other proxies, such as nginx, can use the PROXY protocol. --- charts/docker-mailserver/README.md | 29 ++++++++++++++- charts/docker-mailserver/config/dovecot.cf | 36 +++++++++++++++---- .../docker-mailserver/config/postfix-main.cf | 3 +- .../docker-mailserver/templates/service.yaml | 17 +++++++++ charts/docker-mailserver/values.yaml | 7 ++-- 5 files changed, 80 insertions(+), 12 deletions(-) diff --git a/charts/docker-mailserver/README.md b/charts/docker-mailserver/README.md index f599656e..56cafba8 100644 --- a/charts/docker-mailserver/README.md +++ b/charts/docker-mailserver/README.md @@ -140,7 +140,34 @@ Every variable can be set using `values.yaml`, but note that docker-mailserver e ### Default Configuration By default, the Chart enables `rspamd` and disables `opendkim`, `dmarc`, `policyd-spf` and `clamav`. This is the setup [recommended] (https://docker-mailserver.github.io/docker-mailserver/latest/config/best-practices/dkim_dmarc_spf/) by the docker-mailserver project. -#### Chart Configuration +## Exposing Ports to the Outside World +If you are running a bare-metal Kubernetes cluster, you will need to expose ports to the internet to receive and send emails. In addition, you need to make sure that docker-mailserver receives the correct client IP address so that spam filtering works. + +This can get a bit complicated, as explained in the docker-mailserver (documentation)[https://docker-mailserver.github.io/docker-mailserver/latest/config/advanced/kubernetes/#exposing-your-mail-server-to-the-outside-world]. + +One approach is to use the PROXY protocol, which is also explained in the (documentation)[https://docker-mailserver.github.io/docker-mailserver/latest/config/advanced/kubernetes/#proxy-port-to-service-via-proxy-protocol]. + +The Helm chart supports the use of the proxy protocol via the `proxy_protocol` key. To enable it set the `enable` key to true. You will also want to set the `trustedNetworks` key. + +```yaml +proxy_protocol: + enabled: true + # List of sources (in CIDR format, space-separated) to permit PROXY protocol from + trustedNetworks: "10.0.0.0/8 192.168.0.0/16 172.16.0.0/16" +``` + +However, if you enable the Proxy protocol you will break any clients (for example NextCloud) inside your Kubernetes cluster that talk to Dovecot because they will not be using the PROXY protocol. Therefore, if the PROXY protocol is enabled, the Helm chart will create additional ports that listen without the PROXY protocol by adding 10,000 to the standard port value. + +| Protocol | PROXY Port | No PROXY Port | +| ---------- | ------------ | -------------- | +| imap | 143 | 10143 | +| imaps | 993 | 10993 | +| pop3 | 110 | 10110 | +| pop3s | 995 | 10995 | + +Note thes ports are NOT exposed outside of the Kubernetes cluster. + +## Chart Values The following table lists the configurable parameters of the docker-mailserver chart and their default values. | Parameter | Description | Default | diff --git a/charts/docker-mailserver/config/dovecot.cf b/charts/docker-mailserver/config/dovecot.cf index 2165ede6..4a0ff63f 100644 --- a/charts/docker-mailserver/config/dovecot.cf +++ b/charts/docker-mailserver/config/dovecot.cf @@ -1,19 +1,41 @@ -{{- if .Values.haproxy.enabled }} -haproxy_trusted_networks = {{ .Values.haproxy.trustedNetworks }} -{{- end -}} +{{- if .Values.proxy_protocol.enabled }} +proxy_protocol_trusted_networks = {{ .Values.proxy_protocol.trustedNetworks }} + +{{- if and (.Values.pod.dockermailserver.env.ENABLE_IMAP) (not .Values.pod.dockermailserver.env.SMTP_ONLY) }} service imap-login { inet_listener imap { - {{ if .Values.haproxy.enabled -}}haproxy = yes{{ end }} + proxy_protocol = yes } + # Provide a port for internal cluster clients that don't use the Proxy protocol + inet_listener imap-no-proxy { + port = 10143 + } inet_listener imaps { - {{ if .Values.haproxy.enabled -}}haproxy = yes{{ end }} + proxy_protocol = yes + } + # Provide a port for internal cluster clients that don't use the Proxy protocol + inet_listener imaps-no-proxy { + port = 10993 } } +{{- end -}} + +{{- if and (.Values.pod.dockermailserver.env.ENABLE_POP) (not .Values.pod.dockermailserver.env.SMTP_ONLY) }} service pop3-login { inet_listener pop3 { - {{ if .Values.haproxy.enabled -}}haproxy = yes{{ end }} + proxy_protocol = yes + } + # Provide a port for internal cluster clients that don't use the Proxy protocol + inet_listener pop3-no-proxy { + port = 10110 } inet_listener pop3s { - {{ if .Values.haproxy.enabled -}}haproxy = yes{{ end }} + proxy_protocol = yes + } + # Provide a port for internal cluster clients that don't use the Proxy protocol + inet_listener pop3s-no-proxy { + port = 10995 } } +{{- end -}} +{{- end -}} diff --git a/charts/docker-mailserver/config/postfix-main.cf b/charts/docker-mailserver/config/postfix-main.cf index 96c4b14a..190b85f1 100644 --- a/charts/docker-mailserver/config/postfix-main.cf +++ b/charts/docker-mailserver/config/postfix-main.cf @@ -1,5 +1,4 @@ - {{- /* Enable proxy protocol for postscreen / dovecot */}} - {{- if .Values.haproxy.enabled -}} # Necessary to permit proxy protocol from haproxy to postscreen + {{- if .Values.proxy_protocol.enabled -}} postscreen_upstream_proxy_protocol = haproxy {{- end }} {{- if not .Values.spfTestsDisabled -}} diff --git a/charts/docker-mailserver/templates/service.yaml b/charts/docker-mailserver/templates/service.yaml index 78456324..36f43ded 100644 --- a/charts/docker-mailserver/templates/service.yaml +++ b/charts/docker-mailserver/templates/service.yaml @@ -64,6 +64,15 @@ spec: {{- if eq .Values.service.type "NodePort" }} nodePort: {{ default "30993" .Values.service.nodePort.imaps }} {{- end }} + + {{- if .Values.proxy_protocol.enabled -}} + - name: imap-no-proxy + targetPort: imap + port: 10143 + - name: imaps-no-proxy + targetPort: imaps + port: 10993 + {{- end }} {{- end }} {{- if and (.Values.pod.dockermailserver.env.ENABLE_POP) (not .Values.pod.dockermailserver.env.SMTP_ONLY) }} @@ -79,6 +88,14 @@ spec: {{- if eq .Values.service.type "NodePort" }} nodePort: {{ default "30995" .Values.service.nodePort.pop3s }} {{- end }} + {{- if .Values.proxy_protocol.enabled -}} + - name: pop3-no-proxy + targetPort: imap + port: 10110 + - name: pop3s-no-proxy + targetPort: imaps + port: 10995 + {{- end }} {{- end }} {{- if .Values.pod.dockermailserver.env.ENABLE_RSPAMD }} diff --git a/charts/docker-mailserver/values.yaml b/charts/docker-mailserver/values.yaml index ed69aabf..05cfcc91 100644 --- a/charts/docker-mailserver/values.yaml +++ b/charts/docker-mailserver/values.yaml @@ -427,6 +427,11 @@ rspamd: enabled: false secret: +proxy_protocol: + enabled: false + # List of sources (in CIDR format, space-separated) to permit PROXY protocol from + trustedNetworks: "10.0.0.0/8 192.168.0.0/16 172.16.0.0/16" + ## These values are for the haproxy sub-chart haproxy: # haproxy.enabled will deploy an haproxy sub-chart, configured for the TCP ports used by docker-mailserver @@ -456,8 +461,6 @@ haproxy: # A space-separated list, on a single line, is required # By default, we allow all RFC1918 private ranges, but this can be tightened up for the known IP/range # of your HAProxy instance - # haproxy.trustedNetworks is the list of sources (in CIDR format, space-separated) to permit haproxy PROXY protocol from - trustedNetworks: "10.0.0.0/8 192.168.0.0/16 172.16.0.0/16" # when metrics is enabled, we mount subpath log from pvc into /var/log/mail metrics: From 037022b67d15d978204db2e08f4d47cd0aa9e7ae Mon Sep 17 00:00:00 2001 From: Charlie Savage Date: Sun, 14 Jan 2024 22:40:50 -0800 Subject: [PATCH 18/66] Remove cert-manager, simplify certificate setup. --- charts/docker-mailserver/README.md | 13 ++++++++-- .../templates/deployment.yaml | 25 ++++++++----------- charts/docker-mailserver/values.yaml | 12 ++++----- 3 files changed, 26 insertions(+), 24 deletions(-) diff --git a/charts/docker-mailserver/README.md b/charts/docker-mailserver/README.md index 56cafba8..f21b84df 100644 --- a/charts/docker-mailserver/README.md +++ b/charts/docker-mailserver/README.md @@ -14,7 +14,6 @@ Compose, but it has been [adapted to Kubernetes](https://github.com/docker-mails - [Prerequisites](#prerequisites) - [Getting Started](#getting-started) - [Install](install) - - [Install Cert-manager](#2-install-cert-manager) - [Install Docker Mailserver](#install-docker-mailserver) - [Configuration and Operation](#configuration-and-operation) - [Download setup.sh](#download-setupsh) @@ -140,7 +139,17 @@ Every variable can be set using `values.yaml`, but note that docker-mailserver e ### Default Configuration By default, the Chart enables `rspamd` and disables `opendkim`, `dmarc`, `policyd-spf` and `clamav`. This is the setup [recommended] (https://docker-mailserver.github.io/docker-mailserver/latest/config/best-practices/dkim_dmarc_spf/) by the docker-mailserver project. -## Exposing Ports to the Outside World +### Certificate +You will need to setup a TLS certificate for your email domain. Perhaps the easiest way to do this is use (cert-manager)[https://cert-manager.io/]. + +Once you acquire a certificate, you will need to store it in a TLS secret in the docker-mailserver namespace. Once you have done that, update the values.yaml file like this: + +```yaml +certificate: my-certificate-secret +``` +The chart will then automatically copy the certificate and private key to the `/tmp/dms/custom-certs` director in the container and set correctly set the `SSL_CERT_PATH` and `SSL_KEY_PATH` environment variables. + +### Exposing Ports to the Outside World If you are running a bare-metal Kubernetes cluster, you will need to expose ports to the internet to receive and send emails. In addition, you need to make sure that docker-mailserver receives the correct client IP address so that spam filtering works. This can get a bit complicated, as explained in the docker-mailserver (documentation)[https://docker-mailserver.github.io/docker-mailserver/latest/config/advanced/kubernetes/#exposing-your-mail-server-to-the-outside-world]. diff --git a/charts/docker-mailserver/templates/deployment.yaml b/charts/docker-mailserver/templates/deployment.yaml index 61eeb222..299dbbc3 100644 --- a/charts/docker-mailserver/templates/deployment.yaml +++ b/charts/docker-mailserver/templates/deployment.yaml @@ -65,16 +65,11 @@ spec: secretName: {{ $secret.name }} {{- end }} - - {{- if .Values.pod.dockermailserver.env.SSL_TYPE }} - - name: "ssl-cert" + {{- if .Values.certificate }} + - name: "certificate" secret: - {{- if .Values.ssl.useExisting }} - secretName: {{ .Values.ssl.existingName }} - {{- else }} - secretName: {{ template "dockermailserver.fullname" . }}-tls - {{- end }} - {{- end }} + secretName: {{ .Values.certificate }} + {{- end }} {{- if .Values.additionalVolumes }} # Additional Volumes @@ -89,7 +84,7 @@ spec: value: {{ quote $pval }} {{- end }} - {{- if .Values.pod.dockermailserver.env.SSL_TYPE }} + {{- if .Values.certificate }} - name: SSL_CERT_PATH value: /tmp/dms/custom-certs/tls.crt - name: SSL_KEY_PATH @@ -108,16 +103,16 @@ spec: {{ end }} {{ toYaml .Values.pod.dockermailserver.containerSecurityContext | indent 12 }} volumeMounts: -{{- if .Values.pod.dockermailserver.env.SSL_TYPE }} - - name: ssl-cert + {{- if .Values.certificate }} + - name: certificate mountPath: /tmp/dms/custom-certs readOnly: true -{{- end }} -{{ if .Values.metrics.enabled }} + {{- end }} + {{ if .Values.metrics.enabled }} - name: data mountPath: /var/log/mail subPath: log -{{- end }} + {{- end }} - name: data mountPath: /var/mail subPath: mail diff --git a/charts/docker-mailserver/values.yaml b/charts/docker-mailserver/values.yaml index 05cfcc91..c420990e 100644 --- a/charts/docker-mailserver/values.yaml +++ b/charts/docker-mailserver/values.yaml @@ -72,11 +72,8 @@ configs: [] ## kubectl create secret rspamd.example.com --namespace mail --from-file=rspamd.dkim.rsa-2048-mail-example.com.private.txt= secrets: [] -## Specify an existing secret that contains TLS certificates. The easiest way to create a -## secret is by using cert-manager. -ssl: - useExisting: false - existingName: name-of-existing-secret +## Specify a TLS secret name that contains a certificate and private key for your email domain +certificate: ## Mount additional volumes into the container. Useful for persistent logs or ## creating or injecting additional config files. @@ -183,8 +180,9 @@ pod: ENABLE_MANAGESIEVE: POSTSCREEN_ACTION: enforce SMTP_ONLY: - SSL_TYPE: - # These two values are automatically setup if SSL_TYPE is configured + SSL_TYPE: manual # This should always be manual because the chart just points to an existing + # tls secret (which may be setup manually or by cert-manager) + # These two values are automatically set by the chart based on the certificate key # SSL_CERT_PATH: # SSL_KEY_PATH: SSL_ALT_CERT_PATH: From 53a102dd0f7d31945b6143673f25bf62e86461b7 Mon Sep 17 00:00:00 2001 From: Charlie Savage Date: Sun, 14 Jan 2024 22:44:53 -0800 Subject: [PATCH 19/66] Change .Values.pod.dockermailserver to be .Values.deployment --- charts/docker-mailserver/config/dovecot.cf | 4 +- .../templates/deployment.yaml | 22 +- .../templates/ingress-rspamd.yaml | 2 +- .../docker-mailserver/templates/service.yaml | 8 +- charts/docker-mailserver/values.yaml | 425 +++++++++--------- 5 files changed, 227 insertions(+), 234 deletions(-) diff --git a/charts/docker-mailserver/config/dovecot.cf b/charts/docker-mailserver/config/dovecot.cf index 4a0ff63f..114cfb69 100644 --- a/charts/docker-mailserver/config/dovecot.cf +++ b/charts/docker-mailserver/config/dovecot.cf @@ -1,7 +1,7 @@ {{- if .Values.proxy_protocol.enabled }} proxy_protocol_trusted_networks = {{ .Values.proxy_protocol.trustedNetworks }} -{{- if and (.Values.pod.dockermailserver.env.ENABLE_IMAP) (not .Values.pod.dockermailserver.env.SMTP_ONLY) }} +{{- if and (.Values.deployment.env.ENABLE_IMAP) (not .Values.deployment.env.SMTP_ONLY) }} service imap-login { inet_listener imap { proxy_protocol = yes @@ -20,7 +20,7 @@ service imap-login { } {{- end -}} -{{- if and (.Values.pod.dockermailserver.env.ENABLE_POP) (not .Values.pod.dockermailserver.env.SMTP_ONLY) }} +{{- if and (.Values.deployment.env.ENABLE_POP) (not .Values.deployment.env.SMTP_ONLY) }} service pop3-login { inet_listener pop3 { proxy_protocol = yes diff --git a/charts/docker-mailserver/templates/deployment.yaml b/charts/docker-mailserver/templates/deployment.yaml index 299dbbc3..8fe95df5 100644 --- a/charts/docker-mailserver/templates/deployment.yaml +++ b/charts/docker-mailserver/templates/deployment.yaml @@ -18,15 +18,15 @@ spec: matchLabels: app.kubernetes.io/name: {{ template "dockermailserver.fullname" . }} release: "{{ .Release.Name }}" - strategy: {{- toYaml .Values.pod.dockermailserver.strategy | nindent 4 }} + strategy: {{- toYaml .Values.deployment.strategy | nindent 4 }} template: metadata: labels: app.kubernetes.io/name: {{ template "dockermailserver.fullname" . }} release: "{{ .Release.Name }}" - {{- if .Values.pod.dockermailserver.annotations }} + {{- if .Values.deployment.annotations }} annotations: -{{ toYaml .Values.pod.dockermailserver.annotations | indent 8 }} +{{ toYaml .Values.deployment.annotations | indent 8 }} {{ end }} spec: runtimeClassName: {{ .Values.runtimeClassName }} @@ -79,7 +79,7 @@ spec: containers: - name: docker-mailserver env: - {{- range $pkey, $pval := .Values.pod.dockermailserver.env }} + {{- range $pkey, $pval := .Values.deployment.env }} - name: {{ $pkey }} value: {{ quote $pval }} {{- end }} @@ -96,12 +96,12 @@ spec: resources: {{ toYaml .Values.resources | indent 12 }} securityContext: - {{- if eq .Values.pod.dockermailserver.env.ENABLE_FAIL2BAN 1.0 }} + {{- if eq .Values.deployment.env.ENABLE_FAIL2BAN 1.0 }} capabilities: add: - "NET_ADMIN" {{ end }} -{{ toYaml .Values.pod.dockermailserver.containerSecurityContext | indent 12 }} +{{ toYaml .Values.deployment.containerSecurityContext | indent 12 }} volumeMounts: {{- if .Values.certificate }} - name: certificate @@ -191,26 +191,26 @@ spec: - name: "submission" containerPort: 587 - {{- if and (.Values.pod.dockermailserver.env.ENABLE_IMAP) (not .Values.pod.dockermailserver.env.SMTP_ONLY) }} + {{- if and (.Values.deployment.env.ENABLE_IMAP) (not .Values.deployment.env.SMTP_ONLY) }} - name: "imap" containerPort: 143 - name: "imaps" containerPort: 993 {{- end }} - {{- if and (.Values.pod.dockermailserver.env.ENABLE_POP) (not .Values.pod.dockermailserver.env.SMTP_ONLY) }} + {{- if and (.Values.deployment.env.ENABLE_POP) (not .Values.deployment.env.SMTP_ONLY) }} - name: "pop3" containerPort: 110 - name: "pop3s" containerPort: 995 {{- end }} - {{- if .Values.pod.dockermailserver.env.ENABLE_RSPAMD }} + {{- if .Values.deployment.env.ENABLE_RSPAMD }} - name: "rspamd" containerPort: 11334 {{- end }} - {{- if and (.Values.pod.dockermailserver.env.ENABLE_MANAGESIEVE) (not .Values.pod.dockermailserver.env.SMTP_ONLY) }} + {{- if and (.Values.deployment.env.ENABLE_MANAGESIEVE) (not .Values.deployment.env.SMTP_ONLY) }} - name: "managesieve" containerPort: 4190 {{- end }} @@ -233,7 +233,7 @@ spec: resources: {{ toYaml .Values.metrics.resources | indent 12 }} securityContext: -{{ toYaml .Values.pod.dockermailserver.containerSecurityContext | indent 12 }} +{{ toYaml .Values.deployment.containerSecurityContext | indent 12 }} volumeMounts: - name: data diff --git a/charts/docker-mailserver/templates/ingress-rspamd.yaml b/charts/docker-mailserver/templates/ingress-rspamd.yaml index eab555fe..9aadb4b4 100644 --- a/charts/docker-mailserver/templates/ingress-rspamd.yaml +++ b/charts/docker-mailserver/templates/ingress-rspamd.yaml @@ -1,4 +1,4 @@ -{{- if and (.Values.pod.dockermailserver.env.ENABLE_RSPAMD) (.Values.rspamd.ingress.enabled) -}} +{{- if and (.Values.deployment.env.ENABLE_RSPAMD) (.Values.rspamd.ingress.enabled) -}} apiVersion: networking.k8s.io/v1 kind: Ingress metadata: diff --git a/charts/docker-mailserver/templates/service.yaml b/charts/docker-mailserver/templates/service.yaml index 36f43ded..23a535b4 100644 --- a/charts/docker-mailserver/templates/service.yaml +++ b/charts/docker-mailserver/templates/service.yaml @@ -51,7 +51,7 @@ spec: nodePort: {{ default "30587" .Values.service.nodePort.submission }} {{ end }} - {{- if and (.Values.pod.dockermailserver.env.ENABLE_IMAP) (not .Values.pod.dockermailserver.env.SMTP_ONLY) }} + {{- if and (.Values.deployment.env.ENABLE_IMAP) (not .Values.deployment.env.SMTP_ONLY) }} - name: imap targetPort: imap port: 143 @@ -75,7 +75,7 @@ spec: {{- end }} {{- end }} - {{- if and (.Values.pod.dockermailserver.env.ENABLE_POP) (not .Values.pod.dockermailserver.env.SMTP_ONLY) }} + {{- if and (.Values.deployment.env.ENABLE_POP) (not .Values.deployment.env.SMTP_ONLY) }} - name: pop3 targetPort: pop3 port: 110 @@ -98,13 +98,13 @@ spec: {{- end }} {{- end }} - {{- if .Values.pod.dockermailserver.env.ENABLE_RSPAMD }} + {{- if .Values.deployment.env.ENABLE_RSPAMD }} - name: rspamd targetPort: rspamd port: 11334 {{- end }} - {{- if and (.Values.pod.dockermailserver.env.ENABLE_MANAGESIEVE) (not .Values.pod.dockermailserver.env.SMTP_ONLY) }} + {{- if and (.Values.deployment.env.ENABLE_MANAGESIEVE) (not .Values.deployment.env.SMTP_ONLY) }} - name: managesieve targetPort: managesieve port: 4190 diff --git a/charts/docker-mailserver/values.yaml b/charts/docker-mailserver/values.yaml index c420990e..4bb63d63 100644 --- a/charts/docker-mailserver/values.yaml +++ b/charts/docker-mailserver/values.yaml @@ -105,212 +105,215 @@ livenessTests: commands: - "clamscan /tmp/docker-mailserver/TrustedHosts" -pod: - # pod.dockermailserver section refers to the configuration of the docker-mailserver pod itself. - - # Note that the many environment variables which define the behaviour of docker-mailserver are configured here - # See https://github.com/docker-mailserver/docker-mailserver/blob/master/ENVIRONMENT.md for details - dockermailserver: - - ## Host networking requested for this pod. Use the host’s network namespace. If this option is set, the ports that - ## will be used must be specified. - ## Ref: https://kubernetes.io/docs/api-reference/v1/definitions/#_v1_podspec - # pod.dockermailserver.hostNetwork will configure the pod to use the host's network namespace - hostNetwork: false - ## Use the host’s pid namespace - ## Ref: https://kubernetes.io/docs/api-reference/v1/definitions/#_v1_podspec - # pod.dockermailserver.hostPID defines whether the pod should use the host's PID namespace (default false) - hostPID: false - - ## Update strategy - only really applicable for deployments with RWO PVs attached - ## If replicas = 1, an update can get "stuck", as the previous pod remains attached to the - ## PV, and the "incoming" pod can never start. Setting the strategy to "Recreate" (our default) will - ## terminate the single previous pod, so that the new, incoming pod can attach to the PV - strategy: - # rollingUpdate: - # maxSurge: 1 - # maxUnavailable: 1 - type: "Recreate" - - ## The following variables affect the behaviour of docker-mailserver - ## See https://docker-mailserver.github.io/docker-mailserver/latest/config/environment/ for details - ## Note that an empty value indicates the default as described in the docs above - env: - # ----------------------------------------------- - # --- Required Section --------------------------- - # ----------------------------------------------- - OVERRIDE_HOSTNAME: mail.example.com # You must OVERRIDE this! - - # ----------------------------------------------- - # --- General Section --------------------------- - # ----------------------------------------------- - LOG_LEVEL: info - SUPERVISOR_LOGLEVEL: - ONE_DIR: 1 - DMS_VMAIL_UID: - DMS_VMAIL_GID: - ACCOUNT_PROVISIONER: - POSTMASTER_ADDRESS: - ENABLE_UPDATE_CHECK: 1 - UPDATE_CHECK_INTERVAL: 1d - PERMIT_DOCKER: none - TZ: - NETWORK_INTERFACE: - TLS_LEVEL: - SPOOF_PROTECTION: - ENABLE_SRS: 0 - ENABLE_OPENDKIM: 0 - ENABLE_OPENDMARC: 0 - ENABLE_POLICYD_SPF: 0 - ENABLE_POP3: - ENABLE_IMAP: 1 - ENABLE_CLAMAV: 0 - ENABLE_RSPAMD: 1 - ENABLE_RSPAMD_REDIS: 1 - RSPAMD_LEARN: 0 - RSPAMD_CHECK_AUTHENTICATED: 0 - RSPAMD_GREYLISTING: 0 - RSPAMD_HFILTER: 1 - RSPAMD_HFILTER_HOSTNAME_UNKNOWN_SCORE: 6 - ENABLE_AMAVIS: 0 - AMAVIS_LOGLEVEL: 0 - ENABLE_DNSBL: 0 - ENABLE_FAIL2BAN: 0 - FAIL2BAN_BLOCKTYPE: drop - ENABLE_MANAGESIEVE: - POSTSCREEN_ACTION: enforce - SMTP_ONLY: - SSL_TYPE: manual # This should always be manual because the chart just points to an existing - # tls secret (which may be setup manually or by cert-manager) - # These two values are automatically set by the chart based on the certificate key - # SSL_CERT_PATH: - # SSL_KEY_PATH: - SSL_ALT_CERT_PATH: - SSL_ALT_KEY_PATH: - VIRUSMAILS_DELETE_DELAY: - POSTFIX_DAGENT: - POSTFIX_MAILBOX_SIZE_LIMIT: - ENABLE_QUOTAS: 1 - POSTFIX_MESSAGE_SIZE_LIMIT: - CLAMAV_MESSAGE_SIZE_LIMIT: - PFLOGSUMM_TRIGGER: - PFLOGSUMM_RECIPIENT: - PFLOGSUMM_SENDER: - LOGWATCH_INTERVAL: - LOGWATCH_RECIPIENT: - LOGWATCH_SENDER: - REPORT_RECIPIENT: - REPORT_SENDER: - LOGROTATE_INTERVAL: weekly - POSTFIX_REJECT_UNKNOWN_CLIENT_HOSTNAME: 0 - POSTFIX_INET_PROTOCOLS: all - DOVECOT_INET_PROTOCOLS: all - - # ----------------------------------------------- - # --- SpamAssassin Section ---------------------- - # ----------------------------------------------- - ENABLE_SPAMASSASSIN: 0 - ENABLE_SPAMASSASSIN_KAM: 0 - SPAMASSASSIN_SPAM_TO_INBOX: 1 - MOVE_SPAM_TO_JUNK: 1 - MARK_SPAM_AS_READ: 0 - SA_TAG: 2.0 - SA_TAG2: 6.31 - SA_KILL: 10.0 - SA_SPAM_SUBJECT: '***SPAM*** ' - - # ----------------------------------------------- - # --- Fetchmail Section ------------------------- - # ----------------------------------------------- - ENABLE_FETCHMAIL: 0 - FETCHMAIL_POLL: 300 - FETCHMAIL_PARALLEL: 0 - ENABLE_GETMAIL: 0 - GETMAIL_POLL: 5 - - # ----------------------------------------------- - # --- LDAP Section ------------------------------ - # ----------------------------------------------- - LDAP_START_TLS: - LDAP_SERVER_HOST: - LDAP_SEARCH_BASE: - LDAP_BIND_DN: - LDAP_BIND_PW: - LDAP_QUERY_FILTER_USER: - LDAP_QUERY_FILTER_GROUP: - LDAP_QUERY_FILTER_ALIAS: - LDAP_QUERY_FILTER_DOMAIN: - - # ----------------------------------------------- - # --- Dovecot Section --------------------------- - # ----------------------------------------------- - DOVECOT_TLS: - DOVECOT_USER_FILTER: - DOVECOT_PASS_FILTER: - DOVECOT_MAILBOX_FORMAT: maildir - DOVECOT_AUTH_BIND: - - # ----------------------------------------------- - # --- Postgrey Section -------------------------- - # ----------------------------------------------- - ENABLE_POSTGREY: 0 - POSTGREY_DELAY: 300 - POSTGREY_MAX_AGE: 35 - POSTGREY_TEXT: "Delayed by Postgrey" - POSTGREY_AUTO_WHITELIST_CLIENTS: 5 - - # ----------------------------------------------- - # --- SASL Section ------------------------------ - # ----------------------------------------------- - ENABLE_SASLAUTHD: 0 - SASLAUTHD_MECHANISMS: - SASLAUTHD_MECH_OPTIONS: - SASLAUTHD_LDAP_SERVER: - SASLAUTHD_LDAP_BIND_DN: - SASLAUTHD_LDAP_PASSWORD: - SASLAUTHD_LDAP_SEARCH_BASE: - SASLAUTHD_LDAP_FILTER: - SASLAUTHD_LDAP_START_TLS: - SASLAUTHD_LDAP_TLS_CHECK_PEER: - SASLAUTHD_LDAP_TLS_CACERT_FILE: - SASLAUTHD_LDAP_TLS_CACERT_DIR: - SASLAUTHD_LDAP_PASSWORD_ATTR: - SASLAUTHD_LDAP_AUTH_METHOD: - SASLAUTHD_LDAP_MECH: - - # ----------------------------------------------- - # --- SRS Section ------------------------------- - # ----------------------------------------------- - SRS_SENDER_CLASSES: envelope_sender - SRS_EXCLUDE_DOMAINS: - SRS_SECRET: - - # ----------------------------------------------- - # --- Default Relay Host Section ---------------- - # ----------------------------------------------- - - DEFAULT_RELAY_HOST: - - # ----------------------------------------------- - # --- Multi-Domain Relay Section ---------------- - # ----------------------------------------------- - - RELAY_HOST: - RELAY_PORT: 25 - RELAY_USER: - RELAY_PASSWORD: - - # Whether to enable dovecot replication. Allows the syncronization of a pair of dovecot servers - # https://wiki.dovecot.org/Replication - enable_dovecot_replication: true - - securityContext: - runAsUser: 10001 - runAsGroup: 10001 - - containerSecurityContext: - readOnlyRootFilesystem: false # incompatible with the way docker-mailserver works - privileged: false + +deployment: + ## How many versions of the deployment to run on kubernetes + ## Default: 2 + replicas: 1 + + ## Add annotations to the deployment + ## Useful for using something like stash to backup data (https://stash.run/docs/v0.9.0-rc.0/guides/latest/auto-backup/workload/) + annotations: {} + + ## Host networking requested for this pod. Use the host’s network namespace. If this option is set, the ports that + ## will be used must be specified. + ## Ref: https://kubernetes.io/docs/api-reference/v1/definitions/#_v1_podspec + # pod.dockermailserver.hostNetwork will configure the pod to use the host's network namespace + hostNetwork: false + ## Use the host’s pid namespace + ## Ref: https://kubernetes.io/docs/api-reference/v1/definitions/#_v1_podspec + # pod.dockermailserver.hostPID defines whether the pod should use the host's PID namespace (default false) + hostPID: false + + ## Update strategy - only really applicable for deployments with RWO PVs attached + ## If replicas = 1, an update can get "stuck", as the previous pod remains attached to the + ## PV, and the "incoming" pod can never start. Setting the strategy to "Recreate" (our default) will + ## terminate the single previous pod, so that the new, incoming pod can attach to the PV + strategy: + # rollingUpdate: + # maxSurge: 1 + # maxUnavailable: 1 + type: "Recreate" + + ## The following variables affect the behaviour of docker-mailserver + ## See https://docker-mailserver.github.io/docker-mailserver/latest/config/environment/ for details + ## Note that an empty value indicates the default as described in the docs above + env: + # ----------------------------------------------- + # --- Required Section --------------------------- + # ----------------------------------------------- + OVERRIDE_HOSTNAME: mail.example.com # You must OVERRIDE this! + + # ----------------------------------------------- + # --- General Section --------------------------- + # ----------------------------------------------- + LOG_LEVEL: info + SUPERVISOR_LOGLEVEL: + ONE_DIR: 1 + DMS_VMAIL_UID: + DMS_VMAIL_GID: + ACCOUNT_PROVISIONER: + POSTMASTER_ADDRESS: + ENABLE_UPDATE_CHECK: 1 + UPDATE_CHECK_INTERVAL: 1d + PERMIT_DOCKER: none + TZ: + NETWORK_INTERFACE: + TLS_LEVEL: + SPOOF_PROTECTION: + ENABLE_SRS: 0 + ENABLE_OPENDKIM: 0 + ENABLE_OPENDMARC: 0 + ENABLE_POLICYD_SPF: 0 + ENABLE_POP3: + ENABLE_IMAP: 1 + ENABLE_CLAMAV: 0 + ENABLE_RSPAMD: 1 + ENABLE_RSPAMD_REDIS: 1 + RSPAMD_LEARN: 0 + RSPAMD_CHECK_AUTHENTICATED: 0 + RSPAMD_GREYLISTING: 0 + RSPAMD_HFILTER: 1 + RSPAMD_HFILTER_HOSTNAME_UNKNOWN_SCORE: 6 + ENABLE_AMAVIS: 0 + AMAVIS_LOGLEVEL: 0 + ENABLE_DNSBL: 0 + ENABLE_FAIL2BAN: 0 + FAIL2BAN_BLOCKTYPE: drop + ENABLE_MANAGESIEVE: + POSTSCREEN_ACTION: enforce + SMTP_ONLY: + SSL_TYPE: manual # This should always be manual because the chart just points to an existing + # tls secret (which may be setup manually or by cert-manager) + # These two values are automatically set by the chart based on the certificate key + # SSL_CERT_PATH: + # SSL_KEY_PATH: + SSL_ALT_CERT_PATH: + SSL_ALT_KEY_PATH: + VIRUSMAILS_DELETE_DELAY: + POSTFIX_DAGENT: + POSTFIX_MAILBOX_SIZE_LIMIT: + ENABLE_QUOTAS: 1 + POSTFIX_MESSAGE_SIZE_LIMIT: + CLAMAV_MESSAGE_SIZE_LIMIT: + PFLOGSUMM_TRIGGER: + PFLOGSUMM_RECIPIENT: + PFLOGSUMM_SENDER: + LOGWATCH_INTERVAL: + LOGWATCH_RECIPIENT: + LOGWATCH_SENDER: + REPORT_RECIPIENT: + REPORT_SENDER: + LOGROTATE_INTERVAL: weekly + POSTFIX_REJECT_UNKNOWN_CLIENT_HOSTNAME: 0 + POSTFIX_INET_PROTOCOLS: all + DOVECOT_INET_PROTOCOLS: all + + # ----------------------------------------------- + # --- SpamAssassin Section ---------------------- + # ----------------------------------------------- + ENABLE_SPAMASSASSIN: 0 + ENABLE_SPAMASSASSIN_KAM: 0 + SPAMASSASSIN_SPAM_TO_INBOX: 1 + MOVE_SPAM_TO_JUNK: 1 + MARK_SPAM_AS_READ: 0 + SA_TAG: 2.0 + SA_TAG2: 6.31 + SA_KILL: 10.0 + SA_SPAM_SUBJECT: '***SPAM*** ' + + # ----------------------------------------------- + # --- Fetchmail Section ------------------------- + # ----------------------------------------------- + ENABLE_FETCHMAIL: 0 + FETCHMAIL_POLL: 300 + FETCHMAIL_PARALLEL: 0 + ENABLE_GETMAIL: 0 + GETMAIL_POLL: 5 + + # ----------------------------------------------- + # --- LDAP Section ------------------------------ + # ----------------------------------------------- + LDAP_START_TLS: + LDAP_SERVER_HOST: + LDAP_SEARCH_BASE: + LDAP_BIND_DN: + LDAP_BIND_PW: + LDAP_QUERY_FILTER_USER: + LDAP_QUERY_FILTER_GROUP: + LDAP_QUERY_FILTER_ALIAS: + LDAP_QUERY_FILTER_DOMAIN: + + # ----------------------------------------------- + # --- Dovecot Section --------------------------- + # ----------------------------------------------- + DOVECOT_TLS: + DOVECOT_USER_FILTER: + DOVECOT_PASS_FILTER: + DOVECOT_MAILBOX_FORMAT: maildir + DOVECOT_AUTH_BIND: + + # ----------------------------------------------- + # --- Postgrey Section -------------------------- + # ----------------------------------------------- + ENABLE_POSTGREY: 0 + POSTGREY_DELAY: 300 + POSTGREY_MAX_AGE: 35 + POSTGREY_TEXT: "Delayed by Postgrey" + POSTGREY_AUTO_WHITELIST_CLIENTS: 5 + + # ----------------------------------------------- + # --- SASL Section ------------------------------ + # ----------------------------------------------- + ENABLE_SASLAUTHD: 0 + SASLAUTHD_MECHANISMS: + SASLAUTHD_MECH_OPTIONS: + SASLAUTHD_LDAP_SERVER: + SASLAUTHD_LDAP_BIND_DN: + SASLAUTHD_LDAP_PASSWORD: + SASLAUTHD_LDAP_SEARCH_BASE: + SASLAUTHD_LDAP_FILTER: + SASLAUTHD_LDAP_START_TLS: + SASLAUTHD_LDAP_TLS_CHECK_PEER: + SASLAUTHD_LDAP_TLS_CACERT_FILE: + SASLAUTHD_LDAP_TLS_CACERT_DIR: + SASLAUTHD_LDAP_PASSWORD_ATTR: + SASLAUTHD_LDAP_AUTH_METHOD: + SASLAUTHD_LDAP_MECH: + + # ----------------------------------------------- + # --- SRS Section ------------------------------- + # ----------------------------------------------- + SRS_SENDER_CLASSES: envelope_sender + SRS_EXCLUDE_DOMAINS: + SRS_SECRET: + + # ----------------------------------------------- + # --- Default Relay Host Section ---------------- + # ----------------------------------------------- + + DEFAULT_RELAY_HOST: + + # ----------------------------------------------- + # --- Multi-Domain Relay Section ---------------- + # ----------------------------------------------- + + RELAY_HOST: + RELAY_PORT: 25 + RELAY_USER: + RELAY_PASSWORD: + + # Whether to enable dovecot replication. Allows the syncronization of a pair of dovecot servers + # https://wiki.dovecot.org/Replication + enable_dovecot_replication: true + + securityContext: + runAsUser: 10001 + runAsGroup: 10001 + + containerSecurityContext: + readOnlyRootFilesystem: false # incompatible with the way docker-mailserver works + privileged: false service: ## What scope the service should be exposed in. One of: @@ -337,16 +340,6 @@ service: # hostName: annotations: {} -deployment: - - ## How many versions of the deployment to run on kubernetes - ## Default: 2 - replicas: 1 - - ## Add annotations to the deployment - ## Useful for using something like stash to backup data (https://stash.run/docs/v0.9.0-rc.0/guides/latest/auto-backup/workload/) - annotations: {} - ## More generally, a "request" can be thought of as "how much is this container expected to need usually". it should be ## possible to burst outside these constraints (during a high load operation). However, Kubernetes may kill the pod ## if the node is under too higher load and the burst is outside its request From d050a292f82b606e8d67c65948a8d8d3a39c8485 Mon Sep 17 00:00:00 2001 From: Charlie Savage Date: Sun, 14 Jan 2024 23:13:43 -0800 Subject: [PATCH 20/66] Update PVC to allow specify a volume name. Also list all supported keys so users know they exist. --- charts/docker-mailserver/templates/pvc.yaml | 5 ++++- charts/docker-mailserver/values.yaml | 6 ++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/charts/docker-mailserver/templates/pvc.yaml b/charts/docker-mailserver/templates/pvc.yaml index fba93734..16eb3a52 100644 --- a/charts/docker-mailserver/templates/pvc.yaml +++ b/charts/docker-mailserver/templates/pvc.yaml @@ -10,7 +10,7 @@ metadata: {{ end }} spec: accessModes: - - {{ default "ReadWriteOnce" .Values.persistence.accessMode | quote }} + - {{ .Values.persistence.accessMode }} {{- if .Values.persistence.storageClass }} storageClassName: {{ .Values.persistence.storageClass | quote }} {{- end }} @@ -21,4 +21,7 @@ spec: selector: {{ toYaml .Values.persistence.selector | indent 4 }} {{ end }} + {{- if .Values.persistence.volumeName }} + volumeName: {{ .Values.persistence.volumeName }} + {{ end }} {{- end }} diff --git a/charts/docker-mailserver/values.yaml b/charts/docker-mailserver/values.yaml index 4bb63d63..8690c850 100644 --- a/charts/docker-mailserver/values.yaml +++ b/charts/docker-mailserver/values.yaml @@ -371,10 +371,16 @@ resources: ephemeral-storage: "500Mi" persistence: + # existingClaim: # Specify an existing PVC to use size: "10Gi" # Uncomment the backup.kubernetes.io/deltas annotation below if you use https://github.com/miracle2k/k8s-snapshots annotations: {} # backup.kubernetes.io/deltas: PT1H P2D P30D P180D + accessMode: ReadWriteOnce + # storageClass: + # selector: + # Specify a volumeName, otherwise a new volume will be dynamically provisioned + # volumeName: my-volume ## Monitoring adds the prometheus.io annotations to pods and services, so that the Prometheus Kubernetes SD mechanism ## as configured in the examples will automatically discover both the pods and the services to query. From b8064e31982b747006e1d7d916cb747114c05b19 Mon Sep 17 00:00:00 2001 From: Charlie Savage Date: Sun, 14 Jan 2024 23:42:29 -0800 Subject: [PATCH 21/66] Automatically set SSL_TYPE if certificate secret is set --- charts/docker-mailserver/templates/deployment.yaml | 2 ++ charts/docker-mailserver/values.yaml | 5 ++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/charts/docker-mailserver/templates/deployment.yaml b/charts/docker-mailserver/templates/deployment.yaml index 8fe95df5..954b1c3d 100644 --- a/charts/docker-mailserver/templates/deployment.yaml +++ b/charts/docker-mailserver/templates/deployment.yaml @@ -85,6 +85,8 @@ spec: {{- end }} {{- if .Values.certificate }} + - name: SSL_TYPE + value: manual - name: SSL_CERT_PATH value: /tmp/dms/custom-certs/tls.crt - name: SSL_KEY_PATH diff --git a/charts/docker-mailserver/values.yaml b/charts/docker-mailserver/values.yaml index 8690c850..5491b0ec 100644 --- a/charts/docker-mailserver/values.yaml +++ b/charts/docker-mailserver/values.yaml @@ -183,9 +183,8 @@ deployment: ENABLE_MANAGESIEVE: POSTSCREEN_ACTION: enforce SMTP_ONLY: - SSL_TYPE: manual # This should always be manual because the chart just points to an existing - # tls secret (which may be setup manually or by cert-manager) - # These two values are automatically set by the chart based on the certificate key + # These values are automatically set by the chart based on the certificate key + # SSL_TYPE: # SSL_CERT_PATH: # SSL_KEY_PATH: SSL_ALT_CERT_PATH: From c7885e383e1e3505b31dfcff498fc8c6f40dfa5e Mon Sep 17 00:00:00 2001 From: Charlie Savage Date: Mon, 15 Jan 2024 21:16:09 -0800 Subject: [PATCH 22/66] Fix proxy protocol support --- charts/docker-mailserver/config/dovecot.cf | 10 +++++----- charts/docker-mailserver/config/postfix-main.cf | 2 +- charts/docker-mailserver/templates/service.yaml | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/charts/docker-mailserver/config/dovecot.cf b/charts/docker-mailserver/config/dovecot.cf index 114cfb69..15c37645 100644 --- a/charts/docker-mailserver/config/dovecot.cf +++ b/charts/docker-mailserver/config/dovecot.cf @@ -1,17 +1,17 @@ {{- if .Values.proxy_protocol.enabled }} -proxy_protocol_trusted_networks = {{ .Values.proxy_protocol.trustedNetworks }} +haproxy_trusted_networks = {{ .Values.proxy_protocol.trustedNetworks }} {{- if and (.Values.deployment.env.ENABLE_IMAP) (not .Values.deployment.env.SMTP_ONLY) }} service imap-login { inet_listener imap { - proxy_protocol = yes + haproxy = yes } # Provide a port for internal cluster clients that don't use the Proxy protocol inet_listener imap-no-proxy { port = 10143 } inet_listener imaps { - proxy_protocol = yes + haproxy = yes } # Provide a port for internal cluster clients that don't use the Proxy protocol inet_listener imaps-no-proxy { @@ -23,14 +23,14 @@ service imap-login { {{- if and (.Values.deployment.env.ENABLE_POP) (not .Values.deployment.env.SMTP_ONLY) }} service pop3-login { inet_listener pop3 { - proxy_protocol = yes + haproxy = yes } # Provide a port for internal cluster clients that don't use the Proxy protocol inet_listener pop3-no-proxy { port = 10110 } inet_listener pop3s { - proxy_protocol = yes + haproxy = yes } # Provide a port for internal cluster clients that don't use the Proxy protocol inet_listener pop3s-no-proxy { diff --git a/charts/docker-mailserver/config/postfix-main.cf b/charts/docker-mailserver/config/postfix-main.cf index 190b85f1..c38776b1 100644 --- a/charts/docker-mailserver/config/postfix-main.cf +++ b/charts/docker-mailserver/config/postfix-main.cf @@ -1,5 +1,5 @@ {{- if .Values.proxy_protocol.enabled -}} - postscreen_upstream_proxy_protocol = haproxy + postscreen_upstream_haproxy = haproxy {{- end }} {{- if not .Values.spfTestsDisabled -}} smtpd_recipient_restrictions = permit_sasl_authenticated, permit_mynetworks, reject_unauth_destination, reject_unauth_pipelining, reject_invalid_helo_hostname, reject_non_fqdn_helo_hostname, reject_unknown_recipient_domain, reject_rbl_client zen.spamhaus.org, reject_rbl_client bl.spamcop.net{{ range .Values.rblRejectDomains }}, reject_rbl_client {{ . }}{{ end }} diff --git a/charts/docker-mailserver/templates/service.yaml b/charts/docker-mailserver/templates/service.yaml index 23a535b4..071b3c91 100644 --- a/charts/docker-mailserver/templates/service.yaml +++ b/charts/docker-mailserver/templates/service.yaml @@ -65,7 +65,7 @@ spec: nodePort: {{ default "30993" .Values.service.nodePort.imaps }} {{- end }} - {{- if .Values.proxy_protocol.enabled -}} + {{- if .Values.proxy_protocol.enabled }} - name: imap-no-proxy targetPort: imap port: 10143 @@ -88,7 +88,7 @@ spec: {{- if eq .Values.service.type "NodePort" }} nodePort: {{ default "30995" .Values.service.nodePort.pop3s }} {{- end }} - {{- if .Values.proxy_protocol.enabled -}} + {{- if .Values.proxy_protocol.enabled }} - name: pop3-no-proxy targetPort: imap port: 10110 From e621bd543466d848211b9dea42531aaafade6691 Mon Sep 17 00:00:00 2001 From: Charlie Savage Date: Wed, 17 Jan 2024 00:05:10 -0800 Subject: [PATCH 23/66] More work on proxy support --- charts/docker-mailserver/config/dovecot.cf | 40 +++++--- .../docker-mailserver/config/postfix-main.cf | 9 +- .../docker-mailserver/config/user-patches.sh | 39 ++++++++ .../templates/deployment.yaml | 92 ++++++++++++------- .../docker-mailserver/templates/service.yaml | 42 +++++---- 5 files changed, 150 insertions(+), 72 deletions(-) create mode 100644 charts/docker-mailserver/config/user-patches.sh diff --git a/charts/docker-mailserver/config/dovecot.cf b/charts/docker-mailserver/config/dovecot.cf index 15c37645..c2789bc0 100644 --- a/charts/docker-mailserver/config/dovecot.cf +++ b/charts/docker-mailserver/config/dovecot.cf @@ -4,18 +4,24 @@ haproxy_trusted_networks = {{ .Values.proxy_protocol.trustedNetworks }} {{- if and (.Values.deployment.env.ENABLE_IMAP) (not .Values.deployment.env.SMTP_ONLY) }} service imap-login { inet_listener imap { - haproxy = yes - } - # Provide a port for internal cluster clients that don't use the Proxy protocol - inet_listener imap-no-proxy { - port = 10143 + port = 143 } + inet_listener imaps { + port = 993 + ssl = yes + } + + inet_listener imap_proxy { haproxy = yes + port = 10143 + ssl = no } - # Provide a port for internal cluster clients that don't use the Proxy protocol - inet_listener imaps-no-proxy { + + inet_listener imaps_proxy { + haproxy = yes port = 10993 + ssl = yes } } {{- end -}} @@ -23,18 +29,24 @@ service imap-login { {{- if and (.Values.deployment.env.ENABLE_POP) (not .Values.deployment.env.SMTP_ONLY) }} service pop3-login { inet_listener pop3 { - haproxy = yes + port = 110 } - # Provide a port for internal cluster clients that don't use the Proxy protocol - inet_listener pop3-no-proxy { + + inet_listener pop3s { + port = 995 + ssl = yes + } + + inet_listener pop3_proxy { + haproxy = yes port = 10110 + ssl = no } - inet_listener pop3s { + + inet_listener pop3s_proxy { haproxy = yes - } - # Provide a port for internal cluster clients that don't use the Proxy protocol - inet_listener pop3s-no-proxy { port = 10995 + ssl = yes } } {{- end -}} diff --git a/charts/docker-mailserver/config/postfix-main.cf b/charts/docker-mailserver/config/postfix-main.cf index c38776b1..24bbe03e 100644 --- a/charts/docker-mailserver/config/postfix-main.cf +++ b/charts/docker-mailserver/config/postfix-main.cf @@ -1,6 +1,3 @@ - {{- if .Values.proxy_protocol.enabled -}} - postscreen_upstream_haproxy = haproxy - {{- end }} - {{- if not .Values.spfTestsDisabled -}} - smtpd_recipient_restrictions = permit_sasl_authenticated, permit_mynetworks, reject_unauth_destination, reject_unauth_pipelining, reject_invalid_helo_hostname, reject_non_fqdn_helo_hostname, reject_unknown_recipient_domain, reject_rbl_client zen.spamhaus.org, reject_rbl_client bl.spamcop.net{{ range .Values.rblRejectDomains }}, reject_rbl_client {{ . }}{{ end }} - {{ end -}} +{{- if not .Values.spfTestsDisabled }} + smtpd_recipient_restrictions = permit_sasl_authenticated, permit_mynetworks, reject_unauth_destination, reject_unauth_pipelining, reject_invalid_helo_hostname, reject_non_fqdn_helo_hostname, reject_unknown_recipient_domain, reject_rbl_client zen.spamhaus.org, reject_rbl_client bl.spamcop.net{{ range .Values.rblRejectDomains }}, reject_rbl_client {{ . }}{{ end }} +{{ end -}} diff --git a/charts/docker-mailserver/config/user-patches.sh b/charts/docker-mailserver/config/user-patches.sh new file mode 100644 index 00000000..040dd27c --- /dev/null +++ b/charts/docker-mailserver/config/user-patches.sh @@ -0,0 +1,39 @@ +#!/bin/bash + +{{- if .Values.proxy_protocol.enabled }} +# Make sure to keep this file in sync with https://github.com/docker-mailserver/docker-mailserver/blob/master/target/postfix/master.cf! +cat <> /etc/postfix/master.cf + +# Submission with proxy +10587 inet n - n - - smtpd + -o syslog_name=postfix/submission + -o smtpd_tls_security_level=encrypt + -o smtpd_sasl_auth_enable=yes + -o smtpd_sasl_type=dovecot + -o smtpd_reject_unlisted_recipient=no + -o smtpd_sasl_authenticated_header=yes + -o smtpd_client_restrictions=permit_sasl_authenticated,reject + -o smtpd_relay_restrictions=permit_sasl_authenticated,reject + -o smtpd_sender_restrictions=\$mua_sender_restrictions + -o smtpd_discard_ehlo_keywords= + -o milter_macro_daemon_name=ORIGINATING + -o cleanup_service_name=sender-cleanup + -o smtpd_upstream_proxy_protocol=haproxy + +# Submissions with proxy +10465 inet n - n - - smtpd + -o syslog_name=postfix/submissions + -o smtpd_tls_wrappermode=yes + -o smtpd_sasl_auth_enable=yes + -o smtpd_sasl_type=dovecot + -o smtpd_reject_unlisted_recipient=no + -o smtpd_sasl_authenticated_header=yes + -o smtpd_client_restrictions=permit_sasl_authenticated,reject + -o smtpd_relay_restrictions=permit_sasl_authenticated,reject + -o smtpd_sender_restrictions=\$mua_sender_restrictions + -o smtpd_discard_ehlo_keywords= + -o milter_macro_daemon_name=ORIGINATING + -o cleanup_service_name=sender-cleanup + -o smtpd_upstream_proxy_protocol=haproxy +EOS +{{- end }} \ No newline at end of file diff --git a/charts/docker-mailserver/templates/deployment.yaml b/charts/docker-mailserver/templates/deployment.yaml index 954b1c3d..4611c3af 100644 --- a/charts/docker-mailserver/templates/deployment.yaml +++ b/charts/docker-mailserver/templates/deployment.yaml @@ -35,30 +35,30 @@ spec: securityContext: {{ toYaml .Values.securityContext | indent 8 }} volumes: - - name: "data" + - name: data persistentVolumeClaim: claimName: {{ template "dockermailserver.fullname" . }} - - name: "config" - emptyDir: {} + {{- if .Values.configs.installDefault }} # Default ConfigMap - name: {{ template "dockermailserver.fullname" . }}-configs configMap: name: {{ template "dockermailserver.fullname" . }}-configs + {{- end }} - # User ConfigMaps - {{- range $config := .Values.configs }} + # Custom ConfigMaps + {{- range $config := .Values.configs.custom }} - name: {{ regexReplaceAll "[.]" $config.name "-" }}-config configMap: name: {{ $config.name }} {{- end }} # Default Secret - - name: "secrets" + - name: secrets secret: secretName: {{ template "dockermailserver.fullname" . }}-secrets - # User Secrets + # Custom Secrets {{- range $secret := .Values.secrets }} - name: {{ regexReplaceAll "[.]" $secret.name "-" }}-secret secret: @@ -66,15 +66,15 @@ spec: {{- end }} {{- if .Values.certificate }} - - name: "certificate" + - name: certificate secret: secretName: {{ .Values.certificate }} {{- end }} -{{- if .Values.additionalVolumes }} + {{- if .Values.additionalVolumes }} # Additional Volumes {{ toYaml .Values.additionalVolumes | indent 8 }} -{{- end }} + {{- end }} containers: - name: docker-mailserver @@ -123,18 +123,20 @@ spec: subPath: mail-state ### Default ConfigMap ### - {{- /* Mount configuration files stored in the built-in ConfigMap into the container */}} - {{- range $path, $content := .Files.Glob "config/**" }} - {{- if and (gt (len $content) 0) (not (contains "private" $path)) (not (contains "public" $path)) }} + {{- if .Values.configs.installDefault }} + {{- /* Mount configuration files stored in the built-in ConfigMap into the container */}} + {{- range $path, $content := .Files.Glob "config/**" }} + {{- if and (gt (len $content) 0) (not (contains "private" $path)) (not (contains "public" $path)) }} # {{ $path }} - name: {{ template "dockermailserver.fullname" $ }}-configs subPath: {{ regexReplaceAll "[^-_.\\w]" $path "." }} mountPath: /tmp/docker-mailserver/{{ $path | trimPrefix "config/" }} + {{- end }} {{- end }} {{- end }} - ### User ConfigMaps ### - {{- range $config := .Values.configs }} + ### Custom ConfigMaps ### + {{- range $config := .Values.configs.custom }} {{- range $item := $config.data }} # {{ $item.subPath }} - name: {{ regexReplaceAll "[.]" $config.name "-" }}-config @@ -154,7 +156,7 @@ spec: {{- end }} {{- end }} - ### User Secrets ### + ### Custom Secrets ### {{- range $secret := .Values.secrets }} {{- range $item := $secret.data }} # {{ $item.subPath }} @@ -164,9 +166,10 @@ spec: {{- end }} {{- end }} -{{- if .Values.additionalVolumeMounts }} + {{- if .Values.additionalVolumeMounts }} + # Additional VolumeMounts {{ toYaml .Values.additionalVolumeMounts | indent 12 }} -{{- end }} + {{- end }} livenessProbe: exec: command: @@ -186,36 +189,55 @@ spec: timeoutSeconds: 5 failureThreshold: 3 ports: - - name: "smtp" + - name: smtp containerPort: 25 - - name: "smtps" + + - name: submissions containerPort: 465 - - name: "submission" + - name: submission containerPort: 587 + {{- if .Values.proxy_protocol.enabled }} + - name: subs-proxy + containerPort: 10465 + - name: sub-proxy + containerPort: 10587 + {{- end }} - {{- if and (.Values.deployment.env.ENABLE_IMAP) (not .Values.deployment.env.SMTP_ONLY) }} - - name: "imap" + {{- if and (.Values.deployment.env.ENABLE_IMAP) (not .Values.deployment.env.SMTP_ONLY) }} + - name: imap containerPort: 143 - - name: "imaps" + - name: imaps containerPort: 993 - {{- end }} + {{- if .Values.proxy_protocol.enabled }} + - name: imap-proxy + containerPort: 10143 + - name: imaps-proxy + containerPort: 10993 + {{- end }} + {{- end }} - {{- if and (.Values.deployment.env.ENABLE_POP) (not .Values.deployment.env.SMTP_ONLY) }} - - name: "pop3" + {{- if and (.Values.deployment.env.ENABLE_POP) (not .Values.deployment.env.SMTP_ONLY) }} + - name: pop3 containerPort: 110 - - name: "pop3s" + - name: pop3s containerPort: 995 - {{- end }} + {{- if .Values.proxy_protocol.enabled }} + - name: pop3-proxy + containerPort: 10110 + - name: pop3s-proxy + containerPort: 10995 + {{- end }} + {{- end }} - {{- if .Values.deployment.env.ENABLE_RSPAMD }} - - name: "rspamd" + {{- if .Values.deployment.env.ENABLE_RSPAMD }} + - name: rspamd containerPort: 11334 - {{- end }} + {{- end }} - {{- if and (.Values.deployment.env.ENABLE_MANAGESIEVE) (not .Values.deployment.env.SMTP_ONLY) }} - - name: "managesieve" + {{- if and (.Values.deployment.env.ENABLE_MANAGESIEVE) (not .Values.deployment.env.SMTP_ONLY) }} + - name: managesieve containerPort: 4190 - {{- end }} + {{- end }} {{- if .Values.metrics.enabled }} - name: metrics-exporter diff --git a/charts/docker-mailserver/templates/service.yaml b/charts/docker-mailserver/templates/service.yaml index 071b3c91..daa82a03 100644 --- a/charts/docker-mailserver/templates/service.yaml +++ b/charts/docker-mailserver/templates/service.yaml @@ -38,8 +38,9 @@ spec: {{- if eq .Values.service.type "NodePort" }} nodePort: {{ default "30025" .Values.service.nodePort.smtp }} {{- end }} - - name: smtps - targetPort: smtps + + - name: submissions + targetPort: submissions port: 465 {{- if eq .Values.service.type "NodePort" }} nodePort: {{ default "30465" .Values.service.nodePort.smtps }} @@ -49,9 +50,17 @@ spec: port: 587 {{- if eq .Values.service.type "NodePort" }} nodePort: {{ default "30587" .Values.service.nodePort.submission }} - {{ end }} - - {{- if and (.Values.deployment.env.ENABLE_IMAP) (not .Values.deployment.env.SMTP_ONLY) }} + {{ end }} + {{- if .Values.proxy_protocol.enabled }} + - name: subs-proxy + targetPort: subs-proxy + port: 10465 + - name: sub-proxy + targetPort: sub-proxy + port: 10587 + {{- end }} + + {{- if and (.Values.deployment.env.ENABLE_IMAP) (not .Values.deployment.env.SMTP_ONLY) }} - name: imap targetPort: imap port: 143 @@ -64,18 +73,17 @@ spec: {{- if eq .Values.service.type "NodePort" }} nodePort: {{ default "30993" .Values.service.nodePort.imaps }} {{- end }} - {{- if .Values.proxy_protocol.enabled }} - - name: imap-no-proxy - targetPort: imap + - name: imap-proxy + targetPort: imap-proxy port: 10143 - - name: imaps-no-proxy - targetPort: imaps + - name: imaps-proxy + targetPort: imaps-proxy port: 10993 {{- end }} - {{- end }} + {{- end }} - {{- if and (.Values.deployment.env.ENABLE_POP) (not .Values.deployment.env.SMTP_ONLY) }} + {{- if and (.Values.deployment.env.ENABLE_POP) (not .Values.deployment.env.SMTP_ONLY) }} - name: pop3 targetPort: pop3 port: 110 @@ -89,14 +97,14 @@ spec: nodePort: {{ default "30995" .Values.service.nodePort.pop3s }} {{- end }} {{- if .Values.proxy_protocol.enabled }} - - name: pop3-no-proxy - targetPort: imap + - name: pop3-proxy + targetPort: pop3-proxy port: 10110 - - name: pop3s-no-proxy - targetPort: imaps + - name: pop3s-proxy + targetPort: pop3s-proxy port: 10995 {{- end }} - {{- end }} + {{- end }} {{- if .Values.deployment.env.ENABLE_RSPAMD }} - name: rspamd From 8959829c7d48329001f4379a199a4746bd603593 Mon Sep 17 00:00:00 2001 From: Charlie Savage Date: Wed, 17 Jan 2024 00:05:28 -0800 Subject: [PATCH 24/66] Update how .config key works --- .../templates/configmap-user.yaml | 2 +- .../templates/configmap.yaml | 8 ++-- charts/docker-mailserver/values.yaml | 40 +++++++++---------- 3 files changed, 26 insertions(+), 24 deletions(-) diff --git a/charts/docker-mailserver/templates/configmap-user.yaml b/charts/docker-mailserver/templates/configmap-user.yaml index 0ae36386..be114c35 100644 --- a/charts/docker-mailserver/templates/configmap-user.yaml +++ b/charts/docker-mailserver/templates/configmap-user.yaml @@ -1,4 +1,4 @@ -{{- range $config := .Values.configs }} +{{- range $config := .Values.configs.custom }} {{- if $config.create }} apiVersion: "v1" kind: "ConfigMap" diff --git a/charts/docker-mailserver/templates/configmap.yaml b/charts/docker-mailserver/templates/configmap.yaml index 36fd00f1..5f3a41a6 100644 --- a/charts/docker-mailserver/templates/configmap.yaml +++ b/charts/docker-mailserver/templates/configmap.yaml @@ -1,3 +1,4 @@ +{{- if .Values.configs.installDefault }} apiVersion: "v1" kind: "ConfigMap" metadata: @@ -10,11 +11,12 @@ metadata: data: {{/* Copy files from config subdirectory into a config map. The key for each file is its path stripped of the leading "/config" and then base 64 encoded. */}} -{{- range $path, $content := .Files.Glob "config/**" }} - {{/* Skip empty files and public / private keys */}} - {{- if and (gt (len $content) 0) (not (contains "private" $path)) (not (contains "public" $path)) }} + {{- range $path, $content := .Files.Glob "config/**" }} + {{/* Skip empty files and public / private keys */}} + {{- if and (gt (len $content) 0) (not (contains "private" $path)) (not (contains "public" $path)) }} # {{ $path }} {{ regexReplaceAll "[^-_.\\w]" $path "." }}: | {{ tpl ($.Files.Get $path) $ | indent 6 }} + {{- end }} {{- end }} {{- end }} diff --git a/charts/docker-mailserver/values.yaml b/charts/docker-mailserver/values.yaml index 5491b0ec..69cf126e 100644 --- a/charts/docker-mailserver/values.yaml +++ b/charts/docker-mailserver/values.yaml @@ -27,30 +27,31 @@ serviceAccount: ## files by either referencing existing ConfigMaps (that you create before installing the Chart) ## or by creating new ones (set the create key to true). ## -## configs: -## - name: postfix.example.com -## create: true -## data: -## - subPath: postfix-accounts.cf -## mountPath: postfix-accounts.cf -## content: | -## # A sample user - the password is "password" -## user@example.com|{SHA512-CRYPT}$6$l4023rZnQEy/l0Rg$JeNjAAICB43VAX7GTJ9jeE7DR0LeyR5nU.ftq3c42T5E1IZSuRBqwM8erRh6t0CyIT6aYpBIAopzcQHNUvMV00 +configs: + installDefault: true # Install the built-in config files +## custom: +## - name: postfix.example.com +## create: true +## data: +## - subPath: postfix-accounts.cf +## mountPath: postfix-accounts.cf +## content: | +## # A sample user - the password is "password" +## user@example.com|{SHA512-CRYPT}$6$l4023rZnQEy/l0Rg$JeNjAAICB43VAX7GTJ9jeE7DR0LeyR5nU.ftq3c42T5E1IZSuRBqwM8erRh6t0CyIT6aYpBIAopzcQHNUvMV00 ## -## - name: dovecot.example.com # Name of the ConfigMap -## create: true # Whether to create the ConfigMap (if true, content must be specified) -## data: -## - subPath: dovecot-masters.cf -## mountPath: dovecot-masters.cf # Relative path to /tmp/docker-mailserver/ -## content: | -## # A sample user - the password is "password" -## user@example.com|{SHA512-CRYPT}$6$l4023rZnQEy/l0Rg$JeNjAAICB43VAX7GTJ9jeE7DR0LeyR5nU.ftq3c42T5E1IZSuRBqwM8erRh6t0CyIT6aYpBIAopzcQHNUvMV00 +## - name: dovecot.example.com # Name of the ConfigMap +## create: true # Whether to create the ConfigMap (if true, content must be specified) +## data: +## - subPath: dovecot-masters.cf +## mountPath: dovecot-masters.cf # Relative path to /tmp/docker-mailserver/ +## content: | +## # A sample user - the password is "password" +## user@example.com|{SHA512-CRYPT}$6$l4023rZnQEy/l0Rg$JeNjAAICB43VAX7GTJ9jeE7DR0LeyR5nU.ftq3c42T5E1IZSuRBqwM8erRh6t0CyIT6aYpBIAopzcQHNUvMV00 ## ## If you set the create key to false, then you must manually create the ConfigMaps before deploying the chart. ## ## kubectl create configmap postfix.example.com --namespace mail --from-file=postfix-accounts.cf= ## -configs: [] ## The secrets key works the same way as the configs key. Use secrets to store sensitive information, ## such as DKIM signing keys. @@ -81,7 +82,7 @@ certificate: #- name: host-config-files # hostPath: # path: /tmp/docker-mailserver-config # Directory on host -# type: Directory +# type: DirectoryOrCreate #additionalVolumeMounts: #- name: host-config-files @@ -105,7 +106,6 @@ livenessTests: commands: - "clamscan /tmp/docker-mailserver/TrustedHosts" - deployment: ## How many versions of the deployment to run on kubernetes ## Default: 2 From 4a533a5e34f29e3f3826eab4e16741f97e1a977c Mon Sep 17 00:00:00 2001 From: Charlie Savage Date: Sun, 21 Jan 2024 15:40:22 -0800 Subject: [PATCH 25/66] Remove separate config files - migrate to values.yaml --- .../config/80-replication.conf | 33 ------------ .../config/91-override-sieve.conf | 4 -- .../docker-mailserver/config/am-i-healthy.sh | 7 --- charts/docker-mailserver/config/dovecot.cf | 53 ------------------- .../docker-mailserver/config/postfix-main.cf | 3 -- .../docker-mailserver/config/user-patches.sh | 39 -------------- .../templates/configmap-user.yaml | 20 ------- .../templates/configmap.yaml | 29 +++++----- .../templates/secret-user.yaml | 20 ------- .../docker-mailserver/templates/secret.yaml | 24 +++++---- 10 files changed, 25 insertions(+), 207 deletions(-) delete mode 100644 charts/docker-mailserver/config/80-replication.conf delete mode 100644 charts/docker-mailserver/config/91-override-sieve.conf delete mode 100644 charts/docker-mailserver/config/am-i-healthy.sh delete mode 100644 charts/docker-mailserver/config/dovecot.cf delete mode 100644 charts/docker-mailserver/config/postfix-main.cf delete mode 100644 charts/docker-mailserver/config/user-patches.sh delete mode 100644 charts/docker-mailserver/templates/configmap-user.yaml delete mode 100644 charts/docker-mailserver/templates/secret-user.yaml diff --git a/charts/docker-mailserver/config/80-replication.conf b/charts/docker-mailserver/config/80-replication.conf deleted file mode 100644 index 6b49010b..00000000 --- a/charts/docker-mailserver/config/80-replication.conf +++ /dev/null @@ -1,33 +0,0 @@ -mail_plugins = $mail_plugins notify replication - -service replicator { - process_min_avail = 1 - unix_listener replicator-doveadm { - mode = 0600 - user = docker - } -} - -service aggregator { - fifo_listener replication-notify-fifo { - user = docker - } - unix_listener replication-notify { - user = docker - } -} - -doveadm_port = 4117 -doveadm_password = secret - -service doveadm { - inet_listener { - port = 4117 - ssl = yes - } -} - -plugin { - #mail_replica = tcp:anotherhost.example.com # use doveadm_port - #mail_replica = tcp:anotherhost.example.com:12345 # use port 12345 explicitly -} diff --git a/charts/docker-mailserver/config/91-override-sieve.conf b/charts/docker-mailserver/config/91-override-sieve.conf deleted file mode 100644 index c5bec151..00000000 --- a/charts/docker-mailserver/config/91-override-sieve.conf +++ /dev/null @@ -1,4 +0,0 @@ -plugin { - sieve = /var/mail/sieve/%d/%n/.dovecot.sieve - sieve_dir = /var/mail/sieve/%d/%n/sieve -} diff --git a/charts/docker-mailserver/config/am-i-healthy.sh b/charts/docker-mailserver/config/am-i-healthy.sh deleted file mode 100644 index 3f1b8b0a..00000000 --- a/charts/docker-mailserver/config/am-i-healthy.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/bash -# this script is intended to be used by periodic kubernetes liveness probes to ensure that the container -# (and all its dependent services) is healthy -{{ range .Values.livenessTests.commands -}} -{{ . }} && \ -{{- end }} -echo "All healthy" diff --git a/charts/docker-mailserver/config/dovecot.cf b/charts/docker-mailserver/config/dovecot.cf deleted file mode 100644 index c2789bc0..00000000 --- a/charts/docker-mailserver/config/dovecot.cf +++ /dev/null @@ -1,53 +0,0 @@ -{{- if .Values.proxy_protocol.enabled }} -haproxy_trusted_networks = {{ .Values.proxy_protocol.trustedNetworks }} - -{{- if and (.Values.deployment.env.ENABLE_IMAP) (not .Values.deployment.env.SMTP_ONLY) }} -service imap-login { - inet_listener imap { - port = 143 - } - - inet_listener imaps { - port = 993 - ssl = yes - } - - inet_listener imap_proxy { - haproxy = yes - port = 10143 - ssl = no - } - - inet_listener imaps_proxy { - haproxy = yes - port = 10993 - ssl = yes - } -} -{{- end -}} - -{{- if and (.Values.deployment.env.ENABLE_POP) (not .Values.deployment.env.SMTP_ONLY) }} -service pop3-login { - inet_listener pop3 { - port = 110 - } - - inet_listener pop3s { - port = 995 - ssl = yes - } - - inet_listener pop3_proxy { - haproxy = yes - port = 10110 - ssl = no - } - - inet_listener pop3s_proxy { - haproxy = yes - port = 10995 - ssl = yes - } -} -{{- end -}} -{{- end -}} diff --git a/charts/docker-mailserver/config/postfix-main.cf b/charts/docker-mailserver/config/postfix-main.cf deleted file mode 100644 index 24bbe03e..00000000 --- a/charts/docker-mailserver/config/postfix-main.cf +++ /dev/null @@ -1,3 +0,0 @@ -{{- if not .Values.spfTestsDisabled }} - smtpd_recipient_restrictions = permit_sasl_authenticated, permit_mynetworks, reject_unauth_destination, reject_unauth_pipelining, reject_invalid_helo_hostname, reject_non_fqdn_helo_hostname, reject_unknown_recipient_domain, reject_rbl_client zen.spamhaus.org, reject_rbl_client bl.spamcop.net{{ range .Values.rblRejectDomains }}, reject_rbl_client {{ . }}{{ end }} -{{ end -}} diff --git a/charts/docker-mailserver/config/user-patches.sh b/charts/docker-mailserver/config/user-patches.sh deleted file mode 100644 index 040dd27c..00000000 --- a/charts/docker-mailserver/config/user-patches.sh +++ /dev/null @@ -1,39 +0,0 @@ -#!/bin/bash - -{{- if .Values.proxy_protocol.enabled }} -# Make sure to keep this file in sync with https://github.com/docker-mailserver/docker-mailserver/blob/master/target/postfix/master.cf! -cat <> /etc/postfix/master.cf - -# Submission with proxy -10587 inet n - n - - smtpd - -o syslog_name=postfix/submission - -o smtpd_tls_security_level=encrypt - -o smtpd_sasl_auth_enable=yes - -o smtpd_sasl_type=dovecot - -o smtpd_reject_unlisted_recipient=no - -o smtpd_sasl_authenticated_header=yes - -o smtpd_client_restrictions=permit_sasl_authenticated,reject - -o smtpd_relay_restrictions=permit_sasl_authenticated,reject - -o smtpd_sender_restrictions=\$mua_sender_restrictions - -o smtpd_discard_ehlo_keywords= - -o milter_macro_daemon_name=ORIGINATING - -o cleanup_service_name=sender-cleanup - -o smtpd_upstream_proxy_protocol=haproxy - -# Submissions with proxy -10465 inet n - n - - smtpd - -o syslog_name=postfix/submissions - -o smtpd_tls_wrappermode=yes - -o smtpd_sasl_auth_enable=yes - -o smtpd_sasl_type=dovecot - -o smtpd_reject_unlisted_recipient=no - -o smtpd_sasl_authenticated_header=yes - -o smtpd_client_restrictions=permit_sasl_authenticated,reject - -o smtpd_relay_restrictions=permit_sasl_authenticated,reject - -o smtpd_sender_restrictions=\$mua_sender_restrictions - -o smtpd_discard_ehlo_keywords= - -o milter_macro_daemon_name=ORIGINATING - -o cleanup_service_name=sender-cleanup - -o smtpd_upstream_proxy_protocol=haproxy -EOS -{{- end }} \ No newline at end of file diff --git a/charts/docker-mailserver/templates/configmap-user.yaml b/charts/docker-mailserver/templates/configmap-user.yaml deleted file mode 100644 index be114c35..00000000 --- a/charts/docker-mailserver/templates/configmap-user.yaml +++ /dev/null @@ -1,20 +0,0 @@ -{{- range $config := .Values.configs.custom }} -{{- if $config.create }} -apiVersion: "v1" -kind: "ConfigMap" -metadata: - labels: - app.kubernetes.io/name: {{ template "dockermailserver.fullname" $ }} - chart: "{{ $.Chart.Name }}-{{ $.Chart.Version }}" - heritage: "{{ $.Release.Service }}" - release: "{{ $.Release.Name }}" - name: {{ $config.name }} -data: - {{- range $item := $config.data }} - {{ $item.subPath}}: | -{{ $item.content | indent 6 }} - {{- end }} -{{- end }} ---- -{{- end }} - diff --git a/charts/docker-mailserver/templates/configmap.yaml b/charts/docker-mailserver/templates/configmap.yaml index 5f3a41a6..b997ec12 100644 --- a/charts/docker-mailserver/templates/configmap.yaml +++ b/charts/docker-mailserver/templates/configmap.yaml @@ -1,22 +1,17 @@ -{{- if .Values.configs.installDefault }} +{{- range $config := .Values.configFiles }} +{{- if $config.create }} apiVersion: "v1" kind: "ConfigMap" metadata: labels: - app.kubernetes.io/name: {{ template "dockermailserver.fullname" . }} - chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" - heritage: "{{ .Release.Service }}" - release: "{{ .Release.Name }}" - name: {{ template "dockermailserver.fullname" . }}-configs -data: -{{/* Copy files from config subdirectory into a config map. The key for each file is its path - stripped of the leading "/config" and then base 64 encoded. */}} - {{- range $path, $content := .Files.Glob "config/**" }} - {{/* Skip empty files and public / private keys */}} - {{- if and (gt (len $content) 0) (not (contains "private" $path)) (not (contains "public" $path)) }} - # {{ $path }} - {{ regexReplaceAll "[^-_.\\w]" $path "." }}: | -{{ tpl ($.Files.Get $path) $ | indent 6 }} - {{- end }} - {{- end }} + app.kubernetes.io/name: {{ template "dockermailserver.fullname" $ }} + chart: "{{ $.Chart.Name }}-{{ $.Chart.Version }}" + heritage: "{{ $.Release.Service }}" + release: "{{ $.Release.Name }}" + name: {{ regexReplaceAll "[.]" $config.name "-" }} +data: + {{ $config.key | default $config.path }}: | +{{ tpl $config.data $ | indent 6 }} +--- {{- end }} +{{- end }} \ No newline at end of file diff --git a/charts/docker-mailserver/templates/secret-user.yaml b/charts/docker-mailserver/templates/secret-user.yaml deleted file mode 100644 index 9ada2077..00000000 --- a/charts/docker-mailserver/templates/secret-user.yaml +++ /dev/null @@ -1,20 +0,0 @@ -{{- range $secret := .Values.secrets }} -{{- if $secret.create }} -apiVersion: "v1" -kind: "Secret" -metadata: - labels: - app.kubernetes.io/name: {{ template "dockermailserver.fullname" $ }} - chart: "{{ $.Chart.Name }}-{{ $.Chart.Version }}" - heritage: "{{ $.Release.Service }}" - release: "{{ $.Release.Name }}" - name: {{ $secret.name }} -data: - {{- range $item := $secret.data }} - {{ $item.subPath}}: | -{{ $item.content | indent 6 }} - {{- end }} -{{- end }} ---- -{{- end }} - diff --git a/charts/docker-mailserver/templates/secret.yaml b/charts/docker-mailserver/templates/secret.yaml index be5f19e5..65497bc8 100644 --- a/charts/docker-mailserver/templates/secret.yaml +++ b/charts/docker-mailserver/templates/secret.yaml @@ -1,16 +1,18 @@ +{{- range $secret := .Values.secrets }} +{{- if $secret.create }} apiVersion: "v1" kind: "Secret" metadata: labels: - app.kubernetes.io/name: {{ template "dockermailserver.fullname" . }} - chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" - heritage: "{{ .Release.Service }}" - release: "{{ .Release.Name }}" - name: {{ template "dockermailserver.fullname" . }}-secrets + app.kubernetes.io/name: {{ template "dockermailserver.fullname" $ }} + chart: "{{ $.Chart.Name }}-{{ $.Chart.Version }}" + heritage: "{{ $.Release.Service }}" + release: "{{ $.Release.Name }}" + name: {{ regexReplaceAll "[.]" $secret.name "-" }} data: -{{- range $path, $content := .Files.Glob "config/**" }} - {{- /* Process files that contains public or private or secret in their name */}} - {{- if or (contains "private" $path) (contains "public" $path) (contains ".secret" $path) }} - {{ $path | replace "/" "." }}: {{ tpl ($.Files.Get $path) $ | b64enc }} - {{- end }} -{{ end }} \ No newline at end of file + {{ $secret.key | default $secret.path }}: | +{{ tpl $secret.data $ | indent 6 }} +--- +{{- end }} +{{- end }} + From 0b425e5f6c3a41f767d9b9621de2cd66f84b37d8 Mon Sep 17 00:00:00 2001 From: Charlie Savage Date: Sun, 21 Jan 2024 15:42:00 -0800 Subject: [PATCH 26/66] Support mulitple pvcs --- charts/docker-mailserver/templates/pvc.yaml | 32 ++++++++++----------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/charts/docker-mailserver/templates/pvc.yaml b/charts/docker-mailserver/templates/pvc.yaml index 16eb3a52..14b82d3f 100644 --- a/charts/docker-mailserver/templates/pvc.yaml +++ b/charts/docker-mailserver/templates/pvc.yaml @@ -1,27 +1,27 @@ -{{- if not .Values.persistence.existingClaim -}} ---- -kind: "PersistentVolumeClaim" -apiVersion: "v1" +{{- range $name, $persistence := .Values.persistence -}} +{{- if and (not $persistence.existingClaim) ($persistence.enabled) }} +kind: PersistentVolumeClaim +apiVersion: v1 metadata: - name: {{ template "dockermailserver.fullname" . }} - {{- if .Values.persistence.annotations }} + name: {{ template "dockermailserver.fullname" $ }}-{{ $name }} + {{- if $persistence.annotations }} annotations: - {{ toYaml .Values.persistence.annotations | indent 2 }} + {{ toYaml $persistence.annotations | indent 2 }} {{ end }} spec: accessModes: - - {{ .Values.persistence.accessMode }} - {{- if .Values.persistence.storageClass }} - storageClassName: {{ .Values.persistence.storageClass | quote }} + {{ toYaml $persistence.accessModes | indent 2 }} + + {{- if $persistence.storageClass }} + storageClassName: {{ $persistence.storageClass | quote }} {{- end }} resources: requests: - storage: {{ .Values.persistence.size | quote }} - {{- if .Values.persistence.selector }} + storage: {{ $persistence.size | quote }} + {{- if $persistence.selector }} selector: -{{ toYaml .Values.persistence.selector | indent 4 }} - {{ end }} - {{- if .Values.persistence.volumeName }} - volumeName: {{ .Values.persistence.volumeName }} +{{ toYaml $persistence.selector | indent 4 }} {{ end }} +--- +{{- end }} {{- end }} From d10cb2cd013547f53a06b6b4b825ab16e8880b36 Mon Sep 17 00:00:00 2001 From: Charlie Savage Date: Sun, 21 Jan 2024 15:42:16 -0800 Subject: [PATCH 27/66] Updates for changes to pvcs and config files --- .../templates/deployment.yaml | 115 ++++++------------ 1 file changed, 36 insertions(+), 79 deletions(-) diff --git a/charts/docker-mailserver/templates/deployment.yaml b/charts/docker-mailserver/templates/deployment.yaml index 4611c3af..9ff1d14c 100644 --- a/charts/docker-mailserver/templates/deployment.yaml +++ b/charts/docker-mailserver/templates/deployment.yaml @@ -35,45 +35,38 @@ spec: securityContext: {{ toYaml .Values.securityContext | indent 8 }} volumes: - - name: data - persistentVolumeClaim: - claimName: {{ template "dockermailserver.fullname" . }} - - {{- if .Values.configs.installDefault }} - # Default ConfigMap - - name: {{ template "dockermailserver.fullname" . }}-configs + # ConfigMaps + {{- range $config := .Values.configFiles }} + - name: {{ regexReplaceAll "[.]" $config.name "-" }} configMap: - name: {{ template "dockermailserver.fullname" . }}-configs + name: {{ regexReplaceAll "[.]" $config.name "-" }} {{- end }} - # Custom ConfigMaps - {{- range $config := .Values.configs.custom }} - - name: {{ regexReplaceAll "[.]" $config.name "-" }}-config - configMap: - name: {{ $config.name }} - {{- end }} - - # Default Secret - - name: secrets - secret: - secretName: {{ template "dockermailserver.fullname" . }}-secrets - - # Custom Secrets + # Secrets {{- range $secret := .Values.secrets }} - - name: {{ regexReplaceAll "[.]" $secret.name "-" }}-secret + - name: {{ regexReplaceAll "[.]" $secret.name "-" }} secret: - secretName: {{ $secret.name }} + secretName: {{ regexReplaceAll "[.]" $secret.name "-" }} {{- end }} + # Certificate {{- if .Values.certificate }} - name: certificate secret: secretName: {{ .Values.certificate }} - {{- end }} + {{- end }} - {{- if .Values.additionalVolumes }} - # Additional Volumes -{{ toYaml .Values.additionalVolumes | indent 8 }} + # PVCs + {{- range $name, $persistence := .Values.persistence }} + {{- if $persistence.enabled }} + - name: {{ $name }} + persistentVolumeClaim: + {{- if $persistence.existingClaim}} + claimName: {{ $persistence.existingClaim }} + {{ else }} + claimName: {{ template "dockermailserver.fullname" $ }}-{{ $name }} + {{ end }} + {{- end }} {{- end }} containers: @@ -104,72 +97,36 @@ spec: - "NET_ADMIN" {{ end }} {{ toYaml .Values.deployment.containerSecurityContext | indent 12 }} + volumeMounts: {{- if .Values.certificate }} - name: certificate mountPath: /tmp/dms/custom-certs readOnly: true {{- end }} - {{ if .Values.metrics.enabled }} - - name: data - mountPath: /var/log/mail - subPath: log - {{- end }} - - name: data - mountPath: /var/mail - subPath: mail - - name: data - mountPath: /var/mail-state - subPath: mail-state - ### Default ConfigMap ### - {{- if .Values.configs.installDefault }} - {{- /* Mount configuration files stored in the built-in ConfigMap into the container */}} - {{- range $path, $content := .Files.Glob "config/**" }} - {{- if and (gt (len $content) 0) (not (contains "private" $path)) (not (contains "public" $path)) }} - # {{ $path }} - - name: {{ template "dockermailserver.fullname" $ }}-configs - subPath: {{ regexReplaceAll "[^-_.\\w]" $path "." }} - mountPath: /tmp/docker-mailserver/{{ $path | trimPrefix "config/" }} - {{- end }} + # ConfigFiles via ConfigMaps + {{- range $config := .Values.configFiles }} + - name: {{ regexReplaceAll "[.]" $config.name "-" }} + subPath: {{ $config.key | default $config.path }} + mountPath: /tmp/docker-mailserver/{{ $config.path }} {{- end }} - {{- end }} - ### Custom ConfigMaps ### - {{- range $config := .Values.configs.custom }} - {{- range $item := $config.data }} - # {{ $item.subPath }} - - name: {{ regexReplaceAll "[.]" $config.name "-" }}-config - subPath: {{ $item.subPath }} - mountPath: /tmp/docker-mailserver/{{ $item.mountPath }} + # Config via Secrets + {{- range $secret := .Values.secrets }} + - name: {{ regexReplaceAll "[.]" $secret.name "-" }} + subPath: {{ $secret.key | default $secret.path }} + mountPath: /tmp/docker-mailserver/{{ $secret.path }} {{- end }} - {{- end }} - ### Default Secret ### - {{- range $path, $content := .Files.Glob "config/**" }} - {{- if and (gt (len $content) 0) (or (contains "private" $path) (contains "public" $path)) }} - ### System defined secrets ### - - name: secrets - subPath: {{ regexReplaceAll "[^-_.\\w]" $path "." }} - mountPath: /tmp/docker-mailserver/{{ $path | trimPrefix "config/" }} - readOnly: true + # Volumes + {{- range $name, $persistence := .Values.persistence }} + {{- if $persistence.enabled }} + - name: {{ $name }} + mountPath: {{ $persistence.mountPath }} {{- end }} - {{- end }} - - ### Custom Secrets ### - {{- range $secret := .Values.secrets }} - {{- range $item := $secret.data }} - # {{ $item.subPath }} - - name: {{ regexReplaceAll "[.]" $secret.name "-" }}-secret - subPath: {{ $item.subPath }} - mountPath: /tmp/docker-mailserver/{{ $item.mountPath }} {{- end }} - {{- end }} - {{- if .Values.additionalVolumeMounts }} - # Additional VolumeMounts -{{ toYaml .Values.additionalVolumeMounts | indent 12 }} - {{- end }} livenessProbe: exec: command: From a9bd46be4020514cc8ffc08a2962877322a6c586 Mon Sep 17 00:00:00 2001 From: Charlie Savage Date: Sun, 21 Jan 2024 15:42:31 -0800 Subject: [PATCH 28/66] Simplify notes --- charts/docker-mailserver/templates/NOTES.txt | 69 ++++---------------- 1 file changed, 11 insertions(+), 58 deletions(-) diff --git a/charts/docker-mailserver/templates/NOTES.txt b/charts/docker-mailserver/templates/NOTES.txt index b550d56f..413ad695 100644 --- a/charts/docker-mailserver/templates/NOTES.txt +++ b/charts/docker-mailserver/templates/NOTES.txt @@ -1,15 +1,13 @@ GOOD NEWS! ==================== -You've successfully launched the docker-mailserver helm chart! - -{{- if not .Values.configs -}} +You've successfully installed the docker-mailserver helm chart! Initial Setup ------------ -But wait, dear reader! You haven't configured your mail server yet! You'll need to quickly open a command -prompt into the running container (you have two minutes) and setup a first email account. +If you have not yet configured your mail server you'll need to quickly open a command +prompt inside the running container (you have two minutes) and setup a first email account. kubectl exec -it --namespace mail deploy/docker-mailserver -- bash @@ -19,62 +17,17 @@ This will create a file: cat /tmp/docker-mailserver/postfix-accounts.cf -Next, run the setup command to see your other options: +Next, run the setup command to see additonal options: setup -As you run various setup commands, additional files will be generated in `/tmp/docker-mailserver`. - -Once you are done, you will want to copy them to your local machine (remember when the pod is terminated all of -these files will be deleted!) - - exit # To exit the bash prompt in the container - - mdkir /tmp/config - - cd /tmp/config - - podname=$(kubectl get pod --namespace mail -l app.kubernetes.io/name=docker-mailserver -o jsonpath="{.items[0].metadata.name}") - - kubectl cp mail/$podname:tmp/docker-mailserver /tmp/test - -Once you have copied these files locally, you then need to save them into configmaps or -secrets. Please see the `configs` and `secrets` key in the values.yaml file for more information. Once you have created your custom values.yaml file you -can then redeploy the chart - - helm upgrade --namespace mail {{ .Release.Namespace }} docker-mailserver --values +For more information please refer to this Chart's README file. -{{ end }} +{{ if .Values.proxy_protocol.enabled -}} -{{ if .Values.haproxy.enabled -}} -{{ if .Values.poorMansK8sLb.enabled -}} - -------- -You're running with HAProxy enabled, and poor-mans-k8s-lb turned on. Provided you've correctly setup your -poorMansK8sLb.webhook.url and poorMansK8sLb.webhook.token, you don't need to do anything further in -order for your external access to work -------- - -{{ else if .Values.haproxy.deploy_ingress_chart -}} - -------- -You're configured for HAProxy in "ingress" mode. Theoretically you don't have to do anything, provided the -ingress resource started up correctly - you should be able to access your external IP on the standard email -ports, and have it "Just Work (tm)" -------- - -{{ else -}} - -------- -You've enabled haproxy support, but you've not activated ingress or poorMansK8sLb. You'll need to manually configure -your haproxy to send the incoming traffic to the node running your pod, on the configured nodeport ports. Use the -"send-proxy" value for ports 25,110,143,993 and 995 (but not 465 and 587) -------- -{{ end }} -{{ else }} +Proxy Ports +------------ +You have enabled PROXY protocol support, likely because you are running on a bare metal Kubernetes cluster. This means additional ports have been created that are configured for the PROXY protocol. These ports are in the 10,000 range - thus IMAPs is 10993 (10000 + 993), SUBMISSION is 10587 (10000 + 587), etc. -------- -You've disabled haproxy support. This means that you'll need to MANUALLY configure how your services are exposed -to the internet. You may be running in host mode, or nodeport mode, or some other complex configuration. -------- -{{ end }} +It is now up to you to configure incoming traffic to use these ports. For more information please refer to this Chart's README file. +{{ end }} \ No newline at end of file From 9f8f1402abd98cac094676fff425d8e322ff3286 Mon Sep 17 00:00:00 2001 From: Charlie Savage Date: Sun, 21 Jan 2024 15:42:49 -0800 Subject: [PATCH 29/66] Remove older code --- charts/docker-mailserver/PERSISTENCE.md | 92 ------------------ .../demo-mode-dkim-key-for-example.com.key | 27 ----- .../rainloop_with_haproxy.png | Bin 175456 -> 0 bytes .../docker-mailserver/templates/_helpers.tpl | 11 --- 4 files changed, 130 deletions(-) delete mode 100644 charts/docker-mailserver/PERSISTENCE.md delete mode 100644 charts/docker-mailserver/demo-mode-dkim-key-for-example.com.key delete mode 100644 charts/docker-mailserver/rainloop_with_haproxy.png diff --git a/charts/docker-mailserver/PERSISTENCE.md b/charts/docker-mailserver/PERSISTENCE.md deleted file mode 100644 index 90cdab80..00000000 --- a/charts/docker-mailserver/PERSISTENCE.md +++ /dev/null @@ -1,92 +0,0 @@ -# Persistence - -There are two storage APIs in Kubernetes that handle persistence abstraction: - -- volume.alpha.kubernetes.io/storage-class -- volume.beta.kubernetes.io/storage-class - -These APIs have different behaviours, across different cluster versions. The alpha API will be used if a storage class is not specified in the `storageClass` input, as it defers storage to to the cluster and the cluster will provision some based on its configured defaults. However, if the administrator needs to provision this storage (such as in the local development environment), they should set the `storageClass` attribute to match their configured storage, after which the beta API will be used. - -An example PV might look something like the following: - -```yaml - --- - apiVersion: "v1" - kind: "PersistentVolume" - metadata: - name: "foo-mysql" - annotations: - volume.beta.kubernetes.io/storage-class: "foo-mysql" - spec: - capacity: - storage: "10Gi" - accessModes: - - "ReadWriteOnce" - persistentVolumeReclaimPolicy: "Retain" - hostPath: - path: /mnt/mysql -``` - -## Backup Strategies - -### Google Cloud Persistent Disk - -Kubernetes does not come with any facility to automatically snapshot data on a regular basis. However, with the [k8s-snapshots](https://github.com/miracle2k/k8s-snapshots) application we can mark a persistent volume as requiring backup via a Kubernetes annotation: - -```yaml - --- - apiVersion: "v1" - kind: "PersistentVolume" - metadata: - name: "foo-mysql" - annotations: - backup.kubernetes.io/deltas: 1h 2d 30d 180d # <-- The annotation - # ... -``` - -For more information, see the [k8s-snapshots](https://github.com/miracle2k/k8s-snapshots) repository. - -### Everything Else - -This is not a solution that any repository maintainer has had to solve yet, so they are unaware of any solution to this. - -## Custom VS Automatic persistence - -Kubernetes automating the provisioning and mounting of storage is very handy when first deploying an application. However, it makes all further operations maintaining that application harder as if the application is ever torn down, it cannot be brought back up (easily) with that same persistent volume. Thus, it's a good idea when deploying an application that the developer: - -1. Creates the storage class specifically for this application, or -2. Creates the PVC manually so it can be associated with a particular disk - -### Existing PersistentVolumeClaims - -1. Create the PersistentVolumeClaim -```bash -$ cat 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 Nr+y|FCOX5T%+WPR6ItS7BEKZ3 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 t5{3>baqN&*;)X_gCt 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%0AkE*J7N# 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,Nm0@1{zN(y!gpe^v#`2epx7I(F;{dpD-yhxxf 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%0A}5oa7t+&+!FK?5 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 Date: Sun, 21 Jan 2024 15:43:06 -0800 Subject: [PATCH 30/66] Updated values.yaml for PVCs and configfiles --- charts/docker-mailserver/values.yaml | 308 +++++++++++++++++++-------- 1 file changed, 222 insertions(+), 86 deletions(-) diff --git a/charts/docker-mailserver/values.yaml b/charts/docker-mailserver/values.yaml index 69cf126e..2eb3c687 100644 --- a/charts/docker-mailserver/values.yaml +++ b/charts/docker-mailserver/values.yaml @@ -1,4 +1,5 @@ ---- +fullnameOverride: "" + image: # image.name is the name of the container image to use. Refer to https://hub.docker.com/r/mailserver/docker-mailserver name: "mailserver/docker-mailserver" @@ -15,79 +16,9 @@ priorityClassName: serviceAccount: create: "true" -## ConfigMaps (and Secrets) are used to copy docker-mailserver configuration files -## into running containers. This chart automatically sets up any config files that -## are stored in its chart/config directory. -## -## However, Helm does not provide a way too save external files to a ConfigMap or Secret. -## This is problem for docker-mailserver because you need to setup postfix acounts, -## dovecot accounts, etc. -## -## The configs and secrets keys solve this problem. They allow you to add additional config -## files by either referencing existing ConfigMaps (that you create before installing the Chart) -## or by creating new ones (set the create key to true). -## -configs: - installDefault: true # Install the built-in config files -## custom: -## - name: postfix.example.com -## create: true -## data: -## - subPath: postfix-accounts.cf -## mountPath: postfix-accounts.cf -## content: | -## # A sample user - the password is "password" -## user@example.com|{SHA512-CRYPT}$6$l4023rZnQEy/l0Rg$JeNjAAICB43VAX7GTJ9jeE7DR0LeyR5nU.ftq3c42T5E1IZSuRBqwM8erRh6t0CyIT6aYpBIAopzcQHNUvMV00 -## -## - name: dovecot.example.com # Name of the ConfigMap -## create: true # Whether to create the ConfigMap (if true, content must be specified) -## data: -## - subPath: dovecot-masters.cf -## mountPath: dovecot-masters.cf # Relative path to /tmp/docker-mailserver/ -## content: | -## # A sample user - the password is "password" -## user@example.com|{SHA512-CRYPT}$6$l4023rZnQEy/l0Rg$JeNjAAICB43VAX7GTJ9jeE7DR0LeyR5nU.ftq3c42T5E1IZSuRBqwM8erRh6t0CyIT6aYpBIAopzcQHNUvMV00 -## -## If you set the create key to false, then you must manually create the ConfigMaps before deploying the chart. -## -## kubectl create configmap postfix.example.com --namespace mail --from-file=postfix-accounts.cf= -## - -## The secrets key works the same way as the configs key. Use secrets to store sensitive information, -## such as DKIM signing keys. -## -## secrets: -## - name: rspamd.example.com # This is the name of the Secret -## create: true # If true, create a new Secret -## data: -## - subPath: rspamd.dkim.rsa-2048-mail-example.com.private.txt -## mountPath: rspamd/dkim/rsa-2048-mail-example.com.private.txt -## content: abace # If create is true, then you must specify content. Must be base 64 encoded! -## -## - subPath: rspamd.dkim.rsa-2048-mail-example.com.public -## mountPath: rspamd/dkim/rsa-2048-mail-example.com.public -## content: abace # If create is true, then you must specify content. Must be base 64 encoded! -## -## If you set the create key to false, then you must manually create the ConfigMaps before deploying the chart. -## -## kubectl create secret rspamd.example.com --namespace mail --from-file=rspamd.dkim.rsa-2048-mail-example.com.private.txt= -secrets: [] - ## Specify a TLS secret name that contains a certificate and private key for your email domain certificate: -## Mount additional volumes into the container. Useful for persistent logs or -## creating or injecting additional config files. -#additionalVolumes: -#- name: host-config-files -# hostPath: -# path: /tmp/docker-mailserver-config # Directory on host -# type: DirectoryOrCreate - -#additionalVolumeMounts: -#- name: host-config-files -# mountPath: /tmp/docker-mailserver # Directory in container - # If you choose _not_ to use haproxy, and you're not exposing your services with a load-balanced service # with an external traffic policy in "Local" mode, you risk having the source IP of incoming mail overwritten # with a local Kubernetes cluster IP, as part of the ingress routing of the connection. This, in turn, @@ -160,7 +91,7 @@ deployment: TZ: NETWORK_INTERFACE: TLS_LEVEL: - SPOOF_PROTECTION: + SPOOF_PROTECTION: 1 ENABLE_SRS: 0 ENABLE_OPENDKIM: 0 ENABLE_OPENDMARC: 0 @@ -307,8 +238,8 @@ deployment: enable_dovecot_replication: true securityContext: - runAsUser: 10001 - runAsGroup: 10001 + runAsUser: 5000 + runAsGroup: 5000 containerSecurityContext: readOnlyRootFilesystem: false # incompatible with the way docker-mailserver works @@ -369,17 +300,57 @@ resources: memory: "2048Mi" ephemeral-storage: "500Mi" +# Note this is a dictionary and not a list so invidual keys can be overriden by --set or --value helm parameters persistence: - # existingClaim: # Specify an existing PVC to use - size: "10Gi" - # Uncomment the backup.kubernetes.io/deltas annotation below if you use https://github.com/miracle2k/k8s-snapshots - annotations: {} - # backup.kubernetes.io/deltas: PT1H P2D P30D P180D - accessMode: ReadWriteOnce - # storageClass: - # selector: - # Specify a volumeName, otherwise a new volume will be dynamically provisioned - # volumeName: my-volume + # Stores generated configuration files + # https://docker-mailserver.github.io/docker-mailserver/edge/faq/#what-about-the-docker-datadmsconfig-directory + mail-config: + enabled: true + existingClaim: "" + mountPath: /tmp/docker-mailserver + size: "1Mi" + annotations: {} + accessModes: + - ReadWriteOnce + storageClass: + selector: {} + + # Stores emails + mail-data: + enabled: true + existingClaim: "" + size: 10Gi + mountPath: /var/mail + annotations: {} + accessModes: + - ReadWriteOnce + storageClass: + selector: {} + + # Stores state for Postfix, Dovecot, Fail2Ban, Amavis, PostGrey, ClamAV, SpamAssassin, Rspamd & Redis + # https://docker-mailserver.github.io/docker-mailserver/edge/faq/#what-about-the-docker-datadmsmail-state-directory + mail-state: + enabled: true + existingClaim: "" + mountPath: /var/mail-state + size: "1Gi" + annotations: {} + accessModes: + - ReadWriteOnce + storageClass: + selector: {} + + # Store mail logs + mail-log: + enabled: true + existingClaim: "" + mountPath: /var/log/mail + size: "1Gi" + annotations: {} + accessModes: + - ReadWriteOnce + storageClass: + selector: {} ## Monitoring adds the prometheus.io annotations to pods and services, so that the Prometheus Kubernetes SD mechanism ## as configured in the examples will automatically discover both the pods and the services to query. @@ -424,7 +395,7 @@ rspamd: secret: proxy_protocol: - enabled: false + enabled: true # List of sources (in CIDR format, space-separated) to permit PROXY protocol from trustedNetworks: "10.0.0.0/8 192.168.0.0/16 172.16.0.0/16" @@ -476,4 +447,169 @@ metrics: serviceMonitor: enabled: false - scrapeInterval: 15s \ No newline at end of file + scrapeInterval: 15s + + +## ConfigMaps (and Secrets) are used to copy docker-mailserver configuration files +## into running containers. This chart automatically sets up any config files that +## are stored in its chart/config directory. +## +## However, Helm does not provide a way too save external files to a ConfigMap or Secret. +## This is problem for docker-mailserver because you need to setup postfix acounts, +## dovecot accounts, etc. +## +## The configs and secrets keys solve this problem. They allow you to add additional config +## files by either referencing existing ConfigMaps (that you create before installing the Chart) +## or by creating new ones (set the create key to true). +## +configFiles: + - name: dovecot.cf + create: true + path: dovecot.cf + data: | + {{- if .Values.proxy_protocol.enabled }} + haproxy_trusted_networks = {{ .Values.proxy_protocol.trustedNetworks }} + + {{- if and (.Values.deployment.env.ENABLE_IMAP) (not .Values.deployment.env.SMTP_ONLY) }} + service imap-login { + inet_listener imap { + port = 143 + } + + inet_listener imaps { + port = 993 + ssl = yes + } + + inet_listener imap_proxy { + haproxy = yes + port = 10143 + ssl = no + } + + inet_listener imaps_proxy { + haproxy = yes + port = 10993 + ssl = yes + } + } + {{- end -}} + + {{- if and (.Values.deployment.env.ENABLE_POP) (not .Values.deployment.env.SMTP_ONLY) }} + service pop3-login { + inet_listener pop3 { + port = 110 + } + + inet_listener pop3s { + port = 995 + ssl = yes + } + + inet_listener pop3_proxy { + haproxy = yes + port = 10110 + ssl = no + } + + inet_listener pop3s_proxy { + haproxy = yes + port = 10995 + ssl = yes + } + } + {{- end -}} + {{- end -}} + + - name: postfix-main.cf + create: true + path: postfix-main.cf + data: | + {{- if not .Values.spfTestsDisabled }} + smtpd_recipient_restrictions = permit_sasl_authenticated, permit_mynetworks, reject_unauth_destination, reject_unauth_pipelining, reject_invalid_helo_hostname, reject_non_fqdn_helo_hostname, reject_unknown_recipient_domain, reject_rbl_client zen.spamhaus.org, reject_rbl_client bl.spamcop.net{{ range .Values.rblRejectDomains }}, reject_rbl_client {{ . }}{{ end }} + {{ end -}} + + - name: 91-override-sieve.conf + create: true + path: 91-override-sieve.conf + data: | + plugin { + sieve = /var/mail/sieve/%d/%n/.dovecot.sieve + sieve_dir = /var/mail/sieve/%d/%n/sieve + } + + - name: am-i-health.sh + create: true + path: am-i-health.sh + data: | + #!/bin/bash + # this script is intended to be used by periodic kubernetes liveness probes to ensure that the container + # (and all its dependent services) is healthy + {{ range .Values.livenessTests.commands -}} + {{ . }} && \ + {{- end }} + echo "All healthy" + + - name: user-patches.sh + create: true + path: user-patches.sh + data: | + #!/bin/bash + + {{- if .Values.proxy_protocol.enabled }} + # Make sure to keep this file in sync with https://github.com/docker-mailserver/docker-mailserver/blob/master/target/postfix/master.cf! + cat <> /etc/postfix/master.cf + + # Submission with proxy + 10587 inet n - n - - smtpd + -o syslog_name=postfix/submission + -o smtpd_tls_security_level=encrypt + -o smtpd_sasl_auth_enable=yes + -o smtpd_sasl_type=dovecot + -o smtpd_reject_unlisted_recipient=no + -o smtpd_sasl_authenticated_header=yes + -o smtpd_client_restrictions=permit_sasl_authenticated,reject + -o smtpd_relay_restrictions=permit_sasl_authenticated,reject + -o smtpd_sender_restrictions=\$mua_sender_restrictions + -o smtpd_discard_ehlo_keywords= + -o milter_macro_daemon_name=ORIGINATING + -o cleanup_service_name=sender-cleanup + -o smtpd_upstream_proxy_protocol=haproxy + + # Submissions with proxy + 10465 inet n - n - - smtpd + -o syslog_name=postfix/submissions + -o smtpd_tls_wrappermode=yes + -o smtpd_sasl_auth_enable=yes + -o smtpd_sasl_type=dovecot + -o smtpd_reject_unlisted_recipient=no + -o smtpd_sasl_authenticated_header=yes + -o smtpd_client_restrictions=permit_sasl_authenticated,reject + -o smtpd_relay_restrictions=permit_sasl_authenticated,reject + -o smtpd_sender_restrictions=\$mua_sender_restrictions + -o smtpd_discard_ehlo_keywords= + -o milter_macro_daemon_name=ORIGINATING + -o cleanup_service_name=sender-cleanup + -o smtpd_upstream_proxy_protocol=haproxy + EOS + {{- end }} + + +## The secrets key works the same way as the configs key. Use secrets to store sensitive information, +## such as DKIM signing keys. +## +## secrets: +## - name: rspamd.example.com # This is the name of the Secret +## create: true # If true, create a new Secret +## path: rspamd.dkim.rsa-2048-mail-example.com.private.txt +## data: abace # If create is true, then you must specify content. Must be base 64 encoded! +## +## - name: rspamd.dkim.rsa-2048-mail-example.com.public +## create: true +## path: rspamd/dkim/rsa-2048-mail-example.com.public +## data: abace # If create is true, then you must specify content. Must be base 64 encoded! +## +## If you set the create key to false, then you must manually create the ConfigMaps before deploying the chart. +## +## kubectl create secret rspamd.example.com --namespace mail --from-file=rspamd.dkim.rsa-2048-mail-example.com.private.txt= +secrets: [] From ac3b5483e072eb5ff0e2c793261a20ff9a1b358e Mon Sep 17 00:00:00 2001 From: Charlie Savage Date: Sun, 21 Jan 2024 15:43:21 -0800 Subject: [PATCH 31/66] Updates based on latest changes --- charts/docker-mailserver/README.md | 133 +++++++++++------------------ 1 file changed, 48 insertions(+), 85 deletions(-) diff --git a/charts/docker-mailserver/README.md b/charts/docker-mailserver/README.md index f21b84df..53d6775c 100644 --- a/charts/docker-mailserver/README.md +++ b/charts/docker-mailserver/README.md @@ -1,64 +1,42 @@ -# docker-mailserver-helm - -This helm chart deploys [Docker -Mailserver](https://github.com/docker-mailserver/docker-mailserver) into a -Kubernetes cluster. - -Docker Mailserver was originally intended to be run with Docker or Docker -Compose, but it has been [adapted to Kubernetes](https://github.com/docker-mailserver/docker-mailserver/wiki/Using-in-Kubernetes). - ## Contents - [Contents](#contents) -- [Features](#features) +- [Introduction](#introduction) - [Prerequisites](#prerequisites) - [Getting Started](#getting-started) - - [Install](install) - - [Install Docker Mailserver](#install-docker-mailserver) -- [Configuration and Operation](#configuration-and-operation) - - [Download setup.sh](#download-setupsh) - - [Create / Update / Delete users](#create--update--delete-users) - - [Setup OpenDKIM](#setup-opendkim) - - [Configuration](#docker-mailserver-configuration) - - [Minimal configuration](#minimal-configuration) - - [Chart Configuration](#chart-configuration) - - [docker-mailserver Configuration](#docker-mailserver-configuration) - - [HA Proxy-Ingress Configuration](#ha-proxy-ingress-configuration) +- [Create Configuration Files](#create-configuration-files) +- [Values YAML](#values-yaml) + - [Minimal Configuration](#minimal-configuration) + - [Environmental Variables](#environmental-variables) + - [Ports](#ports) - [Development](#development) - [Testing](#testing) (Created by [gh-md-toc](https://github.com/ekalinin/github-markdown-toc.go)) -## Features - -The chart includes the following features: +## Introduction +This chart deploys [Docker +Mailserver](https://github.com/docker-mailserver/docker-mailserver) into a +Kubernetes cluster. -- Configuration is done in values.yaml and by using the setup script inside the container -- Avoids the [common problem of masking of source IP](https://kubernetes.io/docs/tutorials/services/source-ip/) by supporting haproxy's PROXY protocol (enabled by default) -- Employs [cert-manager](https://github.com/jetstack/cert-manager) to automatically provide/renew SSL certificates -- Starts in "demo" mode, allowing the user to test core functionality before configuring for specific domains -- CI/CD tested against Kubernetes 1.18,1.19, and 1.20 : ![Lint and Test Charts](https://github.com/funkypenguin/helm-docker-mailserver/workflows/Lint%20and%20Test%20Charts/badge.svg) +docker-mailserver is a production-ready, fullstack mail server that supports SMTP, IMAP, LDAP, Anti-spam, Anti-virus, etc.). Just as importantly, it is designed to be simple to install and configure. ## Prerequisites - -- A Kubernetes cluster -- Acquire a custom domain -- Configure a [DNS](https://docker-mailserver.github.io/docker-mailserver/latest/usage/#minimal-dns-setup) -- __Suggested:__ PV provisioner support in the underlying infrastructure -- [Cert-manager](https://github.com/jetstack/cert-manager/tree/master/deploy/charts/cert-manager) => 1.0 requires manual deployment into your cluster (details below) -- [Helm](https://helm.sh) >= 3.0 +- [Helm](https://helm.sh) +- A [Kubernetes](https://kubernetes.io/releases/) cluster with persistent storage and access to email [ports](https://docker-mailserver.github.io/docker-mailserver/latest/config/security/understanding-the-ports/#overview-of-email-ports) +- A custom domain name (for example, example.com) +- Correctly configured [DNS](https://docker-mailserver.github.io/docker-mailserver/latest/usage/#minimal-dns-setup) ## Getting Started -Setting up docker-mailserver requires generating a number of configuration (files)[https://docker-mailserver.github.io/docker-mailserver/latest/config/advanced/optional-config/]. Luckily, docker-mailserver ships with a helpful `setup` command that makes it easy to generate these files. It writes files to the `/tmp/docker-mailserver` directory inside a running container. +Setting up docker-mailserver requires generating a number of configuration (files)[https://docker-mailserver.github.io/docker-mailserver/latest/config/advanced/optional-config/]. To make this easier, docker-mailserver includes a `setup` command that will generate these files. -First run docker-mailserver: +To get started, first install docker-mailserver: ```console helm upgrade --install docker-mailserver docker-mailserver --namespace mail --create-namespace ``` -### Create a User -Next you'll need to quickly open a command prompt in the running container (you have two minutes) and setup an email account. +Next open a command prompt to the running container and create an email account. ```console kubectl exec -it --namespace mail deploy/docker-mailserver -- bash @@ -66,46 +44,36 @@ kubectl exec -it --namespace mail deploy/docker-mailserver -- bash setup email add user@example.com password ``` -This will geneate a new `postfix-accounts.cf` file: +This will create a new `postfix-accounts.cf` file: ```console cat /tmp/docker-mailserver/postfix-accounts.cf ``` -### Create Additional Configuration Files +## Create Configuration Files Assuming you still have a command prompt open in the running container, run the setup command to see additional configuration options: ```console setup ```console -For extensive configuration documentation, please refer to [configuration](https://docker-mailserver.github.io/docker-mailserver/latest/config/environment/). - -As you run various setup commands, additional files will be generated in `/tmp/docker-mailserver`. - -Once you are done, copy the configuration files to your local machine (remember when the pod is terminated all of these files will be deleted!) +As you run various setup commands, additional files will be generated. At a minimum you will want to run: ```console -exit # To exit the bash prompt in the container - -mdkir /tmp/config +setup dovecot-master add user@example.com password +setup config dkim keysize 2048 domain 'example.com' +``` -cd /tmp/config +Configuration files are stored inside the container at `/tmp/docker-mailserver` which by default is mapped to a Kubernetes volume. You may of course add additional configuration files to the volume as needed. -podname=$(kubectl get pod --namespace mail -l app.kubernetes.io/name=docker-mailserver -o jsonpath="{.items[0].metadata.name}") - -kubectl cp mail/$podname:tmp/docker-mailserver /tmp/test -``` +For extensive configuration documentation, please refer to [configuration](https://docker-mailserver.github.io/docker-mailserver/latest/config/environment/). -## Customize values.yaml -Once you have generated configuration files, you need to deploy them along with the Helm Chart. +## Values YAML +In addition to the configuration files generated above, the `values.yaml` file contains a number of knobs for customizing the docker-mailserver installation. -Unfortunately, Helm does not provide a way too include external files in a deployment. Instead, -configuration files need to be stored in ConfigMaps and Secrets that are then mounted as volumes -into a container. +By default, the Chart enables `rspamd` and disables `opendkim`, `dmarc`, `policyd-spf` and `clamav`. This is the setup [recommended] (https://docker-mailserver.github.io/docker-mailserver/latest/config/best-practices/dkim_dmarc_spf/) by the docker-mailserver project. -This can be done by via the `configs` and `secrets` key in the values.yaml file. Please see the comments -in (values.yaml)[./values.yaml] on how to setup these keys. +It also provides a secondary mechanism for adding config files and secrets via the `configFiles` and `secrets` keys. Once you have created your own values.yaml files, then redeploy the chart like this: @@ -119,28 +87,21 @@ You can also override individual configuration setting with `helm upgrade --set` $ helm upgrade docker-mailserver docker-mailserver --namespace mail --set pod.dockermailserver.image="your/image:1.0.0" ``` -### Minimal configuration +### Minimal Configuration There are various settings in `values.yaml` that you must override. -| Parameter | Description | Default | -| -------------------------------------------- | --------------------------------------------------- | ---------------- | -| pod.dockermailserver.pod.OVERRIDE_HOSTNAME | The hostname to be presented on SMTP banners | mail.example.com | -| configs | Specify ConfigMaps that contain configuration files | [] | -| secrets | Specify Secrets that contain configuration files | [] | -| ssl.issuer.name | The name of the cert-manager issuer expected to issue certs | `letsencrypt-staging` | -| ssl.issuer.kind | Whether the issuer is namespaced (`Issuer`) on cluster-wide (`ClusterIssuer`) | ClusterIssuer | -| ssl.dnsname | DNS domain used for DNS01 validation | example.com | +| Parameter | Description | Default | +| --------------------------------- | --------------------------------------------------- | ---------------- | +| deployment.env.[OVERRIDE_HOSTNAME](https://docker-mailserver.github.io/docker-mailserver/latest/config/environment/#override_hostname) | The hostname to be presented on SMTP banners | mail.example.com | +| certificate | Name of a Kubernetes secret that stores TLS certificate for mail domain | | ### Environmental Variables There are **many** environment variables which allow you to customize the behaviour of docker-mailserver. The function of each variable is described at https://github.com/docker-mailserver/docker-mailserver#environment-variables Every variable can be set using `values.yaml`, but note that docker-mailserver expects any true/false values to be set as binary numbers (1/0), rather than boolean (true/false). BadThings(tm) will happen if you try to pass an environment variable as "true" when [`start-mailserver.sh`](https://github.com/docker-mailserver/docker-mailserver/blob/master/target/start-mailserver.sh) is expecting a 1 or a 0! -### Default Configuration -By default, the Chart enables `rspamd` and disables `opendkim`, `dmarc`, `policyd-spf` and `clamav`. This is the setup [recommended] (https://docker-mailserver.github.io/docker-mailserver/latest/config/best-practices/dkim_dmarc_spf/) by the docker-mailserver project. - ### Certificate -You will need to setup a TLS certificate for your email domain. Perhaps the easiest way to do this is use (cert-manager)[https://cert-manager.io/]. +You will need to setup a TLS certificate for your email domain. The easiest way to do this is use (cert-manager)[https://cert-manager.io/]. Once you acquire a certificate, you will need to store it in a TLS secret in the docker-mailserver namespace. Once you have done that, update the values.yaml file like this: @@ -149,7 +110,7 @@ certificate: my-certificate-secret ``` The chart will then automatically copy the certificate and private key to the `/tmp/dms/custom-certs` director in the container and set correctly set the `SSL_CERT_PATH` and `SSL_KEY_PATH` environment variables. -### Exposing Ports to the Outside World +### Ports If you are running a bare-metal Kubernetes cluster, you will need to expose ports to the internet to receive and send emails. In addition, you need to make sure that docker-mailserver receives the correct client IP address so that spam filtering works. This can get a bit complicated, as explained in the docker-mailserver (documentation)[https://docker-mailserver.github.io/docker-mailserver/latest/config/advanced/kubernetes/#exposing-your-mail-server-to-the-outside-world]. @@ -165,22 +126,24 @@ proxy_protocol: trustedNetworks: "10.0.0.0/8 192.168.0.0/16 172.16.0.0/16" ``` -However, if you enable the Proxy protocol you will break any clients (for example NextCloud) inside your Kubernetes cluster that talk to Dovecot because they will not be using the PROXY protocol. Therefore, if the PROXY protocol is enabled, the Helm chart will create additional ports that listen without the PROXY protocol by adding 10,000 to the standard port value. +Enabling the PROXY protocol will create a new port for each protocol by adding 10,000 to the standard port value. Thus: -| Protocol | PROXY Port | No PROXY Port | -| ---------- | ------------ | -------------- | -| imap | 143 | 10143 | -| imaps | 993 | 10993 | -| pop3 | 110 | 10110 | -| pop3s | 995 | 10995 | +| Protocol | Port | PROXY Port | +| ---------- | ------- | ----------- | +| submissions | 465 | 10465 | +| submission | 587 | 10587 | +| imap | 143 | 10143 | +| imaps | 993 | 10993 | +| pop3 | 110 | 10110 | +| pop3s | 995 | 10995 | Note thes ports are NOT exposed outside of the Kubernetes cluster. ## Chart Values The following table lists the configurable parameters of the docker-mailserver chart and their default values. -| Parameter | Description | Default | -|---------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------| +| Parameter | Description | Default | +|------------------|----------------------------------------------------------|-------------------------------| | `image.name` | The name of the container image to use | `mailserver/docker-mailserver` | | `image.tag` | The image tag to use (You may prefer "latest" over "v6.1.0", for example) | `release-v6.1.0` | | `demoMode.enabled` | Start the container with a demo "user@example.com" user (password is "password") | `true` | From a777652cea450fb3346a63a11315bc43ac93cd2c Mon Sep 17 00:00:00 2001 From: Charlie Savage Date: Sun, 21 Jan 2024 15:44:28 -0800 Subject: [PATCH 32/66] Move runtimeClassName and priorityClassName do under deployment key --- charts/docker-mailserver/templates/deployment.yaml | 4 ++-- charts/docker-mailserver/values.yaml | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/charts/docker-mailserver/templates/deployment.yaml b/charts/docker-mailserver/templates/deployment.yaml index 9ff1d14c..b33c20f8 100644 --- a/charts/docker-mailserver/templates/deployment.yaml +++ b/charts/docker-mailserver/templates/deployment.yaml @@ -29,8 +29,8 @@ spec: {{ toYaml .Values.deployment.annotations | indent 8 }} {{ end }} spec: - runtimeClassName: {{ .Values.runtimeClassName }} - priorityClassName: {{ .Values.priorityClassName }} + runtimeClassName: {{ .Values.deployment.runtimeClassName }} + priorityClassName: {{ .Values.deployment.priorityClassName }} serviceAccountName: {{ template "dockermailserver.serviceAccountName" . }} securityContext: {{ toYaml .Values.securityContext | indent 8 }} diff --git a/charts/docker-mailserver/values.yaml b/charts/docker-mailserver/values.yaml index 2eb3c687..5847acaa 100644 --- a/charts/docker-mailserver/values.yaml +++ b/charts/docker-mailserver/values.yaml @@ -7,12 +7,6 @@ image: tag: "13.2.0" pullPolicy: "IfNotPresent" -## Optionally specify a runtimeClassName for the deployment -runtimeClassName: - -## Optionally specify a priorityClassName for the deployment -priorityClassName: - serviceAccount: create: "true" @@ -46,6 +40,12 @@ deployment: ## Useful for using something like stash to backup data (https://stash.run/docs/v0.9.0-rc.0/guides/latest/auto-backup/workload/) annotations: {} + ## Optionally specify a runtimeClassName for the deployment + runtimeClassName: + + ## Optionally specify a priorityClassName for the deployment + priorityClassName: + ## Host networking requested for this pod. Use the host’s network namespace. If this option is set, the ports that ## will be used must be specified. ## Ref: https://kubernetes.io/docs/api-reference/v1/definitions/#_v1_podspec From 544736d359608a743483b624802547c5414f81c3 Mon Sep 17 00:00:00 2001 From: Charlie Savage Date: Sun, 21 Jan 2024 19:11:12 -0800 Subject: [PATCH 33/66] Remove spfTestsDisabled key - seems better to just ensure that client IP addresses are preserved. Plus the override of smtpd_recipient_restrictions no longer matches docker-mailserver --- charts/docker-mailserver/README.md | 9 ++++----- charts/docker-mailserver/values.yaml | 16 ---------------- 2 files changed, 4 insertions(+), 21 deletions(-) diff --git a/charts/docker-mailserver/README.md b/charts/docker-mailserver/README.md index 53d6775c..6eeb1e7d 100644 --- a/charts/docker-mailserver/README.md +++ b/charts/docker-mailserver/README.md @@ -115,9 +115,11 @@ If you are running a bare-metal Kubernetes cluster, you will need to expose port This can get a bit complicated, as explained in the docker-mailserver (documentation)[https://docker-mailserver.github.io/docker-mailserver/latest/config/advanced/kubernetes/#exposing-your-mail-server-to-the-outside-world]. -One approach is to use the PROXY protocol, which is also explained in the (documentation)[https://docker-mailserver.github.io/docker-mailserver/latest/config/advanced/kubernetes/#proxy-port-to-service-via-proxy-protocol]. +If you disable the PROXY protocol and your mail server is not exposed using a load-balancer service with an external traffic policy in "Local" mode, then all incoming mail traffic will look like it comes from a local Kubernetes cluster IP. -The Helm chart supports the use of the proxy protocol via the `proxy_protocol` key. To enable it set the `enable` key to true. You will also want to set the `trustedNetworks` key. +One approach to preserving the client IP address is to use the PROXY protocol, which is also explained in the (documentation)[https://docker-mailserver.github.io/docker-mailserver/latest/config/advanced/kubernetes/#proxy-port-to-service-via-proxy-protocol]. + +The Helm chart supports the use of the proxy protocol via the `proxy_protocol` key. To enable it set the `proxy_protocol.enable` key to true. You will also want to set the `trustedNetworks` key. ```yaml proxy_protocol: @@ -137,8 +139,6 @@ Enabling the PROXY protocol will create a new port for each protocol by adding 1 | pop3 | 110 | 10110 | | pop3s | 995 | 10995 | -Note thes ports are NOT exposed outside of the Kubernetes cluster. - ## Chart Values The following table lists the configurable parameters of the docker-mailserver chart and their default values. @@ -149,7 +149,6 @@ The following table lists the configurable parameters of the docker-mailserver c | `demoMode.enabled` | Start the container with a demo "user@example.com" user (password is "password") | `true` | | `haproxy.enabled` | Support HAProxy PROXY protocol on SMTP, IMAP(S), and POP3(S) connections. Provides real source IP instead of load balancer IP | `false` | | `haproxy.trustedNetworks` | The IPs (*in space-separated CIDR format*) from which to trust inbound HAProxy-enabled connections | `"10.0.0.0/8 192.168.0.0/16 172.16.0.0/16"` | -| `spfTestsDisabled` | Disable all SPF-related spam checks (*if source IP of inbound connections is a problem, and you're not using haproxy*) | `false` | | `domains` | List of domains to be served | `[]` | | `livenessTests.enabled` | Whether to execute liveness tests by running (arbitrary) commands in the docker-mailserver container. Useful to detect component failure (*i.e., clamd dies due to memory pressure*) | `true` | | `livenessTests.enabled` | Array of commands to execute in sequence, to determine container health. A non-zero exit of any command is considered a failure | `[ "clamscan /tmp/docker-mailserver/TrustedHosts" ]` | diff --git a/charts/docker-mailserver/values.yaml b/charts/docker-mailserver/values.yaml index 5847acaa..9e5e1c4d 100644 --- a/charts/docker-mailserver/values.yaml +++ b/charts/docker-mailserver/values.yaml @@ -13,14 +13,6 @@ serviceAccount: ## Specify a TLS secret name that contains a certificate and private key for your email domain certificate: -# If you choose _not_ to use haproxy, and you're not exposing your services with a load-balanced service -# with an external traffic policy in "Local" mode, you risk having the source IP of incoming mail overwritten -# with a local Kubernetes cluster IP, as part of the ingress routing of the connection. This, in turn, -# will cause all incoming to appear to be coming from an internal IP, causing SPF tests to fail. -# Disable the following to bypass SPF tests altogether, to accommodate this scenario. -# spfTestsDisabled will ignore all SPF-based spam tests, in the event that you cannot obtain valid source IP details on ingress emails -spfTestsDisabled: false - # List extra RBL domains to use for hard reject filtering rblRejectDomains: [] @@ -521,14 +513,6 @@ configFiles: {{- end -}} {{- end -}} - - name: postfix-main.cf - create: true - path: postfix-main.cf - data: | - {{- if not .Values.spfTestsDisabled }} - smtpd_recipient_restrictions = permit_sasl_authenticated, permit_mynetworks, reject_unauth_destination, reject_unauth_pipelining, reject_invalid_helo_hostname, reject_non_fqdn_helo_hostname, reject_unknown_recipient_domain, reject_rbl_client zen.spamhaus.org, reject_rbl_client bl.spamcop.net{{ range .Values.rblRejectDomains }}, reject_rbl_client {{ . }}{{ end }} - {{ end -}} - - name: 91-override-sieve.conf create: true path: 91-override-sieve.conf From 1dae69c6dfad0aaa7ed0cdd979939d3afe68d91c Mon Sep 17 00:00:00 2001 From: Charlie Savage Date: Sun, 21 Jan 2024 19:19:37 -0800 Subject: [PATCH 34/66] Add section about PVCs --- charts/docker-mailserver/README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/charts/docker-mailserver/README.md b/charts/docker-mailserver/README.md index 6eeb1e7d..828835a0 100644 --- a/charts/docker-mailserver/README.md +++ b/charts/docker-mailserver/README.md @@ -139,6 +139,16 @@ Enabling the PROXY protocol will create a new port for each protocol by adding 1 | pop3 | 110 | 10110 | | pop3s | 995 | 10995 | +## Volumes +By default, the Chart requests creates four PersistentVolumeClaims. These are defined under the `persistence` key: + +| PVC Name | Default Size | Mount | Description | +| ---------- | ------- | ----------------------- | -------------------------------------| +| mail-config | 1Mi | /tmp/docker-mailserver | Stores generated [configuration](https://docker-mailserver.github.io/docker-mailserver/latest/faq/#what-about-the-docker-datadmsconfig-directory) files | +| mail-data | 10Gi | /var/mail | Stores emails | +| mail-state | 1Gi | /var/mail-state | Stores [state](https://docker-mailserver.github.io/docker-mailserver/latest/faq/#what-about-the-docker-datadmsmail-state-directory) for mail services | +| mail-log | 1Gi | /var/log/mail | Stores log files | + ## Chart Values The following table lists the configurable parameters of the docker-mailserver chart and their default values. From d2627a23822a0b5f27c49e7a316ddb057774b131 Mon Sep 17 00:00:00 2001 From: Charlie Savage Date: Sun, 21 Jan 2024 19:21:36 -0800 Subject: [PATCH 35/66] Update to version 13.3.0 --- charts/docker-mailserver/values.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/charts/docker-mailserver/values.yaml b/charts/docker-mailserver/values.yaml index 9e5e1c4d..b5474a3d 100644 --- a/charts/docker-mailserver/values.yaml +++ b/charts/docker-mailserver/values.yaml @@ -4,7 +4,7 @@ image: # image.name is the name of the container image to use. Refer to https://hub.docker.com/r/mailserver/docker-mailserver name: "mailserver/docker-mailserver" # image.tag is the tag of the container image to use. Refer to https://hub.docker.com/r/mailserver/docker-mailserver - tag: "13.2.0" + tag: "13.3.0" pullPolicy: "IfNotPresent" serviceAccount: From c5353b8d6090442838adfa9babd483361fe6ca54 Mon Sep 17 00:00:00 2001 From: Charlie Savage Date: Wed, 24 Jan 2024 22:37:55 -0800 Subject: [PATCH 36/66] Update to version 13.3.1 --- charts/docker-mailserver/values.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/charts/docker-mailserver/values.yaml b/charts/docker-mailserver/values.yaml index b5474a3d..cce708d5 100644 --- a/charts/docker-mailserver/values.yaml +++ b/charts/docker-mailserver/values.yaml @@ -4,7 +4,7 @@ image: # image.name is the name of the container image to use. Refer to https://hub.docker.com/r/mailserver/docker-mailserver name: "mailserver/docker-mailserver" # image.tag is the tag of the container image to use. Refer to https://hub.docker.com/r/mailserver/docker-mailserver - tag: "13.3.0" + tag: "13.3.1" pullPolicy: "IfNotPresent" serviceAccount: From 0525e5134515b278865f9ec5ed72048f7516f1af Mon Sep 17 00:00:00 2001 From: Charlie Savage Date: Thu, 25 Jan 2024 01:45:50 -0800 Subject: [PATCH 37/66] Fix spelling mistake --- charts/docker-mailserver/templates/NOTES.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/charts/docker-mailserver/templates/NOTES.txt b/charts/docker-mailserver/templates/NOTES.txt index 413ad695..ff84d9a6 100644 --- a/charts/docker-mailserver/templates/NOTES.txt +++ b/charts/docker-mailserver/templates/NOTES.txt @@ -17,7 +17,7 @@ This will create a file: cat /tmp/docker-mailserver/postfix-accounts.cf -Next, run the setup command to see additonal options: +Next, run the setup command to see additional options: setup From abe422bafa5e262cb2956b2ed11edf96f4670d08 Mon Sep 17 00:00:00 2001 From: Charlie Savage Date: Thu, 25 Jan 2024 01:46:41 -0800 Subject: [PATCH 38/66] Disable SPOOF_PROTECTION, see https://github.com/docker-mailserver/docker-mailserver/issues/3824 --- charts/docker-mailserver/values.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/charts/docker-mailserver/values.yaml b/charts/docker-mailserver/values.yaml index cce708d5..fe3a14cc 100644 --- a/charts/docker-mailserver/values.yaml +++ b/charts/docker-mailserver/values.yaml @@ -83,7 +83,7 @@ deployment: TZ: NETWORK_INTERFACE: TLS_LEVEL: - SPOOF_PROTECTION: 1 + SPOOF_PROTECTION: ENABLE_SRS: 0 ENABLE_OPENDKIM: 0 ENABLE_OPENDMARC: 0 From c197edd66d5c51a3238b464875c65678fe8f019e Mon Sep 17 00:00:00 2001 From: Charlie Savage Date: Thu, 25 Jan 2024 18:26:30 -0800 Subject: [PATCH 39/66] Remove haproxy chart and update tests to use proxy_protocol.enabled --- .ci/ct-config.yaml | 4 --- .../on-push-master-publish-chart.yml | 5 ---- charts/docker-mailserver/Chart.yaml | 7 ----- charts/docker-mailserver/ci/ci-haproxy.yaml | 4 --- .../tests/configmap_test.yaml | 4 +-- .../docker-mailserver/tests/haproxy_test.yaml | 6 ++-- charts/docker-mailserver/tests/oobe_test.yaml | 6 ++-- charts/docker-mailserver/values.yaml | 30 ------------------- 8 files changed, 8 insertions(+), 58 deletions(-) diff --git a/.ci/ct-config.yaml b/.ci/ct-config.yaml index 588a905f..3a4dfccf 100644 --- a/.ci/ct-config.yaml +++ b/.ci/ct-config.yaml @@ -1,7 +1,3 @@ # This file defines the config for "ct" (chart tester) used by the helm linting GitHub workflow -# Here we define every possible upstream repo our charts use in `requirements.yaml` -chart-repos: - - haproxy=https://haproxytech.github.io/helm-charts - lint-conf: .ci/lint-config.yaml \ No newline at end of file diff --git a/.github/workflows/on-push-master-publish-chart.yml b/.github/workflows/on-push-master-publish-chart.yml index 96e7f50d..b33c61b3 100644 --- a/.github/workflows/on-push-master-publish-chart.yml +++ b/.github/workflows/on-push-master-publish-chart.yml @@ -26,11 +26,6 @@ jobs: git config user.name "$GITHUB_ACTOR" git config user.email "$GITHUB_ACTOR@users.noreply.github.com" - # We need cert-manager already installed in the cluster because we assume the CRDs exist - - name: Add haproxy repo - run: | - helm repo add haprox https://haproxytech.github.io/helm-charts --force-update - - name: Run chart-releaser uses: helm/chart-releaser-action@v1.6.0 env: diff --git a/charts/docker-mailserver/Chart.yaml b/charts/docker-mailserver/Chart.yaml index a0757488..cddeee99 100644 --- a/charts/docker-mailserver/Chart.yaml +++ b/charts/docker-mailserver/Chart.yaml @@ -20,10 +20,3 @@ icon: https://avatars.githubusercontent.com/u/76868633?s=400&v=4 annotations: artifacthub.io/changes: | - Breaking : Standardized app labels to app.kubernetes.io/name for Istio workload/Cilium compatibility - -dependencies: - - name: "kubernetes-ingress" - version: "1.21.1" - repository: "https://haproxytech.github.io/helm-charts" - condition: haproxy.enabled - alias: "haproxy" diff --git a/charts/docker-mailserver/ci/ci-haproxy.yaml b/charts/docker-mailserver/ci/ci-haproxy.yaml index e99de13d..3f0a1bdb 100644 --- a/charts/docker-mailserver/ci/ci-haproxy.yaml +++ b/charts/docker-mailserver/ci/ci-haproxy.yaml @@ -1,6 +1,2 @@ -# This file exists to facilitate testing various install scenarios using ct -haproxy: - enabled: true - service: type: ClusterIP \ No newline at end of file diff --git a/charts/docker-mailserver/tests/configmap_test.yaml b/charts/docker-mailserver/tests/configmap_test.yaml index 43309e91..49d0c40a 100644 --- a/charts/docker-mailserver/tests/configmap_test.yaml +++ b/charts/docker-mailserver/tests/configmap_test.yaml @@ -12,9 +12,9 @@ tests: pattern: "dbpurgeage" - - it: should configure imaps port 10993 if haproxy enabled + - it: should configure imaps port 10993 if proxy_protocol enabled set: - haproxy.enabled: true + proxy_protocol.enabled: true asserts: - matchRegex: path: data.dovecot\.cf diff --git a/charts/docker-mailserver/tests/haproxy_test.yaml b/charts/docker-mailserver/tests/haproxy_test.yaml index 4d81954b..ba061af5 100644 --- a/charts/docker-mailserver/tests/haproxy_test.yaml +++ b/charts/docker-mailserver/tests/haproxy_test.yaml @@ -4,9 +4,9 @@ templates: - deployment-poor-mans-k8s-lb.yaml tests: - - it: should not add haproxy options to postfix/dovecot if haproxy support is not enabled + - it: should not add proxy_protocol options to postfix/dovecot if proxy_protocol support is not enabled set: - haproxy.enabled: false + proxy_protocol.enabled: false asserts: - notMatchRegex: path: data.postfix-main\.cf @@ -14,7 +14,7 @@ tests: - isNull: path: data.dovecot\.cf - - it: should create phonehome deployment if haproxy is enabled and set to external-auto mode + - it: should create phonehome deployment if proxy_protocol is enabled and set to external-auto mode set: poorMansK8sLb.enabled: true asserts: diff --git a/charts/docker-mailserver/tests/oobe_test.yaml b/charts/docker-mailserver/tests/oobe_test.yaml index 5ed26ec7..31479d35 100644 --- a/charts/docker-mailserver/tests/oobe_test.yaml +++ b/charts/docker-mailserver/tests/oobe_test.yaml @@ -35,8 +35,8 @@ tests: path: data.postfix-main\.cf pattern: smtpd_recipient_restrictions - # HAProxy is enabled by default - - it: should correctly configure postfix/dovecot if haproxy support is enabled + # proxy_protocol is enabled by default + - it: should correctly configure postfix/dovecot if proxy_protocol support is enabled set: asserts: - matchRegex: @@ -46,7 +46,7 @@ tests: path: data.dovecot\.cf pattern: haproxy - - it: should configure imaps port 10993 if haproxy is enabled + - it: should configure imaps port 10993 if proxy_protocol is enabled set: asserts: - matchRegex: diff --git a/charts/docker-mailserver/values.yaml b/charts/docker-mailserver/values.yaml index fe3a14cc..45083345 100644 --- a/charts/docker-mailserver/values.yaml +++ b/charts/docker-mailserver/values.yaml @@ -391,36 +391,6 @@ proxy_protocol: # List of sources (in CIDR format, space-separated) to permit PROXY protocol from trustedNetworks: "10.0.0.0/8 192.168.0.0/16 172.16.0.0/16" -## These values are for the haproxy sub-chart -haproxy: - # haproxy.enabled will deploy an haproxy sub-chart, configured for the TCP ports used by docker-mailserver - enabled: false - controller: - replicaCount: 1 - kind: "Deployment" - enableStaticPorts: false - tcp: - 25: "default/docker-mailserver:25::PROXY-V1" - 110: "default/docker-mailserver:110::PROXY-V1" - 143: "default/docker-mailserver:143::PROXY-V1" - 465: "default/docker-mailserver:465" - 587: "default/docker-mailserver:587" - 993: "default/docker-mailserver:993::PROXY-V1" - 995: "default/docker-mailserver:995::PROXY-V1" - service: - externalTrafficPolicy: "Local" - # Set to avoid CI error when running the generated manifest through kubeval (FIXME) - podAnnotations: - set-to-avoid-lint-errors-in: "docker-mailserver" - - defaultBackend: - replicaCount: 1 - - # These values populate dovecot's list of networks it'll "trust" for incoming haproxy connections. - # A space-separated list, on a single line, is required - # By default, we allow all RFC1918 private ranges, but this can be tightened up for the known IP/range - # of your HAProxy instance - # when metrics is enabled, we mount subpath log from pvc into /var/log/mail metrics: enabled: false From d9d926ea9e5648a96cbea36beabfe37da32bc07d Mon Sep 17 00:00:00 2001 From: Charlie Savage Date: Thu, 25 Jan 2024 20:21:16 -0800 Subject: [PATCH 40/66] Fix bug where path, instead of name, was used as fallback if key isn't specified for a config map. --- charts/docker-mailserver/templates/configmap.yaml | 2 +- charts/docker-mailserver/templates/deployment.yaml | 12 ++++++++++-- charts/docker-mailserver/templates/secret.yaml | 2 +- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/charts/docker-mailserver/templates/configmap.yaml b/charts/docker-mailserver/templates/configmap.yaml index b997ec12..83ad01ca 100644 --- a/charts/docker-mailserver/templates/configmap.yaml +++ b/charts/docker-mailserver/templates/configmap.yaml @@ -10,7 +10,7 @@ metadata: release: "{{ $.Release.Name }}" name: {{ regexReplaceAll "[.]" $config.name "-" }} data: - {{ $config.key | default $config.path }}: | + {{ $config.key | default $config.name }}: | {{ tpl $config.data $ | indent 6 }} --- {{- end }} diff --git a/charts/docker-mailserver/templates/deployment.yaml b/charts/docker-mailserver/templates/deployment.yaml index b33c20f8..bf652332 100644 --- a/charts/docker-mailserver/templates/deployment.yaml +++ b/charts/docker-mailserver/templates/deployment.yaml @@ -108,15 +108,23 @@ spec: # ConfigFiles via ConfigMaps {{- range $config := .Values.configFiles }} - name: {{ regexReplaceAll "[.]" $config.name "-" }} - subPath: {{ $config.key | default $config.path }} + subPath: {{ $config.key | default $config.name }} + {{- if isAbs $config.path }} + mountPath: {{ $config.path }} + {{- else }} mountPath: /tmp/docker-mailserver/{{ $config.path }} + {{- end }} {{- end }} # Config via Secrets {{- range $secret := .Values.secrets }} - name: {{ regexReplaceAll "[.]" $secret.name "-" }} - subPath: {{ $secret.key | default $secret.path }} + subPath: {{ $secret.key | default $secret.name }} + {{- if isAbs $secret.path }} + mountPath: {{ $secret.path }} + {{- else }} mountPath: /tmp/docker-mailserver/{{ $secret.path }} + {{- end }} {{- end }} # Volumes diff --git a/charts/docker-mailserver/templates/secret.yaml b/charts/docker-mailserver/templates/secret.yaml index 65497bc8..3bea2ff0 100644 --- a/charts/docker-mailserver/templates/secret.yaml +++ b/charts/docker-mailserver/templates/secret.yaml @@ -10,7 +10,7 @@ metadata: release: "{{ $.Release.Name }}" name: {{ regexReplaceAll "[.]" $secret.name "-" }} data: - {{ $secret.key | default $secret.path }}: | + {{ $secret.key | default $secret.name }}: | {{ tpl $secret.data $ | indent 6 }} --- {{- end }} From 5987b2c106d6dc0a8e8ae2e68c29f182f39298e6 Mon Sep 17 00:00:00 2001 From: Charlie Savage Date: Thu, 25 Jan 2024 20:21:41 -0800 Subject: [PATCH 41/66] Add support for Dovecot full text search using xapian --- charts/docker-mailserver/values.yaml | 48 ++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/charts/docker-mailserver/values.yaml b/charts/docker-mailserver/values.yaml index 45083345..471c13d0 100644 --- a/charts/docker-mailserver/values.yaml +++ b/charts/docker-mailserver/values.yaml @@ -391,6 +391,15 @@ proxy_protocol: # List of sources (in CIDR format, space-separated) to permit PROXY protocol from trustedNetworks: "10.0.0.0/8 192.168.0.0/16 172.16.0.0/16" +fullTextSearch: + enabled: true + verbose: 1 # 0 (silent), 1 (verbose) or 2 (debug) + resources: + memory: 2GB + cron: + enabled: true # Optimize index every day + schedule: 0 4 * * * # Every day at 4am + # when metrics is enabled, we mount subpath log from pvc into /var/log/mail metrics: enabled: false @@ -483,6 +492,45 @@ configFiles: {{- end -}} {{- end -}} + - name: fts-xapian-plugin.conf + create: true + path: /etc/dovecot/conf.d/10-plugin.conf + data: | + {{- if .Values.fullTextSearch.enabled }} + mail_plugins = $mail_plugins fts fts_xapian + + plugin { + fts_decoder = decode2text + } + + plugin { + fts = xapian + fts_xapian = partial=3 full=20 verbose={{ .Values.fullTextSearch.verbose }} + + fts_autoindex = yes + fts_enforced = yes + + # disable indexing of folders + fts_autoindex_exclude = \Trash + + # Index attachements + fts_decoder = decode2text + } + + service indexer-worker { + # limit size of indexer-worker RAM usage, ex: 512MB, 1GB, 2GB + vsz_limit = {{ .Values.fullTextSearch.resources.memory }} + } + + service decode2text { + executable = script /usr/lib/dovecot/decode2text.sh + user = dovecot + unix_listener decode2text { + mode = 0666 + } + } + {{- end -}} + - name: 91-override-sieve.conf create: true path: 91-override-sieve.conf From 419a0e59928a1641f19f4c834b54dc2fe0d82d68 Mon Sep 17 00:00:00 2001 From: Charlie Savage Date: Thu, 25 Jan 2024 23:23:15 -0800 Subject: [PATCH 42/66] Remove haproxy chart --- .../charts/kubernetes-ingress-1.21.1.tgz | Bin 38428 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 charts/docker-mailserver/charts/kubernetes-ingress-1.21.1.tgz diff --git a/charts/docker-mailserver/charts/kubernetes-ingress-1.21.1.tgz b/charts/docker-mailserver/charts/kubernetes-ingress-1.21.1.tgz deleted file mode 100644 index 0fcabb370cb090e34c199521f9d58ab7496ffef9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 38428 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 XvQ4bmN Date: Sun, 28 Jan 2024 21:16:10 -0800 Subject: [PATCH 43/66] Enable lint workflows to run on push, release or pull requests instead of just pull requests --- .github/workflows/on-push-lint-charts.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/on-push-lint-charts.yml b/.github/workflows/on-push-lint-charts.yml index 904592cd..52a28087 100644 --- a/.github/workflows/on-push-lint-charts.yml +++ b/.github/workflows/on-push-lint-charts.yml @@ -1,14 +1,14 @@ name: Lint and Test Charts on: - # push: - # paths: - # - 'charts/**' - # - '.github/**' + push: + paths: + - 'charts/**' + - '.github/**' pull_request: paths: - - 'charts/**' - - '.github/**' + - 'charts/**' + - '.github/**' workflow_dispatch: env: @@ -16,7 +16,7 @@ env: HELM_VERSION: v3.13.2 concurrency: - group: ${{ github.head_ref }} + group: ${{ github.ref }} cancel-in-progress: true jobs: From 9021f12264b0e65cb49cdd64bc20ac5970bcf15d Mon Sep 17 00:00:00 2001 From: Charlie Savage Date: Sun, 28 Jan 2024 21:51:08 -0800 Subject: [PATCH 44/66] Fix dependency error (see https://github.com/helm/helm/issues/11750) and regenerate Chart.lock --- charts/docker-mailserver/Chart.lock | 9 +++------ charts/docker-mailserver/Chart.yaml | 2 ++ 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/charts/docker-mailserver/Chart.lock b/charts/docker-mailserver/Chart.lock index e6ee2486..709e8f1f 100644 --- a/charts/docker-mailserver/Chart.lock +++ b/charts/docker-mailserver/Chart.lock @@ -1,6 +1,3 @@ -dependencies: -- name: kubernetes-ingress - repository: https://haproxytech.github.io/helm-charts - version: 1.21.1 -digest: sha256:b6fe2da4d22a2af00126a384dcf562bf98dcafaa5536e0adbeab18ebd10906a0 -generated: "2022-04-22T14:22:06.178899+12:00" +dependencies: [] +digest: sha256:643d5437104296e21d906ecb15b2c96ad278f20cfc4af53b12bb6069bd853726 +generated: "2024-01-28T21:49:18.990692471-08:00" diff --git a/charts/docker-mailserver/Chart.yaml b/charts/docker-mailserver/Chart.yaml index cddeee99..aeb400b1 100644 --- a/charts/docker-mailserver/Chart.yaml +++ b/charts/docker-mailserver/Chart.yaml @@ -20,3 +20,5 @@ icon: https://avatars.githubusercontent.com/u/76868633?s=400&v=4 annotations: artifacthub.io/changes: | - Breaking : Standardized app labels to app.kubernetes.io/name for Istio workload/Cilium compatibility + +dependencies: [] \ No newline at end of file From 6724d73ee4591ed822f134e9410007efd477c8a9 Mon Sep 17 00:00:00 2001 From: Charlie Savage Date: Sun, 28 Jan 2024 22:00:22 -0800 Subject: [PATCH 45/66] Fix lint errors --- charts/docker-mailserver/values.yaml | 2 -- 1 file changed, 2 deletions(-) diff --git a/charts/docker-mailserver/values.yaml b/charts/docker-mailserver/values.yaml index 471c13d0..0805c220 100644 --- a/charts/docker-mailserver/values.yaml +++ b/charts/docker-mailserver/values.yaml @@ -420,7 +420,6 @@ metrics: enabled: false scrapeInterval: 15s - ## ConfigMaps (and Secrets) are used to copy docker-mailserver configuration files ## into running containers. This chart automatically sets up any config files that ## are stored in its chart/config directory. @@ -596,7 +595,6 @@ configFiles: EOS {{- end }} - ## The secrets key works the same way as the configs key. Use secrets to store sensitive information, ## such as DKIM signing keys. ## From 68a3876a1f289838f502ca00377ed42175d8b0c7 Mon Sep 17 00:00:00 2001 From: Charlie Savage Date: Sun, 28 Jan 2024 22:00:48 -0800 Subject: [PATCH 46/66] Don't validate maintainers so lint will pass on forked repositories --- .ci/ct-config.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.ci/ct-config.yaml b/.ci/ct-config.yaml index 3a4dfccf..66e88627 100644 --- a/.ci/ct-config.yaml +++ b/.ci/ct-config.yaml @@ -1,3 +1,4 @@ # This file defines the config for "ct" (chart tester) used by the helm linting GitHub workflow -lint-conf: .ci/lint-config.yaml \ No newline at end of file +lint-conf: .ci/lint-config.yaml +validate-maintainers: false \ No newline at end of file From 79a15d39f07013b913eca15726ad14ca6dce2a52 Mon Sep 17 00:00:00 2001 From: Charlie Savage Date: Mon, 29 Jan 2024 00:19:59 -0800 Subject: [PATCH 47/66] Update readme and comments in values.yaml --- charts/docker-mailserver/README.md | 129 +++++++++++---------------- charts/docker-mailserver/values.yaml | 36 ++++---- 2 files changed, 69 insertions(+), 96 deletions(-) diff --git a/charts/docker-mailserver/README.md b/charts/docker-mailserver/README.md index 828835a0..29066b10 100644 --- a/charts/docker-mailserver/README.md +++ b/charts/docker-mailserver/README.md @@ -4,11 +4,13 @@ - [Introduction](#introduction) - [Prerequisites](#prerequisites) - [Getting Started](#getting-started) -- [Create Configuration Files](#create-configuration-files) +- [Configuration Files](#configuration-files) - [Values YAML](#values-yaml) - [Minimal Configuration](#minimal-configuration) - [Environmental Variables](#environmental-variables) - [Ports](#ports) + - [Persistence](#persistence) +- [Upgrading to Version 3.0.0](#upgrading-to-version-3.0.0) - [Development](#development) - [Testing](#testing) @@ -21,6 +23,8 @@ Kubernetes cluster. docker-mailserver is a production-ready, fullstack mail server that supports SMTP, IMAP, LDAP, Anti-spam, Anti-virus, etc.). Just as importantly, it is designed to be simple to install and configure. +!!WARNING!! - Version 3.0.0 is not backwards compatible with previous Chart versions. Please refer to the [upgrade](#upgrading-to-version-3.0.0) section. + ## Prerequisites - [Helm](https://helm.sh) - A [Kubernetes](https://kubernetes.io/releases/) cluster with persistent storage and access to email [ports](https://docker-mailserver.github.io/docker-mailserver/latest/config/security/understanding-the-ports/#overview-of-email-ports) @@ -50,7 +54,7 @@ This will create a new `postfix-accounts.cf` file: cat /tmp/docker-mailserver/postfix-accounts.cf ``` -## Create Configuration Files +## Configuration Files Assuming you still have a command prompt open in the running container, run the setup command to see additional configuration options: ```console @@ -69,7 +73,7 @@ Configuration files are stored inside the container at `/tmp/docker-mailserver` For extensive configuration documentation, please refer to [configuration](https://docker-mailserver.github.io/docker-mailserver/latest/config/environment/). ## Values YAML -In addition to the configuration files generated above, the `values.yaml` file contains a number of knobs for customizing the docker-mailserver installation. +In addition to the configuration files generated above, the `values.yaml` file contains a number of knobs for customizing the docker-mailserver installation. Please refer to the extensive comments in [values.yaml](./values.yaml) for additional information. By default, the Chart enables `rspamd` and disables `opendkim`, `dmarc`, `policyd-spf` and `clamav`. This is the setup [recommended] (https://docker-mailserver.github.io/docker-mailserver/latest/config/best-practices/dkim_dmarc_spf/) by the docker-mailserver project. @@ -115,10 +119,9 @@ If you are running a bare-metal Kubernetes cluster, you will need to expose port This can get a bit complicated, as explained in the docker-mailserver (documentation)[https://docker-mailserver.github.io/docker-mailserver/latest/config/advanced/kubernetes/#exposing-your-mail-server-to-the-outside-world]. -If you disable the PROXY protocol and your mail server is not exposed using a load-balancer service with an external traffic policy in "Local" mode, then all incoming mail traffic will look like it comes from a local Kubernetes cluster IP. - -One approach to preserving the client IP address is to use the PROXY protocol, which is also explained in the (documentation)[https://docker-mailserver.github.io/docker-mailserver/latest/config/advanced/kubernetes/#proxy-port-to-service-via-proxy-protocol]. +One approach to preserving the client IP address is to use the PROXY protocol, which is explained in the (documentation)[https://docker-mailserver.github.io/docker-mailserver/latest/config/advanced/kubernetes/#proxy-port-to-service-via-proxy-protocol]. +### Proxy Protocol The Helm chart supports the use of the proxy protocol via the `proxy_protocol` key. To enable it set the `proxy_protocol.enable` key to true. You will also want to set the `trustedNetworks` key. ```yaml @@ -139,7 +142,9 @@ Enabling the PROXY protocol will create a new port for each protocol by adding 1 | pop3 | 110 | 10110 | | pop3s | 995 | 10995 | -## Volumes +If you disable the PROXY protocol and your mail server is not exposed using a load-balancer service with an external traffic policy in "Local" mode, then all incoming mail traffic will look like it comes from a local Kubernetes cluster IP. + +## Persistence By default, the Chart requests creates four PersistentVolumeClaims. These are defined under the `persistence` key: | PVC Name | Default Size | Mount | Description | @@ -149,79 +154,45 @@ By default, the Chart requests creates four PersistentVolumeClaims. These are de | mail-state | 1Gi | /var/mail-state | Stores [state](https://docker-mailserver.github.io/docker-mailserver/latest/faq/#what-about-the-docker-datadmsmail-state-directory) for mail services | | mail-log | 1Gi | /var/log/mail | Stores log files | -## Chart Values -The following table lists the configurable parameters of the docker-mailserver chart and their default values. - -| Parameter | Description | Default | -|------------------|----------------------------------------------------------|-------------------------------| -| `image.name` | The name of the container image to use | `mailserver/docker-mailserver` | -| `image.tag` | The image tag to use (You may prefer "latest" over "v6.1.0", for example) | `release-v6.1.0` | -| `demoMode.enabled` | Start the container with a demo "user@example.com" user (password is "password") | `true` | -| `haproxy.enabled` | Support HAProxy PROXY protocol on SMTP, IMAP(S), and POP3(S) connections. Provides real source IP instead of load balancer IP | `false` | -| `haproxy.trustedNetworks` | The IPs (*in space-separated CIDR format*) from which to trust inbound HAProxy-enabled connections | `"10.0.0.0/8 192.168.0.0/16 172.16.0.0/16"` | -| `domains` | List of domains to be served | `[]` | -| `livenessTests.enabled` | Whether to execute liveness tests by running (arbitrary) commands in the docker-mailserver container. Useful to detect component failure (*i.e., clamd dies due to memory pressure*) | `true` | -| `livenessTests.enabled` | Array of commands to execute in sequence, to determine container health. A non-zero exit of any command is considered a failure | `[ "clamscan /tmp/docker-mailserver/TrustedHosts" ]` | -| `pod.dockermailserver.hostNetwork` | Whether the pod should be connected to the "host" network (a primitive solution to ingress NAT problem) | `false` | | -| `pod.dockermailserver.hostPID` | Not really sure. TBD. | `None` | | -| `pod.dockermailserver.securityContext.privileged` | Whether to run this pod in "privileged" mode. | `false` | -| `service.type` | What scope the service should be exposed in (*LoadBalancer/NodePort/ClusterIP*) | `NodePort` | -| `service.loadBalancer.publicIp` | The public IP to assign to the service (*if LoadBalancer*) scope selected above | `None` | -| `service.loadBalancer.allowedIps` | The IPs allowed to access the sevice, in CIDR format (*if LoadBalancer*) scope selected above | `[ "0.0.0.0/0" ]` | -| `service.nodeport.smtp` | The port exposed on the node the container is running on, which will be forwarded to docker-mailserver's SMTP port (25) | `30025` | -| `service.nodeport.pop3` | The port exposed on the node the container is running on, which will be forwarded to docker-mailserver's POP3 port (110) | `30110` | -| `service.nodeport.imap` | The port exposed on the node the container is running on, which will be forwarded to docker-mailserver's IMAP port (143) | `30143` | -| `service.nodeport.smtps` | The port exposed on the node the container is running on, which will be forwarded to docker-mailserver's SMTPS port (465) | `30465` | -| `service.nodeport.submission` | The port exposed on the node the container is running on, which will be forwarded to docker-mailserver's submission (*SMTP-over-TLS*) port (587) | `30587` | -| `service.nodeport.imaps` | The port exposed on the node the container is running on, which will be forwarded to docker-mailserver's IMAPS port (993) | `30993` | -| `service.nodeport.pop3s` | The port exposed on the node the container is running on, which will be forwarded to docker-mailserver's IMAPS port (993) | `30995` | -| `deployment.replicas` | How many instances of the container to deploy (*only 1 supported currently*) | `1` | -| `resource.requests.cpu` | Initial share of CPU requested for dockermailserver | `1` | -| `resource.requests.memory` | Initial share of RAM requested dockermailserver (*Initial testing showed clamd would fail due to memory pressure with less than 1.5GB | `1536Mi` | -| `resource.limits.cpu` | Maximum share of CPU available dockermailserver | `2` | -| `resource.limits.memory` | Maximum share of RAM available dockermailserverv | `2048Mi` | -| `persistence.size` | How much space to provision for persistent storage | `10Gi` | -| `persistence.annotations` | Annotations to add to the persistent storage (*for example, to support [k8s-snapshots](https://github.com/miracle2k/k8s-snapshots)*) | `{}` | -| `ssl.issuer.name` | The name of the cert-manager issuer expected to issue certs | `letsencrypt-staging` | -| `ssl.issuer.kind` | Whether the issuer is namespaced (`Issuer`) on cluster-wide (`ClusterIssuer`) | `ClusterIssuer` | -| `ssl.dnsname` | DNS domain used for DNS01 validation | `example.com` | -| `ssl.dns01provider` | The cert-manager DNS01 provider (*more details [coming](https://github.com/funkypenguin/docker-mailserver/issues/6)*) | `cloudflare` | -| `runtimeClassName` | Optionally, set the pod's [runtimeClass](https://kubernetes.io/docs/concepts/containers/runtime-class/) | `""` | -| `priorityClassName` | Optionally, set the pod's [priorityClass](https://kubernetes.io/docs/concepts/scheduling-eviction/pod-priority-preemption/) | `""` | - -#### HA Proxy-Ingress Configuration - -| Parameter | Description | Default | -|-------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------| -| `haproxy.deploy_chart` | Whether to deploy the HAProxy Ingress Controller (recomended) | `true` | -| `haproxy.controller.kind` | Whether your controller is a `DaemonSet` or a `Deployment` | `Deployment` | -| `haproxy.enableStaticPorts` | Whether to enable ports 80 and 443 in addition to the TCP ports we're using below | `false` | -| `haproxy.tcp.25` | How to forward inbound TCP connections on port 25. Use syntax `/:[]` | `default/docker-mailserver:25::PROXY-V1` | -| `haproxy.tcp.110` | How to forward inbound TCP connections on port 110. Use syntax described above. | `default/docker-mailserver:25::PROXY-V1` | -| `haproxy.tcp.143` | How to forward inbound TCP connections on port 143. Use syntax described above. | `default/docker-mailserver:143::PROXY-V1` | -| `haproxy.tcp.465` | How to forward inbound TCP connections on port 465. Use syntax described above. PROXY protocol unsupported. | `default/docker-mailserver:465` | -| `haproxy.tcp.587` | How to forward inbound TCP connections on port 587. Use syntax described above. PROXY protocol unsupported. | `default/docker-mailserver:587` | -| `haproxy.tcp.993` | How to forward inbound TCP connections on port 993. Use syntax described above. | `default/docker-mailserver:993::PROXY-V1` | -| `haproxy.tcp.995` | How to forward inbound TCP connections on port 995. Use syntax described above. | `default/docker-mailserver:995::PROXY-V1` | -| `haproxy.service.externalTrafficPolicy` | Used to preserve source IP per [this doc](https://kubernetes.io/docs/tutorials/services/source-ip/#source-ip-for-services-with-type-loadbalancer) | `Local` | - - -#### postfix exporter metrics -* use dashboard : https://grafana.com/grafana/dashboards/10013-postfix/ - -| Parameter | Description | Default | -|-----------------------------------------|-----------------------------------------------------------------------------------------------|--------------------------------------------------------------------| -| `metrics.enabled` | enable postfix exporter metrics for prometheus | `false` | -| `metrics.resource.requests.memory` | Initial share of RAM for metrics sidecar | `256Mi` | -| `metrics.resource.limits.memory` | Maximum share of RAM for metrics sidecar | `null` | -| `metrics.resource.limits.cpu` | Maximum share of CPU available for metrics | `null` | -| `metrics.resource.requests.cpu` | Iniyial share of CPU available per-pod | `null` | -| `metrics.image.name` | The name of the container image to use | `blackflysolutions/postfix-exporter@sha256` | -| `metrics.image.tag` | The image tag. If use named tag, then remove @sha256 from name, else put sha256 signed value | `7ed7c0534112aff5b44757ae84a206bf659171631edfc325c3c1638d78e74f73` | -| `metrics.image.pullPolicy` | pullPolicy | `IfNotPresent` | -| `metrics.serviceMonitor.enabled` | generate serviceMonitor for metrics | `false` | -| `metrics.serviceMonitor.scrapeInterval` | default scrape interval | `15s` | +## Upgrading to Version 3+ +Version 3.0 is not backwards compatible with previous versions. The biggest changes include: + +* Usage of four PersistentVolumeClaims (PVCs) including one for configuration files +* Rearrangement of keys in `values.yaml` +* Removal of RainLoop, HaProxy +* Removal of Cert Manager +* Use of rspamd by default + +### PersistentVolumeClaims +Previously the Chart created a single PVC to store emails, logs and the state of various docker-mailserver components. Now the Chart creates four PVCs, as described in the [persistence](#persistence) section. One of the PVCs is `mail-config` which is used to store configuration files. + +The addition of the `mail-config` PVC removes the requirement to use the `setup.sh` script and its dependency on Docker or Podman. Instead, you can directly deploy the chart to a Kubernetes cluster. For more information see the [configuration files](#configuration-files) section. + +To upgrade you will need to copy data from the existing PersistentVolume (PV) to one of the new PVs: + +| Original PV | Path | New PV | Path | +| ----------------- | -----------| -----------|-------| +| docker-mailserver | mail-data | mail-data | / | +| docker-mailserver | mail-state | mail-state | / | +| docker-mailserver | mail-log | mail-log | / | + +### Rearrangement of keys +The location of a number of keys has changed in the chart. These include: + +### Rspamd +The Chart now enables Rpsamd by default as recommened by the docker-mailserver documentation. You can disable this change by setting the env variable `ENABLE_RSPAMD` to 0 and setting `ENABLE_OPENDKIM`, `ENABLE_OPENDMARC` and `ENABLE_POLICYD_SPF` to 1. + +If you keep this change, you will need to generate new DKIM signing keys (see the [configuration files](#configuration-files) section for more information). In addition, you may wish to enable the Rspamd ingress (`rspamd.ingress.enabled`) + +### TLS +Support for creating a TLS certificate using `cert-manager` has been removed. Instead, create a secret that contains a certificate *before* installing the chart and reference it via the `certificate` key. Of course you can use `cert-manager` to create this secret - it is just not part of this chart anymore. + +### Proxy +Support for installing HaProxy with the Chart has been removed. Instead, generic support for the Proxy protocol as been [added](#proxy). + +## Parameters + ## Development diff --git a/charts/docker-mailserver/values.yaml b/charts/docker-mailserver/values.yaml index 0805c220..44414b69 100644 --- a/charts/docker-mailserver/values.yaml +++ b/charts/docker-mailserver/values.yaml @@ -1,35 +1,30 @@ +nameOverride: "" fullnameOverride: "" image: - # image.name is the name of the container image to use. Refer to https://hub.docker.com/r/mailserver/docker-mailserver + # The name of the container image to use. See https://hub.docker.com/r/mailserver/docker-mailserver name: "mailserver/docker-mailserver" - # image.tag is the tag of the container image to use. Refer to https://hub.docker.com/r/mailserver/docker-mailserver + # The tag of the container image to use. See https://hub.docker.com/r/mailserver/docker-mailserver tag: "13.3.1" pullPolicy: "IfNotPresent" +# Specify whether to create a serviceAccount for the pod. The name is generated from the +# dockermailserver.serviceAccountName template serviceAccount: - create: "true" + create: true -## Specify a TLS secret name that contains a certificate and private key for your email domain +## Specify the name of a TLS secret that contains a certificate and private key for your email domain. +## See https://kubernetes.io/docs/concepts/configuration/secret/#tls-secrets certificate: # List extra RBL domains to use for hard reject filtering rblRejectDomains: [] -livenessTests: - # livenessTests.enabled will add a liveness test, which will classify a pod as 'unhealthy' if any livenessTests.commands (below) return non-zero - enabled: true - # livenessTests.commands is an array of commands to be executed within the docker-mailserver container, intended to prove that the container is healthy. Each command must exit 0 under normal (healthy) circumstances - commands: - - "clamscan /tmp/docker-mailserver/TrustedHosts" - deployment: - ## How many versions of the deployment to run on kubernetes - ## Default: 2 + ## How many versions of the deployment to run replicas: 1 - ## Add annotations to the deployment - ## Useful for using something like stash to backup data (https://stash.run/docs/v0.9.0-rc.0/guides/latest/auto-backup/workload/) + ## Optionally add additional annotations to the deployment annotations: {} ## Optionally specify a runtimeClassName for the deployment @@ -38,7 +33,7 @@ deployment: ## Optionally specify a priorityClassName for the deployment priorityClassName: - ## Host networking requested for this pod. Use the host’s network namespace. If this option is set, the ports that + ## Use the host’s network namespace. If this option is set, the ports that ## will be used must be specified. ## Ref: https://kubernetes.io/docs/api-reference/v1/definitions/#_v1_podspec # pod.dockermailserver.hostNetwork will configure the pod to use the host's network namespace @@ -58,6 +53,13 @@ deployment: # maxUnavailable: 1 type: "Recreate" + livenessTests: + # livenessTests.enabled will add a liveness test, which will classify a pod as 'unhealthy' if any livenessTests.commands (below) return non-zero + enabled: true + # livenessTests.commands is an array of commands to be executed within the docker-mailserver container, intended to prove that the container is healthy. Each command must exit 0 under normal (healthy) circumstances + commands: + - "clamscan /tmp/docker-mailserver/TrustedHosts" + ## The following variables affect the behaviour of docker-mailserver ## See https://docker-mailserver.github.io/docker-mailserver/latest/config/environment/ for details ## Note that an empty value indicates the default as described in the docs above @@ -227,7 +229,7 @@ deployment: # Whether to enable dovecot replication. Allows the syncronization of a pair of dovecot servers # https://wiki.dovecot.org/Replication - enable_dovecot_replication: true + enable_dovecot_replication: false securityContext: runAsUser: 5000 From c4c13c0eb05942625d72085b0f4e502ebe181319 Mon Sep 17 00:00:00 2001 From: Charlie Savage Date: Mon, 29 Jan 2024 23:13:44 -0800 Subject: [PATCH 48/66] More readme updates --- charts/docker-mailserver/README.md | 58 +++++++++++++++--------------- 1 file changed, 30 insertions(+), 28 deletions(-) diff --git a/charts/docker-mailserver/README.md b/charts/docker-mailserver/README.md index 29066b10..804a927f 100644 --- a/charts/docker-mailserver/README.md +++ b/charts/docker-mailserver/README.md @@ -1,16 +1,20 @@ ## Contents -- [Contents](#contents) - [Introduction](#introduction) - [Prerequisites](#prerequisites) - [Getting Started](#getting-started) -- [Configuration Files](#configuration-files) +- [Configuration](#configuration) + - [Volume](#volume) + - [ConfigMaps](#config-maps) + - [Secrets](#secrets) - [Values YAML](#values-yaml) + - [Environment Variables](#environment-variables) - [Minimal Configuration](#minimal-configuration) - - [Environmental Variables](#environmental-variables) - - [Ports](#ports) - - [Persistence](#persistence) -- [Upgrading to Version 3.0.0](#upgrading-to-version-3.0.0) + - [Certificate](#certificate) +- [Ports](#ports) + - [Proxy Protocol](#proxy-protocol) +- [Persistence](#persistence) +- [Upgrading to Version 3.0.0](#upgrading-to-version-3) - [Development](#development) - [Testing](#testing) @@ -19,11 +23,9 @@ ## Introduction This chart deploys [Docker Mailserver](https://github.com/docker-mailserver/docker-mailserver) into a -Kubernetes cluster. +Kubernetes cluster. Docker Mailserver is a production-ready, fullstack mail server that supports SMTP, IMAP, LDAP, Anti-spam, Anti-virus, etc.). Just as importantly, it is designed to be simple to install and configure. -docker-mailserver is a production-ready, fullstack mail server that supports SMTP, IMAP, LDAP, Anti-spam, Anti-virus, etc.). Just as importantly, it is designed to be simple to install and configure. - -!!WARNING!! - Version 3.0.0 is not backwards compatible with previous Chart versions. Please refer to the [upgrade](#upgrading-to-version-3.0.0) section. +!!WARNING!! - Version 3.0.0 is not backwards compatible with previous versions. Please refer to the [upgrade](#upgrading-to-version-3) section for more information. ## Prerequisites - [Helm](https://helm.sh) @@ -32,7 +34,7 @@ docker-mailserver is a production-ready, fullstack mail server that supports SMT - Correctly configured [DNS](https://docker-mailserver.github.io/docker-mailserver/latest/usage/#minimal-dns-setup) ## Getting Started -Setting up docker-mailserver requires generating a number of configuration (files)[https://docker-mailserver.github.io/docker-mailserver/latest/config/advanced/optional-config/]. To make this easier, docker-mailserver includes a `setup` command that will generate these files. +Setting up docker-mailserver requires generating a number of configuration [files](https://docker-mailserver.github.io/docker-mailserver/latest/config/advanced/optional-config/). To make this easier, docker-mailserver includes a `setup` command that will generate these files. To get started, first install docker-mailserver: @@ -48,43 +50,48 @@ kubectl exec -it --namespace mail deploy/docker-mailserver -- bash setup email add user@example.com password ``` -This will create a new `postfix-accounts.cf` file: +This will create a new `postfix-accounts.cf` file at: ```console cat /tmp/docker-mailserver/postfix-accounts.cf ``` -## Configuration Files -Assuming you still have a command prompt open in the running container, run the setup command to see additional configuration options: +## Configuration +Assuming you still have a command prompt [open](#getting-started) in the running container, run the setup command to see additional configuration options: ```console setup -```console +``` As you run various setup commands, additional files will be generated. At a minimum you will want to run: ```console -setup dovecot-master add user@example.com password +setup email add user@example.com password setup config dkim keysize 2048 domain 'example.com' ``` -Configuration files are stored inside the container at `/tmp/docker-mailserver` which by default is mapped to a Kubernetes volume. You may of course add additional configuration files to the volume as needed. +### Volume +Configuration files are stored on a Kubernetes [volume](#persistence) mounted at `/tmp/docker-mailserver` in the container. The PVC is named `mail-config`. You may of course add additional configuration files to the volume as needed. + +### ConfigMaps +Its is also possible to use ConfigMaps to mount configuration files in the container. This is done by adding to the `configFiles` key in a custom `values.yaml` file. For more information please see the [documentation](./values.yaml#437) in values.yaml -For extensive configuration documentation, please refer to [configuration](https://docker-mailserver.github.io/docker-mailserver/latest/config/environment/). +### Secrets +Secrets can also be used to mount configuration files in the container. For example, dkim keys could be stored in a secret as opposed to a file in the `mail-config` volume. Once again, for more information please see the [documentation](./values.yaml#617) in values.yaml ## Values YAML In addition to the configuration files generated above, the `values.yaml` file contains a number of knobs for customizing the docker-mailserver installation. Please refer to the extensive comments in [values.yaml](./values.yaml) for additional information. -By default, the Chart enables `rspamd` and disables `opendkim`, `dmarc`, `policyd-spf` and `clamav`. This is the setup [recommended] (https://docker-mailserver.github.io/docker-mailserver/latest/config/best-practices/dkim_dmarc_spf/) by the docker-mailserver project. +### Environment Variables +Included in the knobs are **many** environment variables which allow you to customize the behaviour of docker-mailserver. For extensive configuration documentation, please refer to [configuration](https://docker-mailserver.github.io/docker-mailserver/latest/config/environment/). Note that `docker-mailserver` expects any true/false values to be set as numbers (1/0) rather than boolean values (true/false). -It also provides a secondary mechanism for adding config files and secrets via the `configFiles` and `secrets` keys. +By default, the Chart enables `rspamd` and disables `opendkim`, `dmarc`, `policyd-spf` and `clamav`. This is the setup [recommended] (https://docker-mailserver.github.io/docker-mailserver/latest/config/best-practices/dkim_dmarc_spf/) by the docker-mailserver project. Once you have created your own values.yaml files, then redeploy the chart like this: ```console helm upgrade docker-mailserver docker-mailserver --namespace mail --values ``` - You can also override individual configuration setting with `helm upgrade --set`, specifying each parameter using the `--set key=value[,key=value]` argument to `helm install`. For example: ```console @@ -99,11 +106,6 @@ There are various settings in `values.yaml` that you must override. | deployment.env.[OVERRIDE_HOSTNAME](https://docker-mailserver.github.io/docker-mailserver/latest/config/environment/#override_hostname) | The hostname to be presented on SMTP banners | mail.example.com | | certificate | Name of a Kubernetes secret that stores TLS certificate for mail domain | | -### Environmental Variables -There are **many** environment variables which allow you to customize the behaviour of docker-mailserver. The function of each variable is described at https://github.com/docker-mailserver/docker-mailserver#environment-variables - -Every variable can be set using `values.yaml`, but note that docker-mailserver expects any true/false values to be set as binary numbers (1/0), rather than boolean (true/false). BadThings(tm) will happen if you try to pass an environment variable as "true" when [`start-mailserver.sh`](https://github.com/docker-mailserver/docker-mailserver/blob/master/target/start-mailserver.sh) is expecting a 1 or a 0! - ### Certificate You will need to setup a TLS certificate for your email domain. The easiest way to do this is use (cert-manager)[https://cert-manager.io/]. @@ -114,7 +116,7 @@ certificate: my-certificate-secret ``` The chart will then automatically copy the certificate and private key to the `/tmp/dms/custom-certs` director in the container and set correctly set the `SSL_CERT_PATH` and `SSL_KEY_PATH` environment variables. -### Ports +## Ports If you are running a bare-metal Kubernetes cluster, you will need to expose ports to the internet to receive and send emails. In addition, you need to make sure that docker-mailserver receives the correct client IP address so that spam filtering works. This can get a bit complicated, as explained in the docker-mailserver (documentation)[https://docker-mailserver.github.io/docker-mailserver/latest/config/advanced/kubernetes/#exposing-your-mail-server-to-the-outside-world]. @@ -154,7 +156,7 @@ By default, the Chart requests creates four PersistentVolumeClaims. These are de | mail-state | 1Gi | /var/mail-state | Stores [state](https://docker-mailserver.github.io/docker-mailserver/latest/faq/#what-about-the-docker-datadmsmail-state-directory) for mail services | | mail-log | 1Gi | /var/log/mail | Stores log files | -## Upgrading to Version 3+ +## Upgrading to Version 3 Version 3.0 is not backwards compatible with previous versions. The biggest changes include: * Usage of four PersistentVolumeClaims (PVCs) including one for configuration files From f4e9ff641a36791b650f29173d94785d50dcdda5 Mon Sep 17 00:00:00 2001 From: Charlie Savage Date: Tue, 30 Jan 2024 21:40:07 -0800 Subject: [PATCH 49/66] Make configMaps and secrets dictionaries so they are easier to override in custom values.yaml files --- charts/docker-mailserver/Chart.yaml | 2 +- .../templates/configmap.yaml | 10 +++---- .../templates/deployment.yaml | 26 +++++++++---------- charts/docker-mailserver/values.yaml | 16 ++++++------ 4 files changed, 27 insertions(+), 27 deletions(-) diff --git a/charts/docker-mailserver/Chart.yaml b/charts/docker-mailserver/Chart.yaml index aeb400b1..954675f0 100644 --- a/charts/docker-mailserver/Chart.yaml +++ b/charts/docker-mailserver/Chart.yaml @@ -2,7 +2,7 @@ apiVersion: v2 appVersion: "13.2.0" description: A fullstack but simple mailserver (smtp, imap, antispam, antivirus, ssl...) using Docker. name: docker-mailserver -version: 2.3.0 +version: 2.3.1 sources: - https://github.com/docker-mailserver/docker-mailserver-helm maintainers: diff --git a/charts/docker-mailserver/templates/configmap.yaml b/charts/docker-mailserver/templates/configmap.yaml index 83ad01ca..5ad14533 100644 --- a/charts/docker-mailserver/templates/configmap.yaml +++ b/charts/docker-mailserver/templates/configmap.yaml @@ -1,16 +1,16 @@ -{{- range $config := .Values.configFiles }} +{{- range $name, $config := .Values.configMaps }} {{- if $config.create }} -apiVersion: "v1" -kind: "ConfigMap" +apiVersion: v1 +kind: ConfigMap metadata: labels: app.kubernetes.io/name: {{ template "dockermailserver.fullname" $ }} chart: "{{ $.Chart.Name }}-{{ $.Chart.Version }}" heritage: "{{ $.Release.Service }}" release: "{{ $.Release.Name }}" - name: {{ regexReplaceAll "[.]" $config.name "-" }} + name: {{ regexReplaceAll "[.]" $name "-" }} data: - {{ $config.key | default $config.name }}: | + {{ $config.key | default $name }}: | {{ tpl $config.data $ | indent 6 }} --- {{- end }} diff --git a/charts/docker-mailserver/templates/deployment.yaml b/charts/docker-mailserver/templates/deployment.yaml index bf652332..49203ef0 100644 --- a/charts/docker-mailserver/templates/deployment.yaml +++ b/charts/docker-mailserver/templates/deployment.yaml @@ -36,17 +36,17 @@ spec: {{ toYaml .Values.securityContext | indent 8 }} volumes: # ConfigMaps - {{- range $config := .Values.configFiles }} - - name: {{ regexReplaceAll "[.]" $config.name "-" }} + {{- range $name, $config := .Values.configMaps }} + - name: {{ regexReplaceAll "[.]" $name "-" }} configMap: - name: {{ regexReplaceAll "[.]" $config.name "-" }} + name: {{ regexReplaceAll "[.]" $name "-" }} {{- end }} # Secrets - {{- range $secret := .Values.secrets }} - - name: {{ regexReplaceAll "[.]" $secret.name "-" }} + {{- range $name, $secret := .Values.secrets }} + - name: {{ regexReplaceAll "[.]" $name "-" }} secret: - secretName: {{ regexReplaceAll "[.]" $secret.name "-" }} + secretName: {{ regexReplaceAll "[.]" $name "-" }} {{- end }} # Certificate @@ -105,10 +105,10 @@ spec: readOnly: true {{- end }} - # ConfigFiles via ConfigMaps - {{- range $config := .Values.configFiles }} - - name: {{ regexReplaceAll "[.]" $config.name "-" }} - subPath: {{ $config.key | default $config.name }} + # Config via ConfigMaps + {{- range $name, $config := .Values.configMaps }} + - name: {{ regexReplaceAll "[.]" $name "-" }} + subPath: {{ $config.key | default $name }} {{- if isAbs $config.path }} mountPath: {{ $config.path }} {{- else }} @@ -117,9 +117,9 @@ spec: {{- end }} # Config via Secrets - {{- range $secret := .Values.secrets }} - - name: {{ regexReplaceAll "[.]" $secret.name "-" }} - subPath: {{ $secret.key | default $secret.name }} + {{- range $name, $secret := .Values.secrets }} + - name: {{ regexReplaceAll "[.]" $name "-" }} + subPath: {{ $secret.key | default $name }} {{- if isAbs $secret.path }} mountPath: {{ $secret.path }} {{- else }} diff --git a/charts/docker-mailserver/values.yaml b/charts/docker-mailserver/values.yaml index 44414b69..14558fb2 100644 --- a/charts/docker-mailserver/values.yaml +++ b/charts/docker-mailserver/values.yaml @@ -434,8 +434,8 @@ metrics: ## files by either referencing existing ConfigMaps (that you create before installing the Chart) ## or by creating new ones (set the create key to true). ## -configFiles: - - name: dovecot.cf +configMaps: + dovecot.cf: create: true path: dovecot.cf data: | @@ -493,7 +493,7 @@ configFiles: {{- end -}} {{- end -}} - - name: fts-xapian-plugin.conf + fts-xapian-plugin.conf: create: true path: /etc/dovecot/conf.d/10-plugin.conf data: | @@ -532,7 +532,7 @@ configFiles: } {{- end -}} - - name: 91-override-sieve.conf + 91-override-sieve.conf: create: true path: 91-override-sieve.conf data: | @@ -541,19 +541,19 @@ configFiles: sieve_dir = /var/mail/sieve/%d/%n/sieve } - - name: am-i-health.sh + am-i-health.sh: create: true path: am-i-health.sh data: | #!/bin/bash # this script is intended to be used by periodic kubernetes liveness probes to ensure that the container # (and all its dependent services) is healthy - {{ range .Values.livenessTests.commands -}} + {{ range .Values.deployment.livenessTests.commands -}} {{ . }} && \ {{- end }} echo "All healthy" - - name: user-patches.sh + user-patches.sh: create: true path: user-patches.sh data: | @@ -614,4 +614,4 @@ configFiles: ## If you set the create key to false, then you must manually create the ConfigMaps before deploying the chart. ## ## kubectl create secret rspamd.example.com --namespace mail --from-file=rspamd.dkim.rsa-2048-mail-example.com.private.txt= -secrets: [] +secrets: {} From 459efc81fc77402f287b7b481c43766f2bc6bf68 Mon Sep 17 00:00:00 2001 From: Charlie Savage Date: Tue, 30 Jan 2024 22:06:13 -0800 Subject: [PATCH 50/66] Rename proxy_protocol to proxyProtocol to follow standard helm naming conventions --- charts/docker-mailserver/templates/NOTES.txt | 2 +- charts/docker-mailserver/templates/deployment.yaml | 6 +++--- charts/docker-mailserver/templates/service.yaml | 6 +++--- charts/docker-mailserver/tests/configmap_test.yaml | 4 ++-- charts/docker-mailserver/tests/haproxy_test.yaml | 6 +++--- charts/docker-mailserver/tests/oobe_test.yaml | 6 +++--- charts/docker-mailserver/values.yaml | 12 ++++++------ 7 files changed, 21 insertions(+), 21 deletions(-) diff --git a/charts/docker-mailserver/templates/NOTES.txt b/charts/docker-mailserver/templates/NOTES.txt index ff84d9a6..59af26e7 100644 --- a/charts/docker-mailserver/templates/NOTES.txt +++ b/charts/docker-mailserver/templates/NOTES.txt @@ -23,7 +23,7 @@ Next, run the setup command to see additional options: For more information please refer to this Chart's README file. -{{ if .Values.proxy_protocol.enabled -}} +{{ if .Values.proxyProtocol.enabled -}} Proxy Ports ------------ diff --git a/charts/docker-mailserver/templates/deployment.yaml b/charts/docker-mailserver/templates/deployment.yaml index 49203ef0..38bda397 100644 --- a/charts/docker-mailserver/templates/deployment.yaml +++ b/charts/docker-mailserver/templates/deployment.yaml @@ -161,7 +161,7 @@ spec: containerPort: 465 - name: submission containerPort: 587 - {{- if .Values.proxy_protocol.enabled }} + {{- if .Values.proxyProtocol.enabled }} - name: subs-proxy containerPort: 10465 - name: sub-proxy @@ -173,7 +173,7 @@ spec: containerPort: 143 - name: imaps containerPort: 993 - {{- if .Values.proxy_protocol.enabled }} + {{- if .Values.proxyProtocol.enabled }} - name: imap-proxy containerPort: 10143 - name: imaps-proxy @@ -186,7 +186,7 @@ spec: containerPort: 110 - name: pop3s containerPort: 995 - {{- if .Values.proxy_protocol.enabled }} + {{- if .Values.proxyProtocol.enabled }} - name: pop3-proxy containerPort: 10110 - name: pop3s-proxy diff --git a/charts/docker-mailserver/templates/service.yaml b/charts/docker-mailserver/templates/service.yaml index daa82a03..f90f2bdf 100644 --- a/charts/docker-mailserver/templates/service.yaml +++ b/charts/docker-mailserver/templates/service.yaml @@ -51,7 +51,7 @@ spec: {{- if eq .Values.service.type "NodePort" }} nodePort: {{ default "30587" .Values.service.nodePort.submission }} {{ end }} - {{- if .Values.proxy_protocol.enabled }} + {{- if .Values.proxyProtocol.enabled }} - name: subs-proxy targetPort: subs-proxy port: 10465 @@ -73,7 +73,7 @@ spec: {{- if eq .Values.service.type "NodePort" }} nodePort: {{ default "30993" .Values.service.nodePort.imaps }} {{- end }} - {{- if .Values.proxy_protocol.enabled }} + {{- if .Values.proxyProtocol.enabled }} - name: imap-proxy targetPort: imap-proxy port: 10143 @@ -96,7 +96,7 @@ spec: {{- if eq .Values.service.type "NodePort" }} nodePort: {{ default "30995" .Values.service.nodePort.pop3s }} {{- end }} - {{- if .Values.proxy_protocol.enabled }} + {{- if .Values.proxyProtocol.enabled }} - name: pop3-proxy targetPort: pop3-proxy port: 10110 diff --git a/charts/docker-mailserver/tests/configmap_test.yaml b/charts/docker-mailserver/tests/configmap_test.yaml index 49d0c40a..8c2f6484 100644 --- a/charts/docker-mailserver/tests/configmap_test.yaml +++ b/charts/docker-mailserver/tests/configmap_test.yaml @@ -12,9 +12,9 @@ tests: pattern: "dbpurgeage" - - it: should configure imaps port 10993 if proxy_protocol enabled + - it: should configure imaps port 10993 if proxyProtocol enabled set: - proxy_protocol.enabled: true + proxyProtocol.enabled: true asserts: - matchRegex: path: data.dovecot\.cf diff --git a/charts/docker-mailserver/tests/haproxy_test.yaml b/charts/docker-mailserver/tests/haproxy_test.yaml index ba061af5..a28da118 100644 --- a/charts/docker-mailserver/tests/haproxy_test.yaml +++ b/charts/docker-mailserver/tests/haproxy_test.yaml @@ -4,9 +4,9 @@ templates: - deployment-poor-mans-k8s-lb.yaml tests: - - it: should not add proxy_protocol options to postfix/dovecot if proxy_protocol support is not enabled + - it: should not add proxyProtocol options to postfix/dovecot if proxyProtocol support is not enabled set: - proxy_protocol.enabled: false + proxyProtocol.enabled: false asserts: - notMatchRegex: path: data.postfix-main\.cf @@ -14,7 +14,7 @@ tests: - isNull: path: data.dovecot\.cf - - it: should create phonehome deployment if proxy_protocol is enabled and set to external-auto mode + - it: should create phonehome deployment if proxyProtocol is enabled and set to external-auto mode set: poorMansK8sLb.enabled: true asserts: diff --git a/charts/docker-mailserver/tests/oobe_test.yaml b/charts/docker-mailserver/tests/oobe_test.yaml index 31479d35..48d8d130 100644 --- a/charts/docker-mailserver/tests/oobe_test.yaml +++ b/charts/docker-mailserver/tests/oobe_test.yaml @@ -35,8 +35,8 @@ tests: path: data.postfix-main\.cf pattern: smtpd_recipient_restrictions - # proxy_protocol is enabled by default - - it: should correctly configure postfix/dovecot if proxy_protocol support is enabled + # proxyProtocol is enabled by default + - it: should correctly configure postfix/dovecot if proxyProtocol support is enabled set: asserts: - matchRegex: @@ -46,7 +46,7 @@ tests: path: data.dovecot\.cf pattern: haproxy - - it: should configure imaps port 10993 if proxy_protocol is enabled + - it: should configure imaps port 10993 if proxyProtocol is enabled set: asserts: - matchRegex: diff --git a/charts/docker-mailserver/values.yaml b/charts/docker-mailserver/values.yaml index 14558fb2..27f33173 100644 --- a/charts/docker-mailserver/values.yaml +++ b/charts/docker-mailserver/values.yaml @@ -388,7 +388,7 @@ rspamd: enabled: false secret: -proxy_protocol: +proxyProtocol: enabled: true # List of sources (in CIDR format, space-separated) to permit PROXY protocol from trustedNetworks: "10.0.0.0/8 192.168.0.0/16 172.16.0.0/16" @@ -439,8 +439,8 @@ configMaps: create: true path: dovecot.cf data: | - {{- if .Values.proxy_protocol.enabled }} - haproxy_trusted_networks = {{ .Values.proxy_protocol.trustedNetworks }} + {{- if .Values.proxyProtocol.enabled }} + haproxy_trusted_networks = {{ .Values.proxyProtocol.trustedNetworks }} {{- if and (.Values.deployment.env.ENABLE_IMAP) (not .Values.deployment.env.SMTP_ONLY) }} service imap-login { @@ -559,7 +559,7 @@ configMaps: data: | #!/bin/bash - {{- if .Values.proxy_protocol.enabled }} + {{- if .Values.proxyProtocol.enabled }} # Make sure to keep this file in sync with https://github.com/docker-mailserver/docker-mailserver/blob/master/target/postfix/master.cf! cat <> /etc/postfix/master.cf @@ -577,7 +577,7 @@ configMaps: -o smtpd_discard_ehlo_keywords= -o milter_macro_daemon_name=ORIGINATING -o cleanup_service_name=sender-cleanup - -o smtpd_upstream_proxy_protocol=haproxy + -o smtpd_upstream_proxyProtocol=haproxy # Submissions with proxy 10465 inet n - n - - smtpd @@ -593,7 +593,7 @@ configMaps: -o smtpd_discard_ehlo_keywords= -o milter_macro_daemon_name=ORIGINATING -o cleanup_service_name=sender-cleanup - -o smtpd_upstream_proxy_protocol=haproxy + -o smtpd_upstream_proxyProtocol=haproxy EOS {{- end }} From cb1d09585fb207057db76ab92f7c804cb6889f97 Mon Sep 17 00:00:00 2001 From: Charlie Savage Date: Tue, 30 Jan 2024 22:12:36 -0800 Subject: [PATCH 51/66] Updated readme files --- README.md | 6 +-- charts/docker-mailserver/README.md | 86 +++++++++++++++--------------- 2 files changed, 45 insertions(+), 47 deletions(-) diff --git a/README.md b/README.md index 690cac28..f120c702 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,6 @@ # K8s Helm Chart for Docker Mailserver -This repostitory contains a helm chart to deploy [Docker -Mailserver](https://github.com/docker-mailserver/docker-mailserver), a -production-ready fullstack but simple mail server, into a Kubernetes cluster. - -**_LOOKING FOR MAINTAINERS_**! This repository is looking for maintainers that keep the image version up to date and curate the chart. The chart is currently outdated with regards to the image itself as well as documentation and other, related topics. If you are using this Chart, it would be of much help if you provide a solution for issues you encountred in the form of a pull request. +This repostitory contains a helm chart to deploy [Docker Mailserver](https://github.com/docker-mailserver/docker-mailserver) into a Kubernetes cluster. Docker Mailserver is a production-ready, fullstack mail server that supports SMTP, IMAP, LDAP, Anti-spam, Anti-virus, etc.). Just as importantly, it is designed to be simple to install and configure. ## Documentation diff --git a/charts/docker-mailserver/README.md b/charts/docker-mailserver/README.md index 804a927f..38972bb0 100644 --- a/charts/docker-mailserver/README.md +++ b/charts/docker-mailserver/README.md @@ -12,7 +12,6 @@ - [Minimal Configuration](#minimal-configuration) - [Certificate](#certificate) - [Ports](#ports) - - [Proxy Protocol](#proxy-protocol) - [Persistence](#persistence) - [Upgrading to Version 3.0.0](#upgrading-to-version-3) - [Development](#development) @@ -21,8 +20,7 @@ (Created by [gh-md-toc](https://github.com/ekalinin/github-markdown-toc.go)) ## Introduction -This chart deploys [Docker -Mailserver](https://github.com/docker-mailserver/docker-mailserver) into a +This chart deploys [Docker Mailserver](https://github.com/docker-mailserver/docker-mailserver) into a Kubernetes cluster. Docker Mailserver is a production-ready, fullstack mail server that supports SMTP, IMAP, LDAP, Anti-spam, Anti-virus, etc.). Just as importantly, it is designed to be simple to install and configure. !!WARNING!! - Version 3.0.0 is not backwards compatible with previous versions. Please refer to the [upgrade](#upgrading-to-version-3) section for more information. @@ -34,7 +32,7 @@ Kubernetes cluster. Docker Mailserver is a production-ready, fullstack mail serv - Correctly configured [DNS](https://docker-mailserver.github.io/docker-mailserver/latest/usage/#minimal-dns-setup) ## Getting Started -Setting up docker-mailserver requires generating a number of configuration [files](https://docker-mailserver.github.io/docker-mailserver/latest/config/advanced/optional-config/). To make this easier, docker-mailserver includes a `setup` command that will generate these files. +Setting up docker-mailserver requires generating a number of configuration [files](https://docker-mailserver.github.io/docker-mailserver/latest/config/advanced/optional-config/). To make this easier, docker-mailserver includes a `setup` command that can generate these files. To get started, first install docker-mailserver: @@ -42,7 +40,13 @@ To get started, first install docker-mailserver: helm upgrade --install docker-mailserver docker-mailserver --namespace mail --create-namespace ``` -Next open a command prompt to the running container and create an email account. +Next open a command prompt to the running container. + +```console +kubectl exec -it --namespace mail deploy/docker-mailserver -- bash +``` + +And now create a new account for Postfix and Dovecot. ```console kubectl exec -it --namespace mail deploy/docker-mailserver -- bash @@ -50,12 +54,14 @@ kubectl exec -it --namespace mail deploy/docker-mailserver -- bash setup email add user@example.com password ``` -This will create a new `postfix-accounts.cf` file at: +Account information will be saved in a file `postfix-accounts.cf` in the container path: ```console cat /tmp/docker-mailserver/postfix-accounts.cf ``` +This path is [mapped](#persistence) to a Kubernetes Volume. + ## Configuration Assuming you still have a command prompt [open](#getting-started) in the running container, run the setup command to see additional configuration options: @@ -70,22 +76,24 @@ setup email add user@example.com password setup config dkim keysize 2048 domain 'example.com' ``` +These paths are stored to the container path `/tmp/docker-mailserver` which is [mapped](#persistence) to a Kubernetes Volume. + ### Volume -Configuration files are stored on a Kubernetes [volume](#persistence) mounted at `/tmp/docker-mailserver` in the container. The PVC is named `mail-config`. You may of course add additional configuration files to the volume as needed. +Configuration files are stored on a Kubernetes [volume](#persistence) mounted at `/tmp/docker-mailserver` in the container. The PVC is named `mail-config`. You can of course add additional configuration files to the volume as needed. ### ConfigMaps -Its is also possible to use ConfigMaps to mount configuration files in the container. This is done by adding to the `configFiles` key in a custom `values.yaml` file. For more information please see the [documentation](./values.yaml#437) in values.yaml +Its is also possible to use ConfigMaps to mount configuration files in the container. This is done by adding to the `configFiles` key in a custom `values.yaml` file. For more information please see the [documentation](./values.yaml#L425) in values.yaml ### Secrets -Secrets can also be used to mount configuration files in the container. For example, dkim keys could be stored in a secret as opposed to a file in the `mail-config` volume. Once again, for more information please see the [documentation](./values.yaml#617) in values.yaml +Secrets can also be used to mount configuration files in the container. For example, dkim keys could be stored in a secret as opposed to a file in the `mail-config` volume. Once again, for more information please see the [documentation](./values.yaml#L600) in values.yaml ## Values YAML In addition to the configuration files generated above, the `values.yaml` file contains a number of knobs for customizing the docker-mailserver installation. Please refer to the extensive comments in [values.yaml](./values.yaml) for additional information. ### Environment Variables -Included in the knobs are **many** environment variables which allow you to customize the behaviour of docker-mailserver. For extensive configuration documentation, please refer to [configuration](https://docker-mailserver.github.io/docker-mailserver/latest/config/environment/). Note that `docker-mailserver` expects any true/false values to be set as numbers (1/0) rather than boolean values (true/false). +Included in the knobs are **many** environment variables which allow you to customize the behaviour of `docker-mailserver`. These environment variables are documented in the `docker-mailserver's` [configuration](https://docker-mailserver.github.io/docker-mailserver/latest/config/environment/) page. Note that `docker-mailserver` expects any true/false values to be set as numbers (1/0) rather than boolean values (true/false). -By default, the Chart enables `rspamd` and disables `opendkim`, `dmarc`, `policyd-spf` and `clamav`. This is the setup [recommended] (https://docker-mailserver.github.io/docker-mailserver/latest/config/best-practices/dkim_dmarc_spf/) by the docker-mailserver project. +By default, the Chart enables `rspamd` and disables `opendkim`, `dmarc`, `policyd-spf` and `clamav`. This is the setup [recommended](https://docker-mailserver.github.io/docker-mailserver/latest/config/best-practices/dkim_dmarc_spf/) by the `docker-mailserver` project. Once you have created your own values.yaml files, then redeploy the chart like this: @@ -101,10 +109,10 @@ $ helm upgrade docker-mailserver docker-mailserver --namespace mail --set pod.do ### Minimal Configuration There are various settings in `values.yaml` that you must override. -| Parameter | Description | Default | -| --------------------------------- | --------------------------------------------------- | ---------------- | -| deployment.env.[OVERRIDE_HOSTNAME](https://docker-mailserver.github.io/docker-mailserver/latest/config/environment/#override_hostname) | The hostname to be presented on SMTP banners | mail.example.com | -| certificate | Name of a Kubernetes secret that stores TLS certificate for mail domain | | +| Parameter | Description | Default | +| ------------------------------------ | --------------------------------------------------- | ---------------- | +| deployment.env.[OVERRIDE_HOSTNAME](https://docker-mailserver.github.io/docker-mailserver/latest/config/environment/#override_hostname) The hostname to be presented on SMTP banners | mail.example.com | +| `certificate` | Name of a Kubernetes secret that stores TLS certificate for your mail domain | ### Certificate You will need to setup a TLS certificate for your email domain. The easiest way to do this is use (cert-manager)[https://cert-manager.io/]. @@ -114,26 +122,25 @@ Once you acquire a certificate, you will need to store it in a TLS secret in the ```yaml certificate: my-certificate-secret ``` -The chart will then automatically copy the certificate and private key to the `/tmp/dms/custom-certs` director in the container and set correctly set the `SSL_CERT_PATH` and `SSL_KEY_PATH` environment variables. +The chart will then automatically copy the certificate and private key to the `/tmp/dms/custom-certs` director in the container and correctly set the `SSL_CERT_PATH` and `SSL_KEY_PATH` environment variables. ## Ports -If you are running a bare-metal Kubernetes cluster, you will need to expose ports to the internet to receive and send emails. In addition, you need to make sure that docker-mailserver receives the correct client IP address so that spam filtering works. +If you are running on a bare-metal Kubernetes cluster, you will have to expose ports to the internet to receive and send emails. In addition, you need to make sure that `docker-mailserver`` receives the correct client IP address so that spam filtering works. -This can get a bit complicated, as explained in the docker-mailserver (documentation)[https://docker-mailserver.github.io/docker-mailserver/latest/config/advanced/kubernetes/#exposing-your-mail-server-to-the-outside-world]. +This can get a bit complicated, as explained in the `docker-mailserver` [documentation](https://docker-mailserver.github.io/docker-mailserver/latest/config/advanced/kubernetes/#exposing-your-mail-server-to-the-outside-world). -One approach to preserving the client IP address is to use the PROXY protocol, which is explained in the (documentation)[https://docker-mailserver.github.io/docker-mailserver/latest/config/advanced/kubernetes/#proxy-port-to-service-via-proxy-protocol]. +One approach to preserving the client IP address is to use the PROXY protocol, which is explained in the [documentation](https://docker-mailserver.github.io/docker-mailserver/latest/config/advanced/kubernetes/#proxy-port-to-service-via-proxy-protocol). -### Proxy Protocol -The Helm chart supports the use of the proxy protocol via the `proxy_protocol` key. To enable it set the `proxy_protocol.enable` key to true. You will also want to set the `trustedNetworks` key. +The Helm chart supports the use of the proxy protocol via the `proxyProtocol` key. To enable it set the `proxyProtocol.enable` key to true. You will also want to set the `trustedNetworks` key. ```yaml -proxy_protocol: +proxyProtocol: enabled: true # List of sources (in CIDR format, space-separated) to permit PROXY protocol from trustedNetworks: "10.0.0.0/8 192.168.0.0/16 172.16.0.0/16" ``` -Enabling the PROXY protocol will create a new port for each protocol by adding 10,000 to the standard port value. Thus: +Enabling the PROXY protocol will create an additional port for each protocol (by adding 10,000 to the standard port value) that is configured to understand the PROXY protocol. Thus: | Protocol | Port | PROXY Port | | ---------- | ------- | ----------- | @@ -144,10 +151,10 @@ Enabling the PROXY protocol will create a new port for each protocol by adding 1 | pop3 | 110 | 10110 | | pop3s | 995 | 10995 | -If you disable the PROXY protocol and your mail server is not exposed using a load-balancer service with an external traffic policy in "Local" mode, then all incoming mail traffic will look like it comes from a local Kubernetes cluster IP. +If you do not enable the PROXY protocol and your mail server is not exposed using a load-balancer service with an external traffic policy in "Local" mode, then all incoming mail traffic will look like it comes from a local Kubernetes cluster IP. ## Persistence -By default, the Chart requests creates four PersistentVolumeClaims. These are defined under the `persistence` key: +By default, the Chart creates four PersistentVolumeClaims. These are defined under the `persistence` key: | PVC Name | Default Size | Mount | Description | | ---------- | ------- | ----------------------- | -------------------------------------| @@ -159,16 +166,17 @@ By default, the Chart requests creates four PersistentVolumeClaims. These are de ## Upgrading to Version 3 Version 3.0 is not backwards compatible with previous versions. The biggest changes include: -* Usage of four PersistentVolumeClaims (PVCs) including one for configuration files +* Usage of four PersistentVolumeClaims (PVCs) instead of one +* Usage of a PVC to store configuration files * Rearrangement of keys in `values.yaml` -* Removal of RainLoop, HaProxy +* Removal of RainLoop and HaProxy * Removal of Cert Manager -* Use of rspamd by default +* Enable rspamd by default ### PersistentVolumeClaims -Previously the Chart created a single PVC to store emails, logs and the state of various docker-mailserver components. Now the Chart creates four PVCs, as described in the [persistence](#persistence) section. One of the PVCs is `mail-config` which is used to store configuration files. +Previously the Chart created a single PVC to store emails, logs and the state of various `docker-mailserver` components. Now the Chart creates four PVCs, as described in the [persistence](#persistence) section. One of the PVCs is `mail-config` which is used to store configuration files. -The addition of the `mail-config` PVC removes the requirement to use the `setup.sh` script and its dependency on Docker or Podman. Instead, you can directly deploy the chart to a Kubernetes cluster. For more information see the [configuration files](#configuration-files) section. +The addition of the `mail-config` PVC removes the requirement to use the `setup.sh` script and its dependency on Docker or Podman. Instead, you can directly deploy the chart to a Kubernetes cluster. For more information see the [configuration files](#configuration) section. To upgrade you will need to copy data from the existing PersistentVolume (PV) to one of the new PVs: @@ -179,32 +187,26 @@ To upgrade you will need to copy data from the existing PersistentVolume (PV) to | docker-mailserver | mail-log | mail-log | / | ### Rearrangement of keys -The location of a number of keys has changed in the chart. These include: +The location of a number of keys has changed in the chart. Please see `values.yaml` for the changed locations. ### Rspamd -The Chart now enables Rpsamd by default as recommened by the docker-mailserver documentation. You can disable this change by setting the env variable `ENABLE_RSPAMD` to 0 and setting `ENABLE_OPENDKIM`, `ENABLE_OPENDMARC` and `ENABLE_POLICYD_SPF` to 1. +The Chart now enables Rpsamd by default as recommened by the `docker-mailserver` documentation. You can disable this change by setting the environment variable `ENABLE_RSPAMD` to 0 and setting `ENABLE_OPENDKIM`, `ENABLE_OPENDMARC` and `ENABLE_POLICYD_SPF` to 1. -If you keep this change, you will need to generate new DKIM signing keys (see the [configuration files](#configuration-files) section for more information). In addition, you may wish to enable the Rspamd ingress (`rspamd.ingress.enabled`) +If you keep this change, you will need to generate new DKIM signing keys (see the [configuration](#configuration) section for more information). In addition, you may wish to enable the Rspamd ingress (see `rspamd.ingress.enabled`) ### TLS Support for creating a TLS certificate using `cert-manager` has been removed. Instead, create a secret that contains a certificate *before* installing the chart and reference it via the `certificate` key. Of course you can use `cert-manager` to create this secret - it is just not part of this chart anymore. ### Proxy -Support for installing HaProxy with the Chart has been removed. Instead, generic support for the Proxy protocol as been [added](#proxy). - -## Parameters - - +Support for installing HaProxy with the Chart has been removed. Instead, generic support for the Proxy protocol has been [added](#proxy). ## Development ### Testing -[Unit tests](https://github.com/lrills/helm-unittest) are created for every chart template. Tests are applied to confirm expected behaviour and interaction between various configurations -(ie haproxy mode and demo mode) +[Unit tests](https://github.com/lrills/helm-unittest) are created for every chart template. Tests are applied to confirm expected behaviour and interaction between various configurations. -In addition to tests above, a "snapshot" test is created for each manifest file. This permits a final test per-manifest, which confirms that the generated manifest -matches exactly the previous snapshot. If a template change is made, or legit value in values.yaml changes (i.e., the app version) this snapshot test will fail. +In addition to tests above, a "snapshot" test is created for each manifest file. This permits a final test per-manifest, which confirms that the generated manifest matches exactly the previous snapshot. If a template change is made, or legit value in values.yaml changes (i.e., the app version) this snapshot test will fail. If you're comfortable with the changes to the saved snapshot, then regenerate the snapshots, by running the following from the root of the repo From f1aa272fa3c863e909272a3a3b85063b54b1a98b Mon Sep 17 00:00:00 2001 From: Charlie Savage Date: Wed, 31 Jan 2024 00:27:40 -0800 Subject: [PATCH 52/66] Remove custom liveness tests since they don't actually prove liveness. In addition, it seems like using supervisorctl to check enabled components is sufficient. --- charts/docker-mailserver/values.yaml | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/charts/docker-mailserver/values.yaml b/charts/docker-mailserver/values.yaml index 27f33173..7e8ffd70 100644 --- a/charts/docker-mailserver/values.yaml +++ b/charts/docker-mailserver/values.yaml @@ -53,13 +53,6 @@ deployment: # maxUnavailable: 1 type: "Recreate" - livenessTests: - # livenessTests.enabled will add a liveness test, which will classify a pod as 'unhealthy' if any livenessTests.commands (below) return non-zero - enabled: true - # livenessTests.commands is an array of commands to be executed within the docker-mailserver container, intended to prove that the container is healthy. Each command must exit 0 under normal (healthy) circumstances - commands: - - "clamscan /tmp/docker-mailserver/TrustedHosts" - ## The following variables affect the behaviour of docker-mailserver ## See https://docker-mailserver.github.io/docker-mailserver/latest/config/environment/ for details ## Note that an empty value indicates the default as described in the docs above @@ -541,18 +534,6 @@ configMaps: sieve_dir = /var/mail/sieve/%d/%n/sieve } - am-i-health.sh: - create: true - path: am-i-health.sh - data: | - #!/bin/bash - # this script is intended to be used by periodic kubernetes liveness probes to ensure that the container - # (and all its dependent services) is healthy - {{ range .Values.deployment.livenessTests.commands -}} - {{ . }} && \ - {{- end }} - echo "All healthy" - user-patches.sh: create: true path: user-patches.sh From ffdb5b278e63d53b65a3f6e4c5dd185dec813ae5 Mon Sep 17 00:00:00 2001 From: Charlie Savage Date: Wed, 31 Jan 2024 00:28:11 -0800 Subject: [PATCH 53/66] Enable indexing of trash folder in dovecot --- charts/docker-mailserver/values.yaml | 3 --- 1 file changed, 3 deletions(-) diff --git a/charts/docker-mailserver/values.yaml b/charts/docker-mailserver/values.yaml index 7e8ffd70..73fc8803 100644 --- a/charts/docker-mailserver/values.yaml +++ b/charts/docker-mailserver/values.yaml @@ -504,9 +504,6 @@ configMaps: fts_autoindex = yes fts_enforced = yes - # disable indexing of folders - fts_autoindex_exclude = \Trash - # Index attachements fts_decoder = decode2text } From 6109f3cdf7c0d630bafda038c25a54eeb5952af6 Mon Sep 17 00:00:00 2001 From: Charlie Savage Date: Wed, 31 Jan 2024 00:30:11 -0800 Subject: [PATCH 54/66] Remove built-in support for dovecot replication. --- charts/docker-mailserver/values.yaml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/charts/docker-mailserver/values.yaml b/charts/docker-mailserver/values.yaml index 73fc8803..31adb374 100644 --- a/charts/docker-mailserver/values.yaml +++ b/charts/docker-mailserver/values.yaml @@ -220,10 +220,6 @@ deployment: RELAY_USER: RELAY_PASSWORD: - # Whether to enable dovecot replication. Allows the syncronization of a pair of dovecot servers - # https://wiki.dovecot.org/Replication - enable_dovecot_replication: false - securityContext: runAsUser: 5000 runAsGroup: 5000 From d369355e999f19148326f9204f08e00d4e67058a Mon Sep 17 00:00:00 2001 From: Charlie Savage Date: Wed, 31 Jan 2024 00:32:20 -0800 Subject: [PATCH 55/66] Move .Values.resourxces to .Values.deployment.resources since they apply to the deployment. --- .../templates/deployment.yaml | 2 +- charts/docker-mailserver/values.yaml | 60 +++++++++---------- 2 files changed, 31 insertions(+), 31 deletions(-) diff --git a/charts/docker-mailserver/templates/deployment.yaml b/charts/docker-mailserver/templates/deployment.yaml index 38bda397..1297f5f0 100644 --- a/charts/docker-mailserver/templates/deployment.yaml +++ b/charts/docker-mailserver/templates/deployment.yaml @@ -89,7 +89,7 @@ spec: image: {{ .Values.image.name }}:{{ .Values.image.tag }} imagePullPolicy: {{ .Values.image.pullPolicy }} resources: -{{ toYaml .Values.resources | indent 12 }} +{{ toYaml .Values.deployment.resources | indent 12 }} securityContext: {{- if eq .Values.deployment.env.ENABLE_FAIL2BAN 1.0 }} capabilities: diff --git a/charts/docker-mailserver/values.yaml b/charts/docker-mailserver/values.yaml index 31adb374..74cc5f58 100644 --- a/charts/docker-mailserver/values.yaml +++ b/charts/docker-mailserver/values.yaml @@ -228,6 +228,36 @@ deployment: readOnlyRootFilesystem: false # incompatible with the way docker-mailserver works privileged: false + ## More generally, a "request" can be thought of as "how much is this container expected to need usually". it should be + ## possible to burst outside these constraints (during a high load operation). However, Kubernetes may kill the pod + ## if the node is under too higher load and the burst is outside its request + ## + ## Limits are hard limits. Violating them is either impossible, or results in container death. I'm not sure whether + ## making these optional is a good idea or not; at the moment, I think I'm happy to defer QOS to the cluster and try + ## and keep requests close to usage. + ## + ## Requests are what are used to determine whether more software "fits" onto the cluster. + ## + ## Ref: http://kubernetes.io/docs/user-guide/compute-resources/ + ## Ref: https://github.com/kubernetes/kubernetes/blob/master/docs/design/resource-qos.md + ## Ref: https://docs.docker.com/engine/reference/run/#/runtime-constraints-on-resources + resources: + requests: + ## How much CPU this container is expected to need + cpu: "1" + ## How much memory this container is expected to need. + ## Reduce these at requests your peril - too few resources can cause daemons (i.e., clamd) to fail, or timeouts to occur. + ## A test installation with clamd running was killed when it consumed 1437Mi (which is why this value was increased to 1536) + memory: "1536Mi" + ephemeral-storage: "100Mi" + limits: + ## The max CPU this container should be allowed to use + cpu: "2" + ## The max memory this container should be allowed to use. Note: If a container exceeds its memory limit, + ## it may terminated. + memory: "2048Mi" + ephemeral-storage: "500Mi" + service: ## What scope the service should be exposed in. One of: ## - LoadBalancer (to the world) @@ -253,36 +283,6 @@ service: # hostName: annotations: {} -## More generally, a "request" can be thought of as "how much is this container expected to need usually". it should be -## possible to burst outside these constraints (during a high load operation). However, Kubernetes may kill the pod -## if the node is under too higher load and the burst is outside its request -## -## Limits are hard limits. Violating them is either impossible, or results in container death. I'm not sure whether -## making these optional is a good idea or not; at the moment, I think I'm happy to defer QOS to the cluster and try -## and keep requests close to usage. -## -## Requests are what are used to determine whether more software "fits" onto the cluster. -## -## Ref: http://kubernetes.io/docs/user-guide/compute-resources/ -## Ref: https://github.com/kubernetes/kubernetes/blob/master/docs/design/resource-qos.md -## Ref: https://docs.docker.com/engine/reference/run/#/runtime-constraints-on-resources -resources: - requests: - ## How much CPU this container is expected to need - cpu: "1" - ## How much memory this container is expected to need. - ## Reduce these at requests your peril - too few resources can cause daemons (i.e., clamd) to fail, or timeouts to occur. - ## A test installation with clamd running was killed when it consumed 1437Mi (which is why this value was increased to 1536) - memory: "1536Mi" - ephemeral-storage: "100Mi" - limits: - ## The max CPU this container should be allowed to use - cpu: "2" - ## The max memory this container should be allowed to use. Note: If a container exceeds its memory limit, - ## it may terminated. - memory: "2048Mi" - ephemeral-storage: "500Mi" - # Note this is a dictionary and not a list so invidual keys can be overriden by --set or --value helm parameters persistence: # Stores generated configuration files From e2d3aaf7584f2e491bbc244f0b46d9c2720f5bdb Mon Sep 17 00:00:00 2001 From: Charlie Savage Date: Wed, 31 Jan 2024 00:34:36 -0800 Subject: [PATCH 56/66] Move fullTextSearch key under dovecot key. --- charts/docker-mailserver/values.yaml | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/charts/docker-mailserver/values.yaml b/charts/docker-mailserver/values.yaml index 74cc5f58..83ed0e50 100644 --- a/charts/docker-mailserver/values.yaml +++ b/charts/docker-mailserver/values.yaml @@ -377,20 +377,21 @@ rspamd: enabled: false secret: +dovecot: + fullTextSearch: + enabled: true + verbose: 1 # 0 (silent), 1 (verbose) or 2 (debug) + resources: + memory: 2GB + cron: + enabled: true # Optimize index every day + schedule: 0 4 * * * # Every day at 4am + proxyProtocol: enabled: true # List of sources (in CIDR format, space-separated) to permit PROXY protocol from trustedNetworks: "10.0.0.0/8 192.168.0.0/16 172.16.0.0/16" -fullTextSearch: - enabled: true - verbose: 1 # 0 (silent), 1 (verbose) or 2 (debug) - resources: - memory: 2GB - cron: - enabled: true # Optimize index every day - schedule: 0 4 * * * # Every day at 4am - # when metrics is enabled, we mount subpath log from pvc into /var/log/mail metrics: enabled: false @@ -486,7 +487,7 @@ configMaps: create: true path: /etc/dovecot/conf.d/10-plugin.conf data: | - {{- if .Values.fullTextSearch.enabled }} + {{- if .Values.dovecot.fullTextSearch.enabled }} mail_plugins = $mail_plugins fts fts_xapian plugin { @@ -495,7 +496,7 @@ configMaps: plugin { fts = xapian - fts_xapian = partial=3 full=20 verbose={{ .Values.fullTextSearch.verbose }} + fts_xapian = partial=3 full=20 verbose={{ .Values.dovecot.fullTextSearch.verbose }} fts_autoindex = yes fts_enforced = yes @@ -506,7 +507,7 @@ configMaps: service indexer-worker { # limit size of indexer-worker RAM usage, ex: 512MB, 1GB, 2GB - vsz_limit = {{ .Values.fullTextSearch.resources.memory }} + vsz_limit = {{ .Values.dovecot.fullTextSearch.resources.memory }} } service decode2text { From 12eeec9e517510af7e4e7721720705e42788241a Mon Sep 17 00:00:00 2001 From: Charlie Savage Date: Wed, 31 Jan 2024 00:57:33 -0800 Subject: [PATCH 57/66] Remove sieve override --- charts/docker-mailserver/values.yaml | 9 --------- 1 file changed, 9 deletions(-) diff --git a/charts/docker-mailserver/values.yaml b/charts/docker-mailserver/values.yaml index 83ed0e50..f68a6804 100644 --- a/charts/docker-mailserver/values.yaml +++ b/charts/docker-mailserver/values.yaml @@ -519,15 +519,6 @@ configMaps: } {{- end -}} - 91-override-sieve.conf: - create: true - path: 91-override-sieve.conf - data: | - plugin { - sieve = /var/mail/sieve/%d/%n/.dovecot.sieve - sieve_dir = /var/mail/sieve/%d/%n/sieve - } - user-patches.sh: create: true path: user-patches.sh From fd360824277e2f7733ba23e395cea2500175e352 Mon Sep 17 00:00:00 2001 From: Charlie Savage Date: Wed, 31 Jan 2024 18:29:03 -0800 Subject: [PATCH 58/66] Fix failing test - need to add test user --- charts/docker-mailserver/ci/ci-values.yaml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/charts/docker-mailserver/ci/ci-values.yaml b/charts/docker-mailserver/ci/ci-values.yaml index c9cbaf16..542e6f7e 100644 --- a/charts/docker-mailserver/ci/ci-values.yaml +++ b/charts/docker-mailserver/ci/ci-values.yaml @@ -1,10 +1,10 @@ -image: - tag: "8.0.1" - pullPolicy: "Always" +service: + type: ClusterIP -initContainer: - image: - pullPolicy: "Always" +configMaps: + postfix-accounts.cf: + create: true + path: postfix-accounts.cf + data: | + user@example.com|{SHA512-CRYPT}$6$PLsyDsD5kMTmQbe/$b1jT8MvuoBfs/JeBQ9fBQ0JlnWJZ377SW/OxSlNe7ldjgRQ7K4ysGfM6OpkkxQkAu7c7pR7EAR5Y4aIty2/Qi. -service: - type: ClusterIP \ No newline at end of file From 514d66726bef8de68ccff476cd6be02a0f352b61 Mon Sep 17 00:00:00 2001 From: Charlie Savage Date: Wed, 31 Jan 2024 18:29:20 -0800 Subject: [PATCH 59/66] Disable full text search --- charts/docker-mailserver/values.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/charts/docker-mailserver/values.yaml b/charts/docker-mailserver/values.yaml index f68a6804..bccaeb43 100644 --- a/charts/docker-mailserver/values.yaml +++ b/charts/docker-mailserver/values.yaml @@ -379,7 +379,7 @@ rspamd: dovecot: fullTextSearch: - enabled: true + enabled: false verbose: 1 # 0 (silent), 1 (verbose) or 2 (debug) resources: memory: 2GB From 249f6e0110a6f4b28479b3ac94daa9ac0960d9e3 Mon Sep 17 00:00:00 2001 From: Charlie Savage Date: Wed, 31 Jan 2024 19:43:52 -0800 Subject: [PATCH 60/66] Make kube-score happ. --- charts/docker-mailserver/ci/ci-values.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/charts/docker-mailserver/ci/ci-values.yaml b/charts/docker-mailserver/ci/ci-values.yaml index 542e6f7e..1cfb7a41 100644 --- a/charts/docker-mailserver/ci/ci-values.yaml +++ b/charts/docker-mailserver/ci/ci-values.yaml @@ -8,3 +8,6 @@ configMaps: data: | user@example.com|{SHA512-CRYPT}$6$PLsyDsD5kMTmQbe/$b1jT8MvuoBfs/JeBQ9fBQ0JlnWJZ377SW/OxSlNe7ldjgRQ7K4ysGfM6OpkkxQkAu7c7pR7EAR5Y4aIty2/Qi. +image: + # Needed to make kube-score happy + pullPolicy: Always From b69048c6099793cfd53cc0ad655f7c6587536773 Mon Sep 17 00:00:00 2001 From: Charlie Savage Date: Thu, 1 Feb 2024 01:17:40 -0800 Subject: [PATCH 61/66] Fix previously incorrect global search and replace --- charts/docker-mailserver/values.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/charts/docker-mailserver/values.yaml b/charts/docker-mailserver/values.yaml index bccaeb43..64568531 100644 --- a/charts/docker-mailserver/values.yaml +++ b/charts/docker-mailserver/values.yaml @@ -543,7 +543,7 @@ configMaps: -o smtpd_discard_ehlo_keywords= -o milter_macro_daemon_name=ORIGINATING -o cleanup_service_name=sender-cleanup - -o smtpd_upstream_proxyProtocol=haproxy + -o smtpd_upstream_proxy_protocol=haproxy # Submissions with proxy 10465 inet n - n - - smtpd @@ -559,7 +559,7 @@ configMaps: -o smtpd_discard_ehlo_keywords= -o milter_macro_daemon_name=ORIGINATING -o cleanup_service_name=sender-cleanup - -o smtpd_upstream_proxyProtocol=haproxy + -o smtpd_upstream_proxy_protocol=haproxy EOS {{- end }} From 51ba10a09ddaadec565bf28ec2f6ec91bfc66918 Mon Sep 17 00:00:00 2001 From: Charlie Savage Date: Thu, 1 Feb 2024 19:32:27 -0800 Subject: [PATCH 62/66] Change Docker Mailserver to docker-mailserver --- LICENSE | 2 +- README.md | 4 ++-- charts/docker-mailserver/README.md | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/LICENSE b/LICENSE index e381f7f1..60d02331 100644 --- a/LICENSE +++ b/LICENSE @@ -1,7 +1,7 @@ The MIT License (MIT) Copyright 2019 David Young -Copyright 2020 The Docker Mailserver Helm Chart Authors +Copyright 2020 The docker-mailserver Helm Chart Authors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index f120c702..9af47c68 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -# K8s Helm Chart for Docker Mailserver +# K8s Helm Chart for docker-mailserver -This repostitory contains a helm chart to deploy [Docker Mailserver](https://github.com/docker-mailserver/docker-mailserver) into a Kubernetes cluster. Docker Mailserver is a production-ready, fullstack mail server that supports SMTP, IMAP, LDAP, Anti-spam, Anti-virus, etc.). Just as importantly, it is designed to be simple to install and configure. +This repostitory contains a helm chart to deploy [docker-mailserver](https://github.com/docker-mailserver/docker-mailserver) into a Kubernetes cluster. docker-mailserver is a production-ready, fullstack mail server that supports SMTP, IMAP, LDAP, Anti-spam, Anti-virus, etc.). Just as importantly, it is designed to be simple to install and configure. ## Documentation diff --git a/charts/docker-mailserver/README.md b/charts/docker-mailserver/README.md index 38972bb0..25a4df93 100644 --- a/charts/docker-mailserver/README.md +++ b/charts/docker-mailserver/README.md @@ -20,8 +20,8 @@ (Created by [gh-md-toc](https://github.com/ekalinin/github-markdown-toc.go)) ## Introduction -This chart deploys [Docker Mailserver](https://github.com/docker-mailserver/docker-mailserver) into a -Kubernetes cluster. Docker Mailserver is a production-ready, fullstack mail server that supports SMTP, IMAP, LDAP, Anti-spam, Anti-virus, etc.). Just as importantly, it is designed to be simple to install and configure. +This chart deploys [docker-mailserver](https://github.com/docker-mailserver/docker-mailserver) into a +Kubernetes cluster. docker-mailserver is a production-ready, fullstack mail server that supports SMTP, IMAP, LDAP, Anti-spam, Anti-virus, etc.). Just as importantly, it is designed to be simple to install and configure. !!WARNING!! - Version 3.0.0 is not backwards compatible with previous versions. Please refer to the [upgrade](#upgrading-to-version-3) section for more information. From f7d172cac55600ba9446e531cc3210ad50f14ee9 Mon Sep 17 00:00:00 2001 From: Charlie Savage Date: Thu, 1 Feb 2024 19:34:22 -0800 Subject: [PATCH 63/66] Add newline --- charts/docker-mailserver/Chart.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/charts/docker-mailserver/Chart.yaml b/charts/docker-mailserver/Chart.yaml index 954675f0..d61b02af 100644 --- a/charts/docker-mailserver/Chart.yaml +++ b/charts/docker-mailserver/Chart.yaml @@ -21,4 +21,4 @@ annotations: artifacthub.io/changes: | - Breaking : Standardized app labels to app.kubernetes.io/name for Istio workload/Cilium compatibility -dependencies: [] \ No newline at end of file +dependencies: [] From fba4801a4a6450e64c74b66619ecadb55dd93ab1 Mon Sep 17 00:00:00 2001 From: Charlie Savage Date: Thu, 1 Feb 2024 20:18:41 -0800 Subject: [PATCH 64/66] Improve comment about kube-score --- charts/docker-mailserver/ci/ci-values.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/charts/docker-mailserver/ci/ci-values.yaml b/charts/docker-mailserver/ci/ci-values.yaml index 1cfb7a41..c40d0a96 100644 --- a/charts/docker-mailserver/ci/ci-values.yaml +++ b/charts/docker-mailserver/ci/ci-values.yaml @@ -9,5 +9,5 @@ configMaps: user@example.com|{SHA512-CRYPT}$6$PLsyDsD5kMTmQbe/$b1jT8MvuoBfs/JeBQ9fBQ0JlnWJZ377SW/OxSlNe7ldjgRQ7K4ysGfM6OpkkxQkAu7c7pR7EAR5Y4aIty2/Qi. image: - # Needed to make kube-score happy + # Makes kube-score happy, otherwise it complains about the default pullPlicy of "IfNotPresent" pullPolicy: Always From fadcb918dd1a132d932e3745a19e03b1fc29839a Mon Sep 17 00:00:00 2001 From: Charlie Savage Date: Thu, 1 Feb 2024 20:18:52 -0800 Subject: [PATCH 65/66] Fix markdown lint issues. --- charts/docker-mailserver/README.md | 73 +++++++++++++++++++++--------- 1 file changed, 51 insertions(+), 22 deletions(-) diff --git a/charts/docker-mailserver/README.md b/charts/docker-mailserver/README.md index 25a4df93..c02ab853 100644 --- a/charts/docker-mailserver/README.md +++ b/charts/docker-mailserver/README.md @@ -1,16 +1,18 @@ +# docker-mailserver Heml Chart + ## Contents - [Introduction](#introduction) - [Prerequisites](#prerequisites) - [Getting Started](#getting-started) - [Configuration](#configuration) - - [Volume](#volume) - - [ConfigMaps](#config-maps) - - [Secrets](#secrets) + - [Volume](#volume) + - [ConfigMaps](#configmaps) + - [Secrets](#secrets) - [Values YAML](#values-yaml) - - [Environment Variables](#environment-variables) - - [Minimal Configuration](#minimal-configuration) - - [Certificate](#certificate) + - [Environment Variables](#environment-variables) + - [Minimal Configuration](#minimal-configuration) + - [Certificate](#certificate) - [Ports](#ports) - [Persistence](#persistence) - [Upgrading to Version 3.0.0](#upgrading-to-version-3) @@ -20,18 +22,21 @@ (Created by [gh-md-toc](https://github.com/ekalinin/github-markdown-toc.go)) ## Introduction + This chart deploys [docker-mailserver](https://github.com/docker-mailserver/docker-mailserver) into a Kubernetes cluster. docker-mailserver is a production-ready, fullstack mail server that supports SMTP, IMAP, LDAP, Anti-spam, Anti-virus, etc.). Just as importantly, it is designed to be simple to install and configure. !!WARNING!! - Version 3.0.0 is not backwards compatible with previous versions. Please refer to the [upgrade](#upgrading-to-version-3) section for more information. ## Prerequisites + - [Helm](https://helm.sh) - A [Kubernetes](https://kubernetes.io/releases/) cluster with persistent storage and access to email [ports](https://docker-mailserver.github.io/docker-mailserver/latest/config/security/understanding-the-ports/#overview-of-email-ports) - A custom domain name (for example, example.com) - Correctly configured [DNS](https://docker-mailserver.github.io/docker-mailserver/latest/usage/#minimal-dns-setup) ## Getting Started + Setting up docker-mailserver requires generating a number of configuration [files](https://docker-mailserver.github.io/docker-mailserver/latest/config/advanced/optional-config/). To make this easier, docker-mailserver includes a `setup` command that can generate these files. To get started, first install docker-mailserver: @@ -54,6 +59,12 @@ kubectl exec -it --namespace mail deploy/docker-mailserver -- bash setup email add user@example.com password ``` +Alternatively you can do it one step like this: + +```console +$kubectl exec -it --namespace mail deploy/docker-mailserver -- setup email add user@example.com password +``` + Account information will be saved in a file `postfix-accounts.cf` in the container path: ```console @@ -63,6 +74,7 @@ cat /tmp/docker-mailserver/postfix-accounts.cf This path is [mapped](#persistence) to a Kubernetes Volume. ## Configuration + Assuming you still have a command prompt [open](#getting-started) in the running container, run the setup command to see additional configuration options: ```console @@ -79,19 +91,24 @@ setup config dkim keysize 2048 domain 'example.com' These paths are stored to the container path `/tmp/docker-mailserver` which is [mapped](#persistence) to a Kubernetes Volume. ### Volume + Configuration files are stored on a Kubernetes [volume](#persistence) mounted at `/tmp/docker-mailserver` in the container. The PVC is named `mail-config`. You can of course add additional configuration files to the volume as needed. ### ConfigMaps + Its is also possible to use ConfigMaps to mount configuration files in the container. This is done by adding to the `configFiles` key in a custom `values.yaml` file. For more information please see the [documentation](./values.yaml#L425) in values.yaml ### Secrets + Secrets can also be used to mount configuration files in the container. For example, dkim keys could be stored in a secret as opposed to a file in the `mail-config` volume. Once again, for more information please see the [documentation](./values.yaml#L600) in values.yaml ## Values YAML + In addition to the configuration files generated above, the `values.yaml` file contains a number of knobs for customizing the docker-mailserver installation. Please refer to the extensive comments in [values.yaml](./values.yaml) for additional information. ### Environment Variables -Included in the knobs are **many** environment variables which allow you to customize the behaviour of `docker-mailserver`. These environment variables are documented in the `docker-mailserver's` [configuration](https://docker-mailserver.github.io/docker-mailserver/latest/config/environment/) page. Note that `docker-mailserver` expects any true/false values to be set as numbers (1/0) rather than boolean values (true/false). + +Included in the knobs are **many** environment variables which allow you to customize the behaviour of `docker-mailserver`. These environment variables are documented in the `docker-mailserver's` [configuration](https://docker-mailserver.github.io/docker-mailserver/latest/config/environment/) page. Note that `docker-mailserver` expects any true/false values to be set as numbers (1/0) rather than boolean values (true/false). By default, the Chart enables `rspamd` and disables `opendkim`, `dmarc`, `policyd-spf` and `clamav`. This is the setup [recommended](https://docker-mailserver.github.io/docker-mailserver/latest/config/best-practices/dkim_dmarc_spf/) by the `docker-mailserver` project. @@ -100,34 +117,39 @@ Once you have created your own values.yaml files, then redeploy the chart like t ```console helm upgrade docker-mailserver docker-mailserver --namespace mail --values ``` + You can also override individual configuration setting with `helm upgrade --set`, specifying each parameter using the `--set key=value[,key=value]` argument to `helm install`. For example: ```console -$ helm upgrade docker-mailserver docker-mailserver --namespace mail --set pod.dockermailserver.image="your/image:1.0.0" +$helm upgrade docker-mailserver docker-mailserver --namespace mail --set pod.dockermailserver.image="your/image:1.0.0" ``` ### Minimal Configuration + There are various settings in `values.yaml` that you must override. -| Parameter | Description | Default | -| ------------------------------------ | --------------------------------------------------- | ---------------- | -| deployment.env.[OVERRIDE_HOSTNAME](https://docker-mailserver.github.io/docker-mailserver/latest/config/environment/#override_hostname) The hostname to be presented on SMTP banners | mail.example.com | -| `certificate` | Name of a Kubernetes secret that stores TLS certificate for your mail domain | +| Parameter | Description | Default | +| ------------------------------------ | ------------------------------------ | ---------------- | +| deployment.env.[OVERRIDE_HOSTNAME](https://docker-mailserver.github.io/docker-mailserver/latest/config/environment/#override_hostname) | The hostname to be presented on SMTP banners | mail.example.com | +| `certificate` | Name of a Kubernetes secret that stores TLS certificate for your mail domain | | ### Certificate -You will need to setup a TLS certificate for your email domain. The easiest way to do this is use (cert-manager)[https://cert-manager.io/]. + +You will need to setup a TLS certificate for your email domain. The easiest way to do this is use [cert-manager](https://cert-manager.io/). Once you acquire a certificate, you will need to store it in a TLS secret in the docker-mailserver namespace. Once you have done that, update the values.yaml file like this: ```yaml certificate: my-certificate-secret ``` + The chart will then automatically copy the certificate and private key to the `/tmp/dms/custom-certs` director in the container and correctly set the `SSL_CERT_PATH` and `SSL_KEY_PATH` environment variables. ## Ports + If you are running on a bare-metal Kubernetes cluster, you will have to expose ports to the internet to receive and send emails. In addition, you need to make sure that `docker-mailserver`` receives the correct client IP address so that spam filtering works. -This can get a bit complicated, as explained in the `docker-mailserver` [documentation](https://docker-mailserver.github.io/docker-mailserver/latest/config/advanced/kubernetes/#exposing-your-mail-server-to-the-outside-world). +This can get a bit complicated, as explained in the `docker-mailserver` [documentation](https://docker-mailserver.github.io/docker-mailserver/latest/config/advanced/kubernetes/#exposing-your-mail-server-to-the-outside-world). One approach to preserving the client IP address is to use the PROXY protocol, which is explained in the [documentation](https://docker-mailserver.github.io/docker-mailserver/latest/config/advanced/kubernetes/#proxy-port-to-service-via-proxy-protocol). @@ -154,6 +176,7 @@ Enabling the PROXY protocol will create an additional port for each protocol (by If you do not enable the PROXY protocol and your mail server is not exposed using a load-balancer service with an external traffic policy in "Local" mode, then all incoming mail traffic will look like it comes from a local Kubernetes cluster IP. ## Persistence + By default, the Chart creates four PersistentVolumeClaims. These are defined under the `persistence` key: | PVC Name | Default Size | Mount | Description | @@ -164,16 +187,18 @@ By default, the Chart creates four PersistentVolumeClaims. These are defined und | mail-log | 1Gi | /var/log/mail | Stores log files | ## Upgrading to Version 3 + Version 3.0 is not backwards compatible with previous versions. The biggest changes include: -* Usage of four PersistentVolumeClaims (PVCs) instead of one -* Usage of a PVC to store configuration files -* Rearrangement of keys in `values.yaml` -* Removal of RainLoop and HaProxy -* Removal of Cert Manager -* Enable rspamd by default +- Usage of four PersistentVolumeClaims (PVCs) instead of one +- Usage of a PVC to store configuration files +- Rearrangement of keys in `values.yaml` +- Removal of RainLoop and HaProxy +- Removal of Cert Manager +- Enable rspamd by default ### PersistentVolumeClaims + Previously the Chart created a single PVC to store emails, logs and the state of various `docker-mailserver` components. Now the Chart creates four PVCs, as described in the [persistence](#persistence) section. One of the PVCs is `mail-config` which is used to store configuration files. The addition of the `mail-config` PVC removes the requirement to use the `setup.sh` script and its dependency on Docker or Podman. Instead, you can directly deploy the chart to a Kubernetes cluster. For more information see the [configuration files](#configuration) section. @@ -187,17 +212,21 @@ To upgrade you will need to copy data from the existing PersistentVolume (PV) to | docker-mailserver | mail-log | mail-log | / | ### Rearrangement of keys + The location of a number of keys has changed in the chart. Please see `values.yaml` for the changed locations. ### Rspamd + The Chart now enables Rpsamd by default as recommened by the `docker-mailserver` documentation. You can disable this change by setting the environment variable `ENABLE_RSPAMD` to 0 and setting `ENABLE_OPENDKIM`, `ENABLE_OPENDMARC` and `ENABLE_POLICYD_SPF` to 1. If you keep this change, you will need to generate new DKIM signing keys (see the [configuration](#configuration) section for more information). In addition, you may wish to enable the Rspamd ingress (see `rspamd.ingress.enabled`) ### TLS + Support for creating a TLS certificate using `cert-manager` has been removed. Instead, create a secret that contains a certificate *before* installing the chart and reference it via the `certificate` key. Of course you can use `cert-manager` to create this secret - it is just not part of this chart anymore. ### Proxy + Support for installing HaProxy with the Chart has been removed. Instead, generic support for the Proxy protocol has been [added](#proxy). ## Development @@ -211,6 +240,6 @@ In addition to tests above, a "snapshot" test is created for each manifest file. If you're comfortable with the changes to the saved snapshot, then regenerate the snapshots, by running the following from the root of the repo ```console -$ helm plugin install https://github.com/lrills/helm-unittest -$ helm unittest helm-chart/docker-mailserver +$helm plugin install https://github.com/lrills/helm-unittest +$helm unittest helm-chart/docker-mailserver ``` From 65811f3ac170c815b0f92df5d3806fc389fcfd71 Mon Sep 17 00:00:00 2001 From: Charlie Savage Date: Thu, 1 Feb 2024 20:24:02 -0800 Subject: [PATCH 66/66] Update chart versions. --- charts/docker-mailserver/Chart.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/charts/docker-mailserver/Chart.yaml b/charts/docker-mailserver/Chart.yaml index d61b02af..c2285a92 100644 --- a/charts/docker-mailserver/Chart.yaml +++ b/charts/docker-mailserver/Chart.yaml @@ -1,8 +1,8 @@ apiVersion: v2 -appVersion: "13.2.0" +appVersion: "13.3.1" description: A fullstack but simple mailserver (smtp, imap, antispam, antivirus, ssl...) using Docker. name: docker-mailserver -version: 2.3.1 +version: 3.0.0 sources: - https://github.com/docker-mailserver/docker-mailserver-helm maintainers: