Idea

(Big thanks to this post!)

So it turns out I can’t live with self-signed certificates, as I previously outlined in this post. They just don’t play nice with a lot of things - mobile browsers (mobile devices in general, for that matter) and apps (mobile AND desktop).

On my Jellyfin app, I had to edit a configuration file in AppData for it to work. On the mobile app, it just didn’t work, full stop. FreshRSS (FeedMe mobile app) was the same way, it wouldn’t work at all via HTTPS if the certificate was self-signed.

So back to the drawing board it was. From what I was reading, I could host my own CA and load that CA onto ALL my devices / browsers / what have you. And I was simply NOT going to do that.

My goal was to have a custom domain served on my LAN, and what I had been using thus far was *.nas.local to run subdomains on an nginx reverse proxy. DNS handled by my Pi-hole. This was basically the infrastructure that I needed to serve the domain that I ultimately landed on, but I hadn’t made a key connection just yet. All my research was predicated on the fact that I wanted to use a custom tld, and I didn’t have to answer to anyone.

What I didn’t initally realize was, in order to get a public certificate, you have to own a domain. Then, you have to prove to the CA that you actually own that domain.

That’s the key piece of information I was missing. So here’s what I did.

Requirements

  1. Own a public domain, or go purchase one. (This meant that my dreams of using the .local tld just went down the toilet.)
  2. Have a way to serve that domain via local DNS. I already have this infrastructure with my Pi-hole. I’ve also started using Tailscale (in lieu of barebones WireGuard) to serve this DNS (and thus my services) even if I’m not on my LAN.
  3. Have a way to serve subdomains via a reverse proxy. I used bare metal nginx, because I have a bit of familiarity (and if you haven’t noticed, I often abide by the Hands On Imperative)

Steps

  1. Generate a wildcard certificate for that domain using a DNS challenge, and install it on your server that will be serving your subdomains.
  2. Create an A record pointing to your server’s IP, and any CNAME records for subdomains pointing back to the main domain.
  3. Configure the reverse proxy on your server to respond to the relevant subdomains.

Generating the wildcard certificate

On your LAN server:

sudo certbot certonly --manual --preferred-challenges dns -d "*.domain.com"

In your domain provider, add the given TXT record name and value, wait a little while (it will give you a link to check if the TXT record has populated), then hit enter.

That’s it! Just keep in mind that the certificate is only valid for 90 days and will need to be renewed manually.

Create the A and CNAME records

On your Pi-hole, create an A record of domain.com that points to your server’s IP.

Then create any applicable CNAME (subdomain) records. E.g., file.domain.com that points to domain.com.

Verify using dig or nslookup with a machine connected to your LAN that both records work.

Configure your reverse proxy on the LAN server

Here’s an example configuration file:

# /etc/nginx/sites-available/domain.com

# 301 redirect from HTTP to HTTPS
server {
  listen 80;
  listen [::]:80;

  server_name *.domain.com;

  return 301 https://$host$request_uri;
}

# file.domain.com subdomain
server {
  server_name file.domain.com;
  location / {
    proxy_pass http://127.0.0.1:8080;
  }

  listen 443 ssl;
  include snippets/domain-com.conf;
  include snippets/ssl-params.conf;

}

Since I’m hosting 7 (or so) subdomains I decided to create a snippets file containing the certificate and certificate key location - that’s what the snippets/domain-com.conf file is:

# /etc/nginx/snippets/domain-com.conf

ssl_certificate /etc/letsencrypt/live/domain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/domain.com/privkey.pem;

And another file I pillaged from somewhere (here!), adapted slightly:

# /etc/nginx/snippets/ssl-params.conf

ssl_protocols TLSv1.3;
ssl_prefer_server_ciphers on;
ssl_dhparam /etc/nginx/dhparam.pem;
ssl_ciphers EECDH+AESGCM:EDH+AESGCM;
ssl_ecdh_curve secp384r1;
ssl_session_timeout 10m;
ssl_session_cache shared:SSL:10m;
ssl_session_tickets off;
ssl_stapling on;
ssl_stapling_verify on;
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload";
add_header X-Frame-Options DENY;
add_header X-Content-Type-Options nosniff;
add_header X-XSS-Protection "1; mode=block";

Wrap up

The really, really cool thing about this for me is that this domain is only accessible if I am connected to my LAN or tailnet. As far as the WAN is concerned, well, someone owns the domain, but there is no A record associated with it. The only DNS record I have set in my domain provider is the TXT record, which I removed shortly after creating the certificate.

This accomplishes my goal of never having any of these services accessible to the outside while still getting a publicly signed certificate. It’s really quite neat to have a local domain name resolve to my own personal services perfectly indistinguishably from a public domain, while still knowing that I’m not exposing my whole LAN indiscriminately.

Definitely a level up for the homelab!

EOF