diff --git a/nix/module.nix b/nix/module.nix index 465fa9783..37cb4f200 100644 --- a/nix/module.nix +++ b/nix/module.nix @@ -426,6 +426,55 @@ in ''; example = [ "mydomain.internal" ]; }; + + certificates = { + enabled = lib.mkOption { + type = lib.types.bool; + default = false; + description = '' + Whether nodes can get TLS certificates for their MagicDNS name + with `tailscale cert`. Needs a public {option}`base_domain` and + {option}`dns.challenge.provider`. + ''; + }; + + zone = lib.mkOption { + type = lib.types.str; + default = ""; + description = '' + DNS zone the challenge records are written to. Empty means + {option}`base_domain`. + ''; + example = "example.com"; + }; + }; + + challenge.provider = lib.mkOption { + type = lib.types.nullOr ( + lib.types.submodule { + freeformType = settingsFormat.type; + options.name = lib.mkOption { + type = lib.types.str; + description = '' + DNS provider that writes ACME DNS-01 challenge records: + acmedns, cloudflare, gigahost, hetzner, powerdns or rfc2136. + ''; + }; + } + ); + default = null; + description = '' + DNS provider for ACME DNS-01 challenges. Every key besides + `name` is a setting of the provider; a text setting can be read + from a file by adding `_path` to its name. + ''; + example = lib.literalExpression '' + { + name = "cloudflare"; + api_token_path = "''${CREDENTIALS_DIRECTORY}/cloudflare-token"; + } + ''; + }; }; oidc = { @@ -677,6 +726,13 @@ in assertion = with cfg.settings; dns.override_local_dns -> dns.nameservers.global != [ ]; message = "dns.nameservers.global must be set when overriding local DNS"; } + { + assertion = + with cfg.settings; + dns.certificates.enabled + -> dns.magic_dns && dns.base_domain != "" && dns.challenge.provider != null; + message = "dns.certificates.enabled needs dns.magic_dns, dns.base_domain and dns.challenge.provider"; + } (assertRemovedOption [ "settings" "acl_policy_path" ] "Use `policy.path` instead.") (assertRemovedOption [ "settings" "db_host" ] "Use `database.postgres.host` instead.") (assertRemovedOption [ "settings" "db_name" ] "Use `database.postgres.name` instead.") diff --git a/nix/tests/headscale.nix b/nix/tests/headscale.nix index cc42ba1d0..700ff524d 100644 --- a/nix/tests/headscale.nix +++ b/nix/tests/headscale.nix @@ -24,13 +24,21 @@ in nodes = { headscale = { imports = [ self.nixosModules.testkit ]; - services.headscale.settings.dns.extra_records = [ - { - name = "foo.bar"; - type = "A"; - value = "100.64.0.2"; - } - ]; + services.headscale.settings.dns = { + extra_records = [ + { + name = "foo.bar"; + type = "A"; + value = "100.64.0.2"; + } + ]; + # Startup builds the provider but does not call it. + certificates.enabled = true; + challenge.provider = { + name = "rfc2136"; + server = "127.0.0.1:53"; + }; + }; }; # All of peer1's noise goes over the kit's TLS listener, not plain :80. peer1 = { @@ -56,6 +64,9 @@ in peer2.wait_until_succeeds("tailscale ping --until-direct=false -c 1 peer1.tailnet") res = peer1.wait_until_succeeds("${lib.getExe pkgs.dig} +short foo.bar").strip() assert res == "100.64.0.2", res + peer1.wait_until_succeeds( + "tailscale status --json | ${lib.getExe pkgs.jq} -e '.CertDomains == [\"peer1.tailnet\"]'" + ) headscale.succeed("headscale policy set -f ${policy}")