From F to A+ on securityheaders.com
Six HTTP headers took my site from an F to an A+. The last step meant moving every bit of JavaScript out of the page.
- Goal
- Make browsers enforce safe behaviour on my site
- Result
- F to A+
- Tools
- Caddy, Content Security Policy, curl
Why headers matter
Security headers are instructions the server sends with every page, telling the browser how to protect visitors. My site started with none, and securityheaders.com gave it an F.
What I added in Caddy
- Strict-Transport-Security: only ever load the site over HTTPS.
- Content-Security-Policy: the page can only load scripts, styles and fonts from places I list.
- X-Frame-Options: other sites can't put my page in a frame (stops clickjacking).
- X-Content-Type-Options: the browser mustn't guess file types.
- Referrer-Policy and Permissions-Policy: share less, and turn off camera, microphone and location.
I also hid the Server header so the site doesn't advertise what it runs. That got an A.
A to A+
It was capped at A because the policy still allowed inline scripts ('unsafe-inline'), since all the page's code was inside the HTML. That's exactly what cross-site scripting attacks rely on. I moved the code into separate .js files and removed 'unsafe-inline' from script-src. Now even if someone managed to inject code into the page, the browser would refuse to run it.
$ curl -sI https://haydnhomelab.com | grep -i content-security content-security-policy: default-src 'self'; script-src 'self' https://static.cloudflareinsights.com; style-src ... frame-ancestors 'none'
What tripped me up: the first rescan still showed the old grade. curl -sI on the Pi proved the new header was live, and scanning over https:// gave A+.
What I learned
- A Content Security Policy is only strong if it doesn't allow inline scripts.
- Check the real response with
curlinstead of trusting a cached scan.
How I used AI: Claude wrote the Caddy config and moved the page's code into separate files. I deployed it on the Pi and ran the scans. The full, unedited notes are in my build log.