From 020bba35f06ed73e61b6c285289cb133b7d140fc Mon Sep 17 00:00:00 2001 From: shankar0123 Date: Sun, 10 May 2026 22:52:53 +0000 Subject: [PATCH] harden(auth/cookies): __Host- prefix on all three auth cookies (MED-14, BREAKING) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Audit 2026-05-10 — close MED-14 from the HANDOFF.md backend batch (item 5). The session, CSRF, and OIDC pre-login cookies all carry the __Host- prefix; browsers now reject any subdomain attempt to overwrite them. Cookie name changes (BREAKING — existing sessions invalidate): - certctl_session → __Host-certctl_session - certctl_csrf → __Host-certctl_csrf - certctl_oidc_pending → __Host-certctl_oidc_pending The __Host- prefix requires Path=/ + Secure + no Domain attribute. Post-login session + CSRF cookies already met all three. The pre-login cookie's Path widened from '/auth/oidc/' to '/' to satisfy the prefix; the cookie lives 10 minutes and is only consumed by the callback handler, so the wider path scope is harmless. Files touched: - internal/auth/session/domain/types.go — constant rename + comment - internal/auth/session/domain/types_test.go — assertion update - internal/api/handler/auth_session_oidc.go — pre-login set + clear paths widened from /auth/oidc/ to / - web/src/api/client.ts — readCSRFCookie now compares against '__Host-certctl_csrf' - CHANGELOG.md — Unreleased > Security (BREAKING) entry - docs/migration/oidc-enable.md — operator-facing detail of the one-time re-authentication window + GUI customization guidance Operator impact: ONE re-login prompt per active session at the deploy that lands this change. Subsequent logins issue the __Host-prefixed cookie automatically. Existing bookmarked deep links work without modification (cookies are path-scoped, not URL-scoped). Refs: cowork/auth-bundles-fixes-2026-05-10/HANDOFF.md item 5 cowork/auth-bundles-audit-2026-05-10.md MED-14 --- CHANGELOG.md | 22 +++++++++++++++ docs/migration/oidc-enable.md | 16 +++++++++++ internal/api/handler/auth_session_oidc.go | 18 ++++++++---- internal/auth/session/domain/types.go | 32 +++++++++++++++++++--- internal/auth/session/domain/types_test.go | 18 ++++++++---- web/src/api/client.ts | 16 +++++++---- 6 files changed, 100 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2beadf3..cdea33b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,27 @@ # Changelog +## Unreleased + +### Security (BREAKING) + +- **`__Host-` cookie prefix on all three auth cookies (Audit 2026-05-10 MED-14).** + The session cookie, CSRF cookie, and OIDC pre-login cookie are renamed from + `certctl_session` / `certctl_csrf` / `certctl_oidc_pending` to + `__Host-certctl_session` / `__Host-certctl_csrf` / `__Host-certctl_oidc_pending` + to gain browser-enforced subdomain-takeover protection (a `__Host-*` cookie can + only be set with `Path=/` + `Secure` + no `Domain` attribute, and the browser + rejects subdomain attempts to overwrite it). **Active sessions invalidate on + the rolling deploy that lands this change** — operators must re-authenticate + once after upgrading. The GUI's CSRF cookie reader was updated in lockstep. + See `docs/migration/oidc-enable.md` for operator-facing detail. + +### Security + +- **Pre-login cookie Path widened from `/auth/oidc/` to `/` (Audit MED-14 + follow-on).** Required to satisfy the `__Host-` prefix's `Path=/` rule. The + cookie lifetime is unchanged (10 minutes) and only the callback handler + consumes it; the wider path scope is harmless. + ## v2.1.0 - Auth Bundles 1 + 2: RBAC primitive + OIDC SSO + sessions ⚠️ > **SECURITY: AUDIT YOUR API KEYS.** diff --git a/docs/migration/oidc-enable.md b/docs/migration/oidc-enable.md index 18ba846..2fd35b4 100644 --- a/docs/migration/oidc-enable.md +++ b/docs/migration/oidc-enable.md @@ -234,6 +234,22 @@ All ten of these tables are tenant-scoped (`tenant_id` column); single-tenant de - Review the [`auth-threat-model.md`](../operator/auth-threat-model.md) Bundle 2 sections to understand the failure modes the OIDC + sessions surface defends against. - Schedule a rotation reminder for the OIDC `client_secret` (typically 6-12 months; the IdP doesn't auto-rotate it). Edit the provider via the GUI when the time comes; leaving `client_secret` blank in the edit form preserves the existing ciphertext, providing a value rotates. +## `__Host-` cookie rename (Audit 2026-05-10 MED-14, BREAKING) + +Post-Bundle-2 deploys carrying the 2026-05-10 audit-fix wave include a wire-format change to the three auth cookies: they now carry the `__Host-` prefix. The cookie names are: + +- `__Host-certctl_session` (was `certctl_session`) +- `__Host-certctl_csrf` (was `certctl_csrf`) +- `__Host-certctl_oidc_pending` (was `certctl_oidc_pending`) + +The rename gains browser-enforced subdomain-takeover defense: a `__Host-*` cookie can only be set with `Path=/` + `Secure` + no `Domain` attribute, and the browser rejects any subdomain attempt to overwrite it. The protection is free (the existing cookies already met the prerequisites) but the wire-format change means: + +- **Every active session is invalidated by the deploy that lands this change.** Operators see one re-authentication prompt; subsequent logins issue the new `__Host-*`-prefixed cookie. +- **The pre-login cookie's Path widens from `/auth/oidc/` to `/`** — required by the `__Host-` prefix. The cookie lifetime is unchanged (10 minutes) and is only ever consumed by the callback handler; the wider path scope is harmless. +- **No operator action required beyond accepting the one-time re-login window.** The GUI's CSRF cookie reader was updated in lockstep; existing bookmarked deep links work without modification. + +If you have GUI customizations that read `document.cookie` directly, update them to look for `__Host-certctl_csrf` (the lookup in `web/src/api/client.ts` is the in-tree reference). + ## Cross-references - [`docs/operator/oidc-runbooks/index.md`](../operator/oidc-runbooks/index.md) — per-IdP setup guides. diff --git a/internal/api/handler/auth_session_oidc.go b/internal/api/handler/auth_session_oidc.go index 7fe8a6f..5227d93 100644 --- a/internal/api/handler/auth_session_oidc.go +++ b/internal/api/handler/auth_session_oidc.go @@ -243,9 +243,12 @@ func (h *AuthSessionOIDCHandler) LoginInitiate(w http.ResponseWriter, r *http.Re return } http.SetCookie(w, &http.Cookie{ - Name: sessiondomain.PreLoginCookieName, - Value: cookieValue, - Path: "/auth/oidc/", + Name: sessiondomain.PreLoginCookieName, + Value: cookieValue, + // Audit 2026-05-10 MED-14 — `__Host-` prefix requires Path=/. + // The cookie lives 10 minutes and is only ever consumed by the + // callback handler; the wider path scope is harmless. + Path: "/", MaxAge: int((10 * time.Minute).Seconds()), Secure: h.cookieAttrs.Secure, HttpOnly: true, @@ -1104,9 +1107,12 @@ func (h *AuthSessionOIDCHandler) recordAudit(ctx context.Context, action, actor func (h *AuthSessionOIDCHandler) clearPreLoginCookie(w http.ResponseWriter) { http.SetCookie(w, &http.Cookie{ - Name: sessiondomain.PreLoginCookieName, - Value: "", - Path: "/auth/oidc/", + Name: sessiondomain.PreLoginCookieName, + Value: "", + // Audit 2026-05-10 MED-14 — Path=/ matches the write site + // post-`__Host-` rename. The browser only clears cookies that + // match the original Set-Cookie's Name+Path+Domain triple. + Path: "/", MaxAge: -1, Secure: h.cookieAttrs.Secure, HttpOnly: true, diff --git a/internal/auth/session/domain/types.go b/internal/auth/session/domain/types.go index 5fb13bc..9d16477 100644 --- a/internal/auth/session/domain/types.go +++ b/internal/auth/session/domain/types.go @@ -68,19 +68,43 @@ type SessionSigningKey struct { const ( // PostLoginCookieName is the post-authentication session cookie. // Set HttpOnly + Secure + SameSite=Lax (or Strict via env var). - PostLoginCookieName = "certctl_session" + // + // Audit 2026-05-10 MED-14 closure — `__Host-` prefix prevents + // subdomain takeover (sibling subdomain can't set a cookie that + // rides through with our origin's requests). The prefix requires: + // - Path=/ (already) + // - Secure (already; HTTPS-only control plane) + // - No Domain attribute (already) + // Existing sessions invalidate on the rolling deploy that lands + // this rename — operators must re-authenticate once. Documented in + // docs/migration/oidc-enable.md + CHANGELOG.md under BREAKING. + PostLoginCookieName = "__Host-certctl_session" // PreLoginCookieName is the pre-authentication session cookie that // holds the OIDC state + nonce + PKCE verifier across the IdP // redirect. 10-minute lifetime, separate from the post-login - // cookie, Path=/auth/oidc/. - PreLoginCookieName = "certctl_oidc_pending" + // cookie. + // + // Audit 2026-05-10 MED-14 — pre-login cookies historically used + // Path=/auth/oidc/ which is INCOMPATIBLE with the `__Host-` prefix + // (which requires Path=/). Path is widened to / here; the cookie + // only lives for 10 minutes (the pre-login TTL), and is only + // consumed by the callback handler, so the wider path scope is + // harmless. The `__Host-` protection (subdomain-takeover defense) + // is the more valuable property. + PreLoginCookieName = "__Host-certctl_oidc_pending" // CSRFCookieName is the JS-readable cookie holding the CSRF token // plaintext. Mirrors the SHA-256 hash on the session row. The GUI // reads this and echoes the value into the X-CSRF-Token header on // every state-changing request. - CSRFCookieName = "certctl_csrf" + // + // Audit 2026-05-10 MED-14 — `__Host-` prefix applied; the CSRF + // cookie satisfies the requirements identically to the session + // cookie (Path=/, Secure, no Domain). Note this is HttpOnly=false + // (the GUI must read it) — but `__Host-` still applies regardless + // of HttpOnly; the prefix is about scope, not visibility. + CSRFCookieName = "__Host-certctl_csrf" // CookieFormatVersion is the prefix on every session cookie value. // Format: `v1... { return headers; } -// Bundle 2 Phase 8 — read the certctl_csrf cookie value (set by the -// OIDC-callback / break-glass-login flows; JS-readable by design so -// the GUI can echo it into the X-CSRF-Token header on every state- -// changing request). Returns empty string when the cookie isn't set -// (Bearer-mode deployments don't need CSRF; the server's middleware +// Bundle 2 Phase 8 — read the __Host-certctl_csrf cookie value (set +// by the OIDC-callback / break-glass-login flows; JS-readable by +// design so the GUI can echo it into the X-CSRF-Token header on every +// state-changing request). Returns empty string when the cookie isn't +// set (Bearer-mode deployments don't need CSRF; the server's middleware // short-circuits CSRF for Bearer-authenticated requests). +// +// Audit 2026-05-10 MED-14 — cookie name carries the `__Host-` prefix +// (subdomain-takeover defense). The browser includes the prefix in +// document.cookie verbatim; the comparison below matches that. function readCSRFCookie(): string { if (typeof document === 'undefined' || !document.cookie) return ''; for (const part of document.cookie.split(';')) { const [k, ...rest] = part.trim().split('='); - if (k === 'certctl_csrf') { + if (k === '__Host-certctl_csrf') { return decodeURIComponent(rest.join('=')); } }