diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c00f47a..31faea56 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -## Unreleased (2026-11-14 night) — built and NOT released: a clock step is not a new boot (felhom.eu R-1395) +## Unreleased (2026-11-14 night) — built and NOT released: a clock step is not a new boot (felhom.eu R-1395); HSTS one year (R-1050) **MinAgent: 0.158.1** (unchanged — the new field is optional; without it the controller behaves as before). @@ -14,6 +14,13 @@ byte. Tests `TestGuestBootVerdict_AClockStepIsNotANewBoot` (14 cases), `TestGuestBootVerdict_IsWhatTheBootPassReads`; 8 mutations seen red (felhom.eu `documentation/audits/night-build-2026-11-14/C/redproof-controller-boot-key.txt`). **Not proven on a box.** With an older agent a clock step still recreates the apps once. +- **HSTS is one year (R-1050, felhom.eu `09` §3 decision 249).** `infra.HSTSMaxAgeSeconds` 86400 → 31536000; still + no `includeSubDomains`, no `preload` (the test asserts both). The ruling's condition — a month with no https + trouble since 2026-10-15 — was searched on 2026-11-14: the open and closed registers (ASCII fragments, with a + positive and a negative control), the operator's mailbox, the hub's log since its start today; no report of a name + that does not open and none of a box answering http after the floor. **The release must be built on or after + 2026-11-15** (the ruled date). A browser that has seen the new value keeps „https only” for that name for a year; + going back to one day in a later release does not shorten what a browser already holds. ## v0.352.1 (2026-11-14) — Go 1.27.4: the security fix GO-2026-6859 (felhom.eu R-1391) — RELEASED (image `sha256:1a6e8945…`, from `8747744b`, CI run id 2637; rc.1 on scratch guest 9202); per-box floors 09:18 UTC, global floor 2026-11-14 09:21 UTC (MinAgent 0.158.1); golden 0.352.1 baked and vouched the same day diff --git a/controller/internal/infra/infra.go b/controller/internal/infra/infra.go index 4a6652e9..6194760f 100644 --- a/controller/internal/infra/infra.go +++ b/controller/internal/infra/infra.go @@ -103,9 +103,12 @@ const ( ForwardedHeadersMiddleware = "felhom-forwarded-headers" HTTPSOnlyMiddleware = "felhom-https-only" HTTPSOnlyCatchAllRouter = "felhom-https-only-catchall" - // HSTSMaxAgeSeconds is ONE DAY on purpose (decision 244 brief): no includeSubDomains, no preload — a longer value is - // the operator's decision (register row), because a browser keeps it even if the box is ever served without https. - HSTSMaxAgeSeconds = 86400 + // HSTSMaxAgeSeconds is ONE YEAR (felhom.eu `09` §3 decision 249, D2 = A, R-1050): it was one day from 2026-10-15, + // and becomes one year after a month with no https trouble (searched 2026-11-14: the open and closed registers, + // the operator's mailbox, the hub's log — none). NEVER includeSubDomains, NEVER preload: a browser keeps this value + // for the whole year even if a name is ever served without https, and those two cannot be taken back. + // Pinned by TestRenderForwardedHeaders_HTTPSOnly. + HSTSMaxAgeSeconds = 31536000 ) // TraefikData is the per-customer input for the traefik stack. ACMEEmail empty → no Let's Encrypt diff --git a/controller/internal/infra/tunnel_test.go b/controller/internal/infra/tunnel_test.go index 44e0f20d..2fca1227 100644 --- a/controller/internal/infra/tunnel_test.go +++ b/controller/internal/infra/tunnel_test.go @@ -164,8 +164,9 @@ func TestRenderForwardedHeaders_HTTPSOnly(t *testing.T) { t.Errorf("%s must be redirectScheme{scheme: https, permanent: true}, got %v", HTTPSOnlyMiddleware, mws[HTTPSOnlyMiddleware]) } h := mws[ForwardedHeadersMiddleware].(map[string]any)["headers"].(map[string]any) - if h["stsSeconds"] != HSTSMaxAgeSeconds || HSTSMaxAgeSeconds != 86400 { - t.Errorf("HSTS must be one day (86400) until the operator rules otherwise, got %v", h["stsSeconds"]) + // R-1050 (decision 249): one year since the release after 2026-11-15; it was one day (86400) for the first month. + if h["stsSeconds"] != HSTSMaxAgeSeconds || HSTSMaxAgeSeconds != 31536000 { + t.Errorf("HSTS must be one year (31536000) — decision 249 — got %v (constant %d)", h["stsSeconds"], HSTSMaxAgeSeconds) } if h["stsIncludeSubdomains"] != false || h["stsPreload"] != false { t.Errorf("HSTS must not include subdomains nor preload, got %v / %v", h["stsIncludeSubdomains"], h["stsPreload"])