A site passes D1 in March. In September the same site scores three out of four, and nobody has touched the code. The Strict-Transport-Security header is gone. Between the two scans a certificate was renewed, and the renewal came with a change to what terminates TLS. The header was configured on the old terminator and never on the new one. Every page still loads over HTTPS, every browser shows a padlock, and nothing in any dashboard has changed colour.
This post is about that mechanism, and three others like it. None is a bug in the usual sense. Each is a routine operational change that removes a signal the scanner checks, without producing an error a team would notice.
Where the header actually lives
HSTS is a response header. A browser that sees it remembers, for max-age seconds, to use HTTPS for that host without asking, which is what stops a downgrade attack on the first request. MDN's reference covers the directives, and hstspreload.org explains the preload list for sites that want the protection before the first visit. HSTS in one header covers what D1 checks and how to set it.
The detail that matters here is who sets it. The header only makes sense on an HTTPS response, so it is conventionally added by whatever terminates TLS: the application server if it serves HTTPS directly, otherwise a reverse proxy, a load balancer or a CDN. That is also the layer that holds the certificate. When the certificate's home moves, the header's home has to move with it, and it is the part nobody writes down.
A renewal by itself changes nothing. A renewal that comes with a change of terminator does. The common cases are moving from a certificate managed in the application's own configuration to one managed by the platform; switching from one proxy to another during an upgrade; adding a load balancer in front of a server that used to face the internet directly; and turning on a CDN's managed certificate. In each, the new layer serves its own headers. The add_header line or the headers() block that was doing the work is still in the repository, still correct, and no longer on the path a response takes.
What D1 sees
The scanner fetches the homepage, and check D1 records three facts. tlsValid is whether the request completed without a transport error. hsts is whether the response carried a strict-transport-security header at all; the check does not inspect max-age or the other directives. mixedContentReferences is the count of src and href attributes pointing at http:// in the rendered page, or the raw one if no render was possible.
Two points for valid TLS, one for the header, one for no mixed content. The header is the one that vanishes, because it is the only one of the three that depends on configuration living in a specific place.
Three more things that decay the same way
The HSTS case is the cleanest example, but the pattern is general. A change that is correct on its own terms removes something the scanner was counting on, and the site keeps working.
A CDN setting removes bot access. A CDN provider changes a default, or someone on the team enables a managed bot ruleset while dealing with an unrelated scraper. From that moment, requests identifying as an AI crawler get a 403 at the edge, before robots.txt is read. The site is unchanged and fully reachable from a browser. A5 goes to zero, and A2 may follow if the crawlers can no longer fetch robots.txt either. The Cloudflare post describes the most common form.
A template refactor drops the structured data. Product pages get a new layout. The component that emitted the Product and Offer JSON-LD was in the old layout and nobody carried it across, because it renders nothing visible and no test covered it. B1 and B2 fall, and for a commerce site C5 with them. Visually the pages are better than before.
A framework upgrade moves a route to client rendering. A major version changes a default, or a page that used to be server-rendered picks up a client-only dependency, and its content now exists only after hydration. The page looks identical in a browser, which executes the JavaScript. The raw HTML the crawler receives is a shell. A7 drops, and every check that reads the raw HTML of that page loses what it was reading.
None of these produces an error. The application logs are clean, the uptime monitor is green, the analytics show the same visitors, and the traffic that stops arriving is traffic nobody was counting yet.
Why a single scan does not catch it
A scan reports what it saw on the day. If the header was present, the report says so, and the report is correct. It stays correct about the day it was taken and says nothing about today. Every trust signal in the rubric has this property: the check confirms the signal exists, and nothing in the check confirms it will continue to.
The only way to catch decay is to measure twice and compare. That is not a sophisticated idea. It is what a diff is.
What a scheduled re-scan with a diff does
Our monitoring service exists for exactly this. We re-scan on a schedule, diff the result against your last one, and tell you what changed and why it matters. The diff is the point: a report that says D1 went from four to three, and that the hsts field went from true to false, is a report that names the header and the week it disappeared, which is usually enough for whoever changed the proxy to recognise what happened.
The rubric itself changes too. When it does, we re-score you against the new version and explain the delta, rather than silently moving your number. Alerts go to a human who has read your audit, not to a dashboard nobody opens, and if what broke is something we can fix in under an hour, we will usually just fix it.
The deliverables are scheduled re-scans with change diffs against your baseline, alerting on regressions routed to someone who knows your site, re-scoring and an explanation whenever the rubric version changes, and a quarterly review of what moved and what is now worth doing. It runs as a monthly retainer, scoped to the size of the site and how often it changes.
What we do about our own
We sell agent readiness, so a drop in our own score is a broken build rather than a backlog item. Our continuous integration starts the site and runs the full rubric against it on every change, and the HSTS header is set in the framework configuration on every path, with includeSubDomains and preload, alongside the rest of the security headers. Nothing sits in front of the origin to strip or replace them. The result is public at our live report, and it is the same check, run the same way, that the free scan runs against anyone else.
That arrangement is right for a small site with one deployment path. It is not a recommendation for a site with a CDN, a load balancer and three teams deploying to it, which is precisely the site where the header goes missing.
Check your own site
Run the free scan and look at the hsts field in the D1 evidence. If it is true, note the date; if you run the scan again after your next infrastructure change and it is false, you have found the mechanism this post describes. The D1 definition and the rest of the trust pillar are on the methodology page.