Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
catern opened this issue Sep 15, 2021 · 8 comments
Open

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

catern opened this issue Sep 15, 2021 · 8 comments

Comments

@catern
Copy link

catern commented Sep 15, 2021

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
Copy link
Collaborator

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
Copy link
Author

catern commented Sep 15, 2021

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:

  • if I include my DNS records (with dns.nix and nsd) and lego (with security.acme) in my configuration.nix, then when lego runs, I don't have a chance to modify my dns.nix records to add the TXT record based on the Let's Encrypt challenge.
  • However, if I move lego out and run it manually, I don't have the normal NixOS integration. (and it's actually kind of painful to do, especially because I can't just copy out the lego command from the unit generated by NixOS, because it uses various custom systemd directives for sandboxing, for better or for worse)

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
Copy link
Author

catern commented Sep 15, 2021

$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
Copy link
Author

catern commented Sep 19, 2021

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 NixOS/nixpkgs#138478 about one of the prerequisites for that.

@m1cr0man
Copy link

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
Copy link
Author

catern commented Nov 24, 2021

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
Copy link

m1cr0man commented Dec 9, 2021

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
Copy link
Author

catern commented Dec 9, 2021

@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: PowerDNS/pdns#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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants