@@ -0,0 +1,398 @@
1+### COPIED FROM
2+### https://github.com/NixOS/nixpkgs/blob/1d36ad6d16dbf1d3937f899a087a4360332eb141/nixos/modules/security/acme.nix
3+###
4+### to support new plugins in simp_le for ACMEv2 support.
5+### Changed lines are marked with a comment: ### CHANGED
6+
7+{ config, lib, pkgs, ... }:
8+
9+with lib;
10+
11+let
12+
13+ cfg = config.security.acme;
14+
15+ certOpts = { name, ... }: {
16+ options = {
17+ webroot = mkOption {
18+ type = types.str;
19+ example = "/var/lib/acme/acme-challenges";
20+ description = ''
21+ Where the webroot of the HTTP vhost is located.
22+ <filename>.well-known/acme-challenge/</filename> directory
23+ will be created below the webroot if it doesn't exist.
24+ <literal>http://example.org/.well-known/acme-challenge/</literal> must also
25+ be available (notice unencrypted HTTP).
26+ '';
27+ };
28+
29+ domain = mkOption {
30+ type = types.str;
31+ default = name;
32+ description = "Domain to fetch certificate for (defaults to the entry name)";
33+ };
34+
35+ email = mkOption {
36+ type = types.nullOr types.str;
37+ default = null;
38+ description = "Contact email address for the CA to be able to reach you.";
39+ };
40+
41+ user = mkOption {
42+ type = types.str;
43+ default = "root";
44+ description = "User running the ACME client.";
45+ };
46+
47+ group = mkOption {
48+ type = types.str;
49+ default = "root";
50+ description = "Group running the ACME client.";
51+ };
52+
53+ allowKeysForGroup = mkOption {
54+ type = types.bool;
55+ default = false;
56+ description = ''
57+ Give read permissions to the specified group
58+ (<option>security.acme.cert.<name>.group</option>) to read SSL private certificates.
59+ '';
60+ };
61+
62+ postRun = mkOption {
63+ type = types.lines;
64+ default = "";
65+ example = "systemctl reload nginx.service";
66+ description = ''
67+ Commands to run after new certificates go live. Typically
68+ the web server and other servers using certificates need to
69+ be reloaded.
70+
71+ Executed in the same directory with the new certificate.
72+ '';
73+ };
74+
75+ plugins = mkOption {
76+ type = types.listOf (types.enum [
77+ "cert.der" "cert.pem" "chain.pem" "external.sh"
78+ "fullchain.pem" "full.pem" "key.der" "key.pem" "account_key.json" "account_reg.json" ### CHANGED
79+ ]);
80+ default = [ "fullchain.pem" "full.pem" "key.pem" "account_key.json" "account_reg.json" ]; ### CHANGED
81+ description = ''
82+ Plugins to enable. With default settings simp_le will
83+ store public certificate bundle in <filename>fullchain.pem</filename>,
84+ private key in <filename>key.pem</filename> and those two previous
85+ files combined in <filename>full.pem</filename> in its state directory.
86+ '';
87+ };
88+
89+ activationDelay = mkOption {
90+ type = types.nullOr types.str;
91+ default = null;
92+ description = ''
93+ Systemd time span expression to delay copying new certificates to main
94+ state directory. See <citerefentry><refentrytitle>systemd.time</refentrytitle>
95+ <manvolnum>7</manvolnum></citerefentry>.
96+ '';
97+ };
98+
99+ preDelay = mkOption {
100+ type = types.lines;
101+ default = "";
102+ description = ''
103+ Commands to run after certificates are re-issued but before they are
104+ activated. Typically the new certificate is published to DNS.
105+
106+ Executed in the same directory with the new certificate.
107+ '';
108+ };
109+
110+ extraDomains = mkOption {
111+ type = types.attrsOf (types.nullOr types.str);
112+ default = {};
113+ example = literalExample ''
114+ {
115+ "example.org" = "/srv/http/nginx";
116+ "mydomain.org" = null;
117+ }
118+ '';
119+ description = ''
120+ A list of extra domain names, which are included in the one certificate to be issued, with their
121+ own server roots if needed.
122+ '';
123+ };
124+ };
125+ };
126+
127+in
128+
129+{
130+
131+ ###### interface
132+
133+ options = {
134+ security.acme = {
135+ directory = mkOption {
136+ default = "/var/lib/acme";
137+ type = types.str;
138+ description = ''
139+ Directory where certs and other state will be stored by default.
140+ '';
141+ };
142+
143+ validMin = mkOption {
144+ type = types.int;
145+ default = 30 * 24 * 3600;
146+ description = "Minimum remaining validity before renewal in seconds.";
147+ };
148+
149+ renewInterval = mkOption {
150+ type = types.str;
151+ default = "weekly";
152+ description = ''
153+ Systemd calendar expression when to check for renewal. See
154+ <citerefentry><refentrytitle>systemd.time</refentrytitle>
155+ <manvolnum>7</manvolnum></citerefentry>.
156+ '';
157+ };
158+
159+ preliminarySelfsigned = mkOption {
160+ type = types.bool;
161+ default = true;
162+ description = ''
163+ Whether a preliminary self-signed certificate should be generated before
164+ doing ACME requests. This can be useful when certificates are required in
165+ a webserver, but ACME needs the webserver to make its requests.
166+
167+ With preliminary self-signed certificate the webserver can be started and
168+ can later reload the correct ACME certificates.
169+ '';
170+ };
171+
172+ production = mkOption {
173+ type = types.bool;
174+ default = true;
175+ description = ''
176+ If set to true, use Let's Encrypt's production environment
177+ instead of the staging environment. The main benefit of the
178+ staging environment is to get much higher rate limits.
179+
180+ See
181+ <literal>https://letsencrypt.org/docs/staging-environment</literal>
182+ for more detail.
183+ '';
184+ };
185+
186+ certs = mkOption {
187+ default = { };
188+ type = with types; attrsOf (submodule certOpts);
189+ description = ''
190+ Attribute set of certificates to get signed and renewed.
191+ '';
192+ example = literalExample ''
193+ {
194+ "example.com" = {
195+ webroot = "/var/www/challenges/";
196+ email = "foo@example.com";
197+ extraDomains = { "www.example.com" = null; "foo.example.com" = "/var/www/foo/"; };
198+ };
199+ "bar.example.com" = {
200+ webroot = "/var/www/challenges/";
201+ email = "bar@example.com";
202+ };
203+ }
204+ '';
205+ };
206+ };
207+ };
208+
209+ ###### implementation
210+ config = mkMerge [
211+ (mkIf (cfg.certs != { }) {
212+
213+ systemd.services = let
214+ services = concatLists servicesLists;
215+ servicesLists = mapAttrsToList certToServices cfg.certs;
216+ certToServices = cert: data:
217+ let
218+ cpath = lpath + optionalString (data.activationDelay != null) ".staging";
219+ lpath = "${cfg.directory}/${cert}";
220+ rights = if data.allowKeysForGroup then "750" else "700";
221+ cmdline = [ "-v" "-d" data.domain "--default_root" data.webroot "--valid_min" cfg.validMin ]
222+ ++ optionals (data.email != null) [ "--email" data.email ]
223+ ++ concatMap (p: [ "-f" p ]) data.plugins
224+ ++ concatLists (mapAttrsToList (name: root: [ "-d" (if root == null then name else "${name}:${root}")]) data.extraDomains)
225+ ++ optionals (!cfg.production) ["--server" "https://acme-staging.api.letsencrypt.org/directory"];
226+ acmeService = {
227+ description = "Renew ACME Certificate for ${cert}";
228+ after = [ "network.target" "network-online.target" ];
229+ wants = [ "network-online.target" ];
230+ serviceConfig = {
231+ Type = "oneshot";
232+ SuccessExitStatus = [ "0" "1" ];
233+ PermissionsStartOnly = true;
234+ User = data.user;
235+ Group = data.group;
236+ PrivateTmp = true;
237+ };
238+ path = with pkgs; [ simp_le systemd ];
239+ preStart = ''
240+ mkdir -p '${cfg.directory}'
241+ chown 'root:root' '${cfg.directory}'
242+ chmod 755 '${cfg.directory}'
243+ if [ ! -d '${cpath}' ]; then
244+ mkdir '${cpath}'
245+ fi
246+ chmod ${rights} '${cpath}'
247+ chown -R '${data.user}:${data.group}' '${cpath}'
248+ mkdir -p '${data.webroot}/.well-known/acme-challenge'
249+ chown -R '${data.user}:${data.group}' '${data.webroot}/.well-known/acme-challenge'
250+ '';
251+ script = ''
252+ cd '${cpath}'
253+ set +e
254+ simp_le ${escapeShellArgs cmdline}
255+ EXITCODE=$?
256+ set -e
257+ echo "$EXITCODE" > /tmp/lastExitCode
258+ exit "$EXITCODE"
259+ '';
260+ postStop = ''
261+ cd '${cpath}'
262+
263+ if [ -e /tmp/lastExitCode ] && [ "$(cat /tmp/lastExitCode)" = "0" ]; then
264+ ${if data.activationDelay != null then ''
265+
266+ ${data.preDelay}
267+
268+ if [ -d '${lpath}' ]; then
269+ systemd-run --no-block --on-active='${data.activationDelay}' --unit acme-setlive-${cert}.service
270+ else
271+ systemctl --wait start acme-setlive-${cert}.service
272+ fi
273+ '' else data.postRun}
274+
275+ # noop ensuring that the "if" block is non-empty even if
276+ # activationDelay == null and postRun == ""
277+ true
278+ fi
279+ '';
280+
281+ before = [ "acme-certificates.target" ];
282+ wantedBy = [ "acme-certificates.target" ];
283+ };
284+ delayService = {
285+ description = "Set certificate for ${cert} live";
286+ path = with pkgs; [ rsync ];
287+ serviceConfig = {
288+ Type = "oneshot";
289+ };
290+ script = ''
291+ rsync -a --delete-after '${cpath}/' '${lpath}'
292+ '';
293+ postStop = data.postRun;
294+ };
295+ selfsignedService = {
296+ description = "Create preliminary self-signed certificate for ${cert}";
297+ path = [ pkgs.openssl ];
298+ preStart = ''
299+ if [ ! -d '${cpath}' ]
300+ then
301+ mkdir -p '${cpath}'
302+ chmod ${rights} '${cpath}'
303+ chown '${data.user}:${data.group}' '${cpath}'
304+ fi
305+ '';
306+ script =
307+ ''
308+ workdir="$(mktemp -d)"
309+
310+ # Create CA
311+ openssl genrsa -des3 -passout pass:xxxx -out $workdir/ca.pass.key 2048
312+ openssl rsa -passin pass:xxxx -in $workdir/ca.pass.key -out $workdir/ca.key
313+ openssl req -new -key $workdir/ca.key -out $workdir/ca.csr \
314+ -subj "/C=UK/ST=Warwickshire/L=Leamington/O=OrgName/OU=Security Department/CN=example.com"
315+ openssl x509 -req -days 1 -in $workdir/ca.csr -signkey $workdir/ca.key -out $workdir/ca.crt
316+
317+ # Create key
318+ openssl genrsa -des3 -passout pass:xxxx -out $workdir/server.pass.key 2048
319+ openssl rsa -passin pass:xxxx -in $workdir/server.pass.key -out $workdir/server.key
320+ openssl req -new -key $workdir/server.key -out $workdir/server.csr \
321+ -subj "/C=UK/ST=Warwickshire/L=Leamington/O=OrgName/OU=IT Department/CN=example.com"
322+ openssl x509 -req -days 1 -in $workdir/server.csr -CA $workdir/ca.crt \
323+ -CAkey $workdir/ca.key -CAserial $workdir/ca.srl -CAcreateserial \
324+ -out $workdir/server.crt
325+
326+ # Copy key to destination
327+ cp $workdir/server.key ${cpath}/key.pem
328+
329+ # Create fullchain.pem (same format as "simp_le ... -f fullchain.pem" creates)
330+ cat $workdir/{server.crt,ca.crt} > "${cpath}/fullchain.pem"
331+
332+ # Create full.pem for e.g. lighttpd
333+ cat $workdir/{server.key,server.crt,ca.crt} > "${cpath}/full.pem"
334+
335+ # Give key acme permissions
336+ chown '${data.user}:${data.group}' "${cpath}/"{key,fullchain,full}.pem
337+ chmod ${rights} "${cpath}/"{key,fullchain,full}.pem
338+ '';
339+ serviceConfig = {
340+ Type = "oneshot";
341+ PermissionsStartOnly = true;
342+ PrivateTmp = true;
343+ User = data.user;
344+ Group = data.group;
345+ };
346+ unitConfig = {
347+ # Do not create self-signed key when key already exists
348+ ConditionPathExists = "!${cpath}/key.pem";
349+ };
350+ before = [
351+ "acme-selfsigned-certificates.target"
352+ ];
353+ wantedBy = [
354+ "acme-selfsigned-certificates.target"
355+ ];
356+ };
357+ in (
358+ [ { name = "acme-${cert}"; value = acmeService; } ]
359+ ++ optional cfg.preliminarySelfsigned { name = "acme-selfsigned-${cert}"; value = selfsignedService; }
360+ ++ optional (data.activationDelay != null) { name = "acme-setlive-${cert}"; value = delayService; }
361+ );
362+ servicesAttr = listToAttrs services;
363+ injectServiceDep = {
364+ after = [ "acme-selfsigned-certificates.target" ];
365+ wants = [ "acme-selfsigned-certificates.target" "acme-certificates.target" ];
366+ };
367+ in
368+ servicesAttr //
369+ (if config.services.nginx.enable then { nginx = injectServiceDep; } else {}) //
370+ (if config.services.lighttpd.enable then { lighttpd = injectServiceDep; } else {});
371+
372+ systemd.timers = flip mapAttrs' cfg.certs (cert: data: nameValuePair
373+ ("acme-${cert}")
374+ ({
375+ description = "Renew ACME Certificate for ${cert}";
376+ wantedBy = [ "timers.target" ];
377+ timerConfig = {
378+ OnCalendar = cfg.renewInterval;
379+ Unit = "acme-${cert}.service";
380+ Persistent = "yes";
381+ AccuracySec = "5m";
382+ RandomizedDelaySec = "1h";
383+ };
384+ })
385+ );
386+
387+ systemd.targets."acme-selfsigned-certificates" = mkIf cfg.preliminarySelfsigned {};
388+ systemd.targets."acme-certificates" = {};
389+ })
390+
391+ ];
392+
393+ meta = {
394+ maintainers = with lib.maintainers; [ abbradar fpletz globin ];
395+ #doc = ./acme.xml; ### CHANGED
396+ };
397+}
398+