./ahmedhashim

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.