NK-WP-0056-T20 done: oidc-service-client 0.2 with the owner reviews; memo r2
All checks were successful
All checks were successful
ESO role audience + explicit max TTL and ESO policy self-lifecycle (ops-mason R3/R4, railiance-platform R3); consumer role renamed to differ from its policy (railiance-platform R4); default SA and external-secrets/openbao namespaces refused (ops-mason R6); consumer-cannot-read-delivered-secret verification (ops-warden R7, ops-mason R7); excess scope invalid_profile_usage (key-cape R3); attended rotation, broker admission pending, one rollout per case; catalog fields (ops-warden R6). First case consumer_owner_repo rapp-bounded-executor. CCR-2026-0060/0061 names checked by test. Memo r2 validated against informed-decision intake and State Hub dispatcher. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Assistant: claude-code Assistant-Model: opus Assistant-Process: 5502@bnt-lap001 Assistant-Session: 68b3b02f-5d0f-4e71-8421-796f62e50196
This commit is contained in:
parent
1e17b39212
commit
45feaa13cc
10 changed files with 215 additions and 91 deletions
|
|
@ -362,7 +362,7 @@
|
|||
| task | NK-WP-0056-T17 | done | — | workplans/NK-WP-0056-bounded-executor-path.md |
|
||||
| task | NK-WP-0056-T18 | done | — | workplans/NK-WP-0056-bounded-executor-path.md |
|
||||
| task | NK-WP-0056-T19 | todo | — | workplans/NK-WP-0056-bounded-executor-path.md |
|
||||
| task | NK-WP-0056-T20 | todo | — | workplans/NK-WP-0056-bounded-executor-path.md |
|
||||
| task | NK-WP-0056-T20 | done | — | workplans/NK-WP-0056-bounded-executor-path.md |
|
||||
| task | NK-WP-0056-T21 | todo | — | workplans/NK-WP-0056-bounded-executor-path.md |
|
||||
| intake | NK-IN-0001 | closed | blue | docs/intakes/activity-core-ops-sso-operators.md |
|
||||
| intake | NK-IN-0002 | closed | blue | docs/intakes/activity-core-ops-sso-operators.md |
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ C, OPS, X = CLI.C, CLI.OPS, CLI.X
|
|||
ADMISSION = X.REPO / "examples/bounded-execution-admission/bind-rotation-svc-authelia.yaml"
|
||||
SECRET = "synthetic-client-secret/with:specials"
|
||||
CLIENT_KV = "platform/workloads/keycape/nk-bind-executor-approval"
|
||||
APPROVAL_ROLE = "workload-kv-read-keycape-nk-bind-executor-approval-bounded-executor"
|
||||
APPROVAL_ROLE = "keycape-nk-bind-executor-approval-bounded-executor-workload-kv-read"
|
||||
|
||||
|
||||
class World:
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
# oidc-service-client 0.1
|
||||
# oidc-service-client 0.2
|
||||
|
||||
An internal workload obtains short-lived KeyCape access tokens for one resource server with the `client_credentials`
|
||||
grant. The type covers:
|
||||
|
|
@ -7,9 +7,9 @@ grant. The type covers:
|
|||
- the client secret, minted straight into OpenBao custody and readable only by KeyCape (through ESO) and by the
|
||||
consuming workload (through its own Kubernetes-auth login).
|
||||
|
||||
Canon: `canon/standards/security-case-types_v0.1.md`. Status: **proposed** (2026-10-31): complete, tests green,
|
||||
Decision Memo prepared (`decision/`, hub decision `f8044f81`). Next: `acknowledged` by the founder through
|
||||
informed-decision. Its first case and
|
||||
Canon: `canon/standards/security-case-types_v0.1.md`. Status: **proposed** (0.2, 2026-11-01): complete, tests
|
||||
green, owner reviews folded in, Decision Memo r2 prepared (`decision/`, hub decision `f8044f81`). Next:
|
||||
`acknowledged` by the founder through informed-decision. Its first case and
|
||||
reference instance is the bounded executor's approval client (NK-WP-0056-T14, `examples/nk-bind-executor-approval.yaml`).
|
||||
|
||||
## Purpose
|
||||
|
|
@ -29,7 +29,7 @@ equals the token's `sub`, so the bounded executor must hold its own client to fi
|
|||
|
||||
### Reference instances and fit
|
||||
|
||||
| Client | Fits 0.1 | Note |
|
||||
| Client | Fits 0.2 | Note |
|
||||
| --- | --- | --- |
|
||||
| nk-bind-executor-approval (new) | yes | first case |
|
||||
| secrets-engine-approval | yes | adoption would not change its live registration (canon section 9) |
|
||||
|
|
@ -58,11 +58,16 @@ equals the token's `sub`, so the bounded executor must hold its own client to fi
|
|||
- Only two readers exist: KeyCape through the ESO role, and the consumer through its own role. The consumer role is
|
||||
bound to one ServiceAccount in one namespace with audience `openbao`, and its policy reads one path. No metadata,
|
||||
list, parent or sibling access.
|
||||
- There is no Secret copy in the consumer's namespace. The consumer reads natively per call, so a pod in that
|
||||
namespace without the ServiceAccount has nothing to mount.
|
||||
- No Secret copy is made *for the consumer*: it reads natively per call. KeyCape's own ESO-delivered copy,
|
||||
`keycape-<client_id>-client`, lives in `sso`. When the consumer also runs in `sso`, as the first case does, that copy
|
||||
sits beside it. The verification `consumer-cannot-read-delivered-secret` proves with an RBAC can-i answer that the
|
||||
consumer's ServiceAccount cannot get, list or watch Secrets there (ops-warden R7, ops-mason R7, railiance-platform
|
||||
R7).
|
||||
- Tokens last at most 15m and there is no refresh token.
|
||||
- Scopes are a closed list per case. KeyCape refuses an excess scope (`invalid_scope`), and the type verifies that
|
||||
refusal.
|
||||
- Scopes are a closed list per case. KeyCape refuses an excess scope with HTTP 400 `invalid_profile_usage` (feature
|
||||
`scope`) and issues no token (key-cape R3). The type verifies that refusal.
|
||||
- Service tokens carry `principal_type: service` and a fixed assurance of `aal1` (`client_secret`). A service token can
|
||||
never satisfy an aal2 rule. Human approvals stay with humans (key-cape R2).
|
||||
- The subject and tenant are fixed by the registration. The workload cannot choose them.
|
||||
- What a compromised consumer can do is bounded by its scopes at one audience. For approval-engine that means:
|
||||
- it can create approvals naming itself as actor, which still need a human entry under `human_control`;
|
||||
|
|
@ -85,10 +90,13 @@ equals the token's `sub`, so the bounded executor must hold its own client to fi
|
|||
## Attended steps
|
||||
|
||||
`bind-env` and `register-client` change the live identity provider: the KeyCape Deployment and its client registry,
|
||||
which is a Kubernetes Secret. Agents do not read or write Secrets. Both steps run in one attended operator session,
|
||||
`bind-env` first, because KeyCape resolves a confidential client's secret at start and fails closed without it. The
|
||||
restart drops in-flight logins for every client, so runs go in a quiet window. `mint-secret` and `deliver-secret`
|
||||
are unattended.
|
||||
which is a Kubernetes Secret. Agents do not read or write Secrets. Both steps run in one attended operator session
|
||||
and **one rollout**: KeyCape runs one replica with strategy Recreate, so the env patch is itself a restart (key-cape
|
||||
R4). `bind-env` goes first, because KeyCape resolves a confidential client's secret at start and fails closed without
|
||||
it. `deliver-secret` must report Ready immediately before, or the new pod cannot start and every client loses sign-in.
|
||||
The restart drops in-flight logins, so runs go in a quiet window. `mint-secret` and `deliver-secret` are unattended,
|
||||
but `mint-secret` needs a human aal2 login to the issuer role; an agent, including the consumer, cannot mint its own
|
||||
secret (ops-warden R2).
|
||||
|
||||
## Lanes, value handling and gates
|
||||
|
||||
|
|
@ -97,7 +105,7 @@ are unattended.
|
|||
| mint-secret | keycape-client-secret-generate (railiance-platform broker) | generated in custody, never returned |
|
||||
| deliver-secret | ESO role `external-secrets-keycape-<client_id>` | none |
|
||||
| bind-env, register-client | key-cape-oidc-login (attended) | none |
|
||||
| hand-over | the consumer's own deployment path | none; the consumer reads through `workload-kv-read-keycape-<client_id>-<consuming_service>` |
|
||||
| hand-over | the consumer's own deployment path | none; the consumer reads through role `keycape-<client_id>-<consuming_service>-workload-kv-read` (policy `workload-kv-read-keycape-<client_id>-<consuming_service>`) |
|
||||
|
||||
## Known limits
|
||||
|
||||
|
|
@ -106,24 +114,48 @@ are unattended.
|
|||
audience owner reviews the case. A stricter check would need the audience to publish its scopes.
|
||||
- **KeyCape's client registry is still a Secret.** Registration stays attended until KeyCape loads clients from a
|
||||
non-secret registry.
|
||||
- **Rotation is mint-secret, then the consumer's next exchange.** KeyCape reads the secret at start, so a rotation
|
||||
also restarts KeyCape. That is not automated.
|
||||
- **Rotation is attended and has no lane yet.** `mint-secret` only creates and refuses an existing path. A rotation
|
||||
is an attended platform-admin write of the next version with CAS, then the ESO delivery Ready, then one KeyCape
|
||||
restart. Until KeyCape restarts, the consumer, which reads the new value at once, gets 401 (railiance-platform R5,
|
||||
ops-warden R5, key-cape R5).
|
||||
- **The broker must admit the type.** railiance-platform's `keycape_client_secret_generate.py` accepts only
|
||||
`oidc-browser-relying-party@1.1` and a `client_type` parameter. Admitting this type is a reviewed source change that
|
||||
lands after acknowledgement (railiance-platform R1, ops-warden R1). Until then `mint-secret` refuses these cases.
|
||||
- **Each case still needs its CCRs.** The type replaces the shape review, not per-case approval or binding
|
||||
confirmation (railiance-platform R6). For the first case: CCR-2026-0060 (KeyCape's ESO reader) and CCR-2026-0061
|
||||
(the consumer reader).
|
||||
- **Claims are reported by the consumer.** `service-token-claims` is the tested party reporting on itself. KeyCape's
|
||||
non-secret client view at register-client (`keycape-client.py inspect`) is the independent half (ops-warden R8).
|
||||
- **Adopting `approval-engine-operator`** needs a scope review first; see the reference table.
|
||||
|
||||
## Verification and rollback
|
||||
|
||||
Verification never reads the secret or a token:
|
||||
|
||||
- the consumer reports the claims of its own exchanges (sub, aud, tenant, scope and lifetime);
|
||||
- the consumer reports the claims of its own exchanges (sub, aud, tenant, principal_type, scope and lifetime);
|
||||
- an excess scope is refused;
|
||||
- a default identity has no access to custody;
|
||||
- the consumer role admits only its ServiceAccount and audience;
|
||||
- the consumer policy grants read on the one path;
|
||||
- the consumer policy grants read on the one path, plus token lookup-self and revoke-self;
|
||||
- the consumer cannot read Secrets in its namespace (RBAC can-i);
|
||||
- ESO delivery to KeyCape reports Ready.
|
||||
|
||||
Rollback reverses every follow-through step and every materialized item in dependency order: consumer configuration,
|
||||
registration, env binding, delivery, custody, roles before their policies, then the catalog entry.
|
||||
registration, env binding, delivery, custody, roles before their policies, then the catalog entry. Rollback is
|
||||
complete once the consumer's tokens have expired (at most 15m) or been revoked (ops-mason R11).
|
||||
|
||||
## History
|
||||
|
||||
- 0.1 (2026-10-31): first version, for NK-WP-0056-T14; proposed with memo `nk-wp-0056-t14-oidc-service-client-r1`.
|
||||
- 0.2 (2026-11-01, NK-WP-0056-T20): folds in the owner reviews (ops-mason `3c69a25`, ops-warden `95d31c3`, key-cape
|
||||
`655d8d3`, railiance-platform `6858bef`):
|
||||
- the ESO role gains audience and explicit max TTL; the ESO policy gains token self-lifecycle;
|
||||
- the consumer role is renamed so that it differs from its policy;
|
||||
- the `default` ServiceAccount and the `external-secrets` and `openbao` namespaces are refused;
|
||||
- new verification `consumer-cannot-read-delivered-secret`;
|
||||
- the excess-scope code corrected; rotation corrected; one rollout per case;
|
||||
- the catalog entry gains lane, auth_method, `exec_capable: false` and need_keywords.
|
||||
|
||||
Decided here: `consuming_service` stays `bounded-executor`, matching CCR-2026-0061 (ops-mason R9). The first case's
|
||||
`consumer_owner_repo` is `rapp-bounded-executor`, where the consumer's configuration lives (ADR-0015; ops-warden
|
||||
R10). Memo r2 replaces r1.
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Build the informed-decision memo intake envelope that acknowledges oidc-service-client 0.1.
|
||||
"""Build the informed-decision memo intake envelope that acknowledges oidc-service-client 0.2.
|
||||
|
||||
Writes intake.json beside this file and validates it with informed-decision's own intake validator and State Hub's
|
||||
dispatcher contract, when those checkouts are present beside net-kingdom. The envelope is queued by an operator
|
||||
|
|
@ -14,9 +14,9 @@ from pathlib import Path
|
|||
HERE = Path(__file__).resolve().parent
|
||||
HUB_DECISION = "f8044f81-f94c-483a-81f9-4f2b02a60d5f"
|
||||
# Each revision needs a new id: the dispatcher refuses new content under a delivered idempotency key.
|
||||
MEMO_ID = "nk-wp-0056-t14-oidc-service-client-r1"
|
||||
MEMO_ID = "nk-wp-0056-t14-oidc-service-client-r2" # replaces r1 (0.1, before the owner reviews)
|
||||
ITEM = f"{MEMO_ID}-packet"
|
||||
TARGET = "oidc-service-client@0.1"
|
||||
TARGET = "oidc-service-client@0.2"
|
||||
|
||||
packet = (HERE / "packet.md").read_text()
|
||||
digest = "sha256:" + hashlib.sha256(packet.encode("utf-8")).hexdigest()
|
||||
|
|
@ -33,29 +33,30 @@ envelope = {
|
|||
"approval_intent": {
|
||||
"action": "acknowledge",
|
||||
"principal": "net-kingdom-admins",
|
||||
"purpose": "Admit oidc-service-client 0.1 as a SecurityCaseType (canon security-case-types_v0.1 section 5).",
|
||||
"purpose": "Admit oidc-service-client 0.2 as a SecurityCaseType (canon security-case-types_v0.1 section 5).",
|
||||
"target": {"id": TARGET, "type": "security-case-type", "system": "net-kingdom"},
|
||||
},
|
||||
"memo": {
|
||||
"id": MEMO_ID,
|
||||
"version": 1,
|
||||
"question": ("Acknowledge oidc-service-client 0.1 as the reusable SecurityCaseType for internal workloads "
|
||||
"question": ("Acknowledge oidc-service-client 0.2 as the reusable SecurityCaseType for internal workloads "
|
||||
"that obtain KeyCape client_credentials tokens for one resource server?"),
|
||||
"requested_act": "approve",
|
||||
"binding_level": "organizational",
|
||||
"brief": (
|
||||
"NK-WP-0056-T14. Acknowledging admits the shape of confidential service clients: one audience, a closed "
|
||||
"NK-WP-0056-T14/T20, r2 after the owner reviews. Acknowledging admits the shape of confidential service clients: one audience, a closed "
|
||||
"list of resource scopes, a fixed service subject and tenant, at most 15m, the secret minted in custody "
|
||||
"and read only by KeyCape (ESO) and the consumer's own bound role. It registers nothing and accepts no "
|
||||
"case; the first case, the bounded executor's approval client, keeps its own acceptance. One attended "
|
||||
"operator session per case remains, because KeyCape's live client registry is a Kubernetes Secret."
|
||||
"operator session and one KeyCape restart per case remain, because KeyCape's live client registry is a "
|
||||
"Kubernetes Secret; rotation is attended until a lane exists."
|
||||
),
|
||||
"binding": {
|
||||
"justification": None,
|
||||
"principal": {"display_name": "Named reviewer in net-kingdom-admins", "id": "pending-human-session",
|
||||
"identifiers": [], "kind": "person", "role": "reviewer"},
|
||||
"target": {"environment": "prod", "id": TARGET, "kind": "security-case-type",
|
||||
"label": "SecurityCaseType oidc-service-client 0.1", "requires_new_bind": False},
|
||||
"label": "SecurityCaseType oidc-service-client 0.2", "requires_new_bind": False},
|
||||
"terms": None,
|
||||
},
|
||||
"step_kind": "approve",
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"kind": "informed-decision.memo-intake",
|
||||
"version": 1,
|
||||
"idempotency_key": "state-hub.f8044f81-f94c-483a-81f9-4f2b02a60d5f.nk-wp-0056-t14-oidc-service-client-r1",
|
||||
"idempotency_key": "state-hub.f8044f81-f94c-483a-81f9-4f2b02a60d5f.nk-wp-0056-t14-oidc-service-client-r2",
|
||||
"memo_class": "decision-record",
|
||||
"review_group": "net-kingdom-admins",
|
||||
"drafted_by": "net-kingdom NK-WP-0056 (claude-code)",
|
||||
|
|
@ -12,20 +12,20 @@
|
|||
"approval_intent": {
|
||||
"action": "acknowledge",
|
||||
"principal": "net-kingdom-admins",
|
||||
"purpose": "Admit oidc-service-client 0.1 as a SecurityCaseType (canon security-case-types_v0.1 section 5).",
|
||||
"purpose": "Admit oidc-service-client 0.2 as a SecurityCaseType (canon security-case-types_v0.1 section 5).",
|
||||
"target": {
|
||||
"id": "oidc-service-client@0.1",
|
||||
"id": "oidc-service-client@0.2",
|
||||
"type": "security-case-type",
|
||||
"system": "net-kingdom"
|
||||
}
|
||||
},
|
||||
"memo": {
|
||||
"id": "nk-wp-0056-t14-oidc-service-client-r1",
|
||||
"id": "nk-wp-0056-t14-oidc-service-client-r2",
|
||||
"version": 1,
|
||||
"question": "Acknowledge oidc-service-client 0.1 as the reusable SecurityCaseType for internal workloads that obtain KeyCape client_credentials tokens for one resource server?",
|
||||
"question": "Acknowledge oidc-service-client 0.2 as the reusable SecurityCaseType for internal workloads that obtain KeyCape client_credentials tokens for one resource server?",
|
||||
"requested_act": "approve",
|
||||
"binding_level": "organizational",
|
||||
"brief": "NK-WP-0056-T14. Acknowledging admits the shape of confidential service clients: one audience, a closed list of resource scopes, a fixed service subject and tenant, at most 15m, the secret minted in custody and read only by KeyCape (ESO) and the consumer's own bound role. It registers nothing and accepts no case; the first case, the bounded executor's approval client, keeps its own acceptance. One attended operator session per case remains, because KeyCape's live client registry is a Kubernetes Secret.",
|
||||
"brief": "NK-WP-0056-T14/T20, r2 after the owner reviews. Acknowledging admits the shape of confidential service clients: one audience, a closed list of resource scopes, a fixed service subject and tenant, at most 15m, the secret minted in custody and read only by KeyCape (ESO) and the consumer's own bound role. It registers nothing and accepts no case; the first case, the bounded executor's approval client, keeps its own acceptance. One attended operator session and one KeyCape restart per case remain, because KeyCape's live client registry is a Kubernetes Secret; rotation is attended until a lane exists.",
|
||||
"binding": {
|
||||
"justification": null,
|
||||
"principal": {
|
||||
|
|
@ -37,9 +37,9 @@
|
|||
},
|
||||
"target": {
|
||||
"environment": "prod",
|
||||
"id": "oidc-service-client@0.1",
|
||||
"id": "oidc-service-client@0.2",
|
||||
"kind": "security-case-type",
|
||||
"label": "SecurityCaseType oidc-service-client 0.1",
|
||||
"label": "SecurityCaseType oidc-service-client 0.2",
|
||||
"requires_new_bind": false
|
||||
},
|
||||
"terms": null
|
||||
|
|
@ -47,15 +47,15 @@
|
|||
"step_kind": "approve",
|
||||
"packet": [
|
||||
{
|
||||
"item_id": "nk-wp-0056-t14-oidc-service-client-r1-packet",
|
||||
"label": "nk-wp-0056-t14-oidc-service-client-r1-packet",
|
||||
"hash": "sha256:0ed6b3a56d9e4d67d96e51f9ea0e4ddfaca60dee5db9e46f1680576f007758fe"
|
||||
"item_id": "nk-wp-0056-t14-oidc-service-client-r2-packet",
|
||||
"label": "nk-wp-0056-t14-oidc-service-client-r2-packet",
|
||||
"hash": "sha256:24179bcad018c3809f6dc229dcced9586800de17b5c76aea164d2416208294c9"
|
||||
}
|
||||
],
|
||||
"highlights": [
|
||||
{
|
||||
"id": "nk-wp-0056-t14-oidc-service-client-r1-h1",
|
||||
"item_id": "nk-wp-0056-t14-oidc-service-client-r1-packet",
|
||||
"id": "nk-wp-0056-t14-oidc-service-client-r2-h1",
|
||||
"item_id": "nk-wp-0056-t14-oidc-service-client-r2-packet",
|
||||
"severity": "critical",
|
||||
"required_ack": true,
|
||||
"locator": {
|
||||
|
|
@ -66,8 +66,8 @@
|
|||
"note": "Acknowledging changes nothing live and accepts no case. It decides the shape every future service client case will follow, including who may read the client secret."
|
||||
},
|
||||
{
|
||||
"id": "nk-wp-0056-t14-oidc-service-client-r1-h2",
|
||||
"item_id": "nk-wp-0056-t14-oidc-service-client-r1-packet",
|
||||
"id": "nk-wp-0056-t14-oidc-service-client-r2-h2",
|
||||
"item_id": "nk-wp-0056-t14-oidc-service-client-r2-packet",
|
||||
"severity": "warning",
|
||||
"required_ack": true,
|
||||
"locator": {
|
||||
|
|
@ -85,9 +85,9 @@
|
|||
},
|
||||
"packet": [
|
||||
{
|
||||
"item_id": "nk-wp-0056-t14-oidc-service-client-r1-packet",
|
||||
"item_id": "nk-wp-0056-t14-oidc-service-client-r2-packet",
|
||||
"media_type": "text/markdown",
|
||||
"content": "# Acknowledge SecurityCaseType oidc-service-client 0.1\n\nWorkplan: NK-WP-0056 (net-kingdom), task NK-WP-0056-T14\nHub decision: f8044f81-f94c-483a-81f9-4f2b02a60d5f\nCanon: canon/standards/security-case-types_v0.1.md (accepted 2026-10-08), section 5\nType: security-case-types/oidc-service-client/ (type.yaml, README.md, tests)\nFirst case: examples/nk-bind-executor-approval.yaml\n\n## Question\n\nAcknowledge oidc-service-client 0.1 as the reusable SecurityCaseType for internal workloads that obtain KeyCape\nclient_credentials tokens for one resource server?\n\n## One act\n\nMove the type from `proposed` to `acknowledged`. From then on, a confidential service client is a SecurityCase of\nthis type:\n\n- its owner fills in the parameters (audience, resource scopes, service subject, tenant, lifetime of at most 15m,\n consuming ServiceAccount);\n- the case is accepted by the owner and the founder (risk class medium);\n- ops-mason materializes it, and ops-warden follows through.\n\nThe type becomes `implemented` only after its first case is live and verified. That case is the bounded executor's\napproval client `nk-bind-executor-approval`.\n\n## Why now\n\napproval-engine accepts `POST /v1/approvals` only when `binding.actor` equals the token's `sub`. The bounded\nexecutor (NK-WP-0056) must therefore file its own approval as `service:nk-bind-executor`, which needs its own KeyCape\nclient. Every existing service client was registered by a one-off script. The browser type names service clients\nas needing \"a separate type\".\n\n## What acknowledging does not do\n\n- It registers no client, mints no secret, and changes nothing live.\n- It accepts no case. `nk-bind-executor-approval` keeps its own acceptance.\n- It does not change `oidc-browser-relying-party`.\n\n## Shape (decided here, then reused by every case)\n\n- **Grant:** `client_credentials` only. Confidential, an `env:` secretRef, `serviceSubject` and `tenant`, no\n redirect URIs. These are KeyCape's own rules. The type is stricter: a closed list of resource scopes, and at most\n 15m.\n- **Custody:** `platform/workloads/keycape/<client_id>`, minted inside custody by the railiance-platform broker and\n never returned. Exactly two readers:\n - KeyCape, through ESO;\n - the consuming workload, through its own Kubernetes-auth role (one ServiceAccount, one namespace, audience\n `openbao`, 15m, one policy).\n- **No Secret copy beside the consumer.** It reads per call, as the bounded executor's lanes already do\n (CCR-2026-0058 and CCR-2026-0059).\n- **Verification reads no value:**\n - the consumer reports claims (sub, aud, tenant, scope and lifetime);\n - an excess scope is refused;\n - custody is denied to a default identity;\n - the consumer role admits only its ServiceAccount and audience.\n- **Rollback** covers every step and every materialized item.\n\n## Attended steps\n\nOne attended operator session per case remains (`bind-env`, then `register-client`), because KeyCape's live client\nregistry is a Kubernetes Secret and KeyCape reads the client secret at start. Removing it needs a KeyCape change and\na new type version.\n\n## Alternatives considered\n\n- **Keep per-client scripts:** repeats custody and verification per client.\n- **Extend the browser type:** a looser validation and a major change. KeyCape rejects `serviceSubject` on browser\n clients.\n- **ESO-deliver into the consumer's namespace:** leaves a long-lived Secret copy beside the workload.\n- **A static bearer:** bypasses expiry and the subject binding.\n\n## Reviews requested before you answer\n\n- ops-mason: the materialization section;\n- ops-warden: the follow-through and its lanes;\n- key-cape: the field rules;\n- railiance-platform: the mint lane and the consumer role.\n\nIf a review changes the type, a new memo revision replaces this one.\n"
|
||||
"content": "# Acknowledge SecurityCaseType oidc-service-client 0.2\n\nWorkplan: NK-WP-0056 (net-kingdom), tasks NK-WP-0056-T14 and T20\nMemo: nk-wp-0056-t14-oidc-service-client-r2. It replaces r1 (0.1, before the owner reviews). If r1 was ever\nqueued, return it with a comment that r2 replaces it.\nHub decision: f8044f81-f94c-483a-81f9-4f2b02a60d5f\nCanon: canon/standards/security-case-types_v0.1.md (accepted 2026-10-08), section 5\nType: security-case-types/oidc-service-client/ (type.yaml, README.md, tests)\nFirst case: examples/nk-bind-executor-approval.yaml\n\n## Question\n\nAcknowledge oidc-service-client 0.2 as the reusable SecurityCaseType for internal workloads that obtain KeyCape\nclient_credentials tokens for one resource server?\n\n## One act\n\nMove the type from `proposed` to `acknowledged`. From then on, a confidential service client is a SecurityCase of\nthis type:\n\n- its owner fills in the parameters (audience, resource scopes, service subject, tenant, lifetime of at most 15m,\n consuming ServiceAccount);\n- the case is accepted by the owner and the founder (risk class medium);\n- ops-mason materializes it, and ops-warden follows through.\n\nThe type becomes `implemented` only after its first case is live and verified. That case is the bounded executor's\napproval client `nk-bind-executor-approval`.\n\n## Why now\n\napproval-engine accepts `POST /v1/approvals` only when `binding.actor` equals the token's `sub`. The bounded\nexecutor (NK-WP-0056) must therefore file its own approval as `service:nk-bind-executor`, which needs its own KeyCape\nclient. Every existing service client was registered by a one-off script. The browser type names service clients\nas needing \"a separate type\".\n\n## What acknowledging does not do\n\n- It registers no client, mints no secret, and changes nothing live.\n- It accepts no case. `nk-bind-executor-approval` keeps its own acceptance.\n- It does not change `oidc-browser-relying-party`.\n\n## Shape (decided here, then reused by every case)\n\n- **Grant:** `client_credentials` only. Confidential, an `env:` secretRef, `serviceSubject` and `tenant`, no\n redirect URIs. These are KeyCape's own rules. The type is stricter: a closed list of resource scopes, and at most\n 15m.\n- **Custody:** `platform/workloads/keycape/<client_id>`, minted inside custody by the railiance-platform broker and\n never returned. Exactly two readers:\n - KeyCape, through ESO;\n - the consuming workload, through its own Kubernetes-auth role (one dedicated ServiceAccount, never `default`, one\n namespace, audience `openbao`, 15m, one policy).\n- **No Secret copy for the consumer.** It reads per call, as the bounded executor's lanes already do (CCR-2026-0058\n and CCR-2026-0059). KeyCape's own delivered copy lives in `sso`. If the consumer runs there too, an RBAC can-i check\n proves it cannot read Secrets.\n- **Verification reads no value:**\n - the consumer reports claims (sub, aud, tenant, principal_type, scope and lifetime);\n - an excess scope is refused with `invalid_profile_usage`;\n - the consumer cannot read Secrets in its namespace;\n - custody is denied to a default identity;\n - the consumer role admits only its ServiceAccount and audience.\n- **Rollback** covers every step and every materialized item.\n\n## Attended steps\n\nOne attended operator session and **one KeyCape restart** per case remain (`bind-env`, then `register-client`, in\none rollout), because KeyCape's live client registry is a Kubernetes Secret and KeyCape reads the client secret at\nstart. Rotation is also attended for now (a platform-admin write, then a KeyCape restart): the mint step only\ncreates.\n\n## What follows acknowledgement\n\n- railiance-platform admits the type in its secret broker (a reviewed source change).\n- The first case is accepted with its CCRs: CCR-2026-0060 (KeyCape's reader) and CCR-2026-0061 (the executor's\n reader).\n\n## Alternatives considered\n\n- **Keep per-client scripts:** repeats custody and verification per client.\n- **Extend the browser type:** a looser validation and a major change. KeyCape rejects `serviceSubject` on browser\n clients.\n- **ESO-deliver into the consumer's namespace:** leaves a long-lived Secret copy beside the workload.\n- **A static bearer:** bypasses expiry and the subject binding.\n\n## Reviews (done 2026-10-31, folded into 0.2)\n\n- ops-mason (`3c69a25`): materialization accepted with changes, all applied.\n- ops-warden (`95d31c3`): follow-through sound; changes applied (rotation, catalog fields, same-namespace check).\n- key-cape (`655d8d3`): field rules confirmed with `keycape check-config` and a local token test; error code and\n restart notes applied.\n- railiance-platform (`6858bef`): custody and the consumer role accepted. Role renamed; per-case CCRs drafted.\n"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
# Acknowledge SecurityCaseType oidc-service-client 0.1
|
||||
# Acknowledge SecurityCaseType oidc-service-client 0.2
|
||||
|
||||
Workplan: NK-WP-0056 (net-kingdom), task NK-WP-0056-T14
|
||||
Workplan: NK-WP-0056 (net-kingdom), tasks NK-WP-0056-T14 and T20
|
||||
Memo: nk-wp-0056-t14-oidc-service-client-r2. It replaces r1 (0.1, before the owner reviews). If r1 was ever
|
||||
queued, return it with a comment that r2 replaces it.
|
||||
Hub decision: f8044f81-f94c-483a-81f9-4f2b02a60d5f
|
||||
Canon: canon/standards/security-case-types_v0.1.md (accepted 2026-10-08), section 5
|
||||
Type: security-case-types/oidc-service-client/ (type.yaml, README.md, tests)
|
||||
|
|
@ -8,7 +10,7 @@ First case: examples/nk-bind-executor-approval.yaml
|
|||
|
||||
## Question
|
||||
|
||||
Acknowledge oidc-service-client 0.1 as the reusable SecurityCaseType for internal workloads that obtain KeyCape
|
||||
Acknowledge oidc-service-client 0.2 as the reusable SecurityCaseType for internal workloads that obtain KeyCape
|
||||
client_credentials tokens for one resource server?
|
||||
|
||||
## One act
|
||||
|
|
@ -45,22 +47,31 @@ as needing "a separate type".
|
|||
- **Custody:** `platform/workloads/keycape/<client_id>`, minted inside custody by the railiance-platform broker and
|
||||
never returned. Exactly two readers:
|
||||
- KeyCape, through ESO;
|
||||
- the consuming workload, through its own Kubernetes-auth role (one ServiceAccount, one namespace, audience
|
||||
`openbao`, 15m, one policy).
|
||||
- **No Secret copy beside the consumer.** It reads per call, as the bounded executor's lanes already do
|
||||
(CCR-2026-0058 and CCR-2026-0059).
|
||||
- the consuming workload, through its own Kubernetes-auth role (one dedicated ServiceAccount, never `default`, one
|
||||
namespace, audience `openbao`, 15m, one policy).
|
||||
- **No Secret copy for the consumer.** It reads per call, as the bounded executor's lanes already do (CCR-2026-0058
|
||||
and CCR-2026-0059). KeyCape's own delivered copy lives in `sso`. If the consumer runs there too, an RBAC can-i check
|
||||
proves it cannot read Secrets.
|
||||
- **Verification reads no value:**
|
||||
- the consumer reports claims (sub, aud, tenant, scope and lifetime);
|
||||
- an excess scope is refused;
|
||||
- the consumer reports claims (sub, aud, tenant, principal_type, scope and lifetime);
|
||||
- an excess scope is refused with `invalid_profile_usage`;
|
||||
- the consumer cannot read Secrets in its namespace;
|
||||
- custody is denied to a default identity;
|
||||
- the consumer role admits only its ServiceAccount and audience.
|
||||
- **Rollback** covers every step and every materialized item.
|
||||
|
||||
## Attended steps
|
||||
|
||||
One attended operator session per case remains (`bind-env`, then `register-client`), because KeyCape's live client
|
||||
registry is a Kubernetes Secret and KeyCape reads the client secret at start. Removing it needs a KeyCape change and
|
||||
a new type version.
|
||||
One attended operator session and **one KeyCape restart** per case remain (`bind-env`, then `register-client`, in
|
||||
one rollout), because KeyCape's live client registry is a Kubernetes Secret and KeyCape reads the client secret at
|
||||
start. Rotation is also attended for now (a platform-admin write, then a KeyCape restart): the mint step only
|
||||
creates.
|
||||
|
||||
## What follows acknowledgement
|
||||
|
||||
- railiance-platform admits the type in its secret broker (a reviewed source change).
|
||||
- The first case is accepted with its CCRs: CCR-2026-0060 (KeyCape's reader) and CCR-2026-0061 (the executor's
|
||||
reader).
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
|
|
@ -70,11 +81,10 @@ a new type version.
|
|||
- **ESO-deliver into the consumer's namespace:** leaves a long-lived Secret copy beside the workload.
|
||||
- **A static bearer:** bypasses expiry and the subject binding.
|
||||
|
||||
## Reviews requested before you answer
|
||||
## Reviews (done 2026-10-31, folded into 0.2)
|
||||
|
||||
- ops-mason: the materialization section;
|
||||
- ops-warden: the follow-through and its lanes;
|
||||
- key-cape: the field rules;
|
||||
- railiance-platform: the mint lane and the consumer role.
|
||||
|
||||
If a review changes the type, a new memo revision replaces this one.
|
||||
- ops-mason (`3c69a25`): materialization accepted with changes, all applied.
|
||||
- ops-warden (`95d31c3`): follow-through sound; changes applied (rotation, catalog fields, same-namespace check).
|
||||
- key-cape (`655d8d3`): field rules confirmed with `keycape check-config` and a local token test; error code and
|
||||
restart notes applied.
|
||||
- railiance-platform (`6858bef`): custody and the consumer role accepted. Role renamed; per-case CCRs drafted.
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ apiVersion: netkingdom.io/security-case/v0.1
|
|||
kind: SecurityCase
|
||||
spec:
|
||||
id: nk-bind-executor-approval
|
||||
type: oidc-service-client@0.1
|
||||
type: oidc-service-client@0.2
|
||||
status: proposed
|
||||
owner:
|
||||
repo: net-kingdom
|
||||
|
|
@ -23,4 +23,4 @@ spec:
|
|||
consuming_service: bounded-executor
|
||||
consumer_namespace: sso
|
||||
consumer_service_account: nk-bind-executor
|
||||
consumer_owner_repo: net-kingdom
|
||||
consumer_owner_repo: rapp-bounded-executor
|
||||
|
|
|
|||
|
|
@ -60,14 +60,16 @@ def test_executor_case_renders_custody_and_one_attended_session():
|
|||
steps = {s["id"]: s for s in r["follow_through"]}
|
||||
assert steps["mint-secret"]["entry_point"].endswith("bao:platform/workloads/keycape/nk-bind-executor-approval")
|
||||
assert "KEYCAPE_NK_BIND_EXECUTOR_APPROVAL_CLIENT_SECRET" in steps["bind-env"]["entry_point"]
|
||||
assert steps["hand-over"]["owner"] == "net-kingdom"
|
||||
assert steps["hand-over"]["owner"] == "rapp-bounded-executor"
|
||||
assert r["execution"] == {"permitted": False}
|
||||
|
||||
|
||||
def test_consumer_reads_natively_with_its_own_bound_role():
|
||||
items = {m["id"]: m for m in render()["materialization"]}
|
||||
role = yaml.safe_load(items["consumer-reader-role"]["body"])
|
||||
assert items["consumer-reader-role"]["template"] == "workload-kv-read-keycape-nk-bind-executor-approval-bounded-executor"
|
||||
# role and policy names differ: the delegated applier refuses a role named like its policy (railiance-platform R4)
|
||||
assert items["consumer-reader-role"]["template"] == "keycape-nk-bind-executor-approval-bounded-executor-workload-kv-read"
|
||||
assert items["consumer-reader-policy"]["template"] == "workload-kv-read-keycape-nk-bind-executor-approval-bounded-executor"
|
||||
assert role["bound_service_account_names"] == ["nk-bind-executor"]
|
||||
assert role["bound_service_account_namespaces"] == ["sso"]
|
||||
assert role["audience"] == "openbao"
|
||||
|
|
@ -94,7 +96,8 @@ def test_custody_path_is_derived_not_case_settable():
|
|||
def test_case_refusals():
|
||||
for change in [dict(allowed_scopes="openid,approval:read"), dict(token_lifetime="1h"),
|
||||
dict(service_subject="nk-bind-executor"), dict(tenant="platform"), dict(audience=None),
|
||||
dict(consumer_service_account=None)]:
|
||||
dict(consumer_service_account=None), dict(consumer_service_account="default"),
|
||||
dict(consumer_namespace="openbao"), dict(consumer_namespace="external-secrets")]:
|
||||
assert msgs(v.validate_case(with_params(EXECUTOR, **change), as_implemented())), change
|
||||
|
||||
|
||||
|
|
@ -102,7 +105,10 @@ def test_verification_reads_no_value_and_has_negative_checks():
|
|||
checks = render()["verification"]
|
||||
assert all(c["reads_secret_value"] is False for c in checks)
|
||||
assert {c["id"] for c in checks if c["expect"] == "deny"} == {
|
||||
"excess-scope-refused", "custody-denied-to-default", "consumer-login-bound"}
|
||||
"excess-scope-refused", "custody-denied-to-default", "consumer-login-bound",
|
||||
"consumer-cannot-read-delivered-secret"}
|
||||
excess = next(c for c in checks if c["id"] == "excess-scope-refused")["check"]
|
||||
assert "invalid_profile_usage" in excess and "invalid_scope" not in excess
|
||||
|
||||
|
||||
def test_keycape_record_from_the_case():
|
||||
|
|
@ -117,3 +123,34 @@ def test_every_step_and_item_has_a_rollback():
|
|||
reversed_ids = {r["reverses"] for r in TYPE["spec"]["rollback"]}
|
||||
assert {s["id"] for s in TYPE["spec"]["follow_through"]} <= reversed_ids
|
||||
assert {m["id"] for m in TYPE["spec"]["materialization"]} <= reversed_ids
|
||||
|
||||
|
||||
def test_eso_role_and_policy_match_the_live_reader_shape():
|
||||
items = {m["id"]: m for m in render()["materialization"]}
|
||||
role = yaml.safe_load(items["eso-role"]["body"])
|
||||
assert role["audience"] == "openbao" and role["token_explicit_max_ttl"] == "15m" # ops-mason R3
|
||||
policy = items["keycape-reader-policy"]["body"]
|
||||
assert '"auth/token/lookup-self"' in policy and '"auth/token/revoke-self"' in policy # ops-mason R4
|
||||
|
||||
|
||||
def test_catalog_entry_names_no_fetch_route_and_attended_rotation():
|
||||
body = yaml.safe_load(next(m for m in render()["materialization"] if m["id"] == "catalog-entry")["body"])
|
||||
assert body["exec_capable"] is False and "fetch_command" not in body # ops-warden R6
|
||||
assert body["lane"] == "secret" and "keycape-nk-bind-executor-approval-bounded-executor-workload-kv-read" in body["auth_method"]
|
||||
assert "attended platform-admin" in body["rotation"] and "automatable false" in body["rotation"] # R5
|
||||
|
||||
|
||||
def test_first_case_ccrs_name_the_rendered_roles_and_policies():
|
||||
ccrs = REPO.parent / "railiance-platform" / "credential-change-requests"
|
||||
found = {c.name: yaml.safe_load(c.read_text()) for c in ccrs.glob("CCR-2026-006[01]-*.yaml")} if ccrs.exists() else {}
|
||||
if len(found) != 2:
|
||||
import pytest
|
||||
pytest.skip("railiance-platform CCR-2026-0060/0061 not checked out")
|
||||
items = {m["id"]: m["template"] for m in render()["materialization"]}
|
||||
auth = {name.split("-nk-")[0]: ccr["openbao"]["auth"] for name, ccr in found.items()}
|
||||
eso, consumer = auth["CCR-2026-0060"], auth["CCR-2026-0061"]
|
||||
assert eso["role"] == items["eso-role"] and eso["policies"] == [items["keycape-reader-policy"]]
|
||||
assert consumer["role"] == items["consumer-reader-role"]
|
||||
assert consumer["policies"] == [items["consumer-reader-policy"]]
|
||||
assert consumer["bound_claims"] == {"service_account_names": ["nk-bind-executor"],
|
||||
"service_account_namespaces": ["sso"]}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ apiVersion: netkingdom.io/security-case-type/v0.1
|
|||
kind: SecurityCaseType
|
||||
spec:
|
||||
id: oidc-service-client
|
||||
version: "0.1"
|
||||
version: "0.2"
|
||||
status: proposed
|
||||
title: "Confidential OIDC service client (client_credentials) for an internal workload"
|
||||
purpose: >-
|
||||
|
|
@ -81,15 +81,18 @@ spec:
|
|||
- name: consumer_namespace
|
||||
type: string
|
||||
case_settable: true
|
||||
constraints: {pattern: "^[a-z0-9][a-z0-9-]{0,62}$"}
|
||||
constraints: {pattern: "^(?!(external-secrets|openbao)$)[a-z0-9][a-z0-9-]{0,62}$"}
|
||||
sensitivity: security_sensitive
|
||||
description: Namespace of the consuming workload's ServiceAccount.
|
||||
description: >-
|
||||
Namespace of the consuming workload's ServiceAccount. Not external-secrets or openbao (ops-mason R6). If it
|
||||
is sso, KeyCape's own delivered copy lives beside the consumer; verification proves the consumer cannot read
|
||||
it (consumer-cannot-read-delivered-secret).
|
||||
- name: consumer_service_account
|
||||
type: string
|
||||
case_settable: true
|
||||
constraints: {pattern: "^[a-z0-9][a-z0-9-]{0,62}$"}
|
||||
constraints: {pattern: "^(?!default$)[a-z0-9][a-z0-9-]{0,62}$"}
|
||||
sensitivity: security_sensitive
|
||||
description: The ServiceAccount whose openbao-audience token may read the client secret.
|
||||
description: The dedicated ServiceAccount whose openbao-audience token may read the client secret; never default (ops-mason R6).
|
||||
- name: consumer_owner_repo
|
||||
type: string
|
||||
case_settable: true
|
||||
|
|
@ -134,7 +137,15 @@ spec:
|
|||
path "platform/data/workloads/keycape/{client_id}" {{
|
||||
capabilities = ["read"]
|
||||
}}
|
||||
description: KeyCape reads its client secret only through ESO. No metadata, list, parent or sibling access.
|
||||
path "auth/token/lookup-self" {{
|
||||
capabilities = ["read"]
|
||||
}}
|
||||
path "auth/token/revoke-self" {{
|
||||
capabilities = ["update"]
|
||||
}}
|
||||
description: >-
|
||||
KeyCape reads its client secret only through ESO. No metadata, list, parent or sibling access; token
|
||||
self-lifecycle as in CCR-2026-0041 (ops-mason R4, railiance-platform R3).
|
||||
- id: eso-role
|
||||
kind: openbao_role
|
||||
template: "external-secrets-keycape-{client_id}"
|
||||
|
|
@ -142,11 +153,13 @@ spec:
|
|||
auth: kubernetes
|
||||
bound_service_account_names: [external-secrets]
|
||||
bound_service_account_namespaces: [external-secrets]
|
||||
audience: openbao
|
||||
token_policies: [workload-kv-read-keycape-{client_id}-eso]
|
||||
token_no_default_policy: true
|
||||
token_ttl: 15m
|
||||
token_max_ttl: 15m
|
||||
description: The Kubernetes-auth role deliver-secret uses; bound to the ESO service account only.
|
||||
token_explicit_max_ttl: 15m
|
||||
description: The Kubernetes-auth role deliver-secret uses; bound to the ESO service account only (ops-mason R3).
|
||||
- id: consumer-reader-policy
|
||||
kind: openbao_policy
|
||||
template: "workload-kv-read-keycape-{client_id}-{consuming_service}"
|
||||
|
|
@ -163,7 +176,8 @@ spec:
|
|||
description: The consuming workload reads the same secret. No metadata, list, parent or sibling access.
|
||||
- id: consumer-reader-role
|
||||
kind: openbao_role
|
||||
template: "workload-kv-read-keycape-{client_id}-{consuming_service}"
|
||||
# The delegated applier refuses a role named like its policy (railiance-platform R4).
|
||||
template: "keycape-{client_id}-{consuming_service}-workload-kv-read"
|
||||
body: |
|
||||
auth: kubernetes
|
||||
bound_service_account_names: [{consumer_service_account}]
|
||||
|
|
@ -190,8 +204,17 @@ spec:
|
|||
path_template: platform/workloads/keycape/{client_id}
|
||||
field: CLIENT_SECRET
|
||||
delegation: native, intended_owner key-cape
|
||||
rotation: mint-secret then verification; automatable false until a run has been proven
|
||||
description: One entry per case; carries the consumer's workload reference for zone selection (ADR-0009).
|
||||
lane: secret
|
||||
subsystem: keycape
|
||||
auth_method: kubernetes role keycape-{client_id}-{consuming_service}-workload-kv-read (audience openbao), bound to {consumer_namespace}/{consumer_service_account}
|
||||
exec_capable: false
|
||||
need_keywords: [{client_id}, {audience}, {consuming_service}, keycape client secret]
|
||||
rotation: >-
|
||||
attended platform-admin write of the next version with CAS (mint-secret only creates and refuses an
|
||||
existing path), then the ESO delivery Ready, then one KeyCape restart; automatable false, no lane yet
|
||||
description: >-
|
||||
One entry per case; carries the consumer's workload reference for zone selection (ADR-0009). No fetch
|
||||
command: no operator fetch route is implied (ops-warden R6).
|
||||
|
||||
follow_through:
|
||||
- id: mint-secret
|
||||
|
|
@ -219,10 +242,13 @@ spec:
|
|||
attended: true
|
||||
value_handling: none
|
||||
attended_justification: >-
|
||||
A change to the live identity provider's Deployment. It runs in the same
|
||||
attended operator session as register-client, before it, because KeyCape
|
||||
resolves a confidential client's secret at start and fails closed if the
|
||||
binding is missing.
|
||||
A change to the live identity provider's Deployment. KeyCape runs one
|
||||
replica with strategy Recreate, so an env patch is itself a restart
|
||||
(key-cape R4): bind-env and register-client are applied in one rollout,
|
||||
bind-env first, because KeyCape resolves a confidential client's secret
|
||||
at start and fails closed if the binding is missing. deliver-secret must
|
||||
report Ready immediately before, or the new pod cannot start and every
|
||||
client loses sign-in.
|
||||
- id: register-client
|
||||
owner: net-kingdom
|
||||
entry_point: "sso-mfa/k8s/keycape/keycape-client.py apply --client-file <client-from-case> --case-acceptance <record> --check-config <keycape binary> (net-kingdom.keycape-service-client-registration)"
|
||||
|
|
@ -248,11 +274,11 @@ spec:
|
|||
|
||||
verification:
|
||||
- id: service-token-claims
|
||||
check: "{consuming_service} performs one client_credentials exchange per scope in {allowed_scopes} through its own lane and reports the token's sub, aud, tenant, scope and lifetime; expected sub {service_subject}, aud {audience}, tenant {tenant}, lifetime at most {token_lifetime} (claims only; the verifier reads no secret and no token)"
|
||||
check: "{consuming_service} performs one client_credentials exchange per scope in {allowed_scopes} through its own lane and reports the token's sub, aud, tenant, principal_type, scope and lifetime; expected sub {service_subject}, aud {audience}, tenant {tenant}, principal_type service, lifetime at most {token_lifetime} (claims only; the verifier reads no secret and no token)"
|
||||
expect: present
|
||||
reads_secret_value: false
|
||||
- id: excess-scope-refused
|
||||
check: "{consuming_service} requests a scope outside {allowed_scopes} for client {client_id}; KeyCape returns invalid_scope"
|
||||
check: "{consuming_service} requests a scope outside {allowed_scopes} for client {client_id}; KeyCape answers HTTP 400 invalid_profile_usage (feature scope) and issues no token (key-cape R3)"
|
||||
expect: deny
|
||||
reads_secret_value: false
|
||||
- id: custody-denied-to-default
|
||||
|
|
@ -260,13 +286,17 @@ spec:
|
|||
expect: deny
|
||||
reads_secret_value: false
|
||||
- id: consumer-login-bound
|
||||
check: "role workload-kv-read-keycape-{client_id}-{consuming_service} admits only {consumer_namespace}/{consumer_service_account} with audience openbao; another ServiceAccount and a wrong audience are denied (capabilities and login result only)"
|
||||
check: "role keycape-{client_id}-{consuming_service}-workload-kv-read admits only {consumer_namespace}/{consumer_service_account} with audience openbao; another ServiceAccount and a wrong audience are denied (capabilities and login result only)"
|
||||
expect: deny
|
||||
reads_secret_value: false
|
||||
- id: custody-allowed-to-consumer
|
||||
check: "policy workload-kv-read-keycape-{client_id}-{consuming_service} grants read on bao:platform/workloads/keycape/{client_id} and nothing else (capabilities check only)"
|
||||
check: "policy workload-kv-read-keycape-{client_id}-{consuming_service} grants read on bao:platform/workloads/keycape/{client_id} and otherwise only token lookup-self and revoke-self (capabilities check only)"
|
||||
expect: allow
|
||||
reads_secret_value: false
|
||||
- id: consumer-cannot-read-delivered-secret
|
||||
check: "an RBAC can-i check as {consumer_namespace}/{consumer_service_account} for get, list and watch on Secrets in {consumer_namespace} answers no, so KeyCape's delivered copy keycape-{client_id}-client stays out of the consumer's reach (authorization answer only; ops-warden R7, ops-mason R7)"
|
||||
expect: deny
|
||||
reads_secret_value: false
|
||||
- id: secret-delivered
|
||||
check: "the delivery for sso/keycape-{client_id}-client reports Ready=True (status only)"
|
||||
expect: present
|
||||
|
|
@ -290,7 +320,7 @@ spec:
|
|||
- reverses: keycape-reader-policy
|
||||
action: "delete policy workload-kv-read-keycape-{client_id}-eso, after eso-role"
|
||||
- reverses: consumer-reader-role
|
||||
action: "delete Kubernetes-auth role workload-kv-read-keycape-{client_id}-{consuming_service}"
|
||||
action: "delete Kubernetes-auth role keycape-{client_id}-{consuming_service}-workload-kv-read; rollback is complete once the consumer's tokens have expired (at most 15m) or been revoked (ops-mason R11)"
|
||||
- reverses: consumer-reader-policy
|
||||
action: "delete policy workload-kv-read-keycape-{client_id}-{consuming_service}, after consumer-reader-role"
|
||||
- reverses: catalog-entry
|
||||
|
|
|
|||
|
|
@ -896,7 +896,7 @@ Founder decision needed before T14.
|
|||
|
||||
```task
|
||||
id: NK-WP-0056-T20
|
||||
status: todo
|
||||
status: done
|
||||
priority: high
|
||||
assignee: claude-code
|
||||
depends_on: [NK-WP-0056-T14]
|
||||
|
|
@ -935,6 +935,20 @@ R1).
|
|||
|
||||
**Done when:** the type is at r2 with tests green, the memo r2 is validated, and the CCR names agree with the type.
|
||||
|
||||
**Done 2026-11-01.** `oidc-service-client` is at 0.2 (`proposed`) with every review change applied (README history
|
||||
lists them). Settled here:
|
||||
|
||||
- `consuming_service` stays `bounded-executor`;
|
||||
- the first case's `consumer_owner_repo` is `rapp-bounded-executor`.
|
||||
|
||||
The consumer role is `keycape-{client_id}-{consuming_service}-workload-kv-read`. A new test checks CCR-2026-0060 and
|
||||
CCR-2026-0061 against the rendered role and policy names (they agree). 141 related tests pass.
|
||||
|
||||
Memo `nk-wp-0056-t14-oidc-service-client-r2` passes informed-decision's intake validator and State Hub's dispatcher
|
||||
contract (hub decision `f8044f81`). The rapp follows the role rename (`828992e`).
|
||||
|
||||
Next for the founder: queue memo r2 through the State Hub memo dispatcher and acknowledge the type.
|
||||
|
||||
## Executor submits its admission memo to informed-decision
|
||||
|
||||
```task
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue