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

This commit is contained in:
Bob van der Linden 2016-06-01 12:39:46 +02:00 committed by Domen Kožar
parent 164ead312e
commit 4e6697dcb6
2 changed files with 162 additions and 46 deletions

View file

@ -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 {
default = { };
type = types.loaOf types.optionSet;
@ -140,54 +153,126 @@ in
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}")
@ -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 ];

View file

@ -66,4 +66,32 @@ options for the <literal>security.acme</literal> module.</para>
</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>