Securing local services with Let's Encrypt
Introduction
I have an ever-increasing number of services that I host in my homelab, such as Immich and Home Assistant. These services can only be accessed on the local network, so while I can reasonaly safely access them with a self-signed certificate, using a certificate signed by a trusted Certificate Authority will remove browser warnings and resolve potential compatibility issues.
However, how do you get a certificate for a domain that isn’t publicly accessible? When setting up a certificate using Let’s Encrypt, you’ll need to prove that you control the domain1. Tools like Cerbot can use the ACME protocol to automatically do this, normally by provisioning an HTTP resource under your domain. However, this won’t work if you don’t have a Web server that is publicly reachable.
The solution is the ACME DNS challenge and a DNS provider that provides an API to provision DNS records.
Prerequisites
- A domain name that you own.
- A DNS provider that is supported by one of the Certbot DNS plugins (I’m using Cloudflare).
If your DNS provider is not supported, it is possible to manually provision the DNS record, but this will not allow for automatic renewals. Alternatively, a different ACME client may support your DNS provider, such as acme.sh, however this guide will focus on Certbot.
Installing Certbot
These steps are based on the official Certbot installation instructions using pip on a Linux machine2. I recommend that you follow the official documentation for your specific operating system.
Install Certbot dependencies
On Debian-based systems:
sudo apt update
sudo apt install python3 python3-dev python3-venv libaugeas-dev gcc
On Red Hat-based systems:
sudo dnf install python3 python-devel augeas-devel gcc
Remove Certbot OS packages
If you’ve previously installed Certbot packages from your OS package manager, you should remove them before installing Certbot using pip.
Set up a Python virtual environment
sudo python3 -m venv /opt/certbot/
sudo /opt/certbot/bin/pip install --upgrade pip
Install Certbot
sudo /opt/certbot/bin/pip install certbot
# Link certbot to /usr/bin/, enabling use of the certbot command.
sudo ln -s /opt/certbot/bin/certbot /usr/bin/certbot
Install Certbot DNS plugin
# Replace <PLUGIN> with your DNS provider, for example, certbot-dns-cloudflare.
sudo /opt/certbot/bin/pip install certbot-dns-<PLUGIN>
Configure Certbot DNS plugin
These steps are based on the documentation for the Cloudflare plugin. If not using Cloudflare, follow the Certbot documentation for your particular DNS provider.
- Go to your Cloudflare dashboard and create a new API token.
- Ensure that the token has
Zone:DNS:Edit
permissions. This allows the Certbot DNS plugin to make the necessary changes to your DNS records required by the ACME DNS challenge.
Once you have created your token, you should save the token to /etc/letsencrypt/cloudflare.ini
with the following content:
dns_cloudflare_api_token = <your_token>
Get a certificate
You are now ready to get a certificate for your domain. You can create a certificate for the domain itself, e.g., example.com
, or particular subdomains, e.g., photos.example.com
. You can also create a wildcard certificate, e.g., *.example.com
, which can be used for all subdomains under your domain.
Run the following to create a wildcard certificate:
# Replace <PROVIDER> and <DOMAIN> with your DNS provider and your domain.
# You can add addition -d '<DOMAIN>' arguments to add additional domains to the
# certificate.
sudo certbot certonly --dns-<PROVIDER> --dns-<PROVIDER>-credentials \
/etc/letsencrypt/<PROVIDER>.ini -d '*.<DOMAIN>'
# For example, for Cloudflare and example.com:
sudo certbot certonly --dns-cloudflare --dns-cloudflare-credentials \
/etc/letsencrypt/cloudflare.ini -d '*.example.com'
Note: the Certificate Transparency Internet security standard requires that all issued certificates be logged in public logs. These logs can be searched freely on websites such as crt.sh. If you are obtaining a certificate for a specific subdomain rather than a wildcard, it is important to consider the potential exposure of insights into your internal network from these logs.
To ensure your certificate automatically renews, run the following line to add a cron job:
# Twice daily, with a random delay to reduce load on Let's Encrypt's servers,
# check and renew your certificates.
echo "0 0,12 * * * root /opt/certbot/bin/python -c \
'import random; import time; time.sleep(random.random() * 3600)' && \
sudo certbot renew -q" | sudo tee -a /etc/crontab > /dev/null
Test the renewal process by running:
sudo certbot renew --dry-run
Conclusion
You should have a certificate that will automatically renew which you can use to secure your local services. Just point your DNS to your local IP (e.g. photos.example.com
to 192.168.0.2
).
Further reading
- Putting your local services behind a subdomain using NGINX and your new certificate.
- Using Pi-hole for locally resolvable DNS for your internal services.
Please open an issue if you have any comments, questions, or issues.