How to add security headers on Vercel

Six HTTP response headers cover the standard hardening checklist. Below: what each one does (honestly — one of them is already the browser default), then the exact Vercel configuration.

The headers, and what they actually buy you

Strict-Transport-Security max-age=31536000; includeSubDomains
Tells browsers to only ever use HTTPS for this domain (for 1 year). ⚠ includeSubDomains applies the rule to EVERY subdomain — any HTTP-only subdomain (an old intranet host, a device panel) becomes unreachable until the max-age expires; drop that token if you have one. Only meaningful on HTTPS responses — browsers ignore it over HTTP (RFC 6797). Add the preload token only deliberately: preloading is hard to undo (hstspreload.org).
X-Content-Type-Options nosniff
Stops browsers from guessing content types — the only valid value.
X-Frame-Options DENY
Blocks your site from being framed (clickjacking). The modern replacement is CSP frame-ancestors; keeping X-Frame-Options alongside covers older browsers. Use SAMEORIGIN instead if your site frames itself.
Referrer-Policy strict-origin-when-cross-origin
This is already the browser default — setting it makes your policy explicit rather than fixing a hole. Use no-referrer for the strictest option.
Permissions-Policy camera=(), microphone=(), geolocation=()
Disables powerful browser features your site doesn't use. Minimal sane starting set; extend with more features as needed.
Content-Security-Policy (not copy-pasteable — start with Content-Security-Policy-Report-Only)
CSP is site-specific: a canned policy breaks any third-party script or style you load. Start with Content-Security-Policy-Report-Only to observe violations without breaking anything, then tighten into an enforced policy.

On Vercel

vercel.json (project root)

{
  "headers": [
    {
      "source": "/(.*)",
      "headers": [
        { "key": "Strict-Transport-Security", "value": "max-age=63072000; includeSubDomains" },
        { "key": "X-Content-Type-Options", "value": "nosniff" },
        { "key": "X-Frame-Options", "value": "DENY" },
        { "key": "Referrer-Policy", "value": "strict-origin-when-cross-origin" },
        { "key": "Permissions-Policy", "value": "camera=(), microphone=(), geolocation=()" }
      ]
    }
  ]
}

next.config.js (Next.js projects)

module.exports = {
  async headers() {
    return [
      {
        source: '/:path*',
        headers: [
          { key: 'Strict-Transport-Security', value: 'max-age=63072000; includeSubDomains' },
          { key: 'X-Content-Type-Options', value: 'nosniff' },
          { key: 'X-Frame-Options', value: 'DENY' },
          { key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
          { key: 'Permissions-Policy', value: 'camera=(), microphone=(), geolocation=()' },
        ],
      },
    ]
  },
}

Vercel gotchas worth knowing

  • Vercel already sends Strict-Transport-Security (max-age=63072000, 2 years) by default on every deployment — the snippet keeps that max-age and adds includeSubDomains; don't set a lower max-age than the default.
  • In Next.js, when two rules match the same path and header key, the last one wins.

Verified against Vercel: vercel.json headers on 2026-07-12.

Check what your site sends today

The full domain report includes your live response headers, so you can verify the change took effect.

Run a domain report →