How to Configure Security Headers on Vercel
Protect your Vercel-deployed app with essential HTTP headers
TL;DR
TL;DR (15 minutes)
Add security headers on Vercel using vercel.json in your project root. For Next.js apps, you can also use next.config.js or middleware for dynamic headers. Configure X-Content-Type-Options, X-Frame-Options, HSTS, Referrer-Policy, Permissions-Policy, and Content-Security-Policy.
Prerequisites
- A project deployed on Vercel (or ready to deploy)
- Access to your project's Git repository
- Your site served over HTTPS (automatic on Vercel)
- Basic knowledge of JSON configuration
Three Ways to Add Headers on Vercel
| Method | Best For | Dynamic Headers |
|---|---|---|
| vercel.json | All frameworks, static sites | No |
| next.config.js | Next.js projects | No |
| Middleware | Dynamic CSP, nonces | Yes |
Method 1: Using vercel.json
The simplest approach that works with any framework.
Create vercel.json in your project root
{
"headers": [
{
"source": "/(.*)",
"headers": [
{
"key": "X-Content-Type-Options",
"value": "nosniff"
},
{
"key": "X-Frame-Options",
"value": "DENY"
},
{
"key": "X-XSS-Protection",
"value": "1; mode=block"
},
{
"key": "Referrer-Policy",
"value": "strict-origin-when-cross-origin"
},
{
"key": "Permissions-Policy",
"value": "camera=(), microphone=(), geolocation=(), interest-cohort=()"
},
{
"key": "Strict-Transport-Security",
"value": "max-age=31536000; includeSubDomains"
},
{
"key": "Content-Security-Policy",
"value": "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';"
}
]
}
]
}
Add route-specific headers (optional)
Apply different headers to different routes:
{
"headers": [
{
"source": "/(.*)",
"headers": [
{ "key": "X-Content-Type-Options", "value": "nosniff" },
{ "key": "X-Frame-Options", "value": "DENY" },
{ "key": "Referrer-Policy", "value": "strict-origin-when-cross-origin" }
]
},
{
"source": "/api/(.*)",
"headers": [
{ "key": "Cache-Control", "value": "no-store, max-age=0" },
{ "key": "X-Content-Type-Options", "value": "nosniff" }
]
},
{
"source": "/embed",
"headers": [
{ "key": "X-Frame-Options", "value": "SAMEORIGIN" }
]
}
]
}
Deploy to Vercel
# Commit and push your changes
git add vercel.json
git commit -m "Add security headers"
git push
# Or deploy directly with Vercel CLI
vercel --prod
Method 2: Using next.config.js (Next.js)
For Next.js projects, configure headers directly in your Next.js config.
Update next.config.js
/** @type {import('next').NextConfig} */
const securityHeaders = [
{
key: 'X-DNS-Prefetch-Control',
value: 'on'
},
{
key: 'Strict-Transport-Security',
value: 'max-age=63072000; includeSubDomains; preload'
},
{
key: 'X-Content-Type-Options',
value: 'nosniff'
},
{
key: 'X-Frame-Options',
value: 'DENY'
},
{
key: 'X-XSS-Protection',
value: '1; mode=block'
},
{
key: 'Referrer-Policy',
value: 'strict-origin-when-cross-origin'
},
{
key: 'Permissions-Policy',
value: 'camera=(), microphone=(), geolocation=()'
},
{
key: 'Content-Security-Policy',
value: `
default-src 'self';
script-src 'self' 'unsafe-eval' 'unsafe-inline';
style-src 'self' 'unsafe-inline';
img-src 'self' blob: data: https:;
font-src 'self';
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
upgrade-insecure-requests;
`.replace(/\s{2,}/g, ' ').trim()
}
];
const nextConfig = {
async headers() {
return [
{
// Apply to all routes
source: '/:path*',
headers: securityHeaders,
},
];
},
};
module.exports = nextConfig;
Different headers for different routes
const nextConfig = {
async headers() {
return [
{
source: '/:path*',
headers: securityHeaders,
},
{
// Stricter CSP for admin routes
source: '/admin/:path*',
headers: [
...securityHeaders,
{
key: 'Content-Security-Policy',
value: "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self'; frame-ancestors 'none';"
}
],
},
{
// Allow embedding for widget route
source: '/widget',
headers: [
...securityHeaders.filter(h => h.key !== 'X-Frame-Options'),
{
key: 'Content-Security-Policy',
value: "frame-ancestors 'self' https://trusted-domain.com;"
}
],
},
];
},
};
module.exports = nextConfig;
Method 3: Using Middleware (Dynamic Headers)
Use middleware when you need dynamic headers like CSP nonces.
Create middleware.ts in your project root
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
// Generate a unique nonce for this request
const nonce = Buffer.from(crypto.randomUUID()).toString('base64');
// Build CSP with nonce
const cspHeader = `
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;
`.replace(/\s{2,}/g, ' ').trim();
// Clone the request headers
const requestHeaders = new Headers(request.headers);
requestHeaders.set('x-nonce', nonce);
// Create the response
const response = NextResponse.next({
request: {
headers: requestHeaders,
},
});
// Set security headers
response.headers.set('Content-Security-Policy', cspHeader);
response.headers.set('X-Content-Type-Options', 'nosniff');
response.headers.set('X-Frame-Options', 'DENY');
response.headers.set('X-XSS-Protection', '1; mode=block');
response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');
response.headers.set('Permissions-Policy', 'camera=(), microphone=(), geolocation=()');
response.headers.set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
return response;
}
export const config = {
matcher: [
/*
* Match all request paths except:
* - api (API routes)
* - _next/static (static files)
* - _next/image (image optimization files)
* - favicon.ico (favicon file)
*/
'/((?!api|_next/static|_next/image|favicon.ico).*)',
],
};
Use the nonce in your components
// app/layout.tsx
import { headers } from 'next/headers';
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
const nonce = headers().get('x-nonce') || '';
return (
<html lang="en">
<head>
<script
nonce={nonce}
dangerouslySetInnerHTML={{
__html: `console.log('Inline script with nonce');`,
}}
/>
</head>
<body>{children}</body>
</html>
);
}
Configure Script component with nonce (Next.js 13+)
// components/Analytics.tsx
import Script from 'next/script';
import { headers } from 'next/headers';
export function Analytics() {
const nonce = headers().get('x-nonce') || '';
return (
<Script
nonce={nonce}
strategy="afterInteractive"
dangerouslySetInnerHTML={{
__html: `
// Your analytics code here
console.log('Analytics loaded');
`,
}}
/>
);
}
Security Checklist for Vercel Deployments
- Verify vercel.json is in the project root (not in a subdirectory)
- Test CSP in report-only mode before enforcing
- Check that all third-party scripts are allowed in CSP
- Ensure HSTS max-age is appropriate (start with 300, increase to 31536000)
- Verify headers don't conflict between vercel.json and next.config.js
- Test on preview deployments before production
How to Verify It Worked
Method 1: Vercel Dashboard
- Go to your project in the Vercel dashboard
- Click on a deployment
- Go to "Functions" or "Edge Functions" tab
- Check the logs for any header-related errors
Method 2: Browser DevTools
- Visit your deployed site
- Open DevTools (F12)
- Go to Network tab
- Reload the page
- Click on the document request
- Check Response Headers section
Method 3: Command Line
# Check headers with curl
curl -I https://your-app.vercel.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'; ...
Method 4: Online Scanner
Use securityheaders.com to get a comprehensive report and grade.
Common Errors and Troubleshooting
Headers not appearing after deployment
- vercel.json location: Must be in project root, not in a subdirectory
- JSON syntax error: Validate your JSON at jsonlint.com
- Caching: Wait a few minutes or try an incognito window
- Build errors: Check Vercel dashboard for deployment errors
Conflicting headers
- vercel.json vs next.config.js: vercel.json takes precedence. Choose one method.
- Multiple source patterns: More specific patterns override general ones
CSP blocking resources
- Check console errors: Browser console shows CSP violations
- Use report-only first: Replace
Content-Security-PolicywithContent-Security-Policy-Report-Only - Missing domains: Add third-party domains to appropriate directives
Middleware not running
- File location: middleware.ts must be in project root or src/ directory
- Matcher config: Ensure your routes aren't excluded by the matcher
- Export: Must export a function named
middleware
Pro Tip
Use Vercel's preview deployments to test security header changes before merging to production. Each pull request gets a unique URL where you can verify headers work correctly.
Frequently Asked Questions
Does vercel.json work with Next.js projects?
Yes, vercel.json works alongside next.config.js. However, for Next.js projects, it's often cleaner to configure headers in next.config.js. Headers from both files are merged, with vercel.json taking precedence for conflicts.
Can I use different headers for different routes on Vercel?
Yes. In vercel.json, use the 'source' property with glob patterns. For example, '/api/' for API routes or '/admin/' for admin pages. More specific patterns take precedence.
Why aren't my Vercel headers showing up?
Common causes: vercel.json not in project root, JSON syntax errors, deployment not complete, or CDN caching. Try a fresh deployment and check the Vercel dashboard for build errors.
Does Vercel add any security headers by default?
Vercel adds X-Vercel-Id for request tracking but does not add security headers by default. You must configure all security headers yourself via vercel.json, next.config.js, or middleware.
Should I use vercel.json or middleware for security headers?
Use vercel.json for static headers that don't change. Use middleware for dynamic headers like CSP nonces or request-specific logic. You can combine both approaches.
Scan Your Vercel Deployment
Check if your Vercel-hosted site has all the security headers configured correctly.
Start Free Scan