Unverified Commit 7e820610 authored by Kira Bruneau's avatar Kira Bruneau Committed by GitHub
Browse files

Merge pull request #234207 from emilylange/acme-dns

acme-dns: init at 1.0; nixos/acme-dns: init; nixos/acme-dns: init
parents 075dad79 fa21828b
Loading
Loading
Loading
Loading
+2 −0
Original line number Diff line number Diff line
@@ -8,6 +8,8 @@

- Create the first release note entry in this section!

- [acme-dns](https://github.com/joohoi/acme-dns), a limited DNS server to handle ACME DNS challenges easily and securely. Available as [services.acme-dns](#opt-services.acme-dns.enable).

<!-- To avoid merge conflicts, consider adding your item at an arbitrary place in the list instead. -->

- [river](https://github.com/riverwm/river), A dynamic tiling wayland compositor. Available as [programs.river](#opt-programs.river.enable).
+1 −0
Original line number Diff line number Diff line
@@ -808,6 +808,7 @@
  ./services/network-filesystems/xtreemfs.nix
  ./services/network-filesystems/yandex-disk.nix
  ./services/networking/3proxy.nix
  ./services/networking/acme-dns.nix
  ./services/networking/adguardhome.nix
  ./services/networking/alice-lg.nix
  ./services/networking/amuled.nix
+154 −0
Original line number Diff line number Diff line
{ lib
, config
, pkgs
, ...
}:

let
  cfg = config.services.acme-dns;
  format = pkgs.formats.toml { };
  inherit (lib)
    literalExpression
    mdDoc
    mkEnableOption
    mkOption
    mkPackageOptionMD
    types
    ;
  domain = "acme-dns.example.com";
in
{
  options.services.acme-dns = {
    enable = mkEnableOption (mdDoc "acme-dns");

    package = mkPackageOptionMD pkgs "acme-dns" { };

    settings = mkOption {
      description = mdDoc ''
        Free-form settings written directly to the `acme-dns.cfg` file.
        Refer to <https://github.com/joohoi/acme-dns/blob/master/README.md#configuration> for supported values.
      '';

      default = { };

      type = types.submodule {
        freeformType = format.type;
        options = {
          general = {
            listen = mkOption {
              type = types.str;
              description = mdDoc "IP+port combination to bind and serve the DNS server on.";
              default = "[::]:53";
              example = "127.0.0.1:53";
            };

            protocol = mkOption {
              type = types.enum [ "both" "both4" "both6" "udp" "udp4" "udp6" "tcp" "tcp4" "tcp6" ];
              description = mdDoc "Protocols to serve DNS responses on.";
              default = "both";
            };

            domain = mkOption {
              type = types.str;
              description = mdDoc "Domain name to serve the requests off of.";
              example = domain;
            };

            nsname = mkOption {
              type = types.str;
              description = mdDoc "Zone name server.";
              example = domain;
            };

            nsadmin = mkOption {
              type = types.str;
              description = mdDoc "Zone admin email address for `SOA`.";
              example = "admin.example.com";
            };

            records = mkOption {
              type = types.listOf types.str;
              description = mdDoc "Predefined DNS records served in addition to the `_acme-challenge` TXT records.";
              example = literalExpression ''
                [
                  # replace with your acme-dns server's public IPv4
                  "${domain}. A 198.51.100.1"
                  # replace with your acme-dns server's public IPv6
                  "${domain}. AAAA 2001:db8::1"
                  # ${domain} should resolve any *.${domain} records
                  "${domain}. NS ${domain}."
                ]
              '';
            };
          };

          database = {
            engine = mkOption {
              type = types.enum [ "sqlite3" "postgres" ];
              description = mdDoc "Database engine to use.";
              default = "sqlite3";
            };
            connection = mkOption {
              type = types.str;
              description = mdDoc "Database connection string.";
              example = "postgres://user:password@localhost/acmedns";
              default = "/var/lib/acme-dns/acme-dns.db";
            };
          };

          api = {
            ip = mkOption {
              type = types.str;
              description = mdDoc "IP to bind the HTTP API on.";
              default = "[::]";
              example = "127.0.0.1";
            };

            port = mkOption {
              type = types.port;
              description = mdDoc "Listen port for the HTTP API.";
              default = 8080;
              # acme-dns expects this value to be a string
              apply = toString;
            };

            disable_registration = mkOption {
              type = types.bool;
              description = mdDoc "Whether to disable the HTTP registration endpoint.";
              default = false;
              example = true;
            };

            tls = mkOption {
              type = types.enum [ "letsencrypt" "letsencryptstaging" "cert" "none" ];
              description = mdDoc "TLS backend to use.";
              default = "none";
            };
          };


          logconfig = {
            loglevel = mkOption {
              type = types.enum [ "error" "warning" "info" "debug" ];
              description = mdDoc "Level to log on.";
              default = "info";
            };
          };
        };
      };
    };
  };

  config = lib.mkIf cfg.enable {
    systemd.packages = [ cfg.package ];
    systemd.services.acme-dns = {
      wantedBy = [ "multi-user.target" ];
      serviceConfig = {
        ExecStart = [ "" "${lib.getExe cfg.package} -c ${format.generate "acme-dns.toml" cfg.settings}" ];
        StateDirectory = "acme-dns";
        WorkingDirectory = "%S/acme-dns";
        DynamicUser = true;
      };
    };
  };
}
+50 −0
Original line number Diff line number Diff line
import ./make-test-python.nix ({ ... }: {
  name = "acme-dns";

  nodes.machine = { pkgs, ... }: {
    services.acme-dns = {
      enable = true;
      settings = {
        general = rec {
          domain = "acme-dns.home.arpa";
          nsname = domain;
          nsadmin = "admin.home.arpa";
          records = [
            "${domain}. A 127.0.0.1"
            "${domain}. AAAA ::1"
            "${domain}. NS ${domain}."
          ];
        };
        logconfig.loglevel = "debug";
      };
    };
    environment.systemPackages = with pkgs; [ curl bind ];
  };

  testScript = ''
    import json

    machine.wait_for_unit("acme-dns.service")
    machine.wait_for_open_port(53) # dns
    machine.wait_for_open_port(8080) # http api

    result = machine.succeed("curl --fail -X POST http://localhost:8080/register")
    print(result)

    registration = json.loads(result)

    machine.succeed(f'dig -t TXT @localhost {registration["fulldomain"]} | grep "SOA" | grep "admin.home.arpa"')

    # acme-dns exspects a TXT value string length of exactly 43 chars
    txt = "___dummy_validation_token_for_txt_record___"

    machine.succeed(
      "curl --fail -X POST http://localhost:8080/update "
      + f' -H "X-Api-User: {registration["username"]}"'
      + f' -H "X-Api-Key: {registration["password"]}"'
      + f' -d \'{{"subdomain":"{registration["subdomain"]}", "txt":"{txt}"}}\'''
    )

    assert txt in machine.succeed(f'dig -t TXT +short @localhost {registration["fulldomain"]}')
  '';
})
+1 −0
Original line number Diff line number Diff line
@@ -95,6 +95,7 @@ in {
  _3proxy = runTest ./3proxy.nix;
  aaaaxy = runTest ./aaaaxy.nix;
  acme = runTest ./acme.nix;
  acme-dns = handleTest ./acme-dns.nix {};
  adguardhome = runTest ./adguardhome.nix;
  aesmd = runTestOn ["x86_64-linux"] ./aesmd.nix;
  agate = runTest ./web-servers/agate.nix;
Loading