How to Configure Security Headers on Netlify

How-To Guide

How to Configure Security Headers on Netlify

Protect your Netlify-deployed app with essential HTTP headers

TL;DR

TL;DR (15 minutes)

Add security headers on Netlify using a _headers file in your publish directory or [[headers]] sections in netlify.toml. For dynamic headers like CSP nonces, use Edge Functions. Configure X-Content-Type-Options, X-Frame-Options, HSTS, Referrer-Policy, and Content-Security-Policy.

Prerequisites

  • A site deployed on Netlify (or ready to deploy)
  • Access to your project's Git repository
  • Knowledge of your publish directory (dist, build, public, etc.)
  • Basic understanding of HTTP headers

Three Ways to Add Headers on Netlify

MethodBest ForDynamic Headers
_headers fileSimple, readable configurationNo
netlify.tomlComplex configs, environment-specificNo
Edge FunctionsDynamic CSP, nonces, logicYes

Method 1: Using _headers File

The _headers file is the simplest option. One file, no extra configuration syntax.

1

Create _headers in your publish directory

Create a file named _headers (no extension) in your publish directory:

# Security headers for all pages
/*
  X-Content-Type-Options: nosniff
  X-Frame-Options: DENY
  X-XSS-Protection: 1; mode=block
  Referrer-Policy: strict-origin-when-cross-origin
  Permissions-Policy: camera=(), microphone=(), geolocation=(), interest-cohort=()
  Strict-Transport-Security: max-age=31536000; includeSubDomains
  Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' https://fonts.gstatic.com; connect-src 'self' https:; frame-ancestors 'none'; base-uri 'self'; form-action 'self';
2

Add route-specific headers

# Default headers for all pages
/*
  X-Content-Type-Options: nosniff
  X-Frame-Options: DENY
  Referrer-Policy: strict-origin-when-cross-origin
  Strict-Transport-Security: max-age=31536000; includeSubDomains

# API routes - no caching, strict headers
/api/*
  Cache-Control: no-store, max-age=0
  X-Content-Type-Options: nosniff
  Content-Type: application/json

# Allow embedding for widget
/widget
  X-Frame-Options: SAMEORIGIN
  Content-Security-Policy: frame-ancestors 'self' https://trusted-domain.com;

# Static assets - long cache
/assets/*
  Cache-Control: public, max-age=31536000, immutable

# Admin area - strictest CSP
/admin/*
  Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self'; frame-ancestors 'none';
3

Ensure _headers is in the right location

The file must end up in your publish directory after build. Common locations:

FrameworkPlace _headers inPublish Directory
Create React Apppublic/build/
Next.js (static)public/out/
Gatsbystatic/public/
Vue CLIpublic/dist/
Astropublic/dist/
Hugostatic/public/

Method 2: Using netlify.toml

netlify.toml is more verbose, but it lets you scope headers by environment (production vs. deploy preview) and consolidate all your Netlify configuration in one place.

1

Create or update netlify.toml in project root

# Build configuration
[build]
  publish = "dist"
  command = "npm run build"

# Security headers for all routes
[[headers]]
  for = "/*"
  [headers.values]
    X-Content-Type-Options = "nosniff"
    X-Frame-Options = "DENY"
    X-XSS-Protection = "1; mode=block"
    Referrer-Policy = "strict-origin-when-cross-origin"
    Permissions-Policy = "camera=(), microphone=(), geolocation=()"
    Strict-Transport-Security = "max-age=31536000; includeSubDomains"
    Content-Security-Policy = "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; connect-src 'self' https:; frame-ancestors 'none';"
2

Add route-specific headers

# Default security headers
[[headers]]
  for = "/*"
  [headers.values]
    X-Content-Type-Options = "nosniff"
    X-Frame-Options = "DENY"
    Referrer-Policy = "strict-origin-when-cross-origin"
    Strict-Transport-Security = "max-age=31536000; includeSubDomains"

# API routes
[[headers]]
  for = "/api/*"
  [headers.values]
    Cache-Control = "no-store, max-age=0"
    X-Content-Type-Options = "nosniff"

# Static assets with long cache
[[headers]]
  for = "/assets/*"
  [headers.values]
    Cache-Control = "public, max-age=31536000, immutable"

# Admin with strict CSP
[[headers]]
  for = "/admin/*"
  [headers.values]
    Content-Security-Policy = "default-src 'self'; script-src 'self'; style-src 'self'; frame-ancestors 'none';"
3

Use environment-specific headers (advanced)

# Production headers
[context.production]
  [[context.production.headers]]
    for = "/*"
    [context.production.headers.values]
      Strict-Transport-Security = "max-age=31536000; includeSubDomains; preload"
      Content-Security-Policy = "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline';"

# Deploy preview headers (less strict for testing)
[context.deploy-preview]
  [[context.deploy-preview.headers]]
    for = "/*"
    [context.deploy-preview.headers.values]
      Content-Security-Policy = "default-src 'self' 'unsafe-inline' 'unsafe-eval'; img-src 'self' data: https:;"

Method 3: Using Edge Functions (Dynamic Headers)

The _headers file and netlify.toml are static. If you need a CSP nonce per request (required for strict CSP without unsafe-inline), you'll need an Edge Function instead.

1

Create Edge Function

Create netlify/edge-functions/security-headers.ts:

import type { Context } from "@netlify/edge-functions";

export default async function handler(request: Request, context: Context) {
  // Get the response from the origin
  const response = await context.next();

  // Generate a unique nonce for CSP
  const nonce = crypto.randomUUID().replace(/-/g, '');

  // Build CSP with nonce
  const csp = [
    "default-src 'self'",
    `script-src 'self' 'nonce-${nonce}' 'strict-dynamic'`,
    `style-src 'self' 'nonce-${nonce}'`,
    "img-src 'self' blob: data: https:",
    "font-src 'self'",
    "object-src 'none'",
    "base-uri 'self'",
    "form-action 'self'",
    "frame-ancestors 'none'",
    "upgrade-insecure-requests"
  ].join('; ');

  // Clone headers and add security headers
  const headers = new Headers(response.headers);
  headers.set('Content-Security-Policy', csp);
  headers.set('X-Content-Type-Options', 'nosniff');
  headers.set('X-Frame-Options', 'DENY');
  headers.set('X-XSS-Protection', '1; mode=block');
  headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');
  headers.set('Permissions-Policy', 'camera=(), microphone=(), geolocation=()');
  headers.set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
  headers.set('X-Nonce', nonce);

  return new Response(response.body, {
    status: response.status,
    statusText: response.statusText,
    headers
  });
}

export const config = {
  path: "/*",
  excludedPath: ["/api/*", "/_next/*", "/assets/*"]
};
2

Configure Edge Function in netlify.toml

[[edge_functions]]
  function = "security-headers"
  path = "/*"
3

Access nonce in your HTML (optional)

For server-rendered pages, you can read the nonce from the X-Nonce header:

// In your server-side code or build process
export async function getServerSideProps({ req }) {
  const nonce = req.headers['x-nonce'] || '';

  return {
    props: { nonce }
  };
}

// In your component
function MyPage({ nonce }) {
  return (
    <html>
      <head>
        <script nonce={nonce}>
          console.log('Inline script with nonce');
        </script>
      </head>
      <body>...</body>
    </html>
  );
}

Before you go live: confirm the _headers file landed in your publish directory, not the project root. Run netlify build --dry to catch netlify.toml syntax errors. Test your CSP with Content-Security-Policy-Report-Only before switching to enforcement mode. Third-party scripts (analytics, chat widgets) need explicit CSP allowances. Start HSTS with max-age=300 for a week before committing to 31536000.

How to Verify It Worked

Netlify Deploy Log

In the Netlify dashboard, open the deployment and expand the deploy log. Look for "Processing headers" messages and any warnings about _headers or netlify.toml.

Netlify CLI

# Install Netlify CLI
npm install -g netlify-cli

# Test your configuration locally
netlify dev

# Check what headers would be applied
netlify build --dry

Browser DevTools

Open your deployed site, press F12, go to the Network tab, reload the page, click the document request, and check Response Headers.

Command Line

# Check headers with curl
curl -I https://your-site.netlify.app

# Expected output:
# HTTP/2 200
# x-content-type-options: nosniff
# x-frame-options: DENY
# strict-transport-security: max-age=31536000; includeSubDomains
# referrer-policy: strict-origin-when-cross-origin
# content-security-policy: default-src 'self'; ...

Common Errors and Troubleshooting

_headers file not working

The _headers file must be in your publish directory, not the project root. It must be named exactly _headers with no extension. If your build process doesn't copy it there, it won't ship. Check for syntax errors: path on its own line, headers indented below it.

netlify.toml headers not applying

Run netlify build --dry to catch TOML syntax errors before they cost you a deploy. If _headers and netlify.toml both define the same route, they get merged; check for conflicts. Also confirm your for path pattern actually matches the route you're testing.

CSP blocking content

Open the browser console: CSP violations show up with detailed messages about exactly which directive blocked which resource. Use Content-Security-Policy-Report-Only to collect violations without blocking, then fix the allowlist before switching to enforcement.

Edge Function not running

Edge Functions must live in netlify/edge-functions/. The config export with a path value is required. If it's missing, the function registers but never runs. Check Edge Function logs in the Netlify dashboard under Functions.

Each Netlify PR gets its own deploy preview URL. Test your security headers there first. Mistakes in CSP can break your analytics, fonts, or third-party scripts in ways that aren't obvious until real users hit the page.

Frequently Asked Questions

Where should I put the _headers file on Netlify?

The _headers file must be in your publish directory (the folder Netlify serves). For most projects, this is 'public', 'dist', or 'build'. For static site generators, place it in your static assets folder so it gets copied to the build output.

Should I use _headers or netlify.toml for security headers?

_headers is simpler and easier to read at a glance. netlify.toml is worth using when you need environment-specific headers (e.g., stricter CSP in production, looser in deploy previews) or you're already using it for build config. You can use both at once; they get merged, with netlify.toml winning on conflicts.

Does Netlify add any security headers by default?

Netlify adds some basic headers like X-NF-Request-ID for tracking, but it doesn't add security headers by default. You have to configure them yourself via _headers, netlify.toml, or Edge Functions.

Why aren't my _headers working on Netlify?

Most likely causes: the file isn't in the publish directory, the filename has a typo or extension, there's a syntax error in the file, or netlify.toml has conflicting headers that override yours. The deploy log usually tells you which one.

Can I have different headers for different routes on Netlify?

Yes. In _headers, add separate sections for each path. In netlify.toml, add multiple [[headers]] blocks with different 'for' values. You can use wildcards like /api/* or /admin/*.

Scan Your Netlify Site

Check if your Netlify-hosted site has all the security headers configured correctly.

How-To Guides

How to Configure Security Headers on Netlify