{ config, pkgs, lib, ... }: let # Caddy's canonical certificate storage (XDG data dir). Renewals land here, # owned caddy:caddy 0600 — the mail server's non-root Postfix/Dovecot cannot # read it directly, so we copy it into a world-readable distribution dir. caddyCertDir = "/var/lib/caddy/.local/share/caddy/certificates/acme-v02.api.letsencrypt.org-directory/mail.severijnse.eu"; # World-readable distribution dir mounted (RO) into the mail server container. distCertDir = "/var/lib/caddy/certificates/acme-v02.api.letsencrypt.org-directory/mail.severijnse.eu"; zoneFile = "/var/lib/coredns/zones/severijnse.eu.db"; syncScript = pkgs.writeShellScript "tlsa-update" '' set -euo pipefail OPENSSL="${pkgs.openssl}/bin/openssl" SRC_CERT="${caddyCertDir}/mail.severijnse.eu.crt" SRC_KEY="${caddyCertDir}/mail.severijnse.eu.key" DST_CERT="${distCertDir}/mail.severijnse.eu.crt" DST_KEY="${distCertDir}/mail.severijnse.eu.key" # Caddy has not obtained the certificate yet (e.g. first boot before HTTP-01). if [ ! -f "$SRC_CERT" ] || [ ! -f "$SRC_KEY" ]; then echo "tlsa-update: certificate not found at $caddyCertDir, skipping" >&2 exit 0 fi # 1) Propagate Caddy's renewed certificate into the distribution dir the # mail server mounts. Caddy stores certs 0600 caddy:caddy. The cert is # world-readable (Postfix/Dovecot read it as root before dropping # privileges); the private key is restricted to root (0640) so it is not # exposed to other local users. install -D -m 0644 "$SRC_CERT" "$DST_CERT" install -D -m 0640 "$SRC_KEY" "$DST_KEY" # 2) TLSA 3 1 1 = SHA-256 of the certificate's SubjectPublicKeyInfo (SPKI), # NOT the whole certificate. Matching type 1 = SHA-256 of the SPKI DER. HEX=$("$OPENSSL" x509 -in "$DST_CERT" -noout -pubkey 2>/dev/null | "$OPENSSL" pkey -pubin -outform DER 2>/dev/null | "$OPENSSL" dgst -sha256 | cut -d' ' -f2) # 3) Update the CoreDNS zone. coredns.service regenerates this file from the # Nix store on every start, so this unit (which is partOf coredns and runs # after it) re-applies the correct TLSA after each rebuild/restart. if [ -f "${zoneFile}" ]; then sed -i -E "s/^(_25\._tcp\.mail.*TLSA 3 1 1).*/\1 $HEX/" "${zoneFile}" sed -i -E "s/^(_465\._tcp\.mail.*TLSA 3 1 1).*/\1 $HEX/" "${zoneFile}" sed -i -E "s/^(_993\._tcp\.mail.*TLSA 3 1 1).*/\1 $HEX/" "${zoneFile}" chown coredns:coredns "${zoneFile}" chmod 0640 "${zoneFile}" fi # 4) Reload services so the changes take effect immediately. systemctl reload coredns.service || true podman exec mailserver postfix reload || true podman exec mailserver dovecot reload || true echo "tlsa-update: TLSA set to $HEX" ''; in { # Ensure the distribution dir exists (Caddy does not write here). systemd.tmpfiles.rules = [ "d ${distCertDir} 0755 root root - -" ]; systemd.services.tlsa-update = { description = "Sync Caddy TLS certificate to mail server and update DANE/TLSA records"; after = ["caddy.service" "coredns.service"]; partOf = ["coredns.service"]; wantedBy = ["multi-user.target"]; path = with pkgs; [openssl coreutils gnused podman systemd]; serviceConfig = { Type = "oneshot"; ExecStart = "${syncScript}"; User = "root"; Group = "root"; }; }; # Fire as soon as Caddy rewrites the certificate on renewal (the atomic rewrite # changes the directory mtime), eliminating the up-to-24h DANE drift window. systemd.paths.tlsa-update = { description = "Watch Caddy certificate directory for renewal"; wantedBy = ["paths.target"]; pathConfig = { PathModified = [caddyCertDir]; Unit = "tlsa-update.service"; }; }; # Fallback in case a renewal event is missed (e.g. inotify overflow). systemd.timers.tlsa-update = { wantedBy = ["timers.target"]; timerConfig = { OnCalendar = "daily"; Persistent = true; }; }; }