Security Headers

Why Your Security Headers Are Not Working

You added the headers. The scanner still flags them as missing or misconfigured. Here are the five most common reasons this happens and how to diagnose each one.

SiteSecurityScore Team·9 min read·Updated Mar 31, 2026
Developer looking at server configuration on a monitor while debugging a web security issue

Security header problems fall into two categories. Either the header never reaches the browser at all, or it does reach the browser but is configured in a way that makes it ineffective. Both look the same from the outside: the scanner flags an issue, or the browser console shows a violation.

The causes are usually straightforward once you know where to look. This guide works through the five most common failure points in order of how often they appear.

Confirm what the server is actually sending#

Before diagnosing why headers are not working, confirm exactly what headers are being sent. It is common to assume a configuration change took effect when it did not.

The simplest approach is to open your browser's developer tools, go to the Network tab, reload the page, and click on the initial HTML document request. Under "Response Headers" you will see every header the server sent for that request. Security headers you are looking for include content-security-policy, strict-transport-security, x-frame-options, and x-content-type-options. If they are not in the list, the server is not sending them, regardless of what your configuration file says.

From the command line, curl -I https://yourdomain.com shows just the response headers without downloading the page body. This is useful for quick checks during debugging and works well in CI pipelines where you want to assert that specific headers are present after a deployment.

For a more thorough check that catches configuration issues and not just presence, SiteSecurityScore scans all of your security headers at once and flags both missing headers and headers that are present but contain known weaknesses. This is useful when you want to verify a full configuration change rather than check one header at a time.

Your server configuration is not being applied#

The most common reason headers do not appear is that the server configuration file was edited but the change was never picked up. This happens in several ways.

The server was not restarted. Apache and Nginx read their configuration at startup. A change to httpd.conf, .htaccess, or nginx.conf has no effect until you reload the service. Run sudo systemctl reload nginx or sudo systemctl reload apache2 after every configuration edit. Note that reload is preferred over restart because it applies the new config without dropping active connections.

You edited the wrong file. Apache uses a hierarchy of configuration files: the main config, virtual host files, and .htaccess files in directory trees. Headers added in a virtual host block only apply to requests handled by that block. If your site runs on a different virtual host than the one you edited, or if AllowOverride is set to None, your .htaccess changes will be silently ignored.

The required module is not loaded. In Apache, response header manipulation requires mod_headers to be enabled. Run apache2ctl -M | grep headers to check whether it is active. If it is not listed, enable it with a2enmod headers and reload Apache.

Nginx location blocks are overriding server blocks. In Nginx, an add_header directive in a location block replaces, rather than extends, the headers set in the enclosing server block. If you have added security headers at the server level but your responses go through a location block that sets its own headers, the server-level headers will not appear. The solution is to repeat all of your security header directives inside each location block that sends responses to clients.

Test your config before reloading

Run nginx -t or apachectl configtest before reloading your server. Both commands check the configuration for syntax errors and print the first problem they find. Reloading a broken configuration can take your site down, while a dry-run test costs nothing.

A CDN or proxy is stripping your headers#

A content delivery network (CDN) sits between your origin server and your visitors. Every response passes through it. CDNs can add, modify, or remove headers as responses pass through, and their default behavior is not always to pass everything through unchanged.

To check whether your CDN is the problem, compare the headers you see through your domain name against the headers from your origin server directly. If you can access your origin by IP address, run curl -I http://ORIGIN_IP -H "Host: yourdomain.com". If the security headers appear in the direct response but not through the domain, the CDN is stripping them.

Different platforms handle this differently:

  • Cloudflare: Cloudflare passes most security headers through from the origin by default. However, Transform Rules and Page Rules can override or remove them at the edge. Check your Rules configuration in the Cloudflare dashboard if headers are missing. Cloudflare also allows you to add response headers via Transform Rules, which is useful if you prefer to manage headers centrally rather than on each origin server.
  • Amazon CloudFront: CloudFront strips many headers from origin responses unless you explicitly configure a response headers policy. Go to your distribution settings, find the Behaviors section, and attach a response headers policy that includes your security headers. Without this step, even if your origin sends them, they will not reach clients.
  • Vercel: Vercel does not automatically forward all custom headers from origin functions. Define security headers in your vercel.json file under the headers array. Headers set there are applied at the edge to every matching response, regardless of what your serverless function returns.
  • Other reverse proxies: Nginx and HAProxy configured as reverse proxies require explicit proxy_pass_header or http-response add-header directives to forward or inject headers. A default proxy configuration will forward most headers, but some setups strip headers selectively based on their names or values.

The header is present but not effective#

Some of the most common issues involve headers that are technically present but configured in a way that provides little or no real protection. A scanner that checks values, not just presence, will flag these.

Content-Security-Policy with unsafe-inline. The CSP directive unsafe-inline in script-src allows all inline scripts to run on your page. Since most cross-site scripting (XSS) attacks work by injecting inline scripts, a CSP that permits them offers almost no XSS protection despite appearing in your headers. The same applies to unsafe-eval. To get real protection, replace these with nonces or hashes. See the guide on removing unsafe-inline from CSP for migration steps.

Strict-Transport-Security with a short max-age. HSTS (HTTP Strict Transport Security) tells the browser to only connect to your site over HTTPS for a period of time set by max-age. A value of max-age=0 tells the browser to forget the policy immediately. A value of a few hundred seconds provides almost no protection because it expires before most users return to your site. The recommended minimum is one year, expressed as max-age=31536000. For sites that are fully HTTPS and include all subdomains, adding includeSubDomains and preload provides the strongest enforcement.

X-Frame-Options with ALLOW-FROM. The ALLOW-FROM directive was designed to allow embedding from a specific origin. Modern browsers dropped support for it and silently ignore the header when this directive is used, leaving your page unprotected against clickjacking. Use DENY or SAMEORIGIN. If you need to allow embedding from a specific origin, use the frame-ancestors directive in a CSP instead, which all modern browsers support.

Referrer-Policy set to unsafe-url. The unsafe-url value tells the browser to send the full URL of every page to every external resource and link destination, which is the least private setting. It is no better than having no Referrer-Policy header at all. Use strict-origin-when-cross-origin (the browser default as of 2021) or no-referrer if you want to prevent referrer leakage entirely.

A header that is present is not the same as a header that works

Presence checks confirm that a header was sent. They say nothing about whether its value is correct. A Content-Security-Policy containing unsafe-inline, an HSTS header with max-age=0, or an X-Frame-Options: ALLOW-FROM value will all pass a presence check while offering no meaningful protection.

A cache is serving the old response#

Caching can make it look like your headers have not changed when they have. A cached response does not come from your server. It comes from a previous snapshot stored either in the browser or at the CDN edge, and that snapshot was taken before your changes were deployed.

Browser cache. When testing changes, always use incognito or private browsing mode, or perform a hard reload with Ctrl + Shift + R (Windows) or Cmd + Shift + R (Mac) to force the browser to bypass its cache and fetch from the server. In Chrome DevTools you can also check "Disable cache" in the Network tab while DevTools is open. A regular reload (F5) may serve a cached response even if you have made changes.

CDN cache. If your CDN has cached a response from before you deployed new headers, it will keep serving the old version until the cache entry expires or you purge it manually. Most CDN dashboards provide a "purge cache" option. For Cloudflare, go to Caching, then Configuration, and choose Purge Everything. For CloudFront, create an invalidation in your distribution settings. After purging, re-run your scan or curl command to see the updated headers.

Identifying a cached response. Look for an Age header in the response. A non-zero value means the response came from a cache, and the number is how many seconds ago it was originally fetched. Also look for x-cache: HIT or similar CDN-specific indicators. Cloudflare adds cf-cache-status: HIT, and CloudFront adds x-cache: Hit from cloudfront.

FAQ#

Why do my security headers disappear after a deployment?

Deployments often overwrite or reset server configuration files, especially on shared hosting or platform-as-a-service environments. Some deployment pipelines regenerate server configuration from a template that does not include your custom headers. Check whether headers are set in a file that gets replaced on deploy versus a file you control directly. Setting headers at the CDN level rather than the origin server is often more resilient to deployment resets.

My headers show in staging but not in production. Why?

Staging and production environments often have different CDN configurations, load balancers, or server setups. The most common culprit is a production CDN that strips or overrides headers from the origin. Compare the response headers from your origin server directly against those returned through the CDN to isolate where the difference starts.

My CSP is blocking scripts even though I added the source to the policy. Why?

CSP matching is strict. If you added example.com but the script loads from cdn.example.com, it will be blocked because subdomain matching requires an explicit wildcard. Check the browser console for the full blocked URL in the violation message, then match your policy against that URL exactly. Also verify the server is not serving a cached version of the old policy.

HSTS is set on my server but my site still loads over HTTP. Why?

HSTS only enforces the upgrade after the browser has received the header at least once over a valid HTTPS connection. A first-time visitor on HTTP has not seen the header yet, so no enforcement happens on that visit. Additionally, some CDNs strip the Strict-Transport-Security header before it reaches clients. Verify with curl that the header appears in the response through your domain name, not just at the origin.

How do I know whether my CDN is stripping security headers?

Compare headers from a direct request to your origin IP against headers through your domain. Use curl -I with the raw origin IP and set the Host header manually to your domain. If headers appear from the origin but not through the domain, the CDN is stripping them. Check your CDN response headers policy, Page Rules, or edge configuration to find where they are being removed.

References

Was this helpful?

Check your security headers now

Find out exactly which headers your site sends, what values they have, and whether those values are configured correctly. Free, no account required.