skip to content

Strict-Transport-Security with includeSubDomains and a year-long max-age is deployed — how do you safely roll it back for one subdomain?

level: seniorimportance: should knowfreq 42%

answer

  1. state lives in the browser, not the response
  2. removing the field changes nothing
  3. shorten first, zero second
  4. deletion must arrive over TLS
  5. long tail bounded by returning traffic

basics

~20 s

Keep serving TLS and shorten max-age on responses until stored entries are nearly expired, then send max-age=0 over a secure connection to clear them, and only then stop sending the field. Browsers that never return keep the old entry to its full term.

solid answer

~50 s

You cannot recall a policy a browser already stored; the only channel is a later response, and the browser must fetch it over a **secure** connection for the field to be processed. So the rollback is staged. First, keep TLS working and serve a much smaller `max-age` — each returning visit replaces the stored expiry with the new count from reception, so the tree's exposure shrinks with traffic. Second, once the short value has had time to circulate, send `max-age=0` over TLS; that drops the entry and takes `includeSubDomains` coverage with it. Only then stop sending the field, and only then can that name be served without TLS. A browser that never comes back keeps the original entry until its full year runs out, so the timeline is bounded by visitor behaviour rather than by your deploy. If the name is on the pre-loaded list, none of this reaches it.

code

http · 3 lines
http
HTTP/1.1 200 OK
Strict-Transport-Security: max-age=300; includeSubDomains
Content-Type: text/html; charset=utf-8

go deeper

for a junior

Recall that the policy lives in the browser, so taking the header field off your responses does not undo anything already stored.

for a middle

Explain the two-step sequence — shorten the lifetime, then send zero, both over TLS — and why each step must stay in place for a while.

for a senior

Show the operating reality: the rollback completes at the rate visitors return, and moving the name to plaintext first turns the problem into an unrecoverable outage.

for a principal

Treat the asymmetry as a design constraint on rollout: widening is cheap and reversing is not, so scope decisions should be made when they are still cheap to unmake.

## Why a rollback is not a deploy Every other header is a property of the response you are serving right now. `Strict-Transport-Security` is different: processing it creates **state inside the browser** that outlives the response, the connection and the deploy. Removing the field from your responses does not remove that state — it merely stops refreshing it. A browser holding a year-long entry will keep rewriting `http` to `https` for the covered tree for the remainder of that year, no matter what you serve. Two constraints shape everything that follows: 1. **The only channel is a later response.** There is no push, no revocation feed, no way to reach a browser that does not come back. 2. **That response must be carried over a secure connection.** A `Strict-Transport-Security` field received over non-secure transport must be ignored, including one that says `max-age=0`. Turning TLS off first does not clear anything; it makes the name unreachable to every browser holding the policy, with no way left to tell them otherwise. ## The staged sequence 1. **Keep serving TLS, and shorten `max-age`.** Serve a small value — minutes rather than months — while keeping the site working. Every visit that receives the field replaces the stored expiry with the new count *from reception*, so returning browsers drop from a year of remaining coverage to minutes. Leave this running long enough for your actual visitor population to come back at least once; that period is a property of your traffic, not of the specification. 2. **Then send `max-age=0`.** Over a secure connection, this is the specified deletion signal: the browser stops treating the host as a Known HSTS Host and drops the entry, and `includeSubDomains` has no effect at zero, so tree coverage goes with it. Keep serving this for a while too, for the same reason. 3. **Then stop sending the field at all**, still over TLS. 4. **Only now** can the name be served over plaintext without stranding anyone. | step | what is served | what it achieves | |---|---|---| | 1 | `max-age=300; includeSubDomains`, over TLS | shrinks every refreshed entry to minutes | | 2 | `max-age=0`, over TLS | deletes the entry on every browser that returns | | 3 | no field, over TLS | stops recreating policy for anyone | | 4 | plaintext permitted | safe only once the tree is genuinely uncovered | ## What the sequence cannot fix - **The long tail.** A browser that does not visit during steps 1–3 still holds the original entry and will hold it until the year expires, counted from *its* last visit. For a portal used termly, that tail is long. The honest statement in a design review is that the rollback completes at a rate set by returning traffic. - **Names below the parent that are already broken.** If `includeSubDomains` was asserted and some name under the tree cannot serve TLS, it is unreachable *now*, and step 1 cannot be served from that name — the policy came from the parent. The parent is where the shortening must happen. - **A pre-loaded entry.** If the name is on the vendor-run pre-loaded list, the list entry is consulted regardless of what your responses say. Removal is a request to the list operators, it is outside your control, and it reaches users only as browsers ship new releases. This is the asymmetry that makes list submission a different decision from sending a header. ## The mistake worth naming The instinct under pressure is to move the affected name back to plaintext immediately and fix the policy afterwards. That is exactly backwards. Every browser holding the entry will rewrite its request to `https` before it is built, reach a name with no working secure transport, hit a transport error — and on a Known HSTS Host that error is terminal, with no way for the user to proceed. You have converted a policy problem into an outage and simultaneously destroyed the only channel through which the policy could have been withdrawn. ## How to avoid needing this The reason experienced teams stage HSTS upward is precisely this asymmetry: lengthening reaches everyone on their next visit, while shortening reaches only those who come back. A sensible rollout runs a short `max-age` on the single name first, then a longer one, then widens to `includeSubDomains` only after every name under the domain has been enumerated and confirmed to serve TLS — and treats submission to the pre-loaded list as a separate, later, much heavier decision.

  • Why not simply stop sending the header field and wait?
    Because stopping only prevents refreshes; it never shortens what is already stored. Every browser keeps rewriting requests for the covered tree until its own entry expires, counted from its last visit. Shortening first, then sending zero, actively replaces that stored expiry on each return visit, which is the only lever you have.
  • Can the deletion signal be sent from the affected subdomain itself?
    Only if that name still serves TLS, and only for its own entry. Where coverage came from a parent asserting includeSubDomains, the stored entry is the parent's, so the shortening and deletion must be served from the parent. A child cannot cancel a policy it never delivered.
  • How long should each stage run before moving to the next?
    Long enough that your real visitors have returned at least once — which is a traffic question, not a specification one. Measure the interval between repeat visits for the population that matters and run each stage beyond it. Anyone who does not return in that window keeps the old entry regardless, so the stage length buys coverage of the tail, not certainty.

saying these in an interview costs you the question

  • Thinks removing the header field clears stored policies.
  • Turns off TLS first and plans to fix the policy afterwards.
  • Believes a deletion signal works when sent over plain HTTP.
  • Assumes one deploy completes the rollback for all visitors.
  • Expects a pre-loaded entry to disappear with the header field.