Continuing with day 22’s infrastructural focus, day 23 of my (increasingly random) December Adventure brought further thoughts on using Let’s Encrypt SSL certificates, webserver changes, and a side-quest into dnsmasq.

Vanity URLs

One of the larger inconveniences of running self-hosted infrastructure (beyond the obvious administrative cost and decision fatigue that comes from needing to understand many disparate projects) is the myriad different URLs, ports, and self-signed certificates that you need to interact with: default setups will have you needing to remember IP addresses, along with the various ports numbers different services run on. Coupled with the increasing challenge of coaxing browsers to trust self-signed certificates (especially on mobile), this can make for a deeply frustrating experience. The key to tidying up this mess is to run your own DNS server, specify unique domain names for each service, and then use a reverse proxy to forward on the relevant ports.

Having never set up my own DNS server before, I went about configuring dnsmasq on one of my Raspberry Pis. I had expected the process to be incredibly complex, but, as I don’t need it to do much, everything proved incredibly simple. The config file (/etc/dnsmasq.d/local.conf) ended up looking like this:

# Listen on ethernet and Tailscale.
interface=eth0
interface=tailscale0
bind-interfaces

# Ignore local /etc/hosts and /etc/resolv.conf.
no-hosts
no-resolv

# Forward unknown requests to Cloudflare.
server=1.1.1.1

# Provide responses for home.jbmorley.co.uk and sub-domains.
address=/home/jbmorley.co.uk/100.119.64.74
address=/immich.home.jbmorley.co.uk/100.119.64.74
address=/jellyfin.home.jbmorley.co.uk/100.119.64.74
address=/syncthing.home.jbmorley.co.uk/100.119.64.74

I was able to check this was working locally on my Raspberry Pi by explicitly querying localhost using nslookup:

# nslookup immich.home.jbmorley.co.uk localhost
Server:         localhost
Address:        ::1#53

Name:   immich.home.jbmorley.co.uk
Address: 100.119.64.74

The custom DNS entries all resolve to an IP address on my Tailscale VPN, meaning they should work whenever I’m connected to Tailscale, irrespective of where I am.

I was also able to take advantage of Tailscale’s built-in support to automatically route only the DNS queries for the home.jbmorley.co.uk suffix to my DNS server using Split DNS.

Tailscale’s MagicDNS will defer all DNS queries for home.jbmorley.co.uk to 100.126.47.22

SSL

The goal of using fully qualified sub-domains for a domain I own (see above) is to allow me to generate SSL certificates that will be accepted by all my devices. For this, I can use Let’s Encrypt with the DNS-01 challenge with Cloudflare that I dug into on day 22.

While I originally planned to use Caddy to generate certificates, I eventually accepted that I don’t want to have to maintain a custom build outside of the package manager to be able to use the DNS challenge plugins. Instead, I opted to use certbot for this. I generated a certificate for home.jbmorley.co.uk and a wildcard certificate for *.home.jbmorley.co.uk. This ensures I don’t have to generate a new certificate for each and every service I run:

sudo certbot certonly \
    --dns-cloudflare \
    --dns-cloudflare-credentials /etc/letsencrypt/cloudflare.ini \
    -d "*.home.jbmorley.co.uk" \
    -d "home.jbmorley.co.uk"

Web Servers

Having decided not to use Caddy for SSL, I returned to nginx and set up a handful of sites: a simple static site at home.jbmorley.co.uk which offers links to all my other services, and a collection of reverse proxies.

Links

Some time ago, created a lightweight home page with links to the various services I run. This proved incredibly useful, and has become a crucial part of my home infrastructure:

Needless to say, the nginx configuration for this proved very simple:

server {
    listen 80;
    server_name home.jbmorley.co.uk links.home.jbmorley.co.uk;

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

server {
    listen 443 ssl http2;
    server_name home.jbmorley.co.uk links.home.jbmorley.co.uk;

    ssl_certificate     /etc/letsencrypt/live/home.jbmorley.co.uk/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/home.jbmorley.co.uk/privkey.pem;

    root /var/www/links;
    index index.html;

    location / {
        try_files $uri $uri/ =404;
    }
}

Reverse Proxies

Configuring the reverse proxies proved equally simple. For example, my Syncthing proxy looks like this:

server {
    listen 80;
    server_name syncthing.home.jbmorley.co.uk;

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

server {
    listen 443 ssl;
    http2 on;
    server_name syncthing.home.jbmorley.co.uk;

    ssl_certificate     /etc/letsencrypt/live/home.jbmorley.co.uk/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/home.jbmorley.co.uk/privkey.pem;

    location / {
        proxy_pass https://127.0.0.1:8384;

        proxy_http_version 1.1;
        proxy_set_header Host $host;
        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 https;

        # Syncthing uses a self-signed cert by default
        proxy_ssl_verify off;
    }
}

The only notable element here is the proxy_ssl_verify off which ensures nginx will accept the self-signed certificate that Syncthing generates by default.


The day proved significantly more work-like than I’d been hoping for. Still, it’s great to feel like I’m laying the groundwork for future self-hosted infrastructure and tooling.