nix-community / dns.nix

A Nix DSL for DNS zone files [maintainers=@raitobezarius @kirelagin @Tom-Hubrecht]
Mozilla Public License 2.0
127 stars 24 forks source link

Difficult to use ACME/Let's Encrypt with dns.nix #22

Open catern opened 3 years ago

catern commented 3 years ago

First off this library is great, definitely a much nicer way to define DNS records, and I'd be thrilled if it was in core NixOS.

I see a mention of Let's Encrypt in the library; do you use DNS-based authentication with Let's Encrypt? I'm finding it rather difficult to set up with nsd and dns.nix, and if you do use DNS-based authentication, it would be useful to have an example of it.

kirelagin commented 3 years ago

Just to be clear, by “authentication”, do you mean the DNS-01 challenge used to prove the control of the domain? In this case, no, I don’t use it. The reason is that ACME is already supported by NixOS in a fully automatic way via the HTTP-01 challenge and I don’t see an easy way to have DNS-01 supported automatically.

However, I would love to know more about the specific difficulties that you are having. I was under the impression that fulfilling a DNS-01 challenge boils down to creating a TXT record requested by Let’s Encrypt, so I would expect it to be rather straightforward with dns.nix?

catern commented 3 years ago

Yes, I mean DNS-01. Yes, it seems tricky (although not impossible) to have DNS-01 supported automatically (although I don't think the HTTP challenge support is perfect - the automated support can be broken by something as simple and common as setting documentRoot)

My difficulty is this:

From searching around it looks like some people solve this by modifying zone files with various custom scripts, invoked by lego/certbot/etc. I think from a NixOS perspective, the ideal would be to have a file that lego could just overwrite with the TXT record, and then poke the DNS server to load/reload that file. I guess this is what I will try next, although the first way to do that that comes to mind would be to stick an $INCLUDE in the zonefile, and dns.nix doesn't support $INCLUDE it looks like.

catern commented 3 years ago

$INCLUDE is going to be a bit tricky since I'd need to let nsd break out of its sandbox, or bind mount a file in...

Maybe there's a DNS server that allows RFC2136 updates to be applied to a separate zone file from the main zone file? So the main zone file could be generated by dns.nix and then the Let's Encrypt updates could be done with RFC2136 in another zone file that is overlayed on the main one.

catern commented 3 years ago

OK, I eventually settled on an approach of completely overwriting a separate _acme-challenge zonefile, and then triggering a reload in the DNS server. I used powerdns because it's the only DNS server that I could find which has a Unix-socket-based control tool (everything else is localhost TCP with private keys).

Here's what I did:

  services.powerdns = {
    enable = true;
    extraConfig = let
      catern.com = pkgs.writeText "catern.com" (dns.lib.toString "catern.com" (with dns.lib.combinators; {
         my_domain_config
      }));
    in
      ''
    launch=bind
    bind-config=${pkgs.writeText "named.conf" ''
      zone "catern.com" { file "${catern.com}"; };
      zone "_acme-challenge.catern.com" { file "/var/db/bind/_acme-challenge.catern.com."; };
      ''}
    '';
  };
  systemd.services.pdns.serviceConfig.ExecStartPost = "${pkgs.coreutils}/bin/chmod g+w /var/run/pdns/pdns.controlsocket";
  security.acme = {
    acceptTerms = true;
    email = "spencerbaugh@gmail.com";
    certs."catern.com" = let
      update_script = pkgs.writeScript "acme_update_dns.sh" ''
        #!/bin/sh
        set -o errexit -o nounset
        mode="$1"
        record="$2"
        token="$3"
        if test "$mode" = "present";
        then cat >/var/db/bind/$record <<EOF
        $record IN 86400 SOA catern.com. spencerbaugh.gmail.com. ($(date +'%s') 86400 600 864000 60)
        $record IN 10 TXT "$token"
        EOF
        else echo >/var/db/bind/$record;
        fi
        ${pkgs.powerdns}/bin/pdns_control bind-reload-now _acme-challenge.catern.com
        '';
    in {
      domain = "catern.com";
      group = "wwwrun";
      dnsProvider = "exec";
      credentialsFile = pkgs.writeText "conf" "EXEC_PATH=${update_script}";
      dnsPropagationCheck = false;
    };
  };
  users.users.acme.extraGroups = ["pdns"];

This actually works really well. There's no setup required outside the NixOS configuration; no need to create things manually (well, I created /var/db/bind manually but that's a simple tmpfiles snippet).

I think this could be viably integrated into upstream NixOS, so that DNS-based challenges could be supported by NixOS completely automatically in the same way HTTP challenges are supported. I filed https://github.com/NixOS/nixpkgs/issues/138478 about one of the prerequisites for that.

m1cr0man commented 2 years ago

Hey @catern I saw NixOS/nixpkgs#138478 but not this ticket, and I figured I would follow up.

If you are using PowerDNS you should be able to configure Lego to use the PDNS API as a DNS backend as per the lego docs. Following this part of the NixOS manual, you would set your dnsProvider to pdns, and then set the appropriate environment variables in the credentialsFile (as per lego's docs). In this sense, DNS challenges are completely automated without extra scripting required 😃 Lego's vast DNS backend support is one of the main reasons we chose it.

I imagine this solves your request? It would be interesting to know if this works for you. You would still need your external zonefile, and that bit might be a worthy candidate for a PR to the pdns module. Personally I use Bind with RFC2136 to do wildcard certificates for my own domains. I have not tried using PowerDNS.

catern commented 2 years ago

Thanks for the heads up @m1cr0man

I looked into that before, but I didn't want to use the PDNS HTTP API because it requires generating a secret key and allocating a TCP port to the HTTP API. The same goes for the approach outlined in the NixOS manual: It requires generating a secret key.

The nice thing about my current approach is that there's no secret key required and no need to allocate a TCP port (since it's using a Unix socket). This allows it to be completely stateless.

m1cr0man commented 2 years ago

Hey @catern I took that as some feedback and I have updated the NixOS docs in NixOS/nixpkgs#147784 . The DNS-01 section now includes an example service which will generate the DNS keys on start rather than requiring manual intervention. As far as I know, you could totally configure lego to use a unix socket for the API too since I think the Go layers will handle that fine. I too prefer a system that is stateless and can be deployed on multiple hosts. :)

catern commented 2 years ago

@m1cr0man Wow, awesome change! That's much better!

Though, I still like that my approach avoids a secret key entirely in favor of Unix permissions on a Unix socket, and so is totally stateless. Regrettably, PowerDNS's HTTP API doesn't support going over a Unix socket: https://github.com/PowerDNS/pdns/issues/8677 and the API in PowerDNS which can go over a Unix socket (which is not the HTTP API), doesn't support changing DNS records.