A site check that reports "missing security headers" is asking for a handful of lines: an HSTS header (Strict-Transport-Security), a rule about who may frame the page, X-Content-Type-Options: nosniff, a Content-Security-Policy, and no version numbers in the Server or X-Powered-By header. Each is a line or two of configuration. The danger is the snippet copied whole from a tutorial, because some of these headers can lock visitors out or leave the page blank the moment they go live: a policy that allows scripts only from your own origin blocks the inline code a framework writes, plus web fonts and analytics; a rule that extends HTTPS to all subdomains makes a plain-http subdomain unreachable; a header sent before the site redirects to https does nothing at all. This guide takes the headers one at a time and separates what is safe to send today from what should be staged first.

Start from what the site really sends

Look at the response, not at the config file, and look at two addresses: the front page and one page several levels down.

curl -sI https://example.com/
curl -sI https://example.com/docs/getting-started
curl -sI http://example.com/

The deep page matters because a header rule often matches the home page only, and the tool that flagged your site probably requested only the front page. The plain-http request matters for the first header below.

Strict-Transport-Security: the redirect first, subdomains later

This header tells the browser to use https for your site from now on. MDN lists three things that decide whether it helps or hurts (MDN):

  • Browsers discard it when the response came over plain http. If your site still serves pages on http, first make every http request bounce to the https address with a permanent redirect, and leave the header off that redirect.
  • With includeSubDomains, the rule covers every subdomain of the domain. Any subdomain that has no https, an old admin panel or a mail web interface for example, becomes unreachable for visitors who have seen the header.
  • Preloading needs includeSubDomains plus a max-age of a year or more, which is 31536000 seconds.

The people who run the preload list are blunter: tools and guides should not add preload by default, getting onto the list cannot easily be undone, and they consider preloading not recommended because its benefit over plain HSTS is small (HSTS preload). They also suggest growing the value in steps, five minutes first, a week next, a month after that, while you watch for problems.

A safe default is Strict-Transport-Security: max-age=31536000 with nothing else, added once the redirect is in place. Add includeSubDomains only after checking every subdomain, including internal ones. If something goes wrong, MDN's way out is to send max-age=0, which tells browsers to forget the rule.

Framing: one header is enough

To stop other sites from loading yours in a frame, send X-Frame-Options: SAMEORIGIN or a frame-ancestors 'self' rule inside a Content-Security-Policy. SAMEORIGIN is a better default than DENY because sites often frame their own pages. Two details from MDN catch people out. The old ALLOW-FROM value is obsolete, and modern browsers that see it ignore the whole header, so a policy meant to allow one partner ends up allowing everyone (MDN). And neither form works from a meta element in the page: both have to be real response headers (MDN).

nosniff: check the types before you switch it on

X-Content-Type-Options: nosniff tells the browser to trust the declared type of each file. For scripts and stylesheets it goes further: the browser rejects scripts sent without a JavaScript type and stylesheets sent as anything other than text/css (MDN). On a well-configured host that changes nothing. On a server that sends a script as plain text, that script stops loading, so request one script and one stylesheet with curl -sI and read their Content-Type before you add the header.

Content-Security-Policy: enforce only what cannot break rendering

This is the header that takes sites down. A policy such as default-src 'self' looks tidy and blocks every inline script your framework writes, every font from a font host and every analytics script, as soon as it is enforced. The way through is to split the policy in two.

The enforced header carries only rules that do not decide which scripts, styles or images load: frame-ancestors 'self' (the framing rule above), object-src 'none' (no plugin content) and base-uri 'self' (no injected base element redirecting your relative links). Those three can go live today.

The rules that can break the page, the lists of allowed origins for scripts, stylesheets, pictures, fonts and fetch calls, belong in another header, Content-Security-Policy-Report-Only, and those lists should hold only origins your page actually loads from. MDN states that a response may carry both headers and the browser honours each: one blocks, the other merely tells you what it would have blocked (MDN). The reports are the point. They are sent only when the policy names an endpoint with report-to and the response defines it in a Reporting-Endpoints header, or your own code reads them in the browser through a ReportingObserver (MDN). With neither in place, the staged policy runs silently and you learn nothing. Once the reports are quiet, move the source rules into the enforced header.

Two more rules keep this honest. An inline script cannot be allowed by its origin; mark it with a nonce, or allow its exact digest, instead of putting 'unsafe-inline' into the script rule. And a report-only header on its own protects nothing, however strict it reads. Our longer piece on the ways a written policy ends up allowing script anyway goes through what to look for once a policy exists.

If your server runs Express, note that Helmet's default call sends an enforced policy that includes script-src 'self', and its default HSTS header includes subdomains (Helmet). One line of app.use(helmet()) therefore brings both risks above at once. Helmet lets you switch its policy off or into report-only mode and set the HSTS options yourself, which is what you want while the policy is being staged.

Version numbers: hygiene, not armour

A Server: nginx/1.24.0 or X-Powered-By: PHP/8.3.4 header tells anyone which software and release you run, and site checks flag it. Remove the number where it is added: server_tokens off; in nginx (nginx), ServerTokens Prod in the Apache server config, poweredByHeader: false in Next.js (Next.js), app.disable('x-powered-by') in Express. Keep expectations modest, though. The Apache manual itself says that hiding the header does nothing to make a server more secure and calls security through obscurity a myth (Apache). Patch the software; trim the header to clear the report.

Put the headers where the host applies them

A correct header in the wrong file is the quietest failure on this list, because nothing in the file looks wrong while the response stays the same.

  • Netlify. Headers from _headers or the configuration file reach only files Netlify serves itself. Content Netlify proxies from elsewhere, and pages built by functions or edge functions (server rendering included), are left out; there the function itself must add the headers (Netlify).
  • Cloudflare Workers static assets. The _headers file is not applied to responses that Worker code generates, even when the address matches a rule, which covers server-rendered frameworks. If the file names the same header twice, Cloudflare merges the two values into one, separated by a comma; change the line that is there rather than adding another (Cloudflare).
  • Next.js. Rules in the headers() config are checked before the filesystem, which means pages and static files both get them; /:path* as the source reaches every path, and if two rules match and both set one header, the later rule's value is what goes out (Next.js).
  • nginx. add_header lines are inherited from the outer block only if the inner block declares no add_header at all, so a location with a single add_header silently drops all your server-level security headers there. Add always so error responses carry them too (nginx).

A check that can fail

Run the three requests from the start again. The plain-http request should come back as a permanent redirect pointing at https. Both the front page and the deeper page must carry every header you added; a header that appears on one and not the other means the rule's path pattern is too narrow or a layer you did not configure answered. Then open the site in a browser with the console visible and click through a few pages: the enforced part should produce no blocking errors at all, and the report-only part should produce reports where your endpoint or observer can see them. Last, rerun the check that flagged the site. If it still complains about a header that curl shows, it is probably reading a different address than you are.

When this does not apply

Missing headers are the subject here. Reviewing a policy you already have, cookie flags and http addresses inside your markup are separate problems. The host notes cover only the hosts named above, as documented on 12 November 2026; a CDN or proxy in front of your origin may add, change or strip headers, so the response you test is the only truth. We did not test the headers in every browser, and the staged approach to the policy costs time: until you move the source rules into the enforced header, your page has framing and plugin protection but no script restriction.

What to do next

What this work really involves is a short list of decisions made in the right order: redirect before HSTS, subdomains checked before includeSubDomains, types checked before nosniff, a policy split into a safe enforced part and a staged report-only part, and every header placed in the file that this host actually applies. It also means leaving alone the things that are fine, such as a year-long max-age with no preload, framing handled by SAMEORIGIN alone, or a Server header with no version in it.

We sell a skill for that job, Security Headers Writer. You hand it the line from the site check, the headers your site returns now, the config that sets them and the HTML head of a page. Back comes a line for each problem, a block of headers written for your host and ready to copy, and a short list of checks; when nothing is missing, the whole reply is one sentence. The enforced part it writes is limited to rules that leave rendering alone, and the origin lists it stages in report-only mode come from your own page. It reads what you paste and does not visit your site. The store is at https://aiskills402.com.

Read this post as Markdown: /blog/security-headers-without-breaking-page.md · Atom feed.