···11+{ config, lib, pkgs, ... }:
22+33+with lib;
44+55+let
66+77+ cfg = config.security.acme;
88+99+ certOpts = { ... }: {
1010+ options = {
1111+ webroot = mkOption {
1212+ type = types.str;
1313+ description = ''
1414+ Where the webroot of the HTTP vhost is located.
1515+ <filename>.well-known/acme-challenge/</filename> directory
1616+ will be created automatically if it doesn't exist.
1717+ <literal>http://example.org/.well-known/acme-challenge/</literal> must also
1818+ be available (notice unencrypted HTTP).
1919+ '';
2020+ };
2121+2222+ email = mkOption {
2323+ type = types.nullOr types.str;
2424+ default = null;
2525+ description = "Contact email address for the CA to be able to reach you.";
2626+ };
2727+2828+ user = mkOption {
2929+ type = types.str;
3030+ default = "root";
3131+ description = "User running the ACME client.";
3232+ };
3333+3434+ group = mkOption {
3535+ type = types.str;
3636+ default = "root";
3737+ description = "Group running the ACME client.";
3838+ };
3939+4040+ postRun = mkOption {
4141+ type = types.lines;
4242+ default = "";
4343+ example = "systemctl reload nginx.service";
4444+ description = ''
4545+ Commands to run after certificates are re-issued. Typically
4646+ the web server and other servers using certificates need to
4747+ be reloaded.
4848+ '';
4949+ };
5050+5151+ plugins = mkOption {
5252+ type = types.listOf (types.enum [
5353+ "cert.der" "cert.pem" "chain.der" "chain.pem" "external_pem.sh"
5454+ "fullchain.der" "fullchain.pem" "key.der" "key.pem" "account_key.json"
5555+ ]);
5656+ default = [ "fullchain.pem" "key.pem" "account_key.json" ];
5757+ description = ''
5858+ Plugins to enable. With default settings simp_le will
5959+ store public certificate bundle in <filename>fullchain.pem</filename>
6060+ and private key in <filename>key.pem</filename> in its state directory.
6161+ '';
6262+ };
6363+6464+ extraDomains = mkOption {
6565+ type = types.attrsOf (types.nullOr types.str);
6666+ default = {};
6767+ example = {
6868+ "example.org" = "/srv/http/nginx";
6969+ "mydomain.org" = null;
7070+ };
7171+ description = ''
7272+ Extra domain names for which certificates are to be issued, with their
7373+ own server roots if needed.
7474+ '';
7575+ };
7676+ };
7777+ };
7878+7979+in
8080+8181+{
8282+8383+ ###### interface
8484+8585+ options = {
8686+ security.acme = {
8787+ directory = mkOption {
8888+ default = "/var/lib/acme";
8989+ type = types.str;
9090+ description = ''
9191+ Directory where certs and other state will be stored by default.
9292+ '';
9393+ };
9494+9595+ validMin = mkOption {
9696+ type = types.int;
9797+ default = 30 * 24 * 3600;
9898+ description = "Minimum remaining validity before renewal in seconds.";
9999+ };
100100+101101+ renewInterval = mkOption {
102102+ type = types.str;
103103+ default = "weekly";
104104+ description = ''
105105+ Systemd calendar expression when to check for renewal. See
106106+ <citerefentry><refentrytitle>systemd.time</refentrytitle>
107107+ <manvolnum>5</manvolnum></citerefentry>.
108108+ '';
109109+ };
110110+111111+ certs = mkOption {
112112+ default = { };
113113+ type = types.loaOf types.optionSet;
114114+ description = ''
115115+ Attribute set of certificates to get signed and renewed.
116116+ '';
117117+ options = [ certOpts ];
118118+ example = {
119119+ "example.com" = {
120120+ webroot = "/var/www/challenges/";
121121+ email = "foo@example.com";
122122+ extraDomains = { "www.example.com" = null; "foo.example.com" = "/var/www/foo/"; };
123123+ };
124124+ "bar.example.com" = {
125125+ webroot = "/var/www/challenges/";
126126+ email = "bar@example.com";
127127+ };
128128+ };
129129+ };
130130+ };
131131+ };
132132+133133+ ###### implementation
134134+ config = mkMerge [
135135+ (mkIf (cfg.certs != { }) {
136136+137137+ systemd.services = flip mapAttrs' cfg.certs (cert: data:
138138+ let
139139+ cpath = "${cfg.directory}/${cert}";
140140+ cmdline = [ "-v" "-d" cert "--default_root" data.webroot "--valid_min" cfg.validMin ]
141141+ ++ optionals (data.email != null) [ "--email" data.email ]
142142+ ++ concatMap (p: [ "-f" p ]) data.plugins
143143+ ++ concatLists (mapAttrsToList (name: root: [ "-d" (if root == null then name else "${name}:${root}")]) data.extraDomains);
144144+145145+ in nameValuePair
146146+ ("acme-${cert}")
147147+ ({
148148+ description = "ACME cert renewal for ${cert} using simp_le";
149149+ after = [ "network.target" ];
150150+ serviceConfig = {
151151+ Type = "oneshot";
152152+ SuccessExitStatus = [ "0" "1" ];
153153+ PermissionsStartOnly = true;
154154+ User = data.user;
155155+ Group = data.group;
156156+ PrivateTmp = true;
157157+ };
158158+ path = [ pkgs.simp_le ];
159159+ preStart = ''
160160+ mkdir -p '${cfg.directory}'
161161+ if [ ! -d '${cpath}' ]; then
162162+ mkdir -m 700 '${cpath}'
163163+ chown '${data.user}:${data.group}' '${cpath}'
164164+ fi
165165+ '';
166166+ script = ''
167167+ cd '${cpath}'
168168+ set +e
169169+ simp_le ${concatMapStringsSep " " (arg: escapeShellArg (toString arg)) cmdline}
170170+ EXITCODE=$?
171171+ set -e
172172+ echo "$EXITCODE" > /tmp/lastExitCode
173173+ exit "$EXITCODE"
174174+ '';
175175+ postStop = ''
176176+ if [ -e /tmp/lastExitCode ] && [ "$(cat /tmp/lastExitCode)" = "0" ]; then
177177+ echo "Executing postRun hook..."
178178+ ${data.postRun}
179179+ fi
180180+ '';
181181+ })
182182+ );
183183+184184+ systemd.timers = flip mapAttrs' cfg.certs (cert: data: nameValuePair
185185+ ("acme-${cert}")
186186+ ({
187187+ description = "timer for ACME cert renewal of ${cert}";
188188+ wantedBy = [ "timers.target" ];
189189+ timerConfig = {
190190+ OnCalendar = cfg.renewInterval;
191191+ Unit = "acme-${cert}.service";
192192+ };
193193+ })
194194+ );
195195+ })
196196+197197+ { meta.maintainers = with lib.maintainers; [ abbradar fpletz globin ];
198198+ meta.doc = ./acme.xml;
199199+ }
200200+ ];
201201+202202+}
+69
nixos/modules/security/acme.xml
···11+<chapter xmlns="http://docbook.org/ns/docbook"
22+ xmlns:xlink="http://www.w3.org/1999/xlink"
33+ xmlns:xi="http://www.w3.org/2001/XInclude"
44+ version="5.0"
55+ xml:id="module-security-acme">
66+77+<title>SSL/TLS Certificates with ACME</title>
88+99+<para>NixOS supports automatic domain validation & certificate
1010+retrieval and renewal using the ACME protocol. This is currently only
1111+implemented by and for Let's Encrypt. The alternative ACME client
1212+<literal>simp_le</literal> is used under the hood.</para>
1313+1414+<section><title>Prerequisites</title>
1515+1616+<para>You need to have a running HTTP server for verification. The server must
1717+have a webroot defined that can serve
1818+<filename>.well-known/acme-challenge</filename>. This directory must be
1919+writeable by the user that will run the ACME client.</para>
2020+2121+<para>For instance, this generic snippet could be used for Nginx:
2222+2323+<programlisting>
2424+http {
2525+ server {
2626+ server_name _;
2727+ listen 80;
2828+ listen [::]:80;
2929+3030+ location /.well-known/acme-challenge {
3131+ root /var/www/challenges;
3232+ }
3333+3434+ location / {
3535+ return 301 https://$host$request_uri;
3636+ }
3737+ }
3838+}
3939+</programlisting>
4040+</para>
4141+4242+</section>
4343+4444+<section><title>Configuring</title>
4545+4646+<para>To enable ACME certificate retrieval & renewal for a certificate for
4747+<literal>foo.example.com</literal>, add the following in your
4848+<filename>configuration.nix</filename>:
4949+5050+<programlisting>
5151+security.acme.certs."foo.example.com" = {
5252+ webroot = "/var/www/challenges";
5353+ email = "foo@example.com";
5454+};
5555+</programlisting>
5656+</para>
5757+5858+<para>The private key <filename>key.pem</filename> and certificate
5959+<filename>fullchain.pem</filename> will be put into
6060+<filename>/var/lib/acme/foo.example.com</filename>. The target directory can
6161+be configured with the option <literal>security.acme.directory</literal>.
6262+</para>
6363+6464+<para>Refer to <xref linkend="ch-options" /> for all available configuration
6565+options for the <literal>security.acme</literal> module.</para>
6666+6767+</section>
6868+6969+</chapter>