Finish coding-agent high-risk boundary coverage
Assistant: codex Assistant-Model: gpt-5.6-sol Assistant-Session: 01a02669-87ee-7a31-b111-edc95a16e0fa
This commit is contained in:
parent
7a1dcb8a52
commit
429cc912ed
12 changed files with 720 additions and 54 deletions
5
Makefile
5
Makefile
|
|
@ -340,6 +340,9 @@ credential-change-lifecycle-plan: ## Render deactivation/rotation/compromise lif
|
||||||
credential-change-lifecycle-event: ## Record lifecycle event; pass CREDENTIAL_CHANGE_LIFECYCLE_ARGS
|
credential-change-lifecycle-event: ## Record lifecycle event; pass CREDENTIAL_CHANGE_LIFECYCLE_ARGS
|
||||||
scripts/credential-change.py lifecycle-event $(CREDENTIAL_CHANGE) --action $(CREDENTIAL_CHANGE_LIFECYCLE_ACTION) $(CREDENTIAL_CHANGE_LIFECYCLE_ARGS)
|
scripts/credential-change.py lifecycle-event $(CREDENTIAL_CHANGE) --action $(CREDENTIAL_CHANGE_LIFECYCLE_ACTION) $(CREDENTIAL_CHANGE_LIFECYCLE_ARGS)
|
||||||
|
|
||||||
|
agent-high-risk-boundary-check: ## Check policy against the versioned ops-warden high-risk path input
|
||||||
|
python3 scripts/agent_high_risk_boundary.py --json
|
||||||
|
|
||||||
credential-change-import-inventory: ## Import existing lane as non-secret CCR; pass CREDENTIAL_CHANGE_IMPORT_ARGS
|
credential-change-import-inventory: ## Import existing lane as non-secret CCR; pass CREDENTIAL_CHANGE_IMPORT_ARGS
|
||||||
scripts/credential-change.py import-inventory $(CREDENTIAL_CHANGE_IMPORT_ARGS)
|
scripts/credential-change.py import-inventory $(CREDENTIAL_CHANGE_IMPORT_ARGS)
|
||||||
|
|
||||||
|
|
@ -464,4 +467,4 @@ help: ## Show this help
|
||||||
/^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) }' $(MAKEFILE_LIST)
|
/^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) }' $(MAKEFILE_LIST)
|
||||||
|
|
||||||
.PHONY: apps-pg-isolation-probe
|
.PHONY: apps-pg-isolation-probe
|
||||||
.PHONY: db-deploy db-status db-shell db-logs forgejo-db-deploy forgejo-db-status forgejo-db-shell apps-pg-deploy apps-pg-status apps-pg-shell apps-pg-logs net-kingdom-pg-inter-hub-networkpolicy-deploy pg-deploy pg-status pg-pgpool-check valkey-deploy valkey-status openbao-repo openbao-dry-run openbao-overlay-apply openbao-verify-login-overlay openbao-deploy openbao-status openbao-verify openbao-verify-post-unseal openbao-configure-initial openbao-configure-ssh openbao-verify-ssh openbao-verify-authenticated openbao-configure-external-secrets-issue-core openbao-configure-external-secrets-activity-core openbao-configure-external-secrets-forgejo openbao-validate-restore-evidence openbao-validate-emergency-evidence credential-grants-validate credential-change-applier-dry-run credential-change-applier-apply-plan credential-change-applier-apply credential-change-runbook credential-change-record-evidence credential-change-lifecycle-plan credential-change-lifecycle-event credential-change-import-inventory openbao-credential-change-appliers-dry-run openbao-configure-credential-change-appliers openbao-token-grants-dry-run openbao-configure-token-grants openbao-verify-token-grants-dry-run openbao-verify-token-grants openbao-verify-token-grants-smoke credential-helper-dry-run credential-tests credential-exec-ops-warden-smoke argocd-bootstrap-dry-run argocd-bootstrap-deploy argocd-repo-apply argocd-status backup forgejo-backup forgejo-backup-dry-run forgejo-backup-status forgejo-package-prune forgejo-package-prune-dry-run reef-deployables apps-pg-evidence consumption-preflight help
|
.PHONY: db-deploy db-status db-shell db-logs forgejo-db-deploy forgejo-db-status forgejo-db-shell apps-pg-deploy apps-pg-status apps-pg-shell apps-pg-logs net-kingdom-pg-inter-hub-networkpolicy-deploy pg-deploy pg-status pg-pgpool-check valkey-deploy valkey-status openbao-repo openbao-dry-run openbao-overlay-apply openbao-verify-login-overlay openbao-deploy openbao-status openbao-verify openbao-verify-post-unseal openbao-configure-initial openbao-configure-ssh openbao-verify-ssh openbao-verify-authenticated openbao-configure-external-secrets-issue-core openbao-configure-external-secrets-activity-core openbao-configure-external-secrets-forgejo openbao-validate-restore-evidence openbao-validate-emergency-evidence credential-grants-validate credential-change-applier-dry-run credential-change-applier-apply-plan credential-change-applier-apply credential-change-runbook credential-change-record-evidence credential-change-lifecycle-plan credential-change-lifecycle-event credential-change-import-inventory agent-high-risk-boundary-check openbao-credential-change-appliers-dry-run openbao-configure-credential-change-appliers openbao-token-grants-dry-run openbao-configure-token-grants openbao-verify-token-grants-dry-run openbao-verify-token-grants openbao-verify-token-grants-smoke credential-helper-dry-run credential-tests credential-exec-ops-warden-smoke argocd-bootstrap-dry-run argocd-bootstrap-deploy argocd-repo-apply argocd-status backup forgejo-backup forgejo-backup-dry-run forgejo-backup-status forgejo-package-prune forgejo-package-prune-dry-run reef-deployables apps-pg-evidence consumption-preflight help
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@
|
||||||
| 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 | RAILIANCE-WP-0022 | finished | — | workplans/RAILIANCE-WP-0022-agent-high-risk-boundary-coverage.md |
|
||||||
| workplan | RAILIANCE-WP-0023 | finished | — | workplans/RAILIANCE-WP-0023-hub-core-candidate-credential-lanes.md |
|
| workplan | RAILIANCE-WP-0023 | finished | — | workplans/RAILIANCE-WP-0023-hub-core-candidate-credential-lanes.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 |
|
||||||
|
|
@ -91,7 +91,8 @@
|
||||||
| task | RAILIANCE-WP-0022-T02 | 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-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-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 | RAILIANCE-WP-0022-T05 | done | — | workplans/RAILIANCE-WP-0022-agent-high-risk-boundary-coverage.md |
|
||||||
|
| task | RAILIANCE-WP-0022-T06 | done | — | workplans/RAILIANCE-WP-0022-agent-high-risk-boundary-coverage.md |
|
||||||
| task | RAILIANCE-WP-0023-T01 | done | — | workplans/RAILIANCE-WP-0023-hub-core-candidate-credential-lanes.md |
|
| task | RAILIANCE-WP-0023-T01 | done | — | workplans/RAILIANCE-WP-0023-hub-core-candidate-credential-lanes.md |
|
||||||
| task | RAILIANCE-WP-0023-T02 | done | — | workplans/RAILIANCE-WP-0023-hub-core-candidate-credential-lanes.md |
|
| task | RAILIANCE-WP-0023-T02 | done | — | workplans/RAILIANCE-WP-0023-hub-core-candidate-credential-lanes.md |
|
||||||
| task | RAILIANCE-WP-0023-T03 | done | — | workplans/RAILIANCE-WP-0023-hub-core-candidate-credential-lanes.md |
|
| task | RAILIANCE-WP-0023-T03 | done | — | workplans/RAILIANCE-WP-0023-hub-core-candidate-credential-lanes.md |
|
||||||
|
|
|
||||||
117
docs/coding-agent-openbao-identity.md
Normal file
117
docs/coding-agent-openbao-identity.md
Normal file
|
|
@ -0,0 +1,117 @@
|
||||||
|
# Coding-agent OpenBao identity
|
||||||
|
|
||||||
|
`RAILIANCE-WP-0022` requires an OpenBao identity that is distinct from the
|
||||||
|
attended human `platform-admin` role. The identity is split across its proper
|
||||||
|
owners:
|
||||||
|
|
||||||
|
- KeyCape owns a confidential `client_credentials` service principal and its
|
||||||
|
signed short-lived JWT.
|
||||||
|
- railiance-platform owns the exact-bound OpenBao JWT role and the policies on
|
||||||
|
the resulting OpenBao token.
|
||||||
|
- ops-warden remains an interested consumer and SSH-certificate issuer; it does
|
||||||
|
not issue this identity or its credentials.
|
||||||
|
|
||||||
|
The operational identity is the platform-owned AppRole
|
||||||
|
`coding-agent-railiance-platform`. The exact-bound KeyCape JWT role is the
|
||||||
|
issuer-backed migration target; it does not block the deny-wins control while
|
||||||
|
the KeyCape client is being registered.
|
||||||
|
|
||||||
|
## Operational AppRole
|
||||||
|
|
||||||
|
The reviewed role body is `openbao/auth/coding-agent-approle.json`. Apply it
|
||||||
|
with attended platform authority:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bao write auth/approle/role/coding-agent-railiance-platform \
|
||||||
|
@openbao/auth/coding-agent-approle.json
|
||||||
|
```
|
||||||
|
|
||||||
|
This is a standing machine role, not a manually minted token. Every SecretID
|
||||||
|
expires after five minutes and is single-use; resulting tokens last at most 15
|
||||||
|
minutes, have eight uses, omit the default policy, and always carry
|
||||||
|
`agent-high-risk-boundary`. The role also carries the otherwise-readable
|
||||||
|
issue-core policy solely to make the deny-wins invariant directly testable.
|
||||||
|
The boundary permits token metadata lookup, capabilities inspection, and
|
||||||
|
self-revocation; it does not grant token creation or management of any other
|
||||||
|
token.
|
||||||
|
|
||||||
|
Run the non-disclosing verifier while holding attended authority to create one
|
||||||
|
ephemeral SecretID:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 scripts/verify_coding_agent_approle.py
|
||||||
|
```
|
||||||
|
|
||||||
|
The verifier holds the SecretID and token only in memory, calls
|
||||||
|
`sys/capabilities-self` rather than a KV read, and revokes the test token before
|
||||||
|
exit. It prints only policy names, TTL, capabilities, and pass/fail state.
|
||||||
|
|
||||||
|
## KeyCape contract
|
||||||
|
|
||||||
|
The accepted service token must contain exactly the following identity
|
||||||
|
coordinates:
|
||||||
|
|
||||||
|
| Claim | Required value |
|
||||||
|
| --- | --- |
|
||||||
|
| `aud` / client id | `codex-railiance-platform` |
|
||||||
|
| `sub` | `service:codex:railiance-platform` |
|
||||||
|
| `principal_type` | `service` |
|
||||||
|
| `tenant` | `tenant:coulomb` |
|
||||||
|
| `roles` | contains `coding-agent` |
|
||||||
|
| requested scope | `openbao:login` |
|
||||||
|
|
||||||
|
The KeyCape access token lifetime is 15 minutes. Its confidential client secret
|
||||||
|
must be generated and delivered through an approved out-of-repository custody
|
||||||
|
path. It must never appear in Git, State Hub, command arguments, logs, or chat.
|
||||||
|
|
||||||
|
## KeyCape-backed OpenBao role
|
||||||
|
|
||||||
|
The migration-target role body is
|
||||||
|
`openbao/auth/coding-agent-jwt-role.json`. Apply it only with attended platform
|
||||||
|
authority:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bao write auth/netkingdom/role/coding-agent \
|
||||||
|
@openbao/auth/coding-agent-jwt-role.json
|
||||||
|
```
|
||||||
|
|
||||||
|
The role binds the exact service audience, subject, principal type, tenant, and
|
||||||
|
role. It produces a 15-minute, eight-use token without the default policy. The
|
||||||
|
token deliberately combines `agent-high-risk-boundary` with
|
||||||
|
`workload-kv-read-issue-core-runtime`: the latter would otherwise read the
|
||||||
|
issue-core bundle, while the former must win with `deny`.
|
||||||
|
|
||||||
|
## Value-safe verification
|
||||||
|
|
||||||
|
Do not verify by attempting `bao kv get`; an unexpected policy error could
|
||||||
|
print a protected value. After KeyCape exchanges the client credential and the
|
||||||
|
JWT login produces an OpenBao token through a mode-0600 temporary file, use only
|
||||||
|
capabilities and token metadata:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
BAO_TOKEN_FILE=/run/user/$(id -u)/coding-agent.openbao-token
|
||||||
|
BAO_TOKEN=$(head -n 1 "$BAO_TOKEN_FILE") \
|
||||||
|
bao token capabilities \
|
||||||
|
platform/data/workloads/issue-core/issue-core/issue-core-runtime
|
||||||
|
BAO_TOKEN=$(head -n 1 "$BAO_TOKEN_FILE") \
|
||||||
|
bao token capabilities \
|
||||||
|
platform/metadata/workloads/issue-core/issue-core/issue-core-runtime
|
||||||
|
BAO_TOKEN=$(head -n 1 "$BAO_TOKEN_FILE") bao token lookup -format=json \
|
||||||
|
| jq '{display_name:.data.display_name,policies:.data.policies,ttl:.data.ttl}'
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected results are `deny` for the data path and `read` for the metadata path.
|
||||||
|
The lookup must list both policies, omit `default`, and report a TTL no greater
|
||||||
|
than 15 minutes. Shred the temporary JWT and OpenBao token files after the
|
||||||
|
check. Revoke the issued OpenBao token if it remains live.
|
||||||
|
|
||||||
|
## Abort and rollback
|
||||||
|
|
||||||
|
- Abort if any bound claim differs, the JWT issuer/audience is not the live
|
||||||
|
KeyCape contract, or a token would be printed.
|
||||||
|
- Delete `auth/netkingdom/role/coding-agent` if deny-wins does not appear in
|
||||||
|
capabilities or the token includes an unexpected policy.
|
||||||
|
- Delete `auth/approle/role/coding-agent-railiance-platform` if its metadata
|
||||||
|
differs from the reviewed body or its verifier fails.
|
||||||
|
- Revoke the test OpenBao token, disable the KeyCape client, and rotate its
|
||||||
|
confidential secret after suspected disclosure.
|
||||||
|
|
@ -22,6 +22,23 @@ modified.
|
||||||
catalog entries share the Binky IMAP path and Core Hub is an additional
|
catalog entries share the Binky IMAP path and Core Hub is an additional
|
||||||
reviewed deny without a catalog lane.
|
reviewed deny without a catalog lane.
|
||||||
|
|
||||||
|
### 2026-08-22 generated-input reconciliation
|
||||||
|
|
||||||
|
- Ops-warden delivered generated artifact revision
|
||||||
|
`55f0f47a021375b8b25c924953d1b49a24e002c5`: 19 high-risk lanes, 14
|
||||||
|
concrete entries (13 unique paths), and five pattern/non-KV lanes.
|
||||||
|
- The corrected grades add issue-core runtime (whose bundle also contains
|
||||||
|
`GITEA_BACKEND_TOKEN`) and reuse-surface runtime (whose bundle also contains
|
||||||
|
`REUSE_SURFACE_FORGEJO_WEBHOOK_SECRET`). Both data paths now have explicit
|
||||||
|
deny blocks and read-only metadata blocks.
|
||||||
|
- `scripts/agent_high_risk_boundary.py` verified that the vendored input equals
|
||||||
|
the clean upstream artifact aside from generation time and that every entry
|
||||||
|
is covered. Forty-nine focused tests passed.
|
||||||
|
- Under attended `platform-admin` OIDC, the policy was uploaded and read back.
|
||||||
|
OpenBao added only its normal trailing newline. The server-backed ops-warden
|
||||||
|
audit reported 19 high-risk lanes, 14 covered, zero uncovered, and five with
|
||||||
|
no concrete address.
|
||||||
|
|
||||||
## Attachment audit and residual blocker
|
## Attachment audit and residual blocker
|
||||||
|
|
||||||
A metadata-only scan listed and read role configuration under netkingdom OIDC,
|
A metadata-only scan listed and read role configuration under netkingdom OIDC,
|
||||||
|
|
@ -30,14 +47,33 @@ Kubernetes auth, AppRole, and token roles. It found:
|
||||||
- roles attaching `agent-high-risk-boundary`: **0**;
|
- roles attaching `agent-high-risk-boundary`: **0**;
|
||||||
- roles combining it with any `workload-kv-read-*` policy: **0**.
|
- roles combining it with any `workload-kv-read-*` policy: **0**.
|
||||||
|
|
||||||
The live policy is therefore complete but is not automatically attached to a
|
The 2026-08-21 scan accurately found no attachment at that time. On 2026-08-22,
|
||||||
coding-agent identity. The documented manual short-lived token example is not
|
State Hub decision `f0955252-7b20-4c80-86e9-f8080ec60793` selected the
|
||||||
a standing identity and carries no workload-read policy. Attaching the boundary
|
platform-owned `coding-agent-railiance-platform` AppRole as the operational
|
||||||
to `platform-admin` would incorrectly constrain the attended operator role and
|
machine identity while retaining the KeyCape JWT role as an issuer-backed
|
||||||
erase the human/agent distinction, so that change was not made.
|
migration target.
|
||||||
|
|
||||||
The remaining work is an identity-owner decision: define a distinct coding-
|
Live AppRole metadata matched the reviewed source:
|
||||||
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
|
- `bind_secret_id=true`, `secret_id_ttl=300`, and
|
||||||
high-risk deny paths is also requested from ops-warden so policy coverage does
|
`secret_id_num_uses=1`;
|
||||||
not depend on manual catalog transcription.
|
- `token_ttl=900`, `token_max_ttl=900`, `token_num_uses=8`, and no default
|
||||||
|
policy;
|
||||||
|
- policies exactly `agent-high-risk-boundary` and
|
||||||
|
`workload-kv-read-issue-core-runtime`.
|
||||||
|
|
||||||
|
The non-disclosing verifier minted one ephemeral SecretID, logged in, and used
|
||||||
|
only token metadata plus `sys/capabilities-self`. The data path resolved to
|
||||||
|
`deny`; its metadata path resolved to `read`; the token TTL was 900 seconds.
|
||||||
|
The first verification exposed that a no-default-policy token could not revoke
|
||||||
|
itself. That bounded test token was located and revoked by accessor under
|
||||||
|
attended authority, `auth/token/revoke-self:update` was added to the boundary,
|
||||||
|
and the repeat verification passed and self-revoked without warning. No KV read
|
||||||
|
was attempted and no SecretID, token, or protected value was printed or logged.
|
||||||
|
|
||||||
|
The exact-bound JWT role `auth/netkingdom/role/coding-agent` was also applied
|
||||||
|
with audience `codex-railiance-platform`, subject
|
||||||
|
`service:codex:railiance-platform`, `principal_type=service`,
|
||||||
|
`tenant=tenant:coulomb`, and role `coding-agent`. KeyCape client registration
|
||||||
|
is a future identity-hardening handoff; it is not required for the live AppRole
|
||||||
|
attachment proven here.
|
||||||
|
|
|
||||||
13
openbao/auth/coding-agent-approle.json
Normal file
13
openbao/auth/coding-agent-approle.json
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
{
|
||||||
|
"bind_secret_id": true,
|
||||||
|
"secret_id_ttl": "5m",
|
||||||
|
"secret_id_num_uses": 1,
|
||||||
|
"token_policies": [
|
||||||
|
"agent-high-risk-boundary",
|
||||||
|
"workload-kv-read-issue-core-runtime"
|
||||||
|
],
|
||||||
|
"token_ttl": "15m",
|
||||||
|
"token_max_ttl": "15m",
|
||||||
|
"token_num_uses": 8,
|
||||||
|
"token_no_default_policy": true
|
||||||
|
}
|
||||||
23
openbao/auth/coding-agent-jwt-role.json
Normal file
23
openbao/auth/coding-agent-jwt-role.json
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
{
|
||||||
|
"role_type": "jwt",
|
||||||
|
"bound_audiences": [
|
||||||
|
"codex-railiance-platform"
|
||||||
|
],
|
||||||
|
"user_claim": "sub",
|
||||||
|
"bound_claims_type": "string",
|
||||||
|
"bound_claims": {
|
||||||
|
"sub": "service:codex:railiance-platform",
|
||||||
|
"principal_type": "service",
|
||||||
|
"tenant": "tenant:coulomb",
|
||||||
|
"roles": "coding-agent"
|
||||||
|
},
|
||||||
|
"token_policies": [
|
||||||
|
"agent-high-risk-boundary",
|
||||||
|
"workload-kv-read-issue-core-runtime"
|
||||||
|
],
|
||||||
|
"token_ttl": "15m",
|
||||||
|
"token_max_ttl": "15m",
|
||||||
|
"token_num_uses": 8,
|
||||||
|
"token_no_default_policy": true,
|
||||||
|
"verbose_oidc_logging": false
|
||||||
|
}
|
||||||
|
|
@ -22,6 +22,18 @@ path "platform/data/workloads/activity-core/llm-connect/llm-connect-provider-sec
|
||||||
path "platform/metadata/workloads/activity-core/llm-connect/llm-connect-provider-secrets" {
|
path "platform/metadata/workloads/activity-core/llm-connect/llm-connect-provider-secrets" {
|
||||||
capabilities = ["read"]
|
capabilities = ["read"]
|
||||||
}
|
}
|
||||||
|
path "platform/data/workloads/issue-core/issue-core/issue-core-runtime" {
|
||||||
|
capabilities = ["deny"]
|
||||||
|
}
|
||||||
|
path "platform/metadata/workloads/issue-core/issue-core/issue-core-runtime" {
|
||||||
|
capabilities = ["read"]
|
||||||
|
}
|
||||||
|
path "platform/data/workloads/reuse/reuse-surface/runtime-secrets" {
|
||||||
|
capabilities = ["deny"]
|
||||||
|
}
|
||||||
|
path "platform/metadata/workloads/reuse/reuse-surface/runtime-secrets" {
|
||||||
|
capabilities = ["read"]
|
||||||
|
}
|
||||||
path "platform/data/workloads/core-hub/runtime" {
|
path "platform/data/workloads/core-hub/runtime" {
|
||||||
capabilities = ["deny"]
|
capabilities = ["deny"]
|
||||||
}
|
}
|
||||||
|
|
@ -85,3 +97,6 @@ path "sys/capabilities-self" {
|
||||||
path "auth/token/lookup-self" {
|
path "auth/token/lookup-self" {
|
||||||
capabilities = ["read"]
|
capabilities = ["read"]
|
||||||
}
|
}
|
||||||
|
path "auth/token/revoke-self" {
|
||||||
|
capabilities = ["update"]
|
||||||
|
}
|
||||||
|
|
|
||||||
90
openbao/policies/inputs/ops-warden-high-risk-data-paths.yaml
Normal file
90
openbao/policies/inputs/ops-warden-high-risk-data-paths.yaml
Normal file
|
|
@ -0,0 +1,90 @@
|
||||||
|
# GENERATED by ops-warden/scripts/emit_high_risk_paths.py -- do not edit by hand.
|
||||||
|
# Vendored policy input. ops-warden owns the risk grade; railiance-platform owns
|
||||||
|
# whether and how agent-high-risk-boundary denies each path.
|
||||||
|
|
||||||
|
generated_at: "2026-08-21T11:28:33Z"
|
||||||
|
source: ops-warden/registry/routing/catalog.yaml
|
||||||
|
catalog_revision: "55f0f47a021375b8b25c924953d1b49a24e002c5"
|
||||||
|
catalog_revision_date: "2026-08-21T13:26:10+02:00"
|
||||||
|
catalog_dirty: false
|
||||||
|
high_risk_lane_count: 19
|
||||||
|
concrete_path_count: 14
|
||||||
|
|
||||||
|
no_concrete_path:
|
||||||
|
- database-dynamic-credentials
|
||||||
|
- inter-hub-bootstrap-ssh
|
||||||
|
- object-storage-sts
|
||||||
|
- openbao-api-key
|
||||||
|
- ops-warden-warden-sign-token
|
||||||
|
|
||||||
|
paths:
|
||||||
|
- id: agent-harness-binky-mail-approle
|
||||||
|
data_path: tenants/data/binky/company-email/imap
|
||||||
|
metadata_path: tenants/metadata/binky/company-email/imap
|
||||||
|
owner_repo: railiance-platform
|
||||||
|
fields: null
|
||||||
|
- id: agent-harness-forgejo-deploy
|
||||||
|
data_path: platform/data/workloads/agent-harness/forgejo-deploy-key
|
||||||
|
metadata_path: platform/metadata/workloads/agent-harness/forgejo-deploy-key
|
||||||
|
owner_repo: railiance-platform
|
||||||
|
fields: null
|
||||||
|
- id: audit-core-senders
|
||||||
|
data_path: platform/data/workloads/audit-core/senders
|
||||||
|
metadata_path: platform/metadata/workloads/audit-core/senders
|
||||||
|
owner_repo: ops-mason
|
||||||
|
fields: null
|
||||||
|
- id: binky-company-email-imap
|
||||||
|
data_path: tenants/data/binky/company-email/imap
|
||||||
|
metadata_path: tenants/metadata/binky/company-email/imap
|
||||||
|
owner_repo: railiance-platform
|
||||||
|
fields: null
|
||||||
|
- id: binky-qonto-api
|
||||||
|
data_path: tenants/data/binky/qonto-api
|
||||||
|
metadata_path: tenants/metadata/binky/qonto-api
|
||||||
|
owner_repo: railiance-platform
|
||||||
|
fields: null
|
||||||
|
- id: email-connect-transactional
|
||||||
|
data_path: platform/data/workloads/email-connect/transactional
|
||||||
|
metadata_path: platform/metadata/workloads/email-connect/transactional
|
||||||
|
owner_repo: railiance-platform
|
||||||
|
fields: null
|
||||||
|
- id: forgejo-admin-api-token
|
||||||
|
data_path: platform/data/workloads/forgejo/forgejo-admin
|
||||||
|
metadata_path: platform/metadata/workloads/forgejo/forgejo-admin
|
||||||
|
owner_repo: railiance-platform
|
||||||
|
fields: null
|
||||||
|
- id: issue-core-ingestion-api-key
|
||||||
|
data_path: platform/data/workloads/issue-core/issue-core/issue-core-runtime
|
||||||
|
metadata_path: platform/metadata/workloads/issue-core/issue-core/issue-core-runtime
|
||||||
|
owner_repo: railiance-platform
|
||||||
|
fields: [ISSUE_CORE_API_KEY, GITEA_BACKEND_TOKEN]
|
||||||
|
- id: openrouter-llm-connect
|
||||||
|
data_path: platform/data/workloads/activity-core/llm-connect/llm-connect-provider-secrets
|
||||||
|
metadata_path: platform/metadata/workloads/activity-core/llm-connect/llm-connect-provider-secrets
|
||||||
|
owner_repo: railiance-platform
|
||||||
|
fields: null
|
||||||
|
- id: railiance-backup-offsite-lane
|
||||||
|
data_path: platform/data/workloads/railiance/backup/offsite-lane
|
||||||
|
metadata_path: platform/metadata/workloads/railiance/backup/offsite-lane
|
||||||
|
owner_repo: railiance-platform
|
||||||
|
fields: null
|
||||||
|
- id: rapp-qonto-keycape-client
|
||||||
|
data_path: platform/data/workloads/rapp-qonto/keycape-client
|
||||||
|
metadata_path: platform/metadata/workloads/rapp-qonto/keycape-client
|
||||||
|
owner_repo: key-cape
|
||||||
|
fields: null
|
||||||
|
- id: reuse-surface-hub-write-token
|
||||||
|
data_path: platform/data/workloads/reuse/reuse-surface/runtime-secrets
|
||||||
|
metadata_path: platform/metadata/workloads/reuse/reuse-surface/runtime-secrets
|
||||||
|
owner_repo: railiance-platform
|
||||||
|
fields: [REUSE_SURFACE_TOKEN, REUSE_SURFACE_FORGEJO_WEBHOOK_SECRET]
|
||||||
|
- id: scaleway-bootstrap
|
||||||
|
data_path: platform/data/workloads/railiance/scaleway/bootstrap
|
||||||
|
metadata_path: platform/metadata/workloads/railiance/scaleway/bootstrap
|
||||||
|
owner_repo: railiance-platform
|
||||||
|
fields: null
|
||||||
|
- id: whynot-design-npm-publish
|
||||||
|
data_path: platform/data/workloads/coulomb/whynot-design/npm-publish
|
||||||
|
metadata_path: platform/metadata/workloads/coulomb/whynot-design/npm-publish
|
||||||
|
owner_repo: railiance-platform
|
||||||
|
fields: null
|
||||||
146
scripts/agent_high_risk_boundary.py
Normal file
146
scripts/agent_high_risk_boundary.py
Normal file
|
|
@ -0,0 +1,146 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Check the platform-owned agent deny policy against ops-warden's risk input."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
|
||||||
|
REPO_DIR = Path(__file__).resolve().parents[1]
|
||||||
|
DEFAULT_ARTIFACT = (
|
||||||
|
REPO_DIR / "openbao/policies/inputs/ops-warden-high-risk-data-paths.yaml"
|
||||||
|
)
|
||||||
|
DEFAULT_POLICY = REPO_DIR / "openbao/policies/agent-high-risk-boundary.hcl"
|
||||||
|
|
||||||
|
|
||||||
|
def load_artifact(path: Path) -> dict[str, Any]:
|
||||||
|
data = yaml.safe_load(path.read_text(encoding="utf-8"))
|
||||||
|
if not isinstance(data, dict):
|
||||||
|
raise ValueError(f"artifact must be a mapping: {path}")
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def parse_policy(text: str) -> dict[str, set[str]]:
|
||||||
|
paths: dict[str, set[str]] = {}
|
||||||
|
for match in re.finditer(
|
||||||
|
r'path\s+"([^"]+)"\s*\{[^}]*?capabilities\s*=\s*\[([^\]]*)\]',
|
||||||
|
text,
|
||||||
|
re.DOTALL,
|
||||||
|
):
|
||||||
|
paths[match.group(1)] = {
|
||||||
|
value.strip().strip('"\'')
|
||||||
|
for value in match.group(2).split(",")
|
||||||
|
if value.strip()
|
||||||
|
}
|
||||||
|
return paths
|
||||||
|
|
||||||
|
|
||||||
|
def check_boundary(
|
||||||
|
artifact: dict[str, Any], policy: dict[str, set[str]]
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
errors: list[str] = []
|
||||||
|
rows = artifact.get("paths")
|
||||||
|
no_concrete = artifact.get("no_concrete_path")
|
||||||
|
if not isinstance(rows, list):
|
||||||
|
rows = []
|
||||||
|
errors.append("artifact paths must be a list")
|
||||||
|
if not isinstance(no_concrete, list):
|
||||||
|
no_concrete = []
|
||||||
|
errors.append("artifact no_concrete_path must be a list")
|
||||||
|
if artifact.get("catalog_dirty") is not False:
|
||||||
|
errors.append("artifact catalog_dirty must be false")
|
||||||
|
if artifact.get("concrete_path_count") != len(rows):
|
||||||
|
errors.append("artifact concrete_path_count does not match paths")
|
||||||
|
if artifact.get("high_risk_lane_count") != len(rows) + len(no_concrete):
|
||||||
|
errors.append("artifact high_risk_lane_count does not match its entries")
|
||||||
|
|
||||||
|
seen_ids: set[str] = set()
|
||||||
|
unique_paths: set[str] = set()
|
||||||
|
for row in rows:
|
||||||
|
if not isinstance(row, dict):
|
||||||
|
errors.append("artifact path entry must be a mapping")
|
||||||
|
continue
|
||||||
|
lane_id = row.get("id")
|
||||||
|
data_path = row.get("data_path")
|
||||||
|
metadata_path = row.get("metadata_path")
|
||||||
|
if not isinstance(lane_id, str) or not lane_id:
|
||||||
|
errors.append("artifact path entry has no id")
|
||||||
|
continue
|
||||||
|
if lane_id in seen_ids:
|
||||||
|
errors.append(f"duplicate lane id: {lane_id}")
|
||||||
|
seen_ids.add(lane_id)
|
||||||
|
if not isinstance(data_path, str) or "/data/" not in data_path:
|
||||||
|
errors.append(f"{lane_id}: invalid data_path")
|
||||||
|
continue
|
||||||
|
expected_metadata = data_path.replace("/data/", "/metadata/", 1)
|
||||||
|
if metadata_path != expected_metadata:
|
||||||
|
errors.append(f"{lane_id}: metadata_path does not match data_path")
|
||||||
|
continue
|
||||||
|
unique_paths.add(data_path)
|
||||||
|
if "deny" not in policy.get(data_path, set()):
|
||||||
|
errors.append(f"{lane_id}: data path is not denied: {data_path}")
|
||||||
|
if "read" not in policy.get(metadata_path, set()):
|
||||||
|
errors.append(f"{lane_id}: metadata path is not readable: {metadata_path}")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"catalog_revision": artifact.get("catalog_revision"),
|
||||||
|
"high_risk_lanes": artifact.get("high_risk_lane_count"),
|
||||||
|
"concrete_entries": len(rows),
|
||||||
|
"unique_concrete_paths": len(unique_paths),
|
||||||
|
"no_concrete_paths": len(no_concrete),
|
||||||
|
"errors": errors,
|
||||||
|
"ok": not errors,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def comparable_artifact(data: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
return {key: value for key, value in data.items() if key != "generated_at"}
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
|
parser.add_argument("--artifact", type=Path, default=DEFAULT_ARTIFACT)
|
||||||
|
parser.add_argument("--policy", type=Path, default=DEFAULT_POLICY)
|
||||||
|
parser.add_argument(
|
||||||
|
"--upstream",
|
||||||
|
type=Path,
|
||||||
|
help="optionally require the vendored artifact to match this upstream copy",
|
||||||
|
)
|
||||||
|
parser.add_argument("--json", action="store_true")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
try:
|
||||||
|
artifact = load_artifact(args.artifact)
|
||||||
|
policy = parse_policy(args.policy.read_text(encoding="utf-8"))
|
||||||
|
report = check_boundary(artifact, policy)
|
||||||
|
if args.upstream:
|
||||||
|
upstream = load_artifact(args.upstream)
|
||||||
|
if comparable_artifact(artifact) != comparable_artifact(upstream):
|
||||||
|
report["errors"].append("vendored artifact differs from upstream")
|
||||||
|
report["ok"] = False
|
||||||
|
except (OSError, ValueError, yaml.YAMLError) as exc:
|
||||||
|
report = {"ok": False, "errors": [str(exc)]}
|
||||||
|
|
||||||
|
if args.json:
|
||||||
|
print(json.dumps(report, indent=2, sort_keys=True))
|
||||||
|
elif report["ok"]:
|
||||||
|
print(
|
||||||
|
"PASS: agent boundary covers "
|
||||||
|
f"{report['concrete_entries']} high-risk catalog entries at "
|
||||||
|
f"{report['catalog_revision']}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
for error in report["errors"]:
|
||||||
|
print(f"FAIL: {error}", file=sys.stderr)
|
||||||
|
return 0 if report["ok"] else 1
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
140
scripts/verify_coding_agent_approle.py
Normal file
140
scripts/verify_coding_agent_approle.py
Normal file
|
|
@ -0,0 +1,140 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Mint, verify, and revoke a coding-agent AppRole token without printing it."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import urllib.error
|
||||||
|
import urllib.request
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
|
DEFAULT_ADDR = "https://bao.coulomb.social"
|
||||||
|
DATA_PATH = "platform/data/workloads/issue-core/issue-core/issue-core-runtime"
|
||||||
|
METADATA_PATH = (
|
||||||
|
"platform/metadata/workloads/issue-core/issue-core/issue-core-runtime"
|
||||||
|
)
|
||||||
|
EXPECTED_POLICIES = {
|
||||||
|
"agent-high-risk-boundary",
|
||||||
|
"workload-kv-read-issue-core-runtime",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def bao_json(args: list[str]) -> dict[str, Any]:
|
||||||
|
completed = subprocess.run(
|
||||||
|
["bao", *args],
|
||||||
|
check=True,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=30,
|
||||||
|
)
|
||||||
|
return json.loads(completed.stdout)
|
||||||
|
|
||||||
|
|
||||||
|
def api_request(
|
||||||
|
addr: str,
|
||||||
|
method: str,
|
||||||
|
path: str,
|
||||||
|
*,
|
||||||
|
token: str | None = None,
|
||||||
|
payload: dict[str, Any] | None = None,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
headers = {"Content-Type": "application/json"}
|
||||||
|
if token:
|
||||||
|
headers["X-Vault-Token"] = token
|
||||||
|
body = json.dumps(payload).encode() if payload is not None else None
|
||||||
|
request = urllib.request.Request(
|
||||||
|
f"{addr.rstrip('/')}/v1/{path}", data=body, headers=headers, method=method
|
||||||
|
)
|
||||||
|
with urllib.request.urlopen(request, timeout=30) as response:
|
||||||
|
content = response.read()
|
||||||
|
return json.loads(content) if content else {}
|
||||||
|
|
||||||
|
|
||||||
|
def verify(role: str, addr: str) -> dict[str, Any]:
|
||||||
|
role_payload = bao_json(
|
||||||
|
["read", "-format=json", f"auth/approle/role/{role}/role-id"]
|
||||||
|
)
|
||||||
|
role_id = role_payload["data"]["role_id"]
|
||||||
|
secret_payload = bao_json(
|
||||||
|
["write", "-format=json", "-f", f"auth/approle/role/{role}/secret-id"]
|
||||||
|
)
|
||||||
|
secret_id = secret_payload["data"]["secret_id"]
|
||||||
|
|
||||||
|
agent_token: str | None = None
|
||||||
|
report: dict[str, Any] = {}
|
||||||
|
try:
|
||||||
|
login = api_request(
|
||||||
|
addr,
|
||||||
|
"POST",
|
||||||
|
"auth/approle/login",
|
||||||
|
payload={"role_id": role_id, "secret_id": secret_id},
|
||||||
|
)
|
||||||
|
agent_token = login["auth"]["client_token"]
|
||||||
|
lookup = api_request(addr, "GET", "auth/token/lookup-self", token=agent_token)
|
||||||
|
capabilities = api_request(
|
||||||
|
addr,
|
||||||
|
"POST",
|
||||||
|
"sys/capabilities-self",
|
||||||
|
token=agent_token,
|
||||||
|
payload={"paths": [DATA_PATH, METADATA_PATH]},
|
||||||
|
)
|
||||||
|
policies = set(lookup["data"].get("policies", []))
|
||||||
|
ttl = int(lookup["data"].get("ttl", 0))
|
||||||
|
data_caps = capabilities.get(DATA_PATH, [])
|
||||||
|
metadata_caps = capabilities.get(METADATA_PATH, [])
|
||||||
|
errors: list[str] = []
|
||||||
|
if policies != EXPECTED_POLICIES:
|
||||||
|
errors.append(f"unexpected policies: {sorted(policies)}")
|
||||||
|
if ttl <= 0 or ttl > 900:
|
||||||
|
errors.append(f"TTL outside 1..900 seconds: {ttl}")
|
||||||
|
if data_caps != ["deny"]:
|
||||||
|
errors.append(f"data capabilities are not deny: {data_caps}")
|
||||||
|
if "read" not in metadata_caps or "deny" in metadata_caps:
|
||||||
|
errors.append(f"metadata capabilities are not read-only: {metadata_caps}")
|
||||||
|
report = {
|
||||||
|
"role": role,
|
||||||
|
"policies": sorted(policies),
|
||||||
|
"ttl": ttl,
|
||||||
|
"data_capabilities": data_caps,
|
||||||
|
"metadata_capabilities": metadata_caps,
|
||||||
|
"deny_wins": data_caps == ["deny"],
|
||||||
|
"errors": errors,
|
||||||
|
"ok": not errors,
|
||||||
|
}
|
||||||
|
finally:
|
||||||
|
if agent_token:
|
||||||
|
try:
|
||||||
|
api_request(addr, "POST", "auth/token/revoke-self", token=agent_token)
|
||||||
|
except (OSError, urllib.error.HTTPError):
|
||||||
|
report.setdefault("errors", []).append("test token self-revocation failed")
|
||||||
|
report["ok"] = False
|
||||||
|
return report
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
|
parser.add_argument("--role", default="coding-agent-railiance-platform")
|
||||||
|
parser.add_argument("--addr", default=os.environ.get("BAO_ADDR", DEFAULT_ADDR))
|
||||||
|
args = parser.parse_args()
|
||||||
|
try:
|
||||||
|
report = verify(args.role, args.addr)
|
||||||
|
except (
|
||||||
|
KeyError,
|
||||||
|
json.JSONDecodeError,
|
||||||
|
OSError,
|
||||||
|
subprocess.CalledProcessError,
|
||||||
|
subprocess.TimeoutExpired,
|
||||||
|
urllib.error.HTTPError,
|
||||||
|
) as exc:
|
||||||
|
report = {"role": args.role, "ok": False, "errors": [str(exc)]}
|
||||||
|
print(json.dumps(report, indent=2, sort_keys=True))
|
||||||
|
return 0 if report["ok"] else 1
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import importlib.util
|
import importlib.util
|
||||||
|
import json
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
import sys
|
import sys
|
||||||
|
|
@ -17,6 +18,14 @@ assert SPEC.loader is not None
|
||||||
sys.modules[SPEC.name] = credential_change
|
sys.modules[SPEC.name] = credential_change
|
||||||
SPEC.loader.exec_module(credential_change)
|
SPEC.loader.exec_module(credential_change)
|
||||||
|
|
||||||
|
BOUNDARY_SPEC = importlib.util.spec_from_file_location(
|
||||||
|
"agent_high_risk_boundary", REPO_DIR / "scripts/agent_high_risk_boundary.py"
|
||||||
|
)
|
||||||
|
agent_high_risk_boundary = importlib.util.module_from_spec(BOUNDARY_SPEC)
|
||||||
|
assert BOUNDARY_SPEC.loader is not None
|
||||||
|
sys.modules[BOUNDARY_SPEC.name] = agent_high_risk_boundary
|
||||||
|
BOUNDARY_SPEC.loader.exec_module(agent_high_risk_boundary)
|
||||||
|
|
||||||
|
|
||||||
class CredentialChangeTests(unittest.TestCase):
|
class CredentialChangeTests(unittest.TestCase):
|
||||||
def setUp(self) -> None:
|
def setUp(self) -> None:
|
||||||
|
|
@ -155,29 +164,69 @@ class CredentialChangeTests(unittest.TestCase):
|
||||||
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:
|
def test_agent_boundary_denies_every_current_concrete_high_risk_catalog_path(self) -> None:
|
||||||
boundary = (
|
artifact = agent_high_risk_boundary.load_artifact(
|
||||||
REPO_DIR / "openbao/policies/agent-high-risk-boundary.hcl"
|
REPO_DIR
|
||||||
).read_text()
|
/ "openbao/policies/inputs/ops-warden-high-risk-data-paths.yaml"
|
||||||
data_paths = {
|
)
|
||||||
"platform/data/workloads/activity-core/llm-connect/llm-connect-provider-secrets",
|
boundary = agent_high_risk_boundary.parse_policy(
|
||||||
"platform/data/workloads/railiance/backup/offsite-lane",
|
(REPO_DIR / "openbao/policies/agent-high-risk-boundary.hcl").read_text()
|
||||||
"platform/data/workloads/forgejo/forgejo-admin",
|
)
|
||||||
"tenants/data/binky/company-email/imap",
|
report = agent_high_risk_boundary.check_boundary(artifact, boundary)
|
||||||
"tenants/data/binky/qonto-api",
|
self.assertEqual(report["errors"], [])
|
||||||
"platform/data/workloads/coulomb/whynot-design/npm-publish",
|
self.assertEqual(report["high_risk_lanes"], 19)
|
||||||
"platform/data/workloads/rapp-qonto/keycape-client",
|
self.assertEqual(report["concrete_entries"], 14)
|
||||||
"platform/data/workloads/agent-harness/forgejo-deploy-key",
|
self.assertEqual(report["unique_concrete_paths"], 13)
|
||||||
"platform/data/workloads/audit-core/senders",
|
self.assertEqual(report["no_concrete_paths"], 5)
|
||||||
"platform/data/workloads/email-connect/transactional",
|
self.assertEqual(boundary["auth/token/revoke-self"], {"update"})
|
||||||
"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"\]',
|
|
||||||
)
|
|
||||||
|
|
||||||
|
def test_coding_agent_jwt_role_is_exact_bound_and_deny_wins(self) -> None:
|
||||||
|
role = json.loads(
|
||||||
|
(REPO_DIR / "openbao/auth/coding-agent-jwt-role.json").read_text()
|
||||||
|
)
|
||||||
|
self.assertEqual(role["role_type"], "jwt")
|
||||||
|
self.assertEqual(role["bound_audiences"], ["codex-railiance-platform"])
|
||||||
|
self.assertEqual(role["user_claim"], "sub")
|
||||||
|
self.assertEqual(
|
||||||
|
role["bound_claims"],
|
||||||
|
{
|
||||||
|
"sub": "service:codex:railiance-platform",
|
||||||
|
"principal_type": "service",
|
||||||
|
"tenant": "tenant:coulomb",
|
||||||
|
"roles": "coding-agent",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
set(role["token_policies"]),
|
||||||
|
{
|
||||||
|
"agent-high-risk-boundary",
|
||||||
|
"workload-kv-read-issue-core-runtime",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self.assertTrue(role["token_no_default_policy"])
|
||||||
|
self.assertEqual(role["token_ttl"], "15m")
|
||||||
|
self.assertEqual(role["token_max_ttl"], "15m")
|
||||||
|
self.assertEqual(role["token_num_uses"], 8)
|
||||||
|
|
||||||
|
def test_coding_agent_approle_is_bounded_and_deny_wins(self) -> None:
|
||||||
|
role = json.loads(
|
||||||
|
(REPO_DIR / "openbao/auth/coding-agent-approle.json").read_text()
|
||||||
|
)
|
||||||
|
self.assertTrue(role["bind_secret_id"])
|
||||||
|
self.assertEqual(role["secret_id_ttl"], "5m")
|
||||||
|
self.assertEqual(role["secret_id_num_uses"], 1)
|
||||||
|
self.assertEqual(
|
||||||
|
set(role["token_policies"]),
|
||||||
|
{
|
||||||
|
"agent-high-risk-boundary",
|
||||||
|
"workload-kv-read-issue-core-runtime",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self.assertTrue(role["token_no_default_policy"])
|
||||||
|
self.assertEqual(role["token_ttl"], "15m")
|
||||||
|
self.assertEqual(role["token_max_ttl"], "15m")
|
||||||
|
self.assertEqual(role["token_num_uses"], 8)
|
||||||
|
|
||||||
|
def test_core_hub_database_lanes_are_split_and_exact_scope(self) -> None:
|
||||||
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()
|
||||||
|
|
|
||||||
|
|
@ -4,11 +4,11 @@ type: workplan
|
||||||
title: "Close agent high-risk OpenBao boundary coverage"
|
title: "Close agent high-risk OpenBao boundary coverage"
|
||||||
domain: financials
|
domain: financials
|
||||||
repo: railiance-platform
|
repo: railiance-platform
|
||||||
status: blocked
|
status: finished
|
||||||
owner: codex
|
owner: codex
|
||||||
topic_slug: railiance
|
topic_slug: railiance
|
||||||
created: "2026-08-21"
|
created: "2026-08-21"
|
||||||
updated: "2026-08-21"
|
updated: "2026-08-22"
|
||||||
related:
|
related:
|
||||||
- WARDEN-WP-0032
|
- WARDEN-WP-0032
|
||||||
- RISK-F-0009
|
- RISK-F-0009
|
||||||
|
|
@ -43,9 +43,11 @@ priority: high
|
||||||
state_hub_task_id: "44d1a4bf-70bb-4d50-a40a-2158acc96b36"
|
state_hub_task_id: "44d1a4bf-70bb-4d50-a40a-2158acc96b36"
|
||||||
```
|
```
|
||||||
|
|
||||||
Run the capabilities-only ops-warden audit against the policy. The 2026-08-21
|
Run the capabilities-only ops-warden audit against the policy. The initial
|
||||||
reconciliation found 17 high-risk lanes: six covered, six concrete uncovered,
|
2026-08-21 reconciliation found 17 high-risk lanes: six covered, six concrete
|
||||||
and five without a concrete KV address. No credential value was read.
|
uncovered, and five without a concrete KV address. The final generated input
|
||||||
|
contains 19 high-risk lanes, 14 concrete entries, and five without an address.
|
||||||
|
No credential value was read.
|
||||||
|
|
||||||
## T02 — Close the concrete deny gaps
|
## T02 — Close the concrete deny gaps
|
||||||
|
|
||||||
|
|
@ -61,9 +63,10 @@ rapp-qonto Keycape client, agent-harness Forgejo deploy key, audit-core senders,
|
||||||
email-connect transactional, and Scaleway bootstrap. Add regression coverage
|
email-connect transactional, and Scaleway bootstrap. Add regression coverage
|
||||||
for every concrete path currently emitted by the catalog audit.
|
for every concrete path currently emitted by the catalog audit.
|
||||||
|
|
||||||
Completed 2026-08-21. The source policy covers all 12 unique concrete paths
|
Completed 2026-08-22. After ops-warden corrected two multi-field grades, the
|
||||||
(including the Core Hub path, which has no catalog lane), and the local
|
source policy covers all 13 unique concrete catalog paths plus the additional
|
||||||
catalog audit reports all 12 catalog entries covered with none uncovered.
|
reviewed Core Hub path. The generated-input check reports all 14 concrete
|
||||||
|
catalog entries covered with none uncovered.
|
||||||
|
|
||||||
## T03 — Apply and verify live
|
## T03 — Apply and verify live
|
||||||
|
|
||||||
|
|
@ -79,13 +82,11 @@ and rerun the catalog audit with `policy_source: server`. Inspect policy/token
|
||||||
role metadata to determine whether an agent identity carries the boundary and
|
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.
|
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
|
Completed initially on 2026-08-21 and reconciled again on 2026-08-22 under
|
||||||
policy; normalized readback matched source, and the server-backed catalog audit
|
attended `platform-admin` OIDC. OpenBao accepted the policy; normalized
|
||||||
reported 17 high-risk lanes, 12 covered entries, zero uncovered, and five
|
readback matched source, and the final server-backed audit reported 19
|
||||||
pattern/non-KV lanes without a concrete address. Metadata-only inspection of
|
high-risk lanes, 14 covered entries, zero uncovered, and five pattern/non-KV
|
||||||
all discoverable netkingdom, Kubernetes, AppRole, and token roles found zero
|
lanes without a concrete address. No Secret value was read.
|
||||||
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
|
## T04 — Route the result
|
||||||
|
|
||||||
|
|
@ -105,11 +106,15 @@ Completed 2026-08-21 via State Hub message
|
||||||
attachment finding, the distinct-agent-identity blocker, and the requested
|
attachment finding, the distinct-agent-identity blocker, and the requested
|
||||||
versioned generated artifact shape.
|
versioned generated artifact shape.
|
||||||
|
|
||||||
|
Ops-warden delivered revision `55f0f47a021375b8b25c924953d1b49a24e002c5`
|
||||||
|
on 2026-08-21. The platform now vendors it as a checked policy input and has a
|
||||||
|
repeatable upstream-equality plus policy-coverage command.
|
||||||
|
|
||||||
## T05 — Establish a distinct coding-agent identity
|
## T05 — Establish a distinct coding-agent identity
|
||||||
|
|
||||||
```task
|
```task
|
||||||
id: RAILIANCE-WP-0022-T05
|
id: RAILIANCE-WP-0022-T05
|
||||||
status: wait
|
status: done
|
||||||
priority: high
|
priority: high
|
||||||
state_hub_task_id: "47aa5ed9-95c5-4a23-a460-e4bbd3ed6f65"
|
state_hub_task_id: "47aa5ed9-95c5-4a23-a460-e4bbd3ed6f65"
|
||||||
```
|
```
|
||||||
|
|
@ -119,12 +124,40 @@ The policy is live but no role attaches it. Do not add the boundary to
|
||||||
recovery work may require the protected values. The identity owner must define
|
recovery work may require the protected values. The identity owner must define
|
||||||
a distinct coding-agent issuance path, attach this boundary there, and prove
|
a distinct coding-agent issuance path, attach this boundary there, and prove
|
||||||
deny-wins behavior when combined with an otherwise readable workload policy.
|
deny-wins behavior when combined with an otherwise readable workload policy.
|
||||||
This is blocked on an identity-owner decision and is not invented here.
|
Completed 2026-08-22. State Hub decision
|
||||||
|
`f0955252-7b20-4c80-86e9-f8080ec60793` establishes the platform-owned AppRole
|
||||||
|
`coding-agent-railiance-platform` as the operational machine identity. Its
|
||||||
|
single-use SecretIDs expire after five minutes; its tokens last at most 15
|
||||||
|
minutes, have eight uses, omit the default policy, and attach both
|
||||||
|
`agent-high-risk-boundary` and the otherwise-readable issue-core workload
|
||||||
|
policy. A live ephemeral login proved data capabilities resolve to `deny` and
|
||||||
|
metadata to `read`; the token was revoked without reading the KV value.
|
||||||
|
|
||||||
|
The exact-bound KeyCape JWT role is also live as the issuer-backed migration
|
||||||
|
target. KeyCape principal registration remains an identity hardening follow-up,
|
||||||
|
not a blocker to the active AppRole boundary.
|
||||||
|
|
||||||
|
## T06 — Consume the generated catalog artifact
|
||||||
|
|
||||||
|
```task
|
||||||
|
id: RAILIANCE-WP-0022-T06
|
||||||
|
status: done
|
||||||
|
priority: high
|
||||||
|
```
|
||||||
|
|
||||||
|
Consume ops-warden's versioned `high-risk-data-paths.yaml` as a policy-check
|
||||||
|
input, add the two newly regraded issue-core and reuse-surface paths, and rerun
|
||||||
|
source plus live coverage. The input remains ops-warden's risk statement, not
|
||||||
|
the policy authority; railiance-platform continues to own every deny decision.
|
||||||
|
|
||||||
|
Completed 2026-08-22. The vendored input matches the clean upstream artifact,
|
||||||
|
the repository check covers all 14 entries, and the server-backed audit reports
|
||||||
|
zero uncovered. Evidence: `docs/evidence/agent-high-risk-boundary-2026-08-21.md`.
|
||||||
|
|
||||||
## Acceptance
|
## Acceptance
|
||||||
|
|
||||||
- [x] Every concrete high-risk catalog path is denied in the source policy.
|
- [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] The live policy matches source and the server-backed audit passes.
|
||||||
- [x] Agent boundary attachment is established from metadata (currently zero).
|
- [x] A distinct AppRole attachment is established from live metadata.
|
||||||
- [x] Result and generated-artifact follow-up are routed to ops-warden.
|
- [x] Result and generated-artifact follow-up are routed to ops-warden.
|
||||||
- [ ] A distinct coding-agent identity actually attaches the boundary.
|
- [x] A distinct coding-agent identity actually attaches the boundary.
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue