How to use SSL secured by Let's Encrypt for services in a private network without exposing the service to the internet?
Requirements
- SSL for all services on my private network
- Services should only be accessible via VPN (or from within the local network)
- No exposure of the services for certificate generation/renewal
- Certificates should be signed by a public authority (e.g. Let's Encrypt), so no self-signed certs.
For example, I would like to have homeassistant on https://home.cloud.example.com
. However, it should only be accessible via a VPN (in my case: Wireguard). When you use Let's encrypt certbot to generate your certificates, it defaults to the HTTP challenge: it tries to access the actual server in order to verify the domain. This is not an option for my private network. Luckily, we can use the DNS challenge as an alternative.
My solution consists of:
- a DNS challenge
- an Nginx reverse proxy
- a Wireguard VPN
- a DNS forwarder in the private network (e.g. on the router)
Topology
Let's say we have three parts:
- a router (
192.168.2.1
)- which typically exposes a wireguard VPN
- a reverse proxy server (
192.168.2.2
)- with Nginx installed
- a service (
192.168.2.3
), e.g. homeassistant
DNS Config and Certbot
- First: there is no need for DNS records for your service (only a temporary TXT record for the DNS challenge) . Instead, we will assign hostnames on our private DNS forwarder (see below).
- To make things easier, we'll use a wildcard domain:
*.cloud.example.com
. This means that we only need one certificate for all our services.
Install Certbot on the reverse-proxy server. Then, run this:
sudo certbot --server https://acme-v02.api.letsencrypt.org/directory -d *.cloud.example.com --manual --preferred-challenges dns certonly
You will now need to resolve the DNS challenge: Go to your domain registrar and add a DNS TXT-record according to the instructions. Do not press enter yet: It might take some time before the record is created/updated across the internet; meanwhile, you can verify if it is available by opening a new terminal and run host -t txt _acme-challenge.cloud.example.com
. Only when its result contains the TXT-record, go back to certbot and press enter to continue.
The DNS challenge should now resolve and our new keys will be created in etc/letsencrypt/live/cloud.example.com/
. We need those for our reverse proxy.
You can now safely remove the txt record from the registrar again (recommended, since it gives away which domain you use for your private network).
Setup the reverse proxy
The Nginx reverse proxy config looks like this:
# reverse proxy optimized for Home Assistant
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
server_name home.cloud.example.com;
location / {
proxy_pass http://192.168.2.3:8123; # TODO: change this to your local address of homeassistant
proxy_set_header Host $host;
proxy_redirect http:// https://;
proxy_http_version 1.1;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
}
listen 443 ssl;
ssl_certificate /etc/letsencrypt/live/cloud.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/cloud.example.com/privkey.pem;
add_header Strict-Transport-Security "max-age=31536000; includeSubdomains";
proxy_buffering off;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
}
server {
if ($host = home.cloud.example.com) {
return 301 https://$host$request_uri;
}
server_name home.cloud.example.com;
listen 80;
return 404;
}
Store the config in /etc/nginx/sites-available/home.cloud.example.com and activate:
sudo ln -s /etc/nginx/sites-available/home.cloud.example.com /etc/nginx/sites-enabled/home.cloud.example.com
sudo service nginx reload
Repeat this process for every other service you want to expose (the nginx config might differ depending on the service). Note that you can keep the certificate the same, thanks to the wildcard.
Important for homeassistant!
You'll have to upgrade your configuration.yaml. Otherwise homeassistant won't allow access through the reverse proxy:
http:
use_x_forwarded_for: true
trusted_proxies:
- 192.168.2.0/24 # either the whole private subnet
- 192.168.2.2/32 # or only the reverse proxy
Config the private DNS forwarder
We are almost there! There is only one thing left: pointing the domains to your reverse proxy. Since it should only be accessible on your private network, those records should be registered on the DNS forwarder inside the private network. For example, by defining hostnames on the router (which runs the local DNS service).
Go to your router settings and look for a place to define hostnames. For OpenWRT routers, you can find it in the webinterface
Network > DHCP and DNS > Hostnames. Here you can specify hostname(s) for your service(s) (e.g. home.cloud.example.com
) and point them to the ip of the reverse proxy (192.168.2.2
).
In addition, for access via VPN it is important that your VPN configuration specifies the ip of the DNS forwarder (e.g. the ip of the router). For Wireguard, this is done by specifying the DNS in the interface section of the wireguard config file:
[Interface]
PrivateKey = ...
Address = ...
DNS = 192.168.2.1 # the ip of the router
The problem with the alternative
When you register the domains at your registrar, it will probably not work due to DNS rebinding protection of your private DNS forwarder (dnsmasq). Do not disable this! The solution is to exclude your domains by adding them as hostname - but hé, then we do not need to register them at the registrar anymore.
The big elephant in the room: certificate renewal
Yeah, that is something you'll need to figure out for yourself. Let's encrypt certificates are only valid for 3 months. You could implement an automatic renewal mechanism if your domain registrar provides an api. In my case, I'll just have a tri-month calendar item popping up and repeat these steps😆
Conclusion
There you have it: you can now use SSL certificates for all the services on your private network!