HTTP Security Headers a Caddy Web Server Configuration

Written by Paul Bradley

midjourney ai - anthropomorphic shark gladiator

Caddyfile Configuration for Security Headers

I’ve been using Caddy as my web server for the last four years. It’s been rock solid and stable. Consider it if you’re looking for a modern web server. Don’t default to Apache or NGINX; consider the alternatives.

I’d not updated the configuration file for a long time, so I reviewed the HTTP headers that the server emits with each request today. In particular, I’ve focussed on the HTTP headers used to control security.

As such, I’ve added Cross-Origin-Opener-Policy, which provides a way for a document to isolate itself from cross-origin windows opened through window.open() without rel=“noopener” — this is useful, as it means I don’t have to add the rel tag to all links.

I’ve also added the Cross-Origin-Embedder-Policy header, which prevents documents and workers from loading cross-origin resources such as images, scripts and stylesheets.

My current Caddyfile configuration for HTTP headers:

 1header * {
 2    Access-Control-Allow-Origin "https://du89og34n262.cloudfront.net"
 3    Content-Security-Policy "default-src 'self' blob: data: https://du89og34n262.cloudfront.net https://www.youtube-nocookie.com; font-src 'self'; style-src 'self' 'unsafe-inline'; script-src 'self' blob: 'unsafe-inline' 'unsafe-eval' https://www.youtube-nocookie.com; connect-src 'self' https://du89og34n262.cloudfront.net;"
 4    Cross-Origin-Embedder-Policy "require-corp"
 5    Cross-Origin-Opener-Policy "same-origin-allow-popups"
 6    Cross-Origin-Resource-Policy "same-origin"
 7    Permissions-Policy "accelerometer=(self), ambient-light-sensor=(self), autoplay=(self), battery=(self), camera=(self), cross-origin-isolated=(self), display-capture=(self), document-domain=(self), encrypted-media=(self), execution-while-not-rendered=(self), execution-while-out-of-viewport=(self), fullscreen=(self), geolocation=(self), gyroscope=(self), keyboard-map=(self), magnetometer=(self), microphone=(self), midi=(self), navigation-override=(self), payment=(self), picture-in-picture=(self), publickey-credentials-get=(self), screen-wake-lock=(self), sync-xhr=(self), usb=(self), web-share=(self), xr-spatial-tracking=(self)"
 8    Server "paulbradley.dev"
 9    Strict-Transport-Security max-age=31536000;
10    X-Content-Type-Options nosniff
11    X-Frame-Options DENY
12    X-XSS-Protection "0"
13}

Different Content-Type Headers for static files

You should control how your content is cached at intermediary caches and ultimately within your visitors browsers.

This is done using the cache control HTTP header. For your web pages you might choose not to cache your pages, so that visitors always get the latest version of your web page. Setting the max-age directive to zero achieves this. The must-revalidate response directive indicates that the response can be stored in caches and can be reused while fresh. If the response becomes stale, it must be validated with the origin server before reuse.

It’s common web best practice to allow static assets, those that don’t change often, to have a long cache expiry duration. These are typically called immutable static assets. In the configuration below we define a list of file paths for files that should be consider static assets.

If the file the web server is severing matches one of these paths then it’s issued a different cache control header. As you can see, these immutable files are given an expiry date of one year (31536000 seconds). They are also given the immutable directive, which indicates that the response will not be updated while it’s fresh.

 1@immutable {
 2    path /img/*
 3    path /js/*
 4    path /css/*
 5}
 6
 7route {
 8    header Cache-Control public,max-age=0,must-revalidate
 9    header @immutable Cache-Control public,max-age=31536000,immutable
10}

Versions