From 5b01285ee33516cec607cbd069e06b4b8970c088 Mon Sep 17 00:00:00 2001 From: Alejandro Soto Date: Sun, 6 Apr 2025 14:27:57 -0600 Subject: sys: reload services after certificate renewal --- sys/mail/default.nix | 360 +++++++++++++++++++++++++-------------------------- sys/mta/default.nix | 310 ++++++++++++++++++++++---------------------- sys/web/nginx.nix | 1 + 3 files changed, 336 insertions(+), 335 deletions(-) (limited to 'sys') diff --git a/sys/mail/default.nix b/sys/mail/default.nix index 0e789ae..f692383 100644 --- a/sys/mail/default.nix +++ b/sys/mail/default.nix @@ -3,6 +3,8 @@ with lib; let cfg = config.local.mailHost; imapHostname = config.local.domains.imap.main; + cert = config.security.acme.certs.${imapHostname}.directory; + inherit (config.local) users virtual; in { @@ -24,194 +26,190 @@ in config = mkIf cfg.enable { services = { - dovecot2 = - let - cert = config.security.acme.certs.${imapHostname}.directory; - in - { - enable = true; - enablePAM = false; - enableLmtp = true; - - sslServerKey = "${cert}/key.pem"; - sslServerCert = "${cert}/fullchain.pem"; - - modules = [ pkgs.dovecot_pigeonhole ]; - - mailUser = "vmail"; - mailGroup = "vmail"; - mailLocation = "maildir:~/mail"; - mailPlugins.perProtocol.lmtp.enable = [ "sieve" ]; - - # https://github.com/NixOS/nixpkgs/issues/286859 - sieve.extensions = [ - "fileinto" - "mailbox" - ]; - - extraConfig = - let - inherit (config.networking) domain; - - # https://dovecot.org/list/dovecot/2019-March/115250.html - # Otra solución posible (https://serverfault.com/a/1062274/980378): - # auth_username_format = %{if;%d;eq;${domain};%Ln;%Lu} - localEntry = canonical: username: '' - ${username}:::::::user=${canonical} nopassword userdb_user=${canonical} - ''; - - localMailboxes = - pkgs.writeText "local-mailboxes" - (concatStrings - (flatten (mapAttrsToList - (canonical: user: - map (localEntry canonical) ([ canonical ] ++ user.hardAliases)) - users))); - - localCerts = - flatten (mapAttrsToList - (canonical: user: + dovecot2 = { + enable = true; + enablePAM = false; + enableLmtp = true; + + sslServerKey = "${cert}/key.pem"; + sslServerCert = "${cert}/fullchain.pem"; + + modules = [ pkgs.dovecot_pigeonhole ]; + + mailUser = "vmail"; + mailGroup = "vmail"; + mailLocation = "maildir:~/mail"; + mailPlugins.perProtocol.lmtp.enable = [ "sieve" ]; + + # https://github.com/NixOS/nixpkgs/issues/286859 + sieve.extensions = [ + "fileinto" + "mailbox" + ]; + + extraConfig = + let + inherit (config.networking) domain; + + # https://dovecot.org/list/dovecot/2019-March/115250.html + # Otra solución posible (https://serverfault.com/a/1062274/980378): + # auth_username_format = %{if;%d;eq;${domain};%Ln;%Lu} + localEntry = canonical: username: '' + ${username}:::::::user=${canonical} nopassword userdb_user=${canonical} + ''; + + localMailboxes = + pkgs.writeText "local-mailboxes" + (concatStrings + (flatten (mapAttrsToList + (canonical: user: + map (localEntry canonical) ([ canonical ] ++ user.hardAliases)) + users))); + + localCerts = + flatten (mapAttrsToList + (canonical: user: + let + certNames = { + inherit canonical; + logins = [ canonical ] ++ user.hardAliases; + }; + in + map (flip nameValuePair certNames) user.mail.certs) + users); + + vmailCerts = + flatten (flatten (mapAttrsToList + (domain: virtual: mapAttrsToList + (username: user: let + address = "${username}@${domain}"; + certNames = { - inherit canonical; - logins = [ canonical ] ++ user.hardAliases; + canonical = address; + logins = [ address ]; }; in map (flip nameValuePair certNames) user.mail.certs) - users); - - vmailCerts = - flatten (flatten (mapAttrsToList - (domain: virtual: mapAttrsToList - (username: user: - let - address = "${username}@${domain}"; - - certNames = { - canonical = address; - logins = [ address ]; - }; - in - map (flip nameValuePair certNames) user.mail.certs) - virtual.users) - virtual)); - - certLogins = - pkgs.writeText "cert-logins" - (concatLines (flatten (mapAttrsToList - (certPath: names: map - (addr: "${config.local.pki.byPath.${certPath}.commonName}@nodomain,${addr}:::::::user=${names.canonical}") - names.logins) - (listToAttrs (localCerts ++ vmailCerts))))); - - vmailPath = "/var/lib/vmail/%{if;%d;ne;;%Ld;${domain}}"; - in - '' - auth_mechanisms = plain login external - - ssl_ca = <${config.local.pki.ca.mail.fullchain} - ssl_require_crl = yes - ssl_verify_client_cert = yes - - # Esto descarta @domain.tld de locales explícitos, pero lo exige para los demás. - # Implicación: locales implícitos sin dominio fallan en autenticar - auth_username_format = %{if;%Ld;eq;${domain};%Ln;%{if;%d;ne;;%Lu;%Ln@nodomain}} - auth_ssl_username_from_cert = yes - - # TODO: los defaults de nixpkgs dejan los sockets bajo - # /run/dovecot2 con demasiados permisos rwx, arreglar - - service auth { - inet_listener mta-sasl { - port = ${toString cfg.saslPort} - address = ${cfg.mdaListen} - } - } - - service lmtp { - inet_listener mta-lmtp { - port = ${toString cfg.lmtpPort} - address = ${cfg.mdaListen} - } - } - - # FIXME: Esta cadena de passdbs hace que 'doveadm user lookup' - # falle para usuarios locales, pero todo lo demás sirve. Parece - # ser debido a que pam no puede enumerar. - - passdb { - driver = static - args = nopassword - - master = yes - mechanisms = external - - result_success = continue-fail - result_failure = return-fail - result_internalfail = return-fail - } - - passdb { - driver = passwd-file - args = scheme=PLAIN username_format=%{master_user},%Lu ${certLogins} - - mechanisms = external - override_fields = nopassword - - result_failure = return-fail - result_internalfail = return-fail + virtual.users) + virtual)); + + certLogins = + pkgs.writeText "cert-logins" + (concatLines (flatten (mapAttrsToList + (certPath: names: map + (addr: "${config.local.pki.byPath.${certPath}.commonName}@nodomain,${addr}:::::::user=${names.canonical}") + names.logins) + (listToAttrs (localCerts ++ vmailCerts))))); + + vmailPath = "/var/lib/vmail/%{if;%d;ne;;%Ld;${domain}}"; + in + '' + auth_mechanisms = plain login external + + ssl_ca = <${config.local.pki.ca.mail.fullchain} + ssl_require_crl = yes + ssl_verify_client_cert = yes + + # Esto descarta @domain.tld de locales explícitos, pero lo exige para los demás. + # Implicación: locales implícitos sin dominio fallan en autenticar + auth_username_format = %{if;%Ld;eq;${domain};%Ln;%{if;%d;ne;;%Lu;%Ln@nodomain}} + auth_ssl_username_from_cert = yes + + # TODO: los defaults de nixpkgs dejan los sockets bajo + # /run/dovecot2 con demasiados permisos rwx, arreglar + + service auth { + inet_listener mta-sasl { + port = ${toString cfg.saslPort} + address = ${cfg.mdaListen} } + } - passdb { - driver = passwd-file - args = username_format=%Ln ${vmailPath}/passwd + service lmtp { + inet_listener mta-lmtp { + port = ${toString cfg.lmtpPort} + address = ${cfg.mdaListen} } - - passdb { - driver = passwd-file - args = scheme=PLAIN ${localMailboxes} - - # Esta es una forma de determinar si se encontró el usuario en - # el passwd-file por medio de nopassword sin realmente - # autenticarlo. Cuidado con result_success, porque si eso se - # configura mal se permite inicio de sesión con cualquier - # contraseña (!!!). - result_success = continue - result_failure = return-fail - result_internalfail = return-fail - - username_filter = !*@* - } - - passdb { - driver = pam - args = dovecot2 - username_filter = !*@* - #TODO: algo como 'override_fields = allow_nets=...' - } - - userdb { - driver = passwd-file - args = username_format=%Ln ${vmailPath}/passwd - override_fields = uid=vmail gid=vmail home=${vmailPath}/home/%Ln - } - - userdb { - driver = passwd-file - args = ${localMailboxes} - - result_success = continue-ok - result_internalfail = return-fail - skip = found - } - - userdb { - driver = passwd - args = blocking=no - skip = notfound - } - ''; - }; + } + + # FIXME: Esta cadena de passdbs hace que 'doveadm user lookup' + # falle para usuarios locales, pero todo lo demás sirve. Parece + # ser debido a que pam no puede enumerar. + + passdb { + driver = static + args = nopassword + + master = yes + mechanisms = external + + result_success = continue-fail + result_failure = return-fail + result_internalfail = return-fail + } + + passdb { + driver = passwd-file + args = scheme=PLAIN username_format=%{master_user},%Lu ${certLogins} + + mechanisms = external + override_fields = nopassword + + result_failure = return-fail + result_internalfail = return-fail + } + + passdb { + driver = passwd-file + args = username_format=%Ln ${vmailPath}/passwd + } + + passdb { + driver = passwd-file + args = scheme=PLAIN ${localMailboxes} + + # Esta es una forma de determinar si se encontró el usuario en + # el passwd-file por medio de nopassword sin realmente + # autenticarlo. Cuidado con result_success, porque si eso se + # configura mal se permite inicio de sesión con cualquier + # contraseña (!!!). + result_success = continue + result_failure = return-fail + result_internalfail = return-fail + + username_filter = !*@* + } + + passdb { + driver = pam + args = dovecot2 + username_filter = !*@* + #TODO: algo como 'override_fields = allow_nets=...' + } + + userdb { + driver = passwd-file + args = username_format=%Ln ${vmailPath}/passwd + override_fields = uid=vmail gid=vmail home=${vmailPath}/home/%Ln + } + + userdb { + driver = passwd-file + args = ${localMailboxes} + + result_success = continue-ok + result_internalfail = return-fail + skip = found + } + + userdb { + driver = passwd + args = blocking=no + skip = notfound + } + ''; + }; fail2ban.jails.dovecot.settings = { filter = "dovecot[mode=aggressive]"; @@ -225,6 +223,8 @@ in acme.certs.${imapHostname} = { inherit (config.services.dovecot2) group; + + reloadServices = [ "dovecot2.service" ]; }; }; diff --git a/sys/mta/default.nix b/sys/mta/default.nix index 35508e6..64e08f3 100644 --- a/sys/mta/default.nix +++ b/sys/mta/default.nix @@ -11,6 +11,13 @@ with lib; let allDomains = optional (! virtualDomains ? ${domain}) domain ++ attrNames virtualDomains; virtualDomains = filterAttrs (name: _: name != domain) virtual; + cert = config.security.acme.certs.${mtaDomain.main}.directory; + + mtaDomain = + if isPrimary + then domains.smtp + else domains.smtp-backup; + mdaTransport = if isPrimary then "lmtp:inet:${cfg.mdaAddr}:${toString cfg.lmtpPort}" @@ -60,162 +67,153 @@ in ''; }; - postfix = - let - cert = config.security.acme.certs.${mtaDomain.main}.directory; - - mtaDomain = - if isPrimary - then domains.smtp - else domains.smtp-backup; - in - { - enable = true; - enableSmtp = true; - enableSubmissions = isPrimary; - - inherit domain; - hostname = mtaDomain.main; - - #TODO: check_recipient_access para rechazar localhost desde afuera - destination = optionals isPrimary [ "localhost" "$mydomain" ]; - origin = "$mydomain"; - - networksStyle = "host"; - - relayHost = optionalString isBackup domains.smtp.main; - lookupMX = false; - - relayDomains = - if isBackup - then allDomains - else null; - - sslKey = "${cert}/key.pem"; - sslCert = "${cert}/fullchain.pem"; - - # También es postmaster - rootAlias = config.local.sysadmin; - - extraAliases = optionalString isPrimary - (concatLines (flatten (mapAttrsToList - (name: user: map - (alias: "${alias}: ${name}") - user.hardAliases) - users))); - - localRecipients = optionals isPrimary - (map (user: "${user}@${domain}") - (attrNames (users // virtual.${domain}.users))); - - virtual = optionalString isPrimary - (concatLines (flatten (mapAttrsToList - (name: virtual: mapAttrsToList - (alias: targets: "${alias}@${name} ${concatStringsSep ", " targets}") - virtual.aliases) - virtual))); - - mapFiles = optionalAttrs isPrimary { - sender_ccerts = - pkgs.writeText "postfix-sender_ccerts" - (concatLines (flatten (mapAttrsToList - (username: user: map - (alias: "${alias}@${domain} CCERTS ${concatStringsSep "," (map (certPath: config.local.pki.byPath.${certPath}.fingerprint.sha256) user.mail.certs)}") - ([ username ] ++ user.hardAliases)) - (filterAttrs (_: user: user.mail.certs != [ ]) users)))); - - sender_login = - pkgs.writeText "postfix-sender_login" - (concatLines (flatten (mapAttrsToList - (username: user: map - (alias: "${alias}@${domain} ${username}") - ([ username ] ++ user.hardAliases)) - users))); - - virtual_recipients = - pkgs.writeText "postfix-virtual_recipients" - (concatLines (flatten (mapAttrsToList - (virtualDomain: virtual: mapAttrsToList - # El lado derecho de esta tabla debe existir pero nunca se usa - (username: _: "${username}@${virtualDomain} foo") - virtual.users) - virtualDomains))); - - virtual_rules = - pkgs.writeText "postfix-virtual_rules" - (concatLines (flatten (mapAttrsToList - (name: virtual: map - (rule: "/^${rule.pattern}@${name}$/ ${concatStringsSep ", " rule.targets}") - virtual.rules) - virtual))); - }; - - config = { - # user+extension@domain.tld - recipient_delimiter = optionalString isPrimary "+"; - - message_size_limit = toString (50 * 1048576); - - local_transport = mdaTransport; - virtual_transport = mdaTransport; - - smtpd_tls_auth_only = true; - # Nota: smtpd_tls_dh1024_param_file fue deprecado en 3.9 - - tls_append_default_CA = false; # Crítico - } // optionalAttrs isPrimary { - virtual_alias_maps = mkAfter [ "pcre:/etc/postfix/virtual_rules" ]; - virtual_mailbox_domains = attrNames virtualDomains; - virtual_mailbox_maps = [ "hash:/etc/postfix/virtual_recipients" ]; - - smtpd_sasl_type = "dovecot"; - smtpd_sasl_path = "inet:${cfg.mdaAddr}:${toString cfg.saslPort}"; - smtpd_sasl_local_domain = "$mydomain"; - smtpd_sasl_security_options = [ "noanonymous" ]; - - smtpd_tls_CAfile = "${config.local.pki.ca.mail.fullchain}"; - smtpd_tls_ccert_verifydepth = "1"; - - # Inventado, no es parámetro de postfix - local_submission_client_restrictions = [ - "permit_tls_all_clientcerts" - "permit_sasl_authenticated" - "reject" - ]; - - smtpd_sender_login_maps = [ "hash:/etc/postfix/sender_login" ]; - - smtpd_relay_restrictions = [ - "permit_mynetworks" - "permit_tls_all_clientcerts" - "permit_sasl_authenticated" - "reject_unauth_destination" - ]; - - smtpd_sender_restrictions = [ - "check_sender_access hash:/etc/postfix/sender_ccerts" - "reject_sender_login_mismatch" - ]; - - smtpd_milters = "unix:/run/opendkim/opendkim.sock"; - non_smtpd_milters = "$smtpd_milters"; - milter_default_action = "accept"; - } // optionalAttrs isBackup { - inet_interfaces = [ cfg.relayListen ]; - - smtpd_relay_restrictions = [ - "reject_unauth_destination" - ]; - }; - - # Importante: existe submissionOptions por aparte, no son iguales - submissionsOptions = optionalAttrs isPrimary { - smtpd_client_restrictions = "$local_submission_client_restrictions"; - smtpd_sasl_auth_enable = "yes"; - smtpd_tls_ask_ccert = "yes"; - smtpd_tls_security_level = "encrypt"; - }; + postfix = { + enable = true; + enableSmtp = true; + enableSubmissions = isPrimary; + + inherit domain; + hostname = mtaDomain.main; + + #TODO: check_recipient_access para rechazar localhost desde afuera + destination = optionals isPrimary [ "localhost" "$mydomain" ]; + origin = "$mydomain"; + + networksStyle = "host"; + + relayHost = optionalString isBackup domains.smtp.main; + lookupMX = false; + + relayDomains = + if isBackup + then allDomains + else null; + + sslKey = "${cert}/key.pem"; + sslCert = "${cert}/fullchain.pem"; + + # También es postmaster + rootAlias = config.local.sysadmin; + + extraAliases = optionalString isPrimary + (concatLines (flatten (mapAttrsToList + (name: user: map + (alias: "${alias}: ${name}") + user.hardAliases) + users))); + + localRecipients = optionals isPrimary + (map (user: "${user}@${domain}") + (attrNames (users // virtual.${domain}.users))); + + virtual = optionalString isPrimary + (concatLines (flatten (mapAttrsToList + (name: virtual: mapAttrsToList + (alias: targets: "${alias}@${name} ${concatStringsSep ", " targets}") + virtual.aliases) + virtual))); + + mapFiles = optionalAttrs isPrimary { + sender_ccerts = + pkgs.writeText "postfix-sender_ccerts" + (concatLines (flatten (mapAttrsToList + (username: user: map + (alias: "${alias}@${domain} CCERTS ${concatStringsSep "," (map (certPath: config.local.pki.byPath.${certPath}.fingerprint.sha256) user.mail.certs)}") + ([ username ] ++ user.hardAliases)) + (filterAttrs (_: user: user.mail.certs != [ ]) users)))); + + sender_login = + pkgs.writeText "postfix-sender_login" + (concatLines (flatten (mapAttrsToList + (username: user: map + (alias: "${alias}@${domain} ${username}") + ([ username ] ++ user.hardAliases)) + users))); + + virtual_recipients = + pkgs.writeText "postfix-virtual_recipients" + (concatLines (flatten (mapAttrsToList + (virtualDomain: virtual: mapAttrsToList + # El lado derecho de esta tabla debe existir pero nunca se usa + (username: _: "${username}@${virtualDomain} foo") + virtual.users) + virtualDomains))); + + virtual_rules = + pkgs.writeText "postfix-virtual_rules" + (concatLines (flatten (mapAttrsToList + (name: virtual: map + (rule: "/^${rule.pattern}@${name}$/ ${concatStringsSep ", " rule.targets}") + virtual.rules) + virtual))); + }; + + config = { + # user+extension@domain.tld + recipient_delimiter = optionalString isPrimary "+"; + + message_size_limit = toString (50 * 1048576); + + local_transport = mdaTransport; + virtual_transport = mdaTransport; + + smtpd_tls_auth_only = true; + # Nota: smtpd_tls_dh1024_param_file fue deprecado en 3.9 + + tls_append_default_CA = false; # Crítico + } // optionalAttrs isPrimary { + virtual_alias_maps = mkAfter [ "pcre:/etc/postfix/virtual_rules" ]; + virtual_mailbox_domains = attrNames virtualDomains; + virtual_mailbox_maps = [ "hash:/etc/postfix/virtual_recipients" ]; + + smtpd_sasl_type = "dovecot"; + smtpd_sasl_path = "inet:${cfg.mdaAddr}:${toString cfg.saslPort}"; + smtpd_sasl_local_domain = "$mydomain"; + smtpd_sasl_security_options = [ "noanonymous" ]; + + smtpd_tls_CAfile = "${config.local.pki.ca.mail.fullchain}"; + smtpd_tls_ccert_verifydepth = "1"; + + # Inventado, no es parámetro de postfix + local_submission_client_restrictions = [ + "permit_tls_all_clientcerts" + "permit_sasl_authenticated" + "reject" + ]; + + smtpd_sender_login_maps = [ "hash:/etc/postfix/sender_login" ]; + + smtpd_relay_restrictions = [ + "permit_mynetworks" + "permit_tls_all_clientcerts" + "permit_sasl_authenticated" + "reject_unauth_destination" + ]; + + smtpd_sender_restrictions = [ + "check_sender_access hash:/etc/postfix/sender_ccerts" + "reject_sender_login_mismatch" + ]; + + smtpd_milters = "unix:/run/opendkim/opendkim.sock"; + non_smtpd_milters = "$smtpd_milters"; + milter_default_action = "accept"; + } // optionalAttrs isBackup { + inet_interfaces = [ cfg.relayListen ]; + + smtpd_relay_restrictions = [ + "reject_unauth_destination" + ]; + }; + + # Importante: existe submissionOptions por aparte, no son iguales + submissionsOptions = optionalAttrs isPrimary { + smtpd_client_restrictions = "$local_submission_client_restrictions"; + smtpd_sasl_auth_enable = "yes"; + smtpd_tls_ask_ccert = "yes"; + smtpd_tls_security_level = "encrypt"; }; + }; }; #TODO: solo para las destination addresses necesarias @@ -231,5 +229,7 @@ in certs.smtp.enable = isPrimary; certs.smtp-backup.enable = isBackup; }; + + security.acme.certs.${mtaDomain.main}.reloadServices = [ "postfix.service" ]; }; } diff --git a/sys/web/nginx.nix b/sys/web/nginx.nix index e95bc3f..fc24afe 100644 --- a/sys/web/nginx.nix +++ b/sys/web/nginx.nix @@ -78,6 +78,7 @@ in name = domains.${name}.main; value = { group = mkDefault config.services.nginx.group; + reloadServices = [ "nginx.service" ]; }; }) cfg.ownedCerts); -- cgit v1.2.3