Back to Blog
Security8 October 2024Michael Hettwer5 min read

Hardening HTTP response headers: HSTS and CSP

How we roll out HSTS and CSP without breaking sites: staged max-age, report-first CSP, the cheap headers, and the nginx inheritance trap.

Anyone operating a web server has usually finished the encryption chapter: certificate in place, HTTPS redirect set up. The next layer sits in the response headers the browser receives, and it is less conspicuous than a certificate but more effective: it decides whether an on-path attacker even gets the chance to force HTTP, and whether script injected into the page can do damage.

HSTS: the browser may not fall back

The Strict-Transport-Security header is an instruction to the browser: this site is reachable over HTTPS only, and that holds for the duration of the max-age value, even when a later request accidentally points at HTTP. Without the header, the first request of a user is the open spot: an attacker on the path can intercept it before the redirect even happens. With the header, the browser stays safe even when the server stumbles once.

The staged plan that holds in practice is small: start with a short max-age, hours for example, extend to days after an uneventful phase, then weeks, then months. The reason is simple: an aggressively set HSTS value makes the domain unreachable for users when the certificate is ever unusable, and the return takes exactly as long as the value. The certificate topic itself belongs in another post; what counts here is that headers and certificate operations presuppose each other.

The preload list question, where browsers remember the domain permanently, is a genuine decision and not a default: it increases the effect and reduces the flexibility. We recommend deciding it consciously rather than inheriting it from a copy-paste template.

CSP: watch first, then close

Content-Security-Policy answers the question of what a script that reaches the page may do: from which sources it may load, whether it may execute, which connections stay allowed. In practice the rollout fails regularly on one detail: the site works in testing, but some forgotten service, a widget or an analytics call gets blocked. So the staged plan runs in the other direction than with HSTS: observe first, then tighten.

The toolbox for that is report-only mode: the policy is set but only reported, not enforced. The browser sends violation reports, and the real policy gets built from those reports: which sources does the page truly need, which scripts run, which connections must remain allowed. Only when the reports look stable for a while does the policy switch to enforcement mode. The path costs a few weeks of attention and spares the emergency brake where the policy gets rolled back because a paying feature went dark.

The cheap headers worth setting anyway

Besides the two big ones there is a small collection set with little effort that still matters: the header that stops the browser from guessing content types, the statement of how much referrer detail travels to targets, and the control over whether the page may appear in a frame. These headers set quickly and rarely cause trouble; the only misstep is assuming they replace the big work. They are the trim, not the foundation.

The nginx trap worth knowing

On the nginx side there is one quirk that regularly causes confusion: add_header does not inherit from the server level into location blocks once a location block contains its own add_header call. The consequence is that a cleanly set header line suddenly disappears as soon as a single location sets its own headers. The remedy is simple: define headers centrally in one place, or consciously walk the inheritance and repeat the common headers in every location block. Either is legitimate; mixing both is a bug.

Verification as routine

Header hardening does not end with setting them. The check belongs in the routine: a look at the response with the command-line tool of choice shows immediately which headers actually arrive, and a header scanner delivers the systematic list of what is missing. The sensible rhythm matches certificate checks: a check after every configuration change, and once a quarter a full pass over all publicly reachable applications.

What the first weeks afterwards show

The weeks after introducing the headers deliver two kinds of feedback in practice. The first arrives from the CSP reports: some entries show third-party content nobody had on the list, and a few show own behavior that only appears in edge cases, for example in certain document formats or in the print view. Both kinds are valuable, because they make the final policy more precise than any upfront analysis managed. The report-only mode earns its runtime therefore not only as a security exercise but as a survey of the own application.

The second feedback comes from users, and it is quieter: a tightened policy that blocks one forgotten detail shows up as a ticket, not as an alarm. So the rollout includes a short agreement with support about the format such reports arrive in and where they go. Whoever keeps that path open gets the residual defects reported in days instead of months, and the policy reaches its target state without the silent alternative in which single features stop working permanently and nobody attributes it to the header.

A thought on the order of the two big headers belongs in the planning, because they influence each other: a CSP introduced on a site without HSTS guards a path that is circumventable anyway through the unencrypted first request. The sensible order therefore starts at the transport question and adds content control on top. The difference is small in practice, because both measures get planned anyway, but it orders the expectations: the first report week of the CSP says something about the page's content, not about the path's security, and whoever fails to separate that reads the reports wrongly.

If you want your applications' header layer on such a footing, the security hardening and audits page describes the matching service, and the contact page is the fastest way to reach us.

Sources

  • IETF, RFC 6797 (HTTP Strict Transport Security), https://datatracker.ietf.org/doc/html/rfc6797 (accessed 2026-11-03)
  • MDN Web Docs, Content-Security-Policy, https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy (accessed 2026-11-03)
  • nginx documentation, ngx_http_headers_module, https://nginx.org/en/docs/http/ngx_http_headers_module.html (accessed 2026-11-03)