A Sensible Caddyfile
After a decade of nginx modules and Apache configurations, I’ve moved my personal infrastructure to Caddy. It’s a single Go binary that handles TLS automatically and ships with sensible defaults.
Here’s an opinionated Caddyfile I’ve settled on for a modern web stack, with security headers tuned for an application that doesn’t load anything from third parties:
example.com {
@static path *.css *.js *.ico *.txt /sitemap.xml
handle @static {
file_server {
root /opt/app/public
}
}
reverse_proxy localhost:8080
encode {
zstd
gzip
}
header {
-Via
Content-Security-Policy "default-src 'self'; script-src 'self'; connect-src 'self'; img-src 'self'; style-src 'self'; base-uri 'self'; form-action 'self';"
Cross-Origin-Opener-Policy "same-origin"
Origin-Agent-Cluster "?1"
Permissions-Policy "accelerometer=(), autoplay=(), camera=(), display-capture=(), fullscreen=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), picture-in-picture=(), usb=(), web-share=()"
Referrer-Policy "no-referrer"
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
X-Content-Type-Options "nosniff"
X-DNS-Prefetch-Control "off"
X-Download-Options "noopen"
X-Frame-Options "SAMEORIGIN"
X-Permitted-Cross-Domain-Policies "none"
X-XSS-Protection "0"
}
}
www.example.com {
redir https://example.com{uri} permanent
}
The philosophy is to deny everything by default and only enable what the application proves it needs. Treat it as an allowlist: if a feature isn’t explicitly permitted, the browser shouldn’t let the page touch it.
Automatic HTTPS and static assets
Caddy provisions and renews Let’s Encrypt TLS certificates over the ACME protocol on its own. Specifying a domain name in the Caddyfile is enough to get HTTPS, with no manual certificate management.
Caddy serves static files straight from disk and proxies dynamic requests to the application server. Responses get compressed with Zstandard, with gzip as a fallback for clients that don’t support it.
Content security policy
The CSP allows resources only from the same origin ('self') and blocks
inline scripts and external resources that are common XSS vectors.
You can extend this policy with nonces for inline content or additional domains. Doing that usually means moving the CSP header into the application so it can set per-request nonces.
Process isolation
Cross-Origin-Opener-Policy: same-origin prevents cross-origin attacks by
isolating the page’s browsing context. Origin-Agent-Cluster: ?1 asks the
browser to put this site in its own process, which improves isolation
against side-channel attacks and keeps a resource-hungry page from dragging
other sites down.
Permissions
The permissions policy blocks the most sensitive browser APIs by default:
sensors, media devices, payments, USB, and screen capture. When a feature
actually needs one of these (fullscreen for an image gallery, say), flip the
relevant policy from () to self. The default answer is no, and you only
widen access for features that have earned it. That’s the principle of
least privilege
applied to browser APIs.
Privacy and transport security
Referrer-Policy: no-referrer prevents data leakage through referrer
headers. Strict-Transport-Security enforces HTTPS with preload support in
browsers, though HSTS preload submission
requires keeping it enabled for at least a year. X-DNS-Prefetch-Control: off reduces tracking through DNS prefetching.
Content protection
X-Content-Type-Options: nosniff prevents MIME-type confusion attacks.
X-Frame-Options: SAMEORIGIN blocks clickjacking. X-XSS-Protection: 0
disables the legacy XSS filter since CSP covers the same ground better, but
layered defense is the point of having all of these together.
This is a reasonable baseline for a new web application. When a feature comes along that needs something more permissive, change one policy at a time. That way you’ll know what you’ve enabled and why.