Security Headers: Complete Configuration Guide

Security headers are HTTP response headers that instruct browsers how to behave when handling your site's content. They form a critical security layer that costs nothing to implement but prevents...

Key Insights

  • Security headers are your first line of defense against XSS, clickjacking, and data injection attacks—yet most applications ship with incomplete or misconfigured headers.
  • Content-Security-Policy is the most powerful security header but also the most complex; start with report-only mode and progressively tighten restrictions.
  • Implement headers at the infrastructure level (Nginx, CDN) rather than application code whenever possible for consistent, performant protection.

Introduction to Security Headers

Security headers are HTTP response headers that instruct browsers how to behave when handling your site’s content. They form a critical security layer that costs nothing to implement but prevents entire categories of attacks.

When a browser receives your response, it checks these headers before rendering content. A properly configured Content-Security-Policy stops XSS attacks cold. X-Frame-Options prevents your site from being embedded in malicious iframes. Strict-Transport-Security ensures all communication happens over HTTPS.

The browser becomes your security enforcement mechanism. Attackers can’t bypass these protections because they’re enforced client-side before malicious code executes. This is defense in depth—even if an attacker finds an injection vulnerability, security headers can prevent exploitation.

Content-Security-Policy (CSP)

CSP is the most powerful and complex security header. It defines exactly what content sources the browser should trust, blocking everything else.

Understanding Directives

CSP uses directives to control different resource types:

  • default-src: Fallback for all resource types
  • script-src: JavaScript sources
  • style-src: CSS sources
  • img-src: Image sources
  • connect-src: XHR, WebSocket, and fetch destinations
  • frame-src: Iframe sources

Here’s a basic CSP that blocks most attacks:

Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; font-src 'self'; connect-src 'self'; frame-ancestors 'none';

This policy only allows resources from your own origin. It’s restrictive but breaks most real-world applications that use CDNs or inline scripts.

Progressive CSP Implementation

Start with report-only mode to understand what your application actually loads:

Content-Security-Policy-Report-Only: default-src 'self'; report-uri /csp-report;

Once you’ve identified legitimate sources, build a production policy:

Content-Security-Policy: 
  default-src 'self';
  script-src 'self' https://cdn.example.com 'nonce-abc123';
  style-src 'self' 'unsafe-inline';
  img-src 'self' https: data:;
  font-src 'self' https://fonts.gstatic.com;
  connect-src 'self' https://api.example.com;
  frame-ancestors 'none';
  base-uri 'self';
  form-action 'self';
  report-uri /csp-report;

Nonce-Based Inline Scripts

Avoid 'unsafe-inline' for scripts. Use nonces instead:

<script nonce="abc123def456">
  // This script executes because the nonce matches
  initializeApp();
</script>

Generate a cryptographically random nonce per request:

const crypto = require('crypto');

function generateNonce() {
  return crypto.randomBytes(16).toString('base64');
}

app.use((req, res, next) => {
  res.locals.nonce = generateNonce();
  res.setHeader(
    'Content-Security-Policy',
    `script-src 'self' 'nonce-${res.locals.nonce}'`
  );
  next();
});

Cross-Origin Headers

Cross-origin policies control how your resources interact with other origins.

CORS Configuration

CORS headers control which origins can access your API:

// Express.js CORS configuration
const corsOptions = {
  origin: ['https://app.example.com', 'https://admin.example.com'],
  methods: ['GET', 'POST', 'PUT', 'DELETE'],
  allowedHeaders: ['Content-Type', 'Authorization'],
  credentials: true,
  maxAge: 86400 // Cache preflight for 24 hours
};

app.use(cors(corsOptions));

For Nginx, configure CORS at the server level:

location /api/ {
    if ($request_method = 'OPTIONS') {
        add_header 'Access-Control-Allow-Origin' 'https://app.example.com';
        add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE';
        add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization';
        add_header 'Access-Control-Max-Age' 86400;
        return 204;
    }
    
    add_header 'Access-Control-Allow-Origin' 'https://app.example.com';
    add_header 'Access-Control-Allow-Credentials' 'true';
}

COOP and COEP

Cross-Origin-Opener-Policy (COOP) and Cross-Origin-Embedder-Policy (COEP) enable powerful features like SharedArrayBuffer:

Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp

These headers isolate your browsing context, preventing Spectre-style attacks. Required for high-resolution timers and shared memory.

Transport Security Headers

Transport headers ensure secure communication between browser and server.

HSTS Configuration

Strict-Transport-Security forces HTTPS connections:

Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
  • max-age=31536000: Remember for one year
  • includeSubDomains: Apply to all subdomains
  • preload: Submit to browser preload lists

Before enabling preload, ensure every subdomain supports HTTPS. Once preloaded, you cannot easily revert.

MIME Sniffing Prevention

Prevent browsers from guessing content types:

X-Content-Type-Options: nosniff

Always pair this with correct Content-Type headers:

app.use((req, res, next) => {
  res.setHeader('X-Content-Type-Options', 'nosniff');
  next();
});

// Serve JSON with correct type
app.get('/api/data', (req, res) => {
  res.type('application/json').json({ data: 'value' });
});

Frame and Embedding Protection

Prevent your site from being embedded in malicious contexts.

Clickjacking Prevention

Use CSP’s frame-ancestors directive (preferred) or X-Frame-Options:

Content-Security-Policy: frame-ancestors 'none';
X-Frame-Options: DENY

To allow specific embedders:

Content-Security-Policy: frame-ancestors 'self' https://trusted-partner.com;
X-Frame-Options: ALLOW-FROM https://trusted-partner.com

Note: X-Frame-Options: ALLOW-FROM has limited browser support. Prefer frame-ancestors.

Permissions-Policy

Restrict browser features your application doesn’t need:

Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=(), usb=(), magnetometer=(), gyroscope=(), accelerometer=()

This policy disables all listed features. To allow specific origins:

Permissions-Policy: geolocation=(self "https://maps.example.com"), camera=()

Implementation Across Platforms

Nginx Configuration

server {
    # Security headers
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-Frame-Options "DENY" always;
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
    add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; connect-src 'self'; frame-ancestors 'none';" always;
    add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;
}

Apache Configuration

<IfModule mod_headers.c>
    Header always set X-Content-Type-Options "nosniff"
    Header always set X-Frame-Options "DENY"
    Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
    Header always set Content-Security-Policy "default-src 'self'; script-src 'self'; frame-ancestors 'none';"
    Header always set Permissions-Policy "camera=(), microphone=(), geolocation=()"
    Header always set Referrer-Policy "strict-origin-when-cross-origin"
</IfModule>

Express.js Middleware

Use Helmet for comprehensive header management:

const helmet = require('helmet');

app.use(helmet({
  contentSecurityPolicy: {
    directives: {
      defaultSrc: ["'self'"],
      scriptSrc: ["'self'", (req, res) => `'nonce-${res.locals.nonce}'`],
      styleSrc: ["'self'", "'unsafe-inline'"],
      imgSrc: ["'self'", "data:", "https:"],
      connectSrc: ["'self'"],
      frameAncestors: ["'none'"],
    },
  },
  hsts: {
    maxAge: 31536000,
    includeSubDomains: true,
    preload: true,
  },
  frameguard: { action: 'deny' },
  referrerPolicy: { policy: 'strict-origin-when-cross-origin' },
}));

Python/Flask Middleware

from flask import Flask
from flask_talisman import Talisman

app = Flask(__name__)

csp = {
    'default-src': "'self'",
    'script-src': "'self'",
    'style-src': "'self' 'unsafe-inline'",
    'img-src': "'self' data: https:",
    'frame-ancestors': "'none'",
}

Talisman(
    app,
    content_security_policy=csp,
    strict_transport_security=True,
    strict_transport_security_max_age=31536000,
    strict_transport_security_include_subdomains=True,
)

Testing and Monitoring

Auditing Tools

Check your headers at securityheaders.com or use browser DevTools. Automate testing in CI:

// header-test.js
const https = require('https');

const requiredHeaders = {
  'strict-transport-security': /max-age=\d+/,
  'x-content-type-options': 'nosniff',
  'x-frame-options': /DENY|SAMEORIGIN/,
  'content-security-policy': /default-src/,
};

https.get('https://example.com', (res) => {
  let failures = [];
  
  for (const [header, expected] of Object.entries(requiredHeaders)) {
    const value = res.headers[header];
    if (!value) {
      failures.push(`Missing: ${header}`);
    } else if (expected instanceof RegExp && !expected.test(value)) {
      failures.push(`Invalid ${header}: ${value}`);
    } else if (typeof expected === 'string' && value !== expected) {
      failures.push(`Invalid ${header}: ${value}`);
    }
  }
  
  if (failures.length) {
    console.error('Header validation failed:', failures);
    process.exit(1);
  }
  console.log('All security headers validated');
});

CSP Violation Reporting

Implement a report handler to catch violations:

app.post('/csp-report', express.json({ type: 'application/csp-report' }), (req, res) => {
  const report = req.body['csp-report'];
  
  console.log('CSP Violation:', {
    blockedUri: report['blocked-uri'],
    violatedDirective: report['violated-directive'],
    documentUri: report['document-uri'],
    sourceFile: report['source-file'],
    lineNumber: report['line-number'],
  });
  
  // Send to monitoring service
  metrics.increment('csp.violation', {
    directive: report['violated-directive'],
  });
  
  res.status(204).end();
});

Common Misconfigurations

Avoid these mistakes:

  1. Overly permissive CSP: script-src 'unsafe-inline' 'unsafe-eval' defeats the purpose
  2. Missing always in Nginx: Headers won’t be sent on error responses
  3. HSTS without testing: Enabling preload before all subdomains support HTTPS
  4. Wildcard CORS: Access-Control-Allow-Origin: * with credentials fails silently

Security headers require ongoing maintenance. Review them quarterly, monitor CSP reports, and test after every deployment. The investment pays dividends—these headers stop attacks that would otherwise bypass your application security.

Liked this? There's more.

Every week: one practical technique, explained simply, with code you can use immediately.