···
# This is defined with lib.mkMerge so that we can separate the config per function.
163
-
setupService = lib.mkMerge [
165
-
description = "Set up the ACME certificate renewal infrastructure";
166
-
script = lib.mkBefore ''
167
-
${lib.optionalString cfg.defaults.enableDebugLogs "set -x"}
170
-
serviceConfig = commonServiceConfig // {
171
-
# This script runs with elevated privileges, denoted by the +
172
-
# ExecStartPre is used instead of ExecStart so that the `script` continues to work.
173
-
ExecStartPre = "+${lib.getExe privilegedSetupScript}";
164
+
description = "Set up the ACME certificate renewal infrastructure";
165
+
path = [ pkgs.minica ];
175
-
# We don't want this to run every time a renewal happens
176
-
RemainAfterExit = true;
167
+
script = lib.mkBefore ''
168
+
${lib.optionalString cfg.defaults.enableDebugLogs "set -x"}
170
+
test -e ca/key.pem || minica \
171
+
--ca-key ca/key.pem \
172
+
--ca-cert ca/cert.pem \
173
+
--domains selfsigned.local
178
-
# StateDirectory entries are a cleaner, service-level mechanism
179
-
# for dealing with persistent service data
183
-
"acme/.lego/accounts"
185
-
StateDirectoryMode = "0755";
176
+
serviceConfig = commonServiceConfig // {
177
+
# This script runs with elevated privileges, denoted by the +
178
+
# ExecStartPre is used instead of ExecStart so that the `script` continues to work.
179
+
ExecStartPre = "+${lib.getExe privilegedSetupScript}";
187
-
# Creates ${lockdir}. Earlier RemainAfterExit=true means
188
-
# it does not get deleted immediately.
189
-
RuntimeDirectory = "acme";
190
-
RuntimeDirectoryMode = "0700";
181
+
# We don't want this to run every time a renewal happens
182
+
RemainAfterExit = true;
192
-
# Generally, we don't write anything that should be group accessible.
193
-
# Group varies for most ACME units, and setup files are only used
194
-
# under the acme user.
184
+
# StateDirectory entries are a cleaner, service-level mechanism
185
+
# for dealing with persistent service data
189
+
"acme/.lego/accounts"
192
+
BindPaths = "/var/lib/acme/.minica:/tmp/ca";
193
+
StateDirectoryMode = "0755";
199
-
# Avoid race conditions creating the CA for selfsigned certs
200
-
(lib.mkIf cfg.preliminarySelfsigned {
201
-
path = [ pkgs.minica ];
202
-
# Working directory will be /tmp
204
-
test -e ca/key.pem || minica \
205
-
--ca-key ca/key.pem \
206
-
--ca-cert ca/cert.pem \
207
-
--domains selfsigned.local
210
-
StateDirectory = [ "acme/.minica" ];
211
-
BindPaths = "/var/lib/acme/.minica:/tmp/ca";
195
+
# Creates ${lockdir}. Earlier RemainAfterExit=true means
196
+
# it does not get deleted immediately.
197
+
RuntimeDirectory = "acme";
198
+
RuntimeDirectoryMode = "0700";
200
+
# Generally, we don't write anything that should be group accessible.
201
+
# Group varies for most ACME units, and setup files are only used
202
+
# under the acme user.
···
acmeServer = data.server;
useDns = data.dnsProvider != null;
destPath = "/var/lib/acme/${cert}";
222
-
selfsignedDeps = lib.optionals (cfg.preliminarySelfsigned) [ "acme-selfsigned-${cert}.service" ];
# Minica and lego have a "feature" which replaces * with _. We need
# to make this substitution to reference the output files from both programs.
···
certificateKey = if data.csrKey != null then "${data.csrKey}" else "certificates/${keyName}.key";
342
-
inherit accountHash cert selfsignedDeps;
332
+
inherit accountHash cert;
description = "Renew ACME Certificate for ${cert}";
wantedBy = [ "timers.target" ];
339
+
# Avoid triggering certificate renewals accidentally when running s-t-c.
340
+
unitConfig."X-OnlyManualStart" = true;
OnCalendar = data.renewInterval;
351
-
Unit = "acme-${cert}.service";
343
+
Unit = "acme-order-renew-${cert}.service";
# Allow systemd to pick a convenient time within the day
···
367
-
selfsignService = lockfileName: {
368
-
description = "Generate self-signed certificate for ${cert}";
359
+
baseService = lockfileName: {
360
+
description = "Ensure certificate for ${cert}";
362
+
wantedBy = [ "multi-user.target" ];
after = [ "acme-setup.service" ];
370
-
requires = [ "acme-setup.service" ];
366
+
# Whenever this service starts (on boot, through dependencies, through
367
+
# changes) we trigger the acme-order-renew service to give it a chance
368
+
# to catch up with the potentially changed config.
370
+
"acme-setup.service"
371
+
"acme-order-renew-${cert}.service"
373
+
before = [ "acme-order-renew-${cert}.service" ];
375
+
restartTriggers = [
376
+
config.systemd.services."acme-order-renew-${cert}".script
375
-
ConditionPathExists = "!/var/lib/acme/${cert}/key.pem";
StartLimitIntervalSec = 0;
···
389
+
RemainAfterExit = true;
StateDirectory = "acme/${cert}";
"/var/lib/acme/.minica:/tmp/ca"
387
-
"/var/lib/acme/${cert}:/tmp/${keyName}"
395
+
"/var/lib/acme/${cert}:/tmp/out"
···
# minica will output to a folder sharing the name of the first domain
# in the list, which will be ${data.domain}
script = (if (lockfileName == null) then lib.id else wrapInFlock "${lockdir}${lockfileName}") ''
405
+
# Regenerate self-signed certificates (in case the SANs change) until we
406
+
# have seen a succesfull ACME certificate at least once.
407
+
if [ -e out/acme-success ]; then
--domains ${lib.escapeShellArg (builtins.concatStringsSep "," ([ data.domain ] ++ extraDomains))}
# Create files to match directory layout for real certificates
402
-
cp ../ca/cert.pem chain.pem
403
-
cat cert.pem chain.pem > fullchain.pem
404
-
cat key.pem fullchain.pem > full.pem
419
+
cp -vp cert.pem ../out/cert.pem
420
+
cp -vp key.pem ../out/key.pem
422
+
cat out/cert.pem ca/cert.pem > out/fullchain.pem
423
+
cp ca/cert.pem out/chain.pem
424
+
cat out/key.pem out/fullchain.pem > out/full.pem
406
-
# Group might change between runs, re-apply it
407
-
chown '${user}:${data.group}' -- *
426
+
# Fix up the output files to adhere to the group and
427
+
# have consistent permissions. This needs to be kept
428
+
# consistent with the acme-setup script above.
429
+
for fixpath in out certificates; do
430
+
if [ -d "$fixpath" ]; then
431
+
chmod -R u=rwX,g=rX,o= "$fixpath"
432
+
chown -R ${user}:${data.group} "$fixpath"
409
-
# Default permissions make the files unreadable by group + anon
410
-
# Need to be readable by group
436
+
${lib.optionalString (data.webroot != null) ''
437
+
# Ensure the webroot exists. Fixing group is required in case configuration was changed between runs.
438
+
# Lego will fail if the webroot does not exist at all.
440
+
mkdir -p '${data.webroot}/.well-known/acme-challenge' \
441
+
&& chgrp '${data.group}' ${data.webroot}/.well-known/acme-challenge
443
+
echo 'Please ensure ${data.webroot}/.well-known/acme-challenge exists and is writable by acme:${data.group}' \
415
-
renewService = lockfileName: {
416
-
description = "Renew ACME certificate for ${cert}";
450
+
orderRenewService = lockfileName: {
451
+
description = "Order (and renew) ACME certificate for ${cert}";
424
-
wants = [ "network-online.target" ] ++ selfsignedDeps;
425
-
requires = [ "acme-setup.service" ];
427
-
# https://github.com/NixOS/nixpkgs/pull/81371#issuecomment-605526099
428
-
wantedBy = lib.optionals (!config.boot.isContainer) [ "multi-user.target" ];
457
+
"acme-${cert}.service"
460
+
"network-online.target"
461
+
"acme-setup.service"
462
+
"acme-${cert}.service"
464
+
# Ensure that certificates are generated if people use `security.acme.certs`
465
+
# without having/declaring other systemd units that depend on the cert.
···
[[ $expiration_days -gt ${toString data.validMinDays} ]]
526
-
${lib.optionalString (data.webroot != null) ''
527
-
# Ensure the webroot exists. Fixing group is required in case configuration was changed between runs.
528
-
# Lego will fail if the webroot does not exist at all.
530
-
mkdir -p '${data.webroot}/.well-known/acme-challenge' \
531
-
&& chgrp '${data.group}' ${data.webroot}/.well-known/acme-challenge
533
-
echo 'Please ensure ${data.webroot}/.well-known/acme-challenge exists and is writable by acme:${data.group}' \
echo '${domainHash}' > domainhash.txt
540
-
# Check if we can renew.
565
+
# Check if a new order is needed
# We can only renew if the list of domains has not changed.
# We also need an account key. Avoids #190493
if cmp -s domainhash.txt certificates/domainhash.txt && [ -e '${certificateKey}' ] && [ -e 'certificates/${keyName}.crt' ] && [ -n "$(find accounts -name '${data.email}.key')" ]; then
# Even if a cert is not expired, it may be revoked by the CA.
# Try to renew, and silently fail if the cert is not expired.
# Avoids #85794 and resolves #129838
···
557
-
# Otherwise do a full run
elif ! lego ${runOpts}; then
# Produce a nice error for those doing their first nixos-rebuild with these certs
echo Failed to fetch certificates. \
This may mean your DNS records are set up incorrectly. \
562
-
${lib.optionalString (cfg.preliminarySelfsigned) "Selfsigned certs are in place and dependant services will still start."}
585
+
Self-signed certs are in place and dependant services will still start.
# Exit 10 so that users can potentially amend SuccessExitStatus to ignore this error.
# High number to avoid Systemd reserved codes.
···
mv domainhash.txt certificates/
570
-
# Group might change between runs, re-apply it
571
-
chown '${user}:${data.group}' certificates/*
593
+
touch out/acme-success
# Copy all certs to the "real" certs directory
596
+
# lego has only an interesting subset of files available,
597
+
# construct reasonably compatible files that clients can consume
if ! cmp -s 'certificates/${keyName}.crt' out/fullchain.pem; then
echo Installing new certificate
···
cat out/key.pem out/fullchain.pem > out/full.pem
584
-
# By default group will have no access to the cert files.
585
-
# This chmod will fix that.
609
+
# Keep permissions consistent. Needs to be in sync with the other scripts.
610
+
for fixpath in out certificates; do
611
+
if [ -d "$fixpath" ]; then
612
+
chmod -R u=rwX,g=rX,o= "$fixpath"
613
+
chown -R ${user}:${data.group} "$fixpath"
# Also ensure safer permissions on the account directory.
chmod -R u=rwX,g=,o= accounts/.
···
908
-
preliminarySelfsigned = lib.mkOption {
909
-
type = lib.types.bool;
912
-
Whether a preliminary self-signed certificate should be generated before
913
-
doing ACME requests. This can be useful when certificates are required in
914
-
a webserver, but ACME needs the webserver to make its requests.
916
-
With preliminary self-signed certificate the webserver can be started and
917
-
can later reload the correct ACME certificates.
acceptTerms = lib.mkOption {
···
"ACME Directory is now hardcoded to /var/lib/acme and its permissions are managed by systemd. See https://github.com/NixOS/nixpkgs/issues/53852 for more info."
(lib.mkRemovedOptionModule [ "security" "acme" "preDelay" ]
1006
-
"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"
1021
+
"This option has been removed. If you want to make sure that something executes before certificates are provisioned, add a RequiredBy=acme-\${cert}.service and Before=acme-\${cert}.service to the service you want to execute before the cert renewal"
(lib.mkRemovedOptionModule [ "security" "acme" "activationDelay" ]
1009
-
"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"
1024
+
"This option has been removed. If you want to make sure that something executes before certificates are provisioned, add a RequiredBy=acme-\${cert}.service and Before=acme-\${cert}.service to the service you want to execute before the cert renewal"
1026
+
(lib.mkRemovedOptionModule [ "security" "acme" "preliminarySelfsigned" ]
1027
+
"This option has been removed. Preliminary self-signed certificates are now always generated to simplify the dependency structure."
(lib.mkChangedOptionModule
[ "security" "acme" "validMin" ]
···
1164
-
renewServiceFunctions = lib.mapAttrs' (
1165
-
cert: conf: lib.nameValuePair "acme-${cert}" conf.renewService
1182
+
orderRenewServiceFunctions = lib.mapAttrs' (
1183
+
cert: conf: lib.nameValuePair "acme-order-renew-${cert}" conf.orderRenewService
1185
+
orderRenewServices =
if cfg.maxConcurrentRenewals > 0 then
1169
-
roundRobinApplyAttrs renewServiceFunctions concurrencyLockfiles
1187
+
roundRobinApplyAttrs orderRenewServiceFunctions concurrencyLockfiles
1171
-
lib.mapAttrs (_: f: f null) renewServiceFunctions;
1172
-
selfsignServiceFunctions = lib.mapAttrs' (
1173
-
cert: conf: lib.nameValuePair "acme-selfsigned-${cert}" conf.selfsignService
1189
+
lib.mapAttrs (_: f: f null) orderRenewServiceFunctions;
1190
+
baseServiceFunctions = lib.mapAttrs' (
1191
+
cert: conf: lib.nameValuePair "acme-${cert}" conf.baseService
1175
-
selfsignServices =
if cfg.maxConcurrentRenewals > 0 then
1177
-
roundRobinApplyAttrs selfsignServiceFunctions concurrencyLockfiles
1195
+
roundRobinApplyAttrs baseServiceFunctions concurrencyLockfiles
1179
-
lib.mapAttrs (_: f: f null) selfsignServiceFunctions;
1197
+
lib.mapAttrs (_: f: f null) baseServiceFunctions;
acme-setup = setupService;
1185
-
// lib.optionalAttrs cfg.preliminarySelfsigned selfsignServices;
1203
+
// orderRenewServices;
systemd.timers = lib.mapAttrs' (
1188
-
cert: conf: lib.nameValuePair "acme-${cert}" conf.renewTimer
1206
+
cert: conf: lib.nameValuePair "acme-renew-${cert}" conf.renewTimer
1193
-
# Create some targets which can be depended on to be "active" after cert renewals
1194
-
finishedTargets = lib.mapAttrs' (
1196
-
lib.nameValuePair "acme-finished-${cert}" {
1197
-
wantedBy = [ "default.target" ];
1198
-
requires = [ "acme-${cert}.service" ];
1199
-
after = [ "acme-${cert}.service" ];
# Create targets to limit the number of simultaneous account creations
# - Pick a "leader" cert service, which will be in charge of creating the account,
···
dnsConfs = builtins.filter (conf: cfg.certs.${conf.cert}.dnsProvider != null) confs;
leaderConf = if dnsConfs != [ ] then builtins.head dnsConfs else builtins.head confs;
1217
-
leader = "acme-${leaderConf.cert}.service";
1218
-
followers = map (conf: "acme-${conf.cert}.service") (
1225
+
leader = "acme-order-renew-${leaderConf.cert}.service";
1226
+
followers = map (conf: "acme-order-renew-${conf.cert}.service") (
builtins.filter (conf: conf != leaderConf) confs
···
1235
+
unitConfig.RefuseManualStart = true;
) (lib.groupBy (conf: conf.accountHash) (lib.attrValues certConfigs));
1230
-
finishedTargets // accountTargets;