acme: added option `security.acme.preliminarySelfsigned` (#15562)

Changed files
+162 -46
nixos
modules
security
+134 -46
nixos/modules/security/acme.nix
···
'';
};
+
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.
+
'';
+
};
+
certs = mkOption {
default = { };
type = types.loaOf types.optionSet;
···
config = mkMerge [
(mkIf (cfg.certs != { }) {
-
systemd.services = flip mapAttrs' cfg.certs (cert: data:
-
let
-
cpath = "${cfg.directory}/${cert}";
-
rights = if data.allowKeysForGroup then "750" else "700";
-
cmdline = [ "-v" "-d" cert "--default_root" data.webroot "--valid_min" cfg.validMin ]
-
++ optionals (data.email != null) [ "--email" data.email ]
-
++ concatMap (p: [ "-f" p ]) data.plugins
-
++ concatLists (mapAttrsToList (name: root: [ "-d" (if root == null then name else "${name}:${root}")]) data.extraDomains);
+
systemd.services = let
+
services = concatLists servicesLists;
+
servicesLists = mapAttrsToList certToServices cfg.certs;
+
certToServices = cert: data:
+
let
+
cpath = "${cfg.directory}/${cert}";
+
rights = if data.allowKeysForGroup then "750" else "700";
+
cmdline = [ "-v" "-d" cert "--default_root" data.webroot "--valid_min" cfg.validMin ]
+
++ optionals (data.email != null) [ "--email" data.email ]
+
++ concatMap (p: [ "-f" p ]) data.plugins
+
++ concatLists (mapAttrsToList (name: root: [ "-d" (if root == null then name else "${name}:${root}")]) data.extraDomains);
+
acmeService = {
+
description = "Renew ACME Certificate for ${cert}";
+
after = [ "network.target" ];
+
serviceConfig = {
+
Type = "oneshot";
+
SuccessExitStatus = [ "0" "1" ];
+
PermissionsStartOnly = true;
+
User = data.user;
+
Group = data.group;
+
PrivateTmp = true;
+
};
+
path = [ pkgs.simp_le ];
+
preStart = ''
+
mkdir -p '${cfg.directory}'
+
if [ ! -d '${cpath}' ]; then
+
mkdir '${cpath}'
+
fi
+
chmod ${rights} '${cpath}'
+
chown -R '${data.user}:${data.group}' '${cpath}'
+
'';
+
script = ''
+
cd '${cpath}'
+
set +e
+
simp_le ${concatMapStringsSep " " (arg: escapeShellArg (toString arg)) cmdline}
+
EXITCODE=$?
+
set -e
+
echo "$EXITCODE" > /tmp/lastExitCode
+
exit "$EXITCODE"
+
'';
+
postStop = ''
+
if [ -e /tmp/lastExitCode ] && [ "$(cat /tmp/lastExitCode)" = "0" ]; then
+
echo "Executing postRun hook..."
+
${data.postRun}
+
fi
+
'';
-
in nameValuePair
-
("acme-${cert}")
-
({
-
description = "Renew ACME Certificate for ${cert}";
-
after = [ "network.target" ];
-
serviceConfig = {
-
Type = "oneshot";
-
SuccessExitStatus = [ "0" "1" ];
-
PermissionsStartOnly = true;
-
User = data.user;
-
Group = data.group;
-
PrivateTmp = true;
+
before = [ "acme-certificates.target" ];
+
wantedBy = [ "acme-certificates.target" ];
+
};
+
selfsignedService = {
+
description = "Create preliminary self-signed certificate for ${cert}";
+
preStart = ''
+
if [ ! -d '${cpath}' ]
+
then
+
mkdir -p '${cpath}'
+
chmod ${rights} '${cpath}'
+
chown '${data.user}:${data.group}' '${cpath}'
+
fi
+
'';
+
script =
+
''
+
# Create self-signed key
+
workdir="/run/acme-selfsigned-${cert}"
+
${pkgs.openssl.bin}/bin/openssl genrsa -des3 -passout pass:x -out $workdir/server.pass.key 2048
+
${pkgs.openssl.bin}/bin/openssl rsa -passin pass:x -in $workdir/server.pass.key -out $workdir/server.key
+
${pkgs.openssl.bin}/bin/openssl req -new -key $workdir/server.key -out $workdir/server.csr \
+
-subj "/C=UK/ST=Warwickshire/L=Leamington/O=OrgName/OU=IT Department/CN=example.com"
+
${pkgs.openssl.bin}/bin/openssl x509 -req -days 1 -in $workdir/server.csr -signkey $workdir/server.key -out $workdir/server.crt
+
+
# Move key to destination
+
mv $workdir/server.key ${cpath}/key.pem
+
mv $workdir/server.crt ${cpath}/fullchain.pem
+
+
# Clean up working directory
+
rm $workdir/server.csr
+
rm $workdir/server.pass.key
+
+
# Give key acme permissions
+
chmod ${rights} '${cpath}/key.pem'
+
chown '${data.user}:${data.group}' '${cpath}/key.pem'
+
chmod ${rights} '${cpath}/fullchain.pem'
+
chown '${data.user}:${data.group}' '${cpath}/fullchain.pem'
+
'';
+
serviceConfig = {
+
Type = "oneshot";
+
RuntimeDirectory = "acme-selfsigned-${cert}";
+
PermissionsStartOnly = true;
+
User = data.user;
+
Group = data.group;
+
};
+
unitConfig = {
+
# Do not create self-signed key when key already exists
+
ConditionPathExists = "!${cpath}/key.pem";
+
};
+
before = [
+
"acme-selfsigned-certificates.target"
+
];
+
wantedBy = [
+
"acme-selfsigned-certificates.target"
+
];
+
};
+
in (
+
[ { name = "acme-${cert}"; value = acmeService; } ]
+
++
+
(if cfg.preliminarySelfsigned
+
then [ { name = "acme-selfsigned-${cert}"; value = selfsignedService; } ]
+
else []
+
)
+
);
+
servicesAttr = listToAttrs services;
+
nginxAttr = {
+
nginx = {
+
after = [ "acme-selfsigned-certificates.target" ];
+
wants = [ "acme-selfsigned-certificates.target" "acme-certificates.target" ];
+
};
};
-
path = [ pkgs.simp_le ];
-
preStart = ''
-
mkdir -p '${cfg.directory}'
-
if [ ! -d '${cpath}' ]; then
-
mkdir '${cpath}'
-
fi
-
chmod ${rights} '${cpath}'
-
chown -R '${data.user}:${data.group}' '${cpath}'
-
'';
-
script = ''
-
cd '${cpath}'
-
set +e
-
simp_le ${concatMapStringsSep " " (arg: escapeShellArg (toString arg)) cmdline}
-
EXITCODE=$?
-
set -e
-
echo "$EXITCODE" > /tmp/lastExitCode
-
exit "$EXITCODE"
-
'';
-
postStop = ''
-
if [ -e /tmp/lastExitCode ] && [ "$(cat /tmp/lastExitCode)" = "0" ]; then
-
echo "Executing postRun hook..."
-
${data.postRun}
-
fi
-
'';
-
})
-
);
+
in
+
servicesAttr //
+
(if config.services.nginx.enable then nginxAttr else {});
systemd.timers = flip mapAttrs' cfg.certs (cert: data: nameValuePair
("acme-${cert}")
···
};
})
);
+
+
systemd.targets."acme-selfsigned-certificates" = mkIf cfg.preliminarySelfsigned {};
+
systemd.targets."acme-certificates" = {};
})
{ meta.maintainers = with lib.maintainers; [ abbradar fpletz globin ];
+28
nixos/modules/security/acme.xml
···
</section>
+
<section><title>Using ACME certificates in Nginx</title>
+
<para>In practice ACME is mostly used for retrieval and renewal of
+
certificates that will be used in a webserver like Nginx. A configuration for
+
Nginx that uses the certificates from ACME for
+
<literal>foo.example.com</literal> will look similar to:
+
</para>
+
+
<programlisting>
+
services.nginx.httpConfig = ''
+
server {
+
server_name foo.example.com;
+
listen 443 ssl;
+
ssl_certificate ${config.security.acme.directory}/foo.example.com/fullchain.pem;
+
ssl_certificate_key ${config.security.acme.directory}/foo.example.com/key.pem;
+
root /var/www/foo.example.com/;
+
}
+
'';
+
</programlisting>
+
+
<para>Now Nginx will try to use the certificates that will be retrieved by ACME.
+
ACME needs Nginx (or any other webserver) to function and Nginx needs
+
the certificates to actually start. For this reason the ACME module
+
automatically generates self-signed certificates that will be used by Nginx to
+
start. After that Nginx is used by ACME to retrieve the actual ACME
+
certificates. <literal>security.acme.preliminarySelfsigned</literal> can be
+
used to control whether to generate the self-signed certificates.
+
</para>
+
</section>
</chapter>