Using Let’s Encrypt for local servers
I run a few web services on my local network. I trust my network, but I’ve often wanted to use SSL with these services. Using DigitalOcean as a DNS provider and Let’s Encrypt, I finally have this in place. Here’s how I set things up.
Digital Ocean Setup
I’ll assume you already have DigitalOcean setup and hosting DNS for your domain. You just need to create an API token with read/write access (click the API link on https://cloud.digitalocean.com). Save this token somewhere safe — DigitalOcean does not allow you to view it again.
Certbot Installation
sudo apt-get install certbot python-certbot-nginx python3-pip
Note the version of certbot
that is installed. At the time of this writing,
I got version 0.31.0
on Ubuntu Bionic and Raspbian Buster.
Next install the DigitalOcean plugin for certbot
. Adjust 0.31.0
to match
the certbot
version if necessary:
sudo pip3 install certbot-dns-digitalocean==0.31.0
Save the DigitalOcean API token to your system. Replace TOKEN
with the
actual API token. Protect the directory and file from anyone who isn’t root
:
sudo mkdir /etc/letsencrypt
sudo mkdir -m 0700 /etc/letsencrypt/_credentials
sudo sh -c 'echo "dns_digital_ocean_token = TOKEN" > /etc/letsencrypt/_credentials/digitalocean.ini'
sudo chmod 600 /etc/letsencrypt/_credentials/digitalocean.ini
Now we’re ready to go:
sudo certbot certonly \
--dns-digitalocean \
--dns-digitalocean-credentials /etc/letsencrypt/_credentials/digitalocean.ini \
-d example1.priddle.xyz
Test that the renewal works okay:
sudo certbot renew --dry-run
Nginx Config
I use Nginx on all of my servers.
Let’s Encrypt should generate an Nginx configuration file at
/etc/letsencrypt/options-ssl-nginx.conf
. If it doesn’t:
sudo vim /etc/letsencrypt/options-ssl-nginx.conf
And add the following:
# This file contains important security parameters. If you modify this file
# manually, Certbot will be unable to automatically provide future security
# updates. Instead, Certbot will print and log an error message with a path to
# the up-to-date file that you will need to refer to when manually updating
# this file.
ssl_session_cache shared:le_nginx_SSL:1m;
ssl_session_timeout 1440m;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_prefer_server_ciphers on;
ssl_ciphers "ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA:ECDHE-ECDSA-DES-CBC3-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:DES-CBC3-SHA:!DSS";
The vhosts proxy to web servers running on different ports. For example,
instead of accessing 10.0.1.10:8080
I can have Nginx proxy
example1.priddle.xyz
to it.
The vhost configuration at /etc/nginx/sites-available/example1.priddle.xyz
:
# HTTP config (always redirects to HTTPS)
server {
listen 80;
server_name example1.priddle.xyz;
return 301 https://$server_name$request_uri;
}
# HTTPS config
server {
listen 443 ssl;
server_name example1.priddle.xyz;
# SSL configuration
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_certificate /etc/letsencrypt/live/example1.priddle.xyz/fullchain.pem; # managed by Certbot
ssl_certificate_key /etc/letsencrypt/live/example1.priddle.xyz/privkey.pem; # managed by Certbot
# Example config, replace as needed
location / {
proxy_pass http://127.0.0.1:8080/;
}
}
Keep in mind that Let’s Encrypt allows wildcard certificates when you use DNS challenges. This can be done with:
sudo certbot certonly \
--dns-digitalocean \
--dns-digitalocean-credentials /etc/letsencrypt/_credentials/digitalocean.ini \
-d \*.priddle.xyz
Local DNS Configuration
At this point there is a certificate generate and installed, but you can’t yet access this in your browser. There are a few ways you can handle this.
Configure DNS in DigitalOcean
You can add A records directly to DigitalOcean and just point them to your
local IPs. For example, example1.priddle.xyz => 10.0.1.10
. I tried this, but
my router filters out any DNS records like this 😭
Configure in /etc/hosts
If you’re on a Mac or Linux system you can use /etc/hosts
.
sudo vim /etc/hosts
And add a line like:
10.0.1.10 example1.priddle.xyz
Use your PiHole
I had a PiHole running on my network, so I configured it to serve these domains.
sudo vim /etc/dnsmasq.d/99-custom-domains.conf
Add records like the following:
address=/example1.priddle.xyz/10.0.1.10
You need to restart PiHole’s DNS server after making changes:
pihole restartdns
Use dnsmasq
You can use a standard dnsmasq installation just like with PiHole.
Example Nginx Configs
A few example Nginx configs I’m using with this setup:
Deluge
upstream deluge {
server 127.0.0.1:8112;
}
server {
listen 80;
server_name deluge.priddle.xyz;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl;
server_name deluge.priddle.xyz;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_certificate /etc/letsencrypt/live/deluge.priddle.xyz/fullchain.pem; # managed by Certbot
ssl_certificate_key /etc/letsencrypt/live/deluge.priddle.xyz/privkey.pem; # managed by Certbot
location / {
proxy_connect_timeout 59s;
proxy_send_timeout 600;
proxy_read_timeout 36000s; ## Timeout after 10 hours
proxy_buffer_size 64k;
proxy_buffers 16 32k;
proxy_pass_header Set-Cookie;
proxy_hide_header Vary;
proxy_busy_buffers_size 64k;
proxy_temp_file_write_size 64k;
proxy_set_header Accept-Encoding '';
proxy_ignore_headers Cache-Control Expires;
proxy_set_header Referer $http_referer;
proxy_set_header Host $host;
proxy_set_header Cookie $http_cookie;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Server $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Port '443';
proxy_set_header X-Forwarded-Ssl on;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header Authorization '';
proxy_buffering off;
proxy_redirect off;
proxy_pass http://deluge/;
add_header X-Frame-Options SAMEORIGIN;
}
}
Plex
upstream plex {
server 127.0.0.1:32400;
keepalive 32;
}
server {
listen 80;
server_name plex.priddle.xyz;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl;
server_name plex.priddle.xyz;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_certificate /etc/letsencrypt/live/priddle.xyz/fullchain.pem; # managed by Certbot
ssl_certificate_key /etc/letsencrypt/live/priddle.xyz/privkey.pem; # managed by Certbot
location / {
# Plex has A LOT of javascript, xml and html. This helps a lot, but if it causes playback issues with devices turn it off. (Haven't encountered any yet)
gzip on;
gzip_vary on;
gzip_min_length 1000;
gzip_proxied any;
gzip_types text/plain text/css text/xml application/xml text/javascript application/x-javascript image/svg+xml;
gzip_disable "MSIE [1-6]\.";
# Nginx default client_max_body_size is 1MB, which breaks Camera Upload feature from the phones.
# Increasing the limit fixes the issue. Anyhow, if 4K videos are expected to be uploaded, the size might need to be increased even more
client_max_body_size 100M;
# Forward real ip and host to Plex
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
# When using ngx_http_realip_module change $proxy_add_x_forwarded_for to '$http_x_forwarded_for,$realip_remote_addr'
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Sec-WebSocket-Extensions $http_sec_websocket_extensions;
proxy_set_header Sec-WebSocket-Key $http_sec_websocket_key;
proxy_set_header Sec-WebSocket-Version $http_sec_websocket_version;
# Websockets
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
# Buffering off send to the client as soon as the data is received from Plex.
proxy_redirect off;
proxy_buffering off;
proxy_pass http://plex;
}
}
Tautulli (Plex Stats)
upstream tautulli {
server 127.0.0.1:8181;
}
server {
listen 80;
server_name tautulli.priddle.xyz;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl;
server_name tautulli.priddle.xyz;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_certificate /etc/letsencrypt/live/priddle.xyz/fullchain.pem; # managed by Certbot
ssl_certificate_key /etc/letsencrypt/live/priddle.xyz/privkey.pem; # managed by Certbot
location / {
proxy_pass http://tautulli;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Host $server_name;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 90;
}
}
Basic HTML
server {
listen 80;
server_name web.priddle.xyz;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl;
server_name web.priddle.xyz;
root /var/www/html;
index index.html;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_certificate /etc/letsencrypt/live/priddle.xyz/fullchain.pem; # managed by Certbot
ssl_certificate_key /etc/letsencrypt/live/priddle.xyz/privkey.pem; # managed by Certbot
location / {
try_files $uri $uri/ =404;
}
}
Jekyll
server {
listen 80;
server_name web.priddle.xyz;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl;
server_name jekyll.priddle.xyz;
root /var/www/jekyll-site/_site;
index index.html;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_certificate /etc/letsencrypt/live/priddle.xyz/fullchain.pem; # managed by Certbot
ssl_certificate_key /etc/letsencrypt/live/priddle.xyz/privkey.pem; # managed by Certbot
location / {
try_files $uri $uri/ =404;
}
}
OR
upstream jekyll {
server 127.0.0.1:3000;
}
server {
listen 80;
server_name jekyll.priddle.xyz;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl;
server_name jekyll.priddle.xyz;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_certificate /etc/letsencrypt/live/priddle.xyz/fullchain.pem; # managed by Certbot
ssl_certificate_key /etc/letsencrypt/live/priddle.xyz/privkey.pem; # managed by Certbot
location / {
proxy_pass http://jekyll;
}
}