feat: admit existing OpenBao catalog lanes
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 2s

This commit is contained in:
tegwick 2026-08-21 08:20:33 +02:00
parent 9d383442c8
commit 784be978bf
29 changed files with 1490 additions and 79 deletions

View file

@ -54,6 +54,7 @@ SECRETS_ENGINE_HUB_URL="" bash scripts/demo-e2e.sh
- whynot-design real publish closeout: [docs/whynot-design-real-publish-closeout.md](docs/whynot-design-real-publish-closeout.md)
- ops-warden routing contract: [docs/ops-warden-routing-contract.md](docs/ops-warden-routing-contract.md)
- Hardening backlog (exit bootstrap mode): [docs/hardening-backlog.md](docs/hardening-backlog.md)
- Existing-lane catalog admission: [docs/catalog-admission.md](docs/catalog-admission.md)
The implementation is a Python package (`src/secrets_engine/`). OpenBao is
reached only through the `bao` CLI adapter (`openbao.py`); the rest of the code

View file

@ -8,10 +8,13 @@
| Kind | ID | Status | Lane | Source |
| --- | --- | --- | --- | --- |
| workplan | ADHOC-2026-08-21 | finished | — | workplans/ADHOC-2026-08-21.md |
| workplan | SECRETS-WP-0001 | finished | — | workplans/SECRETS-WP-0001-statehub-bootstrap.md |
| workplan | SECRETS-WP-0002 | finished | — | workplans/SECRETS-WP-0002-bootstrap.md |
| workplan | SECRETS-WP-0004 | finished | — | workplans/SECRETS-WP-0004-warden-sign-token-lane.md |
| workplan | SECRETS-WP-0005 | finished | — | workplans/SECRETS-WP-0005-scope-intent-value-gaps.md |
| workplan | SECRETS-WP-0006 | active | — | workplans/SECRETS-WP-0006-catalog-lane-adoption.md |
| task | ADHOC-2026-08-21-T01 | done | — | workplans/ADHOC-2026-08-21.md |
| task | SECRETS-WP-0001-T01 | done | — | workplans/SECRETS-WP-0001-statehub-bootstrap.md |
| task | SECRETS-WP-0001-T02 | done | — | workplans/SECRETS-WP-0001-statehub-bootstrap.md |
| task | SECRETS-WP-0001-T03 | done | — | workplans/SECRETS-WP-0001-statehub-bootstrap.md |
@ -37,3 +40,9 @@
| task | SECRETS-WP-0005-T05 | done | — | workplans/SECRETS-WP-0005-scope-intent-value-gaps.md |
| task | SECRETS-WP-0005-T06 | done | — | workplans/SECRETS-WP-0005-scope-intent-value-gaps.md |
| task | SECRETS-WP-0005-T07 | done | — | workplans/SECRETS-WP-0005-scope-intent-value-gaps.md |
| task | SECRETS-WP-0006-T01 | done | — | workplans/SECRETS-WP-0006-catalog-lane-adoption.md |
| task | SECRETS-WP-0006-T02 | done | — | workplans/SECRETS-WP-0006-catalog-lane-adoption.md |
| task | SECRETS-WP-0006-T03 | progress | — | workplans/SECRETS-WP-0006-catalog-lane-adoption.md |
| task | SECRETS-WP-0006-T04 | done | — | workplans/SECRETS-WP-0006-catalog-lane-adoption.md |
| task | SECRETS-WP-0006-T05 | wait | — | workplans/SECRETS-WP-0006-catalog-lane-adoption.md |
| task | SECRETS-WP-0006-T06 | wait | — | workplans/SECRETS-WP-0006-catalog-lane-adoption.md |

View file

@ -0,0 +1,78 @@
# NON-SECRET catalog metadata derived from railiance-platform CCR-2026-0010.
id: email-connect-transactional
kind: kv
org: coulomb
repo: email-connect
stage: prod
description: >-
IONOS transactional SMTP identity and the shared user-engine ingest bearer for
email-connect. user-engine must never receive the SMTP fields.
mount: platform
path: workloads/email-connect/transactional
mount_management: existing
fields:
- EMAIL_CONNECT_INGEST_TOKEN
- EMAIL_CONNECT_SMTP_USERNAME
- EMAIL_CONNECT_SMTP_PASSWORD
consumers:
- name: email-connect-runtime
auth: token
claim: clustersecretstore:openbao-email-connect
purpose: sync email-connect-runtime through the namespace-limited ESO store
- name: user-engine-ingest-caller
auth: shared-bearer-copy
claim: service:user-engine
purpose: consume only EMAIL_CONNECT_INGEST_TOKEN from its separately managed runtime Secret
- name: secrets-engine-operator-exec
auth: approle
claim: catalog:email-connect-transactional
purpose: approved SMTP/ingest smoke in a child process and non-secret verification
workload_delivery:
- mode: external-secrets
owner: railiance-platform + email-connect
delivery_modes: [exec-env, read-check]
delivery_auth:
method: approle
management: engine
policy_name: se-prod-email-connect-transactional
role_name: se-prod-email-connect-transactional
token_ttl: 15m
token_max_ttl: 30m
secret_id_ttl: 15m
secret_id_num_uses: 1
token_num_uses: 8
approval:
model: ccr
decision_ref: CCR-2026-0010
notes: >-
CCR-2026-0010 approves the existing workload lane. A live native AppRole
apply remains gated by SECRETS-WP-0006-T05 review and operator approval.
verification:
positive: approved delivery AppRole can read each declared field without printing it
negative: unrelated token is denied on platform/data/workloads/email-connect/transactional
risk:
classification: high
notes: SMTP credentials can send as the platform identity and the ingest bearer authorizes transactional requests
rotation:
owner: railiance-platform + email-connect + user-engine
expectation: >-
Rotate overlap-first, wait for ESO refresh, roll email-connect and user-engine
when the bearer changes, verify health, then revoke the old provider password/bearer.
ttl: provider-defined
deactivation:
owner: railiance-platform + email-connect
expectation: >-
Disable the front door, detach ESO/native read access, remove materialized
Secrets, stop email-connect, and revoke the IONOS app password if applicable.
audit:
evidence: CCR id, actor, exact path, field names, ESO refresh timestamp, request ids, and pass/fail only

View file

@ -0,0 +1,77 @@
# NON-SECRET catalog metadata derived from railiance-platform CCR-2026-0006.
id: forgejo-admin-api-token
kind: kv
org: coulomb
repo: railiance-platform
stage: prod
description: >-
Forgejo site-admin PAT and non-secret companion metadata for attended operator
and automation tooling. This lane is distinct from Forgejo SMTP credentials.
mount: platform
path: workloads/forgejo/forgejo-admin
mount_management: existing
fields:
- API_TOKEN
- API_USER
- API_BASE_URL
- TOKEN_SCOPES
- GENERATED_AT
consumers:
- name: forgejo-platform-operators
auth: oidc
claim: group:net-kingdom-admins
purpose: existing attended reads through forgejo-admin-workload-kv-read
- name: forgejo-automation
auth: approle
claim: catalog:forgejo-admin-api-token
purpose: approved package, repository, webhook, and bootstrap commands in a child process
workload_delivery:
- mode: operator-oidc
owner: railiance-platform
delivery_modes: [exec-env, read-check]
delivery_auth:
method: approle
management: engine
# Avoid broad-admin name guards while retaining an exact-path read policy.
policy_name: se-prod-forgejo-operator-api-token
role_name: se-prod-forgejo-operator-api-token
token_ttl: 15m
token_max_ttl: 30m
secret_id_ttl: 15m
secret_id_num_uses: 1
token_num_uses: 8
approval:
model: ccr
decision_ref: CCR-2026-0006
notes: >-
CCR-2026-0006 approves the existing OIDC/operator lane. A live native
AppRole apply remains gated by SECRETS-WP-0006-T05 review and operator approval.
verification:
positive: approved delivery AppRole can read declared fields and an exec smoke authenticates to Forgejo
negative: unrelated token is denied on platform/data/workloads/forgejo/forgejo-admin
risk:
classification: high
notes: API_TOKEN is a Forgejo site-admin PAT with package, repository, webhook, and operator impact
rotation:
owner: railiance-platform + Forgejo operator
expectation: >-
Mint a replacement PAT as the site-admin account, update OpenBao, verify all
consumers through exec delivery, then delete the old PAT in Forgejo.
ttl: provider-defined
deactivation:
owner: railiance-platform + Forgejo operator
expectation: >-
Disable the front door, detach native/OIDC read access, revoke the PAT in
Forgejo, and preserve package/webhook audit evidence.
audit:
evidence: CCR id, actor, exact path, field names, Forgejo token identifier/scopes if non-secret, timestamps, and pass/fail only

View file

@ -0,0 +1,78 @@
# NON-SECRET catalog metadata derived from railiance-platform CCR-2026-0002.
id: issue-core-ingestion-api-key
kind: kv
org: coulomb
repo: issue-core
stage: prod
description: >-
Runtime credentials for issue-core ingestion and its Forgejo backend. The
existing workload path is delivered by ESO; native secrets-engine exec is a
separate exact-path AppRole proposed for attended operator/agent use.
mount: platform
path: workloads/issue-core/issue-core/issue-core-runtime
mount_management: existing
fields:
- ISSUE_CORE_API_KEY
- GITEA_BACKEND_TOKEN
consumers:
- name: issue-core-runtime
auth: kubernetes
claim: serviceaccount:external-secrets/external-secrets
purpose: sync the issue-core runtime Secret through the namespace-limited ESO store
- name: activity-core-issue-sink
auth: kubernetes
claim: serviceaccount:external-secrets/external-secrets
purpose: sync the shared ingestion key for activity-core issue emission
- name: secrets-engine-operator-exec
auth: approle
claim: catalog:issue-core-ingestion-api-key
purpose: approved child-process delivery and non-secret verification only
workload_delivery:
- mode: external-secrets
owner: railiance-platform + issue-core + activity-core
delivery_modes: [exec-env, read-check]
delivery_auth:
method: approle
management: engine
policy_name: se-prod-issue-core-ingestion-api-key
role_name: se-prod-issue-core-ingestion-api-key
token_ttl: 15m
token_max_ttl: 30m
secret_id_ttl: 15m
secret_id_num_uses: 1
token_num_uses: 8
approval:
model: ccr
decision_ref: CCR-2026-0002
notes: >-
CCR-2026-0002 approves the existing workload lane. A live native AppRole
apply remains gated by SECRETS-WP-0006-T05 review and operator approval.
verification:
positive: approved delivery AppRole can read both declared fields without printing them
negative: unrelated token is denied on platform/data/workloads/issue-core/issue-core/issue-core-runtime
risk:
classification: high
notes: path includes the shared ingestion bearer and the issue-core Forgejo backend token
rotation:
owner: railiance-platform + issue-core + activity-core
expectation: >-
Update OpenBao first, wait for both ESO consumers, roll issue-core and the
activity-core workers, then revoke the replaced provider credential.
ttl: provider-defined
deactivation:
owner: railiance-platform + issue-core
expectation: >-
Disable the front door, detach read access, remove materialized workload
Secrets, restart consumers, and preserve non-secret audit evidence.
audit:
evidence: CCR id, actor, exact path, field names, audit request ids, timestamps, and pass/fail only

View file

@ -0,0 +1,72 @@
# NON-SECRET catalog metadata derived from railiance-platform CCR-2026-0003.
id: openrouter-llm-connect
kind: kv
org: coulomb
repo: llm-connect
stage: prod
description: >-
OpenRouter provider credential used by llm-connect for activity-core. The
catalog does not generalize this key to other OpenRouter consumers.
mount: platform
path: workloads/activity-core/llm-connect/llm-connect-provider-secrets
mount_management: existing
fields:
- OPENROUTER_API_KEY
consumers:
- name: llm-connect-activity-core
auth: kubernetes
claim: serviceaccount:external-secrets/external-secrets
purpose: sync llm-connect-provider-secrets for the activity-core deployment
- name: secrets-engine-operator-exec
auth: approle
claim: catalog:openrouter-llm-connect
purpose: approved llm-connect smoke in a child process and non-secret verification
workload_delivery:
- mode: external-secrets
owner: railiance-platform + llm-connect
delivery_modes: [exec-env, read-check]
delivery_auth:
method: approle
management: engine
policy_name: se-prod-openrouter-llm-connect
role_name: se-prod-openrouter-llm-connect
token_ttl: 15m
token_max_ttl: 30m
secret_id_ttl: 15m
secret_id_num_uses: 1
token_num_uses: 8
approval:
model: ccr
decision_ref: CCR-2026-0003
notes: >-
CCR-2026-0003 approves the existing workload lane. A live native AppRole
apply remains gated by SECRETS-WP-0006-T05 review and operator approval.
verification:
positive: approved delivery AppRole can read OPENROUTER_API_KEY without printing it
negative: unrelated token is denied on platform/data/workloads/activity-core/llm-connect/llm-connect-provider-secrets
risk:
classification: high
notes: provider key can incur spend and remains valid outside OpenBao until revoked at OpenRouter
rotation:
owner: railiance-platform + llm-connect provider operator
expectation: >-
Mint a replacement at OpenRouter, update every live cluster copy, wait for
ESO refresh, restart llm-connect, verify, and revoke the old provider key.
ttl: provider-defined
deactivation:
owner: railiance-platform + llm-connect
expectation: >-
Disable the front door and workload read path, remove materialized Secrets,
stop consumers, and revoke the provider key at OpenRouter.
audit:
evidence: CCR id, actor, exact path, field name, provider key identifier if non-secret, timestamps, and pass/fail only

View file

@ -0,0 +1,77 @@
# NON-SECRET catalog metadata derived from railiance-platform CCR-2026-0005.
id: reuse-surface-hub-write-token
kind: kv
org: coulomb
repo: reuse-surface
stage: prod
description: >-
Production federation-hub write bearer and Forgejo webhook HMAC. ESO remains
the workload delivery path; native exec is a separate exact-path AppRole.
mount: platform
path: workloads/reuse/reuse-surface/runtime-secrets
mount_management: existing
fields:
- REUSE_SURFACE_TOKEN
- REUSE_SURFACE_FORGEJO_WEBHOOK_SECRET
consumers:
- name: reuse-surface-runtime
auth: kubernetes
claim: serviceaccount:external-secrets/external-secrets
purpose: sync reuse-surface-env through the namespace-limited ESO store
- name: forgejo-coulomb-org-webhook
auth: shared-hmac
claim: forgejo-org:coulomb
purpose: share the webhook HMAC with the reuse-surface receiver
- name: secrets-engine-operator-exec
auth: approle
claim: catalog:reuse-surface-hub-write-token
purpose: approved hub tooling in a child process and non-secret verification
workload_delivery:
- mode: external-secrets
owner: railiance-platform + reuse-surface + railiance-apps
delivery_modes: [exec-env, read-check]
delivery_auth:
method: approle
management: engine
policy_name: se-prod-reuse-surface-hub-write-token
role_name: se-prod-reuse-surface-hub-write-token
token_ttl: 15m
token_max_ttl: 30m
secret_id_ttl: 15m
secret_id_num_uses: 1
token_num_uses: 8
approval:
model: ccr
decision_ref: CCR-2026-0005
notes: >-
CCR-2026-0005 approves the existing workload lane. A live native AppRole
apply remains gated by SECRETS-WP-0006-T05 review and operator approval.
verification:
positive: approved delivery AppRole can read both declared fields without printing them
negative: unrelated token is denied on platform/data/workloads/reuse/reuse-surface/runtime-secrets
risk:
classification: high
notes: hub writes affect the production federation index and the HMAC has two coordinated consumers
rotation:
owner: railiance-platform + reuse-surface + railiance-apps
expectation: >-
Patch OpenBao, force ESO refresh, roll reuse-surface, and reconfigure the
Forgejo org webhook whenever its HMAC changes.
ttl: rotate-on-compromise
deactivation:
owner: railiance-platform + reuse-surface
expectation: >-
Disable the front door, detach ESO access, and remove the materialized
Secret only after confirming decommission or break-glass posture.
audit:
evidence: CCR id, actor, exact path, field names, ESO refresh timestamp, request ids, and pass/fail only

121
docs/catalog-admission.md Normal file
View file

@ -0,0 +1,121 @@
# Catalog Admission for Existing OpenBao Lanes
Catalog admission and infrastructure ownership are separate decisions. A lane
may already exist on a shared OpenBao mount and already reach its workload
through External Secrets Operator or Kubernetes auth. Adding that lane to the
secrets-engine catalog must not imply that secrets-engine may create the mount,
replace workload delivery, or broaden an existing role.
## Admission dimensions
### Mount management
`mount_management` describes only who creates the OpenBao secrets-engine mount.
| Value | Plan and apply behavior |
| --- | --- |
| `engine` | The guarded plan may include a `kv-mount` action; apply ensures the mount exists. This is the compatibility default for existing catalog entries. |
| `existing` | The plan emits a non-mutating `kv-mount-check`; apply never calls mount creation. Provisioning may still update the exact cataloged KV path after approval. |
Shared production mounts such as `platform` must use `existing` unless a
separate infrastructure decision explicitly delegates mount administration.
### Native delivery authentication
`delivery_auth` describes how secrets-engine itself obtains a scoped token for
positive verification and exec-time delivery. It does not describe or replace
workload authentication.
```yaml
delivery_auth:
method: approle
management: engine
# Optional safe overrides when the catalog id contains an admin-like term.
policy_name: se-prod-forgejo-operator-token
role_name: se-prod-forgejo-operator-token
```
| Management | Meaning |
| --- | --- |
| `engine` | A reviewed apply may create/update the exact read policy and AppRole for this lane. |
| `existing` | Policy and AppRole are externally managed. `role_name` is mandatory; plan/apply emit checks only and never mutate either object. |
| `none` | No native verification or exec delivery is available. Use only for catalog-only metadata without native delivery modes. |
The current native implementation supports AppRole. An entry that declares
`exec-env`, `exec-file`, `npm-config`, `read-check`, or `wrapped` must therefore
declare delivery auth. `exec-file` and `wrapped` remain reserved schema modes;
they are not implemented by `secrets-engine exec` yet.
Engine-managed AppRoles may bound `token_ttl`, `token_max_ttl`,
`secret_id_ttl`, `secret_id_num_uses`, and `token_num_uses`. The admitted
high-risk drafts use 15-minute tokens, a 30-minute maximum, single-use
`secret_id` values, and eight token uses.
### Existing workload delivery
`workload_delivery` records non-secret facts about delivery already operated by
another repository. Every item names both the mechanism and its owner.
```yaml
workload_delivery:
- mode: external-secrets
owner: rapp-example
```
This metadata is evidence and review context. `apply`, `provision`, `verify`,
and `exec` do not change ESO objects, Kubernetes auth roles, deployments, or
provider accounts.
## Example: existing production mount plus native exec adapter
```yaml
id: example-runtime-api-key
kind: kv
org: coulomb
repo: example-service
stage: prod
mount: platform
path: workloads/example-service/runtime
mount_management: existing
fields: [EXAMPLE_API_KEY]
consumers:
- name: example-service-runtime
auth: kubernetes
claim: system:serviceaccount:example:example-service
purpose: production runtime access through ESO
workload_delivery:
- mode: external-secrets
owner: rapp-example-service
delivery_modes: [exec-env, read-check]
delivery_auth:
method: approle
management: engine
# approval, verification, rotation, deactivation, and audit remain required.
```
The guarded plan may propose a new exact-path delivery policy and AppRole after
approval, but it emits only a check for the shared `platform` mount. The
existing Kubernetes/ESO delivery remains untouched.
## Admission checklist
Before accepting an existing production lane:
1. Confirm exact mount, path, and field names from the current owner.
2. Set `mount_management: existing` for shared or pre-provisioned mounts.
3. Record existing workload delivery and its repository owner.
4. Decide separately whether native delivery auth is engine-managed, existing,
or absent.
5. Use exact policy/role overrides when the catalog id would trip broad-admin
name guards; never weaken path or capability guards to accommodate a name.
6. Mark high-risk lanes explicitly; they require named rotation and
deactivation owners and cannot use bootstrap-only approval.
7. Link a resolved approval before live apply, provisioning, verification,
rotation, revoke, or exec. Every privileged live command fails closed when
that approval cannot be resolved.
8. Preserve the interim route until native positive and negative verification
passes without exposing a value.

View file

@ -15,6 +15,12 @@ npm registry/scope live in `delivery_config.npm` as data — the engine never
hardcodes a registry. The pilot publishes `@whynot/design` from the
`coulomb/whynot-design` repo to `https://forgejo.coulomb.social/api/packages/coulomb/npm/`.
Existing production lanes additionally distinguish mount ownership, native
delivery auth, and workload delivery. See
[catalog-admission.md](catalog-admission.md). In particular,
`mount_management: existing` makes mount handling non-mutating; it does not
authorize secrets-engine to replace an existing ESO/Kubernetes delivery path.
## Install
```bash

View file

@ -37,38 +37,56 @@ def apply_plan(client: OpenBaoClient, entry: CatalogEntry, plan: Plan, ttl: str
# 1. KV mount. Auth-capability lanes grant operational access on an existing
# mount (for example ssh/sign/<role>) and never create or store KV values.
if entry.stores_kv_value():
if entry.manages_mount:
if client.kv_mount_exists(entry.mount):
skipped.append(f"kv-mount {entry.mount} (already present)")
else:
client.ensure_kv_mount(entry.mount)
applied.append(f"kv-mount {entry.mount}")
elif entry.stores_kv_value():
skipped.append(f"kv-mount {entry.mount} (externally managed; no mutation)")
else:
skipped.append(f"kv-mount {entry.mount} (not applicable for {entry.kind})")
# 2. Consumer ACL policy (write only if changed).
current = client.read_policy(plan.policy_name)
if current and _normalize(current) == _normalize(plan.policy_hcl):
skipped.append(f"policy {plan.policy_name} (unchanged)")
if entry.manages_delivery_auth:
current = client.read_policy(plan.policy_name)
if current and _normalize(current) == _normalize(plan.policy_hcl):
skipped.append(f"policy {plan.policy_name} (unchanged)")
else:
client.write_policy(plan.policy_name, plan.policy_hcl)
applied.append(f"policy {plan.policy_name}")
else:
client.write_policy(plan.policy_name, plan.policy_hcl)
applied.append(f"policy {plan.policy_name}")
skipped.append(f"policy {plan.policy_name} (externally managed; no mutation)")
# 3. Consumer approle bound to that policy.
client.ensure_approle_enabled()
if entry.kind == "auth-capability":
client.write_approle(
plan.role_name,
[plan.policy_name],
ttl=entry.token_ttl,
max_ttl=entry.token_max_ttl,
secret_id_ttl=entry.secret_id_ttl,
secret_id_num_uses=entry.secret_id_num_uses,
token_num_uses=entry.token_num_uses,
)
if entry.manages_delivery_auth:
client.ensure_approle_enabled()
if entry.kind == "auth-capability":
client.write_approle(
plan.role_name,
[plan.policy_name],
ttl=entry.token_ttl,
max_ttl=entry.token_max_ttl,
secret_id_ttl=entry.secret_id_ttl,
secret_id_num_uses=entry.secret_id_num_uses,
token_num_uses=entry.token_num_uses,
)
else:
client.write_approle(
plan.role_name,
[plan.policy_name],
ttl=entry.delivery_token_ttl or ttl,
max_ttl=entry.delivery_token_max_ttl,
secret_id_ttl=entry.delivery_secret_id_ttl,
secret_id_num_uses=entry.delivery_secret_id_num_uses,
token_num_uses=entry.delivery_token_num_uses,
)
applied.append(f"approle {plan.role_name} -> [{plan.policy_name}]")
elif entry.has_delivery_auth:
skipped.append(f"approle {plan.role_name} (externally managed; no mutation)")
else:
client.write_approle(plan.role_name, [plan.policy_name], ttl=ttl)
applied.append(f"approle {plan.role_name} -> [{plan.policy_name}]")
skipped.append("approle (no native delivery auth declared)")
return ApplyResult(applied=applied, skipped=skipped)

View file

@ -27,6 +27,10 @@ VALID_DELIVERY_MODES = (
"approle-login",
)
VALID_APPROVAL_MODELS = ("decision", "ccr", "dual-control", "bootstrap-only")
VALID_MOUNT_MANAGEMENT = ("engine", "existing")
VALID_DELIVERY_AUTH_MANAGEMENT = ("engine", "existing", "none")
VALID_DELIVERY_AUTH_METHODS = ("approle", "none")
VALID_RISK_CLASSIFICATIONS = ("standard", "high")
REQUIRED_FIELDS = (
"id",
@ -66,6 +70,10 @@ class CatalogEntry:
rotation: dict[str, Any]
deactivation: dict[str, Any]
audit: dict[str, Any]
mount_management: str = "engine"
delivery_auth: dict[str, Any] = field(default_factory=dict)
workload_delivery: list[dict[str, Any]] = field(default_factory=list)
risk: dict[str, Any] = field(default_factory=dict)
delivery_config: dict[str, Any] = field(default_factory=dict)
auth_capability: dict[str, Any] = field(default_factory=dict)
description: str = ""
@ -95,14 +103,62 @@ class CatalogEntry:
def policy_name(self) -> str:
if self.kind == "auth-capability":
return self.auth_capability.get("policy_name") or self.id
if self.delivery_auth.get("policy_name"):
return str(self.delivery_auth["policy_name"])
return f"se-{self.stage}-{self.id}"
@property
def role_name(self) -> str:
if self.kind == "auth-capability":
return self.auth_capability.get("role_name") or self.id
if self.delivery_auth.get("role_name"):
return str(self.delivery_auth["role_name"])
return f"se-{self.stage}-{self.id}"
@property
def manages_mount(self) -> bool:
return self.kind == "kv" and self.mount_management == "engine"
@property
def delivery_auth_method(self) -> str:
if self.kind == "auth-capability":
return "approle"
return str(self.delivery_auth.get("method", "approle"))
@property
def delivery_auth_management(self) -> str:
if self.kind == "auth-capability":
return "engine"
return str(self.delivery_auth.get("management", "engine"))
@property
def manages_delivery_auth(self) -> bool:
return self.delivery_auth_management == "engine"
@property
def has_delivery_auth(self) -> bool:
return self.delivery_auth_management != "none"
@property
def delivery_token_ttl(self) -> str:
return str(self.delivery_auth.get("token_ttl", "30m"))
@property
def delivery_token_max_ttl(self) -> str:
return str(self.delivery_auth.get("token_max_ttl", self.delivery_token_ttl))
@property
def delivery_secret_id_ttl(self) -> str:
return str(self.delivery_auth.get("secret_id_ttl", self.delivery_token_ttl))
@property
def delivery_secret_id_num_uses(self) -> int:
return int(self.delivery_auth.get("secret_id_num_uses", 0))
@property
def delivery_token_num_uses(self) -> int:
return int(self.delivery_auth.get("token_num_uses", 0))
@property
def auth_allowed_paths(self) -> dict[str, list[str]]:
"""Allowed OpenBao paths/capabilities for an auth-capability lane."""
@ -156,8 +212,16 @@ def validate_entry(data: dict[str, Any], *, source: str = "<memory>") -> Catalog
)
if data["kind"] == "auth-capability":
data.setdefault("fields", [])
data.setdefault("mount_management", "existing")
data.setdefault("delivery_auth", {})
data.setdefault("workload_delivery", [])
else:
data.setdefault("auth_capability", {})
data.setdefault("mount_management", "engine")
data.setdefault(
"delivery_auth", {"method": "approle", "management": "engine"}
)
data.setdefault("workload_delivery", [])
missing = [k for k in REQUIRED_FIELDS if k not in data or data[k] in (None, "", [], {})]
if data["kind"] == "kv" and not data.get("fields"):
@ -195,6 +259,75 @@ def validate_entry(data: dict[str, Any], *, source: str = "<memory>") -> Catalog
f"{source}: each consumer needs at least 'name' and 'auth'"
)
if data["mount_management"] not in VALID_MOUNT_MANAGEMENT:
raise CatalogError(
f"{source}: mount_management '{data['mount_management']}' invalid; "
f"must be one of {VALID_MOUNT_MANAGEMENT}"
)
workload_delivery = data["workload_delivery"]
if not isinstance(workload_delivery, list):
raise CatalogError(f"{source}: workload_delivery must be a list")
for item in workload_delivery:
if (
not isinstance(item, dict)
or not isinstance(item.get("mode"), str)
or not item["mode"].strip()
or not isinstance(item.get("owner"), str)
or not item["owner"].strip()
):
raise CatalogError(
f"{source}: each workload_delivery item needs non-empty mode and owner"
)
if data["kind"] == "kv":
delivery_auth = data["delivery_auth"]
if not isinstance(delivery_auth, dict):
raise CatalogError(f"{source}: delivery_auth must be a mapping")
auth_method = delivery_auth.get("method", "approle")
auth_management = delivery_auth.get("management", "engine")
if auth_method not in VALID_DELIVERY_AUTH_METHODS:
raise CatalogError(
f"{source}: delivery_auth.method '{auth_method}' invalid; "
f"must be one of {VALID_DELIVERY_AUTH_METHODS}"
)
if auth_management not in VALID_DELIVERY_AUTH_MANAGEMENT:
raise CatalogError(
f"{source}: delivery_auth.management '{auth_management}' invalid; "
f"must be one of {VALID_DELIVERY_AUTH_MANAGEMENT}"
)
if auth_management == "none" and auth_method != "none":
raise CatalogError(
f"{source}: delivery_auth.management none requires method none"
)
if auth_management != "none" and auth_method != "approle":
raise CatalogError(
f"{source}: current native delivery auth must use approle"
)
if auth_management == "existing" and not delivery_auth.get("role_name"):
raise CatalogError(
f"{source}: existing delivery auth requires delivery_auth.role_name"
)
for ttl_field in ("token_ttl", "token_max_ttl", "secret_id_ttl"):
if ttl_field in delivery_auth and (
not isinstance(delivery_auth[ttl_field], str)
or not delivery_auth[ttl_field].strip()
):
raise CatalogError(
f"{source}: delivery_auth.{ttl_field} must be a non-empty string"
)
for uses_field in ("secret_id_num_uses", "token_num_uses"):
uses = delivery_auth.get(uses_field, 0)
if isinstance(uses, bool) or not isinstance(uses, int) or uses < 0:
raise CatalogError(
f"{source}: delivery_auth.{uses_field} must be a non-negative integer"
)
native_modes = {"exec-env", "exec-file", "npm-config", "read-check", "wrapped"}
if native_modes.intersection(modes) and auth_management == "none":
raise CatalogError(
f"{source}: native delivery/verification modes require delivery_auth"
)
approval = data["approval"]
if not isinstance(approval, dict) or "model" not in approval:
raise CatalogError(f"{source}: approval must include a 'model'")
@ -204,6 +337,27 @@ def validate_entry(data: dict[str, Any], *, source: str = "<memory>") -> Catalog
f"allowed {VALID_APPROVAL_MODELS}"
)
risk = data.get("risk", {})
if not isinstance(risk, dict):
raise CatalogError(f"{source}: risk must be a mapping")
classification = risk.get("classification", "standard")
if classification not in VALID_RISK_CLASSIFICATIONS:
raise CatalogError(
f"{source}: risk.classification '{classification}' invalid; "
f"must be one of {VALID_RISK_CLASSIFICATIONS}"
)
if classification == "high":
if approval["model"] == "bootstrap-only":
raise CatalogError(
f"{source}: high-risk lanes cannot use bootstrap-only approval"
)
for lifecycle_name in ("rotation", "deactivation"):
lifecycle = data[lifecycle_name]
if not isinstance(lifecycle, dict) or not lifecycle.get("owner"):
raise CatalogError(
f"{source}: high-risk lanes require {lifecycle_name}.owner"
)
# npm-config delivery must declare WHERE it publishes (registry + scope), so
# the registry is catalog data, never hardcoded in the engine.
if "npm-config" in modes:

View file

@ -63,6 +63,19 @@ def _writer(cfg: Config) -> EvidenceWriter:
return EvidenceWriter(evidence_dir=cfg.evidence_dir, hub_url=cfg.hub_url, topic_id=cfg.topic_id)
def _require_lane_approval(cfg: Config, entry):
"""Resolve and enforce the lane approval for a privileged live action."""
if not entry.approval_required():
return None
decision = resolve_decision(
hub_url=cfg.hub_url,
repo_root=repo_root(),
decision_ref=entry.approval.get("decision_ref", entry.id),
)
require_approved(entry, decision)
return decision
# -- command handlers ------------------------------------------------------
@ -89,13 +102,23 @@ def cmd_catalog_show(cfg: Config, args) -> int:
print(f"openbao: mount={e.mount} allowed={sorted(e.auth_allowed_paths)}")
print(f"approle: {e.role_name} policy={e.policy_name}")
else:
print(f"openbao: {e.mount}/{e.path} fields={e.fields}")
print(
f"openbao: {e.mount}/{e.path} fields={e.fields} "
f"mount_management={e.mount_management}"
)
print(
f"delivery auth: {e.delivery_auth_method}/"
f"{e.delivery_auth_management} role={e.role_name if e.has_delivery_auth else '-'}"
)
print(f"workload: {e.workload_delivery}")
print(f"consumers: {[c['name'] for c in e.consumers]}")
print(f"delivery: {e.delivery_modes}")
print(f"approval: {e.approval.get('model')} ref={e.approval.get('decision_ref','')}")
print(f"verification: {e.verification}")
print(f"rotation: {e.rotation}")
print(f"deactivation: {e.deactivation}")
if e.risk:
print(f"risk: {e.risk}")
print(f"description: {e.description.strip()}")
return 0
@ -172,6 +195,7 @@ def cmd_provision(cfg: Config, args) -> int:
if args.stage != entry.stage:
from secrets_engine.errors import ProvisioningError
raise ProvisioningError(f"lane '{entry.id}' is stage '{entry.stage}', not '{args.stage}'")
decision = _require_lane_approval(cfg, entry)
client = OpenBaoClient.resolve(cfg.bao_addr, bootstrap_token_file=args.bootstrap_token_file)
field = args.field or (entry.fields[0] if entry.fields else "")
if args.generate:
@ -182,12 +206,13 @@ def cmd_provision(cfg: Config, args) -> int:
mode = "from-file"
print(f"provisioned lane '{entry.id}' field '{f}' ({mode}) — value not displayed")
_writer(cfg).record("provision", result=mode, catalog_id=entry.id, stage=entry.stage,
detail={"field": f})
decision_id=decision.id if decision else "", detail={"field": f})
return 0
def cmd_verify(cfg: Config, args) -> int:
entry = get_entry(cfg.catalog_dir, args.catalog_id)
decision = _require_lane_approval(cfg, entry)
client = OpenBaoClient.resolve(cfg.bao_addr, bootstrap_token_file=args.bootstrap_token_file)
field = args.field or (entry.fields[0] if entry.fields else "")
if entry.stores_kv_value() and not field:
@ -202,7 +227,8 @@ def cmd_verify(cfg: Config, args) -> int:
if not r.passed:
rc = 7
_writer(cfg).record("verify", result=f"{r.check}:{'pass' if r.passed else 'fail'}",
catalog_id=entry.id, stage=entry.stage, detail=r.detail)
catalog_id=entry.id, stage=entry.stage,
decision_id=decision.id if decision else "", detail=r.detail)
return rc
@ -215,13 +241,7 @@ def cmd_handoff(cfg: Config, args) -> int:
raise ProvisioningError(f"lane '{entry.id}' is stage '{entry.stage}', not '{args.stage}'")
if entry.kind != "auth-capability":
raise ProvisioningError(f"lane '{entry.id}' is {entry.kind}; handoff needs auth-capability")
decision = None
if entry.approval_required():
decision = resolve_decision(
hub_url=cfg.hub_url, repo_root=repo_root(),
decision_ref=entry.approval.get("decision_ref", entry.id),
)
require_approved(entry, decision)
decision = _require_lane_approval(cfg, entry)
client = OpenBaoClient.resolve(cfg.bao_addr, bootstrap_token_file=args.bootstrap_token_file)
result = write_approle_handoff(
client,
@ -257,12 +277,7 @@ def cmd_exec(cfg: Config, args) -> int:
from secrets_engine.exec_delivery import exec_with_secret
entry = get_entry(cfg.catalog_dir, args.catalog)
# require approval + readiness before running.
if entry.approval_required():
decision = resolve_decision(
hub_url=cfg.hub_url, repo_root=repo_root(),
decision_ref=entry.approval.get("decision_ref", entry.id),
)
require_approved(entry, decision)
decision = _require_lane_approval(cfg, entry)
if not args.command:
from secrets_engine.errors import DeliveryError
raise DeliveryError("no command after '--'")
@ -270,9 +285,11 @@ def cmd_exec(cfg: Config, args) -> int:
field = args.field or (entry.fields[0] if entry.fields else "")
w = _writer(cfg)
w.record("exec", result="attempt", catalog_id=entry.id, stage=entry.stage,
decision_id=decision.id if decision else "",
detail={"command": args.command[0], "mode": args.mode})
rc = exec_with_secret(client, entry, field, args.command, mode=args.mode)
w.record("exec", result=f"exit-{rc}", catalog_id=entry.id, stage=entry.stage,
decision_id=decision.id if decision else "",
detail={"command": args.command[0]})
return rc
@ -322,6 +339,7 @@ def cmd_route(cfg: Config, args) -> int:
def cmd_revoke(cfg: Config, args) -> int:
entry = get_entry(cfg.catalog_dir, args.catalog_id)
decision = None if args.dry_run else _require_lane_approval(cfg, entry)
client = OpenBaoClient.resolve(cfg.bao_addr, bootstrap_token_file=args.bootstrap_token_file)
if entry.kind == "auth-capability":
if args.dry_run:
@ -336,7 +354,10 @@ def cmd_revoke(cfg: Config, args) -> int:
f"revoked lane '{entry.id}': deleted approle {entry.role_name} "
f"and policy {entry.policy_name}"
)
_writer(cfg).record("revoke", result="auth-capability-deactivated", catalog_id=entry.id, stage=entry.stage)
_writer(cfg).record(
"revoke", result="auth-capability-deactivated", catalog_id=entry.id,
stage=entry.stage, decision_id=decision.id if decision else ""
)
return 0
if args.dry_run:
print(f"(dry-run) would delete KV metadata {entry.mount}/{entry.path} "
@ -344,7 +365,10 @@ def cmd_revoke(cfg: Config, args) -> int:
return 0
client.kv_delete_metadata(entry.mount, entry.path)
print(f"revoked lane '{entry.id}': KV metadata deleted at {entry.mount}/{entry.path}")
_writer(cfg).record("revoke", result="deactivated", catalog_id=entry.id, stage=entry.stage)
_writer(cfg).record(
"revoke", result="deactivated", catalog_id=entry.id, stage=entry.stage,
decision_id=decision.id if decision else ""
)
return 0

View file

@ -52,6 +52,10 @@ def resolve_npm_token_env(entry: CatalogEntry, *, policy_dir=None) -> str:
def _fetch_value(client: OpenBaoClient, entry: CatalogEntry, field: str) -> str:
"""Read the field value via an approle-scoped token. Held in memory only."""
if entry.delivery_auth_method != "approle" or not entry.has_delivery_auth:
raise DeliveryError(
f"lane '{entry.id}' has no AppRole delivery auth for native exec"
)
try:
token = client.approle_login_token(entry.role_name)
except Exception as e:
@ -126,6 +130,10 @@ def exec_with_secret(
"""
if not command:
raise DeliveryError("no command given to exec")
if field not in entry.fields:
raise DeliveryError(
f"field '{field}' not declared in lane '{entry.id}' fields {entry.fields}"
)
declared = set(entry.delivery_modes)
if mode == "auto":

View file

@ -13,6 +13,7 @@ from pathlib import Path
from secrets_engine.catalog import CatalogEntry
from secrets_engine.errors import ProvisioningError
from secrets_engine.openbao import OpenBaoClient
from secrets_engine.safe_paths import containing_git_worktree
@dataclass(frozen=True)
@ -27,12 +28,12 @@ class HandoffResult:
def _assert_outside_git_worktree(path: Path) -> Path:
resolved = path.expanduser().resolve()
for parent in (resolved.parent, *resolved.parent.parents):
if (parent / ".git").exists():
raise ProvisioningError(
f"handoff file {resolved} is inside a Git worktree ({parent}); "
"keep AppRole material outside repos"
)
worktree = containing_git_worktree(resolved)
if worktree is not None:
raise ProvisioningError(
f"handoff file {resolved} is inside a Git worktree ({worktree}); "
"keep AppRole material outside repos"
)
return resolved

View file

@ -23,6 +23,7 @@ from dataclasses import dataclass
from pathlib import Path
from secrets_engine.errors import BackendError, ProvisioningError
from secrets_engine.safe_paths import containing_git_worktree
def _check_token_file(path: Path) -> str:
@ -36,12 +37,12 @@ def _check_token_file(path: Path) -> str:
f"(mode {oct(st.st_mode & 0o777)}); must be 0600"
)
# Refuse a token file living inside a Git worktree.
for parent in path.resolve().parents:
if (parent / ".git").exists():
raise ProvisioningError(
f"bootstrap token file {path} is inside a Git worktree ({parent}); "
"store it outside any repo"
)
worktree = containing_git_worktree(path)
if worktree is not None:
raise ProvisioningError(
f"bootstrap token file {path} is inside a Git worktree ({worktree}); "
"store it outside any repo"
)
token = path.read_text(encoding="utf-8").strip()
if not token:
raise ProvisioningError(f"bootstrap token file {path} is empty")

View file

@ -21,7 +21,7 @@ from secrets_engine.roles import (
@dataclass
class PlanAction:
kind: str # "kv-mount" | "policy" | "approle"
kind: str # mutation or non-mutating check/preview action
target: str # human-readable target
detail: dict[str, Any] = field(default_factory=dict)
@ -87,19 +87,60 @@ def build_plan(entry: CatalogEntry, stage: str, *, decision_id: str = "") -> Pla
assert_path_in_stage(entry) # path must be in-stage, no wildcards
policy_name, policy_hcl = consumer_policy_for(entry) # runs assert_policy_safe
actions = [
PlanAction("kv-mount", entry.mount, {"type": "kv-v2"}),
PlanAction(
"policy",
policy_name,
{"paths": f"{entry.mount}/data/{entry.path}"},
),
PlanAction(
"approle",
entry.role_name,
{"token_policies": policy_name, "auth": "approle"},
),
]
mount_action = (
PlanAction("kv-mount", entry.mount, {"type": "kv-v2", "management": "engine"})
if entry.manages_mount
else PlanAction(
"kv-mount-check",
entry.mount,
{"type": "kv-v2", "management": "existing", "mutation": "none"},
)
)
actions = [mount_action]
if entry.manages_delivery_auth:
actions.extend(
[
PlanAction(
"policy",
policy_name,
{"paths": f"{entry.mount}/data/{entry.path}"},
),
PlanAction(
"approle",
entry.role_name,
{
"token_policies": policy_name,
"auth": "approle",
"token_ttl": entry.delivery_token_ttl,
"token_max_ttl": entry.delivery_token_max_ttl,
"token_num_uses": entry.delivery_token_num_uses,
},
),
]
)
elif entry.has_delivery_auth:
actions.extend(
[
PlanAction(
"policy-check",
policy_name,
{"paths": f"{entry.mount}/data/{entry.path}", "mutation": "none"},
),
PlanAction(
"approle-check",
entry.role_name,
{"auth": "approle", "management": "existing", "mutation": "none"},
),
]
)
else:
actions.append(
PlanAction(
"policy-preview",
policy_name,
{"paths": f"{entry.mount}/data/{entry.path}", "mutation": "none"},
)
)
return Plan(
catalog_id=entry.id,
stage=stage,

View file

@ -18,6 +18,7 @@ from pathlib import Path
from secrets_engine.catalog import CatalogEntry
from secrets_engine.errors import ProvisioningError
from secrets_engine.openbao import OpenBaoClient
from secrets_engine.safe_paths import containing_git_worktree
def _read_value_file(path: Path) -> str:
@ -29,12 +30,12 @@ def _read_value_file(path: Path) -> str:
f"value file {path} is group/other-accessible "
f"(mode {oct(st.st_mode & 0o777)}); must be 0600"
)
for parent in path.resolve().parents:
if (parent / ".git").exists():
raise ProvisioningError(
f"value file {path} is inside a Git worktree ({parent}); "
"keep secret material outside repos"
)
worktree = containing_git_worktree(path)
if worktree is not None:
raise ProvisioningError(
f"value file {path} is inside a Git worktree ({worktree}); "
"keep secret material outside repos"
)
value = path.read_text(encoding="utf-8").strip()
if not value:
raise ProvisioningError(f"value file {path} is empty")
@ -54,7 +55,8 @@ def provision_from_file(
f"field '{field}' not declared in lane '{entry.id}' fields {entry.fields}"
)
value = _read_value_file(Path(file_path))
client.ensure_kv_mount(entry.mount)
if entry.manages_mount:
client.ensure_kv_mount(entry.mount)
client.kv_put(entry.mount, entry.path, field, value)
del value
return field
@ -77,7 +79,8 @@ def provision_generated(client: OpenBaoClient, entry: CatalogEntry, field: str)
)
alphabet = string.ascii_letters + string.digits
value = "test-" + "".join(_secrets.choice(alphabet) for _ in range(32))
client.ensure_kv_mount(entry.mount)
if entry.manages_mount:
client.ensure_kv_mount(entry.mount)
client.kv_put(entry.mount, entry.path, field, value)
del value
return field

View file

@ -60,9 +60,10 @@ def route_lane(
metadata_applied = False
value_present = False
if client is not None and client.is_reachable():
policy_applied = client.read_policy(entry.policy_name) is not None
role_applied = client.approle_exists(entry.role_name)
metadata_applied = policy_applied and role_applied
if entry.has_delivery_auth:
policy_applied = client.read_policy(entry.policy_name) is not None
role_applied = client.approle_exists(entry.role_name)
metadata_applied = policy_applied and role_applied
if entry.stores_kv_value():
# Presence check uses the engine's own token; reports boolean only.
field = entry.fields[0] if entry.fields else ""
@ -80,8 +81,17 @@ def route_lane(
missing = f"approved decision for '{decision_ref}'"
next_command = f"secrets-engine decision inspect {decision_ref or entry.id}"
elif not metadata_applied:
missing = "OpenBao policy/role apply"
next_command = f"secrets-engine apply {decision_ref or entry.id} --stage {entry.stage}"
if entry.kind == "kv" and entry.delivery_auth_management == "existing":
missing = "externally managed OpenBao policy/AppRole readiness"
next_command = (
f"secrets-engine verify {entry.id} --positive --negative"
)
elif entry.kind == "kv" and not entry.has_delivery_auth:
missing = "native delivery auth declaration"
next_command = f"secrets-engine plan {decision_ref or entry.id} --stage {entry.stage}"
else:
missing = "OpenBao policy/role apply"
next_command = f"secrets-engine apply {decision_ref or entry.id} --stage {entry.stage}"
elif entry.kind == "auth-capability":
missing = ""
next_command = (
@ -96,7 +106,12 @@ def route_lane(
)
else:
missing = ""
next_command = f"secrets-engine exec --catalog {entry.id} -- <command...>"
if {"exec-env", "npm-config"}.intersection(entry.delivery_modes):
next_command = f"secrets-engine exec --catalog {entry.id} -- <command...>"
else:
next_command = (
f"secrets-engine verify {entry.id} --positive --negative"
)
return RouteResult(
catalog_id=entry.id,

View file

@ -0,0 +1,20 @@
"""Filesystem checks shared by secret provisioning and handoff paths."""
from __future__ import annotations
from pathlib import Path
def containing_git_worktree(path: Path) -> Path | None:
"""Return the nearest enclosing Git worktree, if one is identifiable.
A real worktree has either a ``.git`` file (linked worktrees/submodules) or
a ``.git`` directory containing ``HEAD``. Merely finding an empty directory
named ``.git`` is not enough; sandbox and test environments may use such a
marker outside any repository.
"""
resolved = path.expanduser().resolve()
for parent in (resolved.parent, *resolved.parent.parents):
marker = parent / ".git"
if marker.is_file() or (marker.is_dir() and (marker / "HEAD").is_file()):
return parent
return None

View file

@ -29,6 +29,12 @@ class VerifyResult:
def verify_positive(client: OpenBaoClient, entry: CatalogEntry, field: str) -> VerifyResult:
"""Approved consumer token must be able to read the field."""
if entry.delivery_auth_method != "approle" or not entry.has_delivery_auth:
return VerifyResult(
"positive",
False,
{"reason": "lane has no AppRole delivery auth", "path": entry.path},
)
try:
token = client.approle_login_token(entry.role_name)
except Exception as e: # backend errors -> failed verification, not a value leak
@ -145,6 +151,10 @@ def verify_auth_capability_negative(client: OpenBaoClient, entry: CatalogEntry)
def run_verification(
client: OpenBaoClient, entry: CatalogEntry, field: str, *, positive: bool, negative: bool
) -> list[VerifyResult]:
if entry.kind == "kv" and field not in entry.fields:
raise VerificationError(
f"field '{field}' not declared in lane '{entry.id}' fields {entry.fields}"
)
results: list[VerifyResult] = []
if entry.kind == "auth-capability":
if positive:

View file

@ -74,6 +74,86 @@ def test_unknown_delivery_mode_rejected():
validate_entry(data)
def test_existing_mount_and_workload_delivery_parse():
data = copy.deepcopy(VALID)
data["mount"] = "platform"
data["mount_management"] = "existing"
data["workload_delivery"] = [
{"mode": "external-secrets", "owner": "rapp-example"}
]
entry = validate_entry(data)
assert entry.manages_mount is False
assert entry.manages_delivery_auth is True
assert entry.workload_delivery[0]["mode"] == "external-secrets"
def test_existing_delivery_auth_requires_explicit_role():
data = copy.deepcopy(VALID)
data["delivery_auth"] = {"method": "approle", "management": "existing"}
with pytest.raises(CatalogError):
validate_entry(data)
data["delivery_auth"]["role_name"] = "existing-exact-role"
entry = validate_entry(data)
assert entry.manages_delivery_auth is False
assert entry.role_name == "existing-exact-role"
def test_native_delivery_rejects_missing_auth():
data = copy.deepcopy(VALID)
data["delivery_auth"] = {"method": "none", "management": "none"}
with pytest.raises(CatalogError):
validate_entry(data)
def test_workload_delivery_requires_mode_and_owner():
data = copy.deepcopy(VALID)
data["workload_delivery"] = [{"mode": "external-secrets"}]
with pytest.raises(CatalogError):
validate_entry(data)
def test_high_risk_lane_requires_owners_and_non_bootstrap_approval():
data = copy.deepcopy(VALID)
data["risk"] = {"classification": "high"}
data["approval"] = {"model": "decision", "decision_ref": "d1"}
with pytest.raises(CatalogError):
validate_entry(data)
data["rotation"]["owner"] = "platform-owner"
data["deactivation"]["owner"] = "platform-owner"
assert validate_entry(data).risk["classification"] == "high"
data["approval"] = {"model": "bootstrap-only"}
with pytest.raises(CatalogError):
validate_entry(data)
def test_admitted_existing_lanes_have_exact_safe_metadata():
entries = load_catalog(repo_root() / "catalog")
expected = {
"issue-core-ingestion-api-key": "workloads/issue-core/issue-core/issue-core-runtime",
"reuse-surface-hub-write-token": "workloads/reuse/reuse-surface/runtime-secrets",
"openrouter-llm-connect": "workloads/activity-core/llm-connect/llm-connect-provider-secrets",
"forgejo-admin-api-token": "workloads/forgejo/forgejo-admin",
"email-connect-transactional": "workloads/email-connect/transactional",
}
for lane_id, path in expected.items():
entry = entries[lane_id]
assert entry.mount == "platform"
assert entry.path == path
assert entry.manages_mount is False
assert entry.manages_delivery_auth is True
assert entry.risk["classification"] == "high"
assert entry.rotation["owner"]
assert entry.deactivation["owner"]
assert entry.workload_delivery
assert entry.approval["decision_ref"].startswith("CCR-2026-")
def test_generic_and_identity_routes_are_not_catalog_lanes():
entries = load_catalog(repo_root() / "catalog")
assert "openbao-api-key" not in entries
assert "key-cape-oidc-login" not in entries
def test_wildcard_path_rejected():
data = copy.deepcopy(VALID)
data["path"] = "test/*"

View file

@ -1,6 +1,8 @@
import pytest
from types import SimpleNamespace
from secrets_engine.catalog import validate_entry
from secrets_engine.cli import _require_lane_approval
from secrets_engine.decisions import Decision, require_approved, resolve_decision
from secrets_engine.errors import DecisionError
@ -50,3 +52,23 @@ def test_local_fixture_resolves(tmp_path):
def test_missing_decision_raises(tmp_path):
with pytest.raises(DecisionError):
resolve_decision(hub_url="http://127.0.0.1:1", repo_root=tmp_path, decision_ref="nope")
def test_privileged_lane_helper_fails_closed_without_decision(tmp_path, monkeypatch):
import secrets_engine.cli as cli
monkeypatch.setattr(cli, "repo_root", lambda: tmp_path)
with pytest.raises(DecisionError):
_require_lane_approval(SimpleNamespace(hub_url=""), _approved())
def test_privileged_lane_helper_accepts_local_approval(tmp_path, monkeypatch):
import secrets_engine.cli as cli
(tmp_path / ".decisions").mkdir()
(tmp_path / ".decisions" / "x.yaml").write_text(
"id: x\ntitle: approved\nstatus: resolved\nsuperseded_by: null\n"
)
monkeypatch.setattr(cli, "repo_root", lambda: tmp_path)
decision = _require_lane_approval(SimpleNamespace(hub_url=""), _approved())
assert decision.id == "x"

View file

@ -1,4 +1,15 @@
from secrets_engine.exec_delivery import _npm_userconfig, _registry_authkey
import copy
import pytest
from secrets_engine.catalog import validate_entry
from secrets_engine.errors import DeliveryError
from secrets_engine.exec_delivery import (
_npm_userconfig,
_registry_authkey,
exec_with_secret,
)
from tests.test_catalog import VALID
def test_registry_authkey_strips_scheme_and_trails_slash():
@ -22,3 +33,40 @@ def test_npm_userconfig_writes_registry_and_token_ref_not_value():
assert (path.stat().st_mode & 0o077) == 0
# cleaned up on context exit
assert not path.exists()
def test_exec_env_injects_only_selected_declared_field(monkeypatch):
data = copy.deepcopy(VALID)
data["fields"] = ["primary", "selected_value"]
entry = validate_entry(data)
def fake_fetch(_client, got_entry, field):
assert got_entry == entry
assert field == "selected_value"
return "test-secret-value"
def fake_spawn(command, env, secret):
assert command == ["probe"]
assert secret == "test-secret-value"
assert env["SELECTED_VALUE"] == "test-secret-value"
assert "PRIMARY" not in env
return 0
monkeypatch.setattr("secrets_engine.exec_delivery._fetch_value", fake_fetch)
monkeypatch.setattr("secrets_engine.exec_delivery._spawn", fake_spawn)
assert (
exec_with_secret(
object(), entry, "selected_value", ["probe"], mode="exec-env"
)
== 0
)
def test_exec_rejects_undeclared_field_before_fetch(monkeypatch):
entry = validate_entry(VALID)
monkeypatch.setattr(
"secrets_engine.exec_delivery._fetch_value",
lambda *_args, **_kwargs: pytest.fail("must not fetch undeclared field"),
)
with pytest.raises(DeliveryError):
exec_with_secret(object(), entry, "other_field", ["probe"], mode="exec-env")

View file

@ -0,0 +1,117 @@
import copy
import os
from secrets_engine.apply import apply_plan
from secrets_engine.catalog import load_catalog, validate_entry
from secrets_engine.config import repo_root
from secrets_engine.plan import build_plan
from secrets_engine.provision import provision_from_file
from secrets_engine.routing import route_lane
from tests.test_catalog import VALID
class RecordingApplyClient:
def __init__(self):
self.policy_writes = []
self.approle_writes = []
def kv_mount_exists(self, _mount):
raise AssertionError("existing mount must not be inspected for mutation")
def ensure_kv_mount(self, _mount):
raise AssertionError("existing mount must not be created")
def read_policy(self, _name):
return None
def write_policy(self, name, hcl):
self.policy_writes.append((name, hcl))
def ensure_approle_enabled(self):
pass
def write_approle(self, role_name, policies, ttl="30m", **_kwargs):
self.approle_writes.append((role_name, policies, ttl))
class RecordingProvisionClient:
def __init__(self):
self.puts = []
def ensure_kv_mount(self, _mount):
raise AssertionError("existing mount must not be created during provision")
def kv_put(self, mount, path, field, value):
self.puts.append((mount, path, field, value))
def _existing_mount_entry():
data = copy.deepcopy(VALID)
data.update(
{
"stage": "prod",
"mount": "platform",
"path": "workloads/example/runtime",
"mount_management": "existing",
"workload_delivery": [
{"mode": "external-secrets", "owner": "rapp-example"}
],
}
)
return validate_entry(data)
def test_apply_existing_mount_only_adds_approved_delivery_auth():
entry = _existing_mount_entry()
plan = build_plan(entry, "prod", decision_id="approved")
client = RecordingApplyClient()
result = apply_plan(client, entry, plan)
assert any("externally managed; no mutation" in item for item in result.skipped)
assert len(client.policy_writes) == 1
assert len(client.approle_writes) == 1
def test_apply_existing_delivery_auth_is_fully_non_mutating():
data = copy.deepcopy(VALID)
data["mount_management"] = "existing"
data["delivery_auth"] = {
"method": "approle",
"management": "existing",
"role_name": "existing-exact-role",
"policy_name": "existing-exact-policy",
}
entry = validate_entry(data)
plan = build_plan(entry, "test", decision_id="approved")
client = RecordingApplyClient()
result = apply_plan(client, entry, plan)
assert client.policy_writes == []
assert client.approle_writes == []
assert len(result.skipped) == 3
assert all("no mutation" in item for item in result.skipped[:2])
def test_provision_existing_mount_never_attempts_mount_creation(tmp_path):
entry = _existing_mount_entry()
value_file = tmp_path / "value"
value_file.write_text("test-only-value", encoding="utf-8")
os.chmod(value_file, 0o600)
client = RecordingProvisionClient()
provision_from_file(client, entry, "api_token", value_file)
assert client.puts == [
("platform", "workloads/example/runtime", "api_token", "test-only-value")
]
def test_admitted_lanes_fail_closed_without_resolved_ccr(tmp_path):
entries = load_catalog(repo_root() / "catalog")
for lane_id in (
"issue-core-ingestion-api-key",
"reuse-surface-hub-write-token",
"openrouter-llm-connect",
"forgejo-admin-api-token",
"email-connect-transactional",
):
result = route_lane(entries[lane_id], hub_url="", repo_root=tmp_path, client=None)
assert result.ready is False
assert result.missing.startswith("approved decision for 'CCR-2026-")
assert result.next_command.startswith("secrets-engine decision inspect CCR-2026-")

View file

@ -65,3 +65,52 @@ def test_valid_plan_builds():
assert plan.policy_name == "se-test-test-lane"
assert any(a.kind == "approle" for a in plan.actions)
assert "secret/data/test/team/thing" in plan.policy_hcl
def test_existing_mount_plan_has_check_not_mount_mutation():
e = _entry(
stage="prod",
mount="platform",
path="workloads/example/runtime",
mount_management="existing",
)
plan = build_plan(e, "prod", decision_id="d1")
assert any(a.kind == "kv-mount-check" for a in plan.actions)
assert not any(a.kind == "kv-mount" for a in plan.actions)
assert "platform/data/workloads/example/runtime" in plan.policy_hcl
def test_existing_auth_plan_has_checks_not_auth_mutations():
e = _entry(
delivery_auth={
"method": "approle",
"management": "existing",
"role_name": "existing-exact-role",
"policy_name": "existing-exact-policy",
}
)
plan = build_plan(e, "test", decision_id="d1")
assert any(a.kind == "policy-check" for a in plan.actions)
assert any(a.kind == "approle-check" for a in plan.actions)
assert not any(a.kind == "policy" for a in plan.actions)
assert not any(a.kind == "approle" for a in plan.actions)
def test_every_admitted_lane_renders_existing_mount_check_and_exact_policy():
from secrets_engine.catalog import load_catalog
from secrets_engine.config import repo_root
entries = load_catalog(repo_root() / "catalog")
for lane_id in (
"issue-core-ingestion-api-key",
"reuse-surface-hub-write-token",
"openrouter-llm-connect",
"forgejo-admin-api-token",
"email-connect-transactional",
):
entry = entries[lane_id]
plan = build_plan(entry, "prod", decision_id=entry.approval["decision_ref"])
assert [a.kind for a in plan.actions] == ["kv-mount-check", "policy", "approle"]
assert f'path "{entry.kv_data_path}"' in plan.policy_hcl
assert "*" not in entry.kv_data_path
assert plan.role_name.startswith("se-prod-")

View file

@ -44,7 +44,7 @@ def bao_dev():
addr = f"http://127.0.0.1:{port}"
token = "se-test-root"
proc = subprocess.Popen(
[bao, "server", "-dev", f"-dev-root-token-id={token}",
[bao, "server", "-dev", "-dev-no-store-token", f"-dev-root-token-id={token}",
f"-dev-listen-address=127.0.0.1:{port}"],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
)

21
tests/test_safe_paths.py Normal file
View file

@ -0,0 +1,21 @@
from secrets_engine.safe_paths import containing_git_worktree
def test_empty_git_named_directory_is_not_a_worktree(tmp_path):
(tmp_path / ".git").mkdir()
assert containing_git_worktree(tmp_path / "secret-file") is None
def test_git_directory_with_head_is_a_worktree(tmp_path):
repo = tmp_path / "repo"
marker = repo / ".git"
marker.mkdir(parents=True)
(marker / "HEAD").write_text("ref: refs/heads/main\n", encoding="utf-8")
assert containing_git_worktree(repo / "secret-file") == repo
def test_git_file_marks_linked_worktree(tmp_path):
repo = tmp_path / "linked"
repo.mkdir()
(repo / ".git").write_text("gitdir: /outside/worktrees/linked\n", encoding="utf-8")
assert containing_git_worktree(repo / "secret-file") == repo

View file

@ -0,0 +1,32 @@
---
id: ADHOC-2026-08-21
type: workplan
title: "OpenBao dev-test and safe-path robustness"
domain: infotech
repo: secrets-engine
status: finished
owner: codex
topic_slug: custodian
created: "2026-08-21"
updated: "2026-08-21"
---
# ADHOC-2026-08-21 - OpenBao dev-test and safe-path robustness
## Keep verification safe in sandboxed environments
```task
id: ADHOC-2026-08-21-T01
status: done
priority: low
```
While verifying SECRETS-WP-0006, the suite exposed two environment-sensitive
test failures. OpenBao 2.5.5 dev mode attempted to persist its root token under
the read-only home directory, and an empty `/tmp/.git` sandbox marker was
treated as a real Git worktree.
The dev fixture now uses `-dev-no-store-token`. Secret provisioning and AppRole
handoff still reject real worktrees (`.git` file or `.git/HEAD`) but ignore an
empty directory that is not a valid Git marker. Unit coverage preserves both
the rejection and false-positive cases.

View file

@ -0,0 +1,228 @@
---
id: SECRETS-WP-0006
type: workplan
title: "Adopt concrete OpenBao credential lanes from ops-warden"
domain: infotech
repo: secrets-engine
status: active
owner: codex
topic_slug: custodian
created: "2026-08-21"
updated: "2026-08-21"
---
# SECRETS-WP-0006 - Adopt concrete OpenBao credential lanes from ops-warden
## Goal
Move concrete, OpenBao-backed credential workflows from ops-warden's interim
proxy into the secrets-engine catalog and operating surface without moving
secret custody out of OpenBao or misassigning generic routing and identity
capabilities to this repo.
The intake originates in ops-warden message
`cbd312f8-347e-44cc-933b-9712e2036646` after earlier requests on 2026-08-11 and
2026-08-15.
## Scope decision
Accept these concrete OpenBao KV lanes for catalog admission and staged
operation:
- `issue-core-ingestion-api-key`;
- `reuse-surface-hub-write-token`;
- `openrouter-llm-connect`;
- `forgejo-admin-api-token`;
- `email-connect-transactional`.
Do not create catalog entries for these generic or non-owned routes:
- `openbao-api-key` remains a generic credential-routing template. Concrete
workload credentials beneath it may become catalog lanes, but the template is
not itself a secret lane.
- `key-cape-oidc-login` remains owned by key-cape/Keycloak. secrets-engine may
consume OIDC identity for OpenBao authentication, but it does not own login,
MFA, or identity-token issuance.
The current implementation is generic at the catalog level for KV
`mount`/`path`/`fields`, and `secrets-engine exec` supports `exec-env` plus the
npm-specific `npm-config` adapter. The remaining delivery-mode names are not all
exec adapters: `read-check` is verification, `approle-login` is auth-capability
handoff, and `exec-file`/`wrapped` are not yet implemented by `exec`.
## Design constraints
- OpenBao remains the custody, policy, lease, and audit backend.
- No raw value, token, password, wrapped token, AppRole material, or accessor is
written to Git, State Hub, chat, prompts, workplans, or normal logs.
- Catalog adoption does not by itself authorize production mutation or value
access. Each production lane needs a resolved approval and a reviewed live
apply/verification plan.
- Existing workload delivery must not be displaced. ESO/Kubernetes delivery and
provider-side rotation remain owned by their current runtime and provider
owners until an explicit migration is approved.
- High-risk lanes must retain exact consumer, field, rotation-owner, denial, and
revocation metadata.
- The production stage role currently manages owner-scoped paths on the
`secret` mount, while these lanes use the existing `platform` mount. Admission
must not silently broaden the stage role or attempt to create that mount.
## Confirm capability and ownership with ops-warden
```task
id: SECRETS-WP-0006-T01
status: done
priority: high
```
Completed 2026-08-21. Replied in the existing thread with State Hub message
`7fb0c2ce-be41-4a1d-95b0-e2cdd97d5718`, marked the inbound message read, and
recorded decision `ae676382-1826-4e04-aa4e-bb77990c7a0d`. The reply accepted
the five concrete KV lanes, declined the generic OpenBao route and key-cape
login route, corrected the six-delivery-mode interpretation, and kept interim
proxies in place pending per-lane verification.
Reply in the existing State Hub thread with the accepted five-lane scope, the
two declined generic/non-owned routes, and the delivery-mode correction. Accept
ops-warden's offer of a PR-shaped non-secret metadata contribution for the five
concrete lanes.
Done when the reply is posted, the inbound message is marked read, and this
decision is recorded as non-secret State Hub evidence.
## Define admission contract for existing production lanes
```task
id: SECRETS-WP-0006-T02
status: done
priority: high
```
Completed 2026-08-21. Added explicit `mount_management`, `delivery_auth`, and
`workload_delivery` semantics, with the operating contract documented in
`docs/catalog-admission.md`. Existing mounts and externally managed delivery
auth now render non-mutating checks; apply cannot create or rewrite them.
Existing workload delivery remains independent metadata, while an optional
engine-managed AppRole is a separate approval-gated native delivery surface.
Define how a catalog entry represents a lane whose KV mount already exists and
whose workload delivery is ESO/Kubernetes rather than a secrets-engine-created
AppRole. Separate these concepts where needed:
- existing-mount versus engine-managed mount;
- workload delivery versus agent/operator exec delivery;
- catalog/verification metadata versus engine-managed consumer auth;
- provider-side rotation owner versus OpenBao value update workflow.
Acceptance:
- Dry-run cannot propose creating or broadening the shared `platform` mount.
- A lane can retain its existing workload auth/delivery while optionally
exposing an approved exec-time adapter.
- The production stage role is not broadened by implication.
## Review and import the five catalog entries
```task
id: SECRETS-WP-0006-T03
status: progress
priority: high
```
Progress 2026-08-21: the offered contribution did not arrive, so drafted all
five entries from ops-warden playbooks and the authoritative railiance-platform
CCRs. Sent message `ceace632-2332-46d8-a2b3-1714fbfa7e2c` requesting line-by-line
confirmation of coordinates, fields, consumers, lifecycle owners, and proposed
native delivery. T03 remains progress until that review returns; interim proxy
ownership is unchanged.
Review ops-warden's proposed entries line by line against the schema, canonical
security boundary, live route metadata, and owning workload documentation.
Correct field lists, mount/path, consumers, delivery modes, approval references,
rotation owners, and negative checks before merging.
Acceptance:
- All five files contain non-secret metadata only.
- Generic `openbao-api-key` and `key-cape-oidc-login` entries are absent.
- Each entry names an exact lane rather than a provider-wide wildcard.
- Each high-risk lane has explicit rotation and deactivation expectations.
## Implement guards and offline verification
```task
id: SECRETS-WP-0006-T04
status: done
priority: high
```
Completed 2026-08-21: implemented non-mutating existing-mount/auth planning and
apply behavior, workload-delivery validation, exact field guards for exec and
verification, safe policy/role overrides, route guidance, and focused tests.
All five admitted entries load and render `kv-mount-check` plus an exact-path
policy and bounded AppRole plan. Generic/wildcard entries and undeclared fields
are negatively covered. Privileged provision, verify, exec, handoff, and live
revoke operations now fail closed without a resolved approval. Full verification
passes 82 tests.
Implement only the schema/planning/delivery changes required by the admitted
entries. Add tests for shared existing mounts, auth/delivery separation,
multi-field selection, exact exec-env injection, and rejection of wildcard or
generic provider-wide entries.
Acceptance:
- The complete catalog loads and validates.
- Each admitted lane renders a non-mutating guarded plan.
- Negative tests prove no wildcard path, broad policy, undeclared field, or
unsupported delivery mode is accepted.
- `uv run pytest` and `git diff --check` pass.
## Stage live apply and verification
```task
id: SECRETS-WP-0006-T05
status: wait
priority: high
```
For each lane, obtain the required decision/operator approval before any live
OpenBao policy, auth-role, provisioning, rotation, or delivery change. Start
with metadata/capability-safe checks and preserve the current ops-warden proxy
until native positive and negative verification succeeds.
Acceptance per lane:
- Approval is resolved and linked.
- Existing workload delivery remains healthy.
- Positive access and unrelated-consumer denial are proven without reading a
value into evidence.
- Rollback/revocation steps are recorded.
## Reconcile routing ownership and retire interim proxies
```task
id: SECRETS-WP-0006-T06
status: wait
priority: medium
```
After a lane is natively ready, update the ops-warden delegation register and
safe next action for that lane. Retire interim proxy operation lane by lane,
not as a seven-lane batch.
Acceptance:
- Native readiness is evidenced before the proxy is retired.
- ops-warden remains the routing front door and does not vend non-SSH secrets.
- The generic OpenBao route and key-cape login route retain their correct
owners.
## Exit criteria
- The five concrete OpenBao lanes have reviewed catalog entries and guarded
offline plans.
- Live ownership moves only for lanes that pass approved positive/negative
verification.
- The two generic/non-owned routes are not mislabeled as secrets-engine lanes.
- No secret material is recorded in repository or coordination evidence.