Close CCR drift and high-risk policy gaps
This commit is contained in:
parent
852a8ab661
commit
382f04412a
12 changed files with 577 additions and 68 deletions
|
|
@ -20,9 +20,10 @@
|
||||||
| workplan | RAILIANCE-WP-0016 | finished | — | workplans/RAILIANCE-WP-0016-apps-pg-resource-evidence.md |
|
| workplan | RAILIANCE-WP-0016 | finished | — | workplans/RAILIANCE-WP-0016-apps-pg-resource-evidence.md |
|
||||||
| workplan | RAILIANCE-WP-0016 | finished | — | workplans/RAILIANCE-WP-0016-architecture-cleanup-backlog.md |
|
| workplan | RAILIANCE-WP-0016 | finished | — | workplans/RAILIANCE-WP-0016-architecture-cleanup-backlog.md |
|
||||||
| workplan | RAILIANCE-WP-0017 | finished | — | workplans/RAILIANCE-WP-0017-consumption-mode-enforcement.md |
|
| workplan | RAILIANCE-WP-0017 | finished | — | workplans/RAILIANCE-WP-0017-consumption-mode-enforcement.md |
|
||||||
|
| workplan | RAILIANCE-WP-0022 | blocked | — | workplans/RAILIANCE-WP-0022-agent-high-risk-boundary-coverage.md |
|
||||||
| workplan | RPF-WP-0018 | finished | — | workplans/RPF-WP-0018-policy-surface-alignment.md |
|
| workplan | RPF-WP-0018 | finished | — | workplans/RPF-WP-0018-policy-surface-alignment.md |
|
||||||
| workplan | RPF-WP-0019 | finished | — | workplans/RPF-WP-0019-apps-pg-recoverability-and-controls.md |
|
| workplan | RPF-WP-0019 | finished | — | workplans/RPF-WP-0019-apps-pg-recoverability-and-controls.md |
|
||||||
| workplan | RPF-WP-0020 | proposed | — | workplans/RPF-WP-0020-ccr-schema-drift.md |
|
| workplan | RPF-WP-0020 | finished | — | workplans/RPF-WP-0020-ccr-schema-drift.md |
|
||||||
| workplan | RPF-WP-0021 | finished | — | workplans/RPF-WP-0021-core-hub-platform-onboarding.md |
|
| workplan | RPF-WP-0021 | finished | — | workplans/RPF-WP-0021-core-hub-platform-onboarding.md |
|
||||||
| task | RAILIANCE-WP-0005-T01 | done | — | workplans/RAILIANCE-WP-0005-credential-request-and-lease-broker.md |
|
| task | RAILIANCE-WP-0005-T01 | done | — | workplans/RAILIANCE-WP-0005-credential-request-and-lease-broker.md |
|
||||||
| task | RAILIANCE-WP-0005-T02 | done | — | workplans/RAILIANCE-WP-0005-credential-request-and-lease-broker.md |
|
| task | RAILIANCE-WP-0005-T02 | done | — | workplans/RAILIANCE-WP-0005-credential-request-and-lease-broker.md |
|
||||||
|
|
@ -85,6 +86,11 @@
|
||||||
| task | RAILIANCE-WP-0016-T04 | done | — | workplans/RAILIANCE-WP-0016-architecture-cleanup-backlog.md |
|
| task | RAILIANCE-WP-0016-T04 | done | — | workplans/RAILIANCE-WP-0016-architecture-cleanup-backlog.md |
|
||||||
| task | RAILIANCE-WP-0016-T05 | done | — | workplans/RAILIANCE-WP-0016-architecture-cleanup-backlog.md |
|
| task | RAILIANCE-WP-0016-T05 | done | — | workplans/RAILIANCE-WP-0016-architecture-cleanup-backlog.md |
|
||||||
| task | RAILIANCE-WP-0017-T01 | done | — | workplans/RAILIANCE-WP-0017-consumption-mode-enforcement.md |
|
| task | RAILIANCE-WP-0017-T01 | done | — | workplans/RAILIANCE-WP-0017-consumption-mode-enforcement.md |
|
||||||
|
| task | RAILIANCE-WP-0022-T01 | done | — | workplans/RAILIANCE-WP-0022-agent-high-risk-boundary-coverage.md |
|
||||||
|
| task | RAILIANCE-WP-0022-T02 | done | — | workplans/RAILIANCE-WP-0022-agent-high-risk-boundary-coverage.md |
|
||||||
|
| task | RAILIANCE-WP-0022-T03 | done | — | workplans/RAILIANCE-WP-0022-agent-high-risk-boundary-coverage.md |
|
||||||
|
| task | RAILIANCE-WP-0022-T04 | done | — | workplans/RAILIANCE-WP-0022-agent-high-risk-boundary-coverage.md |
|
||||||
|
| task | RAILIANCE-WP-0022-T05 | wait | — | workplans/RAILIANCE-WP-0022-agent-high-risk-boundary-coverage.md |
|
||||||
| task | RPF-WP-0018-T01 | done | — | workplans/RPF-WP-0018-policy-surface-alignment.md |
|
| task | RPF-WP-0018-T01 | done | — | workplans/RPF-WP-0018-policy-surface-alignment.md |
|
||||||
| task | RPF-WP-0018-T02 | done | — | workplans/RPF-WP-0018-policy-surface-alignment.md |
|
| task | RPF-WP-0018-T02 | done | — | workplans/RPF-WP-0018-policy-surface-alignment.md |
|
||||||
| task | RPF-WP-0018-T03 | done | — | workplans/RPF-WP-0018-policy-surface-alignment.md |
|
| task | RPF-WP-0018-T03 | done | — | workplans/RPF-WP-0018-policy-surface-alignment.md |
|
||||||
|
|
@ -96,10 +102,10 @@
|
||||||
| task | RPF-WP-0019-T02 | done | — | workplans/RPF-WP-0019-apps-pg-recoverability-and-controls.md |
|
| task | RPF-WP-0019-T02 | done | — | workplans/RPF-WP-0019-apps-pg-recoverability-and-controls.md |
|
||||||
| task | RPF-WP-0019-T03 | done | — | workplans/RPF-WP-0019-apps-pg-recoverability-and-controls.md |
|
| task | RPF-WP-0019-T03 | done | — | workplans/RPF-WP-0019-apps-pg-recoverability-and-controls.md |
|
||||||
| task | RPF-WP-0019-T04 | done | — | workplans/RPF-WP-0019-apps-pg-recoverability-and-controls.md |
|
| task | RPF-WP-0019-T04 | done | — | workplans/RPF-WP-0019-apps-pg-recoverability-and-controls.md |
|
||||||
| task | RPF-WP-0020-T01 | todo | — | workplans/RPF-WP-0020-ccr-schema-drift.md |
|
| task | RPF-WP-0020-T01 | done | — | workplans/RPF-WP-0020-ccr-schema-drift.md |
|
||||||
| task | RPF-WP-0020-T02 | todo | — | workplans/RPF-WP-0020-ccr-schema-drift.md |
|
| task | RPF-WP-0020-T02 | done | — | workplans/RPF-WP-0020-ccr-schema-drift.md |
|
||||||
| task | RPF-WP-0020-T03 | todo | — | workplans/RPF-WP-0020-ccr-schema-drift.md |
|
| task | RPF-WP-0020-T03 | done | — | workplans/RPF-WP-0020-ccr-schema-drift.md |
|
||||||
| task | RPF-WP-0020-T04 | todo | — | workplans/RPF-WP-0020-ccr-schema-drift.md |
|
| task | RPF-WP-0020-T04 | done | — | workplans/RPF-WP-0020-ccr-schema-drift.md |
|
||||||
| task | RPF-WP-0021-T01 | done | — | workplans/RPF-WP-0021-core-hub-platform-onboarding.md |
|
| task | RPF-WP-0021-T01 | done | — | workplans/RPF-WP-0021-core-hub-platform-onboarding.md |
|
||||||
| task | RPF-WP-0021-T02 | done | — | workplans/RPF-WP-0021-core-hub-platform-onboarding.md |
|
| task | RPF-WP-0021-T02 | done | — | workplans/RPF-WP-0021-core-hub-platform-onboarding.md |
|
||||||
| task | RPF-WP-0021-T03 | done | — | workplans/RPF-WP-0021-core-hub-platform-onboarding.md |
|
| task | RPF-WP-0021-T03 | done | — | workplans/RPF-WP-0021-core-hub-platform-onboarding.md |
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ request_type: workload-kv-read
|
||||||
title: email-connect transactional SMTP and ingest token lane
|
title: email-connect transactional SMTP and ingest token lane
|
||||||
status: active
|
status: active
|
||||||
created: '2026-08-12'
|
created: '2026-08-12'
|
||||||
updated: '2026-08-12'
|
updated: '2026-08-21'
|
||||||
requester:
|
requester:
|
||||||
agent: grok
|
agent: grok
|
||||||
reason: >-
|
reason: >-
|
||||||
|
|
@ -117,6 +117,15 @@ verification:
|
||||||
railiance01, namespace-scoped to email-connect.
|
railiance01, namespace-scoped to email-connect.
|
||||||
- Secret value provisioned directly in OpenBao through approved operator custody.
|
- Secret value provisioned directly in OpenBao through approved operator custody.
|
||||||
- Positive and negative verification recorded with non-secret audit ids or timestamps.
|
- Positive and negative verification recorded with non-secret audit ids or timestamps.
|
||||||
|
evidence:
|
||||||
|
- at: '2026-08-20T22:56:00+00:00'
|
||||||
|
actor: codex
|
||||||
|
kind: auth_path_reconciliation
|
||||||
|
result: passed
|
||||||
|
details:
|
||||||
|
- Live ClusterSecretStore openbao-email-connect uses tokenSecretRef external-secrets/openbao-email-connect-eso-token,
|
||||||
|
is namespace-limited to email-connect, and reports Valid.
|
||||||
|
- ExternalSecret email-connect-runtime reports SecretSynced. No Secret value was read.
|
||||||
lifecycle:
|
lifecycle:
|
||||||
deactivate: Disable ops-warden catalog entry and detach ESO role policy.
|
deactivate: Disable ops-warden catalog entry and detach ESO role policy.
|
||||||
rotate: >-
|
rotate: >-
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,16 @@ kind: credential-change-request
|
||||||
schema_version: 1
|
schema_version: 1
|
||||||
request_type: workload-kv-read
|
request_type: workload-kv-read
|
||||||
title: Scaleway bootstrap API key for reef-storage / WP-0002 bucket create
|
title: Scaleway bootstrap API key for reef-storage / WP-0002 bucket create
|
||||||
status: apply_pending
|
status: in_flight
|
||||||
created: '2026-08-14'
|
created: '2026-08-14'
|
||||||
updated: '2026-08-14'
|
updated: '2026-08-14'
|
||||||
|
in_flight:
|
||||||
|
missing_fields:
|
||||||
|
- openbao.policy_file
|
||||||
|
- openbao.auth
|
||||||
|
blocking_reason: Founder bootstrap API key and final operator authentication design
|
||||||
|
are pending; do not invent metadata or fill placeholders.
|
||||||
|
owner: platform-operator
|
||||||
requester:
|
requester:
|
||||||
agent: grok
|
agent: grok
|
||||||
reason: >-
|
reason: >-
|
||||||
|
|
@ -47,7 +54,7 @@ access_frontdoor:
|
||||||
selector: scaleway bootstrap api
|
selector: scaleway bootstrap api
|
||||||
command: bao kv put platform/workloads/railiance/scaleway/bootstrap
|
command: bao kv put platform/workloads/railiance/scaleway/bootstrap
|
||||||
resolvable: false
|
resolvable: false
|
||||||
readiness: waiting-on-ui-replace-of-xxx-placeholders
|
readiness: approved-pending-apply
|
||||||
delivery:
|
delivery:
|
||||||
surface: operator-workstation
|
surface: operator-workstation
|
||||||
target: reef-storage/tools/create-platform-audit-bucket.sh (reads, never prints)
|
target: reef-storage/tools/create-platform-audit-bucket.sh (reads, never prints)
|
||||||
|
|
@ -62,6 +69,11 @@ verification:
|
||||||
- Field names present on the KV path; values not printed.
|
- Field names present on the KV path; values not printed.
|
||||||
negative:
|
negative:
|
||||||
- default-policy token denied on the data path.
|
- default-policy token denied on the data path.
|
||||||
|
activation_conditions:
|
||||||
|
- Founder supplies the bootstrap credential through attended custody outside Git,
|
||||||
|
chat, argv, and State Hub.
|
||||||
|
- Platform operator records the exact policy artifact and authentication method
|
||||||
|
before the request leaves in_flight status.
|
||||||
lifecycle:
|
lifecycle:
|
||||||
deactivate: Delete bootstrap key at Scaleway after the scoped bucket key works.
|
deactivate: Delete bootstrap key at Scaleway after the scoped bucket key works.
|
||||||
rotate: Put a new bootstrap key; do not reuse the scoped backup key.
|
rotate: Put a new bootstrap key; do not reuse the scoped backup key.
|
||||||
|
|
|
||||||
|
|
@ -74,6 +74,7 @@ Suggested states:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
draft
|
draft
|
||||||
|
in_flight
|
||||||
proposed
|
proposed
|
||||||
needs_changes
|
needs_changes
|
||||||
approved
|
approved
|
||||||
|
|
@ -89,6 +90,12 @@ superseded
|
||||||
cancelled
|
cancelled
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`in_flight` is the only state that may explicitly omit completion-only fields.
|
||||||
|
It must declare every omission in `in_flight.missing_fields`, name the owner and
|
||||||
|
blocking reason, and remain non-resolvable. The validator still checks every
|
||||||
|
other field. This is not an applyable state and must never be used to hide a
|
||||||
|
malformed active lane.
|
||||||
|
|
||||||
Only `approved` requests may be applied. Only `verified` requests may become
|
Only `approved` requests may be applied. Only `verified` requests may become
|
||||||
`active`.
|
`active`.
|
||||||
|
|
||||||
|
|
@ -141,6 +148,12 @@ Version 1 should be boring:
|
||||||
- prompt or delegate separately for secret value entry;
|
- prompt or delegate separately for secret value entry;
|
||||||
- record non-secret evidence in State Hub.
|
- record non-secret evidence in State Hub.
|
||||||
|
|
||||||
|
When the schema adds or strengthens a required field, the same change must
|
||||||
|
include a migration pass over every existing CCR. Active declarations must
|
||||||
|
describe the live authentication path; incomplete requests must move to the
|
||||||
|
explicit `in_flight` state rather than relying on a filename exception or a
|
||||||
|
loosened repository-wide assertion.
|
||||||
|
|
||||||
The first implemented CLI slice is:
|
The first implemented CLI slice is:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|
|
||||||
43
docs/evidence/agent-high-risk-boundary-2026-08-21.md
Normal file
43
docs/evidence/agent-high-risk-boundary-2026-08-21.md
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
# Agent high-risk OpenBao boundary — 2026-08-21
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
This is capabilities and metadata evidence for `RAILIANCE-WP-0022`. No
|
||||||
|
credential value was read, no token was minted, and no operator role was
|
||||||
|
modified.
|
||||||
|
|
||||||
|
## Source and live coverage
|
||||||
|
|
||||||
|
- State Hub message `828e4903-30fe-4903-acfd-cd2ecdda437d` reported that the
|
||||||
|
live `agent-high-risk-boundary` lacked the Core Hub path and that six other
|
||||||
|
concrete high-risk catalog paths were absent from both source and live.
|
||||||
|
- Source now denies KV-v2 data and permits metadata only for all concrete
|
||||||
|
high-risk catalog paths. Pattern-only and non-KV lanes do not generate an
|
||||||
|
invented address.
|
||||||
|
- Under attended `platform-admin` OIDC, OpenBao accepted the updated policy.
|
||||||
|
A normalized readback matched the source file.
|
||||||
|
- The ops-warden audit used `policy_source: server` and reported 17 high-risk
|
||||||
|
lanes: 12 covered catalog entries, zero uncovered, and five with no concrete
|
||||||
|
KV address. The policy itself contains 12 unique deny paths because two
|
||||||
|
catalog entries share the Binky IMAP path and Core Hub is an additional
|
||||||
|
reviewed deny without a catalog lane.
|
||||||
|
|
||||||
|
## Attachment audit and residual blocker
|
||||||
|
|
||||||
|
A metadata-only scan listed and read role configuration under netkingdom OIDC,
|
||||||
|
Kubernetes auth, AppRole, and token roles. It found:
|
||||||
|
|
||||||
|
- roles attaching `agent-high-risk-boundary`: **0**;
|
||||||
|
- roles combining it with any `workload-kv-read-*` policy: **0**.
|
||||||
|
|
||||||
|
The live policy is therefore complete but is not automatically attached to a
|
||||||
|
coding-agent identity. The documented manual short-lived token example is not
|
||||||
|
a standing identity and carries no workload-read policy. Attaching the boundary
|
||||||
|
to `platform-admin` would incorrectly constrain the attended operator role and
|
||||||
|
erase the human/agent distinction, so that change was not made.
|
||||||
|
|
||||||
|
The remaining work is an identity-owner decision: define a distinct coding-
|
||||||
|
agent issuance path, attach the boundary, and prove that deny wins when a
|
||||||
|
workload read policy is also present. A versioned generated list of concrete
|
||||||
|
high-risk deny paths is also requested from ops-warden so policy coverage does
|
||||||
|
not depend on manual catalog transcription.
|
||||||
|
|
@ -476,7 +476,8 @@ IONOS STARTTLS credentials and the shared user-engine ingest bearer for the
|
||||||
| Policy file | `openbao/policies/workload-kv-read-email-connect-transactional.hcl` |
|
| Policy file | `openbao/policies/workload-kv-read-email-connect-transactional.hcl` |
|
||||||
| ESO policy | `external-secrets-email-connect` |
|
| ESO policy | `external-secrets-email-connect` |
|
||||||
| ESO policy file | `openbao/policies/external-secrets-email-connect.hcl` |
|
| ESO policy file | `openbao/policies/external-secrets-email-connect.hcl` |
|
||||||
| K8s auth role | `external-secrets-email-connect` (ESO delivery) |
|
| Current ESO auth | policy-limited orphan token in Secret `external-secrets/openbao-email-connect-eso-token` |
|
||||||
|
| K8s auth follow-up | role `external-secrets-email-connect` after the railiance01 auth mount is wired |
|
||||||
| ClusterSecretStore | `openbao-email-connect` (namespace `email-connect` only) |
|
| ClusterSecretStore | `openbao-email-connect` (namespace `email-connect` only) |
|
||||||
| Primary consumer | ExternalSecret `email-connect/email-connect-runtime` → Secret `email-connect-runtime` |
|
| Primary consumer | ExternalSecret `email-connect/email-connect-runtime` → Secret `email-connect-runtime` |
|
||||||
| Package manifests | `email-connect/deploy/k8s/railiance/` |
|
| Package manifests | `email-connect/deploy/k8s/railiance/` |
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,42 @@ path "platform/data/workloads/core-hub/runtime" {
|
||||||
path "platform/metadata/workloads/core-hub/runtime" {
|
path "platform/metadata/workloads/core-hub/runtime" {
|
||||||
capabilities = ["read"]
|
capabilities = ["read"]
|
||||||
}
|
}
|
||||||
|
path "platform/data/workloads/coulomb/whynot-design/npm-publish" {
|
||||||
|
capabilities = ["deny"]
|
||||||
|
}
|
||||||
|
path "platform/metadata/workloads/coulomb/whynot-design/npm-publish" {
|
||||||
|
capabilities = ["read"]
|
||||||
|
}
|
||||||
|
path "platform/data/workloads/rapp-qonto/keycape-client" {
|
||||||
|
capabilities = ["deny"]
|
||||||
|
}
|
||||||
|
path "platform/metadata/workloads/rapp-qonto/keycape-client" {
|
||||||
|
capabilities = ["read"]
|
||||||
|
}
|
||||||
|
path "platform/data/workloads/agent-harness/forgejo-deploy-key" {
|
||||||
|
capabilities = ["deny"]
|
||||||
|
}
|
||||||
|
path "platform/metadata/workloads/agent-harness/forgejo-deploy-key" {
|
||||||
|
capabilities = ["read"]
|
||||||
|
}
|
||||||
|
path "platform/data/workloads/audit-core/senders" {
|
||||||
|
capabilities = ["deny"]
|
||||||
|
}
|
||||||
|
path "platform/metadata/workloads/audit-core/senders" {
|
||||||
|
capabilities = ["read"]
|
||||||
|
}
|
||||||
|
path "platform/data/workloads/email-connect/transactional" {
|
||||||
|
capabilities = ["deny"]
|
||||||
|
}
|
||||||
|
path "platform/metadata/workloads/email-connect/transactional" {
|
||||||
|
capabilities = ["read"]
|
||||||
|
}
|
||||||
|
path "platform/data/workloads/railiance/scaleway/bootstrap" {
|
||||||
|
capabilities = ["deny"]
|
||||||
|
}
|
||||||
|
path "platform/metadata/workloads/railiance/scaleway/bootstrap" {
|
||||||
|
capabilities = ["read"]
|
||||||
|
}
|
||||||
|
|
||||||
# --- tenant high-risk (WARDEN-WP-0028) ---
|
# --- tenant high-risk (WARDEN-WP-0028) ---
|
||||||
path "tenants/data/binky/company-email/imap" {
|
path "tenants/data/binky/company-email/imap" {
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ required_top_level:
|
||||||
|
|
||||||
allowed_statuses:
|
allowed_statuses:
|
||||||
- draft
|
- draft
|
||||||
|
- in_flight
|
||||||
- proposed
|
- proposed
|
||||||
- needs_changes
|
- needs_changes
|
||||||
- approved
|
- approved
|
||||||
|
|
@ -68,10 +69,6 @@ workload_kv_read:
|
||||||
- auth
|
- auth
|
||||||
openbao.auth:
|
openbao.auth:
|
||||||
- method
|
- method
|
||||||
- mount
|
|
||||||
- role
|
|
||||||
- bound_claims
|
|
||||||
- bound_claims_confirmed
|
|
||||||
- policies
|
- policies
|
||||||
access_frontdoor:
|
access_frontdoor:
|
||||||
- type
|
- type
|
||||||
|
|
@ -87,8 +84,35 @@ workload_kv_read:
|
||||||
- rotate
|
- rotate
|
||||||
- compromised
|
- compromised
|
||||||
conditional:
|
conditional:
|
||||||
|
status=in_flight:
|
||||||
|
required:
|
||||||
|
- in_flight.missing_fields
|
||||||
|
- in_flight.blocking_reason
|
||||||
|
- in_flight.owner
|
||||||
|
allowed_missing_fields:
|
||||||
|
- openbao.policy_file
|
||||||
|
- openbao.auth
|
||||||
|
openbao.auth.method=token:
|
||||||
|
required:
|
||||||
|
- openbao.eso_policy_name
|
||||||
|
- openbao.eso_policy_file
|
||||||
|
- openbao.auth.token_secret
|
||||||
|
- openbao.auth.bootstrap_script
|
||||||
|
- openbao.auth.ttl
|
||||||
|
- openbao.auth.kubernetes_followup
|
||||||
|
note: Transitional ESO token auth; delegated applier does not create the token.
|
||||||
|
openbao.auth.method=kubernetes:
|
||||||
|
required:
|
||||||
|
- mount
|
||||||
|
- role
|
||||||
|
- bound_claims
|
||||||
|
- bound_claims_confirmed
|
||||||
openbao.auth.method=oidc:
|
openbao.auth.method=oidc:
|
||||||
required:
|
required:
|
||||||
|
- mount
|
||||||
|
- role
|
||||||
|
- bound_claims
|
||||||
|
- bound_claims_confirmed
|
||||||
- allowed_redirect_uris
|
- allowed_redirect_uris
|
||||||
allowed_redirect_uris: non-empty list of OpenBao callback URIs accepted by the role
|
allowed_redirect_uris: non-empty list of OpenBao callback URIs accepted by the role
|
||||||
groups_claim: requires openbao.auth.oidc_scopes to include groups
|
groups_claim: requires openbao.auth.oidc_scopes to include groups
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ REPO_DIR = Path(__file__).resolve().parents[1]
|
||||||
DEFAULT_CCR_DIR = REPO_DIR / "credential-change-requests"
|
DEFAULT_CCR_DIR = REPO_DIR / "credential-change-requests"
|
||||||
ALLOWED_STATUSES = {
|
ALLOWED_STATUSES = {
|
||||||
"draft",
|
"draft",
|
||||||
|
"in_flight",
|
||||||
"proposed",
|
"proposed",
|
||||||
"needs_changes",
|
"needs_changes",
|
||||||
"approved",
|
"approved",
|
||||||
|
|
@ -189,6 +190,28 @@ def reject_secret_text(text: str, field: str) -> None:
|
||||||
|
|
||||||
|
|
||||||
def validate_workload_kv_read(ccr: dict[str, Any], errors: list[str], warnings: list[str]) -> None:
|
def validate_workload_kv_read(ccr: dict[str, Any], errors: list[str], warnings: list[str]) -> None:
|
||||||
|
in_flight = ccr.get("status") == "in_flight"
|
||||||
|
missing_fields: set[str] = set()
|
||||||
|
if in_flight:
|
||||||
|
declaration = require_object(ccr.get("in_flight"), "in_flight", errors)
|
||||||
|
listed_missing = require_list(
|
||||||
|
declaration.get("missing_fields"), "in_flight.missing_fields", errors
|
||||||
|
)
|
||||||
|
missing_fields = {str(field) for field in listed_missing}
|
||||||
|
if not missing_fields:
|
||||||
|
errors.append("in_flight.missing_fields must not be empty")
|
||||||
|
allowed_missing = {"openbao.policy_file", "openbao.auth"}
|
||||||
|
unsupported_missing = missing_fields - allowed_missing
|
||||||
|
if unsupported_missing:
|
||||||
|
errors.append(
|
||||||
|
"in_flight.missing_fields contains unsupported fields: "
|
||||||
|
+ ", ".join(sorted(unsupported_missing))
|
||||||
|
)
|
||||||
|
require_string(
|
||||||
|
declaration.get("blocking_reason"), "in_flight.blocking_reason", errors
|
||||||
|
)
|
||||||
|
require_string(declaration.get("owner"), "in_flight.owner", errors)
|
||||||
|
|
||||||
target = require_object(ccr.get("target"), "target", errors)
|
target = require_object(ccr.get("target"), "target", errors)
|
||||||
for field in ("domain", "tenant", "workload", "environment", "purpose"):
|
for field in ("domain", "tenant", "workload", "environment", "purpose"):
|
||||||
require_string(target.get(field), f"target.{field}", errors)
|
require_string(target.get(field), f"target.{field}", errors)
|
||||||
|
|
@ -203,9 +226,16 @@ def validate_workload_kv_read(ccr: dict[str, Any], errors: list[str], warnings:
|
||||||
policy_name = require_string(
|
policy_name = require_string(
|
||||||
openbao.get("policy_name"), "openbao.policy_name", errors
|
openbao.get("policy_name"), "openbao.policy_name", errors
|
||||||
)
|
)
|
||||||
policy_file = require_string(
|
policy_file = ""
|
||||||
openbao.get("policy_file"), "openbao.policy_file", errors
|
if "openbao.policy_file" in missing_fields:
|
||||||
)
|
if openbao.get("policy_file") is not None:
|
||||||
|
errors.append(
|
||||||
|
"openbao.policy_file is declared missing but is present"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
policy_file = require_string(
|
||||||
|
openbao.get("policy_file"), "openbao.policy_file", errors
|
||||||
|
)
|
||||||
fields = [str(field) for field in require_list(openbao.get("fields"), "openbao.fields", errors)]
|
fields = [str(field) for field in require_list(openbao.get("fields"), "openbao.fields", errors)]
|
||||||
if not fields:
|
if not fields:
|
||||||
errors.append("openbao.fields must contain at least one field")
|
errors.append("openbao.fields must contain at least one field")
|
||||||
|
|
@ -220,12 +250,20 @@ def validate_workload_kv_read(ccr: dict[str, Any], errors: list[str], warnings:
|
||||||
if not resolved_policy.exists():
|
if not resolved_policy.exists():
|
||||||
errors.append(f"openbao.policy_file does not exist: {policy_file}")
|
errors.append(f"openbao.policy_file does not exist: {policy_file}")
|
||||||
|
|
||||||
auth = require_object(openbao.get("auth"), "openbao.auth", errors)
|
auth: dict[str, Any] = {}
|
||||||
method = require_string(auth.get("method"), "openbao.auth.method", errors)
|
if "openbao.auth" in missing_fields:
|
||||||
if method not in {"oidc", "kubernetes"}:
|
if openbao.get("auth") is not None:
|
||||||
errors.append("openbao.auth.method must be oidc or kubernetes")
|
errors.append("openbao.auth is declared missing but is present")
|
||||||
require_string(auth.get("mount"), "openbao.auth.mount", errors)
|
else:
|
||||||
require_string(auth.get("role"), "openbao.auth.role", errors)
|
auth = require_object(openbao.get("auth"), "openbao.auth", errors)
|
||||||
|
method = ""
|
||||||
|
if auth:
|
||||||
|
method = require_string(auth.get("method"), "openbao.auth.method", errors)
|
||||||
|
if method and method not in {"oidc", "kubernetes", "token"}:
|
||||||
|
errors.append("openbao.auth.method must be oidc, kubernetes, or token")
|
||||||
|
if method in {"oidc", "kubernetes"}:
|
||||||
|
require_string(auth.get("mount"), "openbao.auth.mount", errors)
|
||||||
|
require_string(auth.get("role"), "openbao.auth.role", errors)
|
||||||
if method == "oidc":
|
if method == "oidc":
|
||||||
redirect_uris = require_list(
|
redirect_uris = require_list(
|
||||||
auth.get("allowed_redirect_uris"),
|
auth.get("allowed_redirect_uris"),
|
||||||
|
|
@ -252,22 +290,95 @@ def validate_workload_kv_read(ccr: dict[str, Any], errors: list[str], warnings:
|
||||||
errors.append(
|
errors.append(
|
||||||
f"openbao.auth.oidc_scopes[{index}] must be a non-empty string"
|
f"openbao.auth.oidc_scopes[{index}] must be a non-empty string"
|
||||||
)
|
)
|
||||||
policies = [str(policy) for policy in require_list(auth.get("policies"), "openbao.auth.policies", errors)]
|
policies: list[str] = []
|
||||||
if policies != [policy_name]:
|
if auth:
|
||||||
errors.append("openbao.auth.policies must contain exactly openbao.policy_name")
|
policies = [
|
||||||
for policy in policies:
|
str(policy)
|
||||||
if policy in DISALLOWED_POLICY_NAMES:
|
for policy in require_list(
|
||||||
errors.append(f"openbao.auth.policies contains disallowed policy {policy}")
|
auth.get("policies"), "openbao.auth.policies", errors
|
||||||
ttl = auth.get("ttl")
|
)
|
||||||
if ttl is not None and (not isinstance(ttl, str) or not TTL_RE.match(ttl)):
|
]
|
||||||
errors.append("openbao.auth.ttl must match <positive integer><s|m|h|d>")
|
expected_policy = policy_name
|
||||||
bound_claims = require_object(
|
if method == "token":
|
||||||
auth.get("bound_claims"), "openbao.auth.bound_claims", errors
|
expected_policy = require_string(
|
||||||
)
|
openbao.get("eso_policy_name"), "openbao.eso_policy_name", errors
|
||||||
if not bound_claims:
|
)
|
||||||
errors.append("openbao.auth.bound_claims must not be empty")
|
eso_policy_file = require_string(
|
||||||
if auth.get("bound_claims_confirmed") is not True:
|
openbao.get("eso_policy_file"), "openbao.eso_policy_file", errors
|
||||||
warnings.append("OIDC/Kubernetes bound claim is not confirmed; apply is blocked")
|
)
|
||||||
|
if eso_policy_file and not resolve_repo_path(eso_policy_file).exists():
|
||||||
|
errors.append(
|
||||||
|
f"openbao.eso_policy_file does not exist: {eso_policy_file}"
|
||||||
|
)
|
||||||
|
if policies != [expected_policy]:
|
||||||
|
errors.append(
|
||||||
|
"openbao.auth.policies must contain exactly the policy used by the auth method"
|
||||||
|
)
|
||||||
|
for policy in policies:
|
||||||
|
if policy in DISALLOWED_POLICY_NAMES:
|
||||||
|
errors.append(
|
||||||
|
f"openbao.auth.policies contains disallowed policy {policy}"
|
||||||
|
)
|
||||||
|
ttl = auth.get("ttl")
|
||||||
|
if ttl is not None and (
|
||||||
|
not isinstance(ttl, str) or not TTL_RE.match(ttl)
|
||||||
|
):
|
||||||
|
errors.append("openbao.auth.ttl must match <positive integer><s|m|h|d>")
|
||||||
|
if method in {"oidc", "kubernetes"}:
|
||||||
|
bound_claims = require_object(
|
||||||
|
auth.get("bound_claims"), "openbao.auth.bound_claims", errors
|
||||||
|
)
|
||||||
|
if not bound_claims:
|
||||||
|
errors.append("openbao.auth.bound_claims must not be empty")
|
||||||
|
if auth.get("bound_claims_confirmed") is not True:
|
||||||
|
warnings.append(
|
||||||
|
"OIDC/Kubernetes bound claim is not confirmed; apply is blocked"
|
||||||
|
)
|
||||||
|
elif method == "token":
|
||||||
|
token_secret = require_string(
|
||||||
|
auth.get("token_secret"), "openbao.auth.token_secret", errors
|
||||||
|
)
|
||||||
|
if token_secret and not re.match(
|
||||||
|
r"^[a-z0-9]([-a-z0-9]*[a-z0-9])?/[a-z0-9]([-a-z0-9]*[a-z0-9])?$",
|
||||||
|
token_secret,
|
||||||
|
):
|
||||||
|
errors.append("openbao.auth.token_secret must be namespace/name")
|
||||||
|
require_string(
|
||||||
|
auth.get("bootstrap_script"), "openbao.auth.bootstrap_script", errors
|
||||||
|
)
|
||||||
|
if auth.get("ttl") is None:
|
||||||
|
errors.append("openbao.auth.ttl is required for token auth")
|
||||||
|
followup = require_object(
|
||||||
|
auth.get("kubernetes_followup"),
|
||||||
|
"openbao.auth.kubernetes_followup",
|
||||||
|
errors,
|
||||||
|
)
|
||||||
|
if followup.get("method") != "kubernetes":
|
||||||
|
errors.append(
|
||||||
|
"openbao.auth.kubernetes_followup.method must be kubernetes"
|
||||||
|
)
|
||||||
|
require_string(
|
||||||
|
followup.get("mount"),
|
||||||
|
"openbao.auth.kubernetes_followup.mount",
|
||||||
|
errors,
|
||||||
|
)
|
||||||
|
require_string(
|
||||||
|
followup.get("role"),
|
||||||
|
"openbao.auth.kubernetes_followup.role",
|
||||||
|
errors,
|
||||||
|
)
|
||||||
|
followup_claims = require_object(
|
||||||
|
followup.get("bound_claims"),
|
||||||
|
"openbao.auth.kubernetes_followup.bound_claims",
|
||||||
|
errors,
|
||||||
|
)
|
||||||
|
if not followup_claims:
|
||||||
|
errors.append(
|
||||||
|
"openbao.auth.kubernetes_followup.bound_claims must not be empty"
|
||||||
|
)
|
||||||
|
warnings.append(
|
||||||
|
"token auth is transitional; complete the declared Kubernetes-auth follow-up"
|
||||||
|
)
|
||||||
|
|
||||||
frontdoor = require_object(ccr.get("access_frontdoor"), "access_frontdoor", errors)
|
frontdoor = require_object(ccr.get("access_frontdoor"), "access_frontdoor", errors)
|
||||||
require_string(frontdoor.get("type"), "access_frontdoor.type", errors)
|
require_string(frontdoor.get("type"), "access_frontdoor.type", errors)
|
||||||
|
|
@ -342,14 +453,11 @@ def validate_ccr(path: Path) -> tuple[dict[str, Any], list[str], list[str]]:
|
||||||
|
|
||||||
def render_summary(ccr: dict[str, Any], warnings: list[str]) -> str:
|
def render_summary(ccr: dict[str, Any], warnings: list[str]) -> str:
|
||||||
openbao = ccr["openbao"]
|
openbao = ccr["openbao"]
|
||||||
auth = openbao["auth"]
|
auth = openbao.get("auth") or {}
|
||||||
frontdoor = ccr["access_frontdoor"]
|
frontdoor = ccr["access_frontdoor"]
|
||||||
risk = ccr["risk"]
|
risk = ccr["risk"]
|
||||||
verification = ccr["verification"]
|
verification = ccr["verification"]
|
||||||
fields = ", ".join(openbao["fields"])
|
fields = ", ".join(openbao["fields"])
|
||||||
claim_bits = ", ".join(
|
|
||||||
f"{key}={value}" for key, value in auth.get("bound_claims", {}).items()
|
|
||||||
)
|
|
||||||
lines = [
|
lines = [
|
||||||
f"Request: {ccr['title']}",
|
f"Request: {ccr['title']}",
|
||||||
f"CCR: {ccr['id']} ({ccr['status']})",
|
f"CCR: {ccr['id']} ({ccr['status']})",
|
||||||
|
|
@ -361,13 +469,37 @@ def render_summary(ccr: dict[str, Any], warnings: list[str]) -> str:
|
||||||
"Policy:",
|
"Policy:",
|
||||||
f" {openbao['policy_name']}",
|
f" {openbao['policy_name']}",
|
||||||
"Auth binding:",
|
"Auth binding:",
|
||||||
f" {auth['mount']} {auth['method']} role {auth['role']}",
|
|
||||||
f" bound claims: {claim_bits}",
|
|
||||||
f" confirmed: {auth.get('bound_claims_confirmed') is True}",
|
|
||||||
"Access front door:",
|
|
||||||
f" {frontdoor['type']} {frontdoor['catalog_id']}",
|
|
||||||
f" readiness: {frontdoor.get('readiness')} resolvable={frontdoor.get('resolvable') is True}",
|
|
||||||
]
|
]
|
||||||
|
if auth.get("method") == "token":
|
||||||
|
lines.extend(
|
||||||
|
[
|
||||||
|
f" transitional token via Secret {auth['token_secret']}",
|
||||||
|
f" policy: {', '.join(auth.get('policies', []))}",
|
||||||
|
f" ttl: {auth.get('ttl')}",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
elif auth:
|
||||||
|
claim_bits = ", ".join(
|
||||||
|
f"{key}={value}"
|
||||||
|
for key, value in auth.get("bound_claims", {}).items()
|
||||||
|
)
|
||||||
|
lines.extend(
|
||||||
|
[
|
||||||
|
f" {auth['mount']} {auth['method']} role {auth['role']}",
|
||||||
|
f" bound claims: {claim_bits}",
|
||||||
|
f" confirmed: {auth.get('bound_claims_confirmed') is True}",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
missing = ", ".join(ccr.get("in_flight", {}).get("missing_fields", []))
|
||||||
|
lines.append(f" in flight; declared missing: {missing}")
|
||||||
|
lines.extend(
|
||||||
|
[
|
||||||
|
"Access front door:",
|
||||||
|
f" {frontdoor['type']} {frontdoor['catalog_id']}",
|
||||||
|
f" readiness: {frontdoor.get('readiness')} resolvable={frontdoor.get('resolvable') is True}",
|
||||||
|
]
|
||||||
|
)
|
||||||
if frontdoor.get("command"):
|
if frontdoor.get("command"):
|
||||||
lines.append(f" command: {frontdoor['command']}")
|
lines.append(f" command: {frontdoor['command']}")
|
||||||
lines.append(f"Risk: {risk['classification']}")
|
lines.append(f"Risk: {risk['classification']}")
|
||||||
|
|
@ -1408,8 +1540,15 @@ def apply_blockers(ccr: dict[str, Any]) -> list[str]:
|
||||||
return blockers
|
return blockers
|
||||||
if status not in APPLY_ALLOWED_STATUSES:
|
if status not in APPLY_ALLOWED_STATUSES:
|
||||||
blockers.append(f"apply requires status approved, got {status}")
|
blockers.append(f"apply requires status approved, got {status}")
|
||||||
if ccr["openbao"]["auth"].get("bound_claims_confirmed") is not True:
|
auth = ccr["openbao"].get("auth") or {}
|
||||||
|
if auth.get("method") in {"oidc", "kubernetes"} and auth.get(
|
||||||
|
"bound_claims_confirmed"
|
||||||
|
) is not True:
|
||||||
blockers.append("apply requires confirmed OpenBao auth binding")
|
blockers.append("apply requires confirmed OpenBao auth binding")
|
||||||
|
if auth.get("method") == "token":
|
||||||
|
blockers.append(
|
||||||
|
"delegated apply does not create transitional token-auth bootstrap identities"
|
||||||
|
)
|
||||||
return blockers
|
return blockers
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1432,7 +1571,7 @@ def status_payload(ccr: dict[str, Any], warnings: list[str]) -> dict[str, Any]:
|
||||||
frontdoor_blocked_by = frontdoor_blockers(ccr)
|
frontdoor_blocked_by = frontdoor_blockers(ccr)
|
||||||
frontdoor = ccr["access_frontdoor"]
|
frontdoor = ccr["access_frontdoor"]
|
||||||
openbao = ccr["openbao"]
|
openbao = ccr["openbao"]
|
||||||
auth = openbao["auth"]
|
auth = openbao.get("auth") or {}
|
||||||
return {
|
return {
|
||||||
"id": ccr["id"],
|
"id": ccr["id"],
|
||||||
"title": ccr["title"],
|
"title": ccr["title"],
|
||||||
|
|
@ -1449,9 +1588,10 @@ def status_payload(ccr: dict[str, Any], warnings: list[str]) -> dict[str, Any]:
|
||||||
"kv_path": openbao["kv_path"],
|
"kv_path": openbao["kv_path"],
|
||||||
"fields": openbao["fields"],
|
"fields": openbao["fields"],
|
||||||
"policy_name": openbao["policy_name"],
|
"policy_name": openbao["policy_name"],
|
||||||
"auth_mount": auth["mount"],
|
"auth_mount": auth.get("mount"),
|
||||||
"auth_method": auth["method"],
|
"auth_method": auth.get("method"),
|
||||||
"auth_role": auth["role"],
|
"auth_role": auth.get("role"),
|
||||||
|
"token_secret": auth.get("token_secret"),
|
||||||
"bound_claims_confirmed": auth.get("bound_claims_confirmed") is True,
|
"bound_claims_confirmed": auth.get("bound_claims_confirmed") is True,
|
||||||
},
|
},
|
||||||
"access_frontdoor": {
|
"access_frontdoor": {
|
||||||
|
|
|
||||||
|
|
@ -76,6 +76,55 @@ class CredentialChangeTests(unittest.TestCase):
|
||||||
self.assertEqual(errors, [])
|
self.assertEqual(errors, [])
|
||||||
self.assertEqual(ccr["target"]["rapp"], "rapp-qonto")
|
self.assertEqual(ccr["target"]["rapp"], "rapp-qonto")
|
||||||
|
|
||||||
|
def test_email_connect_declares_live_transitional_token_auth(self) -> None:
|
||||||
|
path = (
|
||||||
|
REPO_DIR
|
||||||
|
/ "credential-change-requests/CCR-2026-0010-email-connect-transactional.yaml"
|
||||||
|
)
|
||||||
|
ccr, errors, warnings = credential_change.validate_ccr(path)
|
||||||
|
self.assertEqual(errors, [])
|
||||||
|
self.assertEqual(ccr["openbao"]["auth"]["method"], "token")
|
||||||
|
self.assertEqual(
|
||||||
|
ccr["openbao"]["auth"]["token_secret"],
|
||||||
|
"external-secrets/openbao-email-connect-eso-token",
|
||||||
|
)
|
||||||
|
self.assertTrue(any("token auth is transitional" in item for item in warnings))
|
||||||
|
rendered = credential_change.render_summary(ccr, warnings)
|
||||||
|
self.assertIn("transitional token via Secret", rendered)
|
||||||
|
|
||||||
|
def test_in_flight_ccr_declares_each_completion_only_omission(self) -> None:
|
||||||
|
path = (
|
||||||
|
REPO_DIR
|
||||||
|
/ "credential-change-requests/CCR-2026-0011-scaleway-object-storage-bootstrap.yaml"
|
||||||
|
)
|
||||||
|
ccr, errors, warnings = credential_change.validate_ccr(path)
|
||||||
|
self.assertEqual(errors, [])
|
||||||
|
self.assertEqual(warnings, [])
|
||||||
|
self.assertEqual(ccr["status"], "in_flight")
|
||||||
|
self.assertEqual(
|
||||||
|
set(ccr["in_flight"]["missing_fields"]),
|
||||||
|
{"openbao.policy_file", "openbao.auth"},
|
||||||
|
)
|
||||||
|
payload = credential_change.status_payload(ccr, warnings)
|
||||||
|
self.assertFalse(payload["apply_allowed"])
|
||||||
|
self.assertFalse(payload["frontdoor_resolvable"])
|
||||||
|
self.assertIn("got in_flight", " ".join(payload["apply_blockers"]))
|
||||||
|
rendered = credential_change.render_summary(ccr, warnings)
|
||||||
|
self.assertIn("in flight; declared missing", rendered)
|
||||||
|
|
||||||
|
def test_in_flight_ccr_cannot_omit_an_undeclared_field(self) -> None:
|
||||||
|
source = (
|
||||||
|
REPO_DIR
|
||||||
|
/ "credential-change-requests/CCR-2026-0011-scaleway-object-storage-bootstrap.yaml"
|
||||||
|
)
|
||||||
|
path = self.unapproved_ccr(source)
|
||||||
|
data = credential_change.load_yaml(path)
|
||||||
|
data["status"] = "in_flight"
|
||||||
|
data["in_flight"]["missing_fields"] = ["openbao.auth"]
|
||||||
|
credential_change.dump_yaml(path, data)
|
||||||
|
_ccr, errors, _warnings = credential_change.validate_ccr(path)
|
||||||
|
self.assertTrue(any("openbao.policy_file" in error for error in errors))
|
||||||
|
|
||||||
def test_core_hub_runtime_lane_is_split_and_agent_denied(self) -> None:
|
def test_core_hub_runtime_lane_is_split_and_agent_denied(self) -> None:
|
||||||
path = (
|
path = (
|
||||||
REPO_DIR
|
REPO_DIR
|
||||||
|
|
@ -105,6 +154,30 @@ class CredentialChangeTests(unittest.TestCase):
|
||||||
self.assertEqual(store["spec"]["provider"]["vault"]["path"], "platform")
|
self.assertEqual(store["spec"]["provider"]["vault"]["path"], "platform")
|
||||||
self.assertEqual(store["spec"]["conditions"][0]["namespaces"], ["core-hub"])
|
self.assertEqual(store["spec"]["conditions"][0]["namespaces"], ["core-hub"])
|
||||||
|
|
||||||
|
def test_agent_boundary_denies_every_current_concrete_high_risk_catalog_path(self) -> None:
|
||||||
|
boundary = (
|
||||||
|
REPO_DIR / "openbao/policies/agent-high-risk-boundary.hcl"
|
||||||
|
).read_text()
|
||||||
|
data_paths = {
|
||||||
|
"platform/data/workloads/activity-core/llm-connect/llm-connect-provider-secrets",
|
||||||
|
"platform/data/workloads/railiance/backup/offsite-lane",
|
||||||
|
"platform/data/workloads/forgejo/forgejo-admin",
|
||||||
|
"tenants/data/binky/company-email/imap",
|
||||||
|
"tenants/data/binky/qonto-api",
|
||||||
|
"platform/data/workloads/coulomb/whynot-design/npm-publish",
|
||||||
|
"platform/data/workloads/rapp-qonto/keycape-client",
|
||||||
|
"platform/data/workloads/agent-harness/forgejo-deploy-key",
|
||||||
|
"platform/data/workloads/audit-core/senders",
|
||||||
|
"platform/data/workloads/email-connect/transactional",
|
||||||
|
"platform/data/workloads/railiance/scaleway/bootstrap",
|
||||||
|
}
|
||||||
|
for path in data_paths:
|
||||||
|
with self.subTest(path=path):
|
||||||
|
self.assertRegex(
|
||||||
|
boundary,
|
||||||
|
rf'path "{path}" \{{\s*capabilities = \["deny"\]',
|
||||||
|
)
|
||||||
|
|
||||||
database_policy = (
|
database_policy = (
|
||||||
REPO_DIR / "openbao/policies/external-secrets-core-hub-database.hcl"
|
REPO_DIR / "openbao/policies/external-secrets-core-hub-database.hcl"
|
||||||
).read_text()
|
).read_text()
|
||||||
|
|
|
||||||
124
workplans/RAILIANCE-WP-0022-agent-high-risk-boundary-coverage.md
Normal file
124
workplans/RAILIANCE-WP-0022-agent-high-risk-boundary-coverage.md
Normal file
|
|
@ -0,0 +1,124 @@
|
||||||
|
---
|
||||||
|
id: RAILIANCE-WP-0022
|
||||||
|
type: workplan
|
||||||
|
title: "Close agent high-risk OpenBao boundary coverage"
|
||||||
|
domain: financials
|
||||||
|
repo: railiance-platform
|
||||||
|
status: blocked
|
||||||
|
owner: codex
|
||||||
|
topic_slug: railiance
|
||||||
|
created: "2026-08-21"
|
||||||
|
updated: "2026-08-21"
|
||||||
|
related:
|
||||||
|
- WARDEN-WP-0032
|
||||||
|
- RISK-F-0009
|
||||||
|
origin: routed
|
||||||
|
origin_ref: "State Hub message 828e4903-30fe-4903-acfd-cd2ecdda437d"
|
||||||
|
---
|
||||||
|
|
||||||
|
# RAILIANCE-WP-0022 — Agent high-risk boundary coverage
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Make the OpenBao `agent-high-risk-boundary` deny every concrete high-risk KV
|
||||||
|
data path in the ops-warden routing catalog, verify the deployed policy, and
|
||||||
|
establish whether any agent identity actually carries the boundary.
|
||||||
|
|
||||||
|
## Boundaries
|
||||||
|
|
||||||
|
- Read policy documents, token-role metadata and capabilities only; never read
|
||||||
|
a Secret value.
|
||||||
|
- A deny is added only for a concrete catalog path graded `risk: high`.
|
||||||
|
- Pattern-only and non-KV lanes are reported but do not produce invented paths.
|
||||||
|
- Operator identities do not receive this boundary; it is for coding-agent
|
||||||
|
identities where deny must override any coincident workload read policy.
|
||||||
|
|
||||||
|
## T01 — Reconcile catalog coverage
|
||||||
|
|
||||||
|
```task
|
||||||
|
id: RAILIANCE-WP-0022-T01
|
||||||
|
status: done
|
||||||
|
priority: high
|
||||||
|
```
|
||||||
|
|
||||||
|
Run the capabilities-only ops-warden audit against the policy. The 2026-08-21
|
||||||
|
reconciliation found 17 high-risk lanes: six covered, six concrete uncovered,
|
||||||
|
and five without a concrete KV address. No credential value was read.
|
||||||
|
|
||||||
|
## T02 — Close the concrete deny gaps
|
||||||
|
|
||||||
|
```task
|
||||||
|
id: RAILIANCE-WP-0022-T02
|
||||||
|
status: done
|
||||||
|
priority: high
|
||||||
|
```
|
||||||
|
|
||||||
|
Add deny-data/read-metadata pairs for the six catalog paths: whynot-design npm,
|
||||||
|
rapp-qonto Keycape client, agent-harness Forgejo deploy key, audit-core senders,
|
||||||
|
email-connect transactional, and Scaleway bootstrap. Add regression coverage
|
||||||
|
for every concrete path currently emitted by the catalog audit.
|
||||||
|
|
||||||
|
Completed 2026-08-21. The source policy covers all 12 unique concrete paths
|
||||||
|
(including the Core Hub path, which has no catalog lane), and the local
|
||||||
|
catalog audit reports all 12 catalog entries covered with none uncovered.
|
||||||
|
|
||||||
|
## T03 — Apply and verify live
|
||||||
|
|
||||||
|
```task
|
||||||
|
id: RAILIANCE-WP-0022-T03
|
||||||
|
status: done
|
||||||
|
priority: high
|
||||||
|
```
|
||||||
|
|
||||||
|
Under attended platform authority, upload the reviewed policy, read it back,
|
||||||
|
and rerun the catalog audit with `policy_source: server`. Inspect policy/token
|
||||||
|
role metadata to determine whether an agent identity carries the boundary and
|
||||||
|
whether any role combines it with a workload-read policy. Do not mint a token.
|
||||||
|
|
||||||
|
Completed 2026-08-21 under attended `platform-admin` OIDC. OpenBao accepted the
|
||||||
|
policy; normalized readback matched source, and the server-backed catalog audit
|
||||||
|
reported 17 high-risk lanes, 12 covered entries, zero uncovered, and five
|
||||||
|
pattern/non-KV lanes without a concrete address. Metadata-only inspection of
|
||||||
|
all discoverable netkingdom, Kubernetes, AppRole, and token roles found zero
|
||||||
|
attachments of `agent-high-risk-boundary` and therefore zero roles combining
|
||||||
|
it with a workload-read policy. No token was minted and no Secret was read.
|
||||||
|
|
||||||
|
## T04 — Route the result
|
||||||
|
|
||||||
|
```task
|
||||||
|
id: RAILIANCE-WP-0022-T04
|
||||||
|
status: done
|
||||||
|
priority: medium
|
||||||
|
```
|
||||||
|
|
||||||
|
Reply to ops-warden with the deployment evidence and remaining attachment
|
||||||
|
finding. Request a generated, versioned concrete-deny artifact so future policy
|
||||||
|
updates consume catalog output rather than relying on a hand-maintained list.
|
||||||
|
|
||||||
|
Completed 2026-08-21 via State Hub message
|
||||||
|
`fe727451-163a-4e14-8ce3-187fea8ce5b3`, including live audit counts, the zero-
|
||||||
|
attachment finding, the distinct-agent-identity blocker, and the requested
|
||||||
|
versioned generated artifact shape.
|
||||||
|
|
||||||
|
## T05 — Establish a distinct coding-agent identity
|
||||||
|
|
||||||
|
```task
|
||||||
|
id: RAILIANCE-WP-0022-T05
|
||||||
|
status: wait
|
||||||
|
priority: high
|
||||||
|
```
|
||||||
|
|
||||||
|
The policy is live but no role attaches it. Do not add the boundary to
|
||||||
|
`platform-admin`: that is an attended human/operator role whose legitimate
|
||||||
|
recovery work may require the protected values. The identity owner must define
|
||||||
|
a distinct coding-agent issuance path, attach this boundary there, and prove
|
||||||
|
deny-wins behavior when combined with an otherwise readable workload policy.
|
||||||
|
This is blocked on an identity-owner decision and is not invented here.
|
||||||
|
|
||||||
|
## Acceptance
|
||||||
|
|
||||||
|
- [x] Every concrete high-risk catalog path is denied in the source policy.
|
||||||
|
- [x] The live policy matches source and the server-backed audit passes.
|
||||||
|
- [x] Agent boundary attachment is established from metadata (currently zero).
|
||||||
|
- [x] Result and generated-artifact follow-up are routed to ops-warden.
|
||||||
|
- [ ] A distinct coding-agent identity actually attaches the boundary.
|
||||||
|
|
@ -4,11 +4,11 @@ type: workplan
|
||||||
title: "Close CCR schema drift: one active lane unmigrated, one draft the suite cannot express"
|
title: "Close CCR schema drift: one active lane unmigrated, one draft the suite cannot express"
|
||||||
domain: financials
|
domain: financials
|
||||||
repo: railiance-platform
|
repo: railiance-platform
|
||||||
status: proposed
|
status: finished
|
||||||
owner: codex
|
owner: codex
|
||||||
topic_slug: railiance
|
topic_slug: railiance
|
||||||
created: "2026-08-18"
|
created: "2026-08-18"
|
||||||
updated: "2026-08-18"
|
updated: "2026-08-21"
|
||||||
related:
|
related:
|
||||||
- RPF-WP-0014
|
- RPF-WP-0014
|
||||||
origin: residual
|
origin: residual
|
||||||
|
|
@ -38,22 +38,22 @@ one makes the suite green first.
|
||||||
|
|
||||||
## The two problems
|
## The two problems
|
||||||
|
|
||||||
**P1 — an active credential lane is unmigrated.**
|
**P1 — an active credential lane uses an unrepresented auth mode.**
|
||||||
`credential-change-requests/CCR-2026-0010-email-connect-transactional.yaml`
|
`credential-change-requests/CCR-2026-0010-email-connect-transactional.yaml`
|
||||||
carries `status: active` and `readiness: ready`, and is missing the entire
|
carries `status: active` and `readiness: ready`. It has always declared the
|
||||||
`openbao.auth` block: `method`, `mount`, `role`, `policies`, `bound_claims`.
|
live transitional ESO token Secret, bootstrap script, policy and TTL, but the
|
||||||
The validator gained those requirements and this CCR was never brought
|
validator understood only OIDC and Kubernetes auth and consequently reported
|
||||||
forward.
|
the role/bound-claim fields for those modes as missing.
|
||||||
|
|
||||||
This is the one that matters. A live lane whose declaration does not describe
|
This is the one that matters. A live lane whose declaration does not describe
|
||||||
how the workload authenticates is a governance gap, not a lint failure — the
|
how the workload authenticates is a governance gap, not a lint failure — the
|
||||||
document that is supposed to be the authority on the lane cannot answer the
|
document that is supposed to be the authority on the lane cannot answer the
|
||||||
first question anyone would ask of it. The lane itself is presumably working,
|
first question anyone would ask of it. The live lane is working, which is
|
||||||
which is exactly what makes it easy to leave.
|
exactly what made this representation gap easy to leave.
|
||||||
|
|
||||||
**P2 — a genuine in-flight draft the suite cannot express.**
|
**P2 — a genuine in-flight request the suite could not express.**
|
||||||
`CCR-2026-0011-scaleway-object-storage-bootstrap.yaml` carries
|
`CCR-2026-0011-scaleway-object-storage-bootstrap.yaml` carries
|
||||||
`status: apply_pending` and `readiness: waiting-on-ui-replace-of-xxx-placeholders`.
|
`status: apply_pending` and an out-of-enum placeholder readiness string.
|
||||||
It is a founder-bootstrap credential still holding placeholder values, and
|
It is a founder-bootstrap credential still holding placeholder values, and
|
||||||
`ops-warden` already tracks it as a draft lane. Its errors include a
|
`ops-warden` already tracks it as a draft lane. Its errors include a
|
||||||
`readiness` value outside the permitted enum, which is the file honestly
|
`readiness` value outside the permitted enum, which is the file honestly
|
||||||
|
|
@ -76,7 +76,7 @@ draft and a real gap produce identical output.
|
||||||
|
|
||||||
```task
|
```task
|
||||||
id: RPF-WP-0020-T01
|
id: RPF-WP-0020-T01
|
||||||
status: todo
|
status: done
|
||||||
priority: high
|
priority: high
|
||||||
state_hub_task_id: "3bf8bf9c-ac33-4ce7-8b3f-5b40135b227c"
|
state_hub_task_id: "3bf8bf9c-ac33-4ce7-8b3f-5b40135b227c"
|
||||||
```
|
```
|
||||||
|
|
@ -88,9 +88,18 @@ policy file, not by inventing plausible values. If the live configuration and
|
||||||
the declaration disagree, the live configuration is the fact and the
|
the declaration disagree, the live configuration is the fact and the
|
||||||
disagreement is the finding.
|
disagreement is the finding.
|
||||||
|
|
||||||
|
Completed 2026-08-21. Live read-only Kubernetes metadata showed
|
||||||
|
`ClusterSecretStore/openbao-email-connect` using
|
||||||
|
`external-secrets/openbao-email-connect-eso-token`, limited to namespace
|
||||||
|
`email-connect`, and reporting Valid; its ExternalSecret reported
|
||||||
|
SecretSynced. The schema now represents this transitional token mode directly,
|
||||||
|
requires the ESO policy artifact, Secret reference, bootstrap script, bounded
|
||||||
|
TTL and explicit Kubernetes-auth follow-up, and keeps delegated token creation
|
||||||
|
out of scope. No Secret value was read.
|
||||||
|
|
||||||
```task
|
```task
|
||||||
id: RPF-WP-0020-T02
|
id: RPF-WP-0020-T02
|
||||||
status: todo
|
status: done
|
||||||
priority: medium
|
priority: medium
|
||||||
state_hub_task_id: "12933d69-82a3-470d-b01c-0c40c28a7984"
|
state_hub_task_id: "12933d69-82a3-470d-b01c-0c40c28a7984"
|
||||||
```
|
```
|
||||||
|
|
@ -100,9 +109,16 @@ a `status` the validator recognises as not-yet-complete, with the test
|
||||||
asserting that such files are still well-formed in every other respect. An
|
asserting that such files are still well-formed in every other respect. An
|
||||||
allowlist of filenames would work today and rot on the next draft.
|
allowlist of filenames would work today and rot on the next draft.
|
||||||
|
|
||||||
|
Completed 2026-08-21. Added the non-applyable `in_flight` status. It requires a
|
||||||
|
named owner, blocking reason and exact `missing_fields`; only
|
||||||
|
`openbao.policy_file` and `openbao.auth` may be declared incomplete, while all
|
||||||
|
other CCR structure remains validated. CCR-2026-0011 now uses this state and a
|
||||||
|
valid non-resolvable front-door readiness without inventing policy/auth
|
||||||
|
metadata or filling credential placeholders.
|
||||||
|
|
||||||
```task
|
```task
|
||||||
id: RPF-WP-0020-T03
|
id: RPF-WP-0020-T03
|
||||||
status: todo
|
status: done
|
||||||
priority: medium
|
priority: medium
|
||||||
state_hub_task_id: "da52b518-c6fb-40b8-acb9-c64724ca4eee"
|
state_hub_task_id: "da52b518-c6fb-40b8-acb9-c64724ca4eee"
|
||||||
```
|
```
|
||||||
|
|
@ -111,9 +127,15 @@ requirement was added without migrating existing active CCRs, other repos
|
||||||
carrying CCRs may have the same gap and no failing test to reveal it. Confirm
|
carrying CCRs may have the same gap and no failing test to reveal it. Confirm
|
||||||
whether the requirement originated here or upstream, and notify accordingly.
|
whether the requirement originated here or upstream, and notify accordingly.
|
||||||
|
|
||||||
|
Completed 2026-08-21. Git history traces the validator requirement to local
|
||||||
|
commit `815b124`; a filesystem-wide declaration search found CCR files and the
|
||||||
|
validator/schema implementation only in `railiance-platform`. There is no
|
||||||
|
upstream CCR implementation to migrate or notify. The migration obligation is
|
||||||
|
now documented locally.
|
||||||
|
|
||||||
```task
|
```task
|
||||||
id: RPF-WP-0020-T04
|
id: RPF-WP-0020-T04
|
||||||
status: todo
|
status: done
|
||||||
priority: low
|
priority: low
|
||||||
state_hub_task_id: "2259ee69-4914-42c4-9175-8c61b2206888"
|
state_hub_task_id: "2259ee69-4914-42c4-9175-8c61b2206888"
|
||||||
```
|
```
|
||||||
|
|
@ -122,6 +144,12 @@ suite passes. Record in `docs/credential-change-approval.md` that a new
|
||||||
required field obliges a migration pass over existing active CCRs — the
|
required field obliges a migration pass over existing active CCRs — the
|
||||||
omission that produced P1.
|
omission that produced P1.
|
||||||
|
|
||||||
|
Completed 2026-08-21. Regression coverage proves the live token-auth shape,
|
||||||
|
explicit in-flight omissions, rejection of undeclared omissions, and safe
|
||||||
|
status/summary rendering. The approval guide now requires a migration pass over
|
||||||
|
all existing CCRs whenever required fields are added or strengthened. The full
|
||||||
|
repository test suite passes.
|
||||||
|
|
||||||
## Risks
|
## Risks
|
||||||
|
|
||||||
**T01 invents values to make the test pass.** The likeliest failure and the
|
**T01 invents values to make the test pass.** The likeliest failure and the
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue