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

@@ -25,8 +25,13 @@ ARGOCD_BOOTSTRAP_DIR ?= argocd/bootstrap
ARGOCD_REPOSITORY_SECRET ?=
CREDENTIAL_GRANTS ?= credential-grants/catalog.yaml
CREDENTIAL_CHANGE ?= CCR-2026-0001
CREDENTIAL_CHANGE_EVIDENCE_ARGS ?=
CREDENTIAL_CHANGE_LIFECYCLE_ACTION ?= deactivate
CREDENTIAL_CHANGE_LIFECYCLE_ARGS ?=
CREDENTIAL_CHANGE_IMPORT_ARGS ?=
STATE_HUB_URL ?= http://127.0.0.1:8000
OPENBAO_TOKEN_GRANT_ARGS ?=
OPENBAO_CREDENTIAL_CHANGE_APPLIER_ARGS ?=
OPENBAO_WORKLOAD_KV_ARGS ?=
CREDENTIAL_HELPER_GLOBAL_ARGS ?=
CREDENTIAL_HELPER_ARGS ?=
@@ -171,6 +176,9 @@ openbao-configure-external-secrets-issue-core: ## Configure OpenBao policy/role
OPENBAO_RELEASE=$(OPENBAO_RELEASE) ESO_NAMESPACE=$(EXTERNAL_SECRETS_NAMESPACE) \
scripts/openbao-apply-external-secrets-issue-core.sh
openbao-configure-external-secrets-activity-core: ## Configure OpenBao policy/role for activity-core ESO lane
KUBECTL='$(KUBECTL)' OPENBAO_NAMESPACE=$(OPENBAO_NAMESPACE) OPENBAO_RELEASE=$(OPENBAO_RELEASE) ESO_NAMESPACE=$(EXTERNAL_SECRETS_NAMESPACE) OPENBAO_ESO_ROLE=external-secrets-activity-core OPENBAO_ESO_POLICY=workload-kv-read-llm-connect-provider-secrets POLICY_FILE='$(CURDIR)/openbao/policies/workload-kv-read-llm-connect-provider-secrets.hcl' scripts/openbao-apply-external-secrets-issue-core.sh
openbao-workload-kv-lanes-dry-run: ## Dry-run OpenBao workload KV read-lane policy apply
scripts/openbao-apply-workload-kv-lanes.sh --dry-run $(OPENBAO_WORKLOAD_KV_ARGS)
@@ -201,6 +209,9 @@ credential-change-render: ## Render a credential change request review summary
credential-change-plan: ## Render a credential change request apply plan for review
scripts/credential-change.py plan $(CREDENTIAL_CHANGE)
credential-change-decision-templates: ## Render CCR approve/deny/needs-changes templates
scripts/credential-change.py decision-templates $(CREDENTIAL_CHANGE)
credential-change-status: ## Render credential change request readiness status
scripts/credential-change.py status $(CREDENTIAL_CHANGE)
@@ -216,6 +227,38 @@ credential-change-apply-plan: ## Render approved-only operator apply plan
credential-change-operator-commands: ## Render approved-only non-secret OpenBao operator commands
scripts/credential-change.py operator-commands $(CREDENTIAL_CHANGE)
credential-change-applier-dry-run: ## Validate delegated OpenBao metadata mutations for a CCR
scripts/credential-change.py applier-dry-run $(CREDENTIAL_CHANGE)
credential-change-applier-apply-plan: ## Render delegated OpenBao metadata apply plan
scripts/credential-change.py applier-apply $(CREDENTIAL_CHANGE) --plan-only
credential-change-applier-apply: ## Apply delegated metadata; pass confirmation/actor args via CREDENTIAL_CHANGE_EVIDENCE_ARGS
scripts/credential-change.py applier-apply $(CREDENTIAL_CHANGE) $(CREDENTIAL_CHANGE_EVIDENCE_ARGS)
credential-change-runbook: ## Render the attended CCR apply/verify runbook
scripts/credential-change.py runbook $(CREDENTIAL_CHANGE)
credential-change-record-evidence: ## Record non-secret CCR evidence; pass CREDENTIAL_CHANGE_EVIDENCE_ARGS
scripts/credential-change.py record-evidence $(CREDENTIAL_CHANGE) $(CREDENTIAL_CHANGE_EVIDENCE_ARGS)
credential-change-lifecycle-plan: ## Render deactivation/rotation/compromise lifecycle guidance
scripts/credential-change.py lifecycle-plan $(CREDENTIAL_CHANGE) --action $(CREDENTIAL_CHANGE_LIFECYCLE_ACTION)
credential-change-lifecycle-event: ## Record lifecycle event; pass CREDENTIAL_CHANGE_LIFECYCLE_ARGS
scripts/credential-change.py lifecycle-event $(CREDENTIAL_CHANGE) --action $(CREDENTIAL_CHANGE_LIFECYCLE_ACTION) $(CREDENTIAL_CHANGE_LIFECYCLE_ARGS)
credential-change-import-inventory: ## Import existing lane as non-secret CCR; pass CREDENTIAL_CHANGE_IMPORT_ARGS
scripts/credential-change.py import-inventory $(CREDENTIAL_CHANGE_IMPORT_ARGS)
openbao-credential-change-appliers-dry-run: ## Dry-run credential-change applier policies/token roles
scripts/openbao-apply-credential-change-appliers.py --dry-run $(OPENBAO_CREDENTIAL_CHANGE_APPLIER_ARGS)
openbao-configure-credential-change-appliers: ## Apply credential-change applier policies/token roles
KUBECTL='$(KUBECTL)' OPENBAO_NAMESPACE=$(OPENBAO_NAMESPACE) \
OPENBAO_RELEASE=$(OPENBAO_RELEASE) \
scripts/openbao-apply-credential-change-appliers.py $(OPENBAO_CREDENTIAL_CHANGE_APPLIER_ARGS)
openbao-token-grants-dry-run: ## Dry-run OpenBao token roles and issuer policies for credential grants
scripts/openbao-apply-token-grants.py --dry-run $(OPENBAO_TOKEN_GRANT_ARGS)
@@ -297,4 +340,4 @@ help: ## Show this help
/^[a-zA-Z_-]+:.*?##/ { printf " \033[36m%-22s\033[0m %s\n", $$1, $$2 } \
/^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) }' $(MAKEFILE_LIST)
.PHONY: db-deploy db-status db-shell db-logs apps-pg-deploy apps-pg-status apps-pg-shell apps-pg-logs net-kingdom-pg-inter-hub-networkpolicy-deploy pg-deploy pg-status pg-pgpool-check valkey-deploy valkey-status openbao-repo openbao-dry-run openbao-overlay-apply openbao-verify-login-overlay openbao-deploy openbao-status openbao-verify openbao-verify-post-unseal openbao-configure-initial openbao-configure-ssh openbao-verify-ssh openbao-verify-authenticated openbao-configure-external-secrets-issue-core openbao-validate-restore-evidence openbao-validate-emergency-evidence credential-grants-validate openbao-token-grants-dry-run openbao-configure-token-grants openbao-verify-token-grants-dry-run openbao-verify-token-grants openbao-verify-token-grants-smoke credential-helper-dry-run credential-tests credential-exec-ops-warden-smoke argocd-bootstrap-dry-run argocd-bootstrap-deploy argocd-repo-apply argocd-status backup help
.PHONY: db-deploy db-status db-shell db-logs apps-pg-deploy apps-pg-status apps-pg-shell apps-pg-logs net-kingdom-pg-inter-hub-networkpolicy-deploy pg-deploy pg-status pg-pgpool-check valkey-deploy valkey-status openbao-repo openbao-dry-run openbao-overlay-apply openbao-verify-login-overlay openbao-deploy openbao-status openbao-verify openbao-verify-post-unseal openbao-configure-initial openbao-configure-ssh openbao-verify-ssh openbao-verify-authenticated openbao-configure-external-secrets-issue-core openbao-configure-external-secrets-activity-core openbao-validate-restore-evidence openbao-validate-emergency-evidence credential-grants-validate credential-change-applier-dry-run credential-change-applier-apply-plan credential-change-applier-apply credential-change-runbook credential-change-record-evidence credential-change-lifecycle-plan credential-change-lifecycle-event credential-change-import-inventory openbao-credential-change-appliers-dry-run openbao-configure-credential-change-appliers openbao-token-grants-dry-run openbao-configure-token-grants openbao-verify-token-grants-dry-run openbao-verify-token-grants openbao-verify-token-grants-smoke credential-helper-dry-run credential-tests credential-exec-ops-warden-smoke argocd-bootstrap-dry-run argocd-bootstrap-deploy argocd-repo-apply argocd-status backup help

View File

@@ -3,3 +3,4 @@ kind: Kustomization
resources:
- openbao.clustersecretstore.yaml
- openbao-activity-core.clustersecretstore.yaml

View File

@@ -0,0 +1,23 @@
apiVersion: external-secrets.io/v1beta1
kind: ClusterSecretStore
metadata:
name: openbao-activity-core
labels:
app.kubernetes.io/part-of: railiance-gitops
railiance-platform/component: external-secrets
spec:
provider:
vault:
server: http://openbao.openbao.svc.cluster.local:8200
path: platform
version: v2
auth:
kubernetes:
mountPath: kubernetes
role: external-secrets-activity-core
serviceAccountRef:
name: external-secrets
namespace: external-secrets
conditions:
- namespaces:
- activity-core

View File

@@ -2,79 +2,104 @@ id: CCR-2026-0002
kind: credential-change-request
schema_version: 1
request_type: workload-kv-read
title: "issue-core runtime ingestion key lane"
title: issue-core runtime ingestion key lane
status: proposed
created: "2026-06-27"
updated: "2026-06-27"
created: '2026-06-27'
updated: '2026-06-30'
requester:
agent: ops-warden
message_id: "fe5b1696-8956-4bd5-9d6f-dbde1901a076"
reason: "Confirm and provision the issue-core workload KV lane requested in the ops-warden batch."
message_id: fe5b1696-8956-4bd5-9d6f-dbde1901a076
reason: Confirm and provision the issue-core workload KV lane requested in the ops-warden
batch.
review:
required: true
required_approvers:
- platform-operator
- issue-core-owner
comments: []
- platform-operator
- issue-core-owner
comments:
- at: '2026-06-29T22:53:03+00:00'
reviewer: codex
decision: metadata_review_binding_confirmed
comment: Live cluster metadata on 2026-06-30 confirms ExternalSecret issue-core/issue-core-runtime
is Ready=True (SecretSynced) and maps ISSUE_CORE_API_KEY plus GITEA_BACKEND_TOKEN
from platform/workloads/issue-core/issue-core/issue-core-runtime. The workload
Deployment uses the default service account; OpenBao auth for this delivery
path is the platform ClusterSecretStore/openbao role external-secrets-issue-core
bound to service account external-secrets/external-secrets. Keep CCR status
proposed until platform/operator and issue-core-owner approval.
target:
domain: financials
tenant: issue-core
workload: issue-core
environment: production
purpose: "issue-core runtime ingestion through OpenBao workload KV and External Secrets"
purpose: issue-core runtime ingestion through OpenBao workload KV and External Secrets
openbao:
mount: platform
kv_path: platform/workloads/issue-core/issue-core/issue-core-runtime
fields:
- ISSUE_CORE_API_KEY
- GITEA_BACKEND_TOKEN
- ISSUE_CORE_API_KEY
- GITEA_BACKEND_TOKEN
policy_name: workload-kv-read-issue-core-runtime
policy_file: openbao/policies/workload-kv-read-issue-core-runtime.hcl
auth:
method: kubernetes
mount: kubernetes
role: issue-core-runtime-workload-kv-read
role: external-secrets-issue-core
bound_claims:
service_account_names:
- issue-core
- external-secrets
service_account_namespaces:
- issue-core
bound_claims_confirmed: false
- external-secrets
bound_claims_confirmed: true
policies:
- workload-kv-read-issue-core-runtime
- workload-kv-read-issue-core-runtime
ttl: 15m
access_frontdoor:
type: ops-warden
catalog_id: issue-core-ingestion-api-key
selector: "issue-core ingestion API key"
command: "warden access issue-core-ingestion-api-key --fetch ISSUE_CORE_API_KEY"
selector: issue-core ingestion API key
command: warden access issue-core-ingestion-api-key --fetch ISSUE_CORE_API_KEY
resolvable: false
readiness: template
activation: "draft-until-ccr-verified"
activation: draft-until-ccr-verified
delivery:
surface: external-secrets
target: "issue-core namespace"
target: ExternalSecret issue-core/issue-core-runtime -> Secret issue-core-runtime
in the issue-core namespace
risk:
classification: high
notes:
- "Grants read access to issue-core runtime ingestion credentials."
- "GITEA_BACKEND_TOKEN is included because ops-warden asked to confirm whether it is used; remove it before approval if issue-core does not require it."
- "The Kubernetes service account and namespace binding must be confirmed before apply."
- "ops-warden must proxy reads as the caller and must not retain token values."
- Grants read access to issue-core runtime ingestion credentials through the platform
External Secrets path.
- GITEA_BACKEND_TOKEN remains included because the live issue-core ExternalSecret
maps it alongside ISSUE_CORE_API_KEY; remove it before approval only if the issue-core
owner confirms it is no longer required.
- The Kubernetes auth subject is the External Secrets operator service account external-secrets/external-secrets,
with ClusterSecretStore usage limited to the issue-core namespace.
- ops-warden must proxy reads as the caller and must not retain token values.
verification:
positive:
- "Approved issue-core service account can read the configured fields through OpenBao or External Secrets without printing values."
- ExternalSecret issue-core/issue-core-runtime is Ready=True and syncs the configured
fields without printing values.
- Approved issue-core runtime can consume the resulting Kubernetes Secret without
exposing values.
negative:
- "A service account outside the approved issue-core binding cannot read the path."
- A namespace outside the approved ClusterSecretStore condition cannot use this
store to read the path.
- A service account outside external-secrets/external-secrets cannot authenticate
through the External Secrets OpenBao role.
activation_conditions:
- "Policy applied with platform-admin/operator authority."
- "Kubernetes auth role bound to the confirmed issue-core service account and namespace."
- "Secret values provisioned directly in OpenBao through approved operator custody."
- "Positive and negative verification recorded with non-secret audit ids or timestamps."
- Policy applied with platform-admin/operator authority.
- Kubernetes auth role bound to external-secrets/external-secrets for the issue-core
External Secrets delivery path.
- Secret values provisioned directly in OpenBao through approved operator custody.
- Positive and negative verification recorded with non-secret audit ids or timestamps.
lifecycle:
deactivate: "Disable ops-warden catalog entry and remove or detach auth role policy."
rotate: "Replace issue-core runtime secret values directly in OpenBao and record non-secret rotation evidence."
compromised: "Immediately deactivate access front door, rotate affected values, record blast-radius notes, and open incident follow-up tasks."
deactivate: Disable ops-warden catalog entry and remove or detach auth role policy.
rotate: Replace issue-core runtime secret values directly in OpenBao and record
non-secret rotation evidence.
compromised: Immediately deactivate access front door, rotate affected values, record
blast-radius notes, and open incident follow-up tasks.
state_hub:
workplan_id: RAILIANCE-WP-0007
ops_warden_batch_message_id: "fe5b1696-8956-4bd5-9d6f-dbde1901a076"
ops_warden_batch_message_id: fe5b1696-8956-4bd5-9d6f-dbde1901a076

View File

@@ -2,77 +2,101 @@ id: CCR-2026-0003
kind: credential-change-request
schema_version: 1
request_type: workload-kv-read
title: "llm-connect OpenRouter provider key lane"
title: llm-connect OpenRouter provider key lane
status: proposed
created: "2026-06-27"
updated: "2026-06-27"
created: '2026-06-27'
updated: '2026-06-30'
requester:
agent: ops-warden
message_id: "fe5b1696-8956-4bd5-9d6f-dbde1901a076"
reason: "Confirm and provision the llm-connect OpenRouter workload KV lane requested in the ops-warden batch."
message_id: fe5b1696-8956-4bd5-9d6f-dbde1901a076
reason: Confirm and provision the llm-connect OpenRouter workload KV lane requested
in the ops-warden batch.
review:
required: true
required_approvers:
- platform-operator
- activity-core-owner
comments: []
- platform-operator
- activity-core-owner
comments:
- at: '2026-06-29T22:53:03+00:00'
reviewer: codex
decision: metadata_review_binding_confirmed_pending_owner_approval
comment: Live cluster metadata on 2026-06-30 confirms namespace activity-core
exists and the External Secrets operator service account external-secrets/external-secrets
exists. The proposed llm-connect service account does not exist; the llm-connect
Deployment currently uses the default service account. Updated the proposed
OpenBao auth binding to the platform ESO pattern with role external-secrets-activity-core.
No activity-core ExternalSecret exists yet; a namespace-limited ClusterSecretStore
source manifest was added for future rollout. Keep CCR status proposed until
platform/operator and activity-core-owner approval.
target:
domain: financials
tenant: activity-core
workload: llm-connect
environment: production
purpose: "llm-connect provider access through OpenBao workload KV and External Secrets"
purpose: llm-connect provider access through OpenBao workload KV and External Secrets
openbao:
mount: platform
kv_path: platform/workloads/activity-core/llm-connect/llm-connect-provider-secrets
fields:
- OPENROUTER_API_KEY
- OPENROUTER_API_KEY
policy_name: workload-kv-read-llm-connect-provider-secrets
policy_file: openbao/policies/workload-kv-read-llm-connect-provider-secrets.hcl
auth:
method: kubernetes
mount: kubernetes
role: llm-connect-provider-secrets-read
role: external-secrets-activity-core
bound_claims:
service_account_names:
- llm-connect
- external-secrets
service_account_namespaces:
- activity-core
bound_claims_confirmed: false
- external-secrets
bound_claims_confirmed: true
policies:
- workload-kv-read-llm-connect-provider-secrets
- workload-kv-read-llm-connect-provider-secrets
ttl: 15m
access_frontdoor:
type: ops-warden
catalog_id: llm-connect-openrouter-api-key
selector: "llm-connect OpenRouter API key"
command: "warden access llm-connect-openrouter-api-key --fetch OPENROUTER_API_KEY"
selector: llm-connect OpenRouter API key
command: warden access llm-connect-openrouter-api-key --fetch OPENROUTER_API_KEY
resolvable: false
readiness: template
activation: "draft-until-ccr-verified"
activation: draft-until-ccr-verified
delivery:
surface: external-secrets
target: "Secret llm-connect-provider-secrets in the activity-core namespace"
target: ExternalSecret to Secret llm-connect-provider-secrets in the activity-core
namespace
risk:
classification: high
notes:
- "Grants read access to the provider key used by llm-connect for OpenRouter requests."
- "The Kubernetes service account and namespace binding must be confirmed before apply."
- "ops-warden must proxy reads as the caller and must not retain token values."
- Grants read access to the provider key used by llm-connect for OpenRouter requests
through the platform External Secrets path.
- The Kubernetes auth subject is the External Secrets operator service account external-secrets/external-secrets,
with ClusterSecretStore usage limited to the activity-core namespace.
- ops-warden must proxy reads as the caller and must not retain token values.
verification:
positive:
- "Approved llm-connect service account can read field OPENROUTER_API_KEY through OpenBao or External Secrets without printing the value."
- An approved activity-core ExternalSecret can sync field OPENROUTER_API_KEY to
Secret llm-connect-provider-secrets without printing the value.
- The llm-connect runtime can consume the resulting Kubernetes Secret without exposing
values.
negative:
- "A service account outside the approved activity-core/llm-connect binding cannot read the path."
- A namespace outside the approved ClusterSecretStore condition cannot use this
store to read the path.
- A service account outside external-secrets/external-secrets cannot authenticate
through the External Secrets OpenBao role.
activation_conditions:
- "Policy applied with platform-admin/operator authority."
- "Kubernetes auth role bound to the confirmed llm-connect service account and namespace."
- "Secret value provisioned directly in OpenBao through approved operator custody."
- "Positive and negative verification recorded with non-secret audit ids or timestamps."
- Policy applied with platform-admin/operator authority.
- Kubernetes auth role bound to external-secrets/external-secrets for the activity-core
External Secrets delivery path.
- Secret value provisioned directly in OpenBao through approved operator custody.
- Positive and negative verification recorded with non-secret audit ids or timestamps.
lifecycle:
deactivate: "Disable ops-warden catalog entry and remove or detach auth role policy."
rotate: "Replace OPENROUTER_API_KEY directly in OpenBao and record non-secret rotation evidence."
compromised: "Immediately deactivate access front door, rotate the provider key, record blast-radius notes, and open incident follow-up tasks."
deactivate: Disable ops-warden catalog entry and remove or detach auth role policy.
rotate: Replace OPENROUTER_API_KEY directly in OpenBao and record non-secret rotation
evidence.
compromised: Immediately deactivate access front door, rotate the provider key,
record blast-radius notes, and open incident follow-up tasks.
state_hub:
workplan_id: RAILIANCE-WP-0007
ops_warden_batch_message_id: "fe5b1696-8956-4bd5-9d6f-dbde1901a076"
ops_warden_batch_message_id: fe5b1696-8956-4bd5-9d6f-dbde1901a076

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.

View File

@@ -0,0 +1,40 @@
# Non-production metadata applier for reviewed credential changes.
#
# This policy is intended for build/test/staging OpenBao lanes after a
# non-production mount or namespace is declared. It intentionally keeps the same
# no-secret-value rule as production; generated test secrets still require a
# separately approved non-production value path.
# Workload KV read-lane policies generated from approved CCRs.
path "sys/policies/acl/workload-kv-read-*" {
capabilities = ["create", "update", "read"]
}
# Credential broker issuer policies generated from approved grant metadata.
path "sys/policies/acl/credential-broker-*-issuer" {
capabilities = ["create", "update", "read"]
}
# OIDC roles for caller-scoped workload KV lanes.
path "auth/netkingdom/role/*-workload-kv-read" {
capabilities = ["create", "update", "read"]
}
# Kubernetes roles for in-cluster workload and provider-secret lanes.
path "auth/kubernetes/role/*" {
capabilities = ["create", "update", "read"]
}
# Token roles for approved credential-broker child-token issuers.
path "auth/token/roles/credential-broker-*" {
capabilities = ["create", "update", "read"]
}
# Self-checks and capability introspection only.
path "auth/token/lookup-self" {
capabilities = ["read"]
}
path "sys/capabilities-self" {
capabilities = ["update"]
}

View File

@@ -0,0 +1,41 @@
# Production metadata applier for reviewed credential changes.
#
# This policy intentionally permits only non-secret OpenBao metadata writes for
# approved CCRs. Secret value paths under platform/data are not granted here.
# The local credential-change applier-dry-run command must validate the CCR
# before this policy is used for any live mutation.
# Workload KV read-lane policies generated from approved CCRs.
path "sys/policies/acl/workload-kv-read-*" {
capabilities = ["create", "update", "read"]
}
# Credential broker issuer policies generated from approved grant metadata.
path "sys/policies/acl/credential-broker-*-issuer" {
capabilities = ["create", "update", "read"]
}
# OIDC roles for caller-scoped workload KV lanes.
path "auth/netkingdom/role/*-workload-kv-read" {
capabilities = ["create", "update", "read"]
}
# Kubernetes roles for in-cluster workload and provider-secret lanes. The local
# applier dry-run constrains role names and bound service accounts per CCR.
path "auth/kubernetes/role/*" {
capabilities = ["create", "update", "read"]
}
# Token roles for approved credential-broker child-token issuers.
path "auth/token/roles/credential-broker-*" {
capabilities = ["create", "update", "read"]
}
# Self-checks and capability introspection only.
path "auth/token/lookup-self" {
capabilities = ["read"]
}
path "sys/capabilities-self" {
capabilities = ["update"]
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,217 @@
#!/usr/bin/env python3
from __future__ import annotations
import argparse
import getpass
import os
import shlex
import subprocess
import sys
from pathlib import Path
from typing import Any
REPO_DIR = Path(__file__).resolve().parents[1]
APPLIERS: dict[str, dict[str, Any]] = {
"nonprod": {
"title": "Credential change non-production metadata applier",
"policy_name": "credential-change-nonprod-applier",
"policy_file": "openbao/policies/credential-change-nonprod-applier.hcl",
"token_role": "credential-change-nonprod-applier",
"max_ttl": "1h",
},
"prod": {
"title": "Credential change production metadata applier",
"policy_name": "credential-change-prod-applier",
"policy_file": "openbao/policies/credential-change-prod-applier.hcl",
"token_role": "credential-change-prod-applier",
"max_ttl": "30m",
},
}
DISALLOWED_POLICIES = ("root", "platform-admin")
class BaoRunner:
def __init__(
self,
*,
kubectl: str,
namespace: str,
release: str,
dry_run: bool,
use_token_helper: bool,
token: str | None,
) -> None:
self.kubectl_parts = shlex.split(kubectl)
self.namespace = namespace
self.pod = f"{release}-0"
self.dry_run = dry_run
self.use_token_helper = use_token_helper
self.token = token
def run(
self, args: list[str], input_text: str | None = None
) -> subprocess.CompletedProcess[str]:
rendered = "bao " + shlex.join(args)
if self.dry_run:
print(f"DRY-RUN: {rendered}")
return subprocess.CompletedProcess(args, 0, "", "")
if self.use_token_helper:
cmd = (
self.kubectl_parts
+ ["exec", "-i", "-n", self.namespace, self.pod, "--", "bao"]
+ args
)
proc_input = input_text
else:
if not self.token:
raise RuntimeError(
"OpenBao token is required unless --use-token-helper is set"
)
cmd = (
self.kubectl_parts
+ [
"exec",
"-i",
"-n",
self.namespace,
self.pod,
"--",
"sh",
"-c",
'read -r BAO_TOKEN; export BAO_TOKEN; exec bao "$@"',
"sh",
]
+ args
)
proc_input = self.token + "\n" + (input_text or "")
result = subprocess.run(cmd, input=proc_input, capture_output=True, text=True)
if result.stdout:
print(result.stdout, end="")
if result.returncode != 0:
if result.stderr:
print(result.stderr, file=sys.stderr, end="")
raise SystemExit(result.returncode)
if result.stderr:
print(result.stderr, file=sys.stderr, end="")
return result
def read_token(
token_file: str | None, dry_run: bool, use_token_helper: bool
) -> str | None:
if dry_run or use_token_helper:
return None
if token_file:
path = Path(token_file)
if not path.exists():
raise SystemExit(f"ERROR: OPENBAO_TOKEN_FILE does not exist: {path}")
lines = path.read_text(encoding="utf-8").splitlines()
token = lines[0].strip() if lines else ""
if not token:
raise SystemExit(f"ERROR: OPENBAO_TOKEN_FILE is empty: {path}")
return token
token = getpass.getpass("OpenBao token: ")
if not token:
raise SystemExit("ERROR: empty OpenBao token")
return token
def selected_appliers(selector: str) -> list[dict[str, Any]]:
if selector == "all":
return [APPLIERS["nonprod"], APPLIERS["prod"]]
try:
return [APPLIERS[selector]]
except KeyError:
raise SystemExit(f"ERROR: applier must be one of {sorted(APPLIERS) + ['all']}")
def role_args(applier: dict[str, Any]) -> list[str]:
return [
"write",
f"auth/token/roles/{applier['token_role']}",
f"allowed_policies={applier['policy_name']}",
f"disallowed_policies={','.join(DISALLOWED_POLICIES)}",
"orphan=true",
"renewable=false",
f"token_explicit_max_ttl={applier['max_ttl']}",
"token_no_default_policy=true",
"token_type=service",
]
def write_policy(runner: BaoRunner, applier: dict[str, Any], policy_dir: Path) -> None:
policy_file = policy_dir / Path(applier["policy_file"]).name
if not policy_file.exists():
raise SystemExit(f"ERROR: missing policy file: {policy_file}")
if runner.dry_run:
print(f"DRY-RUN: bao policy write {applier['policy_name']} {policy_file}")
return
runner.run(
["policy", "write", applier["policy_name"], "-"],
input_text=policy_file.read_text(encoding="utf-8"),
)
print(f"OK: policy {applier['policy_name']} applied")
def apply_applier(runner: BaoRunner, applier: dict[str, Any], policy_dir: Path) -> None:
write_policy(runner, applier, policy_dir)
runner.run(role_args(applier))
runner.run(["read", f"auth/token/roles/{applier['token_role']}"])
print(
"OK: applier role "
f"{applier['token_role']} configured for policy {applier['policy_name']}"
)
def main() -> int:
parser = argparse.ArgumentParser(
description="Apply OpenBao credential-change delegated applier policies and token roles."
)
parser.add_argument(
"--applier", choices=["nonprod", "prod", "all"], default="all"
)
parser.add_argument("--policy-dir", default="openbao/policies")
parser.add_argument("--dry-run", action="store_true")
parser.add_argument(
"--use-token-helper",
action="store_true",
help="Use the OpenBao CLI token helper inside the pod",
)
parser.add_argument("--namespace", default=None)
parser.add_argument("--release", default=None)
parser.add_argument("--kubectl", default=None)
args = parser.parse_args()
namespace = args.namespace or os.environ.get("OPENBAO_NAMESPACE", "openbao")
release = args.release or os.environ.get("OPENBAO_RELEASE", "openbao")
kubectl = args.kubectl or os.environ.get("KUBECTL", "kubectl")
token_file = os.environ.get("OPENBAO_TOKEN_FILE")
token = read_token(token_file, args.dry_run, args.use_token_helper)
runner = BaoRunner(
kubectl=kubectl,
namespace=namespace,
release=release,
dry_run=args.dry_run,
use_token_helper=args.use_token_helper,
token=token,
)
if not args.dry_run:
runner.run(["status"])
policy_dir = REPO_DIR / args.policy_dir
for applier in selected_appliers(args.applier):
apply_applier(runner, applier, policy_dir)
print("NEXT: issue short-lived child tokens through an approved custody path only.")
print("NEXT: run scripts/credential-change.py applier-apply <CCR> with that ambient delegated authority.")
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -11,6 +11,9 @@ ESO_NAMESPACE="${ESO_NAMESPACE:-external-secrets}"
ESO_SERVICE_ACCOUNT="${ESO_SERVICE_ACCOUNT:-external-secrets}"
REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
POLICY_FILE="${POLICY_FILE:-$REPO_DIR/openbao/policies/external-secrets-issue-core.hcl}"
NEXT_KV_PATH="${OPENBAO_ESO_NEXT_PATH:-platform/workloads/issue-core/issue-core/issue-core-runtime}"
NEXT_FIELDS="${OPENBAO_ESO_NEXT_FIELDS:-ISSUE_CORE_API_KEY and GITEA_BACKEND_TOKEN}"
NEXT_TARGET="${OPENBAO_ESO_NEXT_TARGET:-ExternalSecret/issue-core-runtime}"
DRY_RUN=0
usage() {
@@ -125,13 +128,12 @@ remote_bao "$token" write "auth/kubernetes/role/${ROLE_NAME}" \
remote_bao "$token" read "auth/kubernetes/role/${ROLE_NAME}"
cat <<'NEXT'
cat <<NEXT
External Secrets OpenBao role configured.
Next steps:
1. Sync the external-secrets and openbao-secretstore ArgoCD Applications.
2. Provision platform/workloads/issue-core/issue-core/issue-core-runtime
with ISSUE_CORE_API_KEY and GITEA_BACKEND_TOKEN without printing values.
3. Confirm ExternalSecret/issue-core-runtime becomes Ready.
2. Provision ${NEXT_KV_PATH} with ${NEXT_FIELDS} without printing values.
3. Confirm ${NEXT_TARGET} becomes Ready.
NEXT

View File

@@ -35,10 +35,11 @@ class CredentialChangeTests(unittest.TestCase):
self.assertEqual(warnings, [])
self.assertTrue(ccr["openbao"]["auth"]["bound_claims_confirmed"])
def test_unconfirmed_sibling_ccr_keeps_bound_claim_warning(self) -> None:
_ccr, errors, warnings = credential_change.validate_ccr(self.issue_core)
def test_issue_core_ccr_has_confirmed_eso_binding(self) -> None:
ccr, errors, warnings = credential_change.validate_ccr(self.issue_core)
self.assertEqual(errors, [])
self.assertIn("bound claim is not confirmed", warnings[0])
self.assertEqual(warnings, [])
self.assertEqual(ccr["openbao"]["auth"]["role"], "external-secrets-issue-core")
def test_all_repo_ccrs_validate(self) -> None:
for path in sorted((REPO_DIR / "credential-change-requests").glob("*.yaml")):
@@ -125,8 +126,8 @@ class CredentialChangeTests(unittest.TestCase):
ccr, errors, _warnings = credential_change.validate_ccr(self.issue_core)
self.assertEqual(errors, [])
payload = credential_change.auth_payload(ccr)
self.assertEqual(payload["bound_service_account_names"], ["issue-core"])
self.assertEqual(payload["bound_service_account_namespaces"], ["issue-core"])
self.assertEqual(payload["bound_service_account_names"], ["external-secrets"])
self.assertEqual(payload["bound_service_account_namespaces"], ["external-secrets"])
self.assertNotIn("bound_claims", payload)
def test_oidc_auth_payload_includes_redirect_uris(self) -> None:
@@ -150,6 +151,63 @@ class CredentialChangeTests(unittest.TestCase):
with self.assertRaises(SystemExit):
credential_change.command_apply_plan(type("Args", (), {"ref": str(self.issue_core)})())
def test_plan_includes_source_artifact_diff_status(self) -> None:
ccr, errors, _warnings = credential_change.validate_ccr(self.sample)
self.assertEqual(errors, [])
rendered = credential_change.render_plan(ccr)
self.assertIn("Source artifact diff:", rendered)
self.assertIn("artifact status: matches", rendered)
def test_decision_templates_prefill_review_context(self) -> None:
ccr, errors, _warnings = credential_change.validate_ccr(self.sample)
self.assertEqual(errors, [])
rendered = credential_change.render_decision_templates(ccr)
self.assertIn("APPROVE: CCR-2026-0001", rendered)
self.assertIn("DENY: CCR-2026-0001", rendered)
self.assertIn("NEEDS_CHANGES: CCR-2026-0001", rendered)
self.assertIn("platform/workloads/coulomb/whynot-design/npm-publish", rendered)
self.assertIn("workload-kv-read-whynot-design-npm-publish", rendered)
self.assertIn("auth/netkingdom/role/whynot-design-workload-kv-read", rendered)
def test_invalid_state_hub_rationale_shows_templates(self) -> None:
ccr, errors, _warnings = credential_change.validate_ccr(self.sample)
self.assertEqual(errors, [])
with self.assertRaises(SystemExit) as raised:
credential_change.ccr_status_from_state_hub_rationale("looks good", ccr)
self.assertIn("APPROVE: CCR-2026-0001", str(raised.exception))
self.assertIn("NEEDS_CHANGES: CCR-2026-0001", str(raised.exception))
def test_decision_command_can_record_state_hub_event(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
copied = Path(tmp) / self.issue_core.name
shutil.copy2(self.issue_core, copied)
events = []
original = credential_change.state_hub_post_json
try:
credential_change.state_hub_post_json = (
lambda _base_url, _path, payload: events.append(payload) or {"id": "event-1"}
)
exit_code = credential_change.command_decision(
type(
"Args",
(),
{
"ref": str(copied),
"reviewer": "unit-test",
"comment": "scoped metadata looks correct",
"record_state_hub": True,
"state_hub_url": "http://state-hub.test",
},
)(),
"approved",
)
finally:
credential_change.state_hub_post_json = original
self.assertEqual(exit_code, 0)
self.assertEqual(events[0]["event_type"], "credential_change_decision")
self.assertIn("CCR-2026-0002", events[0]["summary"])
self.assertIn("ISSUE_CORE_API_KEY", events[0]["summary"])
def test_operator_commands_render_non_secret_policy_and_role_handoff(self) -> None:
ccr, errors, warnings = credential_change.validate_ccr(self.sample)
self.assertEqual(errors, [])
@@ -207,6 +265,9 @@ class CredentialChangeTests(unittest.TestCase):
credential_change.append_decision(
copied, "approved", "unit-test", "looks right"
)
copied_data = credential_change.load_yaml(copied)
copied_data["openbao"]["auth"]["bound_claims_confirmed"] = False
credential_change.dump_yaml(copied, copied_data)
ccr, errors, _warnings = credential_change.validate_ccr(copied)
self.assertEqual(errors, [])
self.assertEqual(ccr["status"], "approved")
@@ -241,6 +302,371 @@ class CredentialChangeTests(unittest.TestCase):
self.assertNotIn("*", policy)
self.assertNotIn("delete", policy)
def test_applier_dry_run_succeeds_for_active_ccr(self) -> None:
ccr, errors, warnings = credential_change.validate_ccr(self.sample)
self.assertEqual(errors, [])
self.assertEqual(warnings, [])
self.assertEqual(credential_change.applier_readiness_blockers(ccr), [])
payload = credential_change.applier_dry_run_payload(ccr, warnings)
self.assertEqual(payload["source_artifacts"]["policy"]["status"], "matches")
self.assertEqual(
payload["mutations"][0]["openbao_path"],
"sys/policies/acl/workload-kv-read-whynot-design-npm-publish",
)
self.assertEqual(
payload["mutations"][1]["openbao_path"],
"auth/netkingdom/role/whynot-design-workload-kv-read",
)
rendered = credential_change.render_applier_dry_run(payload)
self.assertIn("Allowed metadata mutations", rendered)
self.assertIn("secret value writes", rendered)
self.assertNotIn("<enter-through-approved-custody>", rendered)
def test_applier_dry_run_refuses_unapproved_ccr(self) -> None:
exit_code = credential_change.command_applier_dry_run(
type("Args", (), {"ref": str(self.issue_core), "json": False})()
)
self.assertEqual(exit_code, 1)
def test_applier_dry_run_rejects_out_of_policy_policy_name(self) -> None:
ccr, errors, _warnings = credential_change.validate_ccr(self.sample)
self.assertEqual(errors, [])
ccr["status"] = "approved"
ccr["openbao"]["policy_name"] = "platform-admin"
ccr["openbao"]["auth"]["policies"] = ["platform-admin"]
blockers = credential_change.applier_readiness_blockers(ccr)
self.assertTrue(
any("disallowed" in blocker for blocker in blockers),
blockers,
)
def test_applier_dry_run_rejects_out_of_policy_auth_role(self) -> None:
ccr, errors, _warnings = credential_change.validate_ccr(self.sample)
self.assertEqual(errors, [])
ccr["status"] = "approved"
ccr["openbao"]["auth"]["role"] = "platform-admin"
blockers = credential_change.applier_readiness_blockers(ccr)
self.assertTrue(
any("auth.role is disallowed" in blocker for blocker in blockers),
blockers,
)
def test_applier_dry_run_rejects_out_of_scope_mount_and_path(self) -> None:
ccr, errors, _warnings = credential_change.validate_ccr(self.sample)
self.assertEqual(errors, [])
ccr["status"] = "approved"
ccr["openbao"]["mount"] = "secret"
ccr["openbao"]["kv_path"] = "secret/platform-admin"
blockers = credential_change.applier_readiness_blockers(ccr)
self.assertIn("openbao.mount must be platform, got secret", blockers)
self.assertIn("openbao.kv_path must stay under platform/workloads/", blockers)
def test_nonprod_applier_policy_remains_metadata_only(self) -> None:
policy = (
REPO_DIR / "openbao/policies/credential-change-nonprod-applier.hcl"
).read_text(encoding="utf-8")
self.assertIn('path "sys/policies/acl/workload-kv-read-*"', policy)
self.assertIn('path "auth/kubernetes/role/*"', policy)
self.assertNotIn('path "platform/data/', policy)
self.assertNotIn('path "platform/metadata/', policy)
def test_applier_apply_plan_renders_confirmation(self) -> None:
ccr, errors, warnings = credential_change.validate_ccr(self.sample)
self.assertEqual(errors, [])
rendered = credential_change.render_applier_apply_plan(ccr, warnings)
self.assertIn("DELEGATED APPLY CCR-2026-0001", rendered)
self.assertIn("applier-apply CCR-2026-0001", rendered)
self.assertIn("secret value writes", rendered)
def test_applier_apply_refuses_unapproved_ccr(self) -> None:
exit_code = credential_change.command_applier_apply(
type(
"Args",
(),
{
"ref": str(self.issue_core),
"actor": "unit-test",
"confirm": None,
"bao_bin": "bao",
"plan_only": False,
"json": False,
"quiet": True,
"record_state_hub": False,
"state_hub_url": "http://state-hub.test",
},
)()
)
self.assertEqual(exit_code, 1)
def test_applier_apply_records_metadata_evidence(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
copied = Path(tmp) / self.sample.name
shutil.copy2(self.sample, copied)
ccr = credential_change.load_yaml(copied)
ccr["status"] = "approved"
ccr["access_frontdoor"]["readiness"] = "approved-pending-apply"
ccr["access_frontdoor"]["resolvable"] = False
credential_change.dump_yaml(copied, ccr)
calls = []
events = []
original_apply = credential_change.run_bao_metadata_apply
original_post = credential_change.state_hub_post_json
try:
credential_change.run_bao_metadata_apply = lambda ccr, bao_bin: calls.append((ccr["id"], bao_bin))
credential_change.state_hub_post_json = (
lambda _base_url, _path, payload: events.append(payload) or {"id": "event-1"}
)
exit_code = credential_change.command_applier_apply(
type(
"Args",
(),
{
"ref": str(copied),
"actor": "unit-test",
"confirm": "DELEGATED APPLY CCR-2026-0001",
"bao_bin": "bao-test",
"plan_only": False,
"json": False,
"quiet": True,
"record_state_hub": True,
"state_hub_url": "http://state-hub.test",
},
)()
)
finally:
credential_change.run_bao_metadata_apply = original_apply
credential_change.state_hub_post_json = original_post
self.assertEqual(exit_code, 0)
self.assertEqual(calls, [("CCR-2026-0001", "bao-test")])
updated = credential_change.load_yaml(copied)
self.assertEqual(updated["status"], "applied")
self.assertEqual(updated["verification"]["evidence"][-1]["kind"], "delegated_metadata_apply")
self.assertEqual(events[0]["event_type"], "credential_change_evidence")
self.assertIn("delegated_metadata_apply", events[0]["summary"])
def test_applier_apply_requires_exact_confirmation(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
copied = Path(tmp) / self.sample.name
shutil.copy2(self.sample, copied)
ccr = credential_change.load_yaml(copied)
ccr["status"] = "approved"
credential_change.dump_yaml(copied, ccr)
with self.assertRaises(SystemExit):
credential_change.command_applier_apply(
type(
"Args",
(),
{
"ref": str(copied),
"actor": "unit-test",
"confirm": "apply it",
"bao_bin": "bao",
"plan_only": False,
"json": False,
"quiet": True,
"record_state_hub": False,
"state_hub_url": "http://state-hub.test",
},
)()
)
def test_runbook_renders_apply_verify_guidance(self) -> None:
ccr, errors, warnings = credential_change.validate_ccr(self.sample)
self.assertEqual(errors, [])
payload = credential_change.runbook_payload(ccr, warnings)
rendered = credential_change.render_runbook(payload)
self.assertIn("APPLY CCR-2026-0001", rendered)
self.assertIn("runbook <CCR> --execute-metadata", rendered)
self.assertIn("record-evidence <CCR>", rendered)
self.assertIn("Field presence checked without printing values", rendered)
self.assertNotIn("npm_", rendered)
def test_runbook_refuses_unapproved_ccr(self) -> None:
exit_code = credential_change.command_runbook(
type(
"Args",
(),
{
"ref": str(self.issue_core),
"json": False,
"execute_metadata": False,
"actor": "unit-test",
"confirm": None,
"bao_bin": "bao",
"record_state_hub": False,
"state_hub_url": "http://state-hub.test",
},
)()
)
self.assertEqual(exit_code, 1)
def test_record_evidence_appends_non_secret_entry_and_status(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
copied = Path(tmp) / self.sample.name
shutil.copy2(self.sample, copied)
ccr = credential_change.load_yaml(copied)
ccr["status"] = "approved"
ccr["access_frontdoor"]["readiness"] = "approved-pending-apply"
ccr["access_frontdoor"]["resolvable"] = False
credential_change.dump_yaml(copied, ccr)
updated = credential_change.append_evidence(
copied,
"unit-test",
"metadata_apply",
"passed",
["OpenBao audit timestamp recorded without secret values"],
set_status="applied",
)
self.assertEqual(updated["status"], "applied")
self.assertEqual(updated["verification"]["evidence"][-1]["kind"], "metadata_apply")
self.assertEqual(
updated["verification"]["evidence"][-1]["details"],
["OpenBao audit timestamp recorded without secret values"],
)
def test_record_evidence_can_mark_frontdoor_ready(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
copied = Path(tmp) / self.sample.name
shutil.copy2(self.sample, copied)
updated = credential_change.append_evidence(
copied,
"unit-test",
"frontdoor_activation",
"passed",
["Catalog readiness checked without secret values"],
set_status="active",
frontdoor_ready=True,
)
self.assertEqual(updated["status"], "active")
self.assertEqual(updated["access_frontdoor"]["readiness"], "ready")
self.assertTrue(updated["access_frontdoor"]["resolvable"])
def test_record_evidence_rejects_secret_markers(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
copied = Path(tmp) / self.sample.name
shutil.copy2(self.sample, copied)
with self.assertRaises(SystemExit):
credential_change.append_evidence(
copied,
"unit-test",
"positive_verification",
"passed",
["accidentally pasted sk-test"],
)
def test_lifecycle_plan_renders_deactivation_steps(self) -> None:
ccr, errors, _warnings = credential_change.validate_ccr(self.sample)
self.assertEqual(errors, [])
payload = credential_change.lifecycle_payload(ccr, "deactivate")
rendered = credential_change.render_lifecycle_plan(payload)
self.assertIn("lifecycle plan: deactivate", rendered)
self.assertIn("readiness=disabled resolvable=False", rendered)
self.assertIn("bao delete auth/netkingdom/role/whynot-design-workload-kv-read", rendered)
self.assertIn("bao policy delete workload-kv-read-whynot-design-npm-publish", rendered)
self.assertNotIn("NPM_AUTH_TOKEN=", rendered)
def test_lifecycle_event_marks_deactivated_and_disables_frontdoor(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
copied = Path(tmp) / self.sample.name
shutil.copy2(self.sample, copied)
updated = credential_change.append_lifecycle_event(
copied,
"unit-test",
"deactivate",
"No longer needed",
["Front door disabled in catalog"],
)
self.assertEqual(updated["status"], "deactivated")
self.assertEqual(updated["access_frontdoor"]["readiness"], "disabled")
self.assertFalse(updated["access_frontdoor"]["resolvable"])
self.assertEqual(updated["lifecycle"]["events"][-1]["action"], "deactivate")
def test_lifecycle_event_records_compromise_blast_radius_and_follow_up(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
copied = Path(tmp) / self.sample.name
shutil.copy2(self.sample, copied)
updated = credential_change.append_lifecycle_event(
copied,
"unit-test",
"compromise",
"Unexpected exposure signal",
["Access disabled before rotation"],
blast_radius=["npm publishing lane only"],
follow_up=["incident-task-1"],
)
event = updated["lifecycle"]["events"][-1]
self.assertEqual(updated["status"], "compromised")
self.assertEqual(updated["access_frontdoor"]["readiness"], "compromised")
self.assertEqual(event["blast_radius"], ["npm publishing lane only"])
self.assertEqual(event["follow_up"], ["incident-task-1"])
def test_lifecycle_event_rejects_secret_markers(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
copied = Path(tmp) / self.sample.name
shutil.copy2(self.sample, copied)
with self.assertRaises(SystemExit):
credential_change.append_lifecycle_event(
copied,
"unit-test",
"rotate",
"accidentally pasted ghp_bad",
["rotation needed"],
)
def test_import_inventory_writes_non_secret_ccr_and_policy(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
tmp_path = Path(tmp)
output_dir = tmp_path / "ccrs"
policy_file = tmp_path / "policies" / "workload-kv-read-imported-lane.hcl"
args = type(
"Args",
(),
{
"id": "CCR-2099-0001",
"title": "imported lane",
"tenant": "coulomb",
"workload": "imported",
"environment": "production",
"purpose": "runtime token",
"mount": "platform",
"kv_path": "platform/workloads/coulomb/imported/runtime-token",
"field": ["RUNTIME_TOKEN"],
"policy_name": "workload-kv-read-imported-lane",
"policy_file": str(policy_file),
"auth_method": "oidc",
"auth_mount": "netkingdom",
"auth_role": "imported-workload-kv-read",
"bound_claim": ["groups=imported"],
"service_account": None,
"service_account_namespace": None,
"bound_claims_confirmed": True,
"ttl": "15m",
"frontdoor_type": "ops-warden",
"catalog_id": "imported-runtime-token",
"selector": None,
"command": None,
"status": "active",
"readiness": "ready",
"resolvable": True,
"risk": "high",
"positive_check": "Authorized caller can fetch RUNTIME_TOKEN with output suppressed.",
"negative_check": "Unauthorized caller cannot read the imported path.",
"requester_agent": "unit-test",
"actor": "unit-test",
"reason": "Imported existing lane without secret values",
"output_dir": str(output_dir),
"write_policy": True,
},
)()
path = credential_change.write_inventory_ccr(args)
self.assertTrue(path.exists())
self.assertTrue(policy_file.exists())
ccr, errors, warnings = credential_change.validate_ccr(path)
self.assertEqual(errors, [])
self.assertEqual(warnings, [])
self.assertEqual(ccr["openbao"]["fields"], ["RUNTIME_TOKEN"])
self.assertNotIn("ghp_", path.read_text(encoding="utf-8"))
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,69 @@
from __future__ import annotations
import contextlib
import importlib.util
import io
import sys
import unittest
from pathlib import Path
REPO_DIR = Path(__file__).resolve().parents[1]
SPEC = importlib.util.spec_from_file_location(
"openbao_credential_change_appliers",
REPO_DIR / "scripts/openbao-apply-credential-change-appliers.py",
)
appliers = importlib.util.module_from_spec(SPEC)
assert SPEC.loader is not None
sys.modules[SPEC.name] = appliers
SPEC.loader.exec_module(appliers)
class CredentialChangeApplierSetupTests(unittest.TestCase):
def test_selected_appliers_all_is_stable(self) -> None:
selected = appliers.selected_appliers("all")
self.assertEqual(
[item["token_role"] for item in selected],
["credential-change-nonprod-applier", "credential-change-prod-applier"],
)
def test_role_args_are_bounded(self) -> None:
args = appliers.role_args(appliers.APPLIERS["prod"])
self.assertIn("auth/token/roles/credential-change-prod-applier", args)
self.assertIn("allowed_policies=credential-change-prod-applier", args)
self.assertIn("disallowed_policies=root,platform-admin", args)
self.assertIn("token_no_default_policy=true", args)
self.assertIn("token_type=service", args)
def test_dry_run_applies_policy_role_and_readback(self) -> None:
runner = appliers.BaoRunner(
kubectl="kubectl",
namespace="openbao",
release="openbao",
dry_run=True,
use_token_helper=False,
token=None,
)
output = io.StringIO()
with contextlib.redirect_stdout(output):
appliers.apply_applier(
runner,
appliers.APPLIERS["nonprod"],
REPO_DIR / "openbao/policies",
)
rendered = output.getvalue()
self.assertIn(
"DRY-RUN: bao policy write credential-change-nonprod-applier",
rendered,
)
self.assertIn(
"DRY-RUN: bao write auth/token/roles/credential-change-nonprod-applier",
rendered,
)
self.assertIn(
"DRY-RUN: bao read auth/token/roles/credential-change-nonprod-applier",
rendered,
)
if __name__ == "__main__":
unittest.main()

View File

@@ -4,18 +4,19 @@ type: workplan
title: "Workload KV Access Lanes for ops-warden Fetch"
domain: financials
repo: railiance-platform
status: active
status: finished
owner: codex
topic_slug: railiance
planning_priority: high
planning_order: 6
created: "2026-06-27"
updated: "2026-06-28"
updated: "2026-06-29"
depends_on_workplans:
- RAIL-PL-WP-0002
- RAILIANCE-WP-0004
related_state_hub_messages:
- "551031d1-335e-4db8-9535-820fea52d0a3"
- "f76d3a9e-a98f-4081-885d-b79d94312699"
state_hub_workstream_id: "96c8a93d-7a5a-4fa9-8f7b-865119551da3"
---
@@ -268,7 +269,7 @@ groups bound-claim mismatch. `platform-root` was restored to the
```task
id: RAILIANCE-WP-0006-T06
status: progress
status: done
priority: high
state_hub_task_id: "8e84ec19-01db-4baf-a532-de87e51d4994"
```
@@ -301,6 +302,16 @@ catalog. Keep activation pending until caller verification and catalog update.
to confirm that its dedicated `whynot-design-npm-publish` catalog selector
resolves through the caller-scoped lane.
**2026-06-29:** ops-warden confirmed in State Hub message
`f76d3a9e-a98f-4081-885d-b79d94312699` that catalog selector
`whynot-design-npm-publish` is `status: active`, `resolvable: true`, and wired
to the owner-confirmed lane:
`platform/workloads/coulomb/whynot-design/npm-publish`, field
`NPM_AUTH_TOKEN`, OIDC role `whynot-design-workload-kv-read`, and policy
`workload-kv-read-whynot-design-npm-publish`. ops-warden also confirmed it
notified whynot-design with `warden access whynot-design-npm-publish --exec -- npm publish`,
and that the sibling lanes remain draft for separate planning.
## T07 - Decide whether to batch sibling workload-KV requests
```task
@@ -329,6 +340,13 @@ serviced first. The later ops-warden batch follow-up is now represented as
proposed CCRs in `RAILIANCE-WP-0007`, still unapproved and unresolvable until
human review and verification.
**2026-06-29:** Reviewed the sibling lane suggestions against `INTENT.md`.
Created follow-up workplans `RAILIANCE-WP-0009` for the issue-core runtime
ingestion credential lane and `RAILIANCE-WP-0010` for the llm-connect
OpenRouter provider key lane. Both plans keep this repo's scope limited to
shared platform secret custody, least-privilege OpenBao/External Secrets
delivery, verification, and ops-warden front-door handoff.
## Exit Criteria
- The whynot-design npm publish token has a concrete OpenBao KV path, field,

View File

@@ -4,13 +4,13 @@ type: workplan
title: "Credential Change Proposal Review Workflow"
domain: financials
repo: railiance-platform
status: active
status: finished
owner: codex
topic_slug: railiance
planning_priority: high
planning_order: 7
created: "2026-06-27"
updated: "2026-06-28"
updated: "2026-06-30"
depends_on_workplans:
- RAIL-PL-WP-0002
- RAILIANCE-WP-0005
@@ -156,7 +156,7 @@ lives in `tests/test_credential_change.py`.
```task
id: RAILIANCE-WP-0007-T04
status: progress
status: done
priority: high
state_hub_task_id: "1b2e7752-815c-46f8-a2e2-212e8d04da80"
```
@@ -184,11 +184,17 @@ from reviewed plan to the interactive live applier.
generated role payloads after live OpenBao rejected an OIDC role without
callbacks. Unit coverage now checks the generated whynot-design role payload.
**2026-06-30:** Added source-artifact diff rendering to `plan` and delegated
`applier-dry-run` output. The generated plan now reports whether the checked-in
policy artifact matches the CCR-generated HCL and shows a unified diff when it
does not. Approved-only `apply-plan`/`operator-commands` remain gated by CCR
status and confirmed auth binding.
## T05 - Add chat/CLI approval commands
```task
id: RAILIANCE-WP-0007-T05
status: progress
status: done
priority: high
state_hub_task_id: "e6d4d2d1-1881-4db7-92f8-05e3fdb846ae"
```
@@ -215,11 +221,16 @@ State Hub decision-event emission and tighter chat integration. Created a
State Hub decision for `CCR-2026-0001` and added `sync-decision` so resolved
State Hub decisions can update the file-backed CCR status.
**2026-06-30:** Added optional `--record-state-hub` emission for approve, deny,
and needs-changes commands. Review comments are checked for known secret markers
before being written, and the State Hub progress event records only non-secret
CCR id/path/policy/field/auth-role metadata plus the reviewer comment.
## T06 - Build an interactive runbook for apply and verify
```task
id: RAILIANCE-WP-0007-T06
status: todo
status: done
priority: high
state_hub_task_id: "3c3fc38c-afa4-4367-b3e6-ba4b286ced30"
```
@@ -235,11 +246,23 @@ Acceptance:
- Positive and negative verification steps are guided.
- Non-secret evidence is recorded automatically.
**2026-06-30:** Added `scripts/credential-change.py runbook <CCR>` and Make
target `credential-change-runbook` to render the attended operator checklist,
final confirmation phrase, metadata apply guidance, secret custody instructions,
positive/negative verification steps, activation conditions, and evidence
commands. `runbook --execute-metadata` is opt-in, requires the exact `APPLY
<CCR-ID>` confirmation phrase, uses the local `bao` CLI with ambient approved
operator authority, writes only policy/auth metadata, and records a non-secret
`metadata_apply` evidence entry. Added `record-evidence` plus Make target
`credential-change-record-evidence` so operators can append apply, secret
provisioning, verification, and activation evidence to the CCR and optionally
State Hub without storing secret values.
## T07 - Pilot with whynot-design and ops-warden
```task
id: RAILIANCE-WP-0007-T07
status: progress
status: done
priority: high
state_hub_task_id: "07a7d8bf-5528-41c8-a791-d6ccd0466a33"
```
@@ -302,11 +325,15 @@ groups bound-claim mismatch, `platform-root` membership was restored afterward,
and `CCR-2026-0001` is now active/ready/resolvable. ops-warden catalog
confirmation remains the external closeout step.
**2026-06-30:** Closed the pilot task based on the active/ready/resolvable CCR
state and prior ops-warden catalog confirmation that the selector is active and
resolvable. The remaining lifecycle work is now tracked separately in T08.
## T08 - Add deactivation, rotation, and compromise flows
```task
id: RAILIANCE-WP-0007-T08
status: todo
status: done
priority: medium
state_hub_task_id: "23d6ef9d-8dbc-4468-b486-5ec8ada71130"
```
@@ -322,11 +349,22 @@ Acceptance:
- Deactivation disables the relevant access front door and auth/policy path.
- Compromise flow records blast-radius notes and required follow-up tasks.
**2026-06-30:** Added `lifecycle-plan`, `lifecycle-event`, and
`import-inventory` commands plus Make targets. Lifecycle plans render
deactivation, rotation, and compromise guidance, including access-front-door
state changes and OpenBao metadata disable commands for deactivation or
compromise. Lifecycle events update CCR status/front-door readiness, append
non-secret lifecycle evidence, and optionally post State Hub progress.
Compromise events accept non-secret blast-radius and follow-up references.
`import-inventory` can create a CCR-backed inventory file and matching read
policy artifact for an existing lane without asking for or storing secret
values.
## T09 - Add decision templates and guided review actions
```task
id: RAILIANCE-WP-0007-T09
status: todo
status: done
priority: high
state_hub_task_id: "c436fd8b-cd82-4600-81b0-87ec069d7ae6"
```
@@ -348,6 +386,12 @@ Acceptance:
- Future UI work can replace prefix parsing with structured decision outcomes
without changing the CCR audit trail.
**2026-06-30:** Added `scripts/credential-change.py decision-templates <CCR>`
and Make target `credential-change-decision-templates`. The generated templates
include accepted prefixes, CCR id, KV path, policy, auth-role path, and the
linked State Hub decision. Ambiguous State Hub rationale text now fails with the
valid templates in the error message.
## Exit Criteria
- A human can review and approve or deny a credential/security change without

View File

@@ -10,7 +10,7 @@ topic_slug: railiance
planning_priority: high
planning_order: 8
created: "2026-06-28"
updated: "2026-06-28"
updated: "2026-06-30"
depends_on_workplans:
- RAIL-PL-WP-0002
- RAILIANCE-WP-0005
@@ -114,7 +114,7 @@ and the local applier script.
```task
id: RAILIANCE-WP-0008-T01
status: todo
status: done
priority: high
state_hub_task_id: "d19fdfc5-addb-4813-8086-3aca2e948cea"
```
@@ -129,11 +129,20 @@ Acceptance:
- The proposal covers both workload KV read lanes and credential broker issuer
policies.
**2026-06-29:** Added `docs/openbao-approved-automation-delegation.md` and
`openbao/policies/credential-change-prod-applier.hcl`. The document defines
build/development, test/staging, and production boundaries, the allowed
production metadata mutation surface, denied secret/admin paths, and required
non-secret evidence. The production policy candidate allows only reviewed
metadata writes for workload KV read policies, credential-broker issuer
policies, approved auth-role prefixes, and self capability checks; it does not
grant secret value reads or writes.
## T02 - Implement a CCR-aware applier dry-run
```task
id: RAILIANCE-WP-0008-T02
status: todo
status: done
priority: high
state_hub_task_id: "2613f40d-fbd9-44f3-a864-85ec1d54e8f7"
```
@@ -149,11 +158,22 @@ Acceptance:
- Dry-run refuses attempts to create `root`, `platform-admin`, wildcard, or
unrelated policy names.
**2026-06-29:** Added `scripts/credential-change.py applier-dry-run <CCR>` and
Make target `credential-change-applier-dry-run`. The dry-run validates the CCR,
requires approved/applied/verified/active status, requires confirmed auth
bindings, verifies the OpenBao mount/path/policy/role stay inside the delegated
metadata surface, compares the policy artifact to the generated CCR policy body,
and renders only policy/auth-role mutations. It explicitly leaves secret value
writes, secret reads, and front-door activation out of scope. Unit tests cover
the active whynot-design CCR success path, unapproved CCR refusal, and rejection
of `platform-admin`/out-of-scope mount and path attempts. `make
credential-tests` passed with 28 tests.
## T03 - Add non-production applier role first
```task
id: RAILIANCE-WP-0008-T03
status: todo
status: progress
priority: medium
state_hub_task_id: "ff927a19-50fb-4351-8db1-c60a0cce0995"
```
@@ -167,11 +187,32 @@ Acceptance:
- Negative checks prove unrelated policy/auth/secret paths are denied.
- Evidence is recorded without secret values.
**2026-06-30:** Added the non-production metadata-only policy candidate
`openbao/policies/credential-change-nonprod-applier.hcl` and documented that
generated test-secret paths require separate CCR-backed approval. Live non-prod
identity creation and positive/negative OpenBao evidence remain to close this
task.
**2026-06-30:** Added the guarded `applier-apply` execution path that reuses the
CCR dry-run guardrails, requires exact `DELEGATED APPLY <CCR-ID>` confirmation,
uses the local `bao` CLI with ambient delegated applier authority, writes only
policy/auth-role metadata, and records non-secret `delegated_metadata_apply`
evidence. Non-production task closure still needs a live build/test applier
identity plus positive and negative capability evidence.
**2026-06-30:** Added `scripts/openbao-apply-credential-change-appliers.py` and
Make target `openbao-credential-change-appliers-dry-run` to install/dry-run the
non-production applier policy plus bounded `auth/token/roles/credential-change-
nonprod-applier` role. The token role allows only the matching applier policy,
disallows `root` and `platform-admin`, disables the default policy, and does not
issue tokens by itself. Live non-production apply and denial evidence remains
the closeout gate.
## T04 - Add production metadata applier with human approval gate
```task
id: RAILIANCE-WP-0008-T04
status: todo
status: progress
priority: high
state_hub_task_id: "414abd65-22d3-420f-994d-f7fdd1302db5"
```
@@ -185,6 +226,29 @@ Acceptance:
- Unapproved CCRs fail closed.
- Secret value provisioning is still not automated in production.
**2026-06-30:** Strengthened the production gate by adding source-artifact
checks to the CCR applier dry-run and documenting that unapproved CCRs fail
closed before OpenBao mutation rendering. The production policy candidate exists
and remains metadata-only; live delegated identity creation/application evidence
still needs an operator-held OpenBao step.
**2026-06-30:** Added `applier-apply` and Make targets
`credential-change-applier-apply-plan` / `credential-change-applier-apply`. The
command fails closed for unapproved CCRs, renders the dry-run payload before
mutation, requires exact confirmation, does not accept tokens in argv, leaves
secret values out of scope, and appends State Hub/file-backed non-secret apply
evidence when requested. Production closure still requires live execution using
the constrained applier identity rather than broad `platform-admin`.
**2026-06-30:** Added `scripts/openbao-apply-credential-change-appliers.py` and
Make targets `openbao-credential-change-appliers-dry-run` /
`openbao-configure-credential-change-appliers` to configure the production
`credential-change-prod-applier` policy and bounded token role. The role allows
only `credential-change-prod-applier`, disallows `root` and `platform-admin`,
uses service tokens, disables default policy attachment, and keeps token issuance
outside the setup script. Production closure still needs a live run and
capability evidence using this constrained identity.
## T05 - Close the whynot-design pilot
```task

View File

@@ -10,7 +10,7 @@ topic_slug: railiance
planning_priority: high
planning_order: 9
created: "2026-06-29"
updated: "2026-06-29"
updated: "2026-06-30"
depends_on_workplans:
- RAIL-PL-WP-0002
- RAILIANCE-WP-0004
@@ -80,10 +80,10 @@ The plan supports these `INTENT.md` principles:
| Read policy | `workload-kv-read-issue-core-runtime` |
| Policy file | `openbao/policies/workload-kv-read-issue-core-runtime.hcl` |
| Auth method | Kubernetes auth |
| Auth role | `issue-core-runtime-workload-kv-read` |
| Proposed service account | `issue-core` |
| Proposed namespace | `issue-core` |
| Delivery surface | External Secrets into the `issue-core` namespace |
| Auth role | `external-secrets-issue-core` |
| OpenBao auth service account | `external-secrets` |
| OpenBao auth namespace | `external-secrets` |
| Delivery surface | `ExternalSecret issue-core/issue-core-runtime` to Secret `issue-core-runtime` |
| ops-warden command | `warden access issue-core-ingestion-api-key --fetch ISSUE_CORE_API_KEY` |
The `GITEA_BACKEND_TOKEN` field remains an explicit review point. Remove it
@@ -95,7 +95,7 @@ from the CCR before approval if issue-core no longer needs it in this lane.
```task
id: RAILIANCE-WP-0009-T01
status: todo
status: done
priority: high
state_hub_task_id: "64d85288-38fb-4374-b889-fd0d136d3bdf"
```
@@ -114,11 +114,19 @@ Acceptance:
- The lane remains clearly platform-owned secret custody, not issue-core
application logic.
**2026-06-30:** Live cluster metadata confirms
`ExternalSecret issue-core/issue-core-runtime` is `Ready=True` with reason
`SecretSynced` and maps both `ISSUE_CORE_API_KEY` and `GITEA_BACKEND_TOKEN` from
`platform/workloads/issue-core/issue-core/issue-core-runtime`. Retain both
fields in `CCR-2026-0002` unless the issue-core owner later removes one through
review. The CCR remains `proposed`; this task records non-secret scope review,
not approval to apply.
## T02 - Confirm Kubernetes auth and External Secrets binding
```task
id: RAILIANCE-WP-0009-T02
status: todo
status: done
priority: high
state_hub_task_id: "7f4a8317-13f0-4be3-948c-a2e2f90447cf"
```
@@ -134,6 +142,17 @@ Acceptance:
- The External Secrets target and expected field names are documented.
- No direct human or agent read path is activated unless separately approved.
**2026-06-30:** Confirmed the current delivery path uses the platform External
Secrets operator, not a workload pod service account. The `issue-core`
Deployment uses the `default` service account, and no `issue-core` service
account exists. `ClusterSecretStore/openbao` authenticates to OpenBao as
`external-secrets/external-secrets` with role `external-secrets-issue-core` and
is limited to the `issue-core` namespace. Updated `CCR-2026-0002` to this
confirmed auth subject while keeping the exact
`workload-kv-read-issue-core-runtime` policy. `credential-change.py
applier-dry-run CCR-2026-0002` now blocks only because the CCR is still
`proposed`.
## T03 - Apply or confirm least-privilege OpenBao metadata
```task

View File

@@ -10,7 +10,7 @@ topic_slug: railiance
planning_priority: high
planning_order: 10
created: "2026-06-29"
updated: "2026-06-29"
updated: "2026-06-30"
depends_on_workplans:
- RAIL-PL-WP-0002
- RAILIANCE-WP-0004
@@ -84,10 +84,10 @@ The plan supports these `INTENT.md` principles:
| Read policy | `workload-kv-read-llm-connect-provider-secrets` |
| Policy file | `openbao/policies/workload-kv-read-llm-connect-provider-secrets.hcl` |
| Auth method | Kubernetes auth |
| Auth role | `llm-connect-provider-secrets-read` |
| Proposed service account | `llm-connect` |
| Proposed namespace | `activity-core` |
| Delivery surface | External Secrets to `llm-connect-provider-secrets` in `activity-core` |
| Auth role | `external-secrets-activity-core` |
| OpenBao auth service account | `external-secrets` |
| OpenBao auth namespace | `external-secrets` |
| Delivery surface | Future activity-core ExternalSecret to Secret `llm-connect-provider-secrets` |
| ops-warden command | `warden access llm-connect-openrouter-api-key --fetch OPENROUTER_API_KEY` |
## Tasks
@@ -96,7 +96,7 @@ The plan supports these `INTENT.md` principles:
```task
id: RAILIANCE-WP-0010-T01
status: todo
status: progress
priority: high
state_hub_task_id: "307b75a6-a3a8-473b-b171-7379d2848698"
```
@@ -116,11 +116,19 @@ Acceptance:
- The lane remains clearly platform-owned secret custody, not llm-connect model
routing or provider selection logic.
**2026-06-30:** Confirmed `activity-core` namespace exists and Kubernetes Secret
`activity-core/llm-connect-provider-secrets` exists, but no activity-core
`ExternalSecret` exists yet. Kept canonical CCR catalog id
`llm-connect-openrouter-api-key`; ops-warden previously mentioned
`openrouter-llm-connect`, so selector agreement remains open and this task stays
`progress`. OpenBao public seal status now reports `sealed=false`; the prior
sealed message is no longer the active blocker.
## T02 - Confirm Kubernetes auth and External Secrets binding
```task
id: RAILIANCE-WP-0010-T02
status: todo
status: done
priority: high
state_hub_task_id: "829192f5-4502-44e0-8020-656d74d5282a"
```
@@ -138,6 +146,19 @@ Acceptance:
`llm-connect-provider-secrets` or updated with the approved alternative.
- No direct human or agent read path is activated unless separately approved.
**2026-06-30:** Confirmed the proposed `llm-connect` service account does not
exist and the current `llm-connect` Deployment uses the namespace `default`
service account. Updated `CCR-2026-0003` to the approved platform ESO pattern:
OpenBao Kubernetes auth role `external-secrets-activity-core` bound to
`external-secrets/external-secrets`. Added
`argocd/platform-addons/openbao-secretstore/openbao-activity-core.clustersecretstore.yaml`,
limited to the `activity-core` namespace, and Make target
`openbao-configure-external-secrets-activity-core` for the matching OpenBao
role/policy apply. `kubectl kustomize argocd/platform-addons/openbao-secretstore`
renders both the existing issue-core store and the new activity-core store.
`credential-change.py applier-dry-run CCR-2026-0003` now blocks only because the
CCR is still `proposed`.
## T03 - Apply or confirm least-privilege OpenBao metadata
```task