Security headers tell the browser how to treat your site. They cost nothing, they apply to every page, and many sites still don’t send them.
The headers to set
| Header | What it does |
|---|---|
Strict-Transport-Security |
Tells browsers to always use HTTPS |
Content-Security-Policy |
Limits where scripts, styles and other resources can load from |
X-Content-Type-Options |
Stops browsers guessing file types |
X-Frame-Options |
Stops other sites putting yours in a frame |
Referrer-Policy |
Controls how much of your URL is shared with other sites |
Permissions-Policy |
Turns off browser features you don’t use |
Setting them on a static host
Cloudflare and Netlify both read a _headers file. In an Astro project, put it in public/ so it’s copied into the build:
/*
Strict-Transport-Security: max-age=31536000; includeSubDomains
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: camera=(), microphone=(), geolocation=()
Vercel sets headers in vercel.json. On a site with a server, you can set them in Astro middleware.
The Content-Security-Policy trap
A strict policy blocks inline scripts, and Astro can inline small scripts into your HTML to save a request. The page still loads and returns a 200, so a broken button is easy to miss. We hit exactly this on our own sign-in page.
One fix is to stop Vite inlining anything:
// astro.config.mjs
export default defineConfig({
vite: { build: { assetsInlineLimit: 0 } },
});
Then search the build for leftover inline scripts, and check the browser console on any page with a form. Roll out a new policy carefully, because a policy that’s too strict can break pages without any obvious error. Content-Security-Policy-Report-Only lets you test one first.
Check the result
Look at the response headers on your live site, not your config, because a CDN can add or strip them. Em Region grades the headers on every site in your fleet.