Most websites are one HTTP response away from being much harder to attack. The security headers your web server sends with every page tell the browser what it is allowed to load, whether the site may be framed, how much referrer data to leak and whether HTTPS is mandatory. Configure them properly and you neutralise entire classes of attacks: cross-site scripting, clickjacking, MIME sniffing, protocol downgrade and data leakage through referrers.
This guide is deliberately practical. You get the seven headers that actually move the needle, the exact values we deploy on production sites, copy-ready configuration for both Apache and Nginx, the misconfigurations we see most often on real client servers, and how to verify the result in browser dev tools and with free online scanners.
What are security headers on a website?
Security headers are HTTP response headers returned by your web server alongside the HTML, CSS, images and JSON it serves. They are instructions for the browser, not for your code. A browser that receives X-Frame-Options: SAMEORIGIN will refuse to render your page inside an iframe on another domain, even if your application has no protection of its own.
Two things follow from that:
- Security headers are a defence-in-depth layer. They reduce the impact of a bug, they do not fix the bug.
- They are enforced by the client, so they only protect users on browsers that support them. In practice, every modern browser does.

The 7 security headers that matter most
| Header | Protects against | Recommended baseline value | Risk of breaking the site |
|---|---|---|---|
| Content-Security-Policy | XSS, code injection, unwanted third-party resources | Site specific, start in report-only mode | High |
| Strict-Transport-Security | HTTPS downgrade, SSL stripping, cookie hijacking | max-age=31536000; includeSubDomains | Medium |
| X-Content-Type-Options | MIME sniffing, drive-by script execution | nosniff | Very low |
| X-Frame-Options | Clickjacking, UI redressing | SAMEORIGIN | Low |
| Referrer-Policy | URL and token leakage to third parties | strict-origin-when-cross-origin | Low |
| Permissions-Policy | Abuse of camera, mic, geolocation, payment APIs | Deny everything you do not use | Low |
| Cross-Origin-* (COOP / CORP) | Cross-window attacks, resource theft, side channels | same-origin | Medium |
1. Content-Security-Policy (CSP)
CSP is the most powerful and the most misconfigured of all security headers. It defines an allow-list of sources for scripts, styles, images, fonts, frames and connections. If an attacker manages to inject a script tag, a strict CSP stops it from executing.
A realistic starting policy for a marketing site or a WordPress install:
Content-Security-Policy: default-src 'self'; script-src 'self' https://www.googletagmanager.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self' https://www.google-analytics.com; frame-ancestors 'self'; base-uri 'self'; form-action 'self'; object-src 'none'; upgrade-insecure-requests
Key points we insist on with clients:
- object-src ‘none’ and base-uri ‘self’ cost nothing and close two classic bypasses.
- frame-ancestors is the modern replacement for X-Frame-Options. Keep both for older browsers.
- Avoid
'unsafe-inline'onscript-src. If your CMS or theme requires inline scripts, move to a nonce or hash based policy. - Never ship a wildcard
script-src *. That is a header that scores points on a scanner and protects nothing.
Roll out CSP without breaking production
- Deploy
Content-Security-Policy-Report-Onlywith your candidate policy. - Collect violations with
report-toor a free reporting endpoint for one to two weeks of real traffic. - Add the legitimate sources you discovered, remove the rest.
- Switch the header name to
Content-Security-Policyto start enforcing.
A nonce-based policy, which is what you should aim for long term:
Content-Security-Policy: script-src 'nonce-r4Nd0mV4lu3' 'strict-dynamic' https: 'unsafe-inline'; object-src 'none'; base-uri 'self'
The nonce must be regenerated on every single response and injected into your script tags. That means it has to come from your application layer, not from a static server config.
2. Strict-Transport-Security (HSTS)
HSTS tells the browser to only ever contact your domain over HTTPS, for a given duration. It removes the vulnerable first plaintext request that redirects are exposed to.
Strict-Transport-Security: max-age=31536000; includeSubDomains
Rules to respect:
- Only send HSTS over HTTPS. Sending it over plain HTTP is ignored and pointless.
- includeSubDomains covers every subdomain. Make sure staging, mail interfaces and legacy tools on subdomains all have valid certificates before you enable it.
- Add the
preloaddirective and submit to the preload list only when you are certain. Removal takes months. Preload requiresmax-ageof at least 31536000 andincludeSubDomains. - Test with a short
max-age=300first, then raise it.
3. X-Content-Type-Options
One value, no downside, always set it:
X-Content-Type-Options: nosniff
It stops the browser from guessing a content type that differs from the declared Content-Type. Without it, a user-uploaded file served as text/plain can end up being interpreted as JavaScript.
4. X-Frame-Options
X-Frame-Options: SAMEORIGIN
This blocks clickjacking by preventing other origins from framing your pages. If a partner legitimately needs to embed your content, do not use ALLOW-FROM (it is not supported in modern browsers). Use CSP instead:
Content-Security-Policy: frame-ancestors 'self' https://partner.example.com;
5. Referrer-Policy
Referrer-Policy: strict-origin-when-cross-origin
This sends the full URL for same-origin navigation, only the origin for cross-origin HTTPS requests, and nothing at all when downgrading to HTTP. It is the sane default: you keep useful analytics attribution while stopping password reset tokens and internal search queries from leaking in the Referer header.
Use no-referrer only on highly sensitive areas such as admin panels or account pages, because it will destroy referral data in your analytics.
6. Permissions-Policy
The successor to Feature-Policy. It disables browser APIs your site does not need, including for embedded iframes. This article covers the same ground in more depth.
Permissions-Policy: accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()
An empty allow-list () means the feature is blocked everywhere. If you do need geolocation on your own pages, write geolocation=(self).
7. Cross-Origin isolation headers (COOP, CORP, COEP)
These three are the ones most audits flag as missing because almost nobody sets them.
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Resource-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
- COOP: same-origin severs the window reference between your page and cross-origin openers, killing tabnabbing and cross-window scripting.
- CORP: same-origin prevents other sites from embedding your resources. Use
same-siteif you serve assets across your own subdomains, orcross-originon a public CDN path. - COEP: require-corp is the strictest and breaks any third-party embed (YouTube, Maps, ad tags) that does not send CORP or proper CORS. Only enable it if you genuinely need cross-origin isolation, for example for
SharedArrayBuffer.

Copy-ready Apache configuration
Requires mod_headers. Enable it with a2enmod headers on Debian or Ubuntu, then restart Apache. Place this inside your HTTPS <VirtualHost *:443> block, or in the main config if it applies to all sites.
<IfModule mod_headers.c>
# HSTS: HTTPS vhost only
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"
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 "accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()"
Header always set Cross-Origin-Opener-Policy "same-origin"
Header always set Cross-Origin-Resource-Policy "same-origin"
# Start with report-only, then rename to Content-Security-Policy
Header always set Content-Security-Policy-Report-Only "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self'; frame-ancestors 'self'; base-uri 'self'; form-action 'self'; object-src 'none'; upgrade-insecure-requests"
# Reduce information disclosure
Header always unset X-Powered-By
Header always unset X-AspNet-Version
</IfModule>
# Outside the IfModule block, in the main server config
ServerTokens Prod
ServerSignature Off
Why always matters: without it, Apache only applies the header to successful responses. Error pages such as 404 and 500 would ship without protection.
If you are on shared hosting with no access to the vhost, the same directives work in .htaccess, provided AllowOverride permits FileInfo.

Copy-ready Nginx configuration
Save this as /etc/nginx/snippets/security-headers.conf and include it in each server block.
# HSTS: only inside the listen 443 ssl server block
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" 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 "accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()" always;
add_header Cross-Origin-Opener-Policy "same-origin" always;
add_header Cross-Origin-Resource-Policy "same-origin" always;
add_header Content-Security-Policy-Report-Only "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self'; frame-ancestors 'self'; base-uri 'self'; form-action 'self'; object-src 'none'; upgrade-insecure-requests" always;
And in your server block:
server {
listen 443 ssl;
http2 on;
server_name example.com;
server_tokens off;
include snippets/security-headers.conf;
location / {
try_files $uri $uri/ /index.php?$args;
}
location ~ \.php$ {
# Nginx add_header does NOT inherit if this block sets its own header.
include snippets/security-headers.conf;
fastcgi_pass unix:/run/php/php-fpm.sock;
include fastcgi_params;
fastcgi_hide_header X-Powered-By;
}
}
The single biggest Nginx trap: add_header directives are not merged. As soon as a location, if or nested block declares one add_header of its own, every header inherited from the parent level is discarded. That is why so many sites pass a scanner on the homepage and fail completely on /wp-admin or on PHP responses. Either include the snippet in every block that adds headers, or centralise everything at server level and add nothing lower down. An extended version exists for anyone curious.
Real-world misconfigurations we keep finding
- Headers set on HTTP only, or on the redirect vhost. The browser follows the 301 to HTTPS and receives no headers because the HTTPS block was never touched.
- Duplicate headers. A CDN, a WordPress security plugin and the server config all add
X-Frame-Options. Conflicting duplicates can be ignored entirely by the browser. Pick one layer and own it. - CSP with
script-src 'unsafe-inline' 'unsafe-eval' *. Looks like a policy, protects against nothing. - HSTS with
includeSubDomainson a domain whose subdomains have no certificate. Internal tools go dark and the fix is not instant, because the browser has already cached the policy. - Missing
alwaysin Apache or Nginx. Error responses ship unprotected, and error pages are exactly where injected content tends to be reflected. - Headers dropped by a reverse proxy or CDN. Always test the public URL, not the origin server.
- Deprecated headers still in place.
X-XSS-Protection,Expect-CT,Public-Key-PinsandFeature-Policyshould be removed.X-XSS-Protectionin particular introduced its own vulnerabilities and is no longer honoured by modern browsers. - Verbose
ServerandX-Powered-Byheaders. Free reconnaissance for an attacker. Turn offserver_tokensin Nginx, setServerTokens Prodin Apache, and setexpose_php = Offinphp.ini.

How to check the security headers for your website
With the command line
curl -sI https://example.com | grep -Ei 'content-security|strict-transport|x-frame|x-content-type|referrer-policy|permissions-policy|cross-origin'
Test more than the homepage. Run the same command against a PHP page, a static asset, your login page and a URL that returns 404. If the results differ, you have an inheritance or scope problem.
With browser dev tools
- Open dev tools with F12 and go to the Network tab.
- Reload the page and click the first document request (usually the HTML).
- Open Headers, then Response Headers. Enable the raw view to see duplicates that the pretty view merges.
- Switch to the Console tab. CSP violations are logged there with the exact directive and blocked URI, which is the fastest way to debug a policy.
- Check the Application tab for cookie flags while you are there.
Secure,HttpOnlyandSameSitebelong to the same hardening pass.
With free online scanners
| Tool | Best for |
|---|---|
| securityheaders.com | Fast A to F grade, ideal for a before and after screenshot in a client report |
| MDN HTTP Observatory | Detailed, weighted scoring with actionable recommendations and cookie checks |
| headerscan.com / apivoid | Quick second opinion and a plain-language breakdown of each header |
| hackertarget HTTP Header Check | Raw header dump with an API, handy for scripted checks |
| CSP Evaluator | Judging whether your CSP is actually strict or just long |
Treat scanner grades as a smoke test, not a security assessment. A wildcard CSP scores well and protects nobody. Conversely, a deliberate Referrer-Policy: unsafe-url on a specific analytics path may cost you a grade while being a conscious business decision.

A sane deployment order
- Confirm HTTPS is clean everywhere, then add
X-Content-Type-Options,X-Frame-Options,Referrer-PolicyandPermissions-Policy. Zero to low risk, immediate gain. - Add HSTS with a short
max-age, verify all subdomains, then raise it and consider preload. - Add
Cross-Origin-Opener-PolicyandCross-Origin-Resource-Policy, and test your embeds and popups (payment flows and social logins first). - Deploy CSP in report-only, gather real violation data, tighten, then enforce.
- Remove deprecated and disclosure headers.
- Re-scan after every CDN, plugin or theme change. Headers regress silently.
FAQ
What are the top security headers recommended by OWASP?
The OWASP HTTP Security Response Headers cheat sheet centres on Content-Security-Policy, Strict-Transport-Security, X-Content-Type-Options, X-Frame-Options, Referrer-Policy, Permissions-Policy, the Cross-Origin-* family and Clear-Site-Data on logout. It also recommends removing Server, X-Powered-By and the deprecated X-XSS-Protection.
Do security headers affect SEO or page speed?
They add a few hundred bytes per response, which is negligible. Indirectly they help: HSTS removes a redirect hop on repeat visits, and a site that is not serving injected spam content keeps its rankings. A misconfigured CSP that blocks your own scripts, however, can wreck rendering and Core Web Vitals, which is precisely why report-only mode exists.
Can I add security headers without server access?
Yes. On Apache shared hosting, .htaccess works. If you sit behind Cloudflare or another CDN, you can inject headers with transform rules or an edge worker. WordPress plugins can also set them via PHP, but headers added in PHP will not apply to static files served directly by the web server, so coverage is partial.
Why does my scanner still report a missing header after I added it?
Common causes, in order of frequency: the header was added to the HTTP vhost instead of the HTTPS one, an add_header in a nested Nginx location wiped the inherited set, a CDN is caching an old response (purge and retest), or you forgot the always keyword and the scanner is looking at a non-200 response.
Is X-XSS-Protection still needed?
No. It is deprecated, ignored by current browsers and historically introduced its own information-leak issues. Remove it and rely on a properly scoped Content-Security-Policy instead.
What is the difference between X-Frame-Options and CSP frame-ancestors?
frame-ancestors is the modern, more flexible mechanism and takes precedence in browsers that support both. Keeping X-Frame-Options: SAMEORIGIN alongside it costs nothing and covers older clients, so we ship both. HTTP Security Headers covers this in more depth.
Need a second pair of eyes on your headers?
Header hardening is cheap to do and easy to get subtly wrong. If you want your Apache or Nginx configuration reviewed, your CSP built from real traffic data instead of guesswork, and a documented before and after report, get in touch with the team at Vibe Midia and we will audit your stack.

0 Comments