Automate Let's Encrypt DNS Challenge with Certbot and Gandi.net
It’s always recommended to view web pages through HTTPS connections, even it’s just a static HTML page. So, as a content provider, it’s my duty to host websites with HTTPS. To enable HTTPS on the web server like Apache or Nginx, valid certificates are required. In my case, I have bought and configured a domain name on Gandi.net for my home cluster. It’s better to have different certificates for each service than having a single wildcard certificate for all the services due to security concerns. However, I still use wildcard certificate for one reason (I’ll talk about it later). So in this article I’m going to explain how to get TLS wildcard certificates with Let’s Encrypt using DNS validation.
How DNS Validation of ACME Protocol Works
Let’s Encrypt is a well-known open project and nonprofit certificate authority that provides TLS certificates to hundreds of thousands of websites around the world. Let’s Encrypt uses the ACME (Automatic Certificate Management Environment) protocol to verify that one controls a given domain name and to issue a certificate. There are mainly two ways to do that:
- HTTP validation
- DNS validation
Also, there are two types of domain name:
- Fully-qualified domain names
- Wildcard domain names
It’s worth mentioning that currently, in API version 2, the only one way to get certificates for wildcard domain names is through DNS validation. And I’m going to get certificates for my services running inside of a private network, which means I can only use DNS validation since the domain names I configured for my services are not publicly reachable. Those DNS A records are mapped to private IP addresses, so the HTTP validation is not applicable.
I know it’s not a good practice to expose your internal network architecture through registering public DNS records for private IP addresses. It leaks a great amount of valued information of your environment. But I have to say, it’s super fuxking convenient! A legit way to do that is to have your own private DNS service which serves the private DNS records, and use it internally.
So, how does DNS validation of ACME protocol work? It’s basically done by manipulating TXT records. If you know how HTTP validation works (you should!), it’s the same. It makes you put a specific value in a TXT record so that you can prove the ownership of the domain name you’re requesting for a certificate. After the validation is done, you clean up the TXT record. The website of Let’s Encrypt has a good explaination of it.
Certbot Installation
First we need to install ACME client software to help us get the certificates. There’re various implementation out there, and we choose the recommended one, which is certbot.
sudo apt update
sudo apt install software-properties-common
sudo add-apt-repository universe
sudo add-apt-repository ppa:certbot/certbot
sudo apt update
sudo apt install certbot
Now the software has been installed, we should be able to get certificates easily. For DNS validation, it will guide us to get the certificate in an interactive way. I’ll skip that since it’s pretty straightforward.
You Need Automation: Hooks
After going through the process of DNS validation manually we noticed that it’s just a pain in the ass. The steps are trivial and time-consuming. There must be a way to automate that. And you’re right!
You can use one of certbot’s DNS plugins to achieve this. But unfortunately, my DNS provider, Gandi.net, does not provide a usable plugin.
By the time of writing, there’s a third-party plugin which is not officially backed by Gandi.net. I’ll give it a shot in the future. The value of this article is to show you how to do the automation with the APIs provided by Gandi.net and integrates it with certbot.
The way of plugin is not working, though. Certbot supports pre and post
validation hooks when running in manual mode. The hooks are external scripts
executed by certbot to perform the tasks related to DNS validation. It includes
two command line options --manual-auth-hook
and --manual-cleanup-hook
. Both
flags should be filled with the path to the hook scripts respectively. The main
idea is to place the ACME challenge to TXT record using Gandi.net’s LiveDNS
API. And of course, to clean up the TXT record when the validation is done.
Here’s the auth hook authenticator.sh
written in Bash:
#!/usr/bin/env sh
APIKEY='your-api-key-here'
echo "${CERTBOT_DOMAIN}"
echo "${CERTBOT_VALIDATION}"
domain=$(echo "${CERTBOT_DOMAIN}" | rev | cut -d"." -f1-2 | rev)
subdomain=""
if [ "${domain}" = "${CERTBOT_DOMAIN}" ]; then
echo 'Same!'
else
echo 'Different!'
subdomain=$(echo "${CERTBOT_DOMAIN}" | rev | cut -d"." -f3- | rev)
subdomain_with_dot=".${subdomain}"
fi
result=$(curl -s -H "Authorization: Apikey $APIKEY" https://api.gandi.net/v5/livedns/domains/${domain}/records/_acme-challenge${subdomain_with_dot} | python -m json.tool)
if [ "${result}" = "[]" ]; then
echo 'Newly created!'
curl -s -X POST https://api.gandi.net/v5/livedns/domains/${domain}/records/_acme-challenge${subdomain_with_dot} \
-H "Authorization: Apikey $APIKEY" \
-H "Content-Type: application/json" \
--data '{"rrset_type": "TXT", "rrset_values": ["'${CERTBOT_VALIDATION}'"], "rrset_ttl": "300"}'
else
echo 'Append!'
previous_validation=$(echo "${result}" | python -c "import sys, json; print json.load(sys.stdin)[0]['rrset_values'][0]" | tr -d '"')
echo 'previsou_validation: '${previous_validation}
curl -s -X PUT https://api.gandi.net/v5/livedns/domains/${domain}/records/_acme-challenge${subdomain_with_dot} \
-H "Authorization: Apikey $APIKEY" \
-H "Content-Type: application/json" \
--data '{"items": [{"rrset_type": "TXT", "rrset_values": ["'${previous_validation}'", "'${CERTBOT_VALIDATION}'"], "rrset_ttl": "300"}]}'
fi
sleep 30
And the cleanup hook cleanup.sh
:
#!/usr/bin/env sh
APIKEY='your-api-key-here'
echo "${CERTBOT_DOMAIN}"
domain=$(echo "${CERTBOT_DOMAIN}" | rev | cut -d"." -f1-2 | rev)
subdomain=""
if [ "${domain}" = "${CERTBOT_DOMAIN}" ]; then
echo 'Same!'
else
echo 'Different!'
subdomain=$(echo "${CERTBOT_DOMAIN}" | rev | cut -d"." -f3- | rev)
subdomain_with_dot=".${subdomain}"
fi
curl -s -X DELETE https://api.gandi.net/v5/livedns/domains/${domain}/records/_acme-challenge${subdomain_with_dot} \
-H "Authorization: Apikey $APIKEY"
Just remember to put your valid Gandi.net API token into the scripts. I won’t cover that, either.
Giving them executable bit:
chmod +x authenticator.sh cleanup.sh
The work has been done. It’s time to integrate these hooks scripts with certbot itself.
Generating New Certificates
To generate a wildcard certificate for internal.zespre.com
, try this:
$ sudo certbot certonly --manual \
-d *.internal.zespre.com \
-d internal.zespre.com \
--manual-auth-hook authenticator.sh \
--manual-cleanup-hook cleanup.sh \
--preferred-challenges dns
Saving debug log to /var/log/letsencrypt/letsencrypt.log
Plugins selected: Authenticator manual, Installer None
Obtaining a new certificate
Performing the following challenges:
dns-01 challenge for internal.zespre.com
dns-01 challenge for internal.zespre.com
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NOTE: The IP of this machine will be publicly logged as having requested this
certificate. If you're running certbot in manual mode on a machine that is not
your server, please ensure you're okay with that.
Are you OK with your IP being logged?
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
(Y)es/(N)o: Y
Output from authenticator.sh:
internal.zespre.com
0DrYgNTYVzuMexfppEz03-bL0H8V91Z7qi1NHfefZgs
Different!
Newly created!
{"message":"DNS Record Created"}
Output from authenticator.sh:
internal.zespre.com
vaTN_2ze9AJmBeW1Pe3_NUKsnOBakus8sN8mHmYHAkE
Different!
Append!
previsou_validation: 0DrYgNTYVzuMexfppEz03-bL0H8V91Z7qi1NHfefZgs
{"message":"DNS Record Created"}
Waiting for verification...
Cleaning up challenges
Output from cleanup.sh:
internal.zespre.com
Different!
Output from cleanup.sh:
internal.zespre.com
Different!
IMPORTANT NOTES:
- Congratulations! Your certificate and chain have been saved at:
/etc/letsencrypt/live/internal.zespre.com/fullchain.pem
Your key file has been saved at:
/etc/letsencrypt/live/internal.zespre.com/privkey.pem
Your cert will expire on 2021-03-14. To obtain a new or tweaked
version of this certificate in the future, simply run certbot
again. To non-interactively renew *all* of your certificates, run
"certbot renew"
- If you like Certbot, please consider supporting our work by:
Donating to ISRG / Let's Encrypt: https://letsencrypt.org/donate
Donating to EFF: https://eff.org/donate-le
If you look into the output you’ll find that there are some messages about the process of the validation. Here’s a brief explanation:
Now that the newly generated private key and issued certificates are under the
directory /etc/letsencrypt/live/<your-domain>/
. Make good use of them!
Wrapping Up
In this article we have shown that how ACME DNS validation works, and adding automation to certificate generation with the ability of certbot validation hooks. So we can obtain wildcard certificates for our services running inside private network. Hope you like it!