summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAlejandro Soto <alejandro@34project.org>2025-04-06 14:27:57 -0600
committerAlejandro Soto <alejandro@34project.org>2025-04-06 14:27:57 -0600
commit5b01285ee33516cec607cbd069e06b4b8970c088 (patch)
treea6befbaca9bfee6163579d235ae589cb29313269
parent2585c96ff9b7945cab054e38fbbcaf760e3be38c (diff)
sys: reload services after certificate renewal
Diffstat (limited to '')
-rw-r--r--sys/mail/default.nix360
-rw-r--r--sys/mta/default.nix310
-rw-r--r--sys/web/nginx.nix1
3 files changed, 336 insertions, 335 deletions
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);