From 832d1f4a571bee6a8b719792ec10426205bebea1 Mon Sep 17 00:00:00 2001 From: Lucas Savva Date: Fri, 18 Oct 2019 20:38:50 +0100 Subject: [PATCH 1/9] maintainers: add m1cr0man --- maintainers/maintainer-list.nix | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/maintainers/maintainer-list.nix b/maintainers/maintainer-list.nix index 180eca8fe322..a01e626c6479 100644 --- a/maintainers/maintainer-list.nix +++ b/maintainers/maintainer-list.nix @@ -4216,6 +4216,12 @@ email = "wheatdoge@gmail.com"; name = "Tim Liou"; }; + m1cr0man = { + email = "lucas+nix@m1cr0man.com"; + github = "m1cr0man"; + githubId = 3044438; + name = "Lucas Savva"; + }; m3tti = { email = "mathaeus.peter.sander@gmail.com"; name = "Mathaeus Sander"; From 1e3607d331a650e958b48e0c6a9231e68dd023f8 Mon Sep 17 00:00:00 2001 From: Lucas Savva Date: Sun, 12 Jan 2020 21:05:57 +0000 Subject: [PATCH 2/9] nixos/acme: replace simp-le with lego client Lego allows users to use the DNS-01 challenge to validate their certificates. It is mostly backwards compatible, with a few caveats. - extraDomains can no longer have different webroots to the main webroot for the cert. - An email address is now mandatory for account creation The following other changes were required: - Deprecate security.acme.certs..plugins, as this was specific to simp-le - Rename security.acme.validMin to validMinDays, to avoid confusion and errors. Lego requires the TTL to be specified in days - Add options to cover DNS challenge (dnsProvider, credentialsFile, dnsPropagationCheck) - A shared state directory is now used (/var/lib/acme/.lego) to avoid account creation rate limits and share credentials between certs --- nixos/modules/security/acme.nix | 141 +++++++++++++++++++++++--------- nixos/modules/security/acme.xml | 2 +- 2 files changed, 103 insertions(+), 40 deletions(-) diff --git a/nixos/modules/security/acme.nix b/nixos/modules/security/acme.nix index 890c421b0ea9..9375115ddcbc 100644 --- a/nixos/modules/security/acme.nix +++ b/nixos/modules/security/acme.nix @@ -1,7 +1,5 @@ { config, lib, pkgs, ... }: - with lib; - let cfg = config.security.acme; @@ -76,20 +74,6 @@ let ''; }; - plugins = mkOption { - type = types.listOf (types.enum [ - "cert.der" "cert.pem" "chain.pem" "external.sh" - "fullchain.pem" "full.pem" "key.der" "key.pem" "account_key.json" "account_reg.json" - ]); - default = [ "fullchain.pem" "full.pem" "key.pem" "account_key.json" "account_reg.json" ]; - description = '' - Plugins to enable. With default settings simp_le will - store public certificate bundle in fullchain.pem, - private key in key.pem and those two previous - files combined in full.pem in its state directory. - ''; - }; - directory = mkOption { type = types.str; readOnly = true; @@ -111,6 +95,31 @@ let own server roots if needed. ''; }; + + dnsProvider = mkOption { + type = types.nullOr types.str; + example = "route53"; + default = null; + description = "DNS Challenge provider"; + }; + + credentialsFile = mkOption { + type = types.str; + description = '' + File containing DNS provider credentials passed as environment variables. + See https://go-acme.github.io/lego/dns/ for more information. + ''; + example = "/var/src/secrets/example.org-route53-api-token"; + }; + + dnsPropagationCheck = mkOption { + type = types.bool; + default = true; + description = '' + Toggles LEGo DNS propagation check, which is used alongside DNS-01 + challenge to ensure the DNS entries required are available + ''; + }; }; }; @@ -130,14 +139,21 @@ in (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.") (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") (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") + (mkChangedOptionModule [ "security" "acme" "validMin"] [ "security" "acme" "validMinDays"] (config: config.security.acme.validMin / (24 * 3600))) ]; options = { security.acme = { - validMin = mkOption { + validMinDays = mkOption { type = types.int; - default = 30 * 24 * 3600; - description = "Minimum remaining validity before renewal in seconds."; + default = 30; + description = "Minimum remaining validity before renewal in days."; + }; + + email = mkOption { + type = types.nullOr types.str; + default = null; + description = "Contact email address for the CA to be able to reach you."; }; renewInterval = mkOption { @@ -173,6 +189,15 @@ in ''; }; + acceptTerms = mkOption { + type = types.bool; + default = true; + description = '' + Accept the current Let's Encrypt terms of service. + See https://letsencrypt.org/repository/ + ''; + }; + certs = mkOption { default = { }; type = with types; attrsOf (submodule certOpts); @@ -204,27 +229,47 @@ in config = mkMerge [ (mkIf (cfg.certs != { }) { + assertions = let + certs = (mapAttrsToList (k: v: v) cfg.certs); + in [ + { + assertion = all (certOpts: certOpts.dnsProvider == null || certOpts.webroot == null) certs; + message = '' + Options `security.acme.certs..dnsProvider` and + `security.acme.certs..webroot` are mutually exclusive. + ''; + } + { + assertion = cfg.email != null || all (certOpts: certOpts.email != null) certs; + message = '' + You must define `security.acme.certs..email` or + `security.acme.email` to register with the CA. + ''; + } + ]; + systemd.services = let services = concatLists servicesLists; servicesLists = mapAttrsToList certToServices cfg.certs; certToServices = cert: data: let + # StateDirectory must be relative, and will be created under /var/lib by systemd lpath = "acme/${cert}"; + apath = "/var/lib/${lpath}"; + spath = "/var/lib/acme/.lego"; rights = if data.allowKeysForGroup then "750" else "700"; - cmdline = [ "-v" "-d" data.domain "--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) + globalOpts = [ "-d" data.domain "--email" data.email "--path" "." ] + ++ optionals (cfg.acceptTerms) [ "--accept-tos" ] + ++ optionals (data.dnsProvider != null && !cfg.dnsPropagationCheck) [ "--dns.disable-cp" ] + ++ concatLists (mapAttrsToList (name: root: [ "-d" name ]) data.extraDomains) + ++ (if data.dnsProvider != null then [ "--dns" data.dnsProvider ] else [ "--http" "--http.webroot" data.webroot ]) ++ optionals (cfg.server != null || data.server != null) ["--server" (if data.server == null then cfg.server else data.server)]; + runOpts = escapeShellArgs (globalOpts ++ [ "run" ]); + renewOpts = escapeShellArgs (globalOpts ++ [ "renew" "--days" (toString cfg.validMinDays) ]); acmeService = { description = "Renew ACME Certificate for ${cert}"; after = [ "network.target" "network-online.target" ]; wants = [ "network-online.target" ]; - # simp_le uses requests, which uses certifi under the hood, - # which doesn't respect the system trust store. - # At least in the acme test, we provision a fake CA, impersonating the LE endpoint. - # REQUESTS_CA_BUNDLE is a way to teach python requests to use something else - environment.REQUESTS_CA_BUNDLE = "/etc/ssl/certs/ca-certificates.crt"; serviceConfig = { Type = "oneshot"; # With RemainAfterExit the service is considered active even @@ -233,18 +278,36 @@ in # the permissions of the StateDirectory get adjusted # according to the specified group RemainAfterExit = true; - SuccessExitStatus = [ "0" "1" ]; User = data.user; Group = data.group; PrivateTmp = true; - StateDirectory = lpath; + StateDirectory = "acme/.lego ${lpath}"; StateDirectoryMode = rights; - WorkingDirectory = "/var/lib/${lpath}"; - ExecStart = "${pkgs.simp_le}/bin/simp_le ${escapeShellArgs cmdline}"; + WorkingDirectory = spath; + # Only try loading the credentialsFile if the dns challenge is enabled + EnvironmentFile = if data.dnsProvider != null then data.credentialsFile else null; + ExecStart = pkgs.writeScript "acme-start" '' + #!${pkgs.runtimeShell} -e + ${pkgs.lego}/bin/lego ${renewOpts} || ${pkgs.lego}/bin/lego ${runOpts} + ''; ExecStartPost = let script = pkgs.writeScript "acme-post-start" '' #!${pkgs.runtimeShell} -e + cd ${apath} + + # Test that existing cert is older than new cert + KEY=${spath}/certificates/*${data.domain}.key + if [ -e $KEY -a $KEY -nt key.pem ]; then + cp -p ${spath}/certificates/*${data.domain}.key key.pem + cp -p ${spath}/certificates/*${data.domain}.crt cert.pem + cp -p ${spath}/certificates/*${data.domain}.issuer.crt chain.pem + cat cert.pem chain.pem > fullchain.pem + cat key.pem cert.pem chain.pem > full.pem + chmod ${rights} *.pem + chown '${data.user}:${data.group}' *.pem + fi + ${data.postRun} ''; in @@ -276,17 +339,17 @@ in -out $workdir/server.crt # Copy key to destination - cp $workdir/server.key /var/lib/${lpath}/key.pem + cp $workdir/server.key ${apath}/key.pem # Create fullchain.pem (same format as "simp_le ... -f fullchain.pem" creates) - cat $workdir/{server.crt,ca.crt} > "/var/lib/${lpath}/fullchain.pem" + cat $workdir/{server.crt,ca.crt} > "${apath}/fullchain.pem" # Create full.pem for e.g. lighttpd - cat $workdir/{server.key,server.crt,ca.crt} > "/var/lib/${lpath}/full.pem" + cat $workdir/{server.key,server.crt,ca.crt} > "${apath}/full.pem" # Give key acme permissions - chown '${data.user}:${data.group}' "/var/lib/${lpath}/"{key,fullchain,full}.pem - chmod ${rights} "/var/lib/${lpath}/"{key,fullchain,full}.pem + chown '${data.user}:${data.group}' "${apath}/"{key,fullchain,full}.pem + chmod ${rights} "${apath}/"{key,fullchain,full}.pem ''; serviceConfig = { Type = "oneshot"; @@ -297,7 +360,7 @@ in }; unitConfig = { # Do not create self-signed key when key already exists - ConditionPathExists = "!/var/lib/${lpath}/key.pem"; + ConditionPathExists = "!${apath}/key.pem"; }; }; in ( @@ -334,7 +397,7 @@ in ]; meta = { - maintainers = with lib.maintainers; [ abbradar fpletz globin ]; + maintainers = with lib.maintainers; [ abbradar fpletz globin m1cr0man ]; doc = ./acme.xml; }; } diff --git a/nixos/modules/security/acme.xml b/nixos/modules/security/acme.xml index 9d0a1995e0ff..963ac7a97c33 100644 --- a/nixos/modules/security/acme.xml +++ b/nixos/modules/security/acme.xml @@ -7,7 +7,7 @@ NixOS supports automatic domain validation & certificate retrieval and renewal using the ACME protocol. This is currently only implemented by and - for Let's Encrypt. The alternative ACME client simp_le is + for Let's Encrypt. The alternative ACME client LEGo is used under the hood.
From 9467f2ba2c38fd04565c7f0fd04e0e2c2673f650 Mon Sep 17 00:00:00 2001 From: Lucas Savva Date: Sun, 12 Jan 2020 21:52:28 +0000 Subject: [PATCH 3/9] nixos/acme: Add logic to select right email address --- nixos/modules/security/acme.nix | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/nixos/modules/security/acme.nix b/nixos/modules/security/acme.nix index 9375115ddcbc..3d63fc25711a 100644 --- a/nixos/modules/security/acme.nix +++ b/nixos/modules/security/acme.nix @@ -258,7 +258,8 @@ in apath = "/var/lib/${lpath}"; spath = "/var/lib/acme/.lego"; rights = if data.allowKeysForGroup then "750" else "700"; - globalOpts = [ "-d" data.domain "--email" data.email "--path" "." ] + email = if data.email == null then cfg.email else data.email; + globalOpts = [ "-d" data.domain "--email" email "--path" "." ] ++ optionals (cfg.acceptTerms) [ "--accept-tos" ] ++ optionals (data.dnsProvider != null && !cfg.dnsPropagationCheck) [ "--dns.disable-cp" ] ++ concatLists (mapAttrsToList (name: root: [ "-d" name ]) data.extraDomains) From 61665e33631f7d0b1edca27050c96137b99423db Mon Sep 17 00:00:00 2001 From: Lucas Savva Date: Wed, 15 Jan 2020 09:17:11 +0000 Subject: [PATCH 4/9] nixos/acme: ignore tmpfiles rules for null webroots --- nixos/modules/security/acme.nix | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/nixos/modules/security/acme.nix b/nixos/modules/security/acme.nix index 3d63fc25711a..11775e6aef05 100644 --- a/nixos/modules/security/acme.nix +++ b/nixos/modules/security/acme.nix @@ -7,7 +7,8 @@ let certOpts = { name, ... }: { options = { webroot = mkOption { - type = types.str; + type = types.nullOr types.str; + default = null; example = "/var/lib/acme/acme-challenges"; description = '' Where the webroot of the HTTP vhost is located. @@ -98,8 +99,8 @@ let dnsProvider = mkOption { type = types.nullOr types.str; - example = "route53"; default = null; + example = "route53"; description = "DNS Challenge provider"; }; @@ -261,7 +262,7 @@ in email = if data.email == null then cfg.email else data.email; globalOpts = [ "-d" data.domain "--email" email "--path" "." ] ++ optionals (cfg.acceptTerms) [ "--accept-tos" ] - ++ optionals (data.dnsProvider != null && !cfg.dnsPropagationCheck) [ "--dns.disable-cp" ] + ++ optionals (data.dnsProvider != null && !data.dnsPropagationCheck) [ "--dns.disable-cp" ] ++ concatLists (mapAttrsToList (name: root: [ "-d" name ]) data.extraDomains) ++ (if data.dnsProvider != null then [ "--dns" data.dnsProvider ] else [ "--http" "--http.webroot" data.webroot ]) ++ optionals (cfg.server != null || data.server != null) ["--server" (if data.server == null then cfg.server else data.server)]; @@ -373,8 +374,7 @@ in servicesAttr; systemd.tmpfiles.rules = - flip mapAttrsToList cfg.certs - (cert: data: "d ${data.webroot}/.well-known/acme-challenge - ${data.user} ${data.group}"); + map (data: "d ${data.webroot}/.well-known/acme-challenge - ${data.user} ${data.group}") (filter (data: data.webroot != null) (attrValues cfg.certs)); systemd.timers = flip mapAttrs' cfg.certs (cert: data: nameValuePair ("acme-${cert}") From 769fbf92541d86730be3f0be3f1c958c82644c92 Mon Sep 17 00:00:00 2001 From: Lucas Savva Date: Sun, 19 Jan 2020 18:24:04 +0000 Subject: [PATCH 5/9] nixos/acme: fix some descriptions, default acceptTerms to false --- nixos/modules/security/acme.nix | 43 +++++++++++++++++++++++++-------- nixos/modules/security/acme.xml | 2 +- 2 files changed, 34 insertions(+), 11 deletions(-) diff --git a/nixos/modules/security/acme.nix b/nixos/modules/security/acme.nix index 11775e6aef05..36cf4f7e6817 100644 --- a/nixos/modules/security/acme.nix +++ b/nixos/modules/security/acme.nix @@ -97,18 +97,33 @@ let ''; }; + keyType = mkOption { + type = types.str; + default = "ec384"; + description = '' + Key type to use for private keys. + For an up to date list of supported values check the --key-type option + at https://go-acme.github.io/lego/usage/cli/#usage. + ''; + }; + dnsProvider = mkOption { type = types.nullOr types.str; default = null; example = "route53"; - description = "DNS Challenge provider"; + description = '' + DNS Challenge provider. For a list of supported providers, see the "code" + field of the DNS providers listed at https://go-acme.github.io/lego/dns/. + ''; }; credentialsFile = mkOption { - type = types.str; + type = types.path; description = '' - File containing DNS provider credentials passed as environment variables. - See https://go-acme.github.io/lego/dns/ for more information. + Path to an EnvironmentFile for the cert's service containing any required and + optional environment variables for your selected dnsProvider. + To find out what values you need to set, consult the documentation at + https://go-acme.github.io/lego/dns/ for the corresponding dnsProvider. ''; example = "/var/src/secrets/example.org-route53-api-token"; }; @@ -117,8 +132,8 @@ let type = types.bool; default = true; description = '' - Toggles LEGo DNS propagation check, which is used alongside DNS-01 - challenge to ensure the DNS entries required are available + Toggles lego DNS propagation check, which is used alongside DNS-01 + challenge to ensure the DNS entries required are available. ''; }; }; @@ -192,10 +207,10 @@ in acceptTerms = mkOption { type = types.bool; - default = true; + default = false; description = '' - Accept the current Let's Encrypt terms of service. - See https://letsencrypt.org/repository/ + Accept the CA's terms of service. The default provier is Let's Encrypt, + you can find their ToS at https://letsencrypt.org/repository/ ''; }; @@ -247,6 +262,14 @@ in `security.acme.email` to register with the CA. ''; } + { + assertion = cfg.acceptTerms; + message = '' + You must accept the CA's terms of service before using + the ACME module by setting `security.acme.acceptTerms` + to `true`. For Let's Encrypt's ToS see https://letsencrypt.org/repository/ + ''; + } ]; systemd.services = let @@ -260,7 +283,7 @@ in spath = "/var/lib/acme/.lego"; rights = if data.allowKeysForGroup then "750" else "700"; email = if data.email == null then cfg.email else data.email; - globalOpts = [ "-d" data.domain "--email" email "--path" "." ] + globalOpts = [ "-d" data.domain "--email" email "--path" "." "--key-type" data.keyType ] ++ optionals (cfg.acceptTerms) [ "--accept-tos" ] ++ optionals (data.dnsProvider != null && !data.dnsPropagationCheck) [ "--dns.disable-cp" ] ++ concatLists (mapAttrsToList (name: root: [ "-d" name ]) data.extraDomains) diff --git a/nixos/modules/security/acme.xml b/nixos/modules/security/acme.xml index 963ac7a97c33..2b29c1174845 100644 --- a/nixos/modules/security/acme.xml +++ b/nixos/modules/security/acme.xml @@ -7,7 +7,7 @@ NixOS supports automatic domain validation & certificate retrieval and renewal using the ACME protocol. This is currently only implemented by and - for Let's Encrypt. The alternative ACME client LEGo is + for Let's Encrypt. The alternative ACME client lego is used under the hood.
From 2181313c542364d63674ed34cbf69d7691ddfa0d Mon Sep 17 00:00:00 2001 From: Lucas Savva Date: Mon, 3 Feb 2020 21:37:22 +0000 Subject: [PATCH 6/9] nixos/acme: simplify email resolve logic --- nixos/modules/security/acme.nix | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/nixos/modules/security/acme.nix b/nixos/modules/security/acme.nix index 36cf4f7e6817..907d909ecf15 100644 --- a/nixos/modules/security/acme.nix +++ b/nixos/modules/security/acme.nix @@ -37,7 +37,7 @@ let email = mkOption { type = types.nullOr types.str; - default = null; + default = cfg.email; description = "Contact email address for the CA to be able to reach you."; }; @@ -282,8 +282,7 @@ in apath = "/var/lib/${lpath}"; spath = "/var/lib/acme/.lego"; rights = if data.allowKeysForGroup then "750" else "700"; - email = if data.email == null then cfg.email else data.email; - globalOpts = [ "-d" data.domain "--email" email "--path" "." "--key-type" data.keyType ] + globalOpts = [ "-d" data.domain "--email" data.email "--path" "." "--key-type" data.keyType ] ++ optionals (cfg.acceptTerms) [ "--accept-tos" ] ++ optionals (data.dnsProvider != null && !data.dnsPropagationCheck) [ "--dns.disable-cp" ] ++ concatLists (mapAttrsToList (name: root: [ "-d" name ]) data.extraDomains) From ac983cff48b70c82c6984895c7d7558d7a1bb26a Mon Sep 17 00:00:00 2001 From: Lucas Savva Date: Sun, 9 Feb 2020 02:09:34 +0000 Subject: [PATCH 7/9] nixos/acme: add dns-01 test, fix cert locating bug --- nixos/modules/security/acme.nix | 11 +- nixos/tests/acme.nix | 118 ++++++++++++++++++---- nixos/tests/common/letsencrypt/common.nix | 3 + 3 files changed, 111 insertions(+), 21 deletions(-) diff --git a/nixos/modules/security/acme.nix b/nixos/modules/security/acme.nix index 907d909ecf15..66bc9d8d5058 100644 --- a/nixos/modules/security/acme.nix +++ b/nixos/modules/security/acme.nix @@ -316,23 +316,26 @@ in ''; ExecStartPost = let + keyName = builtins.replaceStrings ["*"] ["_"] data.domain; script = pkgs.writeScript "acme-post-start" '' #!${pkgs.runtimeShell} -e cd ${apath} # Test that existing cert is older than new cert - KEY=${spath}/certificates/*${data.domain}.key + KEY=${spath}/certificates/${keyName}.key if [ -e $KEY -a $KEY -nt key.pem ]; then - cp -p ${spath}/certificates/*${data.domain}.key key.pem - cp -p ${spath}/certificates/*${data.domain}.crt cert.pem - cp -p ${spath}/certificates/*${data.domain}.issuer.crt chain.pem + cp -p ${spath}/certificates/${keyName}.key key.pem + cp -p ${spath}/certificates/${keyName}.crt cert.pem + cp -p ${spath}/certificates/${keyName}.issuer.crt chain.pem cat cert.pem chain.pem > fullchain.pem cat key.pem cert.pem chain.pem > full.pem chmod ${rights} *.pem chown '${data.user}:${data.group}' *.pem fi + echo post stuff ${data.postRun} + echo done ''; in "+${script}"; diff --git a/nixos/tests/acme.nix b/nixos/tests/acme.nix index 6bd315ff1eaa..480e95e67c76 100644 --- a/nixos/tests/acme.nix +++ b/nixos/tests/acme.nix @@ -1,17 +1,51 @@ let commonConfig = ./common/letsencrypt/common.nix; + + dnsScript = {writeScript, dnsAddress, bash, curl}: writeScript "dns-hook.sh" '' + #!${bash}/bin/bash + set -euo pipefail + echo '[INFO]' "[$2]" 'dns-hook.sh' $* + if [ "$1" = "present" ]; then + ${curl}/bin/curl --data '{"host": "'"$2"'", "value": "'"$3"'"}' http://${dnsAddress}:8055/set-txt + else + ${curl}/bin/curl --data '{"host": "'"$2"'"}' http://${dnsAddress}:8055/clear-txt + fi + ''; + in import ./make-test-python.nix { name = "acme"; nodes = rec { - letsencrypt = ./common/letsencrypt; + letsencrypt = { nodes, lib, ... }: { + imports = [ ./common/letsencrypt ]; + networking.nameservers = lib.mkForce [ + nodes.dnsserver.config.networking.primaryIPAddress + ]; + }; - acmeStandalone = { config, pkgs, ... }: { + dnsserver = { nodes, pkgs, ... }: { + networking.firewall.allowedTCPPorts = [ 8055 53 ]; + networking.firewall.allowedUDPPorts = [ 53 ]; + systemd.services.pebble-challtestsrv = { + enable = true; + description = "Pebble ACME challenge test server"; + requires = [ ]; + wantedBy = [ "network.target" ]; + serviceConfig = { + ExecStart = "${pkgs.pebble}/bin/pebble-challtestsrv -dns01 ':53' -defaultIPv6 '' -defaultIPv4 '${nodes.webserver.config.networking.primaryIPAddress}'"; + # Required to bind on privileged ports. + User = "root"; + Group = "root"; + }; + }; + }; + + acmeStandalone = { nodes, lib, config, pkgs, ... }: { imports = [ commonConfig ]; + networking.nameservers = lib.mkForce [ + nodes.dnsserver.config.networking.primaryIPAddress + ]; networking.firewall.allowedTCPPorts = [ 80 ]; - networking.extraHosts = '' - ${config.networking.primaryIPAddress} standalone.com - ''; security.acme = { server = "https://acme-v02.api.letsencrypt.org/dir"; certs."standalone.com" = { @@ -29,14 +63,12 @@ in import ./make-test-python.nix { }; }; - webserver = { config, pkgs, ... }: { + webserver = { nodes, config, pkgs, lib, ... }: let parentConfig = config; in { imports = [ commonConfig ]; networking.firewall.allowedTCPPorts = [ 80 443 ]; - - networking.extraHosts = '' - ${config.networking.primaryIPAddress} a.example.com - ${config.networking.primaryIPAddress} b.example.com - ''; + networking.nameservers = lib.mkForce [ + nodes.dnsserver.config.networking.primaryIPAddress + ]; # A target remains active. Use this to probe the fact that # a service fired eventhough it is not RemainAfterExit @@ -61,10 +93,6 @@ in import ./make-test-python.nix { nesting.clone = [ ({pkgs, ...}: { - - networking.extraHosts = '' - ${config.networking.primaryIPAddress} b.example.com - ''; systemd.targets."acme-finished-b.example.com" = {}; systemd.services."acme-b.example.com" = { wants = [ "acme-finished-b.example.com.target" ]; @@ -79,15 +107,48 @@ in import ./make-test-python.nix { ''; }; }) + ({pkgs, config, nodes, lib, ...}: { + security.acme.certs."example.com" = { + domain = "*.example.com"; + dnsProvider = "exec"; + dnsPropagationCheck = false; + credentialsFile = with pkgs; writeText "wildcard.env" '' + EXEC_PATH=${dnsScript { inherit writeScript bash curl; dnsAddress = nodes.dnsserver.config.networking.primaryIPAddress; }} + ''; + user = config.services.nginx.user; + group = config.services.nginx.group; + }; + systemd.targets."acme-finished-example.com" = {}; + systemd.services."acme-example.com" = { + wants = [ "acme-finished-example.com.target" ]; + before = [ "acme-finished-example.com.target" "nginx.service" ]; + wantedBy = [ "nginx.service" ]; + }; + services.nginx.virtualHosts."c.example.com" = { + forceSSL = true; + sslCertificate = config.security.acme.certs."example.com".directory + "/cert.pem"; + sslTrustedCertificate = config.security.acme.certs."example.com".directory + "/full.pem"; + sslCertificateKey = config.security.acme.certs."example.com".directory + "/key.pem"; + locations."/".root = pkgs.runCommand "docroot" {} '' + mkdir -p "$out" + echo hello world > "$out/index.html" + ''; + }; + }) ]; }; - client = commonConfig; + client = {nodes, lib, ...}: { + imports = [ commonConfig ]; + networking.nameservers = lib.mkForce [ + nodes.dnsserver.config.networking.primaryIPAddress + ]; + }; }; testScript = {nodes, ...}: let - newServerSystem = nodes.webserver2.config.system.build.toplevel; + newServerSystem = nodes.webserver.config.system.build.toplevel; switchToNewServer = "${newServerSystem}/bin/switch-to-configuration test"; in # Note, wait_for_unit does not work for oneshot services that do not have RemainAfterExit=true, @@ -97,6 +158,17 @@ in import ./make-test-python.nix { # can use them to probe that a oneshot fired. It is a bit ugly, but it is the best we can do '' client.start() + dnsserver.start() + + letsencrypt.wait_for_unit("default.target") + dnsserver.wait_for_unit("pebble-challtestsrv.service") + client.succeed( + 'curl --data \'{"host": "acme-v02.api.letsencrypt.org", "addresses": ["${nodes.letsencrypt.config.networking.primaryIPAddress}"]}\' http://${nodes.dnsserver.config.networking.primaryIPAddress}:8055/add-a' + ) + client.succeed( + 'curl --data \'{"host": "standalone.com", "addresses": ["${nodes.acmeStandalone.config.networking.primaryIPAddress}"]}\' http://${nodes.dnsserver.config.networking.primaryIPAddress}:8055/add-a' + ) + letsencrypt.start() acmeStandalone.start() @@ -129,5 +201,17 @@ in import ./make-test-python.nix { client.succeed( "curl --cacert /tmp/ca.crt https://b.example.com/ | grep -qF 'hello world'" ) + + with subtest("Can request wildcard certificates using DNS-01 challenge"): + webserver.succeed( + "${switchToNewServer}" + ) + webserver.succeed( + "/run/current-system/fine-tune/child-2/bin/switch-to-configuration test" + ) + webserver.wait_for_unit("acme-finished-example.com.target") + client.succeed( + "curl --cacert /tmp/ca.crt https://c.example.com/ | grep -qF 'hello world'" + ) ''; } diff --git a/nixos/tests/common/letsencrypt/common.nix b/nixos/tests/common/letsencrypt/common.nix index c530de817bf2..bd559c8dacc5 100644 --- a/nixos/tests/common/letsencrypt/common.nix +++ b/nixos/tests/common/letsencrypt/common.nix @@ -5,5 +5,8 @@ in { nodes.letsencrypt.config.networking.primaryIPAddress ]; + security.acme.acceptTerms = true; + security.acme.email = "webmaster@example.com"; + security.pki.certificateFiles = [ letsencrypt-ca ]; } From 636eb23157554af622f087f04ef0566853473d7a Mon Sep 17 00:00:00 2001 From: Lucas Savva Date: Sun, 9 Feb 2020 11:34:17 +0000 Subject: [PATCH 8/9] nixos/acme: Fix b.example.com test --- nixos/modules/security/acme.nix | 2 -- nixos/tests/acme.nix | 3 ++- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/nixos/modules/security/acme.nix b/nixos/modules/security/acme.nix index 66bc9d8d5058..7da6666f79c6 100644 --- a/nixos/modules/security/acme.nix +++ b/nixos/modules/security/acme.nix @@ -333,9 +333,7 @@ in chown '${data.user}:${data.group}' *.pem fi - echo post stuff ${data.postRun} - echo done ''; in "+${script}"; diff --git a/nixos/tests/acme.nix b/nixos/tests/acme.nix index 480e95e67c76..617dafee0dce 100644 --- a/nixos/tests/acme.nix +++ b/nixos/tests/acme.nix @@ -63,7 +63,7 @@ in import ./make-test-python.nix { }; }; - webserver = { nodes, config, pkgs, lib, ... }: let parentConfig = config; in { + webserver = { nodes, config, pkgs, lib, ... }: { imports = [ commonConfig ]; networking.firewall.allowedTCPPorts = [ 80 443 ]; networking.nameservers = lib.mkForce [ @@ -97,6 +97,7 @@ in import ./make-test-python.nix { systemd.services."acme-b.example.com" = { wants = [ "acme-finished-b.example.com.target" ]; before = [ "acme-finished-b.example.com.target" ]; + after = [ "nginx.service" ]; }; services.nginx.virtualHosts."b.example.com" = { enableACME = true; From d8e697b4fcfd929d05221ac3e67b9c04ac69df86 Mon Sep 17 00:00:00 2001 From: Lucas Savva Date: Sun, 9 Feb 2020 15:59:03 +0000 Subject: [PATCH 9/9] nixos/acme: update release notes for 20.03 --- nixos/doc/manual/release-notes/rl-2003.xml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/nixos/doc/manual/release-notes/rl-2003.xml b/nixos/doc/manual/release-notes/rl-2003.xml index 51f91268eff0..37ac4ec02881 100644 --- a/nixos/doc/manual/release-notes/rl-2003.xml +++ b/nixos/doc/manual/release-notes/rl-2003.xml @@ -441,6 +441,22 @@ users.users.me = now uses the short rather than full version string. + + + The ACME module has switched from simp-le to lego + which allows us to support DNS-01 challenges and wildcard certificates. The following options have been added: + security.acme.acceptTerms, + security.acme.certs.<name>.dnsProvider, + security.acme.certs.<name>.credentialsFile, + security.acme.certs.<name>.dnsPropagationCheck. + As well as this, the options security.acme.acceptTerms and either + security.acme.email or security.acme.certs.<name>.email + must be set in order to use the ACME module. + Certificates will be regenerated from new on the next renewal date. The credentials for simp-le are + preserved and thus it is possible to roll back to previous versions without breaking certificate + generation. + +