Let's Encrypt and acme-dns (or, wildcard certificates with any DNS provider)

In early 2018, Let's Encrypt began issuing wildcard certificates, after significant popular demand. One significant limitation, though, is that users must validate domain control using the DNS-01 challenge, by adding certain DNS TXT records. As a practical matter, that requires that you use a DNS provider with a supported API for automatic updates.

Many DNS providers provide APIs, but many more do not. Of the ones that do, not all are supported by popular ACME clients. And of the supported APIs, almost all are far too powerful–if an attacker were to compromise your API credentials, he could make any changes he wanted to your DNS records, potentially including stealing your domain entirely.

Enter acme-dns. acme-dns is a limited-purpose DNS server, whose only purpose is to serve the DNS TXT records needed for Let's Encrypt validation. It's a lightweight application, and offers an API that ACME clients can use to automatically create and destroy those TXT records. It automatically generates credentials that are only valid for a single subdomain. Between this and the fact that acme-dns will only serve the TXT records used for Let's Encrypt validation, in the worst case, an attacker who compromised your acme-dns API credentials would only be able to mis-issue a certificate for one of your (sub)domains. This still isn't a good thing by any means, but the exposure is far less than with most other DNS APIs.

The effect of this is that if you can create NS and CNAME records on your DNS provider, you can then use acme-dns to automate the DNS updates needed to get and renew your certificates (whether wildcard or not).

Background and Theory

acme-dns will act as the authoritative DNS server for a subdomain of your domain. If your domain is example.com, that subdomain will be acme.example.com. The acme-dns software will generate random hostnames within this subdomain (one random hostname for each FQDN you want to obtain a cert for), of the form 32f5274d-51e3-466d-bf38-eb9980e7bcf3.acme.example.com. You'll add a CNAME record for _acme-challenge.example.com, pointing to the random hostname. When Let's Encrypt tries to validate domain control over example.com, it'll look for DNS records for _acme-challenge.example.com, see the CNAME to 32f5274d-51e3-466d-bf38-eb9980e7bcf3.acme.example.com, and then query for text records for _acme-challenge.32f5274d-51e3-466d-bf38-eb9980e7bcf3.acme.example.com. At that point, acme-dns will serve the challenge that your client told it to serve, validation will succeed, and Let's Encrypt will issue the cert.

Note: Once you've set up acme-dns, you can use it for any domain you control. In other words, you can use a single instance to validate control over any number of domains or subdomains, just by setting individual CNAME records for each desired (sub)domain.

Conventions

Throughout this guide, your domain is represented as example.com. If you have more than one domain, pick one. The external IP address of your Neth server is represented as $EXTERNAL_IP. Make appropriate substitutions below.

DNS Configuration

You'll need to start by publishing the DNS records establishing your acme-dns instance as the authoritative nameserver for acme.example.com. To do this, log in to your DNS provider and add the records below:

ns1.acme.example.com	A	$EXTERNAL_IP
ns2.acme.example.com	A	$EXTERNAL_IP
acme.example.com		NS	ns1.acme.example.com
acme.example.com		NS	ns2.acme.example.com

You're done with DNS records for the time being.

Installing acme-dns

Install the danb35 repo, then run:

yum --enablerepo=danb35 install nethserver-acme-dns

Configuration

The nethserver-acme-dns RPM should configure everything properly, with the possible exception of your external IP address. If all of the following are true, then the templates should set this correctly:

  • You have one, and only one, red interface
  • Your red interface is configured to use a static IP address
  • Your red interface is configured to use a public static IP address
  • The public static IP address on your red interface is reachable from the Internet

If any of these is not true, you’ll need to set the address manually. To do this, do config setprop acme-dns ExternalIP 1.2.3.4, replacing 1.2.3.4 with your public IP address. Then do signal-event nethserver-acme-dns-update.

Testing

To confirm that your acme-dns instance is up and running, run curl -s -X POST http://localhost:8675/register | python -m json.tool. You should get something like this as your output:

{
    "allowfrom": [],
    "fulldomain": "44255c4e-d669-41f3-a141-672a8bd859e6.acme.example.com",
    "password": "x_Trpa04HpgQ4_ZOY7LCF6z23kf6o8i-VV_4qQk4",
    "subdomain": "44255c4e-d669-41f3-a141-672a8bd859e6",
    "username": "cc2d8066-2583-4e2c-a68f-ca45810c4f31"
}

Installation of acme-dns is now complete.

The hook script

Certbot doesn’t know, on its own, how to set DNS entries on acme-dns. Fortunately, the author of acme-dns has provided a script to handle this. Download the script by doing curl -o /etc/letsencrypt/acme-dns-auth.py https://raw.githubusercontent.com/joohoi/acme-dns-certbot-joohoi/master/acme-dns-auth.py followed by chmod 0700 /etc/letsencrypt/acme-dns-auth.py.

You’ll need to edit the script. Near the top, change ACMEDNS_URL to http://localhost:8675. No other changes need to be made.

Issuing the certificate

All the prep work is now done, and it’s time to issue the certificate. Run

certbot certonly --manual --manual-auth-hook /etc/letsencrypt/acme-dns-auth.py \
   --preferred-challenges dns --debug-challenges --post-hook "/sbin/e-smith/signal-event certificate-update" \
   -d example.com -d \*.example.com

If you want more domains, add them with additional -d flags at the end. Certbot will ask you a few questions (email address, agree to TOS, share email with EFF, log IP address), and then show a message like this:

Output from acme-dns-auth.py:
Please add the following CNAME record to your main DNS zone:
_acme-challenge.example.com CNAME 32f5274d-51e3-466d-bf38-eb9980e7bcf3.acme.example.com.

Waiting for verification...

-------------------------------------------------------------------------------
Challenges loaded. Press continue to submit to CA. Pass "-v" for more info about
challenges.
-------------------------------------------------------------------------------
Press Enter to Continue

You’ll need to once again log into your DNS host and add the record specified. You'll only need to do this once for each hostname. Wait a few minutes, then press Enter.

As noted above, once you have acme-dns running, you can use it to validate any domain you control, as long as you can set the appropriate CNAME records. But if you're going to be accessing its API remotely, you should really secure its API using HTTPS.

Get a certificate

We just created a wildcard certificate above, so we could use that, but it's better practice to use a unique certificate for this service. To issue that certificate, run the following:

certbot certonly --manual --manual-auth-hook /etc/letsencrypt/acme-dns-auth.py \
   --preferred-challenges dns --debug-challenges --post-hook "systemctl restart acme-dns" \
   -d acme.example.com

Also as above, you’ll be told to create a CNAME record with your DNS host. Do that, wait a couple of minutes, then press Enter. Your certificate will be generated.

Configure acme-dns for HTTPS

You’ll need to make just a few configuration changes:

config setprop acme-dns-api FullchainPath /etc/letsencrypt/live/acme.example.com/fullchain.pem
config setprop acme-dns-api KeyPath /etc/letsencrypt/live/acme.example.com/privkey.pem
config setprop acme-dns-api access red,green
signal-event nethserver-acme-dns-update

Configure the hook script

You’ll also need to update the hook script. Edit /etc/letsencrypt/acme-dns-auth.py and change ACMEDNS_URL to https://acme.example.com:8675, then save and exit.

Part of the reason for using Let's Encrypt is that you can automate issuance and renewal of your certificates. To do this, you'll need to create a simple cron job. Using your favorite text editor, create /etc/cron.daily/certbot with the following contents:

#!/bin/sh
/usr/bin/certbot renew --quiet

Certbot will run every day, and when one of its certificates is within 30 days of expiration, will attempt to renew it.

You’re done. You’ve made your acme-dns API available via HTTPS over the Internet and your LAN, so you can now make requests for zone updates from other hosts. You can run certbot on those machines, or any other client that knows how to update records using acme-dns; acme.sh is another such client.

Note that, in this configuration, anyone on the Internet can access the API of your acme-dns instance. If the other hosts that might be using it are on your LAN, you might want to change the access property above to just green rather than red,green.