Build your own CDN - Part 3: Allowing bring-your-own domains with automatic TLS certificates
Validating customer domains before Caddy requests their TLS certificates
Part 1 synchronized TLS certificates across the PoPs, and Part 2 distributed Caddy configuration with Nomad. Caddy still needs to request certificates only for names approved by Gitea Pages. Customer domains are not known in advance, so this requires a validation service. The same pattern applies to any SaaS that lets customers bring their own domain.
Caddy's on-demand TLS configuration accepts an ask endpoint. Before requesting a certificate, Caddy sends a GET request with the domain name to that endpoint. A 2xx response approves the request, while any other status code denies it.
The check protects the Let's Encrypt quota from requests for domains outside the application's control. It also prevents someone from pointing an unapproved domain at the infrastructure and receiving a valid TLS certificate.
The Nomad template from Part 2 can add a customer's domain to the Caddy configuration. Adding the name is not enough to establish that the customer controls it. The validation service must make that decision before approving Caddy's request.
The Caddy side of the configuration is small:
{
on_demand_tls {
ask https://validation.service.tld/endpoint
}
}
example.com {
tls {
on_demand
}
respond "Hello World!"
}
For this configuration, Caddy calls https://validation.service.tld/endpoint with a query string such as ?domain=example.com. The initial request waits while Caddy checks the domain and obtains a certificate, so the validation endpoint should respond quickly.
The service can be a small Go application that reads a file of approved domains or a database lookup inside a larger application. If approval depends on external checks, such as confirming that the domain points to the load balancer, those checks can run asynchronously and cache the result in the application's database instead of running for every Caddy request.
The validation handler depends on where the application stores customer domains and which checks it performs, so there is no single implementation to show here. It can be a separate HTTP service or part of an application that already handles customer and billing data. Caddy only depends on the HTTP response, not the implementation language. An endpoint that returns 200 for every name would remove the protection that the ask check is meant to provide.