From e2a99f33eaf3dfd453111b41bb6c8be5103e92ab Mon Sep 17 00:00:00 2001 From: Martin Weinelt Date: Mon, 15 Dec 2025 16:00:13 +0100 Subject: [PATCH 1/6] docs: allow referencing module options --- scripts/generate-options.py | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/generate-options.py b/scripts/generate-options.py index 2e77297..ffc1168 100644 --- a/scripts/generate-options.py +++ b/scripts/generate-options.py @@ -11,6 +11,7 @@ header = """ """ template = """ +({key})= `````{{option}} {key} {description} From 18ee2a44edc8d01f58dea0a1d3d4727c733007f4 Mon Sep 17 00:00:00 2001 From: Martin Weinelt Date: Fri, 19 Dec 2025 02:17:32 +0100 Subject: [PATCH 2/6] docs: extract setup example into .nix file and include That way we get linting of the code for free. --- docs/setup-example.nix | 36 ++++++++++++++++++++++++++++++++++++ docs/setup-guide.rst | 38 ++------------------------------------ flake.nix | 1 + 3 files changed, 39 insertions(+), 36 deletions(-) create mode 100644 docs/setup-example.nix diff --git a/docs/setup-example.nix b/docs/setup-example.nix new file mode 100644 index 0000000..d73d49f --- /dev/null +++ b/docs/setup-example.nix @@ -0,0 +1,36 @@ +{ + imports = [ + (builtins.fetchTarball { + # Pick a release version you are interested in and set its hash, e.g. + url = "https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/archive/nixos-25.11/nixos-mailserver-nixos-25.11.tar.gz"; + # To get the sha256 of the nixos-mailserver tarball, we can use the nix-prefetch-url command: + # release="nixos-25.11"; nix-prefetch-url "https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/archive/${release}/nixos-mailserver-${release}.tar.gz" --unpack + sha256 = "0000000000000000000000000000000000000000000000000000"; + }) + ]; + + mailserver = { + enable = true; + stateVersion = 3; + fqdn = "mail.example.com"; + domains = [ "example.com" ]; + + # A list of all login accounts. To create the password hashes, use + # nix-shell -p mkpasswd --run 'mkpasswd -sm bcrypt' + loginAccounts = { + "user1@example.com" = { + hashedPasswordFile = "/a/file/containing/a/hashed/password"; + aliases = [ "postmaster@example.com" ]; + }; + "user2@example.com" = { + # ... + }; + }; + + # Use Let's Encrypt certificates. Note that this needs to set up a stripped + # down nginx and opens port 80. + certificateScheme = "acme-nginx"; + }; + security.acme.acceptTerms = true; + security.acme.defaults.email = "security@example.com"; +} diff --git a/docs/setup-guide.rst b/docs/setup-guide.rst index 70ff349..e6ef029 100644 --- a/docs/setup-guide.rst +++ b/docs/setup-guide.rst @@ -57,42 +57,8 @@ though there are more possible options (see the `NixOS Mailserver options documentation `_), these should be the most common ones. -.. code:: nix - - { config, pkgs, ... }: { - imports = [ - (builtins.fetchTarball { - # Pick a release version you are interested in and set its hash, e.g. - url = "https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/archive/nixos-25.11/nixos-mailserver-nixos-25.11.tar.gz"; - # To get the sha256 of the nixos-mailserver tarball, we can use the nix-prefetch-url command: - # release="nixos-25.11"; nix-prefetch-url "https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/archive/${release}/nixos-mailserver-${release}.tar.gz" --unpack - sha256 = "0000000000000000000000000000000000000000000000000000"; - }) - ]; - - mailserver = { - enable = true; - stateVersion = 3; - fqdn = "mail.example.com"; - domains = [ "example.com" ]; - - # A list of all login accounts. To create the password hashes, use - # nix-shell -p mkpasswd --run 'mkpasswd -sm bcrypt' - loginAccounts = { - "user1@example.com" = { - hashedPasswordFile = "/a/file/containing/a/hashed/password"; - aliases = ["postmaster@example.com"]; - }; - "user2@example.com" = { ... }; - }; - - # Use Let's Encrypt certificates. Note that this needs to set up a stripped - # down nginx and opens port 80. - certificateScheme = "acme-nginx"; - }; - security.acme.acceptTerms = true; - security.acme.defaults.email = "security@example.com"; - } +.. literalinclude:: ./setup-example.nix + :language: nix After a ``nixos-rebuild switch`` your server should be running all mail components. diff --git a/flake.nix b/flake.nix index 26e1670..2ed9577 100644 --- a/flake.nix +++ b/flake.nix @@ -112,6 +112,7 @@ "logo\\.png" "conf\\.py" "Makefile" + ".*\\.nix" ".*\\.rst" ]; buildInputs = [ From 33ba1ff52b3aa4568e5c6c90f8f99cef78f281b3 Mon Sep 17 00:00:00 2001 From: Martin Weinelt Date: Sun, 19 Oct 2025 23:20:00 +0200 Subject: [PATCH 3/6] Switch to NixOS ACME module for certificate management Drop most of the existing certificate handling, because we're effectively duplicating functionality that NixOS offers for free with better design, testing and maintainance than what we could provide downstream. The remaining two options are to reference an existing `security.acme.certs` configuration through `mailserver.x509.useACMEHost` or to provide existing key material via `mailserver.x509.certificateFile` and `mailserver.x509.privateKeyFile`. Support for automatic creation of self-signed certificates has been removed, because it is undesirable in public mail setups. The updated setup guide now displays the recommended configuration that relies on the NixOS ACME module, but requires further customization to select a suitable challenge. Co-Authored-By: Emily --- default.nix | 129 +++++++++++------------------------- docs/release-notes.rst | 10 +++ docs/setup-example.nix | 22 ++++-- mail-server/assertions.nix | 20 ++++-- mail-server/common.nix | 32 ++++----- mail-server/dovecot.nix | 14 +++- mail-server/environment.nix | 15 ++--- mail-server/networking.nix | 3 +- mail-server/nginx.nix | 59 ----------------- mail-server/postfix.nix | 15 ++++- mail-server/rspamd.nix | 1 + mail-server/systemd.nix | 45 ++----------- scripts/generate-options.py | 2 +- tests/external.nix | 5 +- tests/internal.nix | 1 + tests/lib/cert.pem | 11 +++ tests/lib/config.nix | 11 +++ tests/lib/key.pem | 5 ++ tests/multiple.nix | 5 +- 19 files changed, 166 insertions(+), 239 deletions(-) delete mode 100644 mail-server/nginx.nix create mode 100644 tests/lib/cert.pem create mode 100644 tests/lib/key.pem diff --git a/default.nix b/default.nix index 571abf7..60ba271 100644 --- a/default.nix +++ b/default.nix @@ -32,7 +32,6 @@ let mkRemovedOptionModule mkRenamedOptionModule types - warn ; cfg = config.mailserver; @@ -131,20 +130,6 @@ in description = "The domains that this mail server serves."; }; - certificateDomains = mkOption { - type = types.listOf types.str; - example = [ - "imap.example.com" - "pop3.example.com" - ]; - default = [ ]; - description = '' - ({option}`mailserver.certificateScheme` == `acme-nginx`) - - Secondary domains and subdomains for which it is necessary to generate a certificate. - ''; - }; - messageSizeLimit = mkOption { type = types.int; example = 52428800; @@ -788,91 +773,43 @@ in }; }; - certificateScheme = - let - schemes = [ - "manual" - "selfsigned" - "acme-nginx" - "acme" - ]; - translate = - i: - warn - "Setting mailserver.certificateScheme by number is deprecated, please use names instead: 'mailserver.certificateScheme = ${builtins.toString i}' can be replaced by 'mailserver.certificateScheme = \"${ - (builtins.elemAt schemes (i - 1)) - }\"'." - (builtins.elemAt schemes (i - 1)); - in - mkOption { - type = - with types; - coercedTo (enum [ - 1 - 2 - 3 - ]) translate (enum schemes); - default = "selfsigned"; + x509 = { + useACMEHost = mkOption { + type = with types; nullOr str; + default = null; + example = literalExpression "config.mailserver.fqdn"; description = '' - The scheme to use for managing TLS certificates: + Common name used in the relevant `security.acme.certs` configuration. - 1. `manual`: you specify locations via {option}`mailserver.certificateFile` and - {option}`mailserver.keyFile` and manually copy certificates there. - 2. `selfsigned`: you let the server create new (self-signed) certificates on the fly. - 3. `acme-nginx`: you let the server request certificates from [Let's Encrypt](https://letsencrypt.org) - via NixOS' ACME module. By default, this will set up a stripped-down Nginx server for - {option}`mailserver.fqdn` and open port 80. For this to work, the FQDN must be properly - configured to point to your server (see the [setup guide](setup-guide.rst) for more information). - 4. `acme`: you already have an ACME certificate set up (for example, you're already running a TLS-enabled - Nginx server on the FQDN). This is better than `manual` because the appropriate services will be reloaded - when the certificate is renewed. + Mutually exclusive with {option}`mailserver.x509.certificateFile` and {option}`mailserver.x509.privateKeyFile`. ''; }; - certificateFile = mkOption { - type = types.path; - example = "/root/mail-server.crt"; - description = '' - ({option}`mailserver.certificateScheme` == `manual`) + certificateFile = mkOption { + type = with types; nullOr path; + default = null; + example = "/var/keys/certs/fullchain.pem"; + description = '' + Path to the signed X509 certificate including intermediate certificates. - Location of the certificate. - ''; - }; + This is commonly referred to as {file}`fullchain.pem`. - keyFile = mkOption { - type = types.path; - example = "/root/mail-server.key"; - description = '' - ({option}`mailserver.certificateScheme` == `manual`) + Mutually exclusive with {option}`mailserver.x509.useACMEHost`. + ''; + }; - Location of the key file. - ''; - }; + privateKeyFile = mkOption { + type = with types; nullOr str; + default = null; + example = "/var/keys/certs/privkey.pem"; + description = '' + Path to the X509 private key. - certificateDirectory = mkOption { - type = types.path; - default = "/var/certs"; - description = '' - ({option}`mailserver.certificateScheme` == `selfsigned`) + This is commonly referred to as {file}`privkey.pem`. - This is the folder where the self-signed certificate will be created. The name is - hardcoded to "cert-DOMAIN.pem" and "key-DOMAIN.pem" and the - certificate is valid for 10 years. - ''; - }; - - acmeCertificateName = mkOption { - type = types.str; - default = cfg.fqdn; - defaultText = literalExpression "config.mailserver.fqdn"; - example = "example.com"; - description = '' - ({option}`mailserver.certificateScheme` == `acme`) - - When the `acme` `certificateScheme` is selected, you can use this option - to override the default certificate name. This is useful if you've - generated a wildcard certificate, for example. - ''; + Mutually exclusive with {option}`mailserver.x509.useACMEHost`. + ''; + }; }; enableImap = mkOption { @@ -1502,7 +1439,6 @@ in ./mail-server/dovecot.nix ./mail-server/postfix.nix ./mail-server/rspamd.nix - ./mail-server/nginx.nix ./mail-server/kresd.nix (mkRemovedOptionModule [ "mailserver" "policydSPFExtraConfig" ] '' SPF checking has been migrated to Rspamd, which makes this config redundant. Please look into the rspamd config to migrate your settings. @@ -1531,5 +1467,16 @@ in (mkRemovedOptionModule [ "mailserver" "dmarcReporting" "fromName" ] '' The name in the `FROM` field for DMARC report now uses the `mailserver.systemName`. '') + + (mkRemovedOptionModule [ "mailserver" "certificateDomains" ] '' + Configure `security.acme.certs.''${config.mailserver.fqdn}.extraDomains` instead. + '') + (mkRemovedOptionModule [ "mailserver" "certificateScheme" ] "") + (mkRemovedOptionModule [ "mailserver" "certificateDirectory" ] '' + Automatic creation of self-signed certificates is no longer supported. + '') + (mkRenamedOptionModule [ "mailserver" "acmeCertificateName" ] [ "mailserver" "x509" "useACMEHost" ]) + (mkRenamedOptionModule [ "mailserver" "certificateFile" ] [ "mailserver" "x509" "certificateFile" ]) + (mkRenamedOptionModule [ "mailserver" "keyFile" ] [ "mailserver" "x509" "privateKeyFile" ]) ]; } diff --git a/docs/release-notes.rst b/docs/release-notes.rst index b41110d..36d2499 100644 --- a/docs/release-notes.rst +++ b/docs/release-notes.rst @@ -1,6 +1,16 @@ Release Notes ============= +NixOS 26.05 +----------- + +- Certificate handling was simplified. We recommend setting + :option:`mailserver.x509.useACMEHost` to a ``security.acme.certs`` + configuration. If that does not fit your requirements, configure certificate + and private key using :option:`mailserver.x509.certificateFile` and + :option:`mailserver.x509.privateKeyFile` instead. Support for automatic + creation of self-signed certificates has been removed. + NixOS 25.11 ----------- diff --git a/docs/setup-example.nix b/docs/setup-example.nix index d73d49f..8a4134a 100644 --- a/docs/setup-example.nix +++ b/docs/setup-example.nix @@ -1,3 +1,7 @@ +{ + config, + ... +}: { imports = [ (builtins.fetchTarball { @@ -9,12 +13,24 @@ }) ]; + security.acme = { + acceptTerms = true; + defaults.email = "security@example.com"; + certs.${config.mailserver.fqdn} = { + # Further setup required, check the manual: + # https://nixos.org/manual/nixos/stable/#module-security-acme + }; + }; + mailserver = { enable = true; stateVersion = 3; fqdn = "mail.example.com"; domains = [ "example.com" ]; + # reference an existing ACME configuration + x509.useACMEHost = config.mailserver.fqdn; + # A list of all login accounts. To create the password hashes, use # nix-shell -p mkpasswd --run 'mkpasswd -sm bcrypt' loginAccounts = { @@ -26,11 +42,5 @@ # ... }; }; - - # Use Let's Encrypt certificates. Note that this needs to set up a stripped - # down nginx and opens port 80. - certificateScheme = "acme-nginx"; }; - security.acme.acceptTerms = true; - security.acme.defaults.email = "security@example.com"; } diff --git a/mail-server/assertions.nix b/mail-server/assertions.nix index 1885af4..a5f5fe7 100644 --- a/mail-server/assertions.nix +++ b/mail-server/assertions.nix @@ -38,6 +38,20 @@ in assertion = config.mailserver.stateVersion != null; message = "The `mailserver.stateVersion` option is not set. Check https://nixos-mailserver.readthedocs.io/en/latest/migrations.html to determine the proper value to initialize it at."; } + { + assertion = + config.mailserver.x509.useACMEHost != null + -> config.mailserver.x509.certificateFile == null && config.mailserver.x509.privateKeyFile == null; + message = "Configuring an ACME certificate (`mailserver.x509.useACMEHost`) is not possible while also passing an existing certificate (`mailserver.x509.certificateFile`, `mailserver.x509.privateKeyFile`)."; + } + { + assertion = + config.mailserver.x509.useACMEHost != null + || ( + config.mailserver.x509.certificateFile != null && config.mailserver.x509.privateKeyFile != null + ); + message = "Configure either an ACME certificate (`mailserver.x509.useACMEHost`) or pass an existing certificate (`mailserver.x509.certificateFile`, `mailserver.x509.privateKeyFile`)."; + } ] ++ lib.optionals config.mailserver.ldap.enable [ { @@ -75,11 +89,5 @@ in ''; } ] - ++ lib.optionals (config.mailserver.certificateScheme != "acme") [ - { - assertion = config.mailserver.acmeCertificateName == config.mailserver.fqdn; - message = "When the certificate scheme is not 'acme' (mailserver.certificateScheme != \"acme\"), it is not possible to define mailserver.acmeCertificateName"; - } - ] ); } diff --git a/mail-server/common.nix b/mail-server/common.nix index f060908..04ab71d 100644 --- a/mail-server/common.nix +++ b/mail-server/common.nix @@ -24,28 +24,20 @@ let cfg = config.mailserver; in -{ - # cert :: PATH - certificatePath = - if cfg.certificateScheme == "manual" then - cfg.certificateFile - else if cfg.certificateScheme == "selfsigned" then - "${cfg.certificateDirectory}/cert-${cfg.fqdn}.pem" - else if cfg.certificateScheme == "acme" || cfg.certificateScheme == "acme-nginx" then - "${config.security.acme.certs.${cfg.acmeCertificateName}.directory}/fullchain.pem" - else - throw "unknown certificate scheme"; +rec { + withACME = cfg.x509.useACMEHost != null; - # key :: PATH - keyPath = - if cfg.certificateScheme == "manual" then - cfg.keyFile - else if cfg.certificateScheme == "selfsigned" then - "${cfg.certificateDirectory}/key-${cfg.fqdn}.pem" - else if cfg.certificateScheme == "acme" || cfg.certificateScheme == "acme-nginx" then - "${config.security.acme.certs.${cfg.acmeCertificateName}.directory}/key.pem" + x509CertificateFile = + if withACME then + "${config.security.acme.certs.${cfg.x509.useACMEHost}.directory}/fullchain.pem" else - throw "unknown certificate scheme"; + cfg.x509.certificateFile; + + x509PrivateKeyFile = + if withACME then + "${config.security.acme.certs.${cfg.x509.useACMEHost}.directory}/key.pem" + else + cfg.x509.privateKeyFile; passwordFiles = let diff --git a/mail-server/dovecot.nix b/mail-server/dovecot.nix index e1d927a..6248351 100644 --- a/mail-server/dovecot.nix +++ b/mail-server/dovecot.nix @@ -196,6 +196,12 @@ in multiple languages are present in the configuration. ''; + security.acme.certs = lib.mkIf withACME { + ${cfg.x509.useACMEHost} = { + reloadServices = [ "dovecot.service" ]; + }; + }; + # for sieve-test. Shelling it in on demand usually doesnt' work, as it reads # the global config and tries to open shared libraries configured in there, # which are usually not compatible. @@ -216,8 +222,8 @@ in mailGroup = cfg.vmailGroupName; mailUser = cfg.vmailUserName; mailLocation = dovecotMaildir; - sslServerCert = certificatePath; - sslServerKey = keyPath; + sslServerCert = x509CertificateFile; + sslServerKey = x509PrivateKeyFile; enableDHE = lib.mkDefault false; enableLmtp = true; mailPlugins.globally.enable = lib.optionals cfg.fullTextSearch.enable [ @@ -455,6 +461,10 @@ in ${genPasswdScript} '' + (lib.optionalString cfg.ldap.enable setPwdInLdapConfFile); + reloadTriggers = lib.mkIf (!withACME) [ + x509CertificateFile + x509PrivateKeyFile + ]; }; systemd.services.postfix.restartTriggers = [ diff --git a/mail-server/environment.nix b/mail-server/environment.nix index 462cb05..86a81df 100644 --- a/mail-server/environment.nix +++ b/mail-server/environment.nix @@ -26,14 +26,11 @@ let in { config = lib.mkIf cfg.enable { - environment.systemPackages = - with pkgs; - [ - dovecot - openssh - postfix - rspamd - ] - ++ (if cfg.certificateScheme == "selfsigned" then [ openssl ] else [ ]); + environment.systemPackages = with pkgs; [ + dovecot + openssh + postfix + rspamd + ]; }; } diff --git a/mail-server/networking.nix b/mail-server/networking.nix index a79aa37..2fc00b8 100644 --- a/mail-server/networking.nix +++ b/mail-server/networking.nix @@ -32,8 +32,7 @@ in ++ lib.optional cfg.enableImapSsl 993 ++ lib.optional cfg.enablePop3 110 ++ lib.optional cfg.enablePop3Ssl 995 - ++ lib.optional cfg.enableManageSieve 4190 - ++ lib.optional (cfg.certificateScheme == "acme-nginx") 80; + ++ lib.optional cfg.enableManageSieve 4190; }; }; } diff --git a/mail-server/nginx.nix b/mail-server/nginx.nix deleted file mode 100644 index ab1c28d..0000000 --- a/mail-server/nginx.nix +++ /dev/null @@ -1,59 +0,0 @@ -# nixos-mailserver: a simple mail server -# Copyright (C) 2016-2018 Robin Raymond -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see - -{ - config, - options, - pkgs, - lib, - ... -}: - -with (import ./common.nix { - inherit - config - options - lib - pkgs - ; -}); - -let - cfg = config.mailserver; -in -{ - config = - lib.mkIf (cfg.enable && (cfg.certificateScheme == "acme" || cfg.certificateScheme == "acme-nginx")) - { - services.nginx = lib.mkIf (cfg.certificateScheme == "acme-nginx") { - enable = true; - virtualHosts."${cfg.fqdn}" = { - serverName = cfg.fqdn; - serverAliases = cfg.certificateDomains; - forceSSL = true; - enableACME = true; - }; - }; - - security.acme.certs."${cfg.acmeCertificateName}" = { - extraDomainNames = lib.mkIf (cfg.certificateScheme == "acme") cfg.certificateDomains; - reloadServices = [ - "postfix.service" - "dovecot.service" - ]; - }; - }; -} diff --git a/mail-server/postfix.nix b/mail-server/postfix.nix index 2d0c56a..d6827ae 100644 --- a/mail-server/postfix.nix +++ b/mail-server/postfix.nix @@ -279,6 +279,17 @@ in }; }; + security.acme.certs = lib.mkIf withACME { + ${cfg.x509.useACMEHost} = { + reloadServices = [ "postfix.service" ]; + }; + }; + + systemd.services.postfix.reloadTriggers = lib.mkIf (!withACME) [ + x509CertificateFile + x509PrivateKeyFile + ]; + systemd.services.postfix-setup = lib.mkIf cfg.ldap.enable { preStart = '' ${appendPwdInVirtualMailboxMap} @@ -364,8 +375,8 @@ in # The X509 private key followed by the corresponding certificate smtpd_tls_chain_files = [ - "${keyPath}" - "${certificatePath}" + "${x509PrivateKeyFile}" + "${x509CertificateFile}" ]; # TLS for incoming mail is optional diff --git a/mail-server/rspamd.nix b/mail-server/rspamd.nix index ee48b28..b04aac0 100644 --- a/mail-server/rspamd.nix +++ b/mail-server/rspamd.nix @@ -40,6 +40,7 @@ let pkgs.writeShellScript "dkim-keygen-${domain}" '' if [ ! -f "${privateKey}" ] then + export PATH=${lib.makeBinPath [ pkgs.openssl ]} ${lib.getExe' pkgs.rspamd "rspamadm"} dkim_keygen \ --domain "${domain}" \ --selector "${cfg.dkimSelector}" \ diff --git a/mail-server/systemd.nix b/mail-server/systemd.nix index d50408a..208b8de 100644 --- a/mail-server/systemd.nix +++ b/mail-server/systemd.nix @@ -33,47 +33,16 @@ with (import ./common.nix { let cfg = config.mailserver; - certificatesDeps = - if cfg.certificateScheme == "manual" then - [ ] - else if cfg.certificateScheme == "selfsigned" then - [ "mailserver-selfsigned-certificate.service" ] - else - [ "acme-finished-${cfg.fqdn}.target" ]; - + certificateDeps = lib.optionals withACME [ + "acme-order-renew-${cfg.x509.useACMEHost}.service" + ]; in { config = lib.mkIf cfg.enable { - # Create self signed certificate - systemd.services.mailserver-selfsigned-certificate = - lib.mkIf (cfg.certificateScheme == "selfsigned") - { - after = [ "local-fs.target" ]; - script = '' - # Create certificates if they do not exist yet - dir="${cfg.certificateDirectory}" - fqdn="${cfg.fqdn}" - [[ $fqdn == /* ]] && fqdn=$(< "$fqdn") - key="$dir/key-${cfg.fqdn}.pem"; - cert="$dir/cert-${cfg.fqdn}.pem"; - if [[ ! -f $key || ! -f $cert ]]; then - mkdir -p "${cfg.certificateDirectory}" - (umask 077; "${pkgs.openssl}/bin/openssl" genrsa -out "$key" 2048) && - "${pkgs.openssl}/bin/openssl" req -new -key "$key" -x509 -subj "/CN=$fqdn" \ - -days 3650 -out "$cert" - fi - ''; - serviceConfig = { - Type = "oneshot"; - PrivateTmp = true; - }; - }; - - # Create maildir folder before dovecot startup systemd.services.dovecot = { - wants = certificatesDeps; - after = certificatesDeps; + wants = certificateDeps; + after = certificateDeps; preStart = let directories = lib.strings.escapeShellArgs ( @@ -93,12 +62,12 @@ in # Postfix requires dovecot lmtp socket, dovecot auth socket and certificate to work systemd.services.postfix = { - wants = certificatesDeps; + wants = certificateDeps; after = [ "dovecot.service" ] ++ lib.optional cfg.dkimSigning "rspamd.service" - ++ certificatesDeps; + ++ certificateDeps; requires = [ "dovecot.service" ] ++ lib.optional cfg.dkimSigning "rspamd.service"; }; }; diff --git a/scripts/generate-options.py b/scripts/generate-options.py index ffc1168..1fe948b 100644 --- a/scripts/generate-options.py +++ b/scripts/generate-options.py @@ -26,7 +26,7 @@ options = json.load(f) groups = [ "mailserver.loginAccounts", - "mailserver.certificate", + "mailserver.x509", "mailserver.dkim", "mailserver.srs", "mailserver.dmarcReporting", diff --git a/tests/external.nix b/tests/external.nix index 0f47acb..bdbb546 100644 --- a/tests/external.nix +++ b/tests/external.nix @@ -26,7 +26,10 @@ ./lib/config.nix ]; - environment.systemPackages = with pkgs; [ netcat ]; + environment.systemPackages = with pkgs; [ + netcat + openssl + ]; virtualisation.memorySize = 1024; diff --git a/tests/internal.nix b/tests/internal.nix index 29d0880..a23c152 100644 --- a/tests/internal.nix +++ b/tests/internal.nix @@ -113,6 +113,7 @@ in '' machine.start() machine.wait_for_unit("multi-user.target") + machine.wait_for_unit("dovecot.service") # Regression test for https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/issues/205 with subtest("mail forwarded can are locally kept"): diff --git a/tests/lib/cert.pem b/tests/lib/cert.pem new file mode 100644 index 0000000..ad6cd5c --- /dev/null +++ b/tests/lib/cert.pem @@ -0,0 +1,11 @@ +-----BEGIN CERTIFICATE----- +MIIBizCCATGgAwIBAgIUN4ncJfMVIQSSurMkdE73x4aefTMwCgYIKoZIzj0EAwIw +GzEZMBcGA1UEAwwQdGVzdC5sb2NhbGRvbWFpbjAeFw0yNTEwMTgyMTQ4MTNaFw0z +NTEwMTYyMTQ4MTNaMBsxGTAXBgNVBAMMEHRlc3QubG9jYWxkb21haW4wWTATBgcq +hkjOPQIBBggqhkjOPQMBBwNCAARCJUj4j7eC/7Xso3REUscqHlWPvW9zvl5I6TIy +zEXFsWxM0QxMuNW4oXE56UiCyJklcpk0JfQUGat+kKQqSUJyo1MwUTAdBgNVHQ4E +FgQUW3CnmBf3n/Y30vfj3ERsIQnXu9QwHwYDVR0jBBgwFoAUW3CnmBf3n/Y30vfj +3ERsIQnXu9QwDwYDVR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAgNIADBFAiEAhwAi +K4xdr8KxD5xRvvzShheh48i8X7NtBIQ3bd01Jx4CIG/kYTDK5nDZri7UYOMsgz2l +iWss56p2dGWTL7LrBHgM +-----END CERTIFICATE----- diff --git a/tests/lib/config.nix b/tests/lib/config.nix index 7a8a0b0..b4466b6 100644 --- a/tests/lib/config.nix +++ b/tests/lib/config.nix @@ -10,6 +10,17 @@ # Keep testing submission with explicit TLS mailserver.enableSubmission = true; + # Certificate created for testing purposes from RFC9500 private key + # https://datatracker.ietf.org/doc/rfc9500/ + # openssl req -x509 -new -key key.pem \ + # -subj "/CN=test.localdomain" \ + # -sha256 -days 3650 \ + # -out cert.pem + mailserver.x509 = { + certificateFile = "${./cert.pem}"; + privateKeyFile = "${./key.pem}"; + }; + # Enable second CPU core virtualisation.cores = lib.mkDefault 2; diff --git a/tests/lib/key.pem b/tests/lib/key.pem new file mode 100644 index 0000000..e9bbb11 --- /dev/null +++ b/tests/lib/key.pem @@ -0,0 +1,5 @@ +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIObLW92AqkWunJXowVR2Z5/+yVPBaFHnEedDk5WJxk/BoAoGCCqGSM49 +AwEHoUQDQgAEQiVI+I+3gv+17KN0RFLHKh5Vj71vc75eSOkyMsxFxbFsTNEMTLjV +uKFxOelIgsiZJXKZNCX0FBmrfpCkKklCcg== +-----END EC PRIVATE KEY----- diff --git a/tests/multiple.nix b/tests/multiple.nix index d222b82..94d2527 100644 --- a/tests/multiple.nix +++ b/tests/multiple.nix @@ -93,8 +93,9 @@ in testScript = '' start_all() - domain1.wait_for_unit("multi-user.target") - domain2.wait_for_unit("multi-user.target") + for domain in [domain1, domain2]: + domain.wait_for_unit("multi-user.target") + domain.wait_for_unit("dovecot.service") # TODO put this blocking into the systemd units? domain1.wait_until_succeeds( From ff9b046f0f9c8ca689b47f1d2ddf132ffdfb7c9c Mon Sep 17 00:00:00 2001 From: Martin Weinelt Date: Sun, 16 Nov 2025 18:27:29 +0100 Subject: [PATCH 4/6] Stop recommending bcrypt everywhere By passing no method to mkpasswd we make it select the strongest cipher that libxcrypt recommends. Replaces the example hashes with yescrypt hashes, which is the current default. --- default.nix | 12 ++++++------ docs/setup-example.nix | 2 +- tests/internal.nix | 2 +- tests/multiple.nix | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/default.nix b/default.nix index 60ba271..6b4cad4 100644 --- a/default.nix +++ b/default.nix @@ -152,12 +152,12 @@ in hashedPassword = mkOption { type = with types; nullOr str; default = null; - example = "$6$evQJs5CFQyPAW09S$Cn99Y8.QjZ2IBnSu4qf1vBxDRWkaIZWOtmu1Ddsm3.H3CFpeVc0JU4llIq8HQXgeatvYhh5O33eWG3TSpjzu6/"; + example = "$y$j9T$vfGrwkAaXCjCEWtVNMQck1$383uIXQmn2z0hnmVAA8kwFQmjNj78.nYbvWeyNLIaP1"; description = '' The user's hashed password. Use `mkpasswd` as follows ``` - nix-shell -p mkpasswd --run 'mkpasswd -sm bcrypt' + nix-shell -p mkpasswd --run 'mkpasswd -s' ``` Warning: this is stored in plaintext in the Nix store! @@ -173,7 +173,7 @@ in A file containing the user's hashed password. Use `mkpasswd` as follows ``` - nix-shell -p mkpasswd --run 'mkpasswd -sm bcrypt' + nix-shell -p mkpasswd --run 'mkpasswd -s' ``` ''; }; @@ -275,10 +275,10 @@ in ); example = { user1 = { - hashedPassword = "$6$evQJs5CFQyPAW09S$Cn99Y8.QjZ2IBnSu4qf1vBxDRWkaIZWOtmu1Ddsm3.H3CFpeVc0JU4llIq8HQXgeatvYhh5O33eWG3TSpjzu6/"; + hashedPassword = "$y$j9T$y6eZ1o.IvVNfdGMAsUEvh1$6K/llP52uw2iDh4iSwtAn54/JYy7FzCcoCHmjmx00H5"; }; user2 = { - hashedPassword = "$6$oE0ZNv2n7Vk9gOf$9xcZWCCLGdMflIfuA0vR1Q1Xblw6RZqPrP94mEit2/81/7AKj2bqUai5yPyWE.QYPyv6wLMHZvjw3Rlg7yTCD/"; + hashedPassword = "$y$j9T$hZ.ubq0M897Hw.znxnGG9.$14EJBoOwbwKeWt.W4vpnBPEBZC9mYz4fWI9kOCLoZf4"; }; }; description = '' @@ -287,7 +287,7 @@ in follows ``` - nix-shell -p mkpasswd --run 'mkpasswd -sm bcrypt' + nix-shell -p mkpasswd --run 'mkpasswd -s' ``` ''; default = { }; diff --git a/docs/setup-example.nix b/docs/setup-example.nix index 8a4134a..0a9fb53 100644 --- a/docs/setup-example.nix +++ b/docs/setup-example.nix @@ -32,7 +32,7 @@ x509.useACMEHost = config.mailserver.fqdn; # A list of all login accounts. To create the password hashes, use - # nix-shell -p mkpasswd --run 'mkpasswd -sm bcrypt' + # nix-shell -p mkpasswd --run 'mkpasswd -s' loginAccounts = { "user1@example.com" = { hashedPasswordFile = "/a/file/containing/a/hashed/password"; diff --git a/tests/internal.nix b/tests/internal.nix index a23c152..988d5c4 100644 --- a/tests/internal.nix +++ b/tests/internal.nix @@ -38,7 +38,7 @@ let inherit password; } '' - mkpasswd -sm bcrypt <<<"$password" > $out + mkpasswd -s <<<"$password" > $out ''; hashedPasswordFile = hashPassword "my-password"; diff --git a/tests/multiple.nix b/tests/multiple.nix index 94d2527..19b7aee 100644 --- a/tests/multiple.nix +++ b/tests/multiple.nix @@ -15,7 +15,7 @@ let inherit password; } '' - mkpasswd -sm bcrypt <<<"$password" > $out + mkpasswd -s <<<"$password" > $out ''; password = pkgs.writeText "password" "password"; From 4bbe0d7bab92d2421cbe6d5fa5c7870fde5146eb Mon Sep 17 00:00:00 2001 From: Martin Weinelt Date: Mon, 15 Dec 2025 16:01:50 +0100 Subject: [PATCH 5/6] Fix option reference in aliasesRegExp option --- default.nix | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/default.nix b/default.nix index 6b4cad4..9d0b542 100644 --- a/default.nix +++ b/default.nix @@ -197,7 +197,8 @@ in example = [ ''/^tom\..*@domain\.com$/'' ]; default = [ ]; description = '' - Same as {option}`mailserver.aliases` but using PCRE (Perl compatible regex). + Same as {option}`mailserver.loginAccounts..aliases` but + using PCRE (Perl compatible regex). ''; }; From e437760341730a46c0f4434fa2c3c5a9c6039092 Mon Sep 17 00:00:00 2001 From: Martin Weinelt Date: Fri, 19 Dec 2025 02:52:55 +0100 Subject: [PATCH 6/6] treewide: replace/remove dovecot2 service name The unit name is now dovecot.service. --- default.nix | 4 ++-- docs/migrations.rst | 6 +++--- mail-server/assertions.nix | 2 +- tests/clamav.nix | 4 ++-- tests/external.nix | 12 ++++++------ 5 files changed, 14 insertions(+), 14 deletions(-) diff --git a/default.nix b/default.nix index 9d0b542..030355f 100644 --- a/default.nix +++ b/default.nix @@ -1185,8 +1185,8 @@ in if failed port 25 protocol smtp for 5 cycles then restart check process dovecot with pidfile /var/run/dovecot2/master.pid - start program = "${pkgs.systemd}/bin/systemctl start dovecot2" - stop program = "${pkgs.systemd}/bin/systemctl stop dovecot2" + start program = "${pkgs.systemd}/bin/systemctl start dovecot" + stop program = "${pkgs.systemd}/bin/systemctl stop dovecot" if failed host ${cfg.fqdn} port 993 type tcpssl sslauto protocol imap for 5 cycles then restart check process rspamd with matching "rspamd: main process" diff --git a/docs/migrations.rst b/docs/migrations.rst index 6372687..9aff6db 100644 --- a/docs/migrations.rst +++ b/docs/migrations.rst @@ -45,11 +45,11 @@ For remediating this issue the following steps are required: wcurl https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/raw/master/migrations/nixos-mailserver-migration-03.py chmod +x nixos-mailserver-migration-03.py -2. Stop the ``dovecot2.service``. +2. Stop the ``dovecot.service``. .. code-block:: bash - systemctl stop dovecot2.service + systemctl stop dovecot.service 3. Create a backup or snapshot of your ``mailserver.mailDirectory``, so you can restore should anything go wrong. @@ -101,7 +101,7 @@ This migration is required if you both: For remediating this issue the following steps are required: -1. Stop ``dovecot2.service``. +1. Stop ``dovecot.service``. 2. Move ``/var/vmail/ldap`` below your ``mailserver.mailDirectory``. 3. Update the ``mailserver.stateVersion`` to ``2``. diff --git a/mail-server/assertions.nix b/mail-server/assertions.nix index a5f5fe7..7f9c001 100644 --- a/mail-server/assertions.nix +++ b/mail-server/assertions.nix @@ -71,7 +71,7 @@ in message = '' Issue: The dovecot homedir for LDAP users was previously not respecting `mailserver.mailDirectory`. Remediation: - - Stop the `dovecot2.service` + - Stop the `dovecot.service` - Move `/var/vmail/ldap` below your `mailserver.mailDirectory` - Increase the `stateVersion` to 2. diff --git a/tests/clamav.nix b/tests/clamav.nix index 209e91e..159e542 100644 --- a/tests/clamav.nix +++ b/tests/clamav.nix @@ -248,7 +248,7 @@ with subtest("no warnings or errors"): server.fail("journalctl -u postfix | grep -i error >&2") server.fail("journalctl -u postfix | grep -i warning >&2") - server.fail("journalctl -u dovecot2 | grep -i error >&2") - server.fail("journalctl -u dovecot2 | grep -i warning >&2") + server.fail("journalctl -u dovecot | grep -i error >&2") + server.fail("journalctl -u dovecot | grep -i warning >&2") ''; } diff --git a/tests/external.nix b/tests/external.nix index bdbb546..bf9e0a8 100644 --- a/tests/external.nix +++ b/tests/external.nix @@ -489,9 +489,9 @@ server.wait_until_fails('[ "$(postqueue -p)" != "Mail queue is empty" ]') client.succeed("imap-mark-spam >&2") - server.wait_until_succeeds("journalctl -u dovecot -u dovecot2 | grep -i rspamd-learn-spam.sh >&2") + server.wait_until_succeeds("journalctl -u dovecot | grep -i rspamd-learn-spam.sh >&2") client.succeed("imap-mark-ham >&2") - server.wait_until_succeeds("journalctl -u dovecot -u dovecot2 | grep -i rspamd-learn-ham.sh >&2") + server.wait_until_succeeds("journalctl -u dovecot | grep -i rspamd-learn-ham.sh >&2") with subtest("full text search and indexation"): # send 2 email from user2 to user1 @@ -509,9 +509,9 @@ # should fail because this folder is not indexed client.fail("search Junk a >&2") # check that search really goes through the indexer - server.succeed("journalctl -u dovecot -u dovecot2 | grep 'fts-flatcurve(INBOX): Query ' >&2") + server.succeed("journalctl -u dovecot | grep 'fts-flatcurve(INBOX): Query ' >&2") # check that Junk is not indexed - server.fail("journalctl -u dovecot -u dovecot2 | grep 'fts-flatcurve(JUNK): Indexing ' >&2") + server.fail("journalctl -u dovecot | grep 'fts-flatcurve(JUNK): Indexing ' >&2") with subtest("dmarc reporting"): server.systemctl("start rspamd-dmarc-reporter.service") @@ -519,10 +519,10 @@ with subtest("no warnings or errors"): server.fail("journalctl -u postfix | grep -i error >&2") server.fail("journalctl -u postfix | grep -i warning >&2") - server.fail("journalctl -u dovecot -u dovecot2 | grep -v 'imap-login: Debug: SSL error: Connection closed' | grep -i error >&2") + server.fail("journalctl -u dovecot | grep -v 'imap-login: Debug: SSL error: Connection closed' | grep -i error >&2") # harmless ? https://dovecot.org/pipermail/dovecot/2020-August/119575.html server.fail( - "journalctl -u dovecot -u dovecot2 | \ + "journalctl -u dovecot | \ grep -v 'Expunged message reappeared, giving a new UID' | \ grep -v 'Time moved forwards' | \ grep -i warning >&2"