How to Configure Security Headers on Vercel

Share
How-To Guide

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

MethodBest ForDynamic Headers
vercel.jsonAll frameworks, static sitesNo
next.config.jsNext.js projectsNo
MiddlewareDynamic CSP, noncesYes

Method 1: Using vercel.json

The simplest approach that works with any framework.

1

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';"
        }
      ]
    }
  ]
}
2

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" }
      ]
    }
  ]
}
3

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.

1

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;
2

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.

1

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).*)',
  ],
};
2

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>
  );
}
3

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

  1. Go to your project in the Vercel dashboard
  2. Click on a deployment
  3. Go to "Functions" or "Edge Functions" tab
  4. Check the logs for any header-related errors

Method 2: Browser DevTools

  1. Visit your deployed site
  2. Open DevTools (F12)
  3. Go to Network tab
  4. Reload the page
  5. Click on the document request
  6. 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-Policy with Content-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
How-To Guides

How to Configure Security Headers on Vercel