diff --git a/nixos/doc/manual/release-notes/rl-1909.xml b/nixos/doc/manual/release-notes/rl-1909.xml
index 36bea28530b..60f756b78c6 100644
--- a/nixos/doc/manual/release-notes/rl-1909.xml
+++ b/nixos/doc/manual/release-notes/rl-1909.xml
@@ -318,7 +318,28 @@
services.strongswan-swanctl
services.httpd
-
+
+
+
+ The option has been replaced by a read-only option for each certificate you define. This will be
+ a subdirectory of /var/lib/acme. You can use this read-only option to figure out where the certificates are stored for a specific certificate. For example,
+ the option will use this directory option to find the certs for the virtual host.
+
+
+ and options have been removed. To execute a service before certificates
+ are provisioned or renewed add a RequiredBy=acme-${cert}.service to any service.
+
+
+ Furthermore, the acme module will not automatically add a dependency on lighttpd.service anymore. If you are using certficates provided by letsencrypt
+ for lighttpd, then you should depend on the certificate service acme-${cert}.service> manually.
+
+
+ For nginx, the dependencies are still automatically managed when is enabled just like before. What changed is that nginx now directly depends on the specific certificates that it needs,
+ instead of depending on the catch-all acme-certificates.target. This target unit was also removed from the codebase.
+ This will mean nginx will no longer depend on certificates it isn't explicitly managing and fixes a bug with certificate renewal
+ ordering racing with nginx restarting which could lead to nginx getting in a broken state as described at
+ NixOS/nixpkgs#60180.
+
diff --git a/nixos/modules/rename.nix b/nixos/modules/rename.nix
index 348ad094e5a..1048c2af2ea 100644
--- a/nixos/modules/rename.nix
+++ b/nixos/modules/rename.nix
@@ -256,6 +256,11 @@ with lib;
# binfmt
(mkRenamedOptionModule [ "boot" "binfmtMiscRegistrations" ] [ "boot" "binfmt" "registrations" ])
+
+ # ACME
+ (mkRemovedOptionModule [ "security" "acme" "directory"] "ACME Directory is now hardcoded to /var/lib/acme and its permisisons are managed by systemd. See https://github.com/NixOS/nixpkgs/issues/53852 for more info.")
+ (mkRemovedOptionModule [ "security" "acme" "preDelay"] "This option has been removed. If you want to make sure that something executes before certificates are provisioned, add a RequiredBy=acme-\${cert}.service to the service you want to execute before the cert renewal")
+ (mkRemovedOptionModule [ "security" "acme" "activationDelay"] "This option has been removed. If you want to make sure that something executes before certificates are provisioned, add a RequiredBy=acme-\${cert}.service to the service you want to execute before the cert renewal")
# KSM
(mkRenamedOptionModule [ "hardware" "enableKSM" ] [ "hardware" "ksm" "enable" ])
diff --git a/nixos/modules/security/acme.nix b/nixos/modules/security/acme.nix
index 092704c6fc3..feb54affbf8 100644
--- a/nixos/modules/security/acme.nix
+++ b/nixos/modules/security/acme.nix
@@ -80,25 +80,11 @@ let
'';
};
- activationDelay = mkOption {
- type = types.nullOr types.str;
- default = null;
- description = ''
- Systemd time span expression to delay copying new certificates to main
- state directory. See systemd.time
- 7.
- '';
- };
-
- preDelay = mkOption {
- type = types.lines;
- default = "";
- description = ''
- Commands to run after certificates are re-issued but before they are
- activated. Typically the new certificate is published to DNS.
-
- Executed in the same directory with the new certificate.
- '';
+ directory = mkOption {
+ type = types.str;
+ readOnly = true;
+ default = "/var/lib/acme/${name}";
+ description = "Directory where certificate and other state is stored.";
};
extraDomains = mkOption {
@@ -126,13 +112,6 @@ in
options = {
security.acme = {
- directory = mkOption {
- default = "/var/lib/acme";
- type = types.str;
- description = ''
- Directory where certs and other state will be stored by default.
- '';
- };
validMin = mkOption {
type = types.int;
@@ -181,7 +160,11 @@ in
default = { };
type = with types; attrsOf (submodule certOpts);
description = ''
- Attribute set of certificates to get signed and renewed.
+ Attribute set of certificates to get signed and renewed. Creates
+ acme-''${cert}.{service,timer} systemd units for
+ each certificate defined here. Other services can add dependencies
+ to those units if they rely on the certificates being present,
+ or trigger restarts of the service if certificates get renewed.
'';
example = literalExample ''
{
@@ -209,8 +192,7 @@ in
servicesLists = mapAttrsToList certToServices cfg.certs;
certToServices = cert: data:
let
- cpath = lpath + optionalString (data.activationDelay != null) ".staging";
- lpath = "${cfg.directory}/${cert}";
+ lpath = "acme/${cert}";
rights = if data.allowKeysForGroup then "750" else "700";
cmdline = [ "-v" "-d" data.domain "--default_root" data.webroot "--valid_min" cfg.validMin ]
++ optionals (data.email != null) [ "--email" data.email ]
@@ -224,79 +206,27 @@ in
serviceConfig = {
Type = "oneshot";
SuccessExitStatus = [ "0" "1" ];
- PermissionsStartOnly = true;
User = data.user;
Group = data.group;
PrivateTmp = true;
+ StateDirectory = lpath;
+ StateDirectoryMode = rights;
+ WorkingDirectory = "/var/lib/${lpath}";
+ ExecStart = "${pkgs.simp_le}/bin/simp_le ${escapeShellArgs cmdline}";
+ ExecStopPost =
+ let
+ script = pkgs.writeScript "acme-post-stop" ''
+ #!${pkgs.runtimeShell} -e
+ ${data.postRun}
+ '';
+ in
+ "+${script}";
};
- path = with pkgs; [ simp_le systemd ];
- preStart = ''
- mkdir -p '${cfg.directory}'
- chown 'root:root' '${cfg.directory}'
- chmod 755 '${cfg.directory}'
- if [ ! -d '${cpath}' ]; then
- mkdir '${cpath}'
- fi
- chmod ${rights} '${cpath}'
- chown -R '${data.user}:${data.group}' '${cpath}'
- mkdir -p '${data.webroot}/.well-known/acme-challenge'
- chown -R '${data.user}:${data.group}' '${data.webroot}/.well-known/acme-challenge'
- '';
- script = ''
- cd '${cpath}'
- set +e
- simp_le ${escapeShellArgs cmdline}
- EXITCODE=$?
- set -e
- echo "$EXITCODE" > /tmp/lastExitCode
- exit "$EXITCODE"
- '';
- postStop = ''
- cd '${cpath}'
- if [ -e /tmp/lastExitCode ] && [ "$(cat /tmp/lastExitCode)" = "0" ]; then
- ${if data.activationDelay != null then ''
-
- ${data.preDelay}
-
- if [ -d '${lpath}' ]; then
- systemd-run --no-block --on-active='${data.activationDelay}' --unit acme-setlive-${cert}.service
- else
- systemctl --wait start acme-setlive-${cert}.service
- fi
- '' else data.postRun}
-
- # noop ensuring that the "if" block is non-empty even if
- # activationDelay == null and postRun == ""
- true
- fi
- '';
-
- before = [ "acme-certificates.target" ];
- wantedBy = [ "acme-certificates.target" ];
- };
- delayService = {
- description = "Set certificate for ${cert} live";
- path = with pkgs; [ rsync ];
- serviceConfig = {
- Type = "oneshot";
- };
- script = ''
- rsync -a --delete-after '${cpath}/' '${lpath}'
- '';
- postStop = data.postRun;
};
selfsignedService = {
description = "Create preliminary self-signed certificate for ${cert}";
path = [ pkgs.openssl ];
- preStart = ''
- if [ ! -d '${cpath}' ]
- then
- mkdir -p '${cpath}'
- chmod ${rights} '${cpath}'
- chown '${data.user}:${data.group}' '${cpath}'
- fi
- '';
script =
''
workdir="$(mktemp -d)"
@@ -318,50 +248,41 @@ in
-out $workdir/server.crt
# Copy key to destination
- cp $workdir/server.key ${cpath}/key.pem
+ cp $workdir/server.key /var/lib/${lpath}/key.pem
# Create fullchain.pem (same format as "simp_le ... -f fullchain.pem" creates)
- cat $workdir/{server.crt,ca.crt} > "${cpath}/fullchain.pem"
+ cat $workdir/{server.crt,ca.crt} > "/var/lib/${lpath}/fullchain.pem"
# Create full.pem for e.g. lighttpd
- cat $workdir/{server.key,server.crt,ca.crt} > "${cpath}/full.pem"
+ cat $workdir/{server.key,server.crt,ca.crt} > "/var/lib/${lpath}/full.pem"
# Give key acme permissions
- chown '${data.user}:${data.group}' "${cpath}/"{key,fullchain,full}.pem
- chmod ${rights} "${cpath}/"{key,fullchain,full}.pem
+ chown '${data.user}:${data.group}' "/var/lib/${lpath}/"{key,fullchain,full}.pem
+ chmod ${rights} "/var/lib/${lpath}/"{key,fullchain,full}.pem
'';
serviceConfig = {
Type = "oneshot";
- PermissionsStartOnly = true;
PrivateTmp = true;
+ StateDirectory = lpath;
User = data.user;
Group = data.group;
};
unitConfig = {
# Do not create self-signed key when key already exists
- ConditionPathExists = "!${cpath}/key.pem";
+ ConditionPathExists = "!/var/lib/${lpath}/key.pem";
};
- before = [
- "acme-selfsigned-certificates.target"
- ];
- wantedBy = [
- "acme-selfsigned-certificates.target"
- ];
};
in (
[ { name = "acme-${cert}"; value = acmeService; } ]
++ optional cfg.preliminarySelfsigned { name = "acme-selfsigned-${cert}"; value = selfsignedService; }
- ++ optional (data.activationDelay != null) { name = "acme-setlive-${cert}"; value = delayService; }
);
servicesAttr = listToAttrs services;
- injectServiceDep = {
- after = [ "acme-selfsigned-certificates.target" ];
- wants = [ "acme-selfsigned-certificates.target" "acme-certificates.target" ];
- };
in
- servicesAttr //
- (if config.services.nginx.enable then { nginx = injectServiceDep; } else {}) //
- (if config.services.lighttpd.enable then { lighttpd = injectServiceDep; } else {});
+ servicesAttr;
+
+ systemd.tmpfiles.rules =
+ flip mapAttrsToList cfg.certs
+ (cert: data: "d ${data.webroot}/.well-known/acme-challenge - ${data.user} ${data.group}");
systemd.timers = flip mapAttrs' cfg.certs (cert: data: nameValuePair
("acme-${cert}")
@@ -377,9 +298,6 @@ in
};
})
);
-
- systemd.targets."acme-selfsigned-certificates" = mkIf cfg.preliminarySelfsigned {};
- systemd.targets."acme-certificates" = {};
})
];
diff --git a/nixos/modules/security/acme.xml b/nixos/modules/security/acme.xml
index ef71fe53d0c..9d0a1995e0f 100644
--- a/nixos/modules/security/acme.xml
+++ b/nixos/modules/security/acme.xml
@@ -59,10 +59,8 @@ http {
The private key key.pem and certificate
fullchain.pem will be put into
- /var/lib/acme/foo.example.com. The target directory can
- be configured with the option .
+ /var/lib/acme/foo.example.com.
-
Refer to for all available configuration
options for the security.acme
diff --git a/nixos/modules/services/web-servers/nginx/default.nix b/nixos/modules/services/web-servers/nginx/default.nix
index c1a51fbf8b4..5c65a2388d6 100644
--- a/nixos/modules/services/web-servers/nginx/default.nix
+++ b/nixos/modules/services/web-servers/nginx/default.nix
@@ -4,23 +4,25 @@ with lib;
let
cfg = config.services.nginx;
+ certs = config.security.acme.certs;
+ vhostsConfigs = mapAttrsToList (vhostName: vhostConfig: vhostConfig) virtualHosts;
+ acmeEnabledVhosts = filter (vhostConfig: vhostConfig.enableACME && vhostConfig.useACMEHost == null) vhostsConfigs;
virtualHosts = mapAttrs (vhostName: vhostConfig:
let
serverName = if vhostConfig.serverName != null
then vhostConfig.serverName
else vhostName;
- acmeDirectory = config.security.acme.directory;
in
vhostConfig // {
inherit serverName;
} // (optionalAttrs vhostConfig.enableACME {
- sslCertificate = "${acmeDirectory}/${serverName}/fullchain.pem";
- sslCertificateKey = "${acmeDirectory}/${serverName}/key.pem";
- sslTrustedCertificate = "${acmeDirectory}/${serverName}/fullchain.pem";
+ sslCertificate = "${certs.${serverName}.directory}/fullchain.pem";
+ sslCertificateKey = "${certs.${serverName}.directory}/key.pem";
+ sslTrustedCertificate = "${certs.${serverName}.directory}/full.pem";
}) // (optionalAttrs (vhostConfig.useACMEHost != null) {
- sslCertificate = "${acmeDirectory}/${vhostConfig.useACMEHost}/fullchain.pem";
- sslCertificateKey = "${acmeDirectory}/${vhostConfig.useACMEHost}/key.pem";
- sslTrustedCertificate = "${acmeDirectory}/${vhostConfig.useACMEHost}/fullchain.pem";
+ sslCertificate = "${certs.${vhostConfig.useACMEHost}.directory}/fullchain.pem";
+ sslCertificateKey = "${certs.${vhostConfig.useACMEHost}.directory}/key.pem";
+ sslTrustedCertificate = "${certs.${vhostConfig.useACMEHost}.directory}/fullchain.pem";
})
) cfg.virtualHosts;
enableIPv6 = config.networking.enableIPv6;
@@ -646,8 +648,9 @@ in
systemd.services.nginx = {
description = "Nginx Web Server";
- after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
+ wants = concatLists (map (vhostConfig: ["acme-${vhostConfig.serverName}.service" "acme-selfsigned-${vhostConfig.serverName}.service"]) acmeEnabledVhosts);
+ after = [ "network.target" ] ++ map (vhostConfig: "acme-selfsigned-${vhostConfig.serverName}.service") acmeEnabledVhosts;
stopIfChanged = false;
preStart =
''
@@ -680,8 +683,6 @@ in
security.acme.certs = filterAttrs (n: v: v != {}) (
let
- vhostsConfigs = mapAttrsToList (vhostName: vhostConfig: vhostConfig) virtualHosts;
- acmeEnabledVhosts = filter (vhostConfig: vhostConfig.enableACME && vhostConfig.useACMEHost == null) vhostsConfigs;
acmePairs = map (vhostConfig: { name = vhostConfig.serverName; value = {
user = cfg.user;
group = lib.mkDefault cfg.group;
diff --git a/nixos/tests/acme.nix b/nixos/tests/acme.nix
index 4669a092433..8cfdea4a16e 100644
--- a/nixos/tests/acme.nix
+++ b/nixos/tests/acme.nix
@@ -3,19 +3,49 @@ let
in import ./make-test.nix {
name = "acme";
- nodes = {
+ nodes = rec {
letsencrypt = ./common/letsencrypt;
+ acmeStandalone = { config, pkgs, ... }: {
+ imports = [ commonConfig ];
+ networking.firewall.allowedTCPPorts = [ 80 ];
+ networking.extraHosts = ''
+ ${config.networking.primaryIPAddress} standalone.com
+ '';
+ security.acme.certs."standalone.com" = {
+ webroot = "/var/lib/acme/acme-challenges";
+ };
+ systemd.targets."acme-finished-standalone.com" = {};
+ systemd.services."acme-standalone.com" = {
+ wants = [ "acme-finished-standalone.com.target" ];
+ before = [ "acme-finished-standalone.com.target" ];
+ };
+ services.nginx.enable = true;
+ services.nginx.virtualHosts."standalone.com" = {
+ locations."/.well-known/acme-challenge".root = "/var/lib/acme/acme-challenges";
+ };
+ };
+
webserver = { config, pkgs, ... }: {
imports = [ commonConfig ];
networking.firewall.allowedTCPPorts = [ 80 443 ];
networking.extraHosts = ''
- ${config.networking.primaryIPAddress} example.com
+ ${config.networking.primaryIPAddress} a.example.com
+ ${config.networking.primaryIPAddress} b.example.com
'';
+ # A target remains active. Use this to probe the fact that
+ # a service fired eventhough it is not RemainAfterExit
+ systemd.targets."acme-finished-a.example.com" = {};
+ systemd.services."acme-a.example.com" = {
+ wants = [ "acme-finished-a.example.com.target" ];
+ before = [ "acme-finished-a.example.com.target" ];
+ };
+
services.nginx.enable = true;
- services.nginx.virtualHosts."example.com" = {
+
+ services.nginx.virtualHosts."a.example.com" = {
enableACME = true;
forceSSL = true;
locations."/".root = pkgs.runCommand "docroot" {} ''
@@ -23,17 +53,63 @@ in import ./make-test.nix {
echo hello world > "$out/index.html"
'';
};
+
+ nesting.clone = [
+ ({pkgs, ...}: {
+
+ networking.extraHosts = ''
+ ${config.networking.primaryIPAddress} b.example.com
+ '';
+ systemd.targets."acme-finished-b.example.com" = {};
+ systemd.services."acme-b.example.com" = {
+ wants = [ "acme-finished-b.example.com.target" ];
+ before = [ "acme-finished-b.example.com.target" ];
+ };
+ services.nginx.virtualHosts."b.example.com" = {
+ enableACME = true;
+ forceSSL = true;
+ locations."/".root = pkgs.runCommand "docroot" {} ''
+ mkdir -p "$out"
+ echo hello world > "$out/index.html"
+ '';
+ };
+ })
+ ];
};
client = commonConfig;
};
- testScript = ''
- $letsencrypt->waitForUnit("default.target");
- $letsencrypt->waitForUnit("boulder.service");
- $webserver->waitForUnit("default.target");
- $webserver->waitForUnit("acme-certificates.target");
- $client->waitForUnit("default.target");
- $client->succeed('curl https://example.com/ | grep -qF "hello world"');
- '';
+ testScript = {nodes, ...}:
+ let
+ newServerSystem = nodes.webserver2.config.system.build.toplevel;
+ switchToNewServer = "${newServerSystem}/bin/switch-to-configuration test";
+ in
+ # Note, waitForUnit does not work for oneshot services that do not have RemainAfterExit=true,
+ # this is because a oneshot goes from inactive => activating => inactive, and never
+ # reaches the active state. To work around this, we create some mock target units which
+ # get pulled in by the oneshot units. The target units linger after activation, and hence we
+ # can use them to probe that a oneshot fired. It is a bit ugly, but it is the best we can do
+ ''
+ $client->waitForUnit("default.target");
+ $letsencrypt->waitForUnit("default.target");
+ $letsencrypt->waitForUnit("boulder.service");
+
+ subtest "can request certificate with HTTPS-01 challenge", sub {
+ $acmeStandalone->waitForUnit("default.target");
+ $acmeStandalone->succeed("systemctl start acme-standalone.com.service");
+ $acmeStandalone->waitForUnit("acme-finished-standalone.com.target");
+ };
+
+ subtest "Can request certificate for nginx service", sub {
+ $webserver->waitForUnit("acme-finished-a.example.com.target");
+ $client->succeed('curl https://a.example.com/ | grep -qF "hello world"');
+ };
+
+ subtest "Can add another certificate for nginx service", sub {
+ $webserver->succeed("/run/current-system/fine-tune/child-1/bin/switch-to-configuration test");
+ $webserver->waitForUnit("acme-finished-b.example.com.target");
+ $client->succeed('curl https://b.example.com/ | grep -qF "hello world"');
+ };
+ '';
}