From 4b25f2d139c1647c76950a384846cbe9249a3764 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tao=20Bojl=C3=A9n?= Date: Sat, 4 Jul 2026 12:46:42 +0100 Subject: [PATCH] fix: harden Haraka TLS (renegotiation DoS + untrusted cert chain) Two scanner findings on app.shroud.email:25, both in the SMTP (Haraka) TLS path: 1. Client-initiated TLS renegotiation allowed (DoS). Haraka tls_socket passes secureOptions from tls.ini [main] straight to tls.createSecureContext. Set secureOptions=1073741824 (crypto.constants.SSL_OP_NO_RENEGOTIATION) to refuse renegotiation, closing the CPU-exhaustion vector. 2. Untrusted certificate presented on port 25. The daily cron bundle appended a hardcoded, now-expired intermediate (lets-encrypt-r4.pem, expired 2025-09-15) to Caddy fullchain, producing a broken chain. Caddy {domain}.crt is already leaf + intermediates; copy it verbatim and drop the stale intermediate. Also run the bundle at container startup (not only daily) so a fresh pem_certs volume is populated before Haraka first STARTTLS, and no-op gracefully when Caddy has not issued yet. --- cron/Dockerfile | 10 +++++----- cron/bundle_certs.sh | 19 +++++++++++++----- cron/lets-encrypt-r4.pem | 30 ----------------------------- haraka/haraka_config/config/tls.ini | 5 +++++ 4 files changed, 24 insertions(+), 40 deletions(-) delete mode 100644 cron/lets-encrypt-r4.pem diff --git a/cron/Dockerfile b/cron/Dockerfile index 2c0a5db..cfa6a26 100644 --- a/cron/Dockerfile +++ b/cron/Dockerfile @@ -5,11 +5,11 @@ RUN apk update && \ WORKDIR /workdir -COPY lets-encrypt-r4.pem /workdir/lets-encrypt-r4.pem -COPY bundle_certs.sh /etc/periodic/daily/bundle_certs - -RUN chmod +x /etc/periodic/daily/bundle_certs +COPY bundle_certs.sh /usr/local/bin/bundle_certs +RUN chmod +x /usr/local/bin/bundle_certs VOLUME /pem -CMD ["crond", "-f", "-l", "0"] +# Run bundle once at boot (populate pem_certs before Haraka's first STARTTLS), +# then keep the daily crond for renewals. Caddy's fullchain is copied verbatim. +CMD ["sh", "-c", "bundle_certs; exec crond -f -l 0"] diff --git a/cron/bundle_certs.sh b/cron/bundle_certs.sh index 02e3cc8..917d72c 100644 --- a/cron/bundle_certs.sh +++ b/cron/bundle_certs.sh @@ -3,9 +3,18 @@ set -e if [ -z "$EMAIL_DOMAIN" ]; then echo "EMAIL_DOMAIN is not set"; exit 1; fi -cd /workdir +CERT_DIR="/caddy/certificates/acme-v02.api.letsencrypt.org-directory/$EMAIL_DOMAIN" +LEAF="$CERT_DIR/${EMAIL_DOMAIN}.crt" +KEY="$CERT_DIR/${EMAIL_DOMAIN}.key" + +if [ ! -s "$LEAF" ] || [ ! -s "$KEY" ]; then + echo "Caddy cert not ready yet ($LEAF); skipping" + exit 0 +fi + echo "Copying Caddy certs to Haraka..." -cd "/caddy/certificates/acme-v02.api.letsencrypt.org-directory/$EMAIL_DOMAIN" -cp "${EMAIL_DOMAIN}.key" /pem/tls_key.pem -cat "${EMAIL_DOMAIN}.crt" /workdir/lets-encrypt-r4.pem > /pem/tls_cert.pem -echo "Copied Caddy certs to Haraka." \ No newline at end of file +# Caddy's {domain}.crt is already the full chain (leaf + intermediates). +# Copy it verbatim — appending a separate intermediate would duplicate/break the chain. +cp "$KEY" /pem/tls_key.pem +cp "$LEAF" /pem/tls_cert.pem +echo "Copied Caddy certs to Haraka." diff --git a/cron/lets-encrypt-r4.pem b/cron/lets-encrypt-r4.pem deleted file mode 100644 index 578b3bd..0000000 --- a/cron/lets-encrypt-r4.pem +++ /dev/null @@ -1,30 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIFFjCCAv6gAwIBAgIRAIp5IlCr5SxSbO7Pf8lC3WIwDQYJKoZIhvcNAQELBQAw -TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh -cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMjAwOTA0MDAwMDAw -WhcNMjUwOTE1MTYwMDAwWjAyMQswCQYDVQQGEwJVUzEWMBQGA1UEChMNTGV0J3Mg -RW5jcnlwdDELMAkGA1UEAxMCUjQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK -AoIBAQCzKNx3KdPnkb7ztwoAx/vyVQslImNTNq/pCCDfDa8oPs3Gq1e2naQlGaXS -Mm1Jpgi5xy+hm5PFIEBrhDEgoo4wYCVg79kaiT8faXGy2uo/c0HEkG9m/X2eWNh3 -z81ZdUTJoQp7nz8bDjpmb7Z1z4vLr53AcMX/0oIKr13N4uichZSk5gA16H5OOYHH -IYlgd+odlvKLg3tHxG0ywFJ+Ix5FtXHuo+8XwgOpk4nd9Z/buvHa4H6Xh3GBHhqC -VuQ+fBiiCOUWX6j6qOBIUU0YFKAMo+W2yrO1VRJrcsdafzuM+efZ0Y4STTMzAyrx -E+FCPMIuWWAubeAHRzNl39Jnyk2FAgMBAAGjggEIMIIBBDAOBgNVHQ8BAf8EBAMC -AYYwHQYDVR0lBBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMBMBIGA1UdEwEB/wQIMAYB -Af8CAQAwHQYDVR0OBBYEFDadPuCxQPYnLHy/jZ0xivZUpkYmMB8GA1UdIwQYMBaA -FHm0WeZ7tuXkAXOACIjIGlj26ZtuMDIGCCsGAQUFBwEBBCYwJDAiBggrBgEFBQcw -AoYWaHR0cDovL3gxLmkubGVuY3Iub3JnLzAnBgNVHR8EIDAeMBygGqAYhhZodHRw -Oi8veDEuYy5sZW5jci5vcmcvMCIGA1UdIAQbMBkwCAYGZ4EMAQIBMA0GCysGAQQB -gt8TAQEBMA0GCSqGSIb3DQEBCwUAA4ICAQCJbu5CalWO+H+Az0lmIG14DXmlYHQE -k26umjuCyioWs2icOlZznPTcZvbfq02YPHGTCu3ctggVDULJ+fwOxKekzIqeyLNk -p8dyFwSAr23DYBIVeXDpxHhShvv0MLJzqqDFBTHYe1X5X2Y7oogy+UDJxV2N24/g -Z8lxG4Vr2/VEfUOrw4Tosl5Z+1uzOdvTyBcxD/E5rGgTLczmulctHy3IMTmdTFr0 -FnU0/HMQoquWQuODhFqzMqNcsdbjANUBwOEQrKI8Sy6+b84kHP7PtO+S4Ik8R2k7 -ZeMlE1JmxBi/PZU860YlwT8/qOYToCHVyDjhv8qutbf2QnUl3SV86th2I1QQE14s -0y7CdAHcHkw3sAEeYGkwCA74MO+VFtnYbf9B2JBOhyyWb5087rGzitu5MTAW41X9 -DwTeXEg+a24tAeht+Y1MionHUwa4j7FB/trN3Fnb/r90+4P66ZETVIEcjseUSMHO -w6yqv10/H/dw/8r2EDUincBBX3o9DL3SadqragkKy96HtMiLcqMMGAPm0gti1b6f -bnvOdr0mrIVIKX5nzOeGZORaYLoSD4C8qvFT7U+Um6DMo36cVDNsPmkF575/s3C2 -CxGiCPQqVxPgfNSh+2CPd2Xv04lNeuw6gG89DlOhHuoFKRlmPnom+gwqhz3ZXMfz -TfmvjrBokzCICA== ------END CERTIFICATE----- diff --git a/haraka/haraka_config/config/tls.ini b/haraka/haraka_config/config/tls.ini index 15041fb..48b41d7 100644 --- a/haraka/haraka_config/config/tls.ini +++ b/haraka/haraka_config/config/tls.ini @@ -1,2 +1,7 @@ key=certs/tls_key.pem cert=certs/tls_cert.pem + +; Disable client-initiated TLS renegotiation (DoS via repeated handshakes). +; Value is crypto.constants.SSL_OP_NO_RENEGOTIATION (0x40000000), passed +; through to tls.createSecureContext as secureOptions by Haraka's tls_socket. +secureOptions=1073741824