mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-07 12:21:31 +00:00
harden(auth/cookies): __Host- prefix on all three auth cookies (MED-14, BREAKING)
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
This commit is contained in:
@@ -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.**
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.<session_id>.<signing_key_id>.<base64url-no-pad
|
||||
|
||||
@@ -200,14 +200,20 @@ func TestCookieNamingConstants(t *testing.T) {
|
||||
// `certctl_csrf` by name and the back-channel handlers reference
|
||||
// `certctl_session` directly. A rename without coordinated GUI
|
||||
// updates would silently break login.
|
||||
if PostLoginCookieName != "certctl_session" {
|
||||
t.Errorf("PostLoginCookieName = %q; want certctl_session", PostLoginCookieName)
|
||||
// Audit 2026-05-10 MED-14 — `__Host-` prefix on all three auth
|
||||
// cookies. Subdomain-takeover defense: a cookie named `__Host-*`
|
||||
// can ONLY be set with Path=/ + Secure + no Domain attribute, and
|
||||
// the browser will reject any subdomain attempt to overwrite. The
|
||||
// rename is a BREAKING change on the wire — existing sessions
|
||||
// invalidate on the rolling deploy.
|
||||
if PostLoginCookieName != "__Host-certctl_session" {
|
||||
t.Errorf("PostLoginCookieName = %q; want __Host-certctl_session", PostLoginCookieName)
|
||||
}
|
||||
if PreLoginCookieName != "certctl_oidc_pending" {
|
||||
t.Errorf("PreLoginCookieName = %q; want certctl_oidc_pending", PreLoginCookieName)
|
||||
if PreLoginCookieName != "__Host-certctl_oidc_pending" {
|
||||
t.Errorf("PreLoginCookieName = %q; want __Host-certctl_oidc_pending", PreLoginCookieName)
|
||||
}
|
||||
if CSRFCookieName != "certctl_csrf" {
|
||||
t.Errorf("CSRFCookieName = %q; want certctl_csrf", CSRFCookieName)
|
||||
if CSRFCookieName != "__Host-certctl_csrf" {
|
||||
t.Errorf("CSRFCookieName = %q; want __Host-certctl_csrf", CSRFCookieName)
|
||||
}
|
||||
if CookieFormatVersion != "v1" {
|
||||
t.Errorf("CookieFormatVersion = %q; want v1", CookieFormatVersion)
|
||||
|
||||
+10
-6
@@ -55,17 +55,21 @@ function authHeaders(): Record<string, string> {
|
||||
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('='));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user