How to Protect Self-Hosted Services With mTLS Client Certificates in Caddy
If you run a self-hosted service behind Caddy, you can require each connecting device to prove its identity with a certificate. Mutual…
When you run several services behind Caddy, each hostname often needs the same settings, such as compression or security headers, but sends requests to a different service. A Caddyfile is Caddy’s human-readable configuration format. You can avoid repeating settings with snippets, named blocks that import inserts into site configurations, and use a wildcard certificate, one TLS certificate that covers matching subdomains.
Snippets let you reuse settings, while wildcard certificates let matching hostnames use HTTPS. Keep each service’s routing explicit, and use DNS validation to automate a wildcard certificate. The example shows how the settings work together and what to check before deployment.
Define a snippet in parentheses, then insert it into each site block with import. Caddy treats the imported directives as though you had written them at that location. For example:
(common) {
encode
header X-Content-Type-Options nosniff
}
app.example.com {
import common
reverse_proxy app:8080
}
api.example.com {
import common
reverse_proxy api:8080
}
Both sites share compression and a response header. Each reverse_proxy directive names the service that receives requests for that hostname, so the host-specific routing is easy to find without repeating the shared settings. Caddy documents import as a way to reuse snippets and their arguments.
A snippet does not make one site inherit settings from another. Each site still needs the directives that apply to it. Use a snippet for settings that genuinely repeat. Examples include headers, logging, and encoding; leave backend choices and host-specific behavior in the relevant site block. Caddy’s Caddyfile concepts describe site blocks as separate configuration contexts.
A wildcard address such as *.example.com matches subdomains with one label before example.com. Caddy needs DNS validation to obtain a wildcard certificate: the certificate authority checks a temporary DNS TXT record to confirm control of the domain. Caddy therefore needs a DNS provider module, a component in the Caddy binary that connects to a DNS service, and credentials that let it create and remove the record.
*.example.com {
tls {
dns cloudflare {env.CLOUDFLARE_API_TOKEN}
}
# Fallback for subdomains without a more specific site.
abort
}
app.example.com {
import common
reverse_proxy app:8080
}
api.example.com {
import common
reverse_proxy api:8080
}
# Add this block if the bare domain should also be served.
example.com {
import common
reverse_proxy homepage:8080
}
The dns provider name and credential setup depend on the DNS service and the Caddy binary. The example uses Cloudflare syntax; it works only when the running binary includes the corresponding provider module and the service environment supplies the token. Check the TLS directive documentation and the selected provider’s instructions before using a provider-specific configuration.
The wildcard site address lets Caddy obtain a certificate for matching hostnames, but it does not route requests to a backend service. The specific app.example.com and api.example.com blocks still define where those requests go. Here, abort gives subdomains without a more specific site a fallback behavior. Caddy’s wildcard site patterns show how wildcard and specific site addresses work together.
*.example.com covers app.example.com, but not the bare example.com or a deeper name such as foo.bar.example.com. Add a separate site block for the bare domain if Caddy should serve it.
Caddy 2.10 and later prefers an available wildcard certificate for configured individual subdomains, rather than obtaining a separate certificate for each one. Earlier releases do not share that documented behavior. Wildcard automation also depends on DNS access: Caddy’s Automatic HTTPS documentation explains that DNS validation uses provider credentials, and that DNS provider modules must be included in the Caddy build. If your DNS host lacks a supported module, Caddy documents DNS CNAME delegation as an alternative for the ACME challenge.
Format the Caddyfile, inspect the converted configuration, and validate that Caddy can load and set up the modules it uses:
caddy fmt --overwrite Caddyfile
caddy adapt --config Caddyfile --pretty
caddy adapt --config Caddyfile --validate
caddy adapt converts the Caddyfile into Caddy’s native JSON configuration. A successful conversion by itself does not confirm that Caddy can set up every module, so validation helps when the configuration uses a DNS provider module. The Caddy command-line documentation explains the difference between adapting and validating.
These checks do not prove that the live DNS provider will accept the credentials, that the TXT record will propagate, or that a certificate authority will issue the certificate. Confirm those details in the running service’s environment and logs when you test certificate issuance.
When many subdomains share a domain, one wildcard certificate covers them, reduces the number of certificates Caddy needs, and gives broader hostname coverage. Choose a wildcard certificate when that coverage and the DNS automation are worth the added provider dependency. Individual certificates tie coverage to specific hostnames, though Caddy then manages certificates for more names.
A wildcard certificate also does not hide subdomain names in the TLS handshake. Caddy’s Automatic HTTPS documentation discusses that separate privacy concern and points to Encrypted ClientHello (ECH) as a distinct mechanism.
Give Vroni a GitHub issue, bug report, spec, or rough idea. It reads the repo, plans the change, writes code, runs checks, and works toward a review-ready pull request.
Take a look at vroni.com