TL;DR
The single highest-value thing you can do on Netlify is set security headers, via a _headers file or netlify.toml. Netlify ships none of them for you. The 7 practices below take about 40 minutes end to end. If you only get through two, make them the headers and Function authentication: those are what an attacker reaches from outside.
"Netlify makes deployment easy, but security is still your responsibility. Configure headers, protect your Functions, and never trust the client."
Best Practice 1: Configure Security Headers 5 min
You can do this in a _headers file or in netlify.toml. Either works:
/*
X-Frame-Options: DENY
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: camera=(), microphone=(), geolocation=()
# Stricter CSP for HTML pages
/*.html
Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' https://fonts.gstatic.com
# Cache static assets
/assets/*
Cache-Control: public, max-age=31536000, immutable
Alternative: netlify.toml Configuration
[[headers]]
for = "/*"
[headers.values]
X-Frame-Options = "DENY"
X-Content-Type-Options = "nosniff"
X-XSS-Protection = "1; mode=block"
Referrer-Policy = "strict-origin-when-cross-origin"
[[headers]]
for = "/*.html"
[headers.values]
Content-Security-Policy = "default-src 'self'; script-src 'self' 'unsafe-inline'"
Best Practice 2: Secure Environment Variables 5 min
Netlify keeps environment variables to two places: the build, and your Functions. Nothing reaches the browser unless you put it there.
| Context | Access to Env Vars | Security Implication |
|---|---|---|
| Build time | Yes | Variables can be baked into static files |
| Netlify Functions | Yes | Secure, server-side only |
| Browser (static) | No (unless baked in) | Client-side code cannot read env vars |
Important: Environment variables accessed during build can be included in your static bundle. Use Netlify Functions for operations requiring secrets instead of baking them into your frontend.
// netlify/functions/send-email.js
exports.handler = async (event) => {
// Secret only available server-side
const apiKey = process.env.SENDGRID_API_KEY;
if (event.httpMethod !== 'POST') {
return { statusCode: 405, body: 'Method not allowed' };
}
const { to, subject, body } = JSON.parse(event.body);
// Send email using server-side secret
// ...
return { statusCode: 200, body: JSON.stringify({ success: true }) };
};
Best Practice 3: Secure Netlify Functions 10 min
A Netlify Function is a public HTTP endpoint, reachable by anyone who finds the URL whatever the UI in front of it does:
// netlify/functions/protected-action.js
const jwt = require('jsonwebtoken');
exports.handler = async (event, context) => {
// Only allow POST requests
if (event.httpMethod !== 'POST') {
return {
statusCode: 405,
body: JSON.stringify({ error: 'Method not allowed' })
};
}
// Verify authentication
const authHeader = event.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return {
statusCode: 401,
body: JSON.stringify({ error: 'Unauthorized' })
};
}
try {
const token = authHeader.substring(7);
const decoded = jwt.verify(token, process.env.JWT_SECRET);
// Validate input
const body = JSON.parse(event.body);
if (!body.action || typeof body.action !== 'string') {
return {
statusCode: 400,
body: JSON.stringify({ error: 'Invalid input' })
};
}
// Process authenticated request
const result = await processAction(decoded.userId, body.action);
return {
statusCode: 200,
body: JSON.stringify(result)
};
} catch (error) {
console.error('Function error:', error.message);
return {
statusCode: 401,
body: JSON.stringify({ error: 'Invalid token' })
};
}
};
Best Practice 4: Use Deploy Contexts 5 min
Configure different settings for production, staging, and branch deploys:
# Production settings
[context.production]
environment = { NODE_ENV = "production" }
[context.production.environment]
API_URL = "https://api.yourdomain.com"
# Branch deploy settings (staging)
[context.branch-deploy]
environment = { NODE_ENV = "staging" }
[context.branch-deploy.environment]
API_URL = "https://staging-api.yourdomain.com"
# Deploy preview settings
[context.deploy-preview]
environment = { NODE_ENV = "preview" }
# Set environment variables per context in Netlify Dashboard
# for sensitive values like API keys
Best Practice 5: Protect Deploy Previews 5 min
Deploy previews are how half-finished work ends up on a URL you forgot about. Two settings fix that:
Password Protection
- Enable Site Protection in Site Settings > Access control
- Set a password for branch deploys and deploy previews
- Share password only with team members
Identity-Based Access
# Protect staging site with Netlify Identity
/* 200! Role=admin,editor
Best Practice 6: Configure Redirects Securely 5 min
Use _redirects or netlify.toml for secure routing:
# Force HTTPS
http://yourdomain.com/* https://yourdomain.com/:splat 301!
http://www.yourdomain.com/* https://yourdomain.com/:splat 301!
https://www.yourdomain.com/* https://yourdomain.com/:splat 301!
# Proxy API requests (hides backend URL)
/api/* https://your-backend.com/api/:splat 200
# SPA fallback (but not for API routes)
/* /index.html 200
Best Practice 7: Enable Security Features 5 min
Some of this is already built in. It just isn't switched on:
Netlify Security Features Checklist:
- HTTPS enabled (automatic, but verify)
- Asset optimization enabled (minification)
- Deploy notifications configured
- Build hooks secured (regenerate if exposed)
- Forms spam protection enabled if using Netlify Forms
- Audit log enabled (Team/Enterprise)
Common Netlify Security Mistakes
| Mistake | Risk | Prevention |
|---|---|---|
| No security headers | XSS, clickjacking | Add _headers file with security headers |
| Secrets in build output | Credential exposure | Use Functions for secret operations |
| Unprotected Functions | Unauthorized access | Add authentication to all Functions |
| Exposed build hooks | Unauthorized deploys | Keep hooks secret, regenerate if exposed |
| Open deploy previews | Information disclosure | Password protect previews |
Official Resources: For the latest information, see Netlify Configuration Docs, Netlify Headers Documentation, and Netlify Functions Overview.
How do I add security headers on Netlify?
Create a _headers file in your publish directory, or add [[headers]] sections to netlify.toml. Either way you write rules against path patterns, and Netlify's CDN serves the headers on matched requests. There is nothing to deploy separately; it ships with your site.
Are Netlify environment variables secure?
They're encrypted at rest and only exposed to the build and to your Functions, so the storage side is fine. The leak almost always comes from your own code: a console.log in a Function, or a value pulled into the client bundle at build time because it carried a public prefix.
Should I use _headers or netlify.toml?
Both work. If headers are all you're configuring, _headers is less to read. If you're already using netlify.toml for redirects or build settings, put them there and keep the configuration in one file rather than two.
How do I protect Netlify Functions?
Check who is calling before you do anything else, with a JWT or an API key. Then validate the input, because a Function that trusts its request body is a Function anyone can drive. And strip detail from your error responses: a stack trace tells an attacker what to try next.
Further Reading
Put these practices into action with our step-by-step guides.
Verify Your Netlify Security
We'll check your live site for the headers this guide adds, and for the config that tends to slip through anyway.