Security Headers - The Final, No-BS Reference

The complete, modern security headers setup (HSTS, CSP, nosniff, Referrer-Policy, Permissions-Policy) with simple explanations and copy-paste configs.

5 min read

If you ship websites or web apps, you want two things:

  1. Real security (not scanner theater)
  2. A setup you can copy-paste everywhere and only adapt when features change

This page gives you exactly that.

You’ll leave with:

  • The only headers that actually matter
  • Base values that work almost everywhere
  • Clear rules for where each header belongs
  • A simple way to test and verify
  • No fluff, no outdated advice, no fear-based BS

The modern set (the only ones you need)

The current, no-doubt set is:

  1. Strict-Transport-Security (HSTS)
  2. Content-Security-Policy (CSP)
  3. X-Content-Type-Options
  4. Referrer-Policy
  5. Permissions-Policy

That’s it.

Everything else is:

  • deprecated
  • redundant
  • or advanced/optional

Where each header should live

Rule you can keep forever:

  • HSTS → Edge / CDN / platform
  • Everything else → Application code

Why:

  • Identical behavior across environments
  • No forgotten dashboard toggles
  • CSP stays versioned and intentional

Copy-paste: the Core 5 (base values)

Use this as your reference.
You only add domains to CSP when your site needs them.

# 1) HSTS (edge / CDN)
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload

# 2) CSP (application code)
Content-Security-Policy:
  default-src 'self';
  script-src 'self' 'nonce-{{NONCE}}';
  connect-src 'self';
  img-src 'self' data: https:;
  style-src 'self' 'unsafe-inline';
  font-src 'self';
  frame-src 'self';
  media-src 'self';
  object-src 'none';
  base-uri 'self';
  form-action 'self';
  frame-ancestors 'none';

# 3) MIME hardening
X-Content-Type-Options: nosniff

# 4) Referrer control
Referrer-Policy: strict-origin-when-cross-origin

# 5) Browser feature lockdown
Permissions-Policy:
  camera=(),
  microphone=(),
  geolocation=(),
  accelerometer=(),
  gyroscope=(),
  magnetometer=(),
  usb=(),
  payment=(),
  fullscreen=(self)

1) HSTS — Strict-Transport-Security

What it does

Tells the browser:

“This site is HTTPS-only. Never use HTTP.”

After the browser learns this, it upgrades requests before they are sent.

Why redirects are not enough

Redirects still start on HTTP.
HSTS removes HTTP entirely.

Base value

Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
  • max-age: how long the browser remembers (1 year is standard)
  • includeSubDomains: applies to subdomains
  • preload: allows browser preload lists (only enable if you’re sure)

Where to set it

Set this at:

  • Cloudflare
  • CDN
  • load balancer

Not in app code.


How to check HSTS

Open DevTools → Network → click the document request → Response headers.

If you see Strict-Transport-Security, it’s set.

Redirects (301/302/307) ≠ HSTS.


2) CSP — Content-Security-Policy

What it does

CSP tells the browser:

“Only load and execute what I explicitly allow.”

This is the strongest browser-level protection against:

  • XSS
  • script injection
  • malicious third-party scripts
  • data exfiltration

CSP is a living file

CSP changes when features change.

That’s normal.

Add a service → add its domain.
Remove a service → remove its domain.


default-src 'self';
script-src 'self' 'nonce-{{NONCE}}';
connect-src 'self';
img-src 'self' data: https:;
style-src 'self' 'unsafe-inline';
font-src 'self';
frame-src 'self';
media-src 'self';
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'none';

Why nonces?

Only scripts you explicitly mark can run.
This is modern, strong, and scalable.

Why unsafe-inline for styles?

Pragmatism. Many SSR setups rely on inline styles.

You can tighten this later if needed.


Real-world extensions

YouTube embeds

frame-src 'self' https://www.youtube.com https://www.youtube-nocookie.com;

Stripe

script-src 'self' 'nonce-{{NONCE}}' https://js.stripe.com;
connect-src 'self' https://api.stripe.com;
frame-src 'self' https://js.stripe.com https://hooks.stripe.com;

PostHog (EU)

script-src 'self' 'nonce-{{NONCE}}' https://eu.i.posthog.com https://eu-assets.i.posthog.com;
connect-src 'self' https://eu.i.posthog.com https://eu-assets.i.posthog.com;

Report-Only mode

Start with:

Content-Security-Policy-Report-Only: ...

Then enforce once clean.


3) X-Content-Type-Options

What it does

Stops browsers from guessing file types.

Prevents files being treated as scripts accidentally.

Value

X-Content-Type-Options: nosniff

Set-and-forget.


4) Referrer-Policy

What it does

Controls how much URL information is shared when users click links.

Best default

Referrer-Policy: strict-origin-when-cross-origin
  • Same-site → full referrer
  • Cross-site → origin only
  • HTTPS → HTTP → nothing

5) Permissions-Policy

What it does

Disables powerful browser features unless you allow them.

Best default

Permissions-Policy:
  camera=(),
  microphone=(),
  geolocation=(),
  accelerometer=(),
  gyroscope=(),
  magnetometer=(),
  usb=(),
  payment=(),
  fullscreen=(self)

Astro implementation (SSR)

Create src/middleware.ts:

import type { MiddlewareHandler } from "astro";

export const onRequest: MiddlewareHandler = async (context, next) => {
  const nonce = crypto.randomUUID();
  context.locals.nonce = nonce;

  const response = await next();

  response.headers.set(
    "Content-Security-Policy",
    [
      "default-src 'self'",
      `script-src 'self' 'nonce-${nonce}'`,
      "connect-src 'self'",
      "img-src 'self' data: https:",
      "style-src 'self' 'unsafe-inline'",
      "font-src 'self'",
      "frame-src 'self'",
      "media-src 'self'",
      "object-src 'none'",
      "base-uri 'self'",
      "form-action 'self'",
      "frame-ancestors 'none'",
    ].join("; ")
  );

  response.headers.set("X-Content-Type-Options", "nosniff");
  response.headers.set("Referrer-Policy", "strict-origin-when-cross-origin");
  response.headers.set(
    "Permissions-Policy",
    [
      "camera=()",
      "microphone=()",
      "geolocation=()",
      "accelerometer=()",
      "gyroscope=()",
      "magnetometer=()",
      "usb=()",
      "payment=()",
      "fullscreen=(self)",
    ].join(", ")
  );

  return response;
};

Testing locally

curl -I http://localhost:4321

Or check DevTools → Network → document request.


Production verification

Use:

  • securityheaders.com
  • headerscan.com
  • MDN Observatory

Scanners are secondary.
Browser Network tab is the source of truth.


Common confusion (read once)

Redirects ≠ HSTS

HSTS is a header, not a redirect.

Cloudflare adds headers sometimes

Do not rely on that. Set your policy in code.


Cookies

Use:

  • Secure
  • HttpOnly
  • SameSite=Lax
Set-Cookie: session=...; Secure; HttpOnly; SameSite=Lax; Path=/;

CORS

Access policy, not security.

Report-To / NEL

Telemetry, often injected by CDNs.

SRI

HTML attribute, not a header.

COOP / COEP / CORP

Advanced isolation. Skip unless you know exactly why you need them.


Final checklist

  • HTTPS everywhere
  • HSTS at edge
  • CSP with nonces
  • nosniff
  • Referrer-Policy
  • Permissions-Policy
  • Verified in browser

You’re done.


⚠️ 🚧 Website under construction! 🤪 🏗️