HTTPS certificates for subdomains using Tailscale and Caddy

28/10/2024

This tutorial shows how to obtain HTTPS certificates for subdomains when inside a Tailscale network and using Caddy as a web server.

Introduction

This week I setup for the first time a Tailscale network on a new Raspberry Pi 5 (that I was gifted as graduation gift, thanks again!). I use an Ubuntu VPS before to host my personal website and other services, each has a subdomain and a Let’s Encrypt certificate. I wanted to do the same with the Raspberry Pi, but I didn’t want to expose it to the public internet which is a problem since Let’s Encrypt uses the HTTP challenge to verify the domain ownership.

By looking around in various forums and the Tailscale documentation, I discover that Tailscale allows you to obtain a certificate for each machine and it pairs perfectly with Caddy but sadly it only supports one domain per machine. This is a no go since I want to host multiple services on the Raspberry Pi and I want to use a different subdomain for each.

A second solution I found while exploring possible solutions was to use the DNS challenge. This challenge verifies the domain ownership by adding a TXT record to the domain’s DNS records but I couldn’t find any decent guide on how to do it with Tailscale and Caddy. So I decided to write this tutorial to help others that might be in the same situation as me.

This tutorial will show how to do it with a wildcard DNS record to avoid adding a new record for each subdomain but a similar approach can be used to add a new record for each subdomain.

Prerequisites

Before you begin, you will need the following:

TL;DR

  1. Add the DNS provider plugin to Caddy (WARNING: EXPERIMENTAL)
caddy add-package github.com/caddy-dns/cloudflare 
  1. Obtain the API key from the DNS provider

Create the file /etc/caddy/.env file and add the following line to it:

CF_API_TOKEN=your-api-token
  1. Add a wildcard DNS record

Add an A record: *.home with the tailscale IP address of the server.

  1. Configure Caddy
*.home.fratorgano.me {
    # Handle tls for wildcard subdomains
	tls {
		dns cloudflare {env.CF_API_TOKEN}
	}

    # Handle the request for the pi-hole service
	@pihole host pihole.home.fratorgano.me
    handle @pihole {
        encode zstd gzip
        redir / /admin{uri} # Redirect to the pihole admin page - only needed for the pihole service
        reverse_proxy 127.0.0.1:1080
    }

	# Fallback for otherwise unhandled domains
	handle {
		abort
	}
}
  1. Restart Caddy
sudo systemctl restart caddy

Step 1: Add DNS Provider Plugin to Caddy

The first step is to add the DNS provider plugin to Caddy. This plugin will allow Caddy to automatically add the TXT record to the domain’s DNS records.

There is a guide available on the Caddy Community website that shows two methods to add the plugin to Caddy:

  1. Download a pre-built Caddy binary with the plugin already included.
  2. Build Caddy from source with the plugin included.

None of these methods was good for me since I already had Caddy installed. In the caddy documentation I found the caddy add-package command that allows you to add a plugin to Caddy. This command is experimental and might not work as expected but it worked for me.

You just need to find the correct plugin name considering the DNS provider you are using from the Caddy’s DNS providers repository. For example, I use Cloudflare as my DNS provider so I used the cloudflare plugin. (I actually use namecheap but their API is only available for their business customers so I had to switch DNS providers to Cloudflare)

I used the following command to add the Cloudflare plugin to Caddy:

caddy add-package github.com/caddy-dns/cloudflare 

Step 2: Obtain the API Key from the DNS Provider

This step is different for each DNS provider. You will need to obtain the API key from the DNS provider and add it to the Caddy configuration file. The GitHub repository of the plugin you are using should have a README file with instructions on the specific type of API key you need.

I stored the API in the /etc/caddy/.env file and added the following line to it:

CF_API_TOKEN=your-api-token

So that the Caddyfile can access the API key.

Step 3: Add a wildcard DNS record

This step is also different for each DNS provider. You will need to add a wildcard DNS record to the domain’s DNS records.

In my case I added an A record with the name *.home and the tailscale IP address of the Raspberry Pi.

Step 4: Configure Caddy using the wildcard DNS record

I use the Caddyfile in /etc/caddy/Caddyfile to configure Caddy. The same Github repository of the plugin you are using should have an example of how to configure the Caddyfile.

Here is my setup for Caddy to use the Cloudflare plugin for a wildcard DNS record, taken from the Caddy documentation:

*.home.fratorgano.me {
    # Handle tls for wildcard subdomains
	tls {
		dns cloudflare {env.CF_API_TOKEN}
	}

    # Handle the request for the pi-hole service
	@pihole host pihole.home.fratorgano.me
    handle @pihole {
        encode zstd gzip
        redir / /admin{uri} # Redirect to the pihole admin page - only needed for the pihole service
        reverse_proxy 127.0.0.1:1080
    }

	# Fallback for otherwise unhandled domains
	handle {
		abort
	}
}

The Caddy documentation has a Caddyfile pattern example for wildcard subdomains that I based mine on.

Step 5: Restart Caddy

After you have added the DNS provider plugin to Caddy, obtained the API key from the DNS provider, added the wildcard DNS record, and configured Caddy, you can restart Caddy to apply the changes.

sudo systemctl restart caddy

After restarting Caddy, you should be able to access your services using the subdomains you configured in the Caddyfile.