From a948c49ca7e484636f618cf3933f193092370906 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Na=C3=AFm=20Favier?= Date: Wed, 15 Feb 2023 13:15:09 +0100 Subject: [PATCH] Allow using existing ACME certificates Add a certificate scheme for using an existing ACME certificate without setting up Nginx. Also use names instead of magic numbers for certificate schemes. --- default.nix | 50 ++++++++++++++++++++++++------------- docs/setup-guide.rst | 2 +- mail-server/common.nix | 16 ++++++------ mail-server/environment.nix | 2 +- mail-server/networking.nix | 4 +-- mail-server/nginx.nix | 4 +-- mail-server/systemd.nix | 6 ++--- 7 files changed, 49 insertions(+), 35 deletions(-) diff --git a/default.nix b/default.nix index ef27f43..49103f3 100644 --- a/default.nix +++ b/default.nix @@ -48,7 +48,11 @@ in type = types.listOf types.str; example = [ "imap.example.com" "pop3.example.com" ]; default = []; - description = "Secondary domains and subdomains for which it is necessary to generate a certificate."; + description = '' + ({option}`mailserver.certificateScheme` == `acme-nginx`) + + Secondary domains and subdomains for which it is necessary to generate a certificate. + ''; }; messageSizeLimit = mkOption { @@ -448,19 +452,26 @@ in }; }; - certificateScheme = mkOption { - type = types.enum [ 1 2 3 ]; - default = 2; + certificateScheme = let + schemes = [ "manual" "selfsigned" "acme-nginx" "acme" ]; + translate = i: warn "setting mailserver.certificateScheme by number is deprecated, please use names instead" + (builtins.elemAt schemes (i - 1)); + in mkOption { + type = with types; coercedTo (enum [ 1 2 3 ]) translate (enum schemes); + default = "selfsigned"; description = '' - Certificate Files. There are three options for these. + The scheme to use for managing TLS certificates: - 1) You specify locations and manually copy certificates there. - 2) You let the server create new (self signed) certificates on the fly. - 3) You let the server create a certificate via `Let's Encrypt`. Note that - this implies that a stripped down webserver has to be started. This also - implies that the FQDN must be set as an `A` record to point to the IP of - the server. In particular port 80 on the server will be opened. For details - on how to set up the domain records, see the guide in the readme. + 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. ''; }; @@ -468,8 +479,9 @@ in type = types.path; example = "/root/mail-server.crt"; description = '' - Scheme 1) - Location of the certificate + ({option}`mailserver.certificateScheme` == `manual`) + + Location of the certificate. ''; }; @@ -477,8 +489,9 @@ in type = types.path; example = "/root/mail-server.key"; description = '' - Scheme 1) - Location of the key file + ({option}`mailserver.certificateScheme` == `manual`) + + Location of the key file. ''; }; @@ -486,8 +499,9 @@ in type = types.path; default = "/var/certs"; description = '' - Scheme 2) - This is the folder where the certificate will be created. The name is + ({option}`mailserver.certificateScheme` == `selfsigned`) + + 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. ''; diff --git a/docs/setup-guide.rst b/docs/setup-guide.rst index 8127734..c74a53d 100644 --- a/docs/setup-guide.rst +++ b/docs/setup-guide.rst @@ -81,7 +81,7 @@ these should be the most common ones. # Use Let's Encrypt certificates. Note that this needs to set up a stripped # down nginx and opens port 80. - certificateScheme = 3; + certificateScheme = "acme-nginx"; }; } diff --git a/mail-server/common.nix b/mail-server/common.nix index 2a264a7..e8beb7a 100644 --- a/mail-server/common.nix +++ b/mail-server/common.nix @@ -21,22 +21,22 @@ let in { # cert :: PATH - certificatePath = if cfg.certificateScheme == 1 + certificatePath = if cfg.certificateScheme == "manual" then cfg.certificateFile - else if cfg.certificateScheme == 2 + else if cfg.certificateScheme == "selfsigned" then "${cfg.certificateDirectory}/cert-${cfg.fqdn}.pem" - else if cfg.certificateScheme == 3 + else if cfg.certificateScheme == "acme" || cfg.certificateScheme == "acme-nginx" then "${config.security.acme.certs.${cfg.fqdn}.directory}/fullchain.pem" - else throw "Error: Certificate Scheme must be in { 1, 2, 3 }"; + else throw "unknown certificate scheme"; # key :: PATH - keyPath = if cfg.certificateScheme == 1 + keyPath = if cfg.certificateScheme == "manual" then cfg.keyFile - else if cfg.certificateScheme == 2 + else if cfg.certificateScheme == "selfsigned" then "${cfg.certificateDirectory}/key-${cfg.fqdn}.pem" - else if cfg.certificateScheme == 3 + else if cfg.certificateScheme == "acme" || cfg.certificateScheme == "acme-nginx" then "${config.security.acme.certs.${cfg.fqdn}.directory}/key.pem" - else throw "Error: Certificate Scheme must be in { 1, 2, 3 }"; + else throw "unknown certificate scheme"; passwordFiles = let mkHashFile = name: hash: pkgs.writeText "${builtins.hashString "sha256" name}-password-hash" hash; diff --git a/mail-server/environment.nix b/mail-server/environment.nix index cc85202..e509ea6 100644 --- a/mail-server/environment.nix +++ b/mail-server/environment.nix @@ -23,6 +23,6 @@ in config = with cfg; lib.mkIf enable { environment.systemPackages = with pkgs; [ dovecot opendkim openssh postfix rspamd - ] ++ (if certificateScheme == 2 then [ openssl ] else []); + ] ++ (if certificateScheme == "selfsigned" then [ openssl ] else []); }; } diff --git a/mail-server/networking.nix b/mail-server/networking.nix index e8a222e..6af186a 100644 --- a/mail-server/networking.nix +++ b/mail-server/networking.nix @@ -14,7 +14,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see -{ config, pkgs, lib, ... }: +{ config, lib, ... }: let cfg = config.mailserver; @@ -31,7 +31,7 @@ in ++ lib.optional enablePop3 110 ++ lib.optional enablePop3Ssl 995 ++ lib.optional enableManageSieve 4190 - ++ lib.optional (certificateScheme == 3) 80; + ++ lib.optional (certificateScheme == "acme-nginx") 80; }; }; } diff --git a/mail-server/nginx.nix b/mail-server/nginx.nix index abc6421..e5fa597 100644 --- a/mail-server/nginx.nix +++ b/mail-server/nginx.nix @@ -24,8 +24,8 @@ let acmeRoot = "/var/lib/acme/acme-challenge"; in { - config = lib.mkIf (cfg.enable && cfg.certificateScheme == 3) { - services.nginx = { + 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; diff --git a/mail-server/systemd.nix b/mail-server/systemd.nix index 36e48d6..0fdcf90 100644 --- a/mail-server/systemd.nix +++ b/mail-server/systemd.nix @@ -19,9 +19,9 @@ let cfg = config.mailserver; certificatesDeps = - if cfg.certificateScheme == 1 then + if cfg.certificateScheme == "manual" then [] - else if cfg.certificateScheme == 2 then + else if cfg.certificateScheme == "selfsigned" then [ "mailserver-selfsigned-certificate.service" ] else [ "acme-finished-${cfg.fqdn}.target" ]; @@ -29,7 +29,7 @@ in { config = with cfg; lib.mkIf enable { # Create self signed certificate - systemd.services.mailserver-selfsigned-certificate = lib.mkIf (cfg.certificateScheme == 2) { + systemd.services.mailserver-selfsigned-certificate = lib.mkIf (cfg.certificateScheme == "selfsigned") { after = [ "local-fs.target" ]; script = '' # Create certificates if they do not exist yet