@@ -0,0 +1,430 @@
1+# Copied from https://github.com/NixOS/nixpkgs-channels/blob/70717a337f7ae4e486ba71a500367cad697e5f09/nixos/modules/security/acme.nix # MODIFIED
2+# Lines marked '# MODIFIED' have been added or modified from the original. # MODIFIED
3+{ config, lib, pkgs, ... }:
7+ cfg = config.security.acme;
9+ certOpts = { name, ... }: {
12+ type = types.nullOr types.str;
14+ example = "/var/lib/acme/acme-challenges";
16+ Where the webroot of the HTTP vhost is located.
17+ <filename>.well-known/acme-challenge/</filename> directory
18+ will be created below the webroot if it doesn't exist.
19+ <literal>http://example.org/.well-known/acme-challenge/</literal> must also
20+ be available (notice unencrypted HTTP).
25+ type = types.nullOr types.str;
28+ ACME Directory Resource URI. Defaults to let's encrypt
30+ https://acme-v02.api.letsencrypt.org/directory, if unset.
37+ description = "Domain to fetch certificate for (defaults to the entry name)";
41+ type = types.nullOr types.str;
43+ description = "Contact email address for the CA to be able to reach you.";
49+ description = "User running the ACME client.";
55+ description = "Group running the ACME client.";
58+ allowKeysForGroup = mkOption {
62+ Give read permissions to the specified group
63+ (<option>security.acme.cert.<name>.group</option>) to read SSL private certificates.
70+ example = "systemctl reload nginx.service";
72+ Commands to run after new certificates go live. Typically
73+ the web server and other servers using certificates need to
75+ Executed in the same directory with the new certificate.
79+ directory = mkOption {
82+ default = "/var/lib/acme/${name}";
83+ description = "Directory where certificate and other state is stored.";
86+ extraDomains = mkOption {
87+ type = types.attrsOf (types.nullOr types.str);
89+ example = literalExample ''
91+ "example.org" = null;
92+ "mydomain.org" = null;
96+ A list of extra domain names, which are included in the one certificate to be issued.
97+ Setting a distinct server root is deprecated and not functional in 20.03+
101+ keyType = mkOption {
105+ Key type to use for private keys.
106+ For an up to date list of supported values check the --key-type option
107+ at https://go-acme.github.io/lego/usage/cli/#usage.
111+ dnsProvider = mkOption {
112+ type = types.nullOr types.str;
114+ example = "route53";
116+ DNS Challenge provider. For a list of supported providers, see the "code"
117+ field of the DNS providers listed at https://go-acme.github.io/lego/dns/.
121+ credentialsFile = mkOption {
124+ Path to an EnvironmentFile for the cert's service containing any required and
125+ optional environment variables for your selected dnsProvider.
126+ To find out what values you need to set, consult the documentation at
127+ https://go-acme.github.io/lego/dns/ for the corresponding dnsProvider.
129+ example = "/var/src/secrets/example.org-route53-api-token";
132+ dnsPropagationCheck = mkOption {
136+ Toggles lego DNS propagation check, which is used alongside DNS-01
137+ challenge to ensure the DNS entries required are available.
149+ (mkRemovedOptionModule [ "security" "acme" "production" ] ''
150+ Use security.acme.server to define your staging ACME server URL instead.
151+ To use the let's encrypt staging server, use security.acme.server =
152+ "https://acme-staging-v02.api.letsencrypt.org/directory".
155+ # (mkRemovedOptionModule [ "security" "acme" "directory"] "ACME Directory is now hardcoded to /var/lib/acme and its permisisons are managed by systemd. See https://github.com/NixOS/nixpkgs/issues/53852 for more info.") # MODIFIED
156+ # (mkRemovedOptionModule [ "security" "acme" "preDelay"] "This option has been removed. If you want to make sure that something executes before certificates are provisioned, add a RequiredBy=acme-\${cert}.service to the service you want to execute before the cert renewal") # MODIFIED
157+ # (mkRemovedOptionModule [ "security" "acme" "activationDelay"] "This option has been removed. If you want to make sure that something executes before certificates are provisioned, add a RequiredBy=acme-\${cert}.service to the service you want to execute before the cert renewal") # MODIFIED
158+ (mkChangedOptionModule [ "security" "acme" "validMin"] [ "security" "acme" "validMinDays"] (config: config.security.acme.validMin / (24 * 3600)))
163+ validMinDays = mkOption {
166+ description = "Minimum remaining validity before renewal in days.";
170+ type = types.nullOr types.str;
172+ description = "Contact email address for the CA to be able to reach you.";
175+ renewInterval = mkOption {
179+ Systemd calendar expression when to check for renewal. See
180+ <citerefentry><refentrytitle>systemd.time</refentrytitle>
181+ <manvolnum>7</manvolnum></citerefentry>.
186+ type = types.nullOr types.str;
189+ ACME Directory Resource URI. Defaults to let's encrypt
190+ production endpoint,
191+ <literal>https://acme-v02.api.letsencrypt.org/directory</literal>, if unset.
195+ preliminarySelfsigned = mkOption {
199+ Whether a preliminary self-signed certificate should be generated before
200+ doing ACME requests. This can be useful when certificates are required in
201+ a webserver, but ACME needs the webserver to make its requests.
202+ With preliminary self-signed certificate the webserver can be started and
203+ can later reload the correct ACME certificates.
207+ acceptTerms = mkOption {
211+ Accept the CA's terms of service. The default provier is Let's Encrypt,
212+ you can find their ToS at https://letsencrypt.org/repository/
218+ type = with types; attrsOf (submodule certOpts);
220+ Attribute set of certificates to get signed and renewed. Creates
221+ <literal>acme-''${cert}.{service,timer}</literal> systemd units for
222+ each certificate defined here. Other services can add dependencies
223+ to those units if they rely on the certificates being present,
224+ or trigger restarts of the service if certificates get renewed.
226+ example = literalExample ''
229+ webroot = "/var/www/challenges/";
230+ email = "foo@example.com";
231+ extraDomains = { "www.example.com" = null; "foo.example.com" = null; };
233+ "bar.example.com" = {
234+ webroot = "/var/www/challenges/";
235+ email = "bar@example.com";
243+ ###### implementation
245+ (mkIf (cfg.certs != { }) {
248+ certs = (mapAttrsToList (k: v: v) cfg.certs);
251+ assertion = all (certOpts: certOpts.dnsProvider == null || certOpts.webroot == null) certs;
253+ Options `security.acme.certs.<name>.dnsProvider` and
254+ `security.acme.certs.<name>.webroot` are mutually exclusive.
258+ assertion = cfg.email != null || all (certOpts: certOpts.email != null) certs;
260+ You must define `security.acme.certs.<name>.email` or
261+ `security.acme.email` to register with the CA.
265+ assertion = cfg.acceptTerms;
267+ You must accept the CA's terms of service before using
268+ the ACME module by setting `security.acme.acceptTerms`
269+ to `true`. For Let's Encrypt's ToS see https://letsencrypt.org/repository/
274+ systemd.services = let
275+ services = concatLists servicesLists;
276+ servicesLists = mapAttrsToList certToServices cfg.certs;
277+ certToServices = cert: data:
279+ # StateDirectory must be relative, and will be created under /var/lib by systemd
280+ lpath = "acme/${cert}";
281+ apath = "/var/lib/${lpath}";
282+ spath = "/var/lib/acme/.lego/${cert}";
283+ fileMode = if data.allowKeysForGroup then "640" else "600";
284+ globalOpts = [ "-d" data.domain "--email" data.email "--path" "." "--key-type" data.keyType ]
285+ ++ optionals (cfg.acceptTerms) [ "--accept-tos" ]
286+ ++ optionals (data.dnsProvider != null && !data.dnsPropagationCheck) [ "--dns.disable-cp" ]
287+ ++ concatLists (mapAttrsToList (name: root: [ "-d" name ]) data.extraDomains)
288+ ++ (if data.dnsProvider != null then [ "--dns" data.dnsProvider ] else [ "--http" "--http.webroot" data.webroot ])
289+ ++ optionals (cfg.server != null || data.server != null) ["--server" (if data.server == null then cfg.server else data.server)];
290+ runOpts = escapeShellArgs (globalOpts ++ [ "run" ]);
291+ renewOpts = escapeShellArgs (globalOpts ++ [ "renew" "--days" (toString cfg.validMinDays) ]);
293+ description = "Renew ACME Certificate for ${cert}";
294+ after = [ "network.target" "network-online.target" ];
295+ wants = [ "network-online.target" ];
296+ wantedBy = mkIf (!config.boot.isContainer) [ "multi-user.target" ];
302+ StateDirectory = "acme/.lego/${cert} acme/.lego/accounts ${lpath}";
303+ StateDirectoryMode = if data.allowKeysForGroup then "750" else "700";
304+ WorkingDirectory = spath;
305+ # Only try loading the credentialsFile if the dns challenge is enabled
306+ EnvironmentFile = if data.dnsProvider != null then data.credentialsFile else null;
307+ ExecStart = pkgs.writeScript "acme-start" ''
308+ #!${pkgs.runtimeShell} -e
309+ test -L ${spath}/accounts -o -d ${spath}/accounts || ln -s ../accounts ${spath}/accounts
310+ ${pkgs.lego}/bin/lego ${renewOpts} || ${pkgs.lego}/bin/lego ${runOpts}
314+ keyName = builtins.replaceStrings ["*"] ["_"] data.domain;
315+ script = pkgs.writeScript "acme-post-start" ''
316+ #!${pkgs.runtimeShell} -e
318+ # Test that existing cert is older than new cert
319+ KEY=${spath}/certificates/${keyName}.key
321+ if [ -e $KEY -a $KEY -nt key.pem ]; then
323+ cp -p ${spath}/certificates/${keyName}.key key.pem
324+ cp -p ${spath}/certificates/${keyName}.crt fullchain.pem
325+ cp -p ${spath}/certificates/${keyName}.issuer.crt chain.pem
326+ ln -sf fullchain.pem cert.pem
327+ cat key.pem fullchain.pem > full.pem
329+ chmod ${fileMode} *.pem
330+ chown '${data.user}:${data.group}' *.pem
331+ if [ "$KEY_CHANGED" = "yes" ]; then
332+ : # noop in case postRun is empty
341+ selfsignedService = {
342+ description = "Create preliminary self-signed certificate for ${cert}";
343+ path = [ pkgs.openssl ];
346+ workdir="$(mktemp -d)"
348+ openssl genrsa -des3 -passout pass:xxxx -out $workdir/ca.pass.key 2048
349+ openssl rsa -passin pass:xxxx -in $workdir/ca.pass.key -out $workdir/ca.key
350+ openssl req -new -key $workdir/ca.key -out $workdir/ca.csr \
351+ -subj "/C=UK/ST=Warwickshire/L=Leamington/O=OrgName/OU=Security Department/CN=example.com"
352+ openssl x509 -req -days 1 -in $workdir/ca.csr -signkey $workdir/ca.key -out $workdir/ca.crt
354+ openssl genrsa -des3 -passout pass:xxxx -out $workdir/server.pass.key 2048
355+ openssl rsa -passin pass:xxxx -in $workdir/server.pass.key -out $workdir/server.key
356+ openssl req -new -key $workdir/server.key -out $workdir/server.csr \
357+ -subj "/C=UK/ST=Warwickshire/L=Leamington/O=OrgName/OU=IT Department/CN=example.com"
358+ openssl x509 -req -days 1 -in $workdir/server.csr -CA $workdir/ca.crt \
359+ -CAkey $workdir/ca.key -CAserial $workdir/ca.srl -CAcreateserial \
360+ -out $workdir/server.crt
361+ # Copy key to destination
362+ cp $workdir/server.key ${apath}/key.pem
363+ # Create fullchain.pem (same format as "simp_le ... -f fullchain.pem" creates)
364+ cat $workdir/{server.crt,ca.crt} > "${apath}/fullchain.pem"
365+ # Create full.pem for e.g. lighttpd
366+ cat $workdir/{server.key,server.crt,ca.crt} > "${apath}/full.pem"
367+ # Give key acme permissions
368+ chown '${data.user}:${data.group}' "${apath}/"{key,fullchain,full}.pem
369+ chmod ${fileMode} "${apath}/"{key,fullchain,full}.pem
374+ StateDirectory = lpath;
379+ # Do not create self-signed key when key already exists
380+ ConditionPathExists = "!${apath}/key.pem";
384+ [ { name = "acme-${cert}"; value = acmeService; } ]
385+ ++ optional cfg.preliminarySelfsigned { name = "acme-selfsigned-${cert}"; value = selfsignedService; }
387+ servicesAttr = listToAttrs services;
391+ systemd.tmpfiles.rules =
392+ map (data: "d ${data.webroot}/.well-known/acme-challenge - ${data.user} ${data.group}") (filter (data: data.webroot != null) (attrValues cfg.certs));
394+ systemd.timers = let
395+ # Allow systemd to pick a convenient time within the day
397+ # This allows the coalescing of multiple timer jobs.
398+ # We divide by the number of certificates so that if you
399+ # have many certificates, the renewals are distributed over
400+ # the course of the day to avoid rate limits.
401+ numCerts = length (attrNames cfg.certs);
402+ _24hSecs = 60 * 60 * 24;
403+ AccuracySec = "${toString (_24hSecs / numCerts)}s";
404+ in flip mapAttrs' cfg.certs (cert: data: nameValuePair
407+ description = "Renew ACME Certificate for ${cert}";
408+ wantedBy = [ "timers.target" ];
410+ OnCalendar = cfg.renewInterval;
411+ Unit = "acme-${cert}.service";
413+ inherit AccuracySec;
414+ # Skew randomly within the day, per https://letsencrypt.org/docs/integration-guide/.
415+ RandomizedDelaySec = "24h";
420+ systemd.targets.acme-selfsigned-certificates = mkIf cfg.preliminarySelfsigned {};
421+ systemd.targets.acme-certificates = {};
427+ maintainers = with lib.maintainers; [ abbradar fpletz globin m1cr0man ];
428+ # doc = ./acme.xml; # MODIFIED