summary refs log tree commit diff
diff options
context:
space:
mode:
authorLucas Savva <lucas@m1cr0man.com>2020-02-09 02:09:34 +0000
committerLucas Savva <lucas@m1cr0man.com>2020-02-09 02:09:34 +0000
commitac983cff48b70c82c6984895c7d7558d7a1bb26a (patch)
tree675dc3dd8c66023ea071ba23af7c3c871633fe1e
parent2181313c542364d63674ed34cbf69d7691ddfa0d (diff)
downloadnixpkgs-ac983cff48b70c82c6984895c7d7558d7a1bb26a.tar
nixpkgs-ac983cff48b70c82c6984895c7d7558d7a1bb26a.tar.gz
nixpkgs-ac983cff48b70c82c6984895c7d7558d7a1bb26a.tar.bz2
nixpkgs-ac983cff48b70c82c6984895c7d7558d7a1bb26a.tar.lz
nixpkgs-ac983cff48b70c82c6984895c7d7558d7a1bb26a.tar.xz
nixpkgs-ac983cff48b70c82c6984895c7d7558d7a1bb26a.tar.zst
nixpkgs-ac983cff48b70c82c6984895c7d7558d7a1bb26a.zip
nixos/acme: add dns-01 test, fix cert locating bug
-rw-r--r--nixos/modules/security/acme.nix11
-rw-r--r--nixos/tests/acme.nix118
-rw-r--r--nixos/tests/common/letsencrypt/common.nix3
3 files changed, 111 insertions, 21 deletions
diff --git a/nixos/modules/security/acme.nix b/nixos/modules/security/acme.nix
index 907d909ecf1..66bc9d8d505 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 6bd315ff1ea..480e95e67c7 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
+      ];
+    };
+
+    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 = { config, pkgs, ... }: {
+    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 c530de817bf..bd559c8dacc 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 ];
 }