diff --git a/credential-change-requests/CCR-2026-0001-whynot-design-npm-publish.yaml b/credential-change-requests/CCR-2026-0001-whynot-design-npm-publish.yaml index c26ab94..0ad6f4c 100644 --- a/credential-change-requests/CCR-2026-0001-whynot-design-npm-publish.yaml +++ b/credential-change-requests/CCR-2026-0001-whynot-design-npm-publish.yaml @@ -58,6 +58,12 @@ openbao: allowed_redirect_uris: - https://bao.coulomb.social/ui/vault/auth/netkingdom/oidc/callback - http://localhost:8250/oidc/callback + - http://127.0.0.1:8250/oidc/callback + oidc_scopes: + - openid + - profile + - email + - groups user_claim: sub groups_claim: groups bound_claims: @@ -104,6 +110,14 @@ verification: - OIDC role read showed the whynot-design bound claim, read policy, and callback URIs. - Metadata read showed catalog-id whynot-design-npm-publish. - Secret field presence check found NPM_AUTH_TOKEN without printing or recording the value. + - at: '2026-06-28T11:20:06+00:00' + actor: codex + kind: non_secret_oidc_role_correction + result: applied + details: + - Positive login reported missing groups claim because the role did not request the groups scope. + - Updated auth/netkingdom/role/whynot-design-workload-kv-read with oidc_scopes openid/profile/email/groups. + - Added the 127.0.0.1 local CLI callback URI alongside localhost and browser callbacks. lifecycle: deactivate: Disable ops-warden catalog entry and remove or detach auth role policy. rotate: Replace NPM_AUTH_TOKEN value directly in OpenBao and record non-secret rotation diff --git a/docs/whynot-design-npm-publish-handoff.md b/docs/whynot-design-npm-publish-handoff.md index 541a02d..59c24c4 100644 --- a/docs/whynot-design-npm-publish-handoff.md +++ b/docs/whynot-design-npm-publish-handoff.md @@ -98,7 +98,14 @@ Role payload: "role_type": "oidc", "allowed_redirect_uris": [ "https://bao.coulomb.social/ui/vault/auth/netkingdom/oidc/callback", - "http://localhost:8250/oidc/callback" + "http://localhost:8250/oidc/callback", + "http://127.0.0.1:8250/oidc/callback" + ], + "oidc_scopes": [ + "openid", + "profile", + "email", + "groups" ], "user_claim": "sub", "groups_claim": "groups", @@ -119,7 +126,14 @@ cat >"$role_payload_file" <<'JSON' { "allowed_redirect_uris": [ "https://bao.coulomb.social/ui/vault/auth/netkingdom/oidc/callback", - "http://localhost:8250/oidc/callback" + "http://localhost:8250/oidc/callback", + "http://127.0.0.1:8250/oidc/callback" + ], + "oidc_scopes": [ + "openid", + "profile", + "email", + "groups" ], "bound_claims": { "groups": [ @@ -192,7 +206,7 @@ Only after these are true: - secret metadata confirmed; - policy exists and is scoped to the corrected `coulomb/whynot-design` path; - OIDC role exists and binds only `groups=["whynot-design"]` with approved - browser and local CLI callback URIs; + browser/local CLI callback URIs and `groups` OIDC scope; - positive verification passed; - negative verification passed; diff --git a/docs/workload-kv-access-lanes.md b/docs/workload-kv-access-lanes.md index 8745e2f..e74575a 100644 --- a/docs/workload-kv-access-lanes.md +++ b/docs/workload-kv-access-lanes.md @@ -35,7 +35,7 @@ Ops-warden batch follow-up: | Policy file | `openbao/policies/workload-kv-read-whynot-design-npm-publish.hcl` | | OIDC auth mount | `netkingdom` | | OIDC role | `whynot-design-workload-kv-read` | -| OIDC callback URIs | `https://bao.coulomb.social/ui/vault/auth/netkingdom/oidc/callback`, `http://localhost:8250/oidc/callback` | +| OIDC callback URIs | `https://bao.coulomb.social/ui/vault/auth/netkingdom/oidc/callback`, `http://localhost:8250/oidc/callback`, `http://127.0.0.1:8250/oidc/callback` | | Kubernetes auth role | `whynot-design-workload-kv-read` if an in-cluster service account consumes this lane | | flex-auth ref | `secret.read:whynot-design` if tenant policy requires pre-approval | @@ -116,6 +116,17 @@ OpenBao: ```text https://bao.coulomb.social/ui/vault/auth/netkingdom/oidc/callback http://localhost:8250/oidc/callback +http://127.0.0.1:8250/oidc/callback +``` + +The role must request these OIDC scopes so KeyCape emits the group claim OpenBao +checks: + +```text +openid +profile +email +groups ``` The whynot-design pilot claim is confirmed as `groups=whynot-design`. Before @@ -180,7 +191,9 @@ warden access "npm token" \ ``` Use `--no-policy` only while the local ops-warden config reports -`policy.enabled=false`; remove it once the flex-auth gate is enforced. +`policy.enabled=false`; remove it once the flex-auth gate is enforced. If login +fails with `groups claim not found`, the OpenBao role is missing the `groups` +OIDC scope and must be corrected before retrying. Negative verification: diff --git a/schemas/credential-change-request.schema.yaml b/schemas/credential-change-request.schema.yaml index 4c13f8f..7e6551e 100644 --- a/schemas/credential-change-request.schema.yaml +++ b/schemas/credential-change-request.schema.yaml @@ -86,6 +86,7 @@ workload_kv_read: required: - allowed_redirect_uris allowed_redirect_uris: non-empty list of OpenBao callback URIs accepted by the role + groups_claim: requires openbao.auth.oidc_scopes to include groups access_frontdoor_readiness: allowed: diff --git a/scripts/credential-change.py b/scripts/credential-change.py index 6552c25..a651609 100755 --- a/scripts/credential-change.py +++ b/scripts/credential-change.py @@ -183,6 +183,19 @@ def validate_workload_kv_read(ccr: dict[str, Any], errors: list[str], warnings: errors.append( f"openbao.auth.allowed_redirect_uris[{index}] must be a non-empty string" ) + if auth.get("groups_claim"): + oidc_scopes = require_list( + auth.get("oidc_scopes"), "openbao.auth.oidc_scopes", errors + ) + if "groups" not in oidc_scopes: + errors.append( + "openbao.auth.oidc_scopes must include 'groups' when groups_claim is set" + ) + for index, scope in enumerate(oidc_scopes): + if not isinstance(scope, str) or not scope.strip(): + errors.append( + f"openbao.auth.oidc_scopes[{index}] must be a non-empty string" + ) policies = [str(policy) for policy in require_list(auth.get("policies"), "openbao.auth.policies", errors)] if policies != [policy_name]: errors.append("openbao.auth.policies must contain exactly openbao.policy_name") @@ -362,6 +375,8 @@ def auth_payload(ccr: dict[str, Any]) -> dict[str, Any]: payload["groups_claim"] = auth["groups_claim"] if auth.get("allowed_redirect_uris"): payload["allowed_redirect_uris"] = auth["allowed_redirect_uris"] + if auth.get("oidc_scopes"): + payload["oidc_scopes"] = auth["oidc_scopes"] return payload diff --git a/tests/test_credential_change.py b/tests/test_credential_change.py index 7e3d4f5..2544654 100644 --- a/tests/test_credential_change.py +++ b/tests/test_credential_change.py @@ -142,8 +142,13 @@ class CredentialChangeTests(unittest.TestCase): [ "https://bao.coulomb.social/ui/vault/auth/netkingdom/oidc/callback", "http://localhost:8250/oidc/callback", + "http://127.0.0.1:8250/oidc/callback", ], ) + self.assertEqual( + payload["oidc_scopes"], + ["openid", "profile", "email", "groups"], + ) def test_apply_plan_refuses_unapproved_ccr(self) -> None: with self.assertRaises(SystemExit): @@ -170,6 +175,8 @@ class CredentialChangeTests(unittest.TestCase): self.assertIn('role_payload_file="$(mktemp)"', rendered) self.assertIn('"bound_claims": {', rendered) self.assertIn('"allowed_redirect_uris": [', rendered) + self.assertIn('"oidc_scopes": [', rendered) + self.assertIn('"groups"', rendered) self.assertIn( '"https://bao.coulomb.social/ui/vault/auth/netkingdom/oidc/callback"', rendered, diff --git a/workplans/RAILIANCE-WP-0006-workload-kv-access-lanes.md b/workplans/RAILIANCE-WP-0006-workload-kv-access-lanes.md index 4650853..ec8ab69 100644 --- a/workplans/RAILIANCE-WP-0006-workload-kv-access-lanes.md +++ b/workplans/RAILIANCE-WP-0006-workload-kv-access-lanes.md @@ -191,6 +191,11 @@ subject; do not create an unbounded OIDC role. `workload-kv-read-whynot-design-npm-publish` policy, `ttl=15m`, and the approved browser/local CLI callback URIs. +**2026-06-28:** Positive verification found the OIDC role was missing +`oidc_scopes`, causing OpenBao login to fail with `groups claim not found`. +Updated the live role and source CCR to request `openid`, `profile`, `email`, +and `groups`, matching the platform-admin OIDC scope shape. + ## T04 - Provision the KV path without exposing the token ```task