Add credential-change delegated applier flow
This commit is contained in:
parent
c626bfcf15
commit
a95236d2e5
21 changed files with 2705 additions and 119 deletions
45
Makefile
45
Makefile
|
|
@ -25,8 +25,13 @@ ARGOCD_BOOTSTRAP_DIR ?= argocd/bootstrap
|
||||||
ARGOCD_REPOSITORY_SECRET ?=
|
ARGOCD_REPOSITORY_SECRET ?=
|
||||||
CREDENTIAL_GRANTS ?= credential-grants/catalog.yaml
|
CREDENTIAL_GRANTS ?= credential-grants/catalog.yaml
|
||||||
CREDENTIAL_CHANGE ?= CCR-2026-0001
|
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
|
STATE_HUB_URL ?= http://127.0.0.1:8000
|
||||||
OPENBAO_TOKEN_GRANT_ARGS ?=
|
OPENBAO_TOKEN_GRANT_ARGS ?=
|
||||||
|
OPENBAO_CREDENTIAL_CHANGE_APPLIER_ARGS ?=
|
||||||
OPENBAO_WORKLOAD_KV_ARGS ?=
|
OPENBAO_WORKLOAD_KV_ARGS ?=
|
||||||
CREDENTIAL_HELPER_GLOBAL_ARGS ?=
|
CREDENTIAL_HELPER_GLOBAL_ARGS ?=
|
||||||
CREDENTIAL_HELPER_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) \
|
OPENBAO_RELEASE=$(OPENBAO_RELEASE) ESO_NAMESPACE=$(EXTERNAL_SECRETS_NAMESPACE) \
|
||||||
scripts/openbao-apply-external-secrets-issue-core.sh
|
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
|
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)
|
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
|
credential-change-plan: ## Render a credential change request apply plan for review
|
||||||
scripts/credential-change.py plan $(CREDENTIAL_CHANGE)
|
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
|
credential-change-status: ## Render credential change request readiness status
|
||||||
scripts/credential-change.py status $(CREDENTIAL_CHANGE)
|
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
|
credential-change-operator-commands: ## Render approved-only non-secret OpenBao operator commands
|
||||||
scripts/credential-change.py operator-commands $(CREDENTIAL_CHANGE)
|
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
|
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)
|
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 } \
|
/^[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)
|
/^##@/ { 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:
|
resources:
|
||||||
- openbao.clustersecretstore.yaml
|
- 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
|
kind: credential-change-request
|
||||||
schema_version: 1
|
schema_version: 1
|
||||||
request_type: workload-kv-read
|
request_type: workload-kv-read
|
||||||
title: "issue-core runtime ingestion key lane"
|
title: issue-core runtime ingestion key lane
|
||||||
status: proposed
|
status: proposed
|
||||||
created: "2026-06-27"
|
created: '2026-06-27'
|
||||||
updated: "2026-06-27"
|
updated: '2026-06-30'
|
||||||
requester:
|
requester:
|
||||||
agent: ops-warden
|
agent: ops-warden
|
||||||
message_id: "fe5b1696-8956-4bd5-9d6f-dbde1901a076"
|
message_id: fe5b1696-8956-4bd5-9d6f-dbde1901a076
|
||||||
reason: "Confirm and provision the issue-core workload KV lane requested in the ops-warden batch."
|
reason: Confirm and provision the issue-core workload KV lane requested in the ops-warden
|
||||||
|
batch.
|
||||||
review:
|
review:
|
||||||
required: true
|
required: true
|
||||||
required_approvers:
|
required_approvers:
|
||||||
- platform-operator
|
- platform-operator
|
||||||
- issue-core-owner
|
- issue-core-owner
|
||||||
comments: []
|
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:
|
target:
|
||||||
domain: financials
|
domain: financials
|
||||||
tenant: issue-core
|
tenant: issue-core
|
||||||
workload: issue-core
|
workload: issue-core
|
||||||
environment: production
|
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:
|
openbao:
|
||||||
mount: platform
|
mount: platform
|
||||||
kv_path: platform/workloads/issue-core/issue-core/issue-core-runtime
|
kv_path: platform/workloads/issue-core/issue-core/issue-core-runtime
|
||||||
fields:
|
fields:
|
||||||
- ISSUE_CORE_API_KEY
|
- ISSUE_CORE_API_KEY
|
||||||
- GITEA_BACKEND_TOKEN
|
- GITEA_BACKEND_TOKEN
|
||||||
policy_name: workload-kv-read-issue-core-runtime
|
policy_name: workload-kv-read-issue-core-runtime
|
||||||
policy_file: openbao/policies/workload-kv-read-issue-core-runtime.hcl
|
policy_file: openbao/policies/workload-kv-read-issue-core-runtime.hcl
|
||||||
auth:
|
auth:
|
||||||
method: kubernetes
|
method: kubernetes
|
||||||
mount: kubernetes
|
mount: kubernetes
|
||||||
role: issue-core-runtime-workload-kv-read
|
role: external-secrets-issue-core
|
||||||
bound_claims:
|
bound_claims:
|
||||||
service_account_names:
|
service_account_names:
|
||||||
- issue-core
|
- external-secrets
|
||||||
service_account_namespaces:
|
service_account_namespaces:
|
||||||
- issue-core
|
- external-secrets
|
||||||
bound_claims_confirmed: false
|
bound_claims_confirmed: true
|
||||||
policies:
|
policies:
|
||||||
- workload-kv-read-issue-core-runtime
|
- workload-kv-read-issue-core-runtime
|
||||||
ttl: 15m
|
ttl: 15m
|
||||||
access_frontdoor:
|
access_frontdoor:
|
||||||
type: ops-warden
|
type: ops-warden
|
||||||
catalog_id: issue-core-ingestion-api-key
|
catalog_id: issue-core-ingestion-api-key
|
||||||
selector: "issue-core ingestion API key"
|
selector: issue-core ingestion API key
|
||||||
command: "warden access issue-core-ingestion-api-key --fetch ISSUE_CORE_API_KEY"
|
command: warden access issue-core-ingestion-api-key --fetch ISSUE_CORE_API_KEY
|
||||||
resolvable: false
|
resolvable: false
|
||||||
readiness: template
|
readiness: template
|
||||||
activation: "draft-until-ccr-verified"
|
activation: draft-until-ccr-verified
|
||||||
delivery:
|
delivery:
|
||||||
surface: external-secrets
|
surface: external-secrets
|
||||||
target: "issue-core namespace"
|
target: ExternalSecret issue-core/issue-core-runtime -> Secret issue-core-runtime
|
||||||
|
in the issue-core namespace
|
||||||
risk:
|
risk:
|
||||||
classification: high
|
classification: high
|
||||||
notes:
|
notes:
|
||||||
- "Grants read access to issue-core runtime ingestion credentials."
|
- Grants read access to issue-core runtime ingestion credentials through the platform
|
||||||
- "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."
|
External Secrets path.
|
||||||
- "The Kubernetes service account and namespace binding must be confirmed before apply."
|
- GITEA_BACKEND_TOKEN remains included because the live issue-core ExternalSecret
|
||||||
- "ops-warden must proxy reads as the caller and must not retain token values."
|
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:
|
verification:
|
||||||
positive:
|
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:
|
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:
|
activation_conditions:
|
||||||
- "Policy applied with platform-admin/operator authority."
|
- Policy applied with platform-admin/operator authority.
|
||||||
- "Kubernetes auth role bound to the confirmed issue-core service account and namespace."
|
- Kubernetes auth role bound to external-secrets/external-secrets for the issue-core
|
||||||
- "Secret values provisioned directly in OpenBao through approved operator custody."
|
External Secrets delivery path.
|
||||||
- "Positive and negative verification recorded with non-secret audit ids or timestamps."
|
- Secret values provisioned directly in OpenBao through approved operator custody.
|
||||||
|
- Positive and negative verification recorded with non-secret audit ids or timestamps.
|
||||||
lifecycle:
|
lifecycle:
|
||||||
deactivate: "Disable ops-warden catalog entry and remove or detach auth role policy."
|
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."
|
rotate: Replace issue-core runtime secret values directly in OpenBao and record
|
||||||
compromised: "Immediately deactivate access front door, rotate affected values, record blast-radius notes, and open incident follow-up tasks."
|
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:
|
state_hub:
|
||||||
workplan_id: RAILIANCE-WP-0007
|
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
|
kind: credential-change-request
|
||||||
schema_version: 1
|
schema_version: 1
|
||||||
request_type: workload-kv-read
|
request_type: workload-kv-read
|
||||||
title: "llm-connect OpenRouter provider key lane"
|
title: llm-connect OpenRouter provider key lane
|
||||||
status: proposed
|
status: proposed
|
||||||
created: "2026-06-27"
|
created: '2026-06-27'
|
||||||
updated: "2026-06-27"
|
updated: '2026-06-30'
|
||||||
requester:
|
requester:
|
||||||
agent: ops-warden
|
agent: ops-warden
|
||||||
message_id: "fe5b1696-8956-4bd5-9d6f-dbde1901a076"
|
message_id: fe5b1696-8956-4bd5-9d6f-dbde1901a076
|
||||||
reason: "Confirm and provision the llm-connect OpenRouter workload KV lane requested in the ops-warden batch."
|
reason: Confirm and provision the llm-connect OpenRouter workload KV lane requested
|
||||||
|
in the ops-warden batch.
|
||||||
review:
|
review:
|
||||||
required: true
|
required: true
|
||||||
required_approvers:
|
required_approvers:
|
||||||
- platform-operator
|
- platform-operator
|
||||||
- activity-core-owner
|
- activity-core-owner
|
||||||
comments: []
|
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:
|
target:
|
||||||
domain: financials
|
domain: financials
|
||||||
tenant: activity-core
|
tenant: activity-core
|
||||||
workload: llm-connect
|
workload: llm-connect
|
||||||
environment: production
|
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:
|
openbao:
|
||||||
mount: platform
|
mount: platform
|
||||||
kv_path: platform/workloads/activity-core/llm-connect/llm-connect-provider-secrets
|
kv_path: platform/workloads/activity-core/llm-connect/llm-connect-provider-secrets
|
||||||
fields:
|
fields:
|
||||||
- OPENROUTER_API_KEY
|
- OPENROUTER_API_KEY
|
||||||
policy_name: workload-kv-read-llm-connect-provider-secrets
|
policy_name: workload-kv-read-llm-connect-provider-secrets
|
||||||
policy_file: openbao/policies/workload-kv-read-llm-connect-provider-secrets.hcl
|
policy_file: openbao/policies/workload-kv-read-llm-connect-provider-secrets.hcl
|
||||||
auth:
|
auth:
|
||||||
method: kubernetes
|
method: kubernetes
|
||||||
mount: kubernetes
|
mount: kubernetes
|
||||||
role: llm-connect-provider-secrets-read
|
role: external-secrets-activity-core
|
||||||
bound_claims:
|
bound_claims:
|
||||||
service_account_names:
|
service_account_names:
|
||||||
- llm-connect
|
- external-secrets
|
||||||
service_account_namespaces:
|
service_account_namespaces:
|
||||||
- activity-core
|
- external-secrets
|
||||||
bound_claims_confirmed: false
|
bound_claims_confirmed: true
|
||||||
policies:
|
policies:
|
||||||
- workload-kv-read-llm-connect-provider-secrets
|
- workload-kv-read-llm-connect-provider-secrets
|
||||||
ttl: 15m
|
ttl: 15m
|
||||||
access_frontdoor:
|
access_frontdoor:
|
||||||
type: ops-warden
|
type: ops-warden
|
||||||
catalog_id: llm-connect-openrouter-api-key
|
catalog_id: llm-connect-openrouter-api-key
|
||||||
selector: "llm-connect OpenRouter API key"
|
selector: llm-connect OpenRouter API key
|
||||||
command: "warden access llm-connect-openrouter-api-key --fetch OPENROUTER_API_KEY"
|
command: warden access llm-connect-openrouter-api-key --fetch OPENROUTER_API_KEY
|
||||||
resolvable: false
|
resolvable: false
|
||||||
readiness: template
|
readiness: template
|
||||||
activation: "draft-until-ccr-verified"
|
activation: draft-until-ccr-verified
|
||||||
delivery:
|
delivery:
|
||||||
surface: external-secrets
|
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:
|
risk:
|
||||||
classification: high
|
classification: high
|
||||||
notes:
|
notes:
|
||||||
- "Grants read access to the provider key used by llm-connect for OpenRouter requests."
|
- 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."
|
through the platform External Secrets path.
|
||||||
- "ops-warden must proxy reads as the caller and must not retain token values."
|
- 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:
|
verification:
|
||||||
positive:
|
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:
|
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:
|
activation_conditions:
|
||||||
- "Policy applied with platform-admin/operator authority."
|
- Policy applied with platform-admin/operator authority.
|
||||||
- "Kubernetes auth role bound to the confirmed llm-connect service account and namespace."
|
- Kubernetes auth role bound to external-secrets/external-secrets for the activity-core
|
||||||
- "Secret value provisioned directly in OpenBao through approved operator custody."
|
External Secrets delivery path.
|
||||||
- "Positive and negative verification recorded with non-secret audit ids or timestamps."
|
- Secret value provisioned directly in OpenBao through approved operator custody.
|
||||||
|
- Positive and negative verification recorded with non-secret audit ids or timestamps.
|
||||||
lifecycle:
|
lifecycle:
|
||||||
deactivate: "Disable ops-warden catalog entry and remove or detach auth role policy."
|
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."
|
rotate: Replace OPENROUTER_API_KEY directly in OpenBao and record non-secret rotation
|
||||||
compromised: "Immediately deactivate access front door, rotate the provider key, record blast-radius notes, and open incident follow-up tasks."
|
evidence.
|
||||||
|
compromised: Immediately deactivate access front door, rotate the provider key,
|
||||||
|
record blast-radius notes, and open incident follow-up tasks.
|
||||||
state_hub:
|
state_hub:
|
||||||
workplan_id: RAILIANCE-WP-0007
|
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`.
|
`ClusterSecretStore`.
|
||||||
- OpenBao Kubernetes auth role `external-secrets-issue-core` for the
|
- OpenBao Kubernetes auth role `external-secrets-issue-core` for the
|
||||||
issue-core pilot.
|
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
|
`ClusterSecretStore/openbao` is limited to the `issue-core` namespace.
|
||||||
`issue-core` namespace. Broaden it only with a new platform review when another
|
`ClusterSecretStore/openbao-activity-core` is limited to the `activity-core`
|
||||||
tenant is ready to consume OpenBao through ESO.
|
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:
|
Configure the OpenBao side without printing token values:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
OPENBAO_TOKEN_FILE=~/.local/openbao/platform-admin.token \
|
OPENBAO_TOKEN_FILE=~/.local/openbao/platform-admin.token \
|
||||||
make openbao-configure-external-secrets-issue-core
|
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
|
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-sync-decision CREDENTIAL_CHANGE=CCR-2026-0001
|
||||||
make credential-change-apply-plan 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-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
|
`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;
|
8. record non-secret evidence;
|
||||||
9. notify downstream front doors such as ops-warden.
|
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
|
This lets operators safely drive privileged work without needing to remember
|
||||||
every OpenBao command.
|
every OpenBao command.
|
||||||
|
|
||||||
|
|
@ -241,6 +256,16 @@ Every active CCR needs a deactivate and rotate path:
|
||||||
- `compromised`: emergency state requiring immediate disablement, rotation,
|
- `compromised`: emergency state requiring immediate disablement, rotation,
|
||||||
blast-radius notes, and incident follow-up.
|
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
|
The workflow must support marking an existing credential or lane as compromised
|
||||||
even when the original request predates this system.
|
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` |
|
| KV mount | `platform` |
|
||||||
| OpenBao CLI path | `platform/workloads/coulomb/whynot-design/npm-publish` |
|
| OpenBao CLI path | `platform/workloads/coulomb/whynot-design/npm-publish` |
|
||||||
| Secret field | `NPM_AUTH_TOKEN` |
|
| 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` |
|
| Read policy | `workload-kv-read-whynot-design-npm-publish` |
|
||||||
| Policy file | `openbao/policies/workload-kv-read-whynot-design-npm-publish.hcl` |
|
| Policy file | `openbao/policies/workload-kv-read-whynot-design-npm-publish.hcl` |
|
||||||
| OIDC auth mount | `netkingdom` |
|
| OIDC auth mount | `netkingdom` |
|
||||||
|
|
@ -57,6 +57,13 @@ Expected ops-warden exec shape after activation:
|
||||||
warden access whynot-design-npm-publish --exec -- npm publish
|
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
|
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
|
only in an attended shell or through a process that consumes the value without
|
||||||
logging it.
|
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}"
|
ESO_SERVICE_ACCOUNT="${ESO_SERVICE_ACCOUNT:-external-secrets}"
|
||||||
REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
POLICY_FILE="${POLICY_FILE:-$REPO_DIR/openbao/policies/external-secrets-issue-core.hcl}"
|
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
|
DRY_RUN=0
|
||||||
|
|
||||||
usage() {
|
usage() {
|
||||||
|
|
@ -125,13 +128,12 @@ remote_bao "$token" write "auth/kubernetes/role/${ROLE_NAME}" \
|
||||||
|
|
||||||
remote_bao "$token" read "auth/kubernetes/role/${ROLE_NAME}"
|
remote_bao "$token" read "auth/kubernetes/role/${ROLE_NAME}"
|
||||||
|
|
||||||
cat <<'NEXT'
|
cat <<NEXT
|
||||||
|
|
||||||
External Secrets OpenBao role configured.
|
External Secrets OpenBao role configured.
|
||||||
|
|
||||||
Next steps:
|
Next steps:
|
||||||
1. Sync the external-secrets and openbao-secretstore ArgoCD Applications.
|
1. Sync the external-secrets and openbao-secretstore ArgoCD Applications.
|
||||||
2. Provision platform/workloads/issue-core/issue-core/issue-core-runtime
|
2. Provision ${NEXT_KV_PATH} with ${NEXT_FIELDS} without printing values.
|
||||||
with ISSUE_CORE_API_KEY and GITEA_BACKEND_TOKEN without printing values.
|
3. Confirm ${NEXT_TARGET} becomes Ready.
|
||||||
3. Confirm ExternalSecret/issue-core-runtime becomes Ready.
|
|
||||||
NEXT
|
NEXT
|
||||||
|
|
|
||||||
|
|
@ -35,10 +35,11 @@ class CredentialChangeTests(unittest.TestCase):
|
||||||
self.assertEqual(warnings, [])
|
self.assertEqual(warnings, [])
|
||||||
self.assertTrue(ccr["openbao"]["auth"]["bound_claims_confirmed"])
|
self.assertTrue(ccr["openbao"]["auth"]["bound_claims_confirmed"])
|
||||||
|
|
||||||
def test_unconfirmed_sibling_ccr_keeps_bound_claim_warning(self) -> None:
|
def test_issue_core_ccr_has_confirmed_eso_binding(self) -> None:
|
||||||
_ccr, errors, warnings = credential_change.validate_ccr(self.issue_core)
|
ccr, errors, warnings = credential_change.validate_ccr(self.issue_core)
|
||||||
self.assertEqual(errors, [])
|
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:
|
def test_all_repo_ccrs_validate(self) -> None:
|
||||||
for path in sorted((REPO_DIR / "credential-change-requests").glob("*.yaml")):
|
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)
|
ccr, errors, _warnings = credential_change.validate_ccr(self.issue_core)
|
||||||
self.assertEqual(errors, [])
|
self.assertEqual(errors, [])
|
||||||
payload = credential_change.auth_payload(ccr)
|
payload = credential_change.auth_payload(ccr)
|
||||||
self.assertEqual(payload["bound_service_account_names"], ["issue-core"])
|
self.assertEqual(payload["bound_service_account_names"], ["external-secrets"])
|
||||||
self.assertEqual(payload["bound_service_account_namespaces"], ["issue-core"])
|
self.assertEqual(payload["bound_service_account_namespaces"], ["external-secrets"])
|
||||||
self.assertNotIn("bound_claims", payload)
|
self.assertNotIn("bound_claims", payload)
|
||||||
|
|
||||||
def test_oidc_auth_payload_includes_redirect_uris(self) -> None:
|
def test_oidc_auth_payload_includes_redirect_uris(self) -> None:
|
||||||
|
|
@ -150,6 +151,63 @@ class CredentialChangeTests(unittest.TestCase):
|
||||||
with self.assertRaises(SystemExit):
|
with self.assertRaises(SystemExit):
|
||||||
credential_change.command_apply_plan(type("Args", (), {"ref": str(self.issue_core)})())
|
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:
|
def test_operator_commands_render_non_secret_policy_and_role_handoff(self) -> None:
|
||||||
ccr, errors, warnings = credential_change.validate_ccr(self.sample)
|
ccr, errors, warnings = credential_change.validate_ccr(self.sample)
|
||||||
self.assertEqual(errors, [])
|
self.assertEqual(errors, [])
|
||||||
|
|
@ -207,6 +265,9 @@ class CredentialChangeTests(unittest.TestCase):
|
||||||
credential_change.append_decision(
|
credential_change.append_decision(
|
||||||
copied, "approved", "unit-test", "looks right"
|
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)
|
ccr, errors, _warnings = credential_change.validate_ccr(copied)
|
||||||
self.assertEqual(errors, [])
|
self.assertEqual(errors, [])
|
||||||
self.assertEqual(ccr["status"], "approved")
|
self.assertEqual(ccr["status"], "approved")
|
||||||
|
|
@ -241,6 +302,371 @@ class CredentialChangeTests(unittest.TestCase):
|
||||||
self.assertNotIn("*", policy)
|
self.assertNotIn("*", policy)
|
||||||
self.assertNotIn("delete", 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__":
|
if __name__ == "__main__":
|
||||||
unittest.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"
|
title: "Workload KV Access Lanes for ops-warden Fetch"
|
||||||
domain: financials
|
domain: financials
|
||||||
repo: railiance-platform
|
repo: railiance-platform
|
||||||
status: active
|
status: finished
|
||||||
owner: codex
|
owner: codex
|
||||||
topic_slug: railiance
|
topic_slug: railiance
|
||||||
planning_priority: high
|
planning_priority: high
|
||||||
planning_order: 6
|
planning_order: 6
|
||||||
created: "2026-06-27"
|
created: "2026-06-27"
|
||||||
updated: "2026-06-28"
|
updated: "2026-06-29"
|
||||||
depends_on_workplans:
|
depends_on_workplans:
|
||||||
- RAIL-PL-WP-0002
|
- RAIL-PL-WP-0002
|
||||||
- RAILIANCE-WP-0004
|
- RAILIANCE-WP-0004
|
||||||
related_state_hub_messages:
|
related_state_hub_messages:
|
||||||
- "551031d1-335e-4db8-9535-820fea52d0a3"
|
- "551031d1-335e-4db8-9535-820fea52d0a3"
|
||||||
|
- "f76d3a9e-a98f-4081-885d-b79d94312699"
|
||||||
state_hub_workstream_id: "96c8a93d-7a5a-4fa9-8f7b-865119551da3"
|
state_hub_workstream_id: "96c8a93d-7a5a-4fa9-8f7b-865119551da3"
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -268,7 +269,7 @@ groups bound-claim mismatch. `platform-root` was restored to the
|
||||||
|
|
||||||
```task
|
```task
|
||||||
id: RAILIANCE-WP-0006-T06
|
id: RAILIANCE-WP-0006-T06
|
||||||
status: progress
|
status: done
|
||||||
priority: high
|
priority: high
|
||||||
state_hub_task_id: "8e84ec19-01db-4baf-a532-de87e51d4994"
|
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
|
to confirm that its dedicated `whynot-design-npm-publish` catalog selector
|
||||||
resolves through the caller-scoped lane.
|
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
|
## T07 - Decide whether to batch sibling workload-KV requests
|
||||||
|
|
||||||
```task
|
```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
|
proposed CCRs in `RAILIANCE-WP-0007`, still unapproved and unresolvable until
|
||||||
human review and verification.
|
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
|
## Exit Criteria
|
||||||
|
|
||||||
- The whynot-design npm publish token has a concrete OpenBao KV path, field,
|
- 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"
|
title: "Credential Change Proposal Review Workflow"
|
||||||
domain: financials
|
domain: financials
|
||||||
repo: railiance-platform
|
repo: railiance-platform
|
||||||
status: active
|
status: finished
|
||||||
owner: codex
|
owner: codex
|
||||||
topic_slug: railiance
|
topic_slug: railiance
|
||||||
planning_priority: high
|
planning_priority: high
|
||||||
planning_order: 7
|
planning_order: 7
|
||||||
created: "2026-06-27"
|
created: "2026-06-27"
|
||||||
updated: "2026-06-28"
|
updated: "2026-06-30"
|
||||||
depends_on_workplans:
|
depends_on_workplans:
|
||||||
- RAIL-PL-WP-0002
|
- RAIL-PL-WP-0002
|
||||||
- RAILIANCE-WP-0005
|
- RAILIANCE-WP-0005
|
||||||
|
|
@ -156,7 +156,7 @@ lives in `tests/test_credential_change.py`.
|
||||||
|
|
||||||
```task
|
```task
|
||||||
id: RAILIANCE-WP-0007-T04
|
id: RAILIANCE-WP-0007-T04
|
||||||
status: progress
|
status: done
|
||||||
priority: high
|
priority: high
|
||||||
state_hub_task_id: "1b2e7752-815c-46f8-a2e2-212e8d04da80"
|
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
|
generated role payloads after live OpenBao rejected an OIDC role without
|
||||||
callbacks. Unit coverage now checks the generated whynot-design role payload.
|
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
|
## T05 - Add chat/CLI approval commands
|
||||||
|
|
||||||
```task
|
```task
|
||||||
id: RAILIANCE-WP-0007-T05
|
id: RAILIANCE-WP-0007-T05
|
||||||
status: progress
|
status: done
|
||||||
priority: high
|
priority: high
|
||||||
state_hub_task_id: "e6d4d2d1-1881-4db7-92f8-05e3fdb846ae"
|
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 decision for `CCR-2026-0001` and added `sync-decision` so resolved
|
||||||
State Hub decisions can update the file-backed CCR status.
|
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
|
## T06 - Build an interactive runbook for apply and verify
|
||||||
|
|
||||||
```task
|
```task
|
||||||
id: RAILIANCE-WP-0007-T06
|
id: RAILIANCE-WP-0007-T06
|
||||||
status: todo
|
status: done
|
||||||
priority: high
|
priority: high
|
||||||
state_hub_task_id: "3c3fc38c-afa4-4367-b3e6-ba4b286ced30"
|
state_hub_task_id: "3c3fc38c-afa4-4367-b3e6-ba4b286ced30"
|
||||||
```
|
```
|
||||||
|
|
@ -235,11 +246,23 @@ Acceptance:
|
||||||
- Positive and negative verification steps are guided.
|
- Positive and negative verification steps are guided.
|
||||||
- Non-secret evidence is recorded automatically.
|
- 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
|
## T07 - Pilot with whynot-design and ops-warden
|
||||||
|
|
||||||
```task
|
```task
|
||||||
id: RAILIANCE-WP-0007-T07
|
id: RAILIANCE-WP-0007-T07
|
||||||
status: progress
|
status: done
|
||||||
priority: high
|
priority: high
|
||||||
state_hub_task_id: "07a7d8bf-5528-41c8-a791-d6ccd0466a33"
|
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
|
and `CCR-2026-0001` is now active/ready/resolvable. ops-warden catalog
|
||||||
confirmation remains the external closeout step.
|
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
|
## T08 - Add deactivation, rotation, and compromise flows
|
||||||
|
|
||||||
```task
|
```task
|
||||||
id: RAILIANCE-WP-0007-T08
|
id: RAILIANCE-WP-0007-T08
|
||||||
status: todo
|
status: done
|
||||||
priority: medium
|
priority: medium
|
||||||
state_hub_task_id: "23d6ef9d-8dbc-4468-b486-5ec8ada71130"
|
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.
|
- Deactivation disables the relevant access front door and auth/policy path.
|
||||||
- Compromise flow records blast-radius notes and required follow-up tasks.
|
- 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
|
## T09 - Add decision templates and guided review actions
|
||||||
|
|
||||||
```task
|
```task
|
||||||
id: RAILIANCE-WP-0007-T09
|
id: RAILIANCE-WP-0007-T09
|
||||||
status: todo
|
status: done
|
||||||
priority: high
|
priority: high
|
||||||
state_hub_task_id: "c436fd8b-cd82-4600-81b0-87ec069d7ae6"
|
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
|
- Future UI work can replace prefix parsing with structured decision outcomes
|
||||||
without changing the CCR audit trail.
|
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
|
## Exit Criteria
|
||||||
|
|
||||||
- A human can review and approve or deny a credential/security change without
|
- A human can review and approve or deny a credential/security change without
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ topic_slug: railiance
|
||||||
planning_priority: high
|
planning_priority: high
|
||||||
planning_order: 8
|
planning_order: 8
|
||||||
created: "2026-06-28"
|
created: "2026-06-28"
|
||||||
updated: "2026-06-28"
|
updated: "2026-06-30"
|
||||||
depends_on_workplans:
|
depends_on_workplans:
|
||||||
- RAIL-PL-WP-0002
|
- RAIL-PL-WP-0002
|
||||||
- RAILIANCE-WP-0005
|
- RAILIANCE-WP-0005
|
||||||
|
|
@ -114,7 +114,7 @@ and the local applier script.
|
||||||
|
|
||||||
```task
|
```task
|
||||||
id: RAILIANCE-WP-0008-T01
|
id: RAILIANCE-WP-0008-T01
|
||||||
status: todo
|
status: done
|
||||||
priority: high
|
priority: high
|
||||||
state_hub_task_id: "d19fdfc5-addb-4813-8086-3aca2e948cea"
|
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
|
- The proposal covers both workload KV read lanes and credential broker issuer
|
||||||
policies.
|
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
|
## T02 - Implement a CCR-aware applier dry-run
|
||||||
|
|
||||||
```task
|
```task
|
||||||
id: RAILIANCE-WP-0008-T02
|
id: RAILIANCE-WP-0008-T02
|
||||||
status: todo
|
status: done
|
||||||
priority: high
|
priority: high
|
||||||
state_hub_task_id: "2613f40d-fbd9-44f3-a864-85ec1d54e8f7"
|
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
|
- Dry-run refuses attempts to create `root`, `platform-admin`, wildcard, or
|
||||||
unrelated policy names.
|
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
|
## T03 - Add non-production applier role first
|
||||||
|
|
||||||
```task
|
```task
|
||||||
id: RAILIANCE-WP-0008-T03
|
id: RAILIANCE-WP-0008-T03
|
||||||
status: todo
|
status: progress
|
||||||
priority: medium
|
priority: medium
|
||||||
state_hub_task_id: "ff927a19-50fb-4351-8db1-c60a0cce0995"
|
state_hub_task_id: "ff927a19-50fb-4351-8db1-c60a0cce0995"
|
||||||
```
|
```
|
||||||
|
|
@ -167,11 +187,32 @@ Acceptance:
|
||||||
- Negative checks prove unrelated policy/auth/secret paths are denied.
|
- Negative checks prove unrelated policy/auth/secret paths are denied.
|
||||||
- Evidence is recorded without secret values.
|
- 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
|
## T04 - Add production metadata applier with human approval gate
|
||||||
|
|
||||||
```task
|
```task
|
||||||
id: RAILIANCE-WP-0008-T04
|
id: RAILIANCE-WP-0008-T04
|
||||||
status: todo
|
status: progress
|
||||||
priority: high
|
priority: high
|
||||||
state_hub_task_id: "414abd65-22d3-420f-994d-f7fdd1302db5"
|
state_hub_task_id: "414abd65-22d3-420f-994d-f7fdd1302db5"
|
||||||
```
|
```
|
||||||
|
|
@ -185,6 +226,29 @@ Acceptance:
|
||||||
- Unapproved CCRs fail closed.
|
- Unapproved CCRs fail closed.
|
||||||
- Secret value provisioning is still not automated in production.
|
- 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
|
## T05 - Close the whynot-design pilot
|
||||||
|
|
||||||
```task
|
```task
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ topic_slug: railiance
|
||||||
planning_priority: high
|
planning_priority: high
|
||||||
planning_order: 9
|
planning_order: 9
|
||||||
created: "2026-06-29"
|
created: "2026-06-29"
|
||||||
updated: "2026-06-29"
|
updated: "2026-06-30"
|
||||||
depends_on_workplans:
|
depends_on_workplans:
|
||||||
- RAIL-PL-WP-0002
|
- RAIL-PL-WP-0002
|
||||||
- RAILIANCE-WP-0004
|
- RAILIANCE-WP-0004
|
||||||
|
|
@ -80,10 +80,10 @@ The plan supports these `INTENT.md` principles:
|
||||||
| Read policy | `workload-kv-read-issue-core-runtime` |
|
| Read policy | `workload-kv-read-issue-core-runtime` |
|
||||||
| Policy file | `openbao/policies/workload-kv-read-issue-core-runtime.hcl` |
|
| Policy file | `openbao/policies/workload-kv-read-issue-core-runtime.hcl` |
|
||||||
| Auth method | Kubernetes auth |
|
| Auth method | Kubernetes auth |
|
||||||
| Auth role | `issue-core-runtime-workload-kv-read` |
|
| Auth role | `external-secrets-issue-core` |
|
||||||
| Proposed service account | `issue-core` |
|
| OpenBao auth service account | `external-secrets` |
|
||||||
| Proposed namespace | `issue-core` |
|
| OpenBao auth namespace | `external-secrets` |
|
||||||
| Delivery surface | External Secrets into the `issue-core` namespace |
|
| 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` |
|
| 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
|
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
|
```task
|
||||||
id: RAILIANCE-WP-0009-T01
|
id: RAILIANCE-WP-0009-T01
|
||||||
status: todo
|
status: done
|
||||||
priority: high
|
priority: high
|
||||||
state_hub_task_id: "64d85288-38fb-4374-b889-fd0d136d3bdf"
|
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
|
- The lane remains clearly platform-owned secret custody, not issue-core
|
||||||
application logic.
|
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
|
## T02 - Confirm Kubernetes auth and External Secrets binding
|
||||||
|
|
||||||
```task
|
```task
|
||||||
id: RAILIANCE-WP-0009-T02
|
id: RAILIANCE-WP-0009-T02
|
||||||
status: todo
|
status: done
|
||||||
priority: high
|
priority: high
|
||||||
state_hub_task_id: "7f4a8317-13f0-4be3-948c-a2e2f90447cf"
|
state_hub_task_id: "7f4a8317-13f0-4be3-948c-a2e2f90447cf"
|
||||||
```
|
```
|
||||||
|
|
@ -134,6 +142,17 @@ Acceptance:
|
||||||
- The External Secrets target and expected field names are documented.
|
- The External Secrets target and expected field names are documented.
|
||||||
- No direct human or agent read path is activated unless separately approved.
|
- 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
|
## T03 - Apply or confirm least-privilege OpenBao metadata
|
||||||
|
|
||||||
```task
|
```task
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ topic_slug: railiance
|
||||||
planning_priority: high
|
planning_priority: high
|
||||||
planning_order: 10
|
planning_order: 10
|
||||||
created: "2026-06-29"
|
created: "2026-06-29"
|
||||||
updated: "2026-06-29"
|
updated: "2026-06-30"
|
||||||
depends_on_workplans:
|
depends_on_workplans:
|
||||||
- RAIL-PL-WP-0002
|
- RAIL-PL-WP-0002
|
||||||
- RAILIANCE-WP-0004
|
- RAILIANCE-WP-0004
|
||||||
|
|
@ -84,10 +84,10 @@ The plan supports these `INTENT.md` principles:
|
||||||
| Read policy | `workload-kv-read-llm-connect-provider-secrets` |
|
| Read policy | `workload-kv-read-llm-connect-provider-secrets` |
|
||||||
| Policy file | `openbao/policies/workload-kv-read-llm-connect-provider-secrets.hcl` |
|
| Policy file | `openbao/policies/workload-kv-read-llm-connect-provider-secrets.hcl` |
|
||||||
| Auth method | Kubernetes auth |
|
| Auth method | Kubernetes auth |
|
||||||
| Auth role | `llm-connect-provider-secrets-read` |
|
| Auth role | `external-secrets-activity-core` |
|
||||||
| Proposed service account | `llm-connect` |
|
| OpenBao auth service account | `external-secrets` |
|
||||||
| Proposed namespace | `activity-core` |
|
| OpenBao auth namespace | `external-secrets` |
|
||||||
| Delivery surface | External Secrets to `llm-connect-provider-secrets` in `activity-core` |
|
| 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` |
|
| ops-warden command | `warden access llm-connect-openrouter-api-key --fetch OPENROUTER_API_KEY` |
|
||||||
|
|
||||||
## Tasks
|
## Tasks
|
||||||
|
|
@ -96,7 +96,7 @@ The plan supports these `INTENT.md` principles:
|
||||||
|
|
||||||
```task
|
```task
|
||||||
id: RAILIANCE-WP-0010-T01
|
id: RAILIANCE-WP-0010-T01
|
||||||
status: todo
|
status: progress
|
||||||
priority: high
|
priority: high
|
||||||
state_hub_task_id: "307b75a6-a3a8-473b-b171-7379d2848698"
|
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
|
- The lane remains clearly platform-owned secret custody, not llm-connect model
|
||||||
routing or provider selection logic.
|
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
|
## T02 - Confirm Kubernetes auth and External Secrets binding
|
||||||
|
|
||||||
```task
|
```task
|
||||||
id: RAILIANCE-WP-0010-T02
|
id: RAILIANCE-WP-0010-T02
|
||||||
status: todo
|
status: done
|
||||||
priority: high
|
priority: high
|
||||||
state_hub_task_id: "829192f5-4502-44e0-8020-656d74d5282a"
|
state_hub_task_id: "829192f5-4502-44e0-8020-656d74d5282a"
|
||||||
```
|
```
|
||||||
|
|
@ -138,6 +146,19 @@ Acceptance:
|
||||||
`llm-connect-provider-secrets` or updated with the approved alternative.
|
`llm-connect-provider-secrets` or updated with the approved alternative.
|
||||||
- No direct human or agent read path is activated unless separately approved.
|
- 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
|
## T03 - Apply or confirm least-privilege OpenBao metadata
|
||||||
|
|
||||||
```task
|
```task
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue