nixos/acme: Refactor setup process
Over time, we added a lot of setup services to the ACME module, namely: - acme-selfsigned-ca.service: Creates the selfsigned CA certificates used to generate selfsigned certs for each configured cert. - acme-fixperms.service: Ensures permissions correctness on certs after system configuration changes. - acme-lockfiles.service: Create lockfiles used to implement maxConcurrentRenewals. These numerous setup services complicated the dependency chain for any cert renewal, and also made it difficult to track responsibility for specific setup steps, for example, creating /var/lib/acme or setting permissions of shared folders. This change proposes a new acme-setup.service which encapsulates the functionality of the previous 3 services into one. The service is still defined in 3 separate chunks (using lib.mkMerge) which allows us to keep a logical separation between each step and preserve some optionality in the features. The result is a generally simplified definition of systemd unit dependencies and an obvious entrypoint for future setup extensions.
This commit is contained in:
parent
73cf49b8ad
commit
84af416af6
@ -15,6 +15,9 @@ let
|
||||
mkAccountHash = acmeServer: data: mkHash "${toString acmeServer} ${data.keyType} ${data.email}";
|
||||
accountDirRoot = "/var/lib/acme/.lego/accounts/";
|
||||
|
||||
# Lockdir is acme-setup.service's RuntimeDirectory.
|
||||
# Since that service is a oneshot with RemainAfterExit,
|
||||
# the folder will exist during all renewal services.
|
||||
lockdir = "/run/acme/";
|
||||
concurrencyLockfiles = map (n: "${toString n}.lock") (lib.range 1 cfg.maxConcurrentRenewals);
|
||||
# Assign elements of `baseList` to each element of `needAssignmentList`, until the latter is exhausted.
|
||||
@ -104,38 +107,16 @@ let
|
||||
];
|
||||
};
|
||||
|
||||
# In order to avoid race conditions creating the CA for selfsigned certs,
|
||||
# we have a separate service which will create the necessary files.
|
||||
selfsignCAService = {
|
||||
description = "Generate self-signed certificate authority";
|
||||
|
||||
path = with pkgs; [ minica ];
|
||||
|
||||
unitConfig = {
|
||||
ConditionPathExists = "!/var/lib/acme/.minica/key.pem";
|
||||
StartLimitIntervalSec = 0;
|
||||
};
|
||||
|
||||
serviceConfig = commonServiceConfig // {
|
||||
StateDirectory = "acme/.minica";
|
||||
BindPaths = "/var/lib/acme/.minica:/tmp/ca";
|
||||
UMask = "0077";
|
||||
};
|
||||
|
||||
# Working directory will be /tmp
|
||||
script = ''
|
||||
minica \
|
||||
--ca-key ca/key.pem \
|
||||
--ca-cert ca/cert.pem \
|
||||
--domains selfsigned.local
|
||||
'';
|
||||
};
|
||||
|
||||
# Ensures that directories which are shared across all certs
|
||||
# exist and have the correct user and group, since group
|
||||
# is configurable on a per-cert basis.
|
||||
userMigrationService = let
|
||||
script = with builtins; ''
|
||||
# writeShellScriptBin is used as it produces a nicer binary name, which
|
||||
# journalctl will show when the service is running.
|
||||
privilegedSetupScript = pkgs.writeShellScriptBin "acme-setup-privileged" (
|
||||
''
|
||||
${lib.optionalString cfg.defaults.enableDebugLogs "set -x"}
|
||||
set -euo pipefail
|
||||
cd /var/lib/acme
|
||||
chown -R ${user} .lego/accounts
|
||||
'' + (lib.concatStringsSep "\n" (lib.mapAttrsToList (cert: data: ''
|
||||
for fixpath in ${lib.escapeShellArg cert} .lego/${lib.escapeShellArg cert}; do
|
||||
@ -144,43 +125,59 @@ let
|
||||
chown -R ${user}:${data.group} "$fixpath"
|
||||
fi
|
||||
done
|
||||
'') certConfigs));
|
||||
in {
|
||||
description = "Fix owner and group of all ACME certificates";
|
||||
'') certConfigs))
|
||||
);
|
||||
|
||||
serviceConfig = commonServiceConfig // {
|
||||
# We don't want this to run every time a renewal happens
|
||||
RemainAfterExit = true;
|
||||
# This is defined with lib.mkMerge so that we can separate the config per function.
|
||||
setupService = lib.mkMerge [
|
||||
{
|
||||
description = "Set up the ACME certificate renewal infrastructure";
|
||||
script = lib.mkBefore ''
|
||||
${lib.optionalString cfg.defaults.enableDebugLogs "set -x"}
|
||||
set -euo pipefail
|
||||
'';
|
||||
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}";
|
||||
|
||||
# StateDirectory entries are a cleaner, service-level mechanism
|
||||
# for dealing with persistent service data
|
||||
StateDirectory = [ "acme" "acme/.lego" "acme/.lego/accounts" ];
|
||||
StateDirectoryMode = 755;
|
||||
WorkingDirectory = "/var/lib/acme";
|
||||
# We don't want this to run every time a renewal happens
|
||||
RemainAfterExit = true;
|
||||
|
||||
# Run the start script as root
|
||||
ExecStart = "+" + (pkgs.writeShellScript "acme-fixperms" script);
|
||||
};
|
||||
};
|
||||
lockfilePrepareService = {
|
||||
description = "Manage lock files for acme services";
|
||||
# StateDirectory entries are a cleaner, service-level mechanism
|
||||
# for dealing with persistent service data
|
||||
StateDirectory = [ "acme" "acme/.lego" "acme/.lego/accounts" ];
|
||||
StateDirectoryMode = "0755";
|
||||
|
||||
# ensure all required lock files exist, but none more
|
||||
script = ''
|
||||
GLOBIGNORE="${lib.concatStringsSep ":" concurrencyLockfiles}"
|
||||
rm -f -- *
|
||||
unset GLOBIGNORE
|
||||
# Creates ${lockdir}. Earlier RemainAfterExit=true means
|
||||
# it does not get deleted immediately.
|
||||
RuntimeDirectory = "acme";
|
||||
RuntimeDirectoryMode = "0700";
|
||||
|
||||
xargs touch <<< "${toString concurrencyLockfiles}"
|
||||
'';
|
||||
|
||||
serviceConfig = commonServiceConfig // {
|
||||
# We don't want this to run every time a renewal happens
|
||||
RemainAfterExit = true;
|
||||
WorkingDirectory = lockdir;
|
||||
};
|
||||
};
|
||||
# Generally, we don't write anything that should be group accessible.
|
||||
# Group varies for most ACME units, and setup files are only used
|
||||
# under the acme user.
|
||||
UMask = "0077";
|
||||
};
|
||||
}
|
||||
|
||||
# In order to avoid race conditions creating the CA for selfsigned certs,
|
||||
# we have a separate service which will create the necessary files.
|
||||
(lib.mkIf cfg.preliminarySelfsigned {
|
||||
path = [ pkgs.minica ];
|
||||
# Working directory will be /tmp
|
||||
script = ''
|
||||
test -e ca/key.pem || minica \
|
||||
--ca-key ca/key.pem \
|
||||
--ca-cert ca/cert.pem \
|
||||
--domains selfsigned.local
|
||||
'';
|
||||
serviceConfig = {
|
||||
StateDirectory = [ "acme/.minica" ];
|
||||
BindPaths = "/var/lib/acme/.minica:/tmp/ca";
|
||||
};
|
||||
})
|
||||
];
|
||||
|
||||
certToConfig = cert: data: let
|
||||
acmeServer = data.server;
|
||||
@ -286,10 +283,10 @@ let
|
||||
|
||||
selfsignService = lockfileName: {
|
||||
description = "Generate self-signed certificate for ${cert}";
|
||||
after = [ "acme-selfsigned-ca.service" "acme-fixperms.service" ] ++ lib.optional (cfg.maxConcurrentRenewals > 0) "acme-lockfiles.service";
|
||||
requires = [ "acme-selfsigned-ca.service" "acme-fixperms.service" ] ++ lib.optional (cfg.maxConcurrentRenewals > 0) "acme-lockfiles.service";
|
||||
after = [ "acme-setup.service" ];
|
||||
requires = [ "acme-setup.service" ];
|
||||
|
||||
path = with pkgs; [ minica ];
|
||||
path = [ pkgs.minica ];
|
||||
|
||||
unitConfig = {
|
||||
ConditionPathExists = "!/var/lib/acme/${cert}/key.pem";
|
||||
@ -334,8 +331,9 @@ let
|
||||
|
||||
renewService = lockfileName: {
|
||||
description = "Renew ACME certificate for ${cert}";
|
||||
after = [ "network.target" "network-online.target" "acme-fixperms.service" "nss-lookup.target" ] ++ selfsignedDeps ++ lib.optional (cfg.maxConcurrentRenewals > 0) "acme-lockfiles.service";
|
||||
wants = [ "network-online.target" "acme-fixperms.service" ] ++ selfsignedDeps ++ lib.optional (cfg.maxConcurrentRenewals > 0) "acme-lockfiles.service";
|
||||
after = [ "network.target" "network-online.target" "acme-setup.service" "nss-lookup.target" ] ++ selfsignedDeps;
|
||||
wants = [ "network-online.target" ] ++ selfsignedDeps;
|
||||
requires = [ "acme-setup.service" ];
|
||||
|
||||
# https://github.com/NixOS/nixpkgs/pull/81371#issuecomment-605526099
|
||||
wantedBy = lib.optionals (!config.boot.isContainer) [ "multi-user.target" ];
|
||||
@ -974,12 +972,6 @@ in {
|
||||
|
||||
users.groups.acme = {};
|
||||
|
||||
# for lock files, still use tmpfiles as they should better reside in /run
|
||||
systemd.tmpfiles.rules = [
|
||||
"d ${lockdir} 0700 ${user} - - -"
|
||||
"Z ${lockdir} 0700 ${user} - - -"
|
||||
];
|
||||
|
||||
systemd.services = let
|
||||
renewServiceFunctions = lib.mapAttrs' (cert: conf: lib.nameValuePair "acme-${cert}" conf.renewService) certConfigs;
|
||||
renewServices = if cfg.maxConcurrentRenewals > 0
|
||||
@ -990,12 +982,9 @@ in {
|
||||
then roundRobinApplyAttrs selfsignServiceFunctions concurrencyLockfiles
|
||||
else lib.mapAttrs (_: f: f null) selfsignServiceFunctions;
|
||||
in
|
||||
{ "acme-fixperms" = userMigrationService; }
|
||||
// (lib.optionalAttrs (cfg.maxConcurrentRenewals > 0) {"acme-lockfiles" = lockfilePrepareService; })
|
||||
{ acme-setup = setupService; }
|
||||
// renewServices
|
||||
// (lib.optionalAttrs (cfg.preliminarySelfsigned) ({
|
||||
"acme-selfsigned-ca" = selfsignCAService;
|
||||
} // selfsignServices));
|
||||
// lib.optionalAttrs cfg.preliminarySelfsigned selfsignServices;
|
||||
|
||||
systemd.timers = lib.mapAttrs' (cert: conf: lib.nameValuePair "acme-${cert}" conf.renewTimer) certConfigs;
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user