A Sensible Caddyfile
After many years of nginx modules and Apache configs, I’ve switched to Caddy for my personal stack. It’s a single Go binary that manages TLS and has sensible defaults.
My Caddyfile for web applications that don’t load any third-party content looks like this:
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 {
-Server
Content-Security-Policy "default-src 'self'; base-uri 'self'; form-action 'self'; frame-ancestors 'self'"
Cross-Origin-Opener-Policy "same-origin"
Origin-Agent-Cluster "?1"
Permissions-Policy "accelerometer=(), camera=(), display-capture=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()"
Referrer-Policy "no-referrer"
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
X-Content-Type-Options "nosniff"
}
}
www.example.com {
redir https://example.com{uri} permanent
}
I like to default to off and only turn on what the application needs.
Automatic HTTPS and static assets
Caddy will provision Let’s Encrypt TLS certificates via ACME for you. As long as you have a domain in the Caddyfile, it’ll be served over HTTPS.
Any request that matches @static will be served from disk. All other requests will be proxied to your application. Content will be zstd compressed when possible and gzip for everything else.
Content security policy
The Content Security Policy allows resources to only load from 'self' and doesn’t allow inline scripts or any third-party resources. Be aware that inline styles are blocked as well.
All other resource types will default to default-src, so there’s no reason to specify them. base-uri, form-action and frame-ancestors won’t default, so they’re specified. frame-ancestors 'self' will prevent your page from being iframed by other origins. This replaces the traditional X-Frame-Options header.
You can add more to this policy by using nonces or allowing additional domains, though this will likely require moving the CSP header into your application.
Process isolation
Cross-Origin-Opener-Policy: same-origin will prevent pages from other origins that open yours from getting a handle on it through window.opener. This prevents reverse tabnabbing and some cross-site leaks.
Origin-Agent-Cluster: ?1 will tell the browser that your origin is isolated from other subdomains. This allows your application to be loaded in its own process. It also disables document.domain, which allowed subdomains to act as if they were the same origin.
Permissions
The permissions policy will prevent the page from using browser APIs that access the user’s device. Anything that can access their camera or take money from them should be blocked. Fullscreen and autoplay are only for displaying content and already require user interaction, so I leave them out.
Because you’re blocking third-party content from loading in your CSP, this is mainly a guard against your own origin serving a script it shouldn’t. This header is currently only supported by Chromium browsers.
If you require one of these features, you can set it to self. You should leave everything else blocked, following the principle of least privilege.
Privacy and transport security
Referrer-Policy: no-referrer will prevent you from leaking information via the referrer. It’ll also strip the referrer on same-origin requests, so if you use the Referer header for CSRF or tracking, you’ll want to use strict-origin-when-cross-origin.
Strict-Transport-Security forces HTTPS and can be preloaded in browsers. You’ll need to have a max-age of at least 1 year and includeSubDomains to submit your domain to the preload list. It can take months to get removed from the preload list, so make sure you own all subdomains and plan on keeping them.
Legacy headers
X-Content-Type-Options: nosniff prevents content sniffing and is the only X- header I think is still useful. Here are some headers you’ll see in many examples that you don’t need:
X-Download-Optionswas for IE8.X-Permitted-Cross-Domain-Policieswas for Flash.X-XSS-Protectioncontrols an XSS filter that no current browser ships.X-DNS-Prefetch-Controldoesn’t buy much, since browsers don’t prefetch link hostnames on HTTPS pages by default.
You can always set Server "teapot" in honor of RFC 2324 and confuse bots with your lack of coffee. Removing the header keeps Caddy from announcing itself on every response.
This should cover most of your needs when starting a new web application. When you require certain features, loosen one policy at a time so you know what you’ve enabled and why.