acme: added option `security.acme.preliminarySelfsigned` (#15562)
This commit is contained in:
parent
164ead312e
commit
4e6697dcb6
|
@ -114,6 +114,19 @@ in
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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 {
|
certs = mkOption {
|
||||||
default = { };
|
default = { };
|
||||||
type = types.loaOf types.optionSet;
|
type = types.loaOf types.optionSet;
|
||||||
|
@ -140,54 +153,126 @@ in
|
||||||
config = mkMerge [
|
config = mkMerge [
|
||||||
(mkIf (cfg.certs != { }) {
|
(mkIf (cfg.certs != { }) {
|
||||||
|
|
||||||
systemd.services = flip mapAttrs' cfg.certs (cert: data:
|
systemd.services = let
|
||||||
let
|
services = concatLists servicesLists;
|
||||||
cpath = "${cfg.directory}/${cert}";
|
servicesLists = mapAttrsToList certToServices cfg.certs;
|
||||||
rights = if data.allowKeysForGroup then "750" else "700";
|
certToServices = cert: data:
|
||||||
cmdline = [ "-v" "-d" cert "--default_root" data.webroot "--valid_min" cfg.validMin ]
|
let
|
||||||
++ optionals (data.email != null) [ "--email" data.email ]
|
cpath = "${cfg.directory}/${cert}";
|
||||||
++ concatMap (p: [ "-f" p ]) data.plugins
|
rights = if data.allowKeysForGroup then "750" else "700";
|
||||||
++ concatLists (mapAttrsToList (name: root: [ "-d" (if root == null then name else "${name}:${root}")]) data.extraDomains);
|
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
|
before = [ "acme-certificates.target" ];
|
||||||
("acme-${cert}")
|
wantedBy = [ "acme-certificates.target" ];
|
||||||
({
|
};
|
||||||
description = "Renew ACME Certificate for ${cert}";
|
selfsignedService = {
|
||||||
after = [ "network.target" ];
|
description = "Create preliminary self-signed certificate for ${cert}";
|
||||||
serviceConfig = {
|
preStart = ''
|
||||||
Type = "oneshot";
|
if [ ! -d '${cpath}' ]
|
||||||
SuccessExitStatus = [ "0" "1" ];
|
then
|
||||||
PermissionsStartOnly = true;
|
mkdir -p '${cpath}'
|
||||||
User = data.user;
|
chmod ${rights} '${cpath}'
|
||||||
Group = data.group;
|
chown '${data.user}:${data.group}' '${cpath}'
|
||||||
PrivateTmp = true;
|
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 ];
|
in
|
||||||
preStart = ''
|
servicesAttr //
|
||||||
mkdir -p '${cfg.directory}'
|
(if config.services.nginx.enable then nginxAttr else {});
|
||||||
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
|
|
||||||
'';
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
systemd.timers = flip mapAttrs' cfg.certs (cert: data: nameValuePair
|
systemd.timers = flip mapAttrs' cfg.certs (cert: data: nameValuePair
|
||||||
("acme-${cert}")
|
("acme-${cert}")
|
||||||
|
@ -200,6 +285,9 @@ in
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
systemd.targets."acme-selfsigned-certificates" = mkIf cfg.preliminarySelfsigned {};
|
||||||
|
systemd.targets."acme-certificates" = {};
|
||||||
})
|
})
|
||||||
|
|
||||||
{ meta.maintainers = with lib.maintainers; [ abbradar fpletz globin ];
|
{ meta.maintainers = with lib.maintainers; [ abbradar fpletz globin ];
|
||||||
|
|
|
@ -66,4 +66,32 @@ options for the <literal>security.acme</literal> module.</para>
|
||||||
|
|
||||||
</section>
|
</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>
|
</chapter>
|
||||||
|
|
Loading…
Reference in New Issue