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).

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.

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.

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.

If you've installed a previous version (before version 0.8) of acme-dns, and activated HTTPS for the API, you must remove the DNS CNAME record you created for _acme-challenge.acme.example.com. Acme-dns now handles its own certificate using DNS validation, but this record will conflict with that process.

Install the danb35 repo, then run:

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


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, replacing with your public IP address. Then do signal-event nethserver-acme-dns-update.


TLS Certificate

By default, this module is configured to obtain a TLS certificate from the Let's Encrypt staging server, in order to avoid exceeding the Let's Encrypt rate limits. To ensure that a certificate has been issued, run openssl s_client -connect localhost:8675. At the beginning of the output, you should see this:

[root@neth staging]# openssl s_client -connect acme.example.com:8675
depth=1 CN = Fake LE Intermediate X1
verify error:num=20:unable to get local issuer certificate
Certificate chain
 0 s:/CN=acme.example.com
   i:/CN=Fake LE Intermediate X1
 1 s:/CN=Fake LE Intermediate X1
   i:/CN=Fake LE Root X1

The inclusion of “Fake LE Root X1” and “Fake LE Intermediate X1” in the certificate chain shows that acme-dns is using a certificate from the staging environment, which further shows that all the steps in the issuance process are working. To change to using a “production” certificate, run config setprop acme-dns-api TLSType letsencrypt followed by signal-event nethserver-acme-dns-update. Then re-run the openssl command from above. The output should now look like this:

[root@neth ~]# openssl s_client -connect localhost:8675
depth=2 O = Digital Signature Trust Co., CN = DST Root CA X3
verify return:1
depth=1 C = US, O = Let's Encrypt, CN = Let's Encrypt Authority X3
verify return:1
depth=0 CN = acme.example.com
verify return:1
Certificate chain
 0 s:/CN=acme.example.com
   i:/C=US/O=Let's Encrypt/CN=Let's Encrypt Authority X3
 1 s:/C=US/O=Let's Encrypt/CN=Let's Encrypt Authority X3
   i:/O=Digital Signature Trust Co./CN=DST Root CA X3

The presence of “Let's Encrypt Authority X3” shows that you now have a certificate from the production server.

Testing the API

To confirm that your acme-dns instance is up and running, run curl -s -X POST https://acme.example.com: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.

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 https://acme.example.com:8675. No other changes need to be made.

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
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.

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:

/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.

  • userguide/let_s_encrypt_acme-dns.txt
  • Last modified: 2020/03/19 13:17
  • by Marc