pfSense as Name Server (bind9) with Let's Encrypt/acme DNS-NSupdate/RFC 2136

For a while now I’ve wanted to try to set up a self-contained name server and certificate authority. pfSense seems like an obvious choice since it has bind9 and acme packages.

So far I have been able to:

  1. Deploy pfSense
  2. Install bind and acme packages
  3. Set some A records in bind
  4. Configure the pfSense public IP as the name server for a domain
  5. Configure acme to register a certificate via nsupdate/rf2136

However, acme errors out when attempting to update/add the txt record.

adding _acme-challenge.domain.tld. 60 in txt "blahblah"
; TSIG error with server: tsig indicates error
update failed: NOTAUTH(BADKEY)

Configured in the gui of course, but relevant section of /cf/named/etc/namedb/named.conf is:

view "public" {
        recursion no;
        match-clients { any; };
        allow-recursion { none; };
        key "domain.tld" {
            algorithm hmac-md5;
            secret "REDACTED";
        };

        zone "domain.tld" {
                type master;
                file "/etc/namedb/master/public/domain.tld.DB";
                allow-query { any; };
                allow-transfer { none; };
                update-policy { grant localhost. wildcard *.domain.tld. ANY; };
        };

        zone "." {
                type hint;
                file "/etc/namedb/named.root";
        };

};

Relevant part of the acme config is:


Looks like this person had the same issue:

2 Likes

I got it.

acme config needed Key Name: field to be set to domain.tld which would have been obvious had I taken the time to read the field description.

Also, I didn’t really understand the update-policy parameters. This is the most restrictive option. Although alternatively, I think you could make a dedicated key for _acme-challenge.

grant domain.tld. subdomain _acme-challenge.domain.tld. TXT;

One confusing thing here is that the identity field after grant corresponds to the key name and not a resolving A record. For instance, the above works even when domain.tld has no A record at all as long as the key is called domain.tld.

allow-update localhost; will also work fine, but depending on how you want to utilize this dns server, using update-policy will offer more flexibility.

Also, it’s worth noting that this is not how bind intends you to do ddns from localhost. You are supposed to specify update-policy local; which generates a key at /var/run/named/session.key with a key name of local-ddns. Unfortunately, there is no way to configure acme to use this key in pfsense (as far as I can tell at least). However, this is more permissive than my config above (equivalent to update-policy {grant local-ddns zonesub any;};).


Next things to try:

  1. Enable DNSSEC
  2. Try a better algorithm for the key than md5
  3. Reverse zone
  4. Setup an intermediate CA

This site was helpful for me:

https://www.zytrax.com/books/dns/ch7/xfer.html

Set to auto, done.

The FreeBSD man page says:

Currently, the only supported encryption algorithm for TSIG is HMAC-MD5, which is defined in RFC 2104. Once other algorithms are defined for TSIG, applications will need to ensure they select the appropriate algorithm as well as the key when authenticating each other.

Search was bringing up a man page from FreeBSD 8, so nvm.

However, generating a key via tsig-keygen -a hmac-sha512 domain.tld works with nsupdate -y 'hmac-sha512:domain.tld:key so idk if the doc is out of date or maybe pfsense has a more recent version of nsupdate installed…

Anyway, sha512 works, so cool…

So acme does save an intermedia ca to /tmp/acme/domain.tld/domain.tld/ca.cer but this does not appear in the certificate management area of the gui, so you can’t use it to sign other certs. I guess that’s outside the scope of pfsense though, so maybe the CA functionality should go on a vm with certmonger or even plug it into FreeIPA somehow?

So my understanding of public intermediate CAs was lacking here. There’s obviously no way let’s encrypt would give you something like that. I think I was confusing it with simply distributing wildcard certs. I had only dealt with them on self-signed systems up to this point. I assumed that it was possible to limit trust of an intermediate CA to a specific domain, but that is not the case.

Anyway, I was able to configure split dns in bind with views after some trial and error. While split dns isn’t ideal, it is necessary if you want to use let’s encrypt wildcard certs for private servers (without publicly disclosing the names and private addresses of those servers). For instance if you have a subdomain stor.domain.tld with nas01.stor.domain.tld, etc, you’ll need a public TXT record (or CNAME) at _acme-challenge.stor.domain.tld. This is required to obtain a *.stor.domain.tld cert. This means that you need a public stor.domain.tld zone with a _acme-challenge.stor.domain.tld record and a private zone with a records for your servers (nas01.stor.domain.tld etc).

Luckily this is configured easily in pfsense, and it is possible to have each _acme-challenge record CNAME’d to a single address via the --challenge-alias flag. This allows you to only configure one ddns key and address which is a relief if you use a lot of subdomains internally.