Back to blog
securitynginxapachecloudflarevercel

How to Add Security Headers (Nginx, Apache, Cloudflare, Vercel)

7 min read
Share:
Server room with glowing network connections

So you've run your site through a security headers checker and the grade wasn't great. Now what? The good news is that adding security headers is mostly configuration—no code changes required. The exact steps depend on your hosting setup, so I'll cover the most common ones.

Before you start

A few things to keep in mind:

  • Test in staging first if possible. Some headers (especially CSP) can break functionality if misconfigured.
  • HSTS is sticky. Once a browser sees HSTS, it remembers for the duration of max-age. Start with a short max-age (like 300 seconds) until you're confident everything works.
  • CSP is the tricky one. If you're new to this, add all the other headers first, then tackle CSP separately.

Nginx configuration

Nginx makes it easy—just add headers in your server block. Edit your site's config file (usually in /etc/nginx/sites-available/ or /etc/nginx/conf.d/):

server {
    listen 443 ssl http2;
    server_name example.com;

    # Security headers
    add_header X-Frame-Options "DENY" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-XSS-Protection "1; mode=block" always;
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;
    add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;

    # Content Security Policy (adjust as needed)
    add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; object-src 'none'; frame-ancestors 'none';" always;

    # ... rest of your config
}

The always parameter ensures headers are sent even on error responses (4xx, 5xx).

After editing, test and reload:

sudo nginx -t
sudo systemctl reload nginx

If you're using include directives or have nested location blocks, be aware that add_header in a nested block overrides headers from parent blocks. You may need to repeat headers in each location.

Apache / .htaccess

For Apache, you can add headers in your .htaccess file (if AllowOverride permits) or directly in your VirtualHost config.

Using .htaccess

<IfModule mod_headers.c>
    Header always set X-Frame-Options "DENY"
    Header always set X-Content-Type-Options "nosniff"
    Header always set X-XSS-Protection "1; mode=block"
    Header always set Referrer-Policy "strict-origin-when-cross-origin"
    Header always set Permissions-Policy "camera=(), microphone=(), geolocation=()"
    Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"
    Header always set Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; object-src 'none'; frame-ancestors 'none';"
</IfModule>

Make sure mod_headers is enabled:

sudo a2enmod headers
sudo systemctl restart apache2

In VirtualHost config

Same headers, but in your site's config file (typically /etc/apache2/sites-available/yoursite.conf):

<VirtualHost *:443>
    ServerName example.com

    Header always set X-Frame-Options "DENY"
    Header always set X-Content-Type-Options "nosniff"
    # ... rest of headers

    # ... rest of VirtualHost config
</VirtualHost>

Cloudflare configuration

Cloudflare can add security headers for you—no server changes needed. This is useful if you can't modify your origin server or want Cloudflare to handle it.

Using Transform Rules (recommended)

  1. Go to your Cloudflare dashboard
  2. Select your domain
  3. Navigate to RulesTransform Rules
  4. Click Create rule under "Modify Response Header"
  5. Set up each header:
Header nameOperationValue
X-Frame-OptionsSet staticDENY
X-Content-Type-OptionsSet staticnosniff
Referrer-PolicySet staticstrict-origin-when-cross-origin
Permissions-PolicySet staticcamera=(), microphone=(), geolocation=()
Strict-Transport-SecuritySet staticmax-age=31536000; includeSubDomains

You can add multiple headers in a single rule. Set the filter to "All incoming requests" unless you need path-specific headers.

Cloudflare's free plan includes Transform Rules. You get 10 rules for free, which is usually enough for security headers.

Using Workers (advanced)

For more control, you can use a Cloudflare Worker:

addEventListener('fetch', event => {
  event.respondWith(handleRequest(event.request))
})

async function handleRequest(request) {
  const response = await fetch(request)
  const newResponse = new Response(response.body, response)

  newResponse.headers.set('X-Frame-Options', 'DENY')
  newResponse.headers.set('X-Content-Type-Options', 'nosniff')
  newResponse.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin')
  newResponse.headers.set('Permissions-Policy', 'camera=(), microphone=(), geolocation=()')
  newResponse.headers.set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains')

  return newResponse
}

Deploy this to your domain's route and it'll add headers to all responses.

Vercel / Next.js

For Vercel deployments, you have two options: vercel.json or Next.js middleware/config.

Using vercel.json

Create or edit vercel.json in your project root:

{
  "headers": [
    {
      "source": "/(.*)",
      "headers": [
        { "key": "X-Frame-Options", "value": "DENY" },
        { "key": "X-Content-Type-Options", "value": "nosniff" },
        { "key": "Referrer-Policy", "value": "strict-origin-when-cross-origin" },
        { "key": "Permissions-Policy", "value": "camera=(), microphone=(), geolocation=()" },
        { "key": "Strict-Transport-Security", "value": "max-age=31536000; includeSubDomains" }
      ]
    }
  ]
}

Using next.config.js (Next.js)

/** @type {import('next').NextConfig} */
const nextConfig = {
  async headers() {
    return [
      {
        source: '/(.*)',
        headers: [
          { key: 'X-Frame-Options', value: 'DENY' },
          { key: 'X-Content-Type-Options', value: 'nosniff' },
          { key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
          { key: 'Permissions-Policy', value: 'camera=(), microphone=(), geolocation=()' },
          { key: 'Strict-Transport-Security', value: 'max-age=31536000; includeSubDomains' },
          {
            key: 'Content-Security-Policy',
            value: "default-src 'self'; script-src 'self' 'unsafe-eval' 'unsafe-inline'; style-src 'self' 'unsafe-inline';"
          }
        ]
      }
    ]
  }
}

module.exports = nextConfig

Note: Next.js often requires 'unsafe-eval' and 'unsafe-inline' for CSP due to how it handles scripts and styles. You may need to adjust based on your setup.

Netlify

For Netlify, create or edit netlify.toml in your project root:

[[headers]]
  for = "/*"
  [headers.values]
    X-Frame-Options = "DENY"
    X-Content-Type-Options = "nosniff"
    Referrer-Policy = "strict-origin-when-cross-origin"
    Permissions-Policy = "camera=(), microphone=(), geolocation=()"
    Strict-Transport-Security = "max-age=31536000; includeSubDomains"

Or use a _headers file in your publish directory:

/*
  X-Frame-Options: DENY
  X-Content-Type-Options: nosniff
  Referrer-Policy: strict-origin-when-cross-origin
  Permissions-Policy: camera=(), microphone=(), geolocation=()
  Strict-Transport-Security: max-age=31536000; includeSubDomains

Testing your implementation

After deploying changes, verify the headers are actually being sent:

Browser DevTools

  1. Open DevTools (F12 or right-click → Inspect)
  2. Go to the Network tab
  3. Refresh the page
  4. Click on the main document request
  5. Look at the Response Headers section

Command line

curl -I https://yoursite.com

This shows all response headers. Look for your security headers in the output.

Online checker

Run your site through our Security Headers Checker again. You should see your grade improve.

Troubleshooting common issues

Headers not appearing

  • Cache: Clear your CDN/server cache. Old responses might still be served.
  • Config syntax: Check for typos. One wrong quote can break the whole config.
  • Module not loaded: For Apache, ensure mod_headers is enabled.
  • Wrong location: In Nginx, check if headers are being overridden by nested blocks.

Site broken after adding CSP

CSP is strict by default. If your site breaks:

  1. Check browser console for CSP violation errors
  2. They'll tell you exactly what's being blocked
  3. Add the blocked sources to your CSP (or use 'unsafe-inline' for quick fixes)
  4. Consider using Content-Security-Policy-Report-Only first to log without blocking

HSTS causing issues

If you enabled HSTS and now can't access your site:

  • Clear browser HSTS cache: In Chrome, go to chrome://net-internals/#hsts and delete your domain
  • For browsers that don't have this, clear all browsing data or use a different browser
  • This is why testing with a short max-age first is important

What about older headers?

You might see recommendations for these headers:

  • X-XSS-Protection: Deprecated but harmless. Include it for legacy browser support.
  • Expect-CT: Deprecated as of June 2021. Certificate Transparency is now enforced by default.
  • Public-Key-Pins: Removed from browsers. Don't use it.

Next steps

Once you've got the basic headers working:

  1. Tighten your CSP gradually. Start permissive, then remove allowances you don't need.
  2. Consider HSTS preload. If you're committed to HTTPS forever, submit your domain to the HSTS preload list.
  3. Monitor violations. Set up CSP reporting to catch issues before users do.

Run your site through the Security Headers Checker after each change to track your progress. Most sites can get from an F to a B in under 30 minutes with these configurations.

Found this helpful? Share it with others.

Share:

Ready to block AI crawlers?

Use our free generators to create your blocking rules in seconds.