Glowing white circuit schematic etched on a dark glass panel, lit from the edge.
9 min read

5 Essential Security Headers Every Website Needs in 2026

Security headers are a few lines of server configuration that tell the browser how to treat your pages: only over HTTPS, never inside someone else's frame, never with a guessed content type. They cost nothing to send, yet many sites ship without them or with copy-pasted values that quietly break things. This guide covers the five headers worth setting on every site, a safe value to start with for each, ready-to-paste nginx and Apache snippets, and how to verify the result.

How to add headers without surprises

Two server details cause most "I added the header but it isn't there" problems.

  • nginx needs always. Without it, add_header only applies to successful and redirect responses (200, 201, 204, 206, 301, 302, 303, 304, 307, 308). Your 404 and 500 pages go out unprotected. Add always to every security header.
  • nginx inheritance is all-or-nothing. If a location block contains even one add_header, it inherits none of the add_header lines from the enclosing server block. A common pattern is to put the security headers in a small include file and include it in every block that sets its own headers.
  • Apache needs mod_headers (a2enmod headers on Debian/Ubuntu). Use Header always set so the header also lands on error responses and is not duplicated if the application already sends it.
  • Don't set the same header twice. If your framework (Laravel middleware, Django, Helmet for Express) already sends a header and the web server adds it again, browsers may see two conflicting values. Decide on one layer.

1. Strict-Transport-Security (HSTS)

What it prevents: protocol downgrade and SSL-stripping attacks. Even if you redirect HTTP to HTTPS, the very first request a user types (example.com in the address bar) goes out over plain HTTP, and an attacker on the same network can intercept it. Once a browser has seen HSTS, it rewrites every future request to HTTPS internally, before anything hits the network, and it refuses to let the user click through certificate errors.

Safe starting value: start short, then increase once you are confident every subdomain serves valid HTTPS.

# week 1: five minutes, to catch mistakes cheaply
Strict-Transport-Security: max-age=300

# after testing: one year, covering subdomains
Strict-Transport-Security: max-age=31536000; includeSubDomains
# nginx (inside the HTTPS server block)
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;

# Apache (inside the :443 VirtualHost)
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"

Pitfalls:

  • Browsers ignore HSTS received over plain HTTP, so send it from the HTTPS virtual host. The HTTP host should just redirect.
  • includeSubDomains covers every subdomain, including the forgotten intranet.example.com or an old staging box with no certificate. Inventory them first.
  • Preload is a commitment. Adding preload and submitting the domain at hstspreload.org gets you baked into browser source code, so even first visits are protected. The list requires a max-age of at least one year plus includeSubDomains. Removal is possible but takes a browser release cycle or longer to reach users, and during that time any subdomain without HTTPS is unreachable. Only preload when HTTPS everywhere is permanent.
  • HSTS makes a certificate problem fatal: users cannot bypass the warning. That is the point, but it raises the stakes on monitoring your SSL certificate so it never expires unnoticed.

2. Content-Security-Policy (CSP)

What it prevents: cross-site scripting (XSS) and injected content. CSP is an allowlist of where scripts, styles, images, frames and connections may come from. If an attacker manages to inject a <script> tag, a strict policy stops the browser from running it.

CSP is the most powerful header on this list and the easiest to get wrong, because a real site loads analytics, fonts, payment widgets and inline scripts from many places. A policy that is too strict breaks the site; one with 'unsafe-inline' and wildcards everywhere protects very little.

Safe starting value: roll out in report-only mode first. The browser evaluates the policy and logs violations to the console (and to a reporting endpoint if you configure one) but blocks nothing.

# nginx
add_header Content-Security-Policy-Report-Only "default-src 'self'; img-src 'self' data: https:; object-src 'none'; base-uri 'self'; frame-ancestors 'self'" always;

# Apache
Header always set Content-Security-Policy-Report-Only "default-src 'self'; img-src 'self' data: https:; object-src 'none'; base-uri 'self'; frame-ancestors 'self'"

Rollout plan:

  1. Deploy the report-only policy. Browse every page type (home, checkout, login, admin) with DevTools open and note each violation.
  2. Add the legitimate sources you find (for example script-src 'self' https://www.googletagmanager.com). For inline scripts, prefer nonces or hashes over 'unsafe-inline'.
  3. Run report-only for a while on real traffic. Reports from browser extensions are noise; reports from your own pages are signal.
  4. Switch the header name to Content-Security-Policy. You can keep a stricter report-only policy alongside it to test the next tightening step.

Even if a full script policy is out of reach for now, object-src 'none'; base-uri 'self'; frame-ancestors 'self' is cheap, rarely breaks anything, and closes several attack paths.

3. X-Content-Type-Options

What it prevents: MIME sniffing. Without it, a browser may decide that a file served as text/plain or image/png "looks like" HTML or JavaScript and execute it. That matters wherever users can upload files that you later serve back.

Value: there is only one, and it is safe to deploy immediately.

# nginx
add_header X-Content-Type-Options "nosniff" always;

# Apache
Header always set X-Content-Type-Options "nosniff"

Pitfall: with nosniff, browsers refuse scripts and stylesheets served with the wrong Content-Type. If a misconfigured server sends your CSS as text/plain, the page will lose its styling. Fix the MIME type (nginx's mime.types, Apache's AddType) rather than dropping the header.

4. X-Frame-Options and CSP frame-ancestors

What it prevents: clickjacking. An attacker loads your page in an invisible frame on their site and tricks the user into clicking buttons they cannot see, such as "delete account" or "confirm transfer".

The modern control is the CSP directive frame-ancestors, which lets you list exactly which origins may embed you. The older X-Frame-Options header supports only DENY and SAMEORIGIN (its ALLOW-FROM option is obsolete and ignored by current browsers). When both are present, browsers that understand frame-ancestors use it. Sending both is harmless and covers old clients.

# nginx
add_header X-Frame-Options "SAMEORIGIN" always;
# plus, in your CSP: frame-ancestors 'self';

# Apache
Header always set X-Frame-Options "SAMEORIGIN"

Pitfall: if partners legitimately embed your widget or your status page, DENY breaks them. Use frame-ancestors 'self' https://partner.example.net for those paths and keep DENY or SAMEORIGIN elsewhere.

5. Referrer-Policy

What it prevents: leaking full URLs to third parties. When a user follows a link or your page loads an external resource, the browser sends a Referer header. If your URLs contain password-reset tokens, search terms, order IDs or internal paths, the full address can end up in someone else's logs.

Safe starting value: strict-origin-when-cross-origin. Same-site requests keep the full URL (your own analytics still work), cross-site requests get only the origin (https://example.com/), and nothing is sent when going from HTTPS to HTTP. Modern browsers already default to this, but setting it explicitly protects users on older browsers and documents your intent.

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

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

Use no-referrer on especially sensitive sections such as account settings or admin panels.

Bonus: Permissions-Policy

Permissions-Policy (formerly Feature-Policy) switches off browser features your site never uses, so injected or third-party code cannot request them either. A minimal, safe value for a typical content or shop site:

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

An empty list () disables the feature for your page and every frame inside it. Leave out any feature you actually use.

A note on X-XSS-Protection: it controlled a filter that modern browsers have removed, and the filter itself could be abused. Omit the header or set it to 0; rely on CSP instead.

Putting it together

A complete nginx include, saved for example as /etc/nginx/snippets/security-headers.conf:

add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header Content-Security-Policy-Report-Only "default-src 'self'; img-src 'self' data: https:; object-src 'none'; base-uri 'self'; frame-ancestors 'self'" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;

Then include snippets/security-headers.conf; in the HTTPS server block and in every location that has its own add_header. Test and reload with sudo nginx -t && sudo systemctl reload nginx.

The Apache equivalent goes in the :443 VirtualHost or a conf file enabled with a2enconf:

<IfModule mod_headers.c>
    Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"
    Header always set Content-Security-Policy-Report-Only "default-src 'self'; img-src 'self' data: https:; object-src 'none'; base-uri 'self'; frame-ancestors 'self'"
    Header always set X-Content-Type-Options "nosniff"
    Header always set X-Frame-Options "SAMEORIGIN"
    Header always set Referrer-Policy "strict-origin-when-cross-origin"
    Header always set Permissions-Policy "camera=(), microphone=(), geolocation=()"
</IfModule>

Check with sudo apachectl configtest and reload.

How to verify with curl

Fetch only the response headers and filter for the ones you care about:

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

Then repeat for an error page, which is where a missing always shows up:

curl -sI https://example.com/this-page-does-not-exist | grep -iE 'strict-transport|x-content-type'

Two caveats. curl -I sends a HEAD request, and a few applications answer HEAD differently from GET; if results look odd, use curl -s -D - -o /dev/null https://example.com/ to see the headers of a real GET. And if you sit behind a CDN, the CDN may add, strip or override headers, so test the public hostname, not just the origin. Also check a static asset and an error status response, not only the home page.

Checklist

  • HSTS sent over HTTPS only; start with a short max-age, raise to one year, preload only when HTTPS is permanent for every subdomain.
  • CSP deployed as Content-Security-Policy-Report-Only first; enforced after violations are cleaned up.
  • X-Content-Type-Options: nosniff everywhere, with correct MIME types.
  • frame-ancestors in CSP plus X-Frame-Options: SAMEORIGIN for older browsers.
  • Referrer-Policy: strict-origin-when-cross-origin, stricter on sensitive sections.
  • Permissions-Policy disabling features you never use.
  • nginx: always on every header, and the include repeated in any location with its own add_header.
  • Verified with curl on a normal page, an error page and a static file, through the CDN.

Headers drift: a new nginx location, a CDN rule or a framework upgrade can silently drop one. Uptime Monitor records the security headers (HSTS, CSP, X-Frame-Options, X-Content-Type-Options, Referrer-Policy and more) on every check, and the PDF reports on Plus and Professional show how consistently HSTS, CSP and X-Frame-Options were present over the period, next to uptime, so a regression shows up in your regular uptime report instead of in a security audit. You can start with the free plan and add your site in a couple of minutes.

Keep reading