Add credential-change delegated applier flow

This commit is contained in:
2026-07-01 20:07:26 +02:00
parent c626bfcf15
commit a95236d2e5
21 changed files with 2705 additions and 119 deletions

View File

@@ -192,16 +192,22 @@ The GitOps contract uses:
`ClusterSecretStore`.
- OpenBao Kubernetes auth role `external-secrets-issue-core` for the
issue-core pilot.
- OpenBao Kubernetes auth role `external-secrets-activity-core` for the
activity-core/llm-connect provider-secret lane once approved.
The initial `ClusterSecretStore/openbao` is intentionally limited to the
`issue-core` namespace. Broaden it only with a new platform review when another
tenant is ready to consume OpenBao through ESO.
`ClusterSecretStore/openbao` is limited to the `issue-core` namespace.
`ClusterSecretStore/openbao-activity-core` is limited to the `activity-core`
namespace and is intended for the llm-connect provider-secret lane. Broaden or
add stores only with platform review.
Configure the OpenBao side without printing token values:
```bash
OPENBAO_TOKEN_FILE=~/.local/openbao/platform-admin.token \
make openbao-configure-external-secrets-issue-core
OPENBAO_TOKEN_FILE=~/.local/openbao/platform-admin.token \
make openbao-configure-external-secrets-activity-core
```
The helper keeps Kubernetes auth in local-reviewer mode: OpenBao rereads its

View File

@@ -156,6 +156,12 @@ scripts/credential-change.py needs-changes CCR-2026-0001 --reviewer <name> --com
make credential-change-sync-decision CREDENTIAL_CHANGE=CCR-2026-0001
make credential-change-apply-plan CREDENTIAL_CHANGE=CCR-2026-0001
make credential-change-operator-commands CREDENTIAL_CHANGE=CCR-2026-0001
make credential-change-runbook CREDENTIAL_CHANGE=CCR-2026-0001
scripts/credential-change.py runbook CCR-2026-0001 --execute-metadata --actor <operator> --confirm "APPLY CCR-2026-0001"
scripts/credential-change.py record-evidence CCR-2026-0001 --actor <operator> --kind positive_verification --result passed --detail "<non-secret audit reference>" --record-state-hub
make credential-change-lifecycle-plan CREDENTIAL_CHANGE=CCR-2026-0001 CREDENTIAL_CHANGE_LIFECYCLE_ACTION=deactivate
scripts/credential-change.py lifecycle-event CCR-2026-0001 --action compromise --actor <operator> --reason "<non-secret reason>" --detail "<non-secret evidence>" --blast-radius "<non-secret scope>" --follow-up "<task/ref>" --record-state-hub
scripts/credential-change.py import-inventory CCR-YYYY-NNNN --title "existing lane" --tenant <tenant> --workload <workload> --environment production --purpose "<purpose>" --kv-path platform/workloads/<tenant>/<workload>/<purpose> --field <FIELD_NAME> --auth-method oidc --auth-mount netkingdom --auth-role <role> --bound-claim groups=<group> --bound-claims-confirmed --frontdoor-type ops-warden --catalog-id <catalog-id> --reason "Imported existing lane without secret values"
```
`apply-plan` and `operator-commands` are intentionally guarded: they refuse
@@ -229,6 +235,15 @@ The interactive runbook is the operator bridge:
8. record non-secret evidence;
9. notify downstream front doors such as ops-warden.
`credential-change.py runbook <CCR>` renders the checklist and exact final
confirmation phrase. `--execute-metadata` is intentionally opt-in and requires
that phrase; it uses the local `bao` CLI with ambient approved operator
authority, writes only policy/auth metadata, and records a non-secret
`metadata_apply` evidence entry. Secret value provisioning stays outside the
script through approved OpenBao/operator custody. Verification, activation, and
manual custody events are recorded with `record-evidence`, whose comments are
scanned for known secret markers before the CCR file or State Hub is updated.
This lets operators safely drive privileged work without needing to remember
every OpenBao command.
@@ -241,6 +256,16 @@ Every active CCR needs a deactivate and rotate path:
- `compromised`: emergency state requiring immediate disablement, rotation,
blast-radius notes, and incident follow-up.
`lifecycle-plan` renders the attended checklist for each case, including the
front-door state change and OpenBao metadata disable commands for deactivation
or compromise. `lifecycle-event` records the non-secret lifecycle event in the
CCR, sets the CCR status, and marks the access front door disabled, pending
verification, or compromised as appropriate. For compromise events it accepts
non-secret blast-radius notes and follow-up task references. Existing lanes that
predate CCRs can be imported with `import-inventory`, which writes a CCR and
matching read-policy artifact from metadata only; it never asks for or stores
the secret value.
The workflow must support marking an existing credential or lane as compromised
even when the original request predates this system.

View File

@@ -0,0 +1,127 @@
# OpenBao Approved Automation Delegation
This document specifies the narrow OpenBao metadata surface that approved
credential-change automation may mutate. It exists to avoid routine use of broad
`platform-admin` while keeping secret values under operator custody.
## Scope
The delegated applier is for reviewed metadata only:
- ACL policies generated from approved CCRs;
- auth roles bound to reviewed OIDC claims or Kubernetes service accounts;
- credential-broker issuer policies and token roles generated from reviewed
grant catalog entries;
- readback and capability checks needed to prove the mutation landed.
It must not read, write, print, wrap, unwrap, or proxy managed secret values.
Production secret provisioning remains an attended OpenBao/operator custody
step unless a later workplan approves a stronger dual-control flow.
## Environment Boundaries
Build and development may use sandbox metadata once a non-production OpenBao
mount or namespace is declared. Generated test secrets must stay in the sandbox
and must never be copied into State Hub, prompts, Git, or chat.
The non-production applier policy candidate is
`openbao/policies/credential-change-nonprod-applier.hcl`. It currently grants
only metadata writes, matching the no-secret-value rule used in production.
Any future generated test-secret path needs a separate CCR-backed approval so
it cannot silently expand this delegation.
Test and staging may apply reviewed metadata after owner review. Verification
must include positive and negative access checks, and evidence must be
non-secret.
Production may apply only reviewed non-secret metadata. The production applier
policy is `openbao/policies/credential-change-prod-applier.hcl`, and every live
run must be preceded by `scripts/credential-change.py applier-dry-run <CCR>`.
Unapproved CCRs fail closed before any OpenBao mutation is rendered. Live
metadata mutation uses `scripts/credential-change.py applier-apply <CCR>` with
an exact `DELEGATED APPLY <CCR-ID>` confirmation phrase and the local `bao` CLI
under ambient delegated applier authority; the command does not accept OpenBao
tokens in argv.
## Production Mutation Surface
| Change class | Allowed OpenBao path | Notes |
| --- | --- | --- |
| Workload KV read policies | `sys/policies/acl/workload-kv-read-*` | Generated from CCR mount/path/field metadata. |
| Credential broker issuer policies | `sys/policies/acl/credential-broker-*-issuer` | Generated from grant catalog metadata. |
| OIDC workload roles | `auth/netkingdom/role/*-workload-kv-read` | Bound claims must be confirmed before apply. |
| Kubernetes workload roles | `auth/kubernetes/role/*` | Bound service accounts/namespaces must be confirmed before apply. |
| Credential broker token roles | `auth/token/roles/credential-broker-*` | Child-token roles only; no root or platform-admin policies. |
| Self checks | `auth/token/lookup-self`, `sys/capabilities-self` | Read/update only as required by OpenBao. |
Denied by omission:
- `platform/data/*`, `platform/metadata/*`, or any other secret value path;
- `sys/*` outside the approved ACL policy prefixes;
- `auth/*` outside the approved role prefixes;
- `identity/*`, unseal/recovery material, audit devices, mounts, and root/admin
policy assignment;
- wildcard, parent-directory, or mismatched policy and role names.
## Local Dry-Run Guardrails
The CCR dry-run is deliberately stricter than the OpenBao ACL policy. It must:
1. validate the CCR schema and secret-marker scan;
2. require CCR status `approved`, `applied`, `verified`, or `active`;
3. require `openbao.auth.bound_claims_confirmed=true`;
4. require mount `platform` and path `platform/workloads/...` for workload KV
requests;
5. require policy names to start with `workload-kv-read-` and remain under
`openbao/policies/<policy-name>.hcl`;
6. require OIDC roles to stay under `auth/netkingdom/role/*-workload-kv-read`;
7. require Kubernetes roles to stay under `auth/kubernetes/role/*-workload-kv-read`
or `auth/kubernetes/role/*-secrets-read`;
8. render only exact policy and auth-role metadata mutations;
9. leave secret value writes and front-door activation out of scope.
`applier-apply` reuses the same guardrails, renders the dry-run payload before
mutation, requires exact confirmation, writes only policy/auth-role metadata,
and appends non-secret `delegated_metadata_apply` evidence. For approved CCRs it
can advance file-backed status to `applied`; for already applied/verified/active
CCRs it records idempotent evidence without moving the lifecycle backward.
## Required Evidence
Record only non-secret evidence:
- CCR id and approval/decision reference;
- applier identity and timestamp;
- policy name and auth role path;
- OpenBao request id or audit timestamp;
- positive and negative verification references;
- front-door activation confirmation after verification.
## Applier Identity Setup
`openbao-apply-credential-change-appliers.py` configures the source-owned
metadata applier policies and matching OpenBao token roles:
- `credential-change-nonprod-applier` uses
`openbao/policies/credential-change-nonprod-applier.hcl`;
- `credential-change-prod-applier` uses
`openbao/policies/credential-change-prod-applier.hcl`.
The token roles allow only their matching applier policy, explicitly disallow
`root` and `platform-admin`, disable the default policy, use service tokens,
and do not issue tokens by themselves. Token issuance remains an approved
custody path outside this setup script. Use
`make openbao-credential-change-appliers-dry-run` before any live apply.
## Current Production Policy Candidate
`openbao/policies/credential-change-prod-applier.hcl` is the source candidate
for a future production applier identity. It is not a substitute for CCR review;
it is the OpenBao-side capability envelope used after local dry-run validation.
## Current Non-Production Policy Candidate
`openbao/policies/credential-change-nonprod-applier.hcl` is the source
candidate for a build/test/staging applier identity. It is intentionally
metadata-only until this repo declares a non-production OpenBao mount or
namespace and records live positive/negative evidence for that lane.

View File

@@ -30,7 +30,7 @@ Ops-warden batch follow-up:
| KV mount | `platform` |
| OpenBao CLI path | `platform/workloads/coulomb/whynot-design/npm-publish` |
| Secret field | `NPM_AUTH_TOKEN` |
| Front-door readiness | `applied-pending-verify`, `resolvable=false` until caller verification |
| Front-door readiness | `active`, `resolvable=true` in ops-warden |
| Read policy | `workload-kv-read-whynot-design-npm-publish` |
| Policy file | `openbao/policies/workload-kv-read-whynot-design-npm-publish.hcl` |
| OIDC auth mount | `netkingdom` |
@@ -57,6 +57,13 @@ Expected ops-warden exec shape after activation:
warden access whynot-design-npm-publish --exec -- npm publish
```
Ops-warden confirmed activation in State Hub message
`f76d3a9e-a98f-4081-885d-b79d94312699`: selector
`whynot-design-npm-publish` is active, resolvable, and wired to this
caller-scoped lane. The sibling lanes `issue-core-ingestion-api-key` and
`openrouter-llm-connect` remain draft and are tracked separately by
`RAILIANCE-WP-0009` and `RAILIANCE-WP-0010`.
The fetch command returns the secret value to the authenticated caller. Run it
only in an attended shell or through a process that consumes the value without
logging it.