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-Securitymax-age=31536000; includeSubDomains
Content-Security-Policydefault-src 'self'; object-src 'none'; base-uri 'none'; frame-ancestors 'none'
X-Content-Type-Optionsnosniff
Referrer-Policystrict-origin-when-cross-origin
Permissions-Policygeolocation=(), camera=(), microphone=()
Cross-Origin-Opener-Policysame-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();
});