Live group resolution (#96), OpenBao custody (#29), domain separation (#31) #99

Merged
Cordy merged 13 commits from feat/custody-live-groups into main 2026-08-04 10:42:49 +00:00
Owner

Three items from the encryption review + spaces follow-up, one branch.

#96 — Live group resolution. New auth.WithLiveGroups decorator: in OIDC mode, User.Groups is re-resolved per request through the Keycloak Admin API (spaces.Manager.UserGroups, same service-account client) on a 60s cache instead of riding the 12h session snapshot. Grants apply within a minute; revocations too — the removed member's security window shrinks from session-lifetime to cache TTL. Sits UNDER WithAdminFlag, so admin-group changes also apply live. IdP outage → session snapshot stands (availability over freshness) with a 30s negative cache. App-password requests get live groups too (the decorator wraps the whole Multi chain). LDAP is untouched: it resolves memberOf at bind time already.

#29 — OpenBao key-custody backend (keyCustody: "openbao"). Per-user AND per-group (#18) age identities in a KV v2 mount ({mount}/data/{prefix}/users/<u>, .../groups/<g>), giving uniform custody across Keycloak and Active Directory — AD has no writable profile attributes, which was the gap. Deliberately KV, not transit: the at-rest format stays plain age files (age -d recovery is a sovereignty feature). Lazy provisioning uses check-and-set (cas=0) so racing replicas can't overwrite a live custody key — the loser adopts the winner's identity. Config: openbaoBaseUrl, openbaoToken (or CAIRN_ENC_OPENBAO_TOKEN), openbaoMount, openbaoPathPrefix.

#31 — Per-instance domain separation (verdict honored: deployment custody NOT demoted). New storage.encryption.instanceSeed: deployment custody derives a per-instance identity via HKDF-SHA256(master, seed) (stdlib crypto/hkdf; minimal bech32 encoder validated against age's own parser as oracle). New content encrypts to the derived identity + recovery — two instances sharing one master but different seeds can no longer read each other's data, and the bare master no longer decrypts separated content. The master stays on the decrypt side so pre-separation data keeps working; recovery unaffected. Seed lives in the config secret (not /data) so it can't silently vanish with ephemeral state.

Tests: live-groups suite (override, TTL revocation, IdP-outage fallback + negative cache, admin recompute through WithAdminFlag), fake-OpenBao KV v2 suite (lazy provision, group scope isolation, cas race, recovery), derivation suite (bech32 oracle ×20, determinism, full separation matrix incl. legacy decrypt), UserGroups against the fake Keycloak. Full repo suite green.

Three items from the encryption review + spaces follow-up, one branch. **#96 — Live group resolution.** New `auth.WithLiveGroups` decorator: in OIDC mode, `User.Groups` is re-resolved per request through the Keycloak Admin API (`spaces.Manager.UserGroups`, same service-account client) on a 60s cache instead of riding the 12h session snapshot. Grants apply within a minute; **revocations too** — the removed member's security window shrinks from session-lifetime to cache TTL. Sits UNDER `WithAdminFlag`, so admin-group changes also apply live. IdP outage → session snapshot stands (availability over freshness) with a 30s negative cache. App-password requests get live groups too (the decorator wraps the whole Multi chain). LDAP is untouched: it resolves memberOf at bind time already. **#29 — OpenBao key-custody backend** (`keyCustody: "openbao"`). Per-user AND per-group (#18) age identities in a KV v2 mount (`{mount}/data/{prefix}/users/<u>`, `.../groups/<g>`), giving uniform custody across Keycloak and Active Directory — AD has no writable profile attributes, which was the gap. Deliberately KV, not transit: the at-rest format stays plain age files (`age -d` recovery is a sovereignty feature). Lazy provisioning uses check-and-set (`cas=0`) so racing replicas can't overwrite a live custody key — the loser adopts the winner's identity. Config: `openbaoBaseUrl`, `openbaoToken` (or `CAIRN_ENC_OPENBAO_TOKEN`), `openbaoMount`, `openbaoPathPrefix`. **#31 — Per-instance domain separation** (verdict honored: deployment custody NOT demoted). New `storage.encryption.instanceSeed`: deployment custody derives a per-instance identity via HKDF-SHA256(master, seed) (stdlib `crypto/hkdf`; minimal bech32 encoder validated against age's own parser as oracle). New content encrypts to the derived identity + recovery — two instances sharing one master but different seeds can no longer read each other's data, and the bare master no longer decrypts separated content. The master stays on the decrypt side so pre-separation data keeps working; recovery unaffected. Seed lives in the config secret (not `/data`) so it can't silently vanish with ephemeral state. **Tests:** live-groups suite (override, TTL revocation, IdP-outage fallback + negative cache, admin recompute through WithAdminFlag), fake-OpenBao KV v2 suite (lazy provision, group scope isolation, cas race, recovery), derivation suite (bech32 oracle ×20, determinism, full separation matrix incl. legacy decrypt), `UserGroups` against the fake Keycloak. Full repo suite green.
Cordy merged commit c7efde80e8 into main 2026-08-04 10:42:49 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: Cordy/Cairn#99
No description provided.