982c5a1f0e
- Use an acme user and group, allow group override only - Use hashes to determine when certs actually need to regenerate - Avoid running lego more than necessary - Harden permissions - Support "systemctl clean" for cert regeneration - Support reuse of keys between some configuration changes - Permissions fix services solves for previously root owned certs - Add a note about multiple account creation and emails - Migrate extraDomains to a list - Deprecate user option - Use minica for self-signed certs - Rewrite all tests I thought of a few more cases where things may go wrong, and added tests to cover them. In particular, the web server reload services were depending on the target - which stays alive, meaning that the renewal timer wouldn't be triggering a reload and old certs would stay on the web servers. I encountered some problems ensuring that the reload took place without accidently triggering it as part of the test. The sync commands I added ended up being essential and I'm not sure why, it seems like either node.succeed ends too early or there's an oddity of the vm's filesystem I'm not aware of. - Fix duplicate systemd rules on reload services Since useACMEHost is not unique to every vhost, if one cert was reused many times it would create duplicate entries in ${server}-config-reload.service for wants, before and ConditionPathExists
668 lines
24 KiB
Nix
668 lines
24 KiB
Nix
{ config, lib, pkgs, options, ... }:
|
|
with lib;
|
|
let
|
|
cfg = config.security.acme;
|
|
|
|
# Used to calculate timer accuracy for coalescing
|
|
numCerts = length (builtins.attrNames cfg.certs);
|
|
_24hSecs = 60 * 60 * 24;
|
|
|
|
# There are many services required to make cert renewals work.
|
|
# They all follow a common structure:
|
|
# - They inherit this commonServiceConfig
|
|
# - They all run as the acme user
|
|
# - They all use BindPath and StateDirectory where possible
|
|
# to set up a sort of build environment in /tmp
|
|
# The Group can vary depending on what the user has specified in
|
|
# security.acme.certs.<cert>.group on some of the services.
|
|
commonServiceConfig = {
|
|
Type = "oneshot";
|
|
User = "acme";
|
|
Group = mkDefault "acme";
|
|
UMask = 0027;
|
|
StateDirectoryMode = 750;
|
|
ProtectSystem = "full";
|
|
PrivateTmp = true;
|
|
|
|
WorkingDirectory = "/tmp";
|
|
};
|
|
|
|
# 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";
|
|
};
|
|
|
|
serviceConfig = commonServiceConfig // {
|
|
StateDirectory = "acme/.minica";
|
|
BindPaths = "/var/lib/acme/.minica:/tmp/ca";
|
|
};
|
|
|
|
# Working directory will be /tmp
|
|
script = ''
|
|
minica \
|
|
--ca-key ca/key.pem \
|
|
--ca-cert ca/cert.pem \
|
|
--domains selfsigned.local
|
|
|
|
chmod 600 ca/*
|
|
'';
|
|
};
|
|
|
|
# Previously, all certs were owned by whatever user was configured in
|
|
# config.security.acme.certs.<cert>.user. Now everything is owned by and
|
|
# run by the acme user.
|
|
userMigrationService = {
|
|
description = "Fix owner and group of all ACME certificates";
|
|
|
|
script = with builtins; concatStringsSep "\n" (mapAttrsToList (cert: data: ''
|
|
for fixpath in /var/lib/acme/${escapeShellArg cert} /var/lib/acme/.lego/${escapeShellArg cert}; do
|
|
if [ -d "$fixpath" ]; then
|
|
chmod -R 750 "$fixpath"
|
|
chown -R acme:${data.group} "$fixpath"
|
|
fi
|
|
done
|
|
'') certConfigs);
|
|
|
|
# We don't want this to run every time a renewal happens
|
|
serviceConfig.RemainAfterExit = true;
|
|
};
|
|
|
|
certToConfig = cert: data: let
|
|
acmeServer = if data.server != null then data.server else cfg.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.
|
|
# End users never see this since we rename the certs.
|
|
keyName = builtins.replaceStrings ["*"] ["_"] data.domain;
|
|
|
|
# FIXME when mkChangedOptionModule supports submodules, change to that.
|
|
# This is a workaround
|
|
extraDomains = data.extraDomainNames ++ (
|
|
optionals
|
|
(data.extraDomains != "_mkMergedOptionModule")
|
|
(builtins.attrNames data.extraDomains)
|
|
);
|
|
|
|
# Create hashes for cert data directories based on configuration
|
|
hashData = with builtins; ''
|
|
${data.domain} ${data.keyType}
|
|
${concatStringsSep " " (
|
|
extraDomains
|
|
++ data.extraLegoFlags
|
|
++ data.extraLegoRunFlags
|
|
++ data.extraLegoRenewFlags
|
|
)}
|
|
${toString acmeServer} ${toString data.dnsProvider}
|
|
${toString data.ocspMustStaple}
|
|
'';
|
|
mkHash = with builtins; val: substring 0 20 (hashString "sha256" val);
|
|
certDir = mkHash hashData;
|
|
othersHash = mkHash "${toString acmeServer} ${data.keyType}";
|
|
keyDir = "key-" + othersHash;
|
|
accountDir = "/var/lib/acme/.lego/accounts/" + othersHash;
|
|
|
|
protocolOpts = if useDns then (
|
|
[ "--dns" data.dnsProvider ]
|
|
++ optionals (!data.dnsPropagationCheck) [ "--dns.disable-cp" ]
|
|
) else (
|
|
[ "--http" "--http.webroot" data.webroot ]
|
|
);
|
|
|
|
commonOpts = [
|
|
"--accept-tos" # Checking the option is covered by the assertions
|
|
"--path" "."
|
|
"-d" data.domain
|
|
"--email" data.email
|
|
"--key-type" data.keyType
|
|
] ++ protocolOpts
|
|
++ optionals data.ocspMustStaple [ "--must-staple" ]
|
|
++ optionals (acmeServer != null) [ "--server" acmeServer ]
|
|
++ concatMap (name: [ "-d" name ]) extraDomains
|
|
++ data.extraLegoFlags;
|
|
|
|
runOpts = escapeShellArgs (
|
|
commonOpts
|
|
++ [ "run" ]
|
|
++ data.extraLegoRunFlags
|
|
);
|
|
renewOpts = escapeShellArgs (
|
|
commonOpts
|
|
++ [ "renew" "--reuse-key" "--days" (toString cfg.validMinDays) ]
|
|
++ data.extraLegoRenewFlags
|
|
);
|
|
|
|
in {
|
|
inherit accountDir;
|
|
|
|
webroot = data.webroot;
|
|
group = data.group;
|
|
|
|
renewTimer = {
|
|
description = "Renew ACME Certificate for ${cert}";
|
|
wantedBy = [ "timers.target" ];
|
|
timerConfig = {
|
|
OnCalendar = cfg.renewInterval;
|
|
Unit = "acme-${cert}.service";
|
|
Persistent = "yes";
|
|
|
|
# Allow systemd to pick a convenient time within the day
|
|
# to run the check.
|
|
# This allows the coalescing of multiple timer jobs.
|
|
# We divide by the number of certificates so that if you
|
|
# have many certificates, the renewals are distributed over
|
|
# the course of the day to avoid rate limits.
|
|
AccuracySec = "${toString (_24hSecs / numCerts)}s";
|
|
|
|
# Skew randomly within the day, per https://letsencrypt.org/docs/integration-guide/.
|
|
RandomizedDelaySec = "24h";
|
|
};
|
|
};
|
|
|
|
selfsignService = {
|
|
description = "Generate self-signed certificate for ${cert}";
|
|
after = [ "acme-selfsigned-ca.service" "acme-fixperms.service" ];
|
|
wants = [ "acme-selfsigned-ca.service" "acme-fixperms.service" ];
|
|
|
|
path = with pkgs; [ minica ];
|
|
|
|
unitConfig = {
|
|
ConditionPathExists = "!/var/lib/acme/${cert}/key.pem";
|
|
};
|
|
|
|
serviceConfig = commonServiceConfig // {
|
|
Group = data.group;
|
|
|
|
StateDirectory = "acme/${cert}";
|
|
|
|
BindPaths = "/var/lib/acme/.minica:/tmp/ca /var/lib/acme/${cert}:/tmp/${keyName}";
|
|
};
|
|
|
|
# Working directory will be /tmp
|
|
# minica will output to a folder sharing the name of the first domain
|
|
# in the list, which will be ${data.domain}
|
|
script = ''
|
|
minica \
|
|
--ca-key ca/key.pem \
|
|
--ca-cert ca/cert.pem \
|
|
--domains ${escapeShellArg (builtins.concatStringsSep "," ([ data.domain ] ++ extraDomains))}
|
|
|
|
# Create files to match directory layout for real certificates
|
|
cd '${keyName}'
|
|
cp ../ca/cert.pem chain.pem
|
|
cat cert.pem chain.pem > fullchain.pem
|
|
cat key.pem fullchain.pem > full.pem
|
|
|
|
chmod 640 *
|
|
|
|
# Group might change between runs, re-apply it
|
|
chown 'acme:${data.group}' *
|
|
'';
|
|
};
|
|
|
|
renewService = {
|
|
description = "Renew ACME certificate for ${cert}";
|
|
after = [ "network.target" "network-online.target" "acme-selfsigned-${cert}.service" "acme-fixperms.service" ];
|
|
wants = [ "network-online.target" "acme-selfsigned-${cert}.service" "acme-fixperms.service" ];
|
|
|
|
# https://github.com/NixOS/nixpkgs/pull/81371#issuecomment-605526099
|
|
wantedBy = optionals (!config.boot.isContainer) [ "multi-user.target" ];
|
|
|
|
path = with pkgs; [ lego coreutils ];
|
|
|
|
serviceConfig = commonServiceConfig // {
|
|
Group = data.group;
|
|
|
|
# AccountDir dir will be created by tmpfiles to ensure correct permissions
|
|
# And to avoid deletion during systemctl clean
|
|
# acme/.lego/${cert} is listed so that it is deleted during systemctl clean
|
|
StateDirectory = "acme/${cert} acme/.lego/${cert} acme/.lego/${cert}/${certDir} acme/.lego/${cert}/${keyDir}";
|
|
|
|
# Needs to be space separated, but can't use a multiline string because that'll include newlines
|
|
BindPaths =
|
|
"${accountDir}:/tmp/accounts " +
|
|
"/var/lib/acme/${cert}:/tmp/out " +
|
|
"/var/lib/acme/.lego/${cert}/${certDir}:/tmp/certificates " +
|
|
"/var/lib/acme/.lego/${cert}/${keyDir}:/tmp/keys";
|
|
|
|
# Only try loading the credentialsFile if the dns challenge is enabled
|
|
EnvironmentFile = mkIf useDns data.credentialsFile;
|
|
};
|
|
|
|
# Working directory will be /tmp
|
|
script = ''
|
|
set -euo pipefail
|
|
|
|
# Safely copy keyDir contents into certificates (it might be empty).
|
|
cp -af keys/. certificates/
|
|
|
|
# Check if we can renew
|
|
if [ -e 'certificates/${keyName}.key' -a -e 'certificates/${keyName}.crt' ]; then
|
|
lego ${renewOpts}
|
|
|
|
# Otherwise do a full run
|
|
else
|
|
lego ${runOpts}
|
|
fi
|
|
|
|
chmod 640 certificates/*
|
|
chmod -R 700 accounts/*
|
|
|
|
# Group might change between runs, re-apply it
|
|
chown 'acme:${data.group}' certificates/*
|
|
|
|
# Copy the key to keyDir
|
|
cp -pf 'certificates/${keyName}.key' 'keys/'
|
|
|
|
# Copy all certs to the "real" certs directory
|
|
CERT='certificates/${keyName}.crt'
|
|
CERT_CHANGED=no
|
|
if [ -e "$CERT" -a "$CERT" -nt out/fullchain.pem ]; then
|
|
CERT_CHANGED=yes
|
|
cp -p 'certificates/${keyName}.crt' out/fullchain.pem
|
|
cp -p 'certificates/${keyName}.key' out/key.pem
|
|
cp -p 'certificates/${keyName}.issuer.crt' out/chain.pem
|
|
ln -sf fullchain.pem out/cert.pem
|
|
cat out/key.pem out/fullchain.pem > out/full.pem
|
|
fi
|
|
|
|
if [ "$CERT_CHANGED" = "yes" ]; then
|
|
cd out
|
|
set +euo pipefail
|
|
${data.postRun}
|
|
fi
|
|
'';
|
|
};
|
|
};
|
|
|
|
certConfigs = mapAttrs certToConfig cfg.certs;
|
|
|
|
certOpts = { name, ... }: {
|
|
options = {
|
|
# user option has been removed
|
|
user = mkOption {
|
|
visible = false;
|
|
default = "_mkRemovedOptionModule";
|
|
};
|
|
|
|
# allowKeysForGroup option has been removed
|
|
allowKeysForGroup = mkOption {
|
|
visible = false;
|
|
default = "_mkRemovedOptionModule";
|
|
};
|
|
|
|
# extraDomains was replaced with extraDomainNames
|
|
extraDomains = mkOption {
|
|
visible = false;
|
|
default = "_mkMergedOptionModule";
|
|
};
|
|
|
|
webroot = mkOption {
|
|
type = types.nullOr types.str;
|
|
default = null;
|
|
example = "/var/lib/acme/acme-challenges";
|
|
description = ''
|
|
Where the webroot of the HTTP vhost is located.
|
|
<filename>.well-known/acme-challenge/</filename> directory
|
|
will be created below the webroot if it doesn't exist.
|
|
<literal>http://example.org/.well-known/acme-challenge/</literal> must also
|
|
be available (notice unencrypted HTTP).
|
|
'';
|
|
};
|
|
|
|
server = mkOption {
|
|
type = types.nullOr types.str;
|
|
default = null;
|
|
description = ''
|
|
ACME Directory Resource URI. Defaults to Let's Encrypt's
|
|
production endpoint,
|
|
<link xlink:href="https://acme-v02.api.letsencrypt.org/directory"/>, if unset.
|
|
'';
|
|
};
|
|
|
|
domain = mkOption {
|
|
type = types.str;
|
|
default = name;
|
|
description = "Domain to fetch certificate for (defaults to the entry name).";
|
|
};
|
|
|
|
email = mkOption {
|
|
type = types.nullOr types.str;
|
|
default = cfg.email;
|
|
description = "Contact email address for the CA to be able to reach you.";
|
|
};
|
|
|
|
group = mkOption {
|
|
type = types.str;
|
|
default = "acme";
|
|
description = "Group running the ACME client.";
|
|
};
|
|
|
|
postRun = mkOption {
|
|
type = types.lines;
|
|
default = "";
|
|
example = "cp full.pem backup.pem";
|
|
description = ''
|
|
Commands to run after new certificates go live. Note that
|
|
these commands run as the acme user and configured group.
|
|
|
|
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.";
|
|
};
|
|
|
|
extraDomainNames = mkOption {
|
|
type = types.listOf types.str;
|
|
default = [];
|
|
example = literalExample ''
|
|
[
|
|
"example.org"
|
|
"mydomain.org"
|
|
]
|
|
'';
|
|
description = ''
|
|
A list of extra domain names, which are included in the one certificate to be issued.
|
|
'';
|
|
};
|
|
|
|
keyType = mkOption {
|
|
type = types.str;
|
|
default = "ec256";
|
|
description = ''
|
|
Key type to use for private keys.
|
|
For an up to date list of supported values check the --key-type option
|
|
at <link xlink:href="https://go-acme.github.io/lego/usage/cli/#usage"/>.
|
|
'';
|
|
};
|
|
|
|
dnsProvider = mkOption {
|
|
type = types.nullOr types.str;
|
|
default = null;
|
|
example = "route53";
|
|
description = ''
|
|
DNS Challenge provider. For a list of supported providers, see the "code"
|
|
field of the DNS providers listed at <link xlink:href="https://go-acme.github.io/lego/dns/"/>.
|
|
'';
|
|
};
|
|
|
|
credentialsFile = mkOption {
|
|
type = types.path;
|
|
description = ''
|
|
Path to an EnvironmentFile for the cert's service containing any required and
|
|
optional environment variables for your selected dnsProvider.
|
|
To find out what values you need to set, consult the documentation at
|
|
<link xlink:href="https://go-acme.github.io/lego/dns/"/> for the corresponding dnsProvider.
|
|
'';
|
|
example = "/var/src/secrets/example.org-route53-api-token";
|
|
};
|
|
|
|
dnsPropagationCheck = mkOption {
|
|
type = types.bool;
|
|
default = true;
|
|
description = ''
|
|
Toggles lego DNS propagation check, which is used alongside DNS-01
|
|
challenge to ensure the DNS entries required are available.
|
|
'';
|
|
};
|
|
|
|
ocspMustStaple = mkOption {
|
|
type = types.bool;
|
|
default = false;
|
|
description = ''
|
|
Turns on the OCSP Must-Staple TLS extension.
|
|
Make sure you know what you're doing! See:
|
|
<itemizedlist>
|
|
<listitem><para><link xlink:href="https://blog.apnic.net/2019/01/15/is-the-web-ready-for-ocsp-must-staple/" /></para></listitem>
|
|
<listitem><para><link xlink:href="https://blog.hboeck.de/archives/886-The-Problem-with-OCSP-Stapling-and-Must-Staple-and-why-Certificate-Revocation-is-still-broken.html" /></para></listitem>
|
|
</itemizedlist>
|
|
'';
|
|
};
|
|
|
|
extraLegoFlags = mkOption {
|
|
type = types.listOf types.str;
|
|
default = [];
|
|
description = ''
|
|
Additional global flags to pass to all lego commands.
|
|
'';
|
|
};
|
|
|
|
extraLegoRenewFlags = mkOption {
|
|
type = types.listOf types.str;
|
|
default = [];
|
|
description = ''
|
|
Additional flags to pass to lego renew.
|
|
'';
|
|
};
|
|
|
|
extraLegoRunFlags = mkOption {
|
|
type = types.listOf types.str;
|
|
default = [];
|
|
description = ''
|
|
Additional flags to pass to lego run.
|
|
'';
|
|
};
|
|
};
|
|
};
|
|
|
|
in {
|
|
|
|
options = {
|
|
security.acme = {
|
|
|
|
validMinDays = mkOption {
|
|
type = types.int;
|
|
default = 30;
|
|
description = "Minimum remaining validity before renewal in days.";
|
|
};
|
|
|
|
email = mkOption {
|
|
type = types.nullOr types.str;
|
|
default = null;
|
|
description = "Contact email address for the CA to be able to reach you.";
|
|
};
|
|
|
|
renewInterval = mkOption {
|
|
type = types.str;
|
|
default = "daily";
|
|
description = ''
|
|
Systemd calendar expression when to check for renewal. See
|
|
<citerefentry><refentrytitle>systemd.time</refentrytitle>
|
|
<manvolnum>7</manvolnum></citerefentry>.
|
|
'';
|
|
};
|
|
|
|
server = mkOption {
|
|
type = types.nullOr types.str;
|
|
default = null;
|
|
description = ''
|
|
ACME Directory Resource URI. Defaults to Let's Encrypt's
|
|
production endpoint,
|
|
<link xlink:href="https://acme-v02.api.letsencrypt.org/directory"/>, if unset.
|
|
'';
|
|
};
|
|
|
|
preliminarySelfsigned = mkOption {
|
|
type = types.bool;
|
|
default = true;
|
|
description = ''
|
|
Whether a preliminary self-signed certificate should be generated before
|
|
doing ACME requests. This can be useful when certificates are required in
|
|
a webserver, but ACME needs the webserver to make its requests.
|
|
|
|
With preliminary self-signed certificate the webserver can be started and
|
|
can later reload the correct ACME certificates.
|
|
'';
|
|
};
|
|
|
|
acceptTerms = mkOption {
|
|
type = types.bool;
|
|
default = false;
|
|
description = ''
|
|
Accept the CA's terms of service. The default provider is Let's Encrypt,
|
|
you can find their ToS at <link xlink:href="https://letsencrypt.org/repository/"/>.
|
|
'';
|
|
};
|
|
|
|
certs = mkOption {
|
|
default = { };
|
|
type = with types; attrsOf (submodule certOpts);
|
|
description = ''
|
|
Attribute set of certificates to get signed and renewed. Creates
|
|
<literal>acme-''${cert}.{service,timer}</literal> 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 ''
|
|
{
|
|
"example.com" = {
|
|
webroot = "/var/www/challenges/";
|
|
email = "foo@example.com";
|
|
extraDomainNames = [ "www.example.com" "foo.example.com" ];
|
|
};
|
|
"bar.example.com" = {
|
|
webroot = "/var/www/challenges/";
|
|
email = "bar@example.com";
|
|
};
|
|
}
|
|
'';
|
|
};
|
|
};
|
|
};
|
|
|
|
imports = [
|
|
(mkRemovedOptionModule [ "security" "acme" "production" ] ''
|
|
Use security.acme.server to define your staging ACME server URL instead.
|
|
|
|
To use the let's encrypt staging server, use security.acme.server =
|
|
"https://acme-staging-v02.api.letsencrypt.org/directory".
|
|
''
|
|
)
|
|
(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")
|
|
(mkChangedOptionModule [ "security" "acme" "validMin" ] [ "security" "acme" "validMinDays" ] (config: config.security.acme.validMin / (24 * 3600)))
|
|
];
|
|
|
|
config = mkMerge [
|
|
(mkIf (cfg.certs != { }) {
|
|
|
|
# FIXME Most of these custom warnings and filters for security.acme.certs.* are required
|
|
# because using mkRemovedOptionModule/mkChangedOptionModule with attrsets isn't possible.
|
|
warnings = filter (w: w != "") (mapAttrsToList (cert: data: if data.extraDomains != "_mkMergedOptionModule" then ''
|
|
The option definition `security.acme.certs.${cert}.extraDomains` has changed
|
|
to `security.acme.certs.${cert}.extraDomainNames` and is now a list of strings.
|
|
Setting a custom webroot for extra domains is not possible, instead use separate certs.
|
|
'' else "") cfg.certs);
|
|
|
|
assertions = let
|
|
certs = attrValues cfg.certs;
|
|
in [
|
|
{
|
|
assertion = cfg.email != null || all (certOpts: certOpts.email != null) certs;
|
|
message = ''
|
|
You must define `security.acme.certs.<name>.email` or
|
|
`security.acme.email` to register with the CA. Note that using
|
|
many different addresses for certs may trigger account rate limits.
|
|
'';
|
|
}
|
|
{
|
|
assertion = cfg.acceptTerms;
|
|
message = ''
|
|
You must accept the CA's terms of service before using
|
|
the ACME module by setting `security.acme.acceptTerms`
|
|
to `true`. For Let's Encrypt's ToS see https://letsencrypt.org/repository/
|
|
'';
|
|
}
|
|
] ++ (builtins.concatLists (mapAttrsToList (cert: data: [
|
|
{
|
|
assertion = data.user == "_mkRemovedOptionModule";
|
|
message = ''
|
|
The option definition `security.acme.certs.${cert}.user' no longer has any effect; Please remove it.
|
|
Certificate user is now hard coded to the "acme" user. If you would
|
|
like another user to have access, consider adding them to the
|
|
"acme" group or changing security.acme.certs.${cert}.group.
|
|
'';
|
|
}
|
|
{
|
|
assertion = data.allowKeysForGroup == "_mkRemovedOptionModule";
|
|
message = ''
|
|
The option definition `security.acme.certs.${cert}.allowKeysForGroup' no longer has any effect; Please remove it.
|
|
All certs are readable by the configured group. If this is undesired,
|
|
consider changing security.acme.certs.${cert}.group to an unused group.
|
|
'';
|
|
}
|
|
# * in the cert value breaks building of systemd services, and makes
|
|
# referencing them as a user quite weird too. Best practice is to use
|
|
# the domain option.
|
|
{
|
|
assertion = ! hasInfix "*" cert;
|
|
message = ''
|
|
The cert option path `security.acme.certs.${cert}.dnsProvider`
|
|
cannot contain a * character.
|
|
Instead, set `security.acme.certs.${cert}.domain = "${cert}";`
|
|
and remove the wildcard from the path.
|
|
'';
|
|
}
|
|
{
|
|
assertion = data.dnsProvider == null || data.webroot == null;
|
|
message = ''
|
|
Options `security.acme.certs.${cert}.dnsProvider` and
|
|
`security.acme.certs.${cert}.webroot` are mutually exclusive.
|
|
'';
|
|
}
|
|
]) cfg.certs));
|
|
|
|
users.users.acme = {
|
|
home = "/var/lib/acme";
|
|
group = "acme";
|
|
isSystemUser = true;
|
|
};
|
|
|
|
users.groups.acme = {};
|
|
|
|
systemd.services = {
|
|
"acme-fixperms" = userMigrationService;
|
|
} // (mapAttrs' (cert: conf: nameValuePair "acme-${cert}" conf.renewService) certConfigs)
|
|
// (optionalAttrs (cfg.preliminarySelfsigned) ({
|
|
"acme-selfsigned-ca" = selfsignCAService;
|
|
} // (mapAttrs' (cert: conf: nameValuePair "acme-selfsigned-${cert}" conf.selfsignService) certConfigs)));
|
|
|
|
systemd.timers = mapAttrs' (cert: conf: nameValuePair "acme-${cert}" conf.renewTimer) certConfigs;
|
|
|
|
# .lego and .lego/accounts specified to fix any incorrect permissions
|
|
systemd.tmpfiles.rules = [
|
|
"d /var/lib/acme/.lego - acme acme"
|
|
"d /var/lib/acme/.lego/accounts - acme acme"
|
|
] ++ (unique (concatMap (conf: [
|
|
"d ${conf.accountDir} - acme acme"
|
|
] ++ (optional (conf.webroot != null) "d ${conf.webroot}/.well-known/acme-challenge - acme ${conf.group}")
|
|
) (attrValues certConfigs)));
|
|
|
|
# Create some targets which can be depended on to be "active" after cert renewals
|
|
systemd.targets = mapAttrs' (cert: conf: nameValuePair "acme-finished-${cert}" {
|
|
wantedBy = [ "default.target" ];
|
|
wants = [ "acme-${cert}.service" "acme-selfsigned-${cert}.service" ];
|
|
after = [ "acme-${cert}.service" "acme-selfsigned-${cert}.service" ];
|
|
}) certConfigs;
|
|
})
|
|
];
|
|
|
|
meta = {
|
|
maintainers = lib.teams.acme.members;
|
|
doc = ./acme.xml;
|
|
};
|
|
}
|