From e0b64a9e540b5f2dbea7d39c53a0e824bb51663d Mon Sep 17 00:00:00 2001 From: Florent Daigniere Date: Sat, 6 Apr 2024 17:28:38 +0200 Subject: [PATCH 1/7] simplify config with TLS, PORTS and PROXY_PROTOCOL --- core/dovecot/conf/dovecot.conf | 2 +- core/nginx/conf/nginx.conf | 34 +++++++++++------------ core/nginx/conf/proxy.conf | 2 +- core/nginx/config.py | 40 ++++++++++++++++++++++++++++ core/nginx/dovecot/proxy.conf | 34 ++++++++++++++--------- towncrier/newsfragments/3061.feature | 1 + 6 files changed, 81 insertions(+), 32 deletions(-) create mode 100644 towncrier/newsfragments/3061.feature diff --git a/core/dovecot/conf/dovecot.conf b/core/dovecot/conf/dovecot.conf index 4ea4fc43..75bd3a66 100644 --- a/core/dovecot/conf/dovecot.conf +++ b/core/dovecot/conf/dovecot.conf @@ -5,7 +5,7 @@ log_path = /dev/stderr protocols = imap pop3 lmtp sieve postmaster_address = {{ POSTMASTER }}@{{ DOMAIN }} hostname = {{ HOSTNAMES.split(",")[0] }} -{%- if PROXY_PROTOCOL in ['all', 'all-but-http', 'mail'] %} +{%- if PROXY_PROTOCOL_SMTP %} submission_host = {{ HOSTNAMES.split(",")[0] }} {% else %} submission_host = {{ FRONT_ADDRESS }} diff --git a/core/nginx/conf/nginx.conf b/core/nginx/conf/nginx.conf index 214744ee..a29c570b 100644 --- a/core/nginx/conf/nginx.conf +++ b/core/nginx/conf/nginx.conf @@ -22,7 +22,7 @@ http { {% if REAL_IP_HEADER %} real_ip_header {{ REAL_IP_HEADER }}; - {% elif PROXY_PROTOCOL in ['all', 'all-but-http', 'http'] %} + {% elif (PROXY_PROTOCOL_HTTPS or PROXY_PROTOCOL_HTTP) and REAL_IP_FROM %} real_ip_header proxy_protocol; {% endif %} @@ -54,14 +54,14 @@ http { gzip_min_length 1024; # TODO: figure out how to server pre-compressed assets from admin container - {% if not KUBERNETES_INGRESS and TLS_FLAVOR in [ 'letsencrypt', 'cert' ] %} + {% if PORT_80 and TLS_FLAVOR in [ 'letsencrypt', 'cert' ] %} # Enable the proxy for certbot if the flavor is letsencrypt and not on kubernetes # server { # Listen over HTTP - listen 80{% if PROXY_PROTOCOL in ['all', 'http'] %} proxy_protocol{% endif %}; + listen 80{% if PROXY_PROTOCOL_HTTP %} proxy_protocol{% endif %}; {% if SUBNET6 %} - listen [::]:80{% if PROXY_PROTOCOL in ['all', 'http'] %} proxy_protocol{% endif %}; + listen [::]:80{% if PROXY_PROTOCOL_HTTP %} proxy_protocol{% endif %}; {% endif %} {% if TLS_FLAVOR in ['letsencrypt', 'mail-letsencrypt'] %} location ^~ /.well-known/acme-challenge/ { @@ -95,18 +95,18 @@ http { client_max_body_size {{ MESSAGE_SIZE_LIMIT|int + 8388608 }}; # Listen on HTTP only in kubernetes or behind reverse proxy - {% if KUBERNETES_INGRESS or TLS_FLAVOR in [ 'mail-letsencrypt', 'notls', 'mail' ] %} - listen 80{% if PROXY_PROTOCOL in ['all', 'http'] %} proxy_protocol{% endif %}; + {% if TLS_FLAVOR in [ 'mail-letsencrypt', 'notls', 'mail' ] %} + listen 80{% if PROXY_PROTOCOL_HTTP %} proxy_protocol{% endif %}; {% if SUBNET6 %} - listen [::]:80{% if PROXY_PROTOCOL in ['all', 'http'] %} proxy_protocol{% endif %}; + listen [::]:80{% if PROXY_PROTOCOL_HTTP %} proxy_protocol{% endif %}; {% endif %} {% endif %} - # Only enable HTTPS if TLS is enabled with no error and not on kubernetes - {% if not KUBERNETES_INGRESS and TLS and not TLS_ERROR %} - listen 443 ssl http2{% if PROXY_PROTOCOL in ['all', 'all-but-http', 'http'] %} proxy_protocol{% endif %}; + # Only enable HTTPS if TLS is enabled with no error + {% if TLS_443 and not TLS_ERROR %} + listen 443 ssl http2{% if PROXY_PROTOCOL_HTTPS %} proxy_protocol{% endif %}; {% if SUBNET6 %} - listen [::]:443 ssl http2{% if PROXY_PROTOCOL in ['all', 'all-but-http', 'http'] %} proxy_protocol{% endif %}; + listen [::]:443 ssl http2{% if PROXY_PROTOCOL_HTTPS %} proxy_protocol{% endif %}; {% endif %} include /etc/nginx/tls.conf; @@ -162,7 +162,7 @@ http { {% endif %} # If TLS is failing, prevent access to anything except certbot - {% if not KUBERNETES_INGRESS and TLS_ERROR and not (TLS_FLAVOR in [ 'mail-letsencrypt', 'mail' ]) %} + {% if TLS_ERROR and not (TLS_FLAVOR in [ 'mail-letsencrypt', 'mail' ]) %} location / { return 403; } @@ -310,12 +310,12 @@ mail { resolver {{ RESOLVER }} valid=30s; error_log /dev/stderr info; - {% if TLS and not TLS_ERROR %} + {% if TLS_25 and not TLS_ERROR %} include /etc/nginx/tls.conf; ssl_session_cache shared:SSLMAIL:3m; {% endif %} - {% if PROXY_PROTOCOL in ['all', 'all-but-http', 'mail'] and REAL_IP_FROM %}{% for from_ip in REAL_IP_FROM.split(',') %} + {% if PROXY_PROTOCOL_SMTP and REAL_IP_FROM %}{% for from_ip in REAL_IP_FROM.split(',') %} set_real_ip_from {{ from_ip }}; {% endfor %}{% endif %} @@ -324,11 +324,11 @@ mail { # SMTP is always enabled, to avoid losing emails when TLS is failing server { - listen 25{% if PROXY_PROTOCOL in ['all', 'all-but-http', 'mail'] %} proxy_protocol{% endif %}; + listen 25{% if PROXY_PROTOCOL_SMTP %} proxy_protocol{% endif %}; {% if SUBNET6 %} - listen [::]:25{% if PROXY_PROTOCOL in ['all', 'all-but-http', 'mail'] %} proxy_protocol{% endif %}; + listen [::]:25{% if PROXY_PROTOCOL_SMTP %} proxy_protocol{% endif %}; {% endif %} - {% if TLS and not TLS_ERROR %} + {% if TLS_25 and not TLS_ERROR %} {% if TLS_FLAVOR in ['letsencrypt','mail-letsencrypt'] %} ssl_certificate /certs/letsencrypt/live/mailu/DANE-chain.pem; ssl_certificate /certs/letsencrypt/live/mailu-ecdsa/DANE-chain.pem; diff --git a/core/nginx/conf/proxy.conf b/core/nginx/conf/proxy.conf index ebbd30aa..9f6402cd 100644 --- a/core/nginx/conf/proxy.conf +++ b/core/nginx/conf/proxy.conf @@ -6,7 +6,7 @@ proxy_hide_header True-Client-IP; proxy_hide_header CF-Connecting-IP; proxy_set_header X-Forwarded-Proto $proxy_x_forwarded_proto; -{% if (REAL_IP_HEADER or (PROXY_PROTOCOL in ['http', 'all'])) and REAL_IP_FROM %} +{% if (REAL_IP_HEADER or (PROXY_PROTOCOL_HTTP or PROXY_PROTOCOL_HTTPS)) and REAL_IP_FROM %} proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-By $realip_remote_addr; {% else %} diff --git a/core/nginx/config.py b/core/nginx/config.py index 73cc085c..966a402c 100755 --- a/core/nginx/config.py +++ b/core/nginx/config.py @@ -70,6 +70,44 @@ with open("/etc/resolv.conf") as handle: resolver = content[content.index("nameserver") + 1] args["RESOLVER"] = f"[{resolver}]" if ":" in resolver else resolver +# Configure PROXY_PROTOCOL +PROTO_MAIL=['SMTP', 'POP3', 'POP3S', 'IMAP', 'IMAPS', 'SUBMISSION', 'SUBMISSIONS', 'MANAGESIEVE'] +PROTO_ALL_BUT_HTTP=PROTO_MAIL.copy() +PROTO_ALL_BUT_HTTP.extend(['HTTPS']) +PROTO_ALL=PROTO_ALL_BUT_HTTP.copy() +PROTO_ALL.extend(['HTTP']) +for item in args.get('PROXY_PROTOCOL', '').split(','): + match item: + case '25': args['PROXY_PROTOCOL_SMTP']=True; continue + case '80': args['PROXY_PROTOCOL_HTTP']=True; continue + case '110': args['PROXY_PROTOCOL_POP3']=True; continue + case '143': args['PROXY_PROTOCOL_IMAP']=True; continue + case '443': args['PROXY_PROTOCOL_HTTPS']=True; continue + case '465': args['PROXY_PROTOCOL_SUBMISSIONS']=True; continue + case '587': args['PROXY_PROTOCOL_SUBMISSION']=True; continue + case '993': args['PROXY_PROTOCOL_IMAPS']=True; continue + case '995': args['PROXY_PROTOCOL_POP3S']=True; continue + case '4190': args['PROXY_PROTOCOL_MANAGESIEVE']=True; continue + case 'mail': + for p in PROTO_MAIL: args[f'PROXY_PROTOCOL_{p}']=True; continue + case 'all-but-http': + for p in PROTO_ALL_BUT_HTTP: args[f'PROXY_PROTOCOL_{p}']=True; continue + case 'all': + for p in PROTO_ALL: args[f'PROXY_PROTOCOL_{p}']=True; continue + +PORTS_REQUIRING_TLS=['443', '465', '993', '995'] +ALL_PORTS='25,80,443,465,587,993,995,4190' +for item in args.get('PORTS', ALL_PORTS).split(','): + if item in PORTS_REQUIRING_TLS and args['TLS_FLAVOR'] == 'notls': + continue + args[f'PORT_{item}']=True + +for item in args.get('TLS', ALL_PORTS).split(','): + if item in PORTS_REQUIRING_TLS: + if args['TLS_FLAVOR'] == 'notls': + continue + args[f'TLS_{item}']=True + # TLS configuration cert_name = args.get("TLS_CERT_FILENAME", "cert.pem") keypair_name = args.get("TLS_KEYPAIR_FILENAME", "key.pem") @@ -129,6 +167,8 @@ if args["TLS"] and not all(os.path.exists(file_path) for file_path in args["TLS" print("Missing cert or key file, disabling TLS") args["TLS_ERROR"] = "yes" +args['TLS_PERMISSIVE'] = str(args.get('TLS_PERMISSIVE')).lower() not in ('false', 'no') + # Build final configuration paths conf.jinja("/conf/tls.conf", args, "/etc/nginx/tls.conf") conf.jinja("/conf/proxy.conf", args, "/etc/nginx/proxy.conf") diff --git a/core/nginx/dovecot/proxy.conf b/core/nginx/dovecot/proxy.conf index db5a5f03..40ed607a 100644 --- a/core/nginx/dovecot/proxy.conf +++ b/core/nginx/dovecot/proxy.conf @@ -75,11 +75,12 @@ service anvil { } } +{%- if PORT_4190 %} service managesieve-login { executable = managesieve-login inet_listener sieve { port = 4190 -{%- if PROXY_PROTOCOL in ['all', 'all-but-http', 'mail'] %} +{%- if PROXY_PROTOCOL_MANAGESIEVE %} haproxy = yes {% endif %} } @@ -87,6 +88,7 @@ service managesieve-login { port = 14190 } } +{% endif %} protocol imap { mail_max_userip_connections = 20 @@ -94,42 +96,46 @@ protocol imap { } service imap-login { +{%- if PORT_143 %} inet_listener imap { port = 143 -{%- if PROXY_PROTOCOL in ['all', 'all-but-http', 'mail'] %} +{%- if PROXY_PROTOCOL_IMAP %} haproxy = yes {% endif %} } +{% endif %} +{%- if TLS_993 and PORT_993 %} inet_listener imaps { port = 993 -{%- if TLS %} ssl = yes -{% endif %} -{%- if PROXY_PROTOCOL in ['all', 'all-but-http', 'mail'] %} +{%- if PROXY_PROTOCOL_IMAPS %} haproxy = yes {% endif %} } +{% endif %} inet_listener imap-webmail { port = 10143 } } service pop3-login { +{%- if PORT_110 %} inet_listener pop3 { port = 110 -{%- if PROXY_PROTOCOL in ['all', 'all-but-http', 'mail'] %} +{%- if PROXY_PROTOCOL_POP3 %} haproxy = yes {% endif %} } +{% endif %} +{%- if TLS_995 and PORT_995 %} inet_listener pop3s { port = 995 -{%- if TLS %} ssl = yes -{% endif %} -{%- if PROXY_PROTOCOL in ['all', 'all-but-http', 'mail'] %} +{%- if PROXY_PROTOCOL_POP3S %} haproxy = yes {% endif %} } +{% endif %} } recipient_delimiter = {{ RECIPIENT_DELIMITER }} @@ -141,21 +147,23 @@ service lmtp { } service submission-login { +{%- if PORT_587 %} inet_listener submission { port = 587 -{%- if PROXY_PROTOCOL in ['all', 'all-but-http', 'mail'] %} +{%- if PROXY_PROTOCOL_SUBMISSION %} haproxy = yes {% endif %} } +{% endif %} +{%- if TLS_465 and PORT_465 %} inet_listener submissions { port = 465 -{%- if TLS %} ssl = yes -{% endif %} -{%- if PROXY_PROTOCOL in ['all', 'all-but-http', 'mail'] %} +{%- if PROXY_PROTOCOL_SUBMISSIONS %} haproxy = yes {% endif %} } +{% endif %} inet_listener submission-webmail { port = 10025 } diff --git a/towncrier/newsfragments/3061.feature b/towncrier/newsfragments/3061.feature new file mode 100644 index 00000000..66b6e669 --- /dev/null +++ b/towncrier/newsfragments/3061.feature @@ -0,0 +1 @@ +Introduce new settings for configuring proxying and TLS. Drop TLS_FLAVOR=mail-letsencrypt From 2b6405227b0e681f2374747b237cf9421f29ed8b Mon Sep 17 00:00:00 2001 From: Florent Daigniere Date: Sat, 6 Apr 2024 18:16:52 +0200 Subject: [PATCH 2/7] fix #3162: ensure snappymail works with notls --- webmails/snappymail/defaults/default.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/webmails/snappymail/defaults/default.json b/webmails/snappymail/defaults/default.json index 12252aee..dee93332 100644 --- a/webmails/snappymail/defaults/default.json +++ b/webmails/snappymail/defaults/default.json @@ -3,7 +3,7 @@ "IMAP": { "host": "{{ FRONT_ADDRESS }}", "port": 10143, - "secure": 2, + "secure": 3, "shortLogin": false, "ssl": { "verify_peer": false, @@ -20,7 +20,7 @@ "SMTP": { "host": "{{ FRONT_ADDRESS }}", "port": 10025, - "secure": 2, + "secure": 3, "shortLogin": false, "ssl": { "verify_peer": false, @@ -37,7 +37,7 @@ "Sieve": { "host": "{{ FRONT_ADDRESS }}", "port": 14190, - "type": 2, + "type": 3, "shortLogin": false, "ssl": { "verify_peer": false, From c701358c9d526909d7af8bc646bea6aa83db65f0 Mon Sep 17 00:00:00 2001 From: Florent Daigniere Date: Mon, 8 Apr 2024 09:02:09 +0200 Subject: [PATCH 3/7] simplify --- core/dovecot/conf/dovecot.conf | 2 +- core/nginx/conf/nginx.conf | 20 ++++++++-------- core/nginx/conf/proxy.conf | 2 +- core/nginx/config.py | 42 ++++++++++++++-------------------- core/nginx/dovecot/proxy.conf | 21 +++++++++-------- 5 files changed, 41 insertions(+), 46 deletions(-) diff --git a/core/dovecot/conf/dovecot.conf b/core/dovecot/conf/dovecot.conf index 75bd3a66..ef28efb1 100644 --- a/core/dovecot/conf/dovecot.conf +++ b/core/dovecot/conf/dovecot.conf @@ -5,7 +5,7 @@ log_path = /dev/stderr protocols = imap pop3 lmtp sieve postmaster_address = {{ POSTMASTER }}@{{ DOMAIN }} hostname = {{ HOSTNAMES.split(",")[0] }} -{%- if PROXY_PROTOCOL_SMTP %} +{%- if PROXY_PROTOCOL_25 %} submission_host = {{ HOSTNAMES.split(",")[0] }} {% else %} submission_host = {{ FRONT_ADDRESS }} diff --git a/core/nginx/conf/nginx.conf b/core/nginx/conf/nginx.conf index a29c570b..d67ce65a 100644 --- a/core/nginx/conf/nginx.conf +++ b/core/nginx/conf/nginx.conf @@ -22,7 +22,7 @@ http { {% if REAL_IP_HEADER %} real_ip_header {{ REAL_IP_HEADER }}; - {% elif (PROXY_PROTOCOL_HTTPS or PROXY_PROTOCOL_HTTP) and REAL_IP_FROM %} + {% elif (PROXY_PROTOCOL_80 or PROXY_PROTOCOL_443) and REAL_IP_FROM %} real_ip_header proxy_protocol; {% endif %} @@ -59,9 +59,9 @@ http { # server { # Listen over HTTP - listen 80{% if PROXY_PROTOCOL_HTTP %} proxy_protocol{% endif %}; + listen 80{% if PROXY_PROTOCOL_80 %} proxy_protocol{% endif %}; {% if SUBNET6 %} - listen [::]:80{% if PROXY_PROTOCOL_HTTP %} proxy_protocol{% endif %}; + listen [::]:80{% if PROXY_PROTOCOL_80 %} proxy_protocol{% endif %}; {% endif %} {% if TLS_FLAVOR in ['letsencrypt', 'mail-letsencrypt'] %} location ^~ /.well-known/acme-challenge/ { @@ -96,17 +96,17 @@ http { # Listen on HTTP only in kubernetes or behind reverse proxy {% if TLS_FLAVOR in [ 'mail-letsencrypt', 'notls', 'mail' ] %} - listen 80{% if PROXY_PROTOCOL_HTTP %} proxy_protocol{% endif %}; + listen 80{% if PROXY_HTTPPROTOCOL_80 %} proxy_protocol{% endif %}; {% if SUBNET6 %} - listen [::]:80{% if PROXY_PROTOCOL_HTTP %} proxy_protocol{% endif %}; + listen [::]:80{% if PROXY_PROTOCOL_80 %} proxy_protocol{% endif %}; {% endif %} {% endif %} # Only enable HTTPS if TLS is enabled with no error {% if TLS_443 and not TLS_ERROR %} - listen 443 ssl http2{% if PROXY_PROTOCOL_HTTPS %} proxy_protocol{% endif %}; + listen 443 ssl http2{% if PROXY_PROTOCOL_443 %} proxy_protocol{% endif %}; {% if SUBNET6 %} - listen [::]:443 ssl http2{% if PROXY_PROTOCOL_HTTPS %} proxy_protocol{% endif %}; + listen [::]:443 ssl http2{% if PROXY_PROTOCOL_443 %} proxy_protocol{% endif %}; {% endif %} include /etc/nginx/tls.conf; @@ -315,7 +315,7 @@ mail { ssl_session_cache shared:SSLMAIL:3m; {% endif %} - {% if PROXY_PROTOCOL_SMTP and REAL_IP_FROM %}{% for from_ip in REAL_IP_FROM.split(',') %} + {% if PROXY_PROTOCOL_25 and REAL_IP_FROM %}{% for from_ip in REAL_IP_FROM.split(',') %} set_real_ip_from {{ from_ip }}; {% endfor %}{% endif %} @@ -324,9 +324,9 @@ mail { # SMTP is always enabled, to avoid losing emails when TLS is failing server { - listen 25{% if PROXY_PROTOCOL_SMTP %} proxy_protocol{% endif %}; + listen 25{% if PROXY_PROTOCOL_25 %} proxy_protocol{% endif %}; {% if SUBNET6 %} - listen [::]:25{% if PROXY_PROTOCOL_SMTP %} proxy_protocol{% endif %}; + listen [::]:25{% if PROXY_PROTOCOL_25 %} proxy_protocol{% endif %}; {% endif %} {% if TLS_25 and not TLS_ERROR %} {% if TLS_FLAVOR in ['letsencrypt','mail-letsencrypt'] %} diff --git a/core/nginx/conf/proxy.conf b/core/nginx/conf/proxy.conf index 9f6402cd..7a3d8721 100644 --- a/core/nginx/conf/proxy.conf +++ b/core/nginx/conf/proxy.conf @@ -6,7 +6,7 @@ proxy_hide_header True-Client-IP; proxy_hide_header CF-Connecting-IP; proxy_set_header X-Forwarded-Proto $proxy_x_forwarded_proto; -{% if (REAL_IP_HEADER or (PROXY_PROTOCOL_HTTP or PROXY_PROTOCOL_HTTPS)) and REAL_IP_FROM %} +{% if (REAL_IP_HEADER or (PROXY_PROTOCOL_80 or PROXY_PROTOCOL_443)) and REAL_IP_FROM %} proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-By $realip_remote_addr; {% else %} diff --git a/core/nginx/config.py b/core/nginx/config.py index 966a402c..3d92d1b9 100755 --- a/core/nginx/config.py +++ b/core/nginx/config.py @@ -71,29 +71,22 @@ with open("/etc/resolv.conf") as handle: args["RESOLVER"] = f"[{resolver}]" if ":" in resolver else resolver # Configure PROXY_PROTOCOL -PROTO_MAIL=['SMTP', 'POP3', 'POP3S', 'IMAP', 'IMAPS', 'SUBMISSION', 'SUBMISSIONS', 'MANAGESIEVE'] +PROTO_MAIL=['25', '110', '995', '143', '993', '587', '465', '4190'] PROTO_ALL_BUT_HTTP=PROTO_MAIL.copy() -PROTO_ALL_BUT_HTTP.extend(['HTTPS']) +PROTO_ALL_BUT_HTTP.extend(['443']) PROTO_ALL=PROTO_ALL_BUT_HTTP.copy() -PROTO_ALL.extend(['HTTP']) +PROTO_ALL.extend(['80']) for item in args.get('PROXY_PROTOCOL', '').split(','): - match item: - case '25': args['PROXY_PROTOCOL_SMTP']=True; continue - case '80': args['PROXY_PROTOCOL_HTTP']=True; continue - case '110': args['PROXY_PROTOCOL_POP3']=True; continue - case '143': args['PROXY_PROTOCOL_IMAP']=True; continue - case '443': args['PROXY_PROTOCOL_HTTPS']=True; continue - case '465': args['PROXY_PROTOCOL_SUBMISSIONS']=True; continue - case '587': args['PROXY_PROTOCOL_SUBMISSION']=True; continue - case '993': args['PROXY_PROTOCOL_IMAPS']=True; continue - case '995': args['PROXY_PROTOCOL_POP3S']=True; continue - case '4190': args['PROXY_PROTOCOL_MANAGESIEVE']=True; continue - case 'mail': - for p in PROTO_MAIL: args[f'PROXY_PROTOCOL_{p}']=True; continue - case 'all-but-http': - for p in PROTO_ALL_BUT_HTTP: args[f'PROXY_PROTOCOL_{p}']=True; continue - case 'all': - for p in PROTO_ALL: args[f'PROXY_PROTOCOL_{p}']=True; continue + if item.isdigit(): + args[f'PROXY_PROTOCOL_{item}']=True + elif item == 'mail': + for p in PROTO_MAIL: args[f'PROXY_PROTOCOL_{p}']=True + elif item == 'all-but-http': + for p in PROTO_ALL_BUT_HTTP: args[f'PROXY_PROTOCOL_{p}']=True + elif item == 'all': + for p in PROTO_ALL: args[f'PROXY_PROTOCOL_{p}']=True + else: + log.error(f'Not sure what to do with {item} in PROXY_PROTOCOL ({args.get("PROXY_PROTOCOL")})') PORTS_REQUIRING_TLS=['443', '465', '993', '995'] ALL_PORTS='25,80,443,465,587,993,995,4190' @@ -102,11 +95,10 @@ for item in args.get('PORTS', ALL_PORTS).split(','): continue args[f'PORT_{item}']=True -for item in args.get('TLS', ALL_PORTS).split(','): - if item in PORTS_REQUIRING_TLS: - if args['TLS_FLAVOR'] == 'notls': - continue - args[f'TLS_{item}']=True +if args['TLS_FLAVOR'] != 'notls': + for item in args.get('TLS', ALL_PORTS).split(','): + if item in PORTS_REQUIRING_TLS: + args[f'TLS_{item}']=True # TLS configuration cert_name = args.get("TLS_CERT_FILENAME", "cert.pem") diff --git a/core/nginx/dovecot/proxy.conf b/core/nginx/dovecot/proxy.conf index 40ed607a..95972547 100644 --- a/core/nginx/dovecot/proxy.conf +++ b/core/nginx/dovecot/proxy.conf @@ -80,7 +80,7 @@ service managesieve-login { executable = managesieve-login inet_listener sieve { port = 4190 -{%- if PROXY_PROTOCOL_MANAGESIEVE %} +{%- if PROXY_PROTOCOL_4190 %} haproxy = yes {% endif %} } @@ -99,7 +99,7 @@ service imap-login { {%- if PORT_143 %} inet_listener imap { port = 143 -{%- if PROXY_PROTOCOL_IMAP %} +{%- if PROXY_PROTOCOL_143 %} haproxy = yes {% endif %} } @@ -108,7 +108,7 @@ service imap-login { inet_listener imaps { port = 993 ssl = yes -{%- if PROXY_PROTOCOL_IMAPS %} +{%- if PROXY_PROTOCOL_993 %} haproxy = yes {% endif %} } @@ -122,7 +122,7 @@ service pop3-login { {%- if PORT_110 %} inet_listener pop3 { port = 110 -{%- if PROXY_PROTOCOL_POP3 %} +{%- if PROXY_PROTOCOL_110 %} haproxy = yes {% endif %} } @@ -131,7 +131,7 @@ service pop3-login { inet_listener pop3s { port = 995 ssl = yes -{%- if PROXY_PROTOCOL_POP3S %} +{%- if PROXY_PROTOCOL_995 %} haproxy = yes {% endif %} } @@ -147,19 +147,22 @@ service lmtp { } service submission-login { -{%- if PORT_587 %} inet_listener submission { +{%- if PORT_587 %} port = 587 -{%- if PROXY_PROTOCOL_SUBMISSION %} +{%- if PROXY_PROTOCOL_587 %} haproxy = yes {% endif %} - } +{%- else %} +# if the section is unset the port is bound anyways + port = 0 {% endif %} + } {%- if TLS_465 and PORT_465 %} inet_listener submissions { port = 465 ssl = yes -{%- if PROXY_PROTOCOL_SUBMISSIONS %} +{%- if PROXY_PROTOCOL_645 %} haproxy = yes {% endif %} } From 614042344db14ea91262056ed2af09dcd0796495 Mon Sep 17 00:00:00 2001 From: Florent Daigniere Date: Mon, 8 Apr 2024 09:46:39 +0200 Subject: [PATCH 4/7] document --- core/nginx/conf/nginx.conf | 4 ++-- core/nginx/config.py | 2 +- docs/compose/setup.rst | 12 ++++------- docs/configuration.rst | 30 ++++++++++++++++++---------- docs/reverse.rst | 28 ++------------------------ towncrier/newsfragments/3061.feature | 7 ++++++- 6 files changed, 35 insertions(+), 48 deletions(-) diff --git a/core/nginx/conf/nginx.conf b/core/nginx/conf/nginx.conf index d67ce65a..bbd86fdc 100644 --- a/core/nginx/conf/nginx.conf +++ b/core/nginx/conf/nginx.conf @@ -310,7 +310,7 @@ mail { resolver {{ RESOLVER }} valid=30s; error_log /dev/stderr info; - {% if TLS_25 and not TLS_ERROR %} + {% if TLS and not TLS_ERROR %} include /etc/nginx/tls.conf; ssl_session_cache shared:SSLMAIL:3m; {% endif %} @@ -328,7 +328,7 @@ mail { {% if SUBNET6 %} listen [::]:25{% if PROXY_PROTOCOL_25 %} proxy_protocol{% endif %}; {% endif %} - {% if TLS_25 and not TLS_ERROR %} + {% if TLS and not TLS_ERROR %} {% if TLS_FLAVOR in ['letsencrypt','mail-letsencrypt'] %} ssl_certificate /certs/letsencrypt/live/mailu/DANE-chain.pem; ssl_certificate /certs/letsencrypt/live/mailu-ecdsa/DANE-chain.pem; diff --git a/core/nginx/config.py b/core/nginx/config.py index 3d92d1b9..1f06424b 100755 --- a/core/nginx/config.py +++ b/core/nginx/config.py @@ -89,7 +89,7 @@ for item in args.get('PROXY_PROTOCOL', '').split(','): log.error(f'Not sure what to do with {item} in PROXY_PROTOCOL ({args.get("PROXY_PROTOCOL")})') PORTS_REQUIRING_TLS=['443', '465', '993', '995'] -ALL_PORTS='25,80,443,465,587,993,995,4190' +ALL_PORTS='25,80,443,465,993,995,4190' for item in args.get('PORTS', ALL_PORTS).split(','): if item in PORTS_REQUIRING_TLS and args['TLS_FLAVOR'] == 'notls': continue diff --git a/docs/compose/setup.rst b/docs/compose/setup.rst index 81433ba3..ae1a6387 100644 --- a/docs/compose/setup.rst +++ b/docs/compose/setup.rst @@ -31,18 +31,14 @@ Sets the ``TLS_FLAVOR`` to one of the following values: - ``cert`` is the default and requires certificates to be setup manually; -- ``letsencrypt`` will use the *Letsencrypt!* CA to generate automatic certificates; -- ``mail`` is similar to ``cert`` except that TLS will only be served for - emails (IMAP and SMTP), not HTTP (use it behind reverse proxies); -- ``mail-letsencrypt`` is similar to ``letsencrypt`` except that TLS will only be served for - emails (IMAP and SMTP), not HTTP (use it behind reverse proxies); +- ``letsencrypt`` will use the *Letsencrypt!* CA to obtain certificates automatically; - ``notls`` will disable TLS, this is not recommended except for testing. .. note:: When using *Letsencrypt!* you have to make sure that the DNS ``A`` and ``AAAA`` records for the - all hostnames mentioned in the ``HOSTNAMES`` variable match with the ip addresses of you server. - Or else certificate generation will fail! See also: :ref:`dns_setup`. + all hostnames mentioned in the ``HOSTNAMES`` variable match with the ip addresses of you server + or else certificate generation will fail! See also: :ref:`dns_setup`. Bind address ```````````` @@ -91,7 +87,7 @@ Finish setting up TLS Mailu relies heavily on TLS and must have a key pair and a certificate available, at least for the hostname configured in the ``mailu.env`` file. -If you set ``TLS_FLAVOR`` to ``cert`` or ``mail`` then you must create a ``certs`` directory +If you set ``TLS_FLAVOR`` to ``cert`` then you must create a ``certs`` directory in your root path and setup a key-certificate pair there: - ``cert.pem`` contains the certificate (override with ``TLS_CERT_FILENAME``), diff --git a/docs/configuration.rst b/docs/configuration.rst index 50a576fd..f5818509 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -62,8 +62,7 @@ The ``AUTH_RATELIMIT_EXEMPTION`` (default: '') is a comma separated list of netw CIDRs that won't be subject to any form of rate limiting. Specifying ``0.0.0.0/0, ::/0`` there is a good way to disable rate limiting altogether. -The ``TLS_FLAVOR`` sets how Mailu handles TLS connections. Setting this value to -``notls`` will cause Mailu not to serve any web content! More on :ref:`tls_flavor`. +The ``TLS_FLAVOR`` sets how Mailu obtains a x509 certificate. More on :ref:`tls_flavor`. The ``DEFAULT_SPAM_THRESHOLD`` (default: 80) is the default spam tolerance used when creating a new user. @@ -248,9 +247,20 @@ but slows down the performance of modern devices. .. _`android handsets older than 7.1.1`: https://community.letsencrypt.org/t/production-chain-changes/150739 -The ``TLS_PERMISSIVE`` (default: true) setting controls whether ciphers and protocols offered on port 25 for STARTTLS are optimized for maximum compatibility. We **strongly recommend** that you do **not** change this setting on the basis that any encryption beats no encryption. If you are subject to compliance requirements and are not afraid of losing emails as a result of artificially reducing compatibility, set it to 'false'. Keep in mind that servers that are running a software stack old enough to not be compatible with the current TLS requirements will either a) deliver in plaintext b) bounce emails c) silently drop emails; moreover, modern servers will benefit from various downgrade protections (DOWNGRD, RFC7507) making the security argument mostly a moot point. +The ``TLS_PERMISSIVE`` (default: true) setting controls whether ciphers and protocols offered on port 25 +for STARTTLS are optimized for maximum compatibility. We **strongly recommend** that you do **not** change +this setting on the basis that any encryption beats no encryption. If you are subject to compliance +requirements and are not afraid of losing emails as a result of artificially reducing compatibility, +set it to 'false'. Keep in mind that servers that are running a software stack old enough to not be +compatible with the current TLS requirements will either a) deliver in plaintext b) bounce emails +c) silently drop emails; moreover, modern servers will benefit from various downgrade protections +(DOWNGRD, RFC7507) making the security argument mostly a moot point. -The ``COMPRESSION`` (default: unset) setting controls whether emails are stored compressed at rest on disk. Valid values are ``gz``, ``bz2`` or ``zstd`` and additional settings can be configured via ``COMPRESSION_LEVEL``, see `zlib_save_level`_ for accepted values. If the underlying filesystem supports compression natively you should use it instead of this setting as it will be more efficient and will improve compatibility with 3rd party tools. +The ``COMPRESSION`` (default: unset) setting controls whether emails are stored compressed at rest on disk. +Valid values are ``gz``, ``bz2`` or ``zstd`` and additional settings can be configured via +``COMPRESSION_LEVEL``, see `zlib_save_level`_ for accepted values. If the underlying filesystem +supports compression natively you should use it instead of this setting as it will be more efficient +and will improve compatibility with 3rd party tools. .. _`zlib_save_level`: https://doc.dovecot.org/settings/plugin/zlib-plugin/#plugin_setting-zlib-zlib_save_level @@ -267,13 +277,13 @@ The ``TZ`` sets the timezone Mailu will use. The timezone naming convention usua .. _`TZ database name`: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones -The ``PROXY_PROTOCOL`` (default: unset) allows the the front container to receive TCP and HTTP connections with -the `PROXY protocol`_ (originally introduced in HAProxy, now also configurable in other proxy servers). -It can be set to: -* ``http`` to accept the ``PROXY`` protocol on nginx's HTTP proxy ports -* ``mail`` to accept the ``PROXY`` protocol on nginx's mail proxy ports -* ``all`` to accept the ``PROXY`` protocol on all nginx's HTTP and mail proxy ports +The ``PORTS`` (default: '25,80,443,465,993,995,4190') setting determines which services should be enabled. It is a comma delimited list of ports numbers. +If you need to re-enable IMAP, POP3 and Submission, you can append '110,143,587' to that list. + +The ``PROXY_PROTOCOL`` (default: unset) setting allows the the front container to receive TCP and HTTP connections with +the `PROXY protocol`_ (originally introduced in HAProxy, now also configurable in other proxy servers). +It can be set to a comma delimited list of ports on which it should be enabled. .. _`PROXY protocol`: https://github.com/haproxy/haproxy/blob/master/doc/proxy-protocol.txt diff --git a/docs/reverse.rst b/docs/reverse.rst index de427d5d..56958d9b 100644 --- a/docs/reverse.rst +++ b/docs/reverse.rst @@ -28,11 +28,8 @@ and add a section like follows: - "--entrypoints.web.address=:http" - "--entrypoints.websecure.address=:https" - "--entrypoints.smtp.address=:smtp" - - "--entrypoints.submission.address=:submission" - "--entrypoints.submissions.address=:submissions" - - "--entrypoints.imap.address=:imap" - "--entrypoints.imaps.address=:imaps" - - "--entrypoints.pop3.address=:pop3" - "--entrypoints.pop3s.address=:pop3s" - "--entrypoints.sieve.address=:sieve" # - "--api.insecure=true" @@ -42,11 +39,8 @@ and add a section like follows: - "80:80" - "443:443" - "465:465" - - "587:587" - "993:993" - "995:995" - - "110:110" - - "143:143" - "4190:4190" # The Web UI (enabled by --api.insecure=true) # - "8080:8080" @@ -80,36 +74,18 @@ and then add the following to the front section: - "traefik.tcp.services.smtp.loadbalancer.server.port=25" - "traefik.tcp.services.smtp.loadbalancer.proxyProtocol.version=2" - - "traefik.tcp.routers.submission.rule=HostSNI(`*`)" - - "traefik.tcp.routers.submission.entrypoints=submission" - - "traefik.tcp.routers.submission.service=submission" - - "traefik.tcp.services.submission.loadbalancer.server.port=587" - - "traefik.tcp.services.submission.loadbalancer.proxyProtocol.version=2" - - "traefik.tcp.routers.submissions.rule=HostSNI(`*`)" - "traefik.tcp.routers.submissions.entrypoints=submissions" - "traefik.tcp.routers.submissions.service=submissions" - "traefik.tcp.services.submissions.loadbalancer.server.port=465" - "traefik.tcp.services.submissions.loadbalancer.proxyProtocol.version=2" - - "traefik.tcp.routers.imap.rule=HostSNI(`*`)" - - "traefik.tcp.routers.imap.entrypoints=imap" - - "traefik.tcp.routers.imap.service=imap" - - "traefik.tcp.services.imap.loadbalancer.server.port=143" - - "traefik.tcp.services.imap.loadbalancer.proxyProtocol.version=2" - - "traefik.tcp.routers.imaps.rule=HostSNI(`*`)" - "traefik.tcp.routers.imaps.entrypoints=imaps" - "traefik.tcp.routers.imaps.service=imaps" - "traefik.tcp.services.imaps.loadbalancer.server.port=993" - "traefik.tcp.services.imaps.loadbalancer.proxyProtocol.version=2" - - "traefik.tcp.routers.pop3.rule=HostSNI(`*`)" - - "traefik.tcp.routers.pop3.entrypoints=pop3" - - "traefik.tcp.routers.pop3.service=pop3" - - "traefik.tcp.services.pop3.loadbalancer.server.port=110" - - "traefik.tcp.services.pop3.loadbalancer.proxyProtocol.version=2" - - "traefik.tcp.routers.pop3s.rule=HostSNI(`*`)" - "traefik.tcp.routers.pop3s.entrypoints=pop3s" - "traefik.tcp.routers.pop3s.service=pop3s" @@ -129,9 +105,9 @@ in mailu.env: .. code-block:: docker REAL_IP_FROM=192.168.203.0/24 - PROXY_PROTOCOL=all-but-http + PROXY_PROTOCOL=25,443,465,993,995,4190 TRAEFIK_VERSION=v2 - TLS_FLAVOR=mail-letsencrypt + TLS_FLAVOR=letsencrypt WEBROOT_REDIRECT=/sso/login Using the above configuration, Traefik will proxy all the traffic related to Mailu's FQDNs without requiring duplicate certificates. diff --git a/towncrier/newsfragments/3061.feature b/towncrier/newsfragments/3061.feature index 66b6e669..b8f3e4cd 100644 --- a/towncrier/newsfragments/3061.feature +++ b/towncrier/newsfragments/3061.feature @@ -1 +1,6 @@ -Introduce new settings for configuring proxying and TLS. Drop TLS_FLAVOR=mail-letsencrypt +Introduce new settings for configuring proxying and TLS. Disable POP3, IMAP and SUBMISSION by default, see https://nostarttls.secvuln.info/ +- Drop TLS_FLAVOR=mail-* +- Change the meaning of PROXY_PROTOCOL, introduce PORTS +- Disable POP3, IMAP and SUBMISSION ports by default, to re-enable ensure PORTS include 110, 143 and 587 + +MANAGESIEVE with implicit TLS is not a thing clients support... so 4190 is enabled by default. From 4e28a053e3f883014e503231b419d52c4d4eae0d Mon Sep 17 00:00:00 2001 From: Florent Daigniere Date: Mon, 8 Apr 2024 09:55:20 +0200 Subject: [PATCH 5/7] enable all ports for tests --- tests/compose/core/mailu.env | 2 ++ tests/compose/fetchmail/mailu.env | 2 ++ tests/compose/filters/mailu.env | 2 ++ tests/compose/webdav/mailu.env | 2 ++ tests/compose/webmail/mailu.env | 2 ++ 5 files changed, 10 insertions(+) diff --git a/tests/compose/core/mailu.env b/tests/compose/core/mailu.env index 30ecd830..5b99d33f 100644 --- a/tests/compose/core/mailu.env +++ b/tests/compose/core/mailu.env @@ -147,3 +147,5 @@ REJECT_UNLISTED_RECIPIENT= INITIAL_ADMIN_ACCOUNT=admin INITIAL_ADMIN_DOMAIN=mailu.io INITIAL_ADMIN_PW=FooBar + +PORTS=25,80,443,110,995,143,993,587,465,4190 \ No newline at end of file diff --git a/tests/compose/fetchmail/mailu.env b/tests/compose/fetchmail/mailu.env index af736912..570fccd4 100644 --- a/tests/compose/fetchmail/mailu.env +++ b/tests/compose/fetchmail/mailu.env @@ -142,3 +142,5 @@ REAL_IP_FROM= # choose wether mailu bounces (no) or rejects (yes) mail when recipient is unknown (value: yes, no) REJECT_UNLISTED_RECIPIENT= + +PORTS=25,80,443,110,995,143,993,587,465,4190 \ No newline at end of file diff --git a/tests/compose/filters/mailu.env b/tests/compose/filters/mailu.env index 0c48baf3..f8536e65 100644 --- a/tests/compose/filters/mailu.env +++ b/tests/compose/filters/mailu.env @@ -142,3 +142,5 @@ REAL_IP_FROM= # choose wether mailu bounces (no) or rejects (yes) mail when recipient is unknown (value: yes, no) REJECT_UNLISTED_RECIPIENT= + +PORTS=25,80,443,110,995,143,993,587,465,4190 \ No newline at end of file diff --git a/tests/compose/webdav/mailu.env b/tests/compose/webdav/mailu.env index 88f3c671..cbda1e67 100644 --- a/tests/compose/webdav/mailu.env +++ b/tests/compose/webdav/mailu.env @@ -142,3 +142,5 @@ REAL_IP_FROM= # choose wether mailu bounces (no) or rejects (yes) mail when recipient is unknown (value: yes, no) REJECT_UNLISTED_RECIPIENT= + +PORTS=25,80,443,110,995,143,993,587,465,4190 \ No newline at end of file diff --git a/tests/compose/webmail/mailu.env b/tests/compose/webmail/mailu.env index 53b86cba..fc78eeab 100644 --- a/tests/compose/webmail/mailu.env +++ b/tests/compose/webmail/mailu.env @@ -142,3 +142,5 @@ REAL_IP_FROM= # choose wether mailu bounces (no) or rejects (yes) mail when recipient is unknown (value: yes, no) REJECT_UNLISTED_RECIPIENT= + +PORTS=25,80,443,110,995,143,993,587,465,4190 \ No newline at end of file From c63bd0ce38832bea9e12894d9fca1f567506286b Mon Sep 17 00:00:00 2001 From: Florent Daigniere Date: Sun, 9 Jun 2024 11:59:05 +0200 Subject: [PATCH 6/7] Update core/nginx/conf/nginx.conf Co-authored-by: Dimitri Huisman <52963853+Diman0@users.noreply.github.com> --- core/nginx/conf/nginx.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/nginx/conf/nginx.conf b/core/nginx/conf/nginx.conf index bbd86fdc..e7d4885e 100644 --- a/core/nginx/conf/nginx.conf +++ b/core/nginx/conf/nginx.conf @@ -96,7 +96,7 @@ http { # Listen on HTTP only in kubernetes or behind reverse proxy {% if TLS_FLAVOR in [ 'mail-letsencrypt', 'notls', 'mail' ] %} - listen 80{% if PROXY_HTTPPROTOCOL_80 %} proxy_protocol{% endif %}; + listen 80{% if PROXY_PROTOCOL_80 %} proxy_protocol{% endif %}; {% if SUBNET6 %} listen [::]:80{% if PROXY_PROTOCOL_80 %} proxy_protocol{% endif %}; {% endif %} From 52e02d4c565edcce288ab3adcad9a2567c4aaa7e Mon Sep 17 00:00:00 2001 From: Florent Daigniere Date: Sun, 9 Jun 2024 11:59:12 +0200 Subject: [PATCH 7/7] Update core/nginx/dovecot/proxy.conf Co-authored-by: Dimitri Huisman <52963853+Diman0@users.noreply.github.com> --- core/nginx/dovecot/proxy.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/nginx/dovecot/proxy.conf b/core/nginx/dovecot/proxy.conf index 95972547..a6e64719 100644 --- a/core/nginx/dovecot/proxy.conf +++ b/core/nginx/dovecot/proxy.conf @@ -162,7 +162,7 @@ service submission-login { inet_listener submissions { port = 465 ssl = yes -{%- if PROXY_PROTOCOL_645 %} +{%- if PROXY_PROTOCOL_465 %} haproxy = yes {% endif %} }