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}";
|
mkAccountHash = acmeServer: data: mkHash "${toString acmeServer} ${data.keyType} ${data.email}";
|
||||||
accountDirRoot = "/var/lib/acme/.lego/accounts/";
|
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/";
|
lockdir = "/run/acme/";
|
||||||
concurrencyLockfiles = map (n: "${toString n}.lock") (lib.range 1 cfg.maxConcurrentRenewals);
|
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.
|
# 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
|
# Ensures that directories which are shared across all certs
|
||||||
# exist and have the correct user and group, since group
|
# exist and have the correct user and group, since group
|
||||||
# is configurable on a per-cert basis.
|
# is configurable on a per-cert basis.
|
||||||
userMigrationService = let
|
# writeShellScriptBin is used as it produces a nicer binary name, which
|
||||||
script = with builtins; ''
|
# 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
|
chown -R ${user} .lego/accounts
|
||||||
'' + (lib.concatStringsSep "\n" (lib.mapAttrsToList (cert: data: ''
|
'' + (lib.concatStringsSep "\n" (lib.mapAttrsToList (cert: data: ''
|
||||||
for fixpath in ${lib.escapeShellArg cert} .lego/${lib.escapeShellArg cert}; do
|
for fixpath in ${lib.escapeShellArg cert} .lego/${lib.escapeShellArg cert}; do
|
||||||
@ -144,43 +125,59 @@ let
|
|||||||
chown -R ${user}:${data.group} "$fixpath"
|
chown -R ${user}:${data.group} "$fixpath"
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
'') certConfigs));
|
'') certConfigs))
|
||||||
in {
|
);
|
||||||
description = "Fix owner and group of all ACME certificates";
|
|
||||||
|
|
||||||
|
# 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 // {
|
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
|
# We don't want this to run every time a renewal happens
|
||||||
RemainAfterExit = true;
|
RemainAfterExit = true;
|
||||||
|
|
||||||
# StateDirectory entries are a cleaner, service-level mechanism
|
# StateDirectory entries are a cleaner, service-level mechanism
|
||||||
# for dealing with persistent service data
|
# for dealing with persistent service data
|
||||||
StateDirectory = [ "acme" "acme/.lego" "acme/.lego/accounts" ];
|
StateDirectory = [ "acme" "acme/.lego" "acme/.lego/accounts" ];
|
||||||
StateDirectoryMode = 755;
|
StateDirectoryMode = "0755";
|
||||||
WorkingDirectory = "/var/lib/acme";
|
|
||||||
|
|
||||||
# Run the start script as root
|
# Creates ${lockdir}. Earlier RemainAfterExit=true means
|
||||||
ExecStart = "+" + (pkgs.writeShellScript "acme-fixperms" script);
|
# it does not get deleted immediately.
|
||||||
};
|
RuntimeDirectory = "acme";
|
||||||
};
|
RuntimeDirectoryMode = "0700";
|
||||||
lockfilePrepareService = {
|
|
||||||
description = "Manage lock files for acme services";
|
|
||||||
|
|
||||||
# ensure all required lock files exist, but none more
|
# 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 = ''
|
script = ''
|
||||||
GLOBIGNORE="${lib.concatStringsSep ":" concurrencyLockfiles}"
|
test -e ca/key.pem || minica \
|
||||||
rm -f -- *
|
--ca-key ca/key.pem \
|
||||||
unset GLOBIGNORE
|
--ca-cert ca/cert.pem \
|
||||||
|
--domains selfsigned.local
|
||||||
xargs touch <<< "${toString concurrencyLockfiles}"
|
|
||||||
'';
|
'';
|
||||||
|
serviceConfig = {
|
||||||
serviceConfig = commonServiceConfig // {
|
StateDirectory = [ "acme/.minica" ];
|
||||||
# We don't want this to run every time a renewal happens
|
BindPaths = "/var/lib/acme/.minica:/tmp/ca";
|
||||||
RemainAfterExit = true;
|
|
||||||
WorkingDirectory = lockdir;
|
|
||||||
};
|
};
|
||||||
};
|
})
|
||||||
|
];
|
||||||
|
|
||||||
certToConfig = cert: data: let
|
certToConfig = cert: data: let
|
||||||
acmeServer = data.server;
|
acmeServer = data.server;
|
||||||
@ -286,10 +283,10 @@ let
|
|||||||
|
|
||||||
selfsignService = lockfileName: {
|
selfsignService = lockfileName: {
|
||||||
description = "Generate self-signed certificate for ${cert}";
|
description = "Generate self-signed certificate for ${cert}";
|
||||||
after = [ "acme-selfsigned-ca.service" "acme-fixperms.service" ] ++ lib.optional (cfg.maxConcurrentRenewals > 0) "acme-lockfiles.service";
|
after = [ "acme-setup.service" ];
|
||||||
requires = [ "acme-selfsigned-ca.service" "acme-fixperms.service" ] ++ lib.optional (cfg.maxConcurrentRenewals > 0) "acme-lockfiles.service";
|
requires = [ "acme-setup.service" ];
|
||||||
|
|
||||||
path = with pkgs; [ minica ];
|
path = [ pkgs.minica ];
|
||||||
|
|
||||||
unitConfig = {
|
unitConfig = {
|
||||||
ConditionPathExists = "!/var/lib/acme/${cert}/key.pem";
|
ConditionPathExists = "!/var/lib/acme/${cert}/key.pem";
|
||||||
@ -334,8 +331,9 @@ let
|
|||||||
|
|
||||||
renewService = lockfileName: {
|
renewService = lockfileName: {
|
||||||
description = "Renew ACME certificate for ${cert}";
|
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";
|
after = [ "network.target" "network-online.target" "acme-setup.service" "nss-lookup.target" ] ++ selfsignedDeps;
|
||||||
wants = [ "network-online.target" "acme-fixperms.service" ] ++ selfsignedDeps ++ lib.optional (cfg.maxConcurrentRenewals > 0) "acme-lockfiles.service";
|
wants = [ "network-online.target" ] ++ selfsignedDeps;
|
||||||
|
requires = [ "acme-setup.service" ];
|
||||||
|
|
||||||
# https://github.com/NixOS/nixpkgs/pull/81371#issuecomment-605526099
|
# https://github.com/NixOS/nixpkgs/pull/81371#issuecomment-605526099
|
||||||
wantedBy = lib.optionals (!config.boot.isContainer) [ "multi-user.target" ];
|
wantedBy = lib.optionals (!config.boot.isContainer) [ "multi-user.target" ];
|
||||||
@ -974,12 +972,6 @@ in {
|
|||||||
|
|
||||||
users.groups.acme = {};
|
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
|
systemd.services = let
|
||||||
renewServiceFunctions = lib.mapAttrs' (cert: conf: lib.nameValuePair "acme-${cert}" conf.renewService) certConfigs;
|
renewServiceFunctions = lib.mapAttrs' (cert: conf: lib.nameValuePair "acme-${cert}" conf.renewService) certConfigs;
|
||||||
renewServices = if cfg.maxConcurrentRenewals > 0
|
renewServices = if cfg.maxConcurrentRenewals > 0
|
||||||
@ -990,12 +982,9 @@ in {
|
|||||||
then roundRobinApplyAttrs selfsignServiceFunctions concurrencyLockfiles
|
then roundRobinApplyAttrs selfsignServiceFunctions concurrencyLockfiles
|
||||||
else lib.mapAttrs (_: f: f null) selfsignServiceFunctions;
|
else lib.mapAttrs (_: f: f null) selfsignServiceFunctions;
|
||||||
in
|
in
|
||||||
{ "acme-fixperms" = userMigrationService; }
|
{ acme-setup = setupService; }
|
||||||
// (lib.optionalAttrs (cfg.maxConcurrentRenewals > 0) {"acme-lockfiles" = lockfilePrepareService; })
|
|
||||||
// renewServices
|
// renewServices
|
||||||
// (lib.optionalAttrs (cfg.preliminarySelfsigned) ({
|
// lib.optionalAttrs cfg.preliminarySelfsigned selfsignServices;
|
||||||
"acme-selfsigned-ca" = selfsignCAService;
|
|
||||||
} // selfsignServices));
|
|
||||||
|
|
||||||
systemd.timers = lib.mapAttrs' (cert: conf: lib.nameValuePair "acme-${cert}" conf.renewTimer) certConfigs;
|
systemd.timers = lib.mapAttrs' (cert: conf: lib.nameValuePair "acme-${cert}" conf.renewTimer) certConfigs;
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user