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.
This commit is contained in:
Naïm Favier 2023-02-15 13:15:09 +01:00 committed by lewo
parent 42c5564791
commit a948c49ca7
7 changed files with 49 additions and 35 deletions

View file

@ -48,7 +48,11 @@ in
type = types.listOf types.str; type = types.listOf types.str;
example = [ "imap.example.com" "pop3.example.com" ]; example = [ "imap.example.com" "pop3.example.com" ];
default = []; 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 { messageSizeLimit = mkOption {
@ -448,19 +452,26 @@ in
}; };
}; };
certificateScheme = mkOption { certificateScheme = let
type = types.enum [ 1 2 3 ]; schemes = [ "manual" "selfsigned" "acme-nginx" "acme" ];
default = 2; 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 = '' 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. 1. `manual`: you specify locations via {option}`mailserver.certificateFile` and
2) You let the server create new (self signed) certificates on the fly. {option}`mailserver.keyFile` and manually copy certificates there.
3) You let the server create a certificate via `Let's Encrypt`. Note that 2. `selfsigned`: you let the server create new (self-signed) certificates on the fly.
this implies that a stripped down webserver has to be started. This also 3. `acme-nginx`: you let the server request certificates from [Let's Encrypt](https://letsencrypt.org)
implies that the FQDN must be set as an `A` record to point to the IP of via NixOS' ACME module. By default, this will set up a stripped-down Nginx server for
the server. In particular port 80 on the server will be opened. For details {option}`mailserver.fqdn` and open port 80. For this to work, the FQDN must be properly
on how to set up the domain records, see the guide in the readme. 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; type = types.path;
example = "/root/mail-server.crt"; example = "/root/mail-server.crt";
description = '' description = ''
Scheme 1) ({option}`mailserver.certificateScheme` == `manual`)
Location of the certificate
Location of the certificate.
''; '';
}; };
@ -477,8 +489,9 @@ in
type = types.path; type = types.path;
example = "/root/mail-server.key"; example = "/root/mail-server.key";
description = '' description = ''
Scheme 1) ({option}`mailserver.certificateScheme` == `manual`)
Location of the key file
Location of the key file.
''; '';
}; };
@ -486,8 +499,9 @@ in
type = types.path; type = types.path;
default = "/var/certs"; default = "/var/certs";
description = '' description = ''
Scheme 2) ({option}`mailserver.certificateScheme` == `selfsigned`)
This is the folder where the certificate will be created. The name is
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 hardcoded to "cert-DOMAIN.pem" and "key-DOMAIN.pem" and the
certificate is valid for 10 years. certificate is valid for 10 years.
''; '';

View file

@ -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 # Use Let's Encrypt certificates. Note that this needs to set up a stripped
# down nginx and opens port 80. # down nginx and opens port 80.
certificateScheme = 3; certificateScheme = "acme-nginx";
}; };
} }

View file

@ -21,22 +21,22 @@ let
in in
{ {
# cert :: PATH # cert :: PATH
certificatePath = if cfg.certificateScheme == 1 certificatePath = if cfg.certificateScheme == "manual"
then cfg.certificateFile then cfg.certificateFile
else if cfg.certificateScheme == 2 else if cfg.certificateScheme == "selfsigned"
then "${cfg.certificateDirectory}/cert-${cfg.fqdn}.pem" 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" 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 # key :: PATH
keyPath = if cfg.certificateScheme == 1 keyPath = if cfg.certificateScheme == "manual"
then cfg.keyFile then cfg.keyFile
else if cfg.certificateScheme == 2 else if cfg.certificateScheme == "selfsigned"
then "${cfg.certificateDirectory}/key-${cfg.fqdn}.pem" 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" 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 passwordFiles = let
mkHashFile = name: hash: pkgs.writeText "${builtins.hashString "sha256" name}-password-hash" hash; mkHashFile = name: hash: pkgs.writeText "${builtins.hashString "sha256" name}-password-hash" hash;

View file

@ -23,6 +23,6 @@ in
config = with cfg; lib.mkIf enable { config = with cfg; lib.mkIf enable {
environment.systemPackages = with pkgs; [ environment.systemPackages = with pkgs; [
dovecot opendkim openssh postfix rspamd dovecot opendkim openssh postfix rspamd
] ++ (if certificateScheme == 2 then [ openssl ] else []); ] ++ (if certificateScheme == "selfsigned" then [ openssl ] else []);
}; };
} }

View file

@ -14,7 +14,7 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/> # along with this program. If not, see <http://www.gnu.org/licenses/>
{ config, pkgs, lib, ... }: { config, lib, ... }:
let let
cfg = config.mailserver; cfg = config.mailserver;
@ -31,7 +31,7 @@ in
++ lib.optional enablePop3 110 ++ lib.optional enablePop3 110
++ lib.optional enablePop3Ssl 995 ++ lib.optional enablePop3Ssl 995
++ lib.optional enableManageSieve 4190 ++ lib.optional enableManageSieve 4190
++ lib.optional (certificateScheme == 3) 80; ++ lib.optional (certificateScheme == "acme-nginx") 80;
}; };
}; };
} }

View file

@ -24,8 +24,8 @@ let
acmeRoot = "/var/lib/acme/acme-challenge"; acmeRoot = "/var/lib/acme/acme-challenge";
in in
{ {
config = lib.mkIf (cfg.enable && cfg.certificateScheme == 3) { config = lib.mkIf (cfg.enable && (cfg.certificateScheme == "acme" || cfg.certificateScheme == "acme-nginx")) {
services.nginx = { services.nginx = lib.mkIf (cfg.certificateScheme == "acme-nginx") {
enable = true; enable = true;
virtualHosts."${cfg.fqdn}" = { virtualHosts."${cfg.fqdn}" = {
serverName = cfg.fqdn; serverName = cfg.fqdn;

View file

@ -19,9 +19,9 @@
let let
cfg = config.mailserver; cfg = config.mailserver;
certificatesDeps = 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" ] [ "mailserver-selfsigned-certificate.service" ]
else else
[ "acme-finished-${cfg.fqdn}.target" ]; [ "acme-finished-${cfg.fqdn}.target" ];
@ -29,7 +29,7 @@ in
{ {
config = with cfg; lib.mkIf enable { config = with cfg; lib.mkIf enable {
# Create self signed certificate # 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" ]; after = [ "local-fs.target" ];
script = '' script = ''
# Create certificates if they do not exist yet # Create certificates if they do not exist yet