Security headers

What are HTTP security headers? A practical guide

A response header panel listing Strict-Transport-Security, Content-Security-Policy, X-Frame-Options and X-Content-Type-Options as present and Permissions-Policy as missing.

HTTP security headers are instructions your server sends with each page that tell the visitor's browser how to protect them: use HTTPS only, run only the scripts you allow, refuse to be framed by other sites, and leak less information. They cost nothing, take minutes to add, and are checked by every serious security scanner. Six of them matter for most websites.

This guide explains what each header does in plain terms, which ones to add first so nothing breaks, and how to set them on nginx, Apache and Cloudflare. The command output shown is real, taken from this website. The server configuration examples follow the nginx and Apache documentation; test them on a staging copy of your own site before going live.

How security headers work

When a browser requests a page, the server answers with a status line, a set of headers and then the HTML. Most headers describe the content (its type, length, caching). Security headers are different: they switch on protections that live inside the browser. The browser can't apply a protection your server never asked for.

Diagram: the server sends five security headers with the page, and the browser enforces each one, from HTTPS-only to not guessing file types.
Each header switches on one protection in the visitor's browser.

Because they act in the browser, headers protect your visitors more than your server. They don't fix a vulnerable plugin or a weak password. What they do is shrink the damage when something else goes wrong, for example when an attacker manages to inject a script into a page.

The headers that matter, and what each one does

HeaderWhat it doesTypical value
Strict-Transport-SecurityMakes browsers use HTTPS for your site, even if someone types or links to http://max-age=31536000
Content-Security-PolicyLists where scripts, styles, images and frames may load from. Your main defence against injected scripts (XSS)Site-specific; start with default-src 'self'
X-Frame-Options or CSP frame-ancestorsStops other sites loading your pages in a hidden frame (clickjacking)DENY or SAMEORIGIN
X-Content-Type-OptionsStops the browser guessing a file's type, so an uploaded text file can't be run as a scriptnosniff
Referrer-PolicyControls how much of the current URL is sent to the next site a visitor goes tostrict-origin-when-cross-origin
Permissions-PolicyTurns off browser features your site doesn't use, such as the camera or geolocationcamera=(), microphone=(), geolocation=()

Strict-Transport-Security (HSTS)

A redirect from http:// to https:// only protects visitors after their first, unencrypted request has already been sent. HSTS closes that gap: once a browser has seen the header over HTTPS, it converts every later http:// request to HTTPS itself, for max-age seconds. It's defined in RFC 6797. Add includeSubDomains only when every subdomain serves HTTPS, because the browser will refuse plain HTTP on all of them.

Content-Security-Policy (CSP)

CSP is the most powerful header and the easiest to get wrong. It tells the browser which sources are allowed for each kind of resource. A policy of default-src 'self' allows only files from your own domain. A policy that includes 'unsafe-inline' for scripts allows any inline script, including an injected one, which removes most of the protection. Roll it out in report-only mode first: send the policy as Content-Security-Policy-Report-Only, fix what the browser reports, and only then switch to the enforcing header.

X-Frame-Options and frame-ancestors

Clickjacking loads your page invisibly inside another site so a visitor clicks your buttons without knowing. X-Frame-Options: DENY refuses all framing; SAMEORIGIN allows only your own pages. The CSP directive frame-ancestors does the same job with more control and takes priority in browsers that support it. Setting both is harmless and covers older browsers. The old ALLOW-FROM value is obsolete; use frame-ancestors instead.

X-Content-Type-Options

Its only valid value is nosniff. It rarely breaks anything, so it's a safe first header.

Referrer-Policy

Current browsers already default to strict-origin-when-cross-origin, which sends only your domain (not the full path) to other sites. Setting it explicitly makes the behaviour the same in every browser, and lets you choose a stricter value such as no-referrer on pages with sensitive URLs.

Permissions-Policy

If your site never uses the camera, microphone or location, say so. A script injected into your page then can't ask for them. List only features you're sure you don't use.

Headers you can drop

X-XSS-Protection controlled a filter that modern browsers have removed; the OWASP Secure Headers Project recommends not using it, or setting it to 0. Public-Key-Pins (HPKP) is obsolete and dangerous to set. Version headers such as Server: Apache/2.4.58 or X-Powered-By: PHP/8.2 aren't security headers at all: removing them hides version details from casual scanning.

Which headers to add first

  1. X-Content-Type-Options: nosniff and Referrer-Policy. They almost never break anything.
  2. Clickjacking protection (X-Frame-Options: SAMEORIGIN), unless another site legitimately embeds your pages.
  3. HSTS, once every page works over HTTPS. Start with a short max-age such as 300 (five minutes), then raise it.
  4. Permissions-Policy for features you don't use.
  5. Content-Security-Policy, last, in report-only mode first.

How to add security headers

Set headers wherever your responses are produced: the web server, your hosting control panel, a CDN, or the application. A scanner sees whatever reaches the browser, wherever it was added. The examples below set the first four headers. Adjust values to your site, and test on a staging copy first.

nginx

server {
    # ...
    add_header Strict-Transport-Security "max-age=300" always;
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;
}

always adds the header to error responses too. A common trap: if a location block contains any add_header of its own, nginx drops all the add_header lines inherited from the server block for that location. Repeat them, or keep them in an included file. Run nginx -t before reloading.

Apache

<IfModule mod_headers.c>
    Header always set Strict-Transport-Security "max-age=300"
    Header always set X-Frame-Options "SAMEORIGIN"
    Header always set X-Content-Type-Options "nosniff"
    Header always set Referrer-Policy "strict-origin-when-cross-origin"
</IfModule>

This needs mod_headers enabled. It can go in the virtual host or, on shared hosting, in .htaccess. Check the syntax with apachectl configtest.

Cloudflare

HSTS has its own setting under SSL/TLS → Edge Certificates → HTTP Strict Transport Security (HSTS). For the other headers, use Rules → Transform Rules → Response Header Transform Rules (named "Modify Response Header" in some dashboards) and add a "Set static" operation per header. Headers set there are added to responses as they leave Cloudflare, so they apply even if your origin server sends none.

WordPress and other platforms

If you can't edit server configuration, your host's control panel or a CDN is usually a cleaner place than a plugin. Whichever you use, set each header in one place only: two layers sending different values make the result hard to predict.

How to test your headers

From a terminal, curl -sI shows the response headers. This is the actual output for this website on 31 October 2026:

$ curl -sI https://primedevforce.com/
strict-transport-security: max-age=31536000
content-security-policy: default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; connect-src 'self'; font-src 'self'; object-src 'none'; base-uri 'none'; form-action 'self'; frame-ancestors 'none'; upgrade-insecure-requests
x-frame-options: DENY
x-content-type-options: nosniff
referrer-policy: strict-origin-when-cross-origin
permissions-policy: camera=(), microphone=(), geolocation=(), payment=(), usb=()
cross-origin-opener-policy: same-origin

(Other headers are omitted.) Check more than the homepage: login pages, error pages and anything served by a different application often send different headers. Browser developer tools (Network tab, click the request, then Response Headers) show the same information.

For a report that explains each result, use the free security headers check. It also tests whether HTTPS is enforced, flags 'unsafe-inline' and 'unsafe-eval' in a CSP, and reports cookie flags, with the exact header value as evidence for each finding.

Common mistakes

  • Headers only on some responses. Missing always in nginx, or headers set in one application and not another.
  • A long HSTS max-age with includeSubDomains before every subdomain has HTTPS. Browsers will refuse to load the ones that don't, and you can't undo it until max-age expires.
  • A CSP that allows 'unsafe-inline' scripts. It looks protected in a scanner's "present" column but blocks very little.
  • Duplicate headers from the server and a CDN with different values.
  • Setting CSP in a <meta> tag only. It works for many directives, but not frame-ancestors or reporting, and many scanners (including ours) evaluate the header only.

What a headers scan can and can't tell you

A header check confirms which protections your server asks for, and spots values that weaken them. It can't prove a policy is right for your site, find vulnerabilities in your code, or see headers on pages it didn't request. Our check reads the homepage response; verified owners on Pro also get weekly checks of up to five key pages. Treat a clean result as "these protections are switched on", not as "this site is secure".

Questions

Do security headers affect SEO?

Not directly. Google doesn't rank pages on security headers. HTTPS is a lightweight ranking signal, and HSTS helps make sure visitors always reach the HTTPS version.

Can security headers break my site?

Some can. A Content Security Policy that misses a source blocks that script or image, and HSTS with includeSubDomains breaks any subdomain without HTTPS. Add them in the order above, with short lifetimes and report-only mode first.

Should every page send the same headers?

Mostly. The HTTPS, framing and content-type headers should be on every response. CSP and Permissions-Policy can differ where a page genuinely needs something extra, such as an embedded video.

Sources

About the author

Prime Dev Force Security team. The people who build and run Prime Dev Force Security, an external website, DNS and email security checker. Every guide cites the standards it relies on, and its examples are run against our own test domains before publishing.