Identities for entities did.bot

serve: send Strict-Transport-Security from the server when its certificate is publicly trusted #210

Closed permadeath.com
Closed opened by permadeath.com

What failed. Mozilla HTTP Observatory (algorithm 6) graded the dashboard at https://pds.did.bot C+ (60) on 2026-10-14, losing 20 points to strict-transport-security: hsts-not-implemented. Qualys SSL Labs graded pds.did.bot and tourmaline-lemming.test.pds.did.bot A, not A+, for the same reason. curl -sI https://pds.did.bot/ shows no Strict-Transport-Security header. Scan: https://developer.mozilla.org/en-US/observatory/analyze?host=pds.did.bot

Why it matters. The dashboard holds the operator's session, and /oauth/browser holds an agent's browser session. did.bot's CloudFront header (max-age=63072000; includeSubDomains) covers *.pds.did.bot only for a browser that has already visited did.bot. An operator who runs didbot under their own domain gets no HSTS at all. security_headers in crates/didbot-serve/src/routes/mod.rs is the only place the server's own response headers are set.

Spec. RFC 6797 §6.1 and §8.1: a host that sends the header over a secure connection gets every later request upgraded to HTTPS, and certificate errors become hard failures with no click-through.

Smallest fix. In security_headers, add Strict-Transport-Security: max-age=<n>; includeSubDomains only when the run's acme_environment is production. A staging leaf plus HSTS locks browsers out of every agent hostname, which is why infra/site/ gates did.bot's header on pds_serves_a_production_certificate. includeSubDomains on the zone covers every agent hostname, and all of them are served by this process under the zone's wildcard certificate. max-age is a new config key with a documented range. The owner should choose its default: it commits every browser that sees it for that long.

Fixed by #1184 on main: d49e96f9 sends Strict-Transport-Security from the server itself once it holds a production certificate.

Sign up or Sign in to add to the discussion.