Internal Auto-Renewing LetsEncrypt Certificates

This is an old post!

This post is over 2 years old. Solutions referenced in this article may no longer be valid. Please consider this when utilizing any information referenced here.

I have a well-documented obsession with pretty URLs, and this extends even to my internal home network. I have way too much stuff bouncing around in my head to have to remember IP addresses when a domain name is much easier to remember.

LetsEncrypt launched to offer free SSL certificates to anyone, but the most crucial feature of their infrastructure, and one someone should have figured out before then, was scriptable automatically renewing certificates. Basically they validate you do in fact own the domain using automated methods, then issue you the new certificate. Thus, your certificates can be renewed on a schedule with no interaction from you.

Traditionally, they have done this by placing a file in the webroot and looking for that file before issuing the certificate (see my earlier blog post about Zero Downtime nginx Letsencrypt Certificate Renewals Without the nginx Plugin for more detail about this.)

But what happens when you want to issue an internal certificate? One for a service that is not accessible to the outside world, and thus, not visible using the webroot method? Well, it turns out there is a solution for that too!

Why?

Why would you want to use SSL certificates on a home network? Well, besides keeping my skills sharp and getting practice with new technologies, I write a number of internal Mac applications I use at home. I distribute these internally using Sparkle, a self-updating framework that Mac users should be pretty familiar with.

Sparkle requires updates to be distributed over SSL, even if they are local. So I needed an SSL certificate for that local host and domain.

Enter DNS

LetsEncrypt also supports an alternate method of domain validation using DNS entries. You can create a special TXT entry in your domain message using a string they generate. They then look for this TXT entry and, if they find it, issue the certificate. This is the DNS-01 challenge.

But this requires a manual step - you have to create the TXT entry each time you want to renew your certificates. Which defeats the purpose of making them auto renew. So, while this is a good way to get your certificates issued for internal services, it still needs another piece.

Enter Cloudflare

For many, many years I used Dyn for my internal DNS needs. I had a domain name for my house delegated to Dyn, and used the Dynamic DNS features in pfSense, as well as ddclient, to update it whenver the IP for my home Internet connection changed. This allows me to use VPN to reach the home no matter where I am.

In 2002 while still a student at Auburn, I signed up for a “lifetime” account with Dyn (back when they were still DynDNS) so that I could access my dorm computer from the labs.

After the Oracle acquisition, I knew it was only a matter of time until “lifetime” accounts died. And sure enough, earlier this year, Oracle announced that “lifetime” accounts were going away in May 2020. I guess I can’t really complain. If I recall, it was like $20 back in 2002, and I got 17 years out of that $20. One of the better payoffs for things I have purchased in my life.

So I migrated everything to Cloudflare over the weekend, since I already had an account with Cloudflare (that powers this blog). Crucially, Cloudflare is not a all-in type of service. You can opt to only use the DNS service of a domain, and that is what I did for my internal domain.

And, since Cloudflare has an API, using Cloudflare means that there is now an option for automatically creating those TXT entries. As it turns out, certbot already has a plugin for working directly with Cloudflare!

How To Do It

First, be sure you are using the most up-to-date code, which may not be what is in your distro’s package repository. I am using Ubuntu here, so if you are using a different distro, you will need to translate these commands into those suitable for your package manager:

$ sudo add-apt-repository ppa:certbot/certbot
$ sudo apt install certbot python3-certbot-dns-cloudflare

Next, you will need an API key from Cloudflare. Go to “My Profile” (in the top right corner) then “API Tokens”. You will need the “Global API Key”. (Side note: this is the easiest way, but you could probably also generate a specific key that just has permission to update a certain zone.)

Now, you need to create a small config file for the updater to use.

$ sudo vi /etc/letsencrypt/cloudflare.ini
dns_cloudflare_email = [email protected]
dns_cloudflare_api_key = <api key here>

Set the permissions on the file to be user only. This is for security reasons, and also because the plugin will complain if you don’t.

$ sudo chmod 700 /etc/letsencrypt/cloudflare.ini

Now, you are ready to get your certificates!

sudo certbot certonly \
  --dns-cloudflare \
  --dns-cloudflare-credentials /etc/letsencrypt/cloudflare.ini \
  -d internal.example.com

And let it do its thing. If everything went right, you should see a congratuations message about your new certificates.

Automating It

So this works fine for getting your certs, but we want to automate this so that we don’t have to think about it. With that in mind, you can create a cron entry that will run a variation of this command:

$ sudo vi /etc/cron.daily/certbot-internal.example.com
#!/usr/bin/env bash
/usr/bin/certbot certonly \
  --quiet -n --agree-tos \
  --dns-cloudflare \
  --dns-cloudflare-credentials /etc/letsencrypt/cloudflare.ini \
  --deploy-hook "systemctl reload nginx" \
  -d internal.example.com

The only difference here is a few more arguments that are documented in my previous post about this subject. We also added a deploy hook that reloads the web server to pick up the new certificate.

Finally, set your cron script executable:

$ chmod +x /etc/cron.daily/certbot-internal.example.com

And now you have auto-renewing internal SSL certificates.

Comments (0)

Interested in why you can't leave comments on my blog? Read the article about why comments are uniquely terrible and need to die. If you are still interested in commenting on this article, feel free to reach out to me directly and/or share it on social media.

Contact Me
Share It
Home Assistant
One of the big missing pieces from my conversion to Home Assistant was Amazon Alexa integration. It wasn’t something we used a lot, but it was a nice to have. Especially for walking out a room and saying “Alexa, turn off the living room lights.” I had been putting it off a bit because the setup instructions are rather complex. But this weekend I found myself with a couple free hours and decided to work through it. It actually wasn’t as difficult as I expected it to be, but it is definitely not the type of thing a beginner or someone who does not have some programming and sysadmin background could accomplish. But in working through it, there was one thing that was an immediate red flag for me: the need to expose your Home Assistant installation to the Internet. It makes sense that you would need to do this - the Amazon mothership needs to send data to you to take an action after all. But exposing my entire home automation system to the Internet seems like a really, really bad idea. So in doing this, rather than expose port 443 on my router to the Internet and open my entire home to a Shodan attack, I decided to try something a bit different.
Read More
Apache
I am currently in the process of migrating a bunch of sites on this machine from Apache to nginx. Rather than take everything down and migrate it all at once, I wanted to do this incrementally. But that raises a question: how do you incrementally migrate site configs from one to the other on the same machine, since both servers will need to be running and listening on ports 80 and 443? The solution I came up with was to move Apache to different ports (8080 and 4443) and to set the default nginx config to be a reverse proxy!
Read More