Select Page

Essential Security Headers Every Website Should Have (And How to Add Them in Apache and Nginx)

by | Oct 9, 2026 | Uncategorized

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.
server code security

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' on script-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

  1. Deploy Content-Security-Policy-Report-Only with your candidate policy.
  2. Collect violations with report-to or a free reporting endpoint for one to two weeks of real traffic.
  3. Add the legitimate sources you discovered, remove the rest.
  4. Switch the header name to Content-Security-Policy to 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 preload directive and submit to the preload list only when you are certain. Removal takes months. Preload requires max-age of at least 31536000 and includeSubDomains.
  • Test with a short max-age=300 first, 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-site if you serve assets across your own subdomains, or cross-origin on 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.
server code security

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.

server code security

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

  1. 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.
  2. 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.
  3. CSP with script-src 'unsafe-inline' 'unsafe-eval' *. Looks like a policy, protects against nothing.
  4. HSTS with includeSubDomains on 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.
  5. Missing always in Apache or Nginx. Error responses ship unprotected, and error pages are exactly where injected content tends to be reflected.
  6. Headers dropped by a reverse proxy or CDN. Always test the public URL, not the origin server.
  7. Deprecated headers still in place. X-XSS-Protection, Expect-CT, Public-Key-Pins and Feature-Policy should be removed. X-XSS-Protection in particular introduced its own vulnerabilities and is no longer honoured by modern browsers.
  8. Verbose Server and X-Powered-By headers. Free reconnaissance for an attacker. Turn off server_tokens in Nginx, set ServerTokens Prod in Apache, and set expose_php = Off in php.ini.
server code security

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

  1. Open dev tools with F12 and go to the Network tab.
  2. Reload the page and click the first document request (usually the HTML).
  3. Open Headers, then Response Headers. Enable the raw view to see duplicates that the pretty view merges.
  4. 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.
  5. Check the Application tab for cookie flags while you are there. Secure, HttpOnly and SameSite belong 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.

server code security

A sane deployment order

  1. Confirm HTTPS is clean everywhere, then add X-Content-Type-Options, X-Frame-Options, Referrer-Policy and Permissions-Policy. Zero to low risk, immediate gain.
  2. Add HSTS with a short max-age, verify all subdomains, then raise it and consider preload.
  3. Add Cross-Origin-Opener-Policy and Cross-Origin-Resource-Policy, and test your embeds and popups (payment flows and social logins first).
  4. Deploy CSP in report-only, gather real violation data, tighten, then enforce.
  5. Remove deprecated and disclosure headers.
  6. 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

Submit a Comment

Your email address will not be published. Required fields are marked *