CMS & Web/Explainer
Security headers explained: what to send, what to retire and how to roll out CSP
A handful of HTTP response headers tell browsers to enforce HTTPS, block framing and limit what scripts may load. Here is what each one does, which ones to drop and how to add them on Apache and Nginx.
sha256("/cms/security-headers/")Every web page arrives with a set of HTTP response headers, and some of them are instructions to the browser: only use HTTPS for this site, do not let other sites put this page in a frame, do not guess file types. These security headers cost nothing to send and protect your visitors against whole classes of attack. They do not fix a vulnerable application, but they make many flaws much harder to exploit.
What a security header actually does
A security header changes how the browser treats your page. The server sends it, the browser enforces it. That has two consequences. First, the protection only works in browsers that understand the header. Support is broad for most of the headers below, but check the browser compatibility tables on MDN for newer ones such as Permissions-Policy. Second, a wrong value can break your own site, so each header needs a short test before it goes live.
Headers are set by the web server, a reverse proxy, a CDN or the application. Set each header in one place only. If two layers both add the same header, browsers may receive two conflicting values.
Six headers worth sending
| Header | What it does | Sensible starting value |
|---|---|---|
Strict-Transport-Security | Tells the browser to use only HTTPS for this host for the given time | max-age=31536000; includeSubDomains |
Content-Security-Policy | Lists the sources from which scripts, styles, images and frames may load, and who may embed the page | Start in report-only mode, see below |
X-Content-Type-Options | Stops the browser from guessing a file type that differs from the declared one | nosniff |
Referrer-Policy | Controls how much of the current address is passed on when a visitor follows a link | strict-origin-when-cross-origin |
Permissions-Policy | Switches off browser features such as camera, microphone or location for your pages and embedded frames | camera=(), microphone=(), geolocation=() |
Cross-Origin-Opener-Policy | Separates your page from windows opened by or from other sites | same-origin |
(HTTP Strict Transport Security) only counts when sent over HTTPS; browsers ignore it on plain HTTP. With includeSubDomains it covers every subdomain, so confirm first that all of them serve HTTPS. Start with a short max-age, such as one day, and raise it to a year once nothing breaks.
Content-Security-Policy () is the most powerful header and the hardest to get right, because a strict policy blocks inline scripts that many sites rely on. Its frame-ancestors directive decides which sites may show your page in a frame, which defends against : tricking a visitor into clicking something on your page through an invisible overlay.
Cross-Origin-Opener-Policy (COOP) with same-origin can break sign-in or payment flows that open a popup and talk back to your page. If that happens, use same-origin-allow-popups instead.
The other three are simple. nosniff stops a browser from running an uploaded text file as a script just because its content looks like one. strict-origin-when-cross-origin passes the full address within your own site, only the domain to other sites and nothing on a step from HTTPS down to HTTP. Current browsers use it as their default, but setting it explicitly keeps the behaviour predictable. In Permissions-Policy, an empty list () switches a feature off for your pages and for every frame they embed.
Headers to retire: X-XSS-Protection and X-Frame-Options
X-XSS-Protection switched on a filter in some older browsers that tried to detect cross-site scripting. It is deprecated and non-standard, and the filter itself could create vulnerabilities on otherwise safe pages. The OWASP guidance is to leave the header out or send 0, and to rely on a CSP that blocks inline scripts instead.
X-Frame-Options still works, but it is superseded. It knows only DENY and SAMEORIGIN, while frame-ancestors can list exactly which sites may embed you. The CSP standard says an enforced frame-ancestors directive overrides X-Frame-Options. A report-only policy does not, so keep X-Frame-Options until frame-ancestors is enforced.
Apache: .htaccess with mod_headers
On Apache the Header directive comes from mod_headers. In .htaccess it works only if the server configuration allows it with AllowOverride FileInfo or All. Use the always condition so the headers are also sent on redirects and error pages:
# Requires mod_headers and AllowOverride FileInfo
# HSTS only on HTTPS requests
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains" "expr=%{HTTPS} == 'on'"
Header always set X-Content-Type-Options "nosniff"
Header always set Referrer-Policy "strict-origin-when-cross-origin"
Header always set Permissions-Policy "camera=(), microphone=(), geolocation=()"
Header always set Cross-Origin-Opener-Policy "same-origin"
Header always set X-Frame-Options "SAMEORIGIN"
Header always set Content-Security-Policy-Report-Only "default-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'self'"If the site answers with error 500 after the change, mod_headers is not loaded or the override is not allowed. Behind a proxy or CDN that ends TLS, %{HTTPS} may be off, so set HSTS at that layer instead. If you put the lines into the VirtualHost, run apachectl configtest before reloading; .htaccess files are only read when a request arrives.
Nginx: add_header with always
On Nginx, add_header without the always parameter only applies to a list of success and redirect codes, so error pages would go out without protection. Add the lines to the server block that listens on port 443:
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
add_header Cross-Origin-Opener-Policy "same-origin" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Content-Security-Policy-Report-Only "default-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'self'" always;Watch the inheritance rule: a location block that sets any add_header of its own drops every header from the server level. Repeat the lines there, or keep them in a snippet file that you include in each place. Since version 1.29.3, Nginx also offers add_header_inherit merge; to change this behaviour. Run nginx -t before every reload.
curl -sI https://www.example.com/ | grep -iE 'strict-transport|content-security|x-content-type|referrer|permissions|cross-origin|x-frame'Rolling out CSP without breaking the site
A CSP that blocks something your site needs breaks it silently for every visitor. That is why the rollout starts with Content-Security-Policy-Report-Only: the browser reports what it would block but blocks nothing. This header works only as a real HTTP header, not in a meta element.
- Send a strict draft policy as
Content-Security-Policy-Report-Only, as in the examples above. - Collect violations. The browser console shows them on every page you open; for real traffic, define a reporting endpoint with the
Reporting-Endpointsheader and thereport-todirective, and keep the olderreport-urias a fallback becausereport-tois not yet supported everywhere. - Fix the causes: move inline scripts into files, or allow them with a or hash, and add the third party sources you really need.
- Switch the header name to
Content-Security-Policyonce the reports are quiet for a week or two. - Keep a stricter next version running in report-only mode alongside the enforced one, and tighten step by step.
Reporting-Endpoints: csp="https://www.example.com/csp-reports"
Content-Security-Policy-Report-Only: default-src 'self'; object-src 'none'; base-uri 'self'; report-to csp; report-uri /csp-reportsSources and further reading
- MDN: Strict-Transport-Securitydeveloper.mozilla.org
- MDN: Content-Security-Policy-Report-Onlydeveloper.mozilla.org
- MDN: X-XSS-Protectiondeveloper.mozilla.org
- OWASP HTTP Headers Cheat Sheetcheatsheetseries.owasp.org
- Apache HTTP Server: mod_headershttpd.apache.org
- Nginx: ngx_http_headers_modulenginx.org
General information, checked on 31 October 2026. Test changes on one system first and keep a way back. Found something outdated? Tell the editors.