Add credential-change delegated applier flow
This commit is contained in:
45
Makefile
45
Makefile
@@ -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
|
||||
|
||||
@@ -3,3 +3,4 @@ kind: Kustomization
|
||||
|
||||
resources:
|
||||
- openbao.clustersecretstore.yaml
|
||||
- openbao-activity-core.clustersecretstore.yaml
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
127
docs/openbao-approved-automation-delegation.md
Normal file
127
docs/openbao-approved-automation-delegation.md
Normal 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.
|
||||
@@ -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.
|
||||
|
||||
40
openbao/policies/credential-change-nonprod-applier.hcl
Normal file
40
openbao/policies/credential-change-nonprod-applier.hcl
Normal 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"]
|
||||
}
|
||||
41
openbao/policies/credential-change-prod-applier.hcl
Normal file
41
openbao/policies/credential-change-prod-applier.hcl
Normal 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
217
scripts/openbao-apply-credential-change-appliers.py
Executable file
217
scripts/openbao-apply-credential-change-appliers.py
Executable 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())
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
69
tests/test_credential_change_appliers.py
Normal file
69
tests/test_credential_change_appliers.py
Normal 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()
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user