[{"data":1,"prerenderedAt":717},["ShallowReactive",2],{"blog-how-to/netlify-headers":3},{"id":4,"title":5,"body":6,"category":688,"date":689,"dateModified":689,"description":690,"draft":691,"extension":692,"faq":693,"featured":691,"headerVariant":703,"image":704,"keywords":704,"meta":705,"navigation":706,"ogDescription":707,"ogTitle":704,"path":708,"readTime":704,"schemaOrg":709,"schemaType":710,"seo":711,"sitemap":712,"stem":713,"tags":714,"twitterCard":715,"__hash__":716},"blog/blog/how-to/netlify-headers.md","How to Configure Security Headers on Netlify",{"type":7,"value":8,"toc":653},"minimark",[9,13,17,21,43,48,64,68,122,126,129,154,167,253,257,260,272,283,295,299,302,321,333,348,377,381,385,403,407,413,417,437,441,447,451,455,485,489,515,519,543,547,570,579,583,617,629],[10,11],"category-badge",{"category":12},"How-To Guide",[14,15,5],"h1",{"id":16},"how-to-configure-security-headers-on-netlify",[18,19,20],"p",{},"Protect your Netlify-deployed app with essential HTTP headers",[22,23,24,27],"tldr",{},[18,25,26],{},"TL;DR (15 minutes)",[18,28,29,30,34,35,38,39,42],{},"Add security headers on Netlify using a ",[31,32,33],"code",{},"_headers"," file in your publish directory or ",[31,36,37],{},"[[headers]]"," sections in ",[31,40,41],{},"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.",[44,45,47],"h2",{"id":46},"prerequisites","Prerequisites",[49,50,51,55,58,61],"ul",{},[52,53,54],"li",{},"A site deployed on Netlify (or ready to deploy)",[52,56,57],{},"Access to your project's Git repository",[52,59,60],{},"Knowledge of your publish directory (dist, build, public, etc.)",[52,62,63],{},"Basic understanding of HTTP headers",[44,65,67],{"id":66},"three-ways-to-add-headers-on-netlify","Three Ways to Add Headers on Netlify",[69,70,71,87],"table",{},[72,73,74],"thead",{},[75,76,77,81,84],"tr",{},[78,79,80],"th",{},"Method",[78,82,83],{},"Best For",[78,85,86],{},"Dynamic Headers",[88,89,90,102,111],"tbody",{},[75,91,92,96,99],{},[93,94,95],"td",{},"_headers file",[93,97,98],{},"Simple, readable configuration",[93,100,101],{},"No",[75,103,104,106,109],{},[93,105,41],{},[93,107,108],{},"Complex configs, environment-specific",[93,110,101],{},[75,112,113,116,119],{},[93,114,115],{},"Edge Functions",[93,117,118],{},"Dynamic CSP, nonces, logic",[93,120,121],{},"Yes",[44,123,125],{"id":124},"method-1-using-_headers-file","Method 1: Using _headers File",[18,127,128],{},"The simplest and most readable approach.",[130,131,133,138,144],"step",{"number":132},"1",[134,135,137],"h3",{"id":136},"create-_headers-in-your-publish-directory","Create _headers in your publish directory",[18,139,140,141,143],{},"Create a file named ",[31,142,33],{}," (no extension) in your publish directory:",[145,146,151],"pre",{"className":147,"code":149,"language":150},[148],"language-text","# Security headers for all pages\n/*\n  X-Content-Type-Options: nosniff\n  X-Frame-Options: DENY\n  X-XSS-Protection: 1; mode=block\n  Referrer-Policy: strict-origin-when-cross-origin\n  Permissions-Policy: camera=(), microphone=(), geolocation=(), interest-cohort=()\n  Strict-Transport-Security: max-age=31536000; includeSubDomains\n  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';\n","text",[31,152,149],{"__ignoreMap":153},"",[130,155,157,161],{"number":156},"2",[134,158,160],{"id":159},"add-route-specific-headers","Add route-specific headers",[145,162,165],{"className":163,"code":164,"language":150},[148],"# Default headers for all pages\n/*\n  X-Content-Type-Options: nosniff\n  X-Frame-Options: DENY\n  Referrer-Policy: strict-origin-when-cross-origin\n  Strict-Transport-Security: max-age=31536000; includeSubDomains\n\n# API routes - no caching, strict headers\n/api/*\n  Cache-Control: no-store, max-age=0\n  X-Content-Type-Options: nosniff\n  Content-Type: application/json\n\n# Allow embedding for widget\n/widget\n  X-Frame-Options: SAMEORIGIN\n  Content-Security-Policy: frame-ancestors 'self' https://trusted-domain.com;\n\n# Static assets - long cache\n/assets/*\n  Cache-Control: public, max-age=31536000, immutable\n\n# Admin area - strictest CSP\n/admin/*\n  Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self'; frame-ancestors 'none';\n",[31,166,164],{"__ignoreMap":153},[130,168,170,174,177],{"number":169},"3",[134,171,173],{"id":172},"ensure-_headers-is-in-the-right-location","Ensure _headers is in the right location",[18,175,176],{},"The file must end up in your publish directory after build. Common locations:",[69,178,179,192],{},[72,180,181],{},[75,182,183,186,189],{},[78,184,185],{},"Framework",[78,187,188],{},"Place _headers in",[78,190,191],{},"Publish Directory",[88,193,194,205,215,225,235,244],{},[75,195,196,199,202],{},[93,197,198],{},"Create React App",[93,200,201],{},"public/",[93,203,204],{},"build/",[75,206,207,210,212],{},[93,208,209],{},"Next.js (static)",[93,211,201],{},[93,213,214],{},"out/",[75,216,217,220,223],{},[93,218,219],{},"Gatsby",[93,221,222],{},"static/",[93,224,201],{},[75,226,227,230,232],{},[93,228,229],{},"Vue CLI",[93,231,201],{},[93,233,234],{},"dist/",[75,236,237,240,242],{},[93,238,239],{},"Astro",[93,241,201],{},[93,243,234],{},[75,245,246,249,251],{},[93,247,248],{},"Hugo",[93,250,222],{},[93,252,201],{},[44,254,256],{"id":255},"method-2-using-netlifytoml","Method 2: Using netlify.toml",[18,258,259],{},"More verbose but offers additional features.",[130,261,262,266],{"number":132},[134,263,265],{"id":264},"create-or-update-netlifytoml-in-project-root","Create or update netlify.toml in project root",[145,267,270],{"className":268,"code":269,"language":150},[148],"# Build configuration\n[build]\n  publish = \"dist\"\n  command = \"npm run build\"\n\n# Security headers for all routes\n[[headers]]\n  for = \"/*\"\n  [headers.values]\n    X-Content-Type-Options = \"nosniff\"\n    X-Frame-Options = \"DENY\"\n    X-XSS-Protection = \"1; mode=block\"\n    Referrer-Policy = \"strict-origin-when-cross-origin\"\n    Permissions-Policy = \"camera=(), microphone=(), geolocation=()\"\n    Strict-Transport-Security = \"max-age=31536000; includeSubDomains\"\n    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';\"\n",[31,271,269],{"__ignoreMap":153},[130,273,274,277],{"number":156},[134,275,160],{"id":276},"add-route-specific-headers-1",[145,278,281],{"className":279,"code":280,"language":150},[148],"# Default security headers\n[[headers]]\n  for = \"/*\"\n  [headers.values]\n    X-Content-Type-Options = \"nosniff\"\n    X-Frame-Options = \"DENY\"\n    Referrer-Policy = \"strict-origin-when-cross-origin\"\n    Strict-Transport-Security = \"max-age=31536000; includeSubDomains\"\n\n# API routes\n[[headers]]\n  for = \"/api/*\"\n  [headers.values]\n    Cache-Control = \"no-store, max-age=0\"\n    X-Content-Type-Options = \"nosniff\"\n\n# Static assets with long cache\n[[headers]]\n  for = \"/assets/*\"\n  [headers.values]\n    Cache-Control = \"public, max-age=31536000, immutable\"\n\n# Admin with strict CSP\n[[headers]]\n  for = \"/admin/*\"\n  [headers.values]\n    Content-Security-Policy = \"default-src 'self'; script-src 'self'; style-src 'self'; frame-ancestors 'none';\"\n",[31,282,280],{"__ignoreMap":153},[130,284,285,289],{"number":169},[134,286,288],{"id":287},"use-environment-specific-headers-advanced","Use environment-specific headers (advanced)",[145,290,293],{"className":291,"code":292,"language":150},[148],"# Production headers\n[context.production]\n  [[context.production.headers]]\n    for = \"/*\"\n    [context.production.headers.values]\n      Strict-Transport-Security = \"max-age=31536000; includeSubDomains; preload\"\n      Content-Security-Policy = \"default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline';\"\n\n# Deploy preview headers (less strict for testing)\n[context.deploy-preview]\n  [[context.deploy-preview.headers]]\n    for = \"/*\"\n    [context.deploy-preview.headers.values]\n      Content-Security-Policy = \"default-src 'self' 'unsafe-inline' 'unsafe-eval'; img-src 'self' data: https:;\"\n",[31,294,292],{"__ignoreMap":153},[44,296,298],{"id":297},"method-3-using-edge-functions-dynamic-headers","Method 3: Using Edge Functions (Dynamic Headers)",[18,300,301],{},"Use Edge Functions when you need dynamic headers like CSP nonces.",[130,303,304,308,315],{"number":132},[134,305,307],{"id":306},"create-edge-function","Create Edge Function",[18,309,310,311,314],{},"Create ",[31,312,313],{},"netlify/edge-functions/security-headers.ts",":",[145,316,319],{"className":317,"code":318,"language":150},[148],"import type { Context } from \"@netlify/edge-functions\";\n\nexport default async function handler(request: Request, context: Context) {\n  // Get the response from the origin\n  const response = await context.next();\n\n  // Generate a unique nonce for CSP\n  const nonce = crypto.randomUUID().replace(/-/g, '');\n\n  // Build CSP with nonce\n  const csp = [\n    \"default-src 'self'\",\n    `script-src 'self' 'nonce-${nonce}' 'strict-dynamic'`,\n    `style-src 'self' 'nonce-${nonce}'`,\n    \"img-src 'self' blob: data: https:\",\n    \"font-src 'self'\",\n    \"object-src 'none'\",\n    \"base-uri 'self'\",\n    \"form-action 'self'\",\n    \"frame-ancestors 'none'\",\n    \"upgrade-insecure-requests\"\n  ].join('; ');\n\n  // Clone headers and add security headers\n  const headers = new Headers(response.headers);\n  headers.set('Content-Security-Policy', csp);\n  headers.set('X-Content-Type-Options', 'nosniff');\n  headers.set('X-Frame-Options', 'DENY');\n  headers.set('X-XSS-Protection', '1; mode=block');\n  headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');\n  headers.set('Permissions-Policy', 'camera=(), microphone=(), geolocation=()');\n  headers.set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');\n  headers.set('X-Nonce', nonce);\n\n  return new Response(response.body, {\n    status: response.status,\n    statusText: response.statusText,\n    headers\n  });\n}\n\nexport const config = {\n  path: \"/*\",\n  excludedPath: [\"/api/*\", \"/_next/*\", \"/assets/*\"]\n};\n",[31,320,318],{"__ignoreMap":153},[130,322,323,327],{"number":156},[134,324,326],{"id":325},"configure-edge-function-in-netlifytoml","Configure Edge Function in netlify.toml",[145,328,331],{"className":329,"code":330,"language":150},[148],"[[edge_functions]]\n  function = \"security-headers\"\n  path = \"/*\"\n",[31,332,330],{"__ignoreMap":153},[130,334,335,339,342],{"number":169},[134,336,338],{"id":337},"access-nonce-in-your-html-optional","Access nonce in your HTML (optional)",[18,340,341],{},"For server-rendered pages, you can read the nonce from the X-Nonce header:",[145,343,346],{"className":344,"code":345,"language":150},[148],"// In your server-side code or build process\nexport async function getServerSideProps({ req }) {\n  const nonce = req.headers['x-nonce'] || '';\n\n  return {\n    props: { nonce }\n  };\n}\n\n// In your component\nfunction MyPage({ nonce }) {\n  return (\n    \u003Chtml>\n      \u003Chead>\n        \u003Cscript nonce={nonce}>\n          console.log('Inline script with nonce');\n        \u003C/script>\n      \u003C/head>\n      \u003Cbody>...\u003C/body>\n    \u003C/html>\n  );\n}\n",[31,347,345],{"__ignoreMap":153},[349,350,351,354],"warning-box",{},[18,352,353],{},"Security Checklist for Netlify Deployments",[49,355,356,359,365,368,371,374],{},[52,357,358],{},"Verify _headers file is in the publish directory (not project root)",[52,360,361,362],{},"Check netlify.toml syntax with the Netlify CLI: ",[31,363,364],{},"netlify build --dry",[52,366,367],{},"Test CSP with Content-Security-Policy-Report-Only first",[52,369,370],{},"Ensure third-party scripts (analytics, chat widgets) are allowed in CSP",[52,372,373],{},"Start HSTS with short max-age (300) before increasing to 31536000",[52,375,376],{},"Test on deploy previews before production deployment",[44,378,380],{"id":379},"how-to-verify-it-worked","How to Verify It Worked",[134,382,384],{"id":383},"method-1-netlify-deploy-log","Method 1: Netlify Deploy Log",[386,387,388,391,394,397,400],"ol",{},[52,389,390],{},"Go to your site in the Netlify dashboard",[52,392,393],{},"Click on a deployment",[52,395,396],{},"Expand the deploy log",[52,398,399],{},"Look for \"Processing headers\" messages",[52,401,402],{},"Check for any warnings about _headers or netlify.toml",[134,404,406],{"id":405},"method-2-netlify-cli","Method 2: Netlify CLI",[145,408,411],{"className":409,"code":410,"language":150},[148],"# Install Netlify CLI\nnpm install -g netlify-cli\n\n# Test your configuration locally\nnetlify dev\n\n# Check what headers would be applied\nnetlify build --dry\n",[31,412,410],{"__ignoreMap":153},[134,414,416],{"id":415},"method-3-browser-devtools","Method 3: Browser DevTools",[386,418,419,422,425,428,431,434],{},[52,420,421],{},"Visit your deployed Netlify site",[52,423,424],{},"Open DevTools (F12)",[52,426,427],{},"Go to Network tab",[52,429,430],{},"Reload the page",[52,432,433],{},"Click on the document request",[52,435,436],{},"Check Response Headers section",[134,438,440],{"id":439},"method-4-command-line","Method 4: Command Line",[145,442,445],{"className":443,"code":444,"language":150},[148],"# Check headers with curl\ncurl -I https://your-site.netlify.app\n\n# Expected output:\n# HTTP/2 200\n# x-content-type-options: nosniff\n# x-frame-options: DENY\n# strict-transport-security: max-age=31536000; includeSubDomains\n# referrer-policy: strict-origin-when-cross-origin\n# content-security-policy: default-src 'self'; ...\n",[31,446,444],{"__ignoreMap":153},[44,448,450],{"id":449},"common-errors-and-troubleshooting","Common Errors and Troubleshooting",[134,452,454],{"id":453},"_headers-file-not-working","_headers file not working",[49,456,457,464,473,479],{},[52,458,459,463],{},[460,461,462],"strong",{},"Wrong location",": Must be in publish directory, not project root",[52,465,466,469,470,472],{},[460,467,468],{},"Wrong filename",": Must be exactly ",[31,471,33],{}," with no extension",[52,474,475,478],{},[460,476,477],{},"Build process issue",": File might not be copied to build output",[52,480,481,484],{},[460,482,483],{},"Syntax error",": Check for proper formatting (path on its own line, headers indented)",[134,486,488],{"id":487},"netlifytoml-headers-not-applying","netlify.toml headers not applying",[49,490,491,499,505],{},[52,492,493,496,497],{},[460,494,495],{},"TOML syntax",": Use a TOML validator or ",[31,498,364],{},[52,500,501,504],{},[460,502,503],{},"Conflicting with _headers",": Both files are merged; check for conflicts",[52,506,507,510,511,514],{},[460,508,509],{},"Wrong path pattern",": Ensure ",[31,512,513],{},"for"," matches your routes",[134,516,518],{"id":517},"csp-blocking-content","CSP blocking content",[49,520,521,527,537],{},[52,522,523,526],{},[460,524,525],{},"Check console",": Browser console shows CSP violation details",[52,528,529,532,533,536],{},[460,530,531],{},"Use report-only",": Test with ",[31,534,535],{},"Content-Security-Policy-Report-Only"," first",[52,538,539,542],{},[460,540,541],{},"Add missing sources",": Include third-party domains in appropriate directives",[134,544,546],{"id":545},"edge-function-not-running","Edge Function not running",[49,548,549,558,564],{},[52,550,551,554,555],{},[460,552,553],{},"File location",": Must be in ",[31,556,557],{},"netlify/edge-functions/",[52,559,560,563],{},[460,561,562],{},"Export config",": Ensure path configuration is exported",[52,565,566,569],{},[460,567,568],{},"Check logs",": Review Edge Function logs in Netlify dashboard",[571,572,573,576],"tip-box",{},[18,574,575],{},"Pro Tip",[18,577,578],{},"Use Netlify's deploy previews to test security headers on feature branches before merging to production. Each PR gets a unique URL where you can verify headers work correctly without affecting your live site.",[44,580,582],{"id":581},"frequently-asked-questions","Frequently Asked Questions",[584,585,586,593,599,605,611],"faq-section",{},[587,588,590],"faq-item",{"question":589},"Where should I put the _headers file on Netlify?",[18,591,592],{},"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.",[587,594,596],{"question":595},"Should I use _headers or netlify.toml for security headers?",[18,597,598],{},"_headers is simpler and easier to read. netlify.toml offers more features like environment-specific headers. Both work equally well. You can even use both - they get merged, with netlify.toml taking precedence for conflicts.",[587,600,602],{"question":601},"Does Netlify add any security headers by default?",[18,603,604],{},"Netlify adds some basic headers like X-NF-Request-ID for tracking but does not add security headers by default. You must configure all security headers yourself via _headers, netlify.toml, or Edge Functions.",[587,606,608],{"question":607},"Why aren't my _headers working on Netlify?",[18,609,610],{},"Common issues: File not in publish directory, wrong filename, syntax errors, or conflicting headers in netlify.toml. Check your deploy log for header-related warnings.",[587,612,614],{"question":613},"Can I have different headers for different routes on Netlify?",[18,615,616],{},"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/*.",[618,619,622,626],"cta-box",{"href":620,"label":621},"/","Start Free Scan",[44,623,625],{"id":624},"scan-your-netlify-site","Scan Your Netlify Site",[18,627,628],{},"Check if your Netlify-hosted site has all the security headers configured correctly.",[630,631,632,638,643,648],"related-articles",{},[633,634],"related-card",{"description":635,"href":636,"title":637},"Complete guide to all essential security headers.","/blog/how-to/add-security-headers","Security Headers Overview",[633,639],{"description":640,"href":641,"title":642},"Configure headers with vercel.json and middleware.","/blog/how-to/vercel-headers","Security Headers on Vercel",[633,644],{"description":645,"href":646,"title":647},"Deep dive into Content Security Policy setup.","/blog/how-to/csp-setup","CSP Configuration Guide",[633,649],{"description":650,"href":651,"title":652},"Complete security guide for Netlify deployments.","/blog/best-practices/netlify","Netlify Security Best Practices",{"title":153,"searchDepth":654,"depth":654,"links":655},2,[656,657,658,664,669,674,680,686,687],{"id":46,"depth":654,"text":47},{"id":66,"depth":654,"text":67},{"id":124,"depth":654,"text":125,"children":659},[660,662,663],{"id":136,"depth":661,"text":137},3,{"id":159,"depth":661,"text":160},{"id":172,"depth":661,"text":173},{"id":255,"depth":654,"text":256,"children":665},[666,667,668],{"id":264,"depth":661,"text":265},{"id":276,"depth":661,"text":160},{"id":287,"depth":661,"text":288},{"id":297,"depth":654,"text":298,"children":670},[671,672,673],{"id":306,"depth":661,"text":307},{"id":325,"depth":661,"text":326},{"id":337,"depth":661,"text":338},{"id":379,"depth":654,"text":380,"children":675},[676,677,678,679],{"id":383,"depth":661,"text":384},{"id":405,"depth":661,"text":406},{"id":415,"depth":661,"text":416},{"id":439,"depth":661,"text":440},{"id":449,"depth":654,"text":450,"children":681},[682,683,684,685],{"id":453,"depth":661,"text":454},{"id":487,"depth":661,"text":488},{"id":517,"depth":661,"text":518},{"id":545,"depth":661,"text":546},{"id":581,"depth":654,"text":582},{"id":624,"depth":654,"text":625},"how-to","2026-01-19","Step-by-step guide to adding security headers on Netlify. Configure via _headers file, netlify.toml, and Edge Functions. Includes CSP, HSTS, and all essential headers.",false,"md",[694,696,698,700,702],{"question":589,"answer":695},"The _headers file must be in your publish directory (the folder Netlify serves). For most projects, this is the 'public', 'dist', or 'build' folder. For static site generators, place it in your static assets folder so it gets copied to the build output.",{"question":595,"answer":697},"_headers is simpler and easier to read. netlify.toml offers more features like environment-specific headers and is useful if you already use it for other configuration. Both work equally well for security headers. You can even use both - they get merged.",{"question":601,"answer":699},"Netlify adds some basic headers like X-NF-Request-ID for tracking, but does not add security headers by default. You must configure all security headers yourself via _headers, netlify.toml, or Edge Functions.",{"question":607,"answer":701},"Common issues: 1) File is not in the publish directory, 2) File has wrong name (must be exactly '_headers' with no extension), 3) Syntax errors in the file, 4) Conflicting headers in netlify.toml. Check your deploy log for header-related warnings.",{"question":613,"answer":616},"yellow",null,{},true,"Complete guide to setting up security headers on Netlify with _headers file and netlify.toml.","/blog/how-to/netlify-headers","[object Object]","HowTo",{"title":5,"description":690},{"loc":708},"blog/how-to/netlify-headers",[],"summary_large_image","cqWNqYAwt22HMXXpPH5CU6BTSC6tXxvWMCoDHx1JeFA",1775843928238]