Caddy with Private HTTPS

  1. Introduction
    1. Caddy Setup

Introduction

This post will walk through setting up a virtual machine with caddy. We will configure it to use the Cloudflare provider to resolve ACME challenges, enabling us to provision HTTPS certificates through Let’s Encrypt even on private routes.

Caddy Setup

The debian server has a running Caddy (XCaddy for Cloudflare DNS resolution on private wildcard certificates) service.

sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/xcaddy/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-xcaddy-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/xcaddy/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-xcaddy.list
sudo apt update
sudo apt install xcaddy

XCaddy has a dependency on go-lang.

wget https://go.dev/dl/go1.24.2.linux-amd64.tar.gz
# as root
rm -rf /usr/local/go && tar -C /usr/local -xzf go1.24.2.linux-amd64.tar.gz

echo "export PATH=$PATH:/usr/local/go/bin" > /etc/profile.d/golang_path.sh

After this is done, reload your shell or manually add the go binary to your path.

Building the caddy server with Cloudflare support.

xcaddy build --with github.com/caddy-dns/cloudflare

This should have emitted a caddy binary.
Follow the manual installation method to setup Caddy to run as a service..
Because we are using Caddy predominately with its API, we prefer the caddy-api.service unit file.

sudo mv caddy /usr/bin/
sudo groupadd --system caddy
sudo useradd --system \
--gid caddy \
--create-home \
--home-dir /var/lib/caddy \
--shell /usr/sbin/nologin \
--comment "Caddy web server" \
caddy

sudo wget https://raw.githubusercontent.com/caddyserver/dist/refs/heads/master/init/caddy-api.service -O /etc/systemd/system/caddy.service
sudo systemctl daemon-reload
sudo systemctl enable --now caddy

The Cloudflare Caddy module requires an API token, which we will store into an environment variable CF_API_TOKEN. The JSON payload will have a reference to the environment variable "{env.CF_API_TOKEN}". This will need to be explicitly added into our unit service file.

Configure an account level API token, with Zone.Zone:Read and Zone.DNS:Edit permissions for your domain. Optionally set the token Client IP Address Filtering rule to the Caddy server’s IP.

Edit the systemctl service and add this token to the environment:

sudo systemctl edit caddy
### Editing /etc/systemd/system/caddy.service.d/override.conf
### Anything between here and the comment below will become the new contents of the file
[Service]
Environment="CF_API_TOKEN=<redacted>"


### Lines below this comment will be discarded

### /etc/systemd/system/caddy.service
# interactive command of json is needed because cloudflare SSH proxy only supports interactive SSH sessions. scp does not work. If you're not using cloudflare zero trust you should be able to just copy this caddy file over to your server.
base64 caddy.json | ssh alex@<redacted> 'base64 -d > ~/caddy.json'

ssh alex@<redacted>

curl localhost:2019/load \
-H "Content-Type: application/json" \
-d @caddy.json

A sample caddy.json can be found below.

{
"apps": {
"http": {
"servers": {
"cloud": {
"listen": [
":80",
":443"
],
"routes": [
{
"match": [
{
"host": [
"private.u0.vc"
]
}
],
"handle": [
{
"body": "Hello, world! This is a private route and should require CloudFlare Warp enabled VPN to access.",
"handler": "static_response"
}
]
},
{
"match": [
{
"host": [
"u0.vc"
]
}
],
"handle": [
{
"body": "Hello, world! This is a publicly accessible route.",
"handler": "static_response"
}
]
}
]
}
}
},
"tls": {
"automation": {
"policies": [
{
"subjects": [
"*.u0.vc",
"u0.vc"
],
"issuers": [
{
"module": "acme",
"email": "[email protected]",
"challenges": {
"dns": {
"provider": {
"name": "cloudflare",
"api_token": "{env.CF_API_TOKEN}"
}
}
}
}
]
}
]
}
}
}
}