summary refs log tree commit diff
path: root/nixos/tests/acme.nix
blob: 6bd315ff1eaa427b9477384bb4ff84ef66516041 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
let
  commonConfig = ./common/letsencrypt/common.nix;
in import ./make-test-python.nix {
  name = "acme";

  nodes = rec {
    letsencrypt = ./common/letsencrypt;

    acmeStandalone = { config, pkgs, ... }: {
      imports = [ commonConfig ];
      networking.firewall.allowedTCPPorts = [ 80 ];
      networking.extraHosts = ''
        ${config.networking.primaryIPAddress} standalone.com
      '';
      security.acme = {
        server = "https://acme-v02.api.letsencrypt.org/dir";
        certs."standalone.com" = {
            webroot = "/var/lib/acme/acme-challenges";
        };
      };
      systemd.targets."acme-finished-standalone.com" = {};
      systemd.services."acme-standalone.com" = {
        wants = [ "acme-finished-standalone.com.target" ];
        before = [ "acme-finished-standalone.com.target" ];
      };
      services.nginx.enable = true;
      services.nginx.virtualHosts."standalone.com" = {
        locations."/.well-known/acme-challenge".root = "/var/lib/acme/acme-challenges";
      };
    };

    webserver = { config, pkgs, ... }: {
      imports = [ commonConfig ];
      networking.firewall.allowedTCPPorts = [ 80 443 ];

      networking.extraHosts = ''
        ${config.networking.primaryIPAddress} a.example.com
        ${config.networking.primaryIPAddress} b.example.com
      '';

      # A target remains active. Use this to probe the fact that
      # a service fired eventhough it is not RemainAfterExit
      systemd.targets."acme-finished-a.example.com" = {};
      systemd.services."acme-a.example.com" = {
        wants = [ "acme-finished-a.example.com.target" ];
        before = [ "acme-finished-a.example.com.target" ];
      };

      services.nginx.enable = true;

      services.nginx.virtualHosts."a.example.com" = {
        enableACME = true;
        forceSSL = true;
        locations."/".root = pkgs.runCommand "docroot" {} ''
          mkdir -p "$out"
          echo hello world > "$out/index.html"
        '';
      };

      security.acme.server = "https://acme-v02.api.letsencrypt.org/dir";

      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" ];
            before = [ "acme-finished-b.example.com.target" ];
          };
          services.nginx.virtualHosts."b.example.com" = {
            enableACME = true;
            forceSSL = true;
            locations."/".root = pkgs.runCommand "docroot" {} ''
              mkdir -p "$out"
              echo hello world > "$out/index.html"
            '';
          };
        })
      ];
    };

    client = commonConfig;
  };

  testScript = {nodes, ...}:
    let
      newServerSystem = nodes.webserver2.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,
    # this is because a oneshot goes from inactive => activating => inactive, and never
    # reaches the active state. To work around this, we create some mock target units which
    # get pulled in by the oneshot units. The target units linger after activation, and hence we
    # can use them to probe that a oneshot fired. It is a bit ugly, but it is the best we can do
    ''
      client.start()
      letsencrypt.start()
      acmeStandalone.start()

      letsencrypt.wait_for_unit("default.target")
      letsencrypt.wait_for_unit("pebble.service")

      with subtest("can request certificate with HTTPS-01 challenge"):
          acmeStandalone.wait_for_unit("default.target")
          acmeStandalone.succeed("systemctl start acme-standalone.com.service")
          acmeStandalone.wait_for_unit("acme-finished-standalone.com.target")

      client.wait_for_unit("default.target")

      client.succeed("curl https://acme-v02.api.letsencrypt.org:15000/roots/0 > /tmp/ca.crt")
      client.succeed(
          "curl https://acme-v02.api.letsencrypt.org:15000/intermediate-keys/0 >> /tmp/ca.crt"
      )

      with subtest("Can request certificate for nginx service"):
          webserver.wait_for_unit("acme-finished-a.example.com.target")
          client.succeed(
              "curl --cacert /tmp/ca.crt https://a.example.com/ | grep -qF 'hello world'"
          )

      with subtest("Can add another certificate for nginx service"):
          webserver.succeed(
              "/run/current-system/fine-tune/child-1/bin/switch-to-configuration test"
          )
          webserver.wait_for_unit("acme-finished-b.example.com.target")
          client.succeed(
              "curl --cacert /tmp/ca.crt https://b.example.com/ | grep -qF 'hello world'"
          )
    '';
}