medium9 min readLast updated May 27, 2026

HTTP Security Headers: What They Are and How to Add Them

HTTP security headers like HSTS, CSP, and X-Frame-Options protect your site from clickjacking, XSS, and MIME sniffing. Learn how to add them on Nginx and Apache.

What are HTTP security headers?

HTTP security headers are directives sent by your web server in the HTTP response that instruct the browser to enable (or disable) certain security features. They are one of the easiest and most effective ways to protect your website and its users from common attacks like cross-site scripting (XSS), clickjacking, and data injection.

The good news: adding them is usually a few lines of server configuration. The bad news: most websites are missing at least some of them.

Essential security headers

Strict-Transport-Security (HSTS)

HSTS tells the browser to always use HTTPS when connecting to your site, even if the user types http://. This prevents protocol downgrade attacks and cookie hijacking over insecure connections.

Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
  • max-age=63072000 -- remember this rule for 2 years (in seconds)
  • includeSubDomains -- apply to all subdomains
  • preload -- eligible for browser HSTS preload lists (see hstspreload.org)

Important: Only enable HSTS after you are certain that HTTPS works correctly on your site and all subdomains. Once a browser receives this header, it will refuse to connect over HTTP for the specified duration. See our guide on TLS certificates to make sure your certificates are healthy first.

Content-Security-Policy (CSP)

CSP controls which resources (scripts, styles, images, fonts, frames) the browser is allowed to load. It is the most powerful defence against XSS attacks.

A strict CSP:

Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'

A more permissive CSP (for sites using third-party scripts):

Content-Security-Policy: default-src 'self'; script-src 'self' https://cdn.jsdelivr.net https://www.google-analytics.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' https://fonts.gstatic.com; frame-ancestors 'none'

Tip: Start with Content-Security-Policy-Report-Only to test your policy without breaking the site:

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

This logs violations without blocking anything, so you can fine-tune the policy.

X-Frame-Options

Prevents your site from being embedded in an iframe on another domain. This protects against clickjacking attacks.

X-Frame-Options: DENY

Options:

  • DENY -- never allow framing
  • SAMEORIGIN -- allow framing only from your own domain

Note: frame-ancestors in CSP is the modern replacement, but X-Frame-Options should still be set for older browser compatibility.

X-Content-Type-Options

Prevents browsers from MIME-sniffing a response away from the declared Content-Type. This stops attacks where a browser interprets a file differently than intended (e.g., treating a text file as executable JavaScript).

X-Content-Type-Options: nosniff

There is only one valid value. Always set it.

Referrer-Policy

Controls how much referrer information is included when navigating away from your site. This prevents leaking sensitive URLs (with tokens, session IDs, or internal paths) to third-party sites.

Referrer-Policy: strict-origin-when-cross-origin

Common values:

  • no-referrer -- never send referrer information
  • strict-origin-when-cross-origin -- send full URL for same-origin requests, only the origin for cross-origin requests, and nothing for downgrades (HTTPS to HTTP)
  • same-origin -- only send referrer for same-origin requests

Permissions-Policy

Controls which browser features (camera, microphone, geolocation, payment, etc.) your site is allowed to use. This limits the damage if your site is compromised -- an attacker cannot enable the webcam, for example.

Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=()

The () means "disabled for all origins." Specify domains if needed:

Permissions-Policy: camera=(self "https://meet.yourcompany.com"), microphone=(self)

How to check your current headers

Using securityheaders.com

Visit Security Headers and enter your URL. It grades your site A+ to F and lists missing headers with explanations.

Using curl

curl -sI https://yourcompany.com | grep -iE "strict-transport|content-security|x-frame|x-content-type|referrer-policy|permissions-policy"

Using browser DevTools

Open DevTools (F12) > Network tab > click any request > Headers tab. Check the response headers for security headers.

How to add headers on Nginx

Add these directives to your server block (usually in /etc/nginx/sites-enabled/ or /etc/nginx/conf.d/):

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

    # HSTS (2 years)
    add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;

    # CSP (adjust to your needs)
    add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'" always;

    # Clickjacking protection
    add_header X-Frame-Options "DENY" always;

    # MIME sniffing protection
    add_header X-Content-Type-Options "nosniff" always;

    # Referrer policy
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;

    # Permissions policy
    add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=()" always;

    # ... rest of your config
}

Test and reload:

sudo nginx -t
sudo systemctl reload nginx

Important: The always parameter ensures headers are sent even on error responses (404, 500, etc.). Without it, Nginx only adds headers on successful responses.

How to add headers on Apache

Add these to your vhost configuration or .htaccess:

<IfModule mod_headers.c>
    # HSTS (2 years)
    Header always set Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"

    # CSP (adjust to your needs)
    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'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'"

    # Clickjacking protection
    Header always set X-Frame-Options "DENY"

    # MIME sniffing protection
    Header always set X-Content-Type-Options "nosniff"

    # Referrer policy
    Header always set Referrer-Policy "strict-origin-when-cross-origin"

    # Permissions policy
    Header always set Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=()"
</IfModule>

Make sure mod_headers is enabled:

sudo a2enmod headers
sudo apachectl configtest
sudo systemctl reload apache2

Common mistakes

CSP too strict breaks the site

A CSP that blocks inline scripts ('unsafe-inline' not included in script-src) will break most websites that use inline JavaScript. Start with Content-Security-Policy-Report-Only, monitor violations, then tighten gradually.

HSTS before HTTPS is fully working

If you enable HSTS but your HTTPS is misconfigured (expired cert, mixed content), users will be locked out of your site for the duration of max-age. Always verify HTTPS is working on all pages and subdomains before enabling HSTS.

Missing "always" in Nginx

Without always, Nginx does not add headers to error responses. An attacker-triggered 404 page without security headers can still be exploited.

Duplicate headers

If headers are set in multiple places (main config, vhost, .htaccess, application code), they can conflict or duplicate. Check with curl -sI to verify the final output.

Forgetting subdomains

Your main domain might have perfect headers, but api.yourcompany.com or staging.yourcompany.com might have none. Check every subdomain.

Also ensure your TLS cipher suites are properly configured -- security headers and strong TLS work together to protect your users.

How SurfaceScan helps

SurfaceScan checks HTTP security headers on every web-facing host in your attack surface. It flags missing headers, weak configurations (like CSP with unsafe-eval), and HSTS that is set with a too-short max-age. Findings appear in the Vulnerabilities section with the specific header that is missing or misconfigured, the affected URL, and recommended values. Because SurfaceScan checks all your subdomains automatically, you catch gaps on staging or API servers that manual checks might miss.

Related articles