What good looks like
The full A-grade header set, ready to paste into your stack.
Jump to: Nginx · Apache (.htaccess or vhost) · Caddy · Netlify / Cloudflare Pages (_headers file) · Cloudflare Workers · Express + Helmet
Don't want the whole set? Generate a fix pack with only what your site is missing.
The target
Serve everything over HTTPS with a 301 from plain HTTP, then send these
headers on every response. Cookie flags (Secure; HttpOnly; SameSite)
are set per-cookie in your app, not here. Treat the CSP as a starting point —
tighten or loosen default-src to match what your pages load.
| Strict-Transport-Security | max-age=31536000; includeSubDomains |
| Content-Security-Policy | default-src 'self'; object-src 'none'; base-uri 'none'; frame-ancestors 'none' |
| X-Content-Type-Options | nosniff |
| Referrer-Policy | strict-origin-when-cross-origin |
| Permissions-Policy | geolocation=(), camera=(), microphone=() |
| Cross-Origin-Opener-Policy | same-origin |
Per stack
Nginx
Goes in your HTTPS server block. The first block 301s all plain-HTTP traffic; server_tokens off hides the version from the Server header.
# Redirect all plain-HTTP traffic to HTTPS
server {
listen 80;
listen [::]:80;
server_name example.com;
return 301 https://$host$request_uri;
}
# Inside your HTTPS server block:
server_tokens off;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header Content-Security-Policy "default-src 'self'; object-src 'none'; base-uri 'none'; frame-ancestors 'none'" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "geolocation=(), camera=(), microphone=()" always;
add_header Cross-Origin-Opener-Policy "same-origin" always;
Apache (.htaccess or vhost)
Requires mod_headers and mod_rewrite. ServerTokens Prod (server config only, not .htaccess) trims the version from the Server header.
# Redirect all plain-HTTP traffic to HTTPS
RewriteEngine On
RewriteCond %{HTTPS} !=on
RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"
Header always set Content-Security-Policy "default-src 'self'; object-src 'none'; base-uri 'none'; frame-ancestors 'none'"
Header always set X-Content-Type-Options "nosniff"
Header always set Referrer-Policy "strict-origin-when-cross-origin"
Header always set Permissions-Policy "geolocation=(), camera=(), microphone=()"
Header always set Cross-Origin-Opener-Policy "same-origin"
Header always unset X-Powered-By
# In the main server config (not .htaccess):
ServerTokens Prod
Caddy
Caddy serves HTTPS and redirects HTTP automatically, so only the headers need declaring.
example.com {
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains"
Content-Security-Policy "default-src 'self'; object-src 'none'; base-uri 'none'; frame-ancestors 'none'"
X-Content-Type-Options "nosniff"
Referrer-Policy "strict-origin-when-cross-origin"
Permissions-Policy "geolocation=(), camera=(), microphone=()"
Cross-Origin-Opener-Policy "same-origin"
-Server
}
}
Netlify / Cloudflare Pages (_headers file)
A plain-text _headers file at the root of your publish directory. Both platforms redirect HTTP to HTTPS for you. On Vercel, set the same headers in vercel.json instead.
/*
Strict-Transport-Security: max-age=31536000; includeSubDomains
Content-Security-Policy: default-src 'self'; object-src 'none'; base-uri 'none'; frame-ancestors 'none'
X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: geolocation=(), camera=(), microphone=()
Cross-Origin-Opener-Policy: same-origin
Cloudflare Workers
Set the headers on every Response your Worker returns — a spread over a shared object keeps them in one place. (This is how Canopy does it.)
const SECURITY_HEADERS = {
"strict-transport-security": "max-age=31536000; includeSubDomains",
"content-security-policy": "default-src 'self'; object-src 'none'; base-uri 'none'; frame-ancestors 'none'",
"x-content-type-options": "nosniff",
"referrer-policy": "strict-origin-when-cross-origin",
"permissions-policy": "geolocation=(), camera=(), microphone=()",
"cross-origin-opener-policy": "same-origin",
};
export default {
async fetch(request) {
return new Response("hello", {
headers: { "content-type": "text/plain", ...SECURITY_HEADERS },
});
},
};
Express + Helmet
Helmet sets most of this by default; the options below align it exactly with the set Canopy grades. Disabling x-powered-by hides the framework fingerprint.
const helmet = require("helmet");
app.disable("x-powered-by");
app.use(helmet({
strictTransportSecurity: { maxAge: 31536000, includeSubDomains: true },
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
objectSrc: ["'none'"],
baseUri: ["'none'"],
frameAncestors: ["'none'"],
},
},
referrerPolicy: { policy: "strict-origin-when-cross-origin" },
crossOriginOpenerPolicy: { policy: "same-origin" },
}));
// Permissions-Policy isn't covered by Helmet — set it directly:
app.use((req, res, next) => {
res.setHeader("Permissions-Policy", "geolocation=(), camera=(), microphone=()");
next();
});