···
# This is defined with lib.mkMerge so that we can separate the config per function.
+
description = "Set up the ACME certificate renewal infrastructure";
+
path = [ pkgs.minica ];
+
script = lib.mkBefore ''
+
${lib.optionalString cfg.defaults.enableDebugLogs "set -x"}
+
test -e ca/key.pem || minica \
+
--ca-cert ca/cert.pem \
+
--domains selfsigned.local
+
serviceConfig = commonServiceConfig // {
+
# This script runs with elevated privileges, denoted by the +
+
# ExecStartPre is used instead of ExecStart so that the `script` continues to work.
+
ExecStartPre = "+${lib.getExe privilegedSetupScript}";
+
# We don't want this to run every time a renewal happens
+
RemainAfterExit = true;
+
# StateDirectory entries are a cleaner, service-level mechanism
+
# for dealing with persistent service data
+
BindPaths = "/var/lib/acme/.minica:/tmp/ca";
+
StateDirectoryMode = "0755";
+
# Creates ${lockdir}. Earlier RemainAfterExit=true means
+
# it does not get deleted immediately.
+
RuntimeDirectory = "acme";
+
RuntimeDirectoryMode = "0700";
+
# Generally, we don't write anything that should be group accessible.
+
# Group varies for most ACME units, and setup files are only used
···
acmeServer = data.server;
useDns = data.dnsProvider != null;
destPath = "/var/lib/acme/${cert}";
# 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";
+
inherit accountHash cert;
description = "Renew ACME Certificate for ${cert}";
wantedBy = [ "timers.target" ];
+
# Avoid triggering certificate renewals accidentally when running s-t-c.
+
unitConfig."X-OnlyManualStart" = true;
OnCalendar = data.renewInterval;
+
Unit = "acme-order-renew-${cert}.service";
# Allow systemd to pick a convenient time within the day
···
+
baseService = lockfileName: {
+
description = "Ensure certificate for ${cert}";
+
wantedBy = [ "multi-user.target" ];
after = [ "acme-setup.service" ];
+
# Whenever this service starts (on boot, through dependencies, through
+
# changes) we trigger the acme-order-renew service to give it a chance
+
# to catch up with the potentially changed config.
+
"acme-order-renew-${cert}.service"
+
before = [ "acme-order-renew-${cert}.service" ];
+
config.systemd.services."acme-order-renew-${cert}".script
StartLimitIntervalSec = 0;
···
+
RemainAfterExit = true;
StateDirectory = "acme/${cert}";
"/var/lib/acme/.minica:/tmp/ca"
+
"/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}") ''
+
# Regenerate self-signed certificates (in case the SANs change) until we
+
# have seen a succesfull ACME certificate at least once.
+
if [ -e out/acme-success ]; then
--domains ${lib.escapeShellArg (builtins.concatStringsSep "," ([ data.domain ] ++ extraDomains))}
# Create files to match directory layout for real certificates
+
cp -vp cert.pem ../out/cert.pem
+
cp -vp key.pem ../out/key.pem
+
cat out/cert.pem ca/cert.pem > out/fullchain.pem
+
cp ca/cert.pem out/chain.pem
+
cat out/key.pem out/fullchain.pem > out/full.pem
+
# Fix up the output files to adhere to the group and
+
# have consistent permissions. This needs to be kept
+
# consistent with the acme-setup script above.
+
for fixpath in out certificates; do
+
if [ -d "$fixpath" ]; then
+
chmod -R u=rwX,g=rX,o= "$fixpath"
+
chown -R ${user}:${data.group} "$fixpath"
+
${lib.optionalString (data.webroot != null) ''
+
# Ensure the webroot exists. Fixing group is required in case configuration was changed between runs.
+
# Lego will fail if the webroot does not exist at all.
+
mkdir -p '${data.webroot}/.well-known/acme-challenge' \
+
&& chgrp '${data.group}' ${data.webroot}/.well-known/acme-challenge
+
echo 'Please ensure ${data.webroot}/.well-known/acme-challenge exists and is writable by acme:${data.group}' \
+
orderRenewService = lockfileName: {
+
description = "Order (and renew) ACME certificate for ${cert}";
+
"network-online.target"
+
# Ensure that certificates are generated if people use `security.acme.certs`
+
# without having/declaring other systemd units that depend on the cert.
···
[[ $expiration_days -gt ${toString data.validMinDays} ]]
echo '${domainHash}' > domainhash.txt
+
# 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
···
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. \
+
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/
# Copy all certs to the "real" certs directory
+
# lego has only an interesting subset of files available,
+
# 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
+
# Keep permissions consistent. Needs to be in sync with the other scripts.
+
for fixpath in out certificates; do
+
if [ -d "$fixpath" ]; then
+
chmod -R u=rwX,g=rX,o= "$fixpath"
+
chown -R ${user}:${data.group} "$fixpath"
# Also ensure safer permissions on the account directory.
chmod -R u=rwX,g=,o= accounts/.
···
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" ]
+
"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" ]
+
"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" "preliminarySelfsigned" ]
+
"This option has been removed. Preliminary self-signed certificates are now always generated to simplify the dependency structure."
(lib.mkChangedOptionModule
[ "security" "acme" "validMin" ]
···
+
orderRenewServiceFunctions = lib.mapAttrs' (
+
cert: conf: lib.nameValuePair "acme-order-renew-${cert}" conf.orderRenewService
if cfg.maxConcurrentRenewals > 0 then
+
roundRobinApplyAttrs orderRenewServiceFunctions concurrencyLockfiles
+
lib.mapAttrs (_: f: f null) orderRenewServiceFunctions;
+
baseServiceFunctions = lib.mapAttrs' (
+
cert: conf: lib.nameValuePair "acme-${cert}" conf.baseService
if cfg.maxConcurrentRenewals > 0 then
+
roundRobinApplyAttrs baseServiceFunctions concurrencyLockfiles
+
lib.mapAttrs (_: f: f null) baseServiceFunctions;
acme-setup = setupService;
systemd.timers = lib.mapAttrs' (
+
cert: conf: lib.nameValuePair "acme-renew-${cert}" conf.renewTimer
# 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;
+
leader = "acme-order-renew-${leaderConf.cert}.service";
+
followers = map (conf: "acme-order-renew-${conf.cert}.service") (
builtins.filter (conf: conf != leaderConf) confs
···
+
unitConfig.RefuseManualStart = true;
) (lib.groupBy (conf: conf.accountHash) (lib.attrValues certConfigs));