Prepare scoped factory audit custody and enforce receiver compatibility
Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a07ff8-19d0-7820-b4d0-1353833cb7fc
This commit is contained in:
parent
d3a502b45c
commit
bfe65a5b05
11 changed files with 1053 additions and 1 deletions
|
|
@ -0,0 +1,110 @@
|
|||
id: CCR-2026-0021
|
||||
kind: credential-change-request
|
||||
schema_version: 1
|
||||
request_type: workload-kv-read
|
||||
title: approval-engine load-bearing audit sender custody and delivery
|
||||
status: proposed
|
||||
created: '2026-09-11'
|
||||
updated: '2026-09-11'
|
||||
requester:
|
||||
agent: codex
|
||||
reason: AUDIT-WP-0009-T09 and HFACT-WP-0001-T03 require the existing exact sender declaration. This
|
||||
request supplies the missing platform custody and ESO delivery; receiver compatibility and service
|
||||
rollout remain explicit gates.
|
||||
review:
|
||||
required: true
|
||||
required_approvers:
|
||||
- platform-operator
|
||||
- audit-core-owner
|
||||
- approval-engine-owner
|
||||
comments: []
|
||||
target:
|
||||
domain: financials
|
||||
tenant: platform
|
||||
workload: approval-engine
|
||||
environment: production
|
||||
purpose: Provide only the approval-engine sender with source=approval-engine, tenants=[tenant:platform],
|
||||
may_write=true, may_read=false, evidence_kind=load-bearing, secret_policy=redact. Preserve the
|
||||
existing receiver registry and every other sender.
|
||||
openbao:
|
||||
mount: platform
|
||||
kv_path: platform/workloads/approval-engine/audit-sender
|
||||
fields:
|
||||
- AUDIT_TOKEN
|
||||
- CUSTODY_REQUEST
|
||||
policy_name: workload-kv-read-approval-engine-audit
|
||||
policy_file: openbao/policies/workload-kv-read-approval-engine-audit.hcl
|
||||
metadata_read: false
|
||||
auth:
|
||||
method: kubernetes
|
||||
mount: kubernetes
|
||||
role: external-secrets-approval-engine-audit
|
||||
bound_claims:
|
||||
service_account_names:
|
||||
- external-secrets
|
||||
service_account_namespaces:
|
||||
- external-secrets
|
||||
bound_claims_confirmed: true
|
||||
policies:
|
||||
- workload-kv-read-approval-engine-audit
|
||||
ttl: 15m
|
||||
access_frontdoor:
|
||||
type: external-secrets
|
||||
catalog_id: approval-engine-audit
|
||||
readiness: pending-review
|
||||
resolvable: false
|
||||
delivery:
|
||||
surface: external-secrets
|
||||
target: 'ClusterSecretStore openbao-approval-engine-audit restricted to namespace approval-engine;
|
||||
ExternalSecret and Secret approval-engine/approval-engine-audit, key audit-token. Source: manifests/factory-audit-senders.yaml.
|
||||
A missing namespace stays a workload-owner prerequisite; this packet creates none.'
|
||||
risk:
|
||||
classification: high
|
||||
notes:
|
||||
- A bearer permits append for its exact sender and tenant, never reading or changing stored evidence.
|
||||
- The registry update reads existing sender credentials inside the attended platform process only.
|
||||
No registry is delivered to a producer.
|
||||
- OpenBao login TTL limits the ESO reader session, not the audit bearer. Revocation must remove
|
||||
the sender token from the receiver registry and prove refusal.
|
||||
- New paths extend the existing coding-agent deny boundary. No existing high-risk grant is widened.
|
||||
verification:
|
||||
positive:
|
||||
- Independent CAS=0 token creation and exact sender merge with registry-version compare-and-set;
|
||||
repeated runs preserve both tokens and other registry fields.
|
||||
- ESO delivers only AUDIT_TOKEN into approval-engine/approval-engine-audit:audit-token; source and
|
||||
receiver copies agree without displaying values.
|
||||
- Compatible deployed receiver accepts and deduplicates a declared synthetic event for the exact
|
||||
sender and tenant, retaining load-bearing/redact declarations.
|
||||
negative:
|
||||
- Sibling sender path, full registry and parent listing are denied to each workload reader. Wrong
|
||||
service account/namespace and disallowed store namespace fail.
|
||||
- Receiver denies sibling source, wrong tenant, every read route and revoked token. No existing
|
||||
sender is removed or re-scoped.
|
||||
- Proposed or altered CCR, legacy receiver, malformed/duplicate registry identity, token collision,
|
||||
stale registry version or partial/conflicting custody refuses without overwriting.
|
||||
activation_conditions:
|
||||
- All three named owner reviews are approved before any native credential mutation.
|
||||
- A current image supporting evidence_kind is published/admitted by audit-core and deployed with
|
||||
its source scope and network policy; current c2fe39a image fails compatibility.
|
||||
- Use the Warden attended platform-admin login envelope; preserve independent receipt and revoke
|
||||
the session on exit.
|
||||
- Apply reviewed policy/auth metadata and ESO projection only after receiver compatibility and namespace
|
||||
readiness. An interrupted seed resumes from durable KV values, never blindly rotates or deletes.
|
||||
- Record native positive/negative evidence before declaring verified or active. Custody alone does
|
||||
not admit UI, human approval or factory execution.
|
||||
lifecycle:
|
||||
deactivate: Stop the exact producer; remove only its admitted token from the registry using CAS
|
||||
and reload/verify receiver refusal. Then detach its reader policy and remove its ExternalSecret/projection,
|
||||
retaining KV versions for investigation. Do not delete audit events or other sender entries.
|
||||
rotate: 'Reviewed overlap-first rotation: append a replacement to this sender only, deliver it,
|
||||
prove acceptance, then remove the predecessor and prove refusal. The first-provision helper refuses
|
||||
rotation and unexpected existing values.'
|
||||
compromised: Stop affected producer and revoke the exact receiver token first; inspect affected
|
||||
source/tenant events, rotate through a separate reviewed action, and preserve the independent
|
||||
audit trail.
|
||||
state_hub:
|
||||
workplan_id: RPF-WP-0035
|
||||
task_id: RPF-WP-0035-T08
|
||||
related_workplan: AUDIT-WP-0009-T09
|
||||
decision_id: 2c9fe9f0-034a-41d7-9d49-b99df488fdc8
|
||||
decision_api_url: http://127.0.0.1:8000/decisions/2c9fe9f0-034a-41d7-9d49-b99df488fdc8
|
||||
|
|
@ -0,0 +1,110 @@
|
|||
id: CCR-2026-0022
|
||||
kind: credential-change-request
|
||||
schema_version: 1
|
||||
request_type: workload-kv-read
|
||||
title: informed-decision load-bearing audit sender custody and delivery
|
||||
status: proposed
|
||||
created: '2026-09-11'
|
||||
updated: '2026-09-11'
|
||||
requester:
|
||||
agent: codex
|
||||
reason: AUDIT-WP-0009-T11 and HFACT-WP-0001-T03 require the existing exact sender declaration. This
|
||||
request supplies the missing platform custody and ESO delivery; receiver compatibility and service
|
||||
rollout remain explicit gates.
|
||||
review:
|
||||
required: true
|
||||
required_approvers:
|
||||
- platform-operator
|
||||
- audit-core-owner
|
||||
- informed-decision-owner
|
||||
comments: []
|
||||
target:
|
||||
domain: financials
|
||||
tenant: platform
|
||||
workload: informed-decision
|
||||
environment: production
|
||||
purpose: Provide only the informed-decision sender with source=informed-decision, tenants=[tenant:platform],
|
||||
may_write=true, may_read=false, evidence_kind=load-bearing, secret_policy=redact. Preserve the
|
||||
existing receiver registry and every other sender.
|
||||
openbao:
|
||||
mount: platform
|
||||
kv_path: platform/workloads/informed-decision/audit-sender
|
||||
fields:
|
||||
- AUDIT_TOKEN
|
||||
- CUSTODY_REQUEST
|
||||
policy_name: workload-kv-read-informed-decision-audit
|
||||
policy_file: openbao/policies/workload-kv-read-informed-decision-audit.hcl
|
||||
metadata_read: false
|
||||
auth:
|
||||
method: kubernetes
|
||||
mount: kubernetes
|
||||
role: external-secrets-informed-decision-audit
|
||||
bound_claims:
|
||||
service_account_names:
|
||||
- external-secrets
|
||||
service_account_namespaces:
|
||||
- external-secrets
|
||||
bound_claims_confirmed: true
|
||||
policies:
|
||||
- workload-kv-read-informed-decision-audit
|
||||
ttl: 15m
|
||||
access_frontdoor:
|
||||
type: external-secrets
|
||||
catalog_id: informed-decision-audit
|
||||
readiness: pending-review
|
||||
resolvable: false
|
||||
delivery:
|
||||
surface: external-secrets
|
||||
target: 'ClusterSecretStore openbao-informed-decision-audit restricted to namespace informed-decision;
|
||||
ExternalSecret and Secret informed-decision/informed-decision-audit, key token. Source: manifests/factory-audit-senders.yaml.
|
||||
A missing namespace stays a workload-owner prerequisite; this packet creates none.'
|
||||
risk:
|
||||
classification: high
|
||||
notes:
|
||||
- A bearer permits append for its exact sender and tenant, never reading or changing stored evidence.
|
||||
- The registry update reads existing sender credentials inside the attended platform process only.
|
||||
No registry is delivered to a producer.
|
||||
- OpenBao login TTL limits the ESO reader session, not the audit bearer. Revocation must remove
|
||||
the sender token from the receiver registry and prove refusal.
|
||||
- New paths extend the existing coding-agent deny boundary. No existing high-risk grant is widened.
|
||||
verification:
|
||||
positive:
|
||||
- Independent CAS=0 token creation and exact sender merge with registry-version compare-and-set;
|
||||
repeated runs preserve both tokens and other registry fields.
|
||||
- ESO delivers only AUDIT_TOKEN into informed-decision/informed-decision-audit:token; source and
|
||||
receiver copies agree without displaying values.
|
||||
- Compatible deployed receiver accepts and deduplicates a declared synthetic event for the exact
|
||||
sender and tenant, retaining load-bearing/redact declarations.
|
||||
negative:
|
||||
- Sibling sender path, full registry and parent listing are denied to each workload reader. Wrong
|
||||
service account/namespace and disallowed store namespace fail.
|
||||
- Receiver denies sibling source, wrong tenant, every read route and revoked token. No existing
|
||||
sender is removed or re-scoped.
|
||||
- Proposed or altered CCR, legacy receiver, malformed/duplicate registry identity, token collision,
|
||||
stale registry version or partial/conflicting custody refuses without overwriting.
|
||||
activation_conditions:
|
||||
- All three named owner reviews are approved before any native credential mutation.
|
||||
- A current image supporting evidence_kind is published/admitted by audit-core and deployed with
|
||||
its source scope and network policy; current c2fe39a image fails compatibility.
|
||||
- Use the Warden attended platform-admin login envelope; preserve independent receipt and revoke
|
||||
the session on exit.
|
||||
- Apply reviewed policy/auth metadata and ESO projection only after receiver compatibility and namespace
|
||||
readiness. An interrupted seed resumes from durable KV values, never blindly rotates or deletes.
|
||||
- Record native positive/negative evidence before declaring verified or active. Custody alone does
|
||||
not admit UI, human approval or factory execution.
|
||||
lifecycle:
|
||||
deactivate: Stop the exact producer; remove only its admitted token from the registry using CAS
|
||||
and reload/verify receiver refusal. Then detach its reader policy and remove its ExternalSecret/projection,
|
||||
retaining KV versions for investigation. Do not delete audit events or other sender entries.
|
||||
rotate: 'Reviewed overlap-first rotation: append a replacement to this sender only, deliver it,
|
||||
prove acceptance, then remove the predecessor and prove refusal. The first-provision helper refuses
|
||||
rotation and unexpected existing values.'
|
||||
compromised: Stop affected producer and revoke the exact receiver token first; inspect affected
|
||||
source/tenant events, rotate through a separate reviewed action, and preserve the independent
|
||||
audit trail.
|
||||
state_hub:
|
||||
workplan_id: RPF-WP-0035
|
||||
task_id: RPF-WP-0035-T08
|
||||
related_workplan: AUDIT-WP-0009-T11
|
||||
decision_id: ee4ff001-256a-4406-9cb8-51be2cd5d31b
|
||||
decision_api_url: http://127.0.0.1:8000/decisions/ee4ff001-256a-4406-9cb8-51be2cd5d31b
|
||||
121
docs/credential-lane-designs/factory-audit-senders-review.md
Normal file
121
docs/credential-lane-designs/factory-audit-senders-review.md
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
# Factory audit senders: concrete custody review
|
||||
|
||||
CCR-2026-0021 and CCR-2026-0022 are **proposed**. RPF-WP-0035-T08 owns the
|
||||
platform work; AUDIT-WP-0009-T09/T11 retain receiver admission. This packet
|
||||
requires the named platform operator, Audit Core owner and each producer owner
|
||||
to approve its request before native credential mutation.
|
||||
|
||||
## Scope to approve
|
||||
|
||||
| Request | Producer / exact source | OpenBao path | Producer Secret / key |
|
||||
| --- | --- | --- | --- |
|
||||
| CCR-2026-0021 | approval-engine | platform/workloads/approval-engine/audit-sender | approval-engine/approval-engine-audit / audit-token |
|
||||
| CCR-2026-0022 | informed-decision | platform/workloads/informed-decision/audit-sender | informed-decision/informed-decision-audit / token |
|
||||
|
||||
Each sender is limited to `tenant:platform`, write true, read false,
|
||||
`evidence_kind: load-bearing`, `secret_policy: redact`. The producer receives
|
||||
only its own `AUDIT_TOKEN`. `CUSTODY_REQUEST` is a non-secret provenance marker
|
||||
stored beside that token, enabling an interrupted first provision to identify
|
||||
its own version-1 value. It is not projected to the producer.
|
||||
|
||||
Each lane has its own exact-path policy, Kubernetes auth role and namespace-
|
||||
restricted ClusterSecretStore. Only `external-secrets/external-secrets` may
|
||||
log in to the reader role (15-minute session). Neither producer can read the
|
||||
full sender registry or its sibling's token. The coding-agent boundary gains
|
||||
exact denies on the two new data/metadata paths; existing rules are preserved.
|
||||
|
||||
The approved first-provision operation would generate two independent values
|
||||
in the attended platform process, store them with CAS=0, and append the exact
|
||||
identities to `platform/workloads/audit-core/senders` with compare-and-set on
|
||||
its observed KV version. That process necessarily reads the existing registry
|
||||
in memory; it never emits its credentials or gives it to a producer. No token
|
||||
passes through Git, a Hub record, command arguments or stdout/stderr.
|
||||
|
||||
## Blocking receiver finding
|
||||
|
||||
On 2026-09-11 the native receiver is 1/1 Ready on
|
||||
`forgejo.coulomb.social/coulomb/audit-core@sha256:c2fe39a0185b99be3fc0cb14d2de69772b8e66e20490097c9d11d90cc39719a6`.
|
||||
Its `SenderIdentity` has **no evidence_kind support**. A synthetic registration
|
||||
probe in the native pod fails the load-bearing contract; the same probe against
|
||||
current Audit Core source passes. Readiness did not detect this contract gap.
|
||||
|
||||
`factory_audit_custody.py receiver-check` now enforces that contract before any
|
||||
credential access. An approved CCR alone cannot bypass it. Audit Core must
|
||||
build, publish, admit and roll out its current implementation and source scope
|
||||
first. Its existing AUDIT-WP-0009-T09/T11 tasks retain that work; no completed
|
||||
source task is reopened. No receiver restart or deployment was done here.
|
||||
|
||||
Approval Engine's namespace is also absent. Three of four ESO objects passed
|
||||
server dry-run in their intended namespaces; its ExternalSecret was refused
|
||||
because that namespace does not exist. This packet creates no namespace,
|
||||
workload or network permission. Its owner must supply that prerequisite.
|
||||
|
||||
## Executable sequence
|
||||
|
||||
Review the source and exact policies:
|
||||
|
||||
```sh
|
||||
python3 scripts/credential-change.py render CCR-2026-0021
|
||||
python3 scripts/credential-change.py render CCR-2026-0022
|
||||
python3 scripts/factory_audit_custody.py plan
|
||||
```
|
||||
|
||||
After a compatible receiver release, the credential-free check is:
|
||||
|
||||
```sh
|
||||
python3 scripts/factory_audit_custody.py receiver-check \
|
||||
--kubeconfig /operator/railiance-kubeconfig \
|
||||
--expected-receiver-image forgejo.coulomb.social/coulomb/audit-core@sha256:ADMITTED_DIGEST \
|
||||
--receipt /operator/unique-receiver-receipt.json
|
||||
```
|
||||
|
||||
The seed command requires approved CCR files with all named owner reviews,
|
||||
the expected image, exact Railiance cluster UID, ready/current pod, both target
|
||||
namespaces and the Warden attended platform-admin login envelope. Route via
|
||||
`warden route show openbao-platform-admin-login --json` before arranging access.
|
||||
Use its installed attended wrapper; do not copy a token into the command.
|
||||
|
||||
Inside that envelope, invoke:
|
||||
|
||||
```sh
|
||||
python3 scripts/factory_audit_custody.py seed \
|
||||
--kubeconfig /operator/railiance-kubeconfig \
|
||||
--expected-receiver-image forgejo.coulomb.social/coulomb/audit-core@sha256:ADMITTED_DIGEST \
|
||||
--confirm 'PROVISION CCR-2026-0021 CCR-2026-0022' \
|
||||
--receipt /operator/unique-custody-receipt.json
|
||||
```
|
||||
|
||||
A successful seed deliberately reports
|
||||
`custody_seeded_pending_delivery_and_receiver_reload`. It applies the exact
|
||||
reader metadata and seeds custody, but does not apply ESO objects, reload the
|
||||
shared receiver or claim live ingestion. Preserve that receipt. After an
|
||||
interruption, review it and use `--resume` with a new receipt: only exact
|
||||
request-owned version-1 custody is reused. Changed tokens, source scopes,
|
||||
provenance, unknown duplicate identities and concurrent registry writes refuse.
|
||||
No automatic deletion or rotation is attempted on failure.
|
||||
|
||||
Under the same reviewed lane, the subsequent attended delivery uses
|
||||
`manifests/factory-audit-senders.yaml`; verify both stores/ExternalSecrets and
|
||||
exact token equality inside the protected process. Sync the existing registry
|
||||
ExternalSecret, verify its update, and reload Audit Core through its owner
|
||||
runbook. Then run each real producer's accepted/duplicate and wrong-source,
|
||||
wrong-tenant/read-refusal checks and retrieve evidence with the independent
|
||||
operator reader. The script does not yet automate these owner acceptance steps.
|
||||
Do not mark either CCR verified/active until those receipts exist.
|
||||
|
||||
## Revocation and limits
|
||||
|
||||
ESO login expiry does **not** expire an audit bearer. Stop the affected producer,
|
||||
remove only its token from the receiver registry using CAS, reload the receiver
|
||||
and prove refusal, then disable its reader/projection. Retain stored events and
|
||||
KV provenance. Rotation is separately reviewed and overlap-first. Existing
|
||||
senders and reader permissions remain intact.
|
||||
|
||||
`warden route show audit-core-senders` still points to the older Mason bootstrap.
|
||||
The receiver runbook records the later OpenBao migration; this packet uses that
|
||||
current authority and the platform's existing attended custody mechanics. It
|
||||
creates no second registry or generic credential broker.
|
||||
|
||||
Factory policy/caller admission, registered human binding, CCR-2026-0019's
|
||||
operator group, native runtime delivery and spending/operating acceptance stay
|
||||
with their existing records. Sender approval does not admit any of those acts.
|
||||
81
docs/evidence/2026-09-11-factory-audit-custody.json
Normal file
81
docs/evidence/2026-09-11-factory-audit-custody.json
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
{
|
||||
"schema": "platform.factory-audit-custody-preparation.v1",
|
||||
"observed_at": "2026-09-11T00:00:15.014528+00:00",
|
||||
"platform_base_commit": "d3a502b45cebc0a658d0ee7499f1cfcf2a5902a4",
|
||||
"owner_task": "RPF-WP-0035-T08",
|
||||
"source_records": [
|
||||
"CCR-2026-0021",
|
||||
"CCR-2026-0022",
|
||||
"AUDIT-WP-0009-T09",
|
||||
"AUDIT-WP-0009-T11"
|
||||
],
|
||||
"status": "proposed_custody_with_receiver_release_prerequisite",
|
||||
"verification": {
|
||||
"new_tests_passed": 17,
|
||||
"existing_credential_change_tests_passed": 53,
|
||||
"tests_skipped": 0,
|
||||
"local_openbao_test_cases": 11,
|
||||
"actual_receiver_source_contract_passed": true,
|
||||
"all_22_ccrs_valid": true,
|
||||
"server_dry_run": {
|
||||
"objects": 4,
|
||||
"exact_namespace_passed": 3,
|
||||
"refusal": "approval-engine namespace absent",
|
||||
"cluster_changes_applied": 0
|
||||
}
|
||||
},
|
||||
"native_receiver": {
|
||||
"image": "forgejo.coulomb.social/coulomb/audit-core@sha256:c2fe39a0185b99be3fc0cb14d2de69772b8e66e20490097c9d11d90cc39719a6",
|
||||
"ready_replicas": 1,
|
||||
"replicas": 1,
|
||||
"evidence_kind_supported": false,
|
||||
"probe": "synthetic SenderRegistry contract in exact deployed pod; no environment registry read",
|
||||
"entry_refusal": "receiver_lacks_sender_contract",
|
||||
"credential_reads": 0,
|
||||
"source_contract_commit": "5c0ad522fb36092aa7ec2e8d72f63a5a91853b5b"
|
||||
},
|
||||
"recovery_proven": [
|
||||
"interruption after first token write reuses the same value",
|
||||
"lost registry write reply reconciles without another write",
|
||||
"concurrent registry change is preserved by CAS refusal then deliberate resume",
|
||||
"wrong provenance/scope/duplicate names/token collisions refuse",
|
||||
"each exact read policy denies sibling data, full registry and metadata",
|
||||
"all named approvals and exact credential coordinates are enforced"
|
||||
],
|
||||
"routing_finding": {
|
||||
"catalog": "audit-core-senders",
|
||||
"status": "draft",
|
||||
"catalog_points_to": "ops-mason bootstrap",
|
||||
"current_authority": "platform/workloads/audit-core/senders per audit-core operator runbook",
|
||||
"execution_owner": "railiance-platform attended custody; existing platform helper pattern"
|
||||
},
|
||||
"pending_decisions": [
|
||||
{
|
||||
"ccr": "CCR-2026-0021",
|
||||
"status": 201,
|
||||
"decision_id": "2c9fe9f0-034a-41d7-9d49-b99df488fdc8"
|
||||
},
|
||||
{
|
||||
"ccr": "CCR-2026-0022",
|
||||
"status": 201,
|
||||
"decision_id": "ee4ff001-256a-4406-9cb8-51be2cd5d31b"
|
||||
}
|
||||
],
|
||||
"source_sha256": {
|
||||
"scripts/factory_audit_custody.py": "77b5bd7bd1b27a642d87d73d799b8896ebca1808b7c95fc47dd603ba74d8d30e",
|
||||
"tests/test_factory_audit_custody.py": "aef55cb98862dd62e2202fe4d2348aa8915f23d059331fd07d5644598452dc9f",
|
||||
"manifests/factory-audit-senders.yaml": "3d682dfd517b516dcb20c40373aae2557e4d8d2b49b3392d75c2cb36cd80785d",
|
||||
"openbao/policies/agent-high-risk-boundary.hcl": "9d863886c815740e65458417949c73a616b6e038594979b343ea07d08c585f64",
|
||||
"openbao/policies/workload-kv-read-approval-engine-audit.hcl": "a527c3bdfe7bf356fcd19f499ebc3ad75a71256a22c19882ad0dfdd09e0f0eb9",
|
||||
"openbao/policies/workload-kv-read-informed-decision-audit.hcl": "389e952546d32692e87b4f249752a9bee0fafa9fc58bf50ab8904110bf6b3780",
|
||||
"credential-change-requests/CCR-2026-0021-approval-engine-audit.yaml": "33f3bb5d32f2dbada864e1b89cdb47fcb9742dfe313eb391ca2f342be6bc119a",
|
||||
"credential-change-requests/CCR-2026-0022-informed-decision-audit.yaml": "7656519a7be847e6cf1b4e3e03200c68f5a36d87db8c81b84a16511b5dfe5d3a",
|
||||
"docs/credential-lane-designs/factory-audit-senders-review.md": "70e7a3568821672c84e0ce0388f7b8bc7d1b920354b38153995ec45022c53e52"
|
||||
},
|
||||
"native_credentials_created": 0,
|
||||
"native_secret_reads": 0,
|
||||
"native_policy_writes": 0,
|
||||
"native_deployments": 0,
|
||||
"factory_attempts": 0,
|
||||
"paid_model_calls": 0
|
||||
}
|
||||
94
manifests/factory-audit-senders.yaml
Normal file
94
manifests/factory-audit-senders.yaml
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
# CCR-2026-0021/0022. Review only; no credentials or namespaces.
|
||||
apiVersion: external-secrets.io/v1
|
||||
kind: ClusterSecretStore
|
||||
metadata:
|
||||
name: openbao-approval-engine-audit
|
||||
annotations:
|
||||
railiance.io/credential-change: CCR-2026-0021
|
||||
railiance.io/admission: proposed
|
||||
spec:
|
||||
provider:
|
||||
vault:
|
||||
server: http://openbao.openbao.svc:8200
|
||||
path: platform
|
||||
version: v2
|
||||
auth:
|
||||
kubernetes:
|
||||
mountPath: kubernetes
|
||||
role: external-secrets-approval-engine-audit
|
||||
serviceAccountRef:
|
||||
name: external-secrets
|
||||
namespace: external-secrets
|
||||
conditions:
|
||||
- namespaces:
|
||||
- approval-engine
|
||||
---
|
||||
apiVersion: external-secrets.io/v1
|
||||
kind: ExternalSecret
|
||||
metadata:
|
||||
name: approval-engine-audit
|
||||
namespace: approval-engine
|
||||
annotations:
|
||||
railiance.io/credential-change: CCR-2026-0021
|
||||
railiance.io/admission: proposed
|
||||
spec:
|
||||
refreshInterval: 5m
|
||||
secretStoreRef:
|
||||
kind: ClusterSecretStore
|
||||
name: openbao-approval-engine-audit
|
||||
target:
|
||||
name: approval-engine-audit
|
||||
creationPolicy: Owner
|
||||
deletionPolicy: Retain
|
||||
data:
|
||||
- secretKey: audit-token
|
||||
remoteRef:
|
||||
key: workloads/approval-engine/audit-sender
|
||||
property: AUDIT_TOKEN
|
||||
---
|
||||
apiVersion: external-secrets.io/v1
|
||||
kind: ClusterSecretStore
|
||||
metadata:
|
||||
name: openbao-informed-decision-audit
|
||||
annotations:
|
||||
railiance.io/credential-change: CCR-2026-0022
|
||||
railiance.io/admission: proposed
|
||||
spec:
|
||||
provider:
|
||||
vault:
|
||||
server: http://openbao.openbao.svc:8200
|
||||
path: platform
|
||||
version: v2
|
||||
auth:
|
||||
kubernetes:
|
||||
mountPath: kubernetes
|
||||
role: external-secrets-informed-decision-audit
|
||||
serviceAccountRef:
|
||||
name: external-secrets
|
||||
namespace: external-secrets
|
||||
conditions:
|
||||
- namespaces:
|
||||
- informed-decision
|
||||
---
|
||||
apiVersion: external-secrets.io/v1
|
||||
kind: ExternalSecret
|
||||
metadata:
|
||||
name: informed-decision-audit
|
||||
namespace: informed-decision
|
||||
annotations:
|
||||
railiance.io/credential-change: CCR-2026-0022
|
||||
railiance.io/admission: proposed
|
||||
spec:
|
||||
refreshInterval: 5m
|
||||
secretStoreRef:
|
||||
kind: ClusterSecretStore
|
||||
name: openbao-informed-decision-audit
|
||||
target:
|
||||
name: informed-decision-audit
|
||||
creationPolicy: Owner
|
||||
deletionPolicy: Retain
|
||||
data:
|
||||
- secretKey: token
|
||||
remoteRef:
|
||||
key: workloads/informed-decision/audit-sender
|
||||
property: AUDIT_TOKEN
|
||||
|
|
@ -129,3 +129,9 @@ path "platform/data/workloads/secrets-engine/approval-client" { capabilities = [
|
|||
path "platform/data/workloads/approval-engine/operator-client" { capabilities = ["deny"] }
|
||||
path "platform/metadata/workloads/secrets-engine/approval-client" { capabilities = ["deny"] }
|
||||
path "platform/metadata/workloads/approval-engine/operator-client" { capabilities = ["deny"] }
|
||||
|
||||
# Proposed factory audit sender custody: CCR-2026-0021 / CCR-2026-0022.
|
||||
path "platform/data/workloads/approval-engine/audit-sender" { capabilities = ["deny"] }
|
||||
path "platform/metadata/workloads/approval-engine/audit-sender" { capabilities = ["deny"] }
|
||||
path "platform/data/workloads/informed-decision/audit-sender" { capabilities = ["deny"] }
|
||||
path "platform/metadata/workloads/informed-decision/audit-sender" { capabilities = ["deny"] }
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
path "platform/data/workloads/approval-engine/audit-sender" {
|
||||
capabilities = ["read"]
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
path "platform/data/workloads/informed-decision/audit-sender" {
|
||||
capabilities = ["read"]
|
||||
}
|
||||
268
scripts/factory_audit_custody.py
Normal file
268
scripts/factory_audit_custody.py
Normal file
|
|
@ -0,0 +1,268 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Attended first-provision custody for the two factory audit senders.
|
||||
|
||||
Plan and receiver-check never read credential values. Seed needs the reviewed
|
||||
CCRs and a compatible receiver; it does not deploy or reload any workload.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import copy
|
||||
import hashlib
|
||||
import importlib.util
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import secrets
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from state_hub_preflight_lane import ROOT, LaneError, assert_cluster, bao, command, data
|
||||
from keycape_approval_custody import read_optional, role_matches
|
||||
|
||||
REGISTRY = 'platform/data/workloads/audit-core/senders'
|
||||
BOUNDARY = 'sys/policies/acl/agent-high-risk-boundary'
|
||||
LANES = (('CCR-2026-0021', 'approval-engine', 'audit-token'),
|
||||
('CCR-2026-0022', 'informed-decision', 'token'))
|
||||
CONFIRM = 'PROVISION CCR-2026-0021 CCR-2026-0022'
|
||||
# This probes synthetic identities only; no environment-backed registry is read.
|
||||
RECEIVER_PROBE = '''import json
|
||||
from audit_core.senders import SenderRegistry
|
||||
entry={"name":"compatibility-fixture","tokens":["fixture-only"],"sources":["compatibility-fixture"],"tenants":["tenant:platform"],"may_write":True,"may_read":False,"evidence_kind":"load-bearing","secret_policy":"redact"}
|
||||
r=SenderRegistry.from_env({"AUDIT_CORE_SENDERS":json.dumps([entry])})
|
||||
s=r.authenticate("Bearer fixture-only")
|
||||
print(json.dumps({"load_bearing":getattr(s,"evidence_kind",None)=="load-bearing","redact":s.secret_policy=="redact","write_only":s.may_write and not s.may_read,"source_exact":s.permits_source("compatibility-fixture") and not s.permits_source("sibling"),"tenant_exact":s.permits_tenant("tenant:platform") and not s.permits_tenant("tenant:sibling")}))
|
||||
'''
|
||||
|
||||
|
||||
def require(condition, code):
|
||||
if not condition:
|
||||
raise LaneError(code)
|
||||
|
||||
|
||||
def credential_module():
|
||||
spec = importlib.util.spec_from_file_location('factory_ccr', ROOT / 'scripts/credential-change.py')
|
||||
module = importlib.util.module_from_spec(spec); spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
def contracts(*, approved=False):
|
||||
module = credential_module()
|
||||
result = []
|
||||
for ref, name, secret_key in LANES:
|
||||
path = module.resolve_ccr(ref)
|
||||
ccr, errors, _ = module.validate_ccr(path)
|
||||
require(not errors, 'invalid_credential_request')
|
||||
if approved:
|
||||
require(ccr['status'] in {'approved', 'applied', 'verified', 'active'}, 'approved_ccrs_required')
|
||||
for owner in ('platform-operator', 'audit-core-owner', name + '-owner'):
|
||||
require(any(x.get('decision') == 'approved' and owner in x.get('reviewer', '')
|
||||
for x in ccr['review']['comments']), 'named_owner_reviews_required')
|
||||
suffix = name + '-audit'; policy = 'workload-kv-read-' + suffix
|
||||
expected_role = {'bound_service_account_names':['external-secrets'],
|
||||
'bound_service_account_namespaces':['external-secrets'],
|
||||
'policies':policy,'ttl':'15m'}
|
||||
expected_path = f'platform/workloads/{name}/audit-sender'
|
||||
require(ccr['openbao']['kv_path'] == expected_path
|
||||
and ccr['openbao']['fields'] == ['AUDIT_TOKEN', 'CUSTODY_REQUEST']
|
||||
and ccr['openbao']['policy_name'] == policy
|
||||
and ccr['openbao']['metadata_read'] is False
|
||||
and ccr['openbao']['auth']['role'] == 'external-secrets-' + suffix
|
||||
and ccr['openbao']['auth']['mount'] == 'kubernetes'
|
||||
and ccr['openbao']['auth']['bound_claims_confirmed'] is True
|
||||
and module.auth_payload(ccr) == expected_role, 'exact_contract_required')
|
||||
hcl = module.generated_policy_hcl(ccr)
|
||||
require((ROOT / ccr['openbao']['policy_file']).read_text() == hcl, 'policy_source_drift')
|
||||
result.append({'ccr':ref,'name':name,'kv':expected_path.replace('platform/','platform/data/',1),
|
||||
'metadata':expected_path.replace('platform/','platform/metadata/',1),
|
||||
'policy':policy,'hcl':hcl,'role':'external-secrets-'+suffix,
|
||||
'role_payload':expected_role,'store':'openbao-'+suffix,'secret':suffix,
|
||||
'secret_key':secret_key,'source_sha256':hashlib.sha256(path.read_bytes()).hexdigest()})
|
||||
return result
|
||||
|
||||
|
||||
def receiver_check(kube, expected_image):
|
||||
require(re.fullmatch(r'forgejo\.coulomb\.social/coulomb/audit-core@sha256:[0-9a-f]{64}', expected_image), 'receiver_digest_required')
|
||||
assert_cluster(kube)
|
||||
dep = data(command(kube + ['-n','audit-core','get','deployment','audit-core','-o','json']))
|
||||
require(dep['spec']['replicas'] == 1 and dep.get('status',{}).get('readyReplicas') == 1
|
||||
and dep['status'].get('observedGeneration',0) >= dep['metadata']['generation'], 'receiver_not_ready')
|
||||
containers = dep['spec']['template']['spec']['containers']
|
||||
require(len(containers) == 1 and containers[0]['image'] == expected_image, 'receiver_image_mismatch')
|
||||
pods = data(command(kube + ['-n','audit-core','get','pods','-l','app.kubernetes.io/name=audit-core','-o','json']))['items']
|
||||
require(len(pods) == 1 and not pods[0]['metadata'].get('deletionTimestamp'), 'receiver_rollout_in_progress')
|
||||
pod = pods[0]
|
||||
require(pod['spec']['containers'][0]['image'] == expected_image
|
||||
and all(x.get('ready') for x in pod.get('status',{}).get('containerStatuses',[]))
|
||||
and pod.get('status',{}).get('containerStatuses'), 'receiver_pod_not_current')
|
||||
proof = data(command(kube + ['-n','audit-core','exec',pod['metadata']['name'],'-c',containers[0]['name'],
|
||||
'--','python','-c',RECEIVER_PROBE]))
|
||||
require(set(proof) == {'load_bearing','redact','write_only','source_exact','tenant_exact'}
|
||||
and all(v is True for v in proof.values()), 'receiver_lacks_sender_contract')
|
||||
return {'image':expected_image,'deployment_uid':dep['metadata']['uid'],
|
||||
'deployment_resource_version':dep['metadata']['resourceVersion'],'pod_uid':pod['metadata']['uid'],
|
||||
'capabilities':proof,'synthetic_probe_only':True,'credential_reads':0}
|
||||
|
||||
|
||||
def snapshot():
|
||||
raw = data(bao(['read','-format=json',REGISTRY]))['data']
|
||||
require(type(raw['metadata']['version']) is int and raw['metadata']['version'] > 0, 'registry_version_missing')
|
||||
body = raw['data']; require(isinstance(body,dict) and isinstance(body.get('senders.json'),str), 'registry_shape_invalid')
|
||||
try:
|
||||
rows = json.loads(body['senders.json'])
|
||||
except (ValueError, TypeError):
|
||||
raise LaneError('registry_shape_invalid') from None
|
||||
require(isinstance(rows,list) and bool(rows), 'registry_must_preserve_existing_senders')
|
||||
names = set(); tokens = set()
|
||||
for row in rows:
|
||||
require(isinstance(row,dict) and isinstance(row.get('name'),str) and row['name']
|
||||
and row['name'] not in names, 'registry_identity_invalid_or_duplicate')
|
||||
names.add(row['name'])
|
||||
values = row.get('tokens') or ([row['token']] if row.get('token') else [])
|
||||
require(isinstance(values,list) and bool(values), 'registry_tokens_invalid')
|
||||
for value in values:
|
||||
require(isinstance(value,str) and bool(value) and value not in tokens, 'registry_token_collision')
|
||||
tokens.add(value)
|
||||
return raw['metadata']['version'], body, rows, tokens
|
||||
|
||||
|
||||
def desired(lane, token):
|
||||
return {'name':lane['name'],'tokens':[token],'sources':[lane['name']],
|
||||
'tenants':['tenant:platform'],'may_write':True,'may_read':False,
|
||||
'evidence_kind':'load-bearing','secret_policy':'redact'}
|
||||
|
||||
|
||||
def seed(lanes, receipt, save, *, resume=False):
|
||||
version, body, rows, used_tokens = snapshot()
|
||||
originals = copy.deepcopy(rows)
|
||||
found = {r['name']:r for r in rows}
|
||||
values = {}; pending = []
|
||||
# Validate every existing custody object before any write. Only this exact
|
||||
# request's version-1 values can be reused after interruption.
|
||||
for lane in lanes:
|
||||
old = read_optional(lane['metadata'])
|
||||
if old is None:
|
||||
require(lane['name'] not in found, 'registry_sender_without_custody')
|
||||
pending.append(lane)
|
||||
else:
|
||||
require(resume and old['current_version'] == 1
|
||||
and not old['versions']['1'].get('destroyed')
|
||||
and not old['versions']['1'].get('deletion_time'), 'existing_custody_requires_reviewed_resume')
|
||||
existing = data(bao(['read','-format=json',lane['kv']]))['data']
|
||||
require(existing['metadata']['version'] == 1
|
||||
and set(existing['data']) == {'AUDIT_TOKEN','CUSTODY_REQUEST'}
|
||||
and existing['data']['CUSTODY_REQUEST'] == lane['ccr'], 'custody_provenance_mismatch')
|
||||
token = existing['data']['AUDIT_TOKEN']
|
||||
require(isinstance(token,str) and re.fullmatch(r'[A-Za-z0-9_-]{64}',token), 'custody_token_shape')
|
||||
require(token not in values.values(), 'sender_token_collision')
|
||||
if lane['name'] in found:
|
||||
require(found[lane['name']] == desired(lane,token), 'registered_sender_drift')
|
||||
else:
|
||||
require(token not in used_tokens, 'sender_token_collision')
|
||||
values[lane['name']] = token
|
||||
receipt.update(registry_version_before=version,phase='custody_seed')
|
||||
save()
|
||||
for lane in pending:
|
||||
token = secrets.token_urlsafe(48)
|
||||
require(token not in used_tokens and token not in values.values(), 'sender_token_collision')
|
||||
result = data(bao(['write','-format=json',lane['kv'],'-'],payload={
|
||||
'options':{'cas':0},'data':{'AUDIT_TOKEN':token,'CUSTODY_REQUEST':lane['ccr']}}))
|
||||
require(result['data']['version'] == 1,'initial_custody_version_mismatch')
|
||||
values[lane['name']] = token
|
||||
receipt.setdefault('seeded',[]).append({'ccr':lane['ccr'],'version':1,'request_id':result.get('request_id')})
|
||||
save()
|
||||
updated = list(rows)
|
||||
for lane in lanes:
|
||||
if lane['name'] not in found:
|
||||
updated.append(desired(lane,values[lane['name']]))
|
||||
if updated != rows:
|
||||
receipt['phase']='registry_cas'; save()
|
||||
payload = dict(body, **{'senders.json':json.dumps(updated,separators=(',',':'))})
|
||||
result = data(bao(['write','-format=json',REGISTRY,'-'],payload={'options':{'cas':version},'data':payload}))
|
||||
receipt['registry_write_request_id']=result.get('request_id')
|
||||
# Readback also catches a concurrent removal rather than claiming a success.
|
||||
after_version, after_body, after_rows, _ = snapshot()
|
||||
require(all(x in after_rows for x in originals)
|
||||
and all(desired(lane,values[lane['name']]) in after_rows for lane in lanes)
|
||||
and all(after_body.get(k)==v for k,v in body.items() if k!='senders.json'), 'registry_readback_mismatch')
|
||||
receipt.update(registry_version_after=after_version,unchanged_other_senders=True,
|
||||
unchanged_other_registry_fields=True,credentials_reused=resume,
|
||||
status='custody_seeded_pending_delivery_and_receiver_reload')
|
||||
save()
|
||||
|
||||
|
||||
def prepare_metadata(lanes):
|
||||
# Avoid overwriting an unrelated policy or role. Do all drift checks first.
|
||||
for lane in lanes:
|
||||
policy=read_optional('sys/policies/acl/'+lane['policy'])
|
||||
role=read_optional('auth/kubernetes/role/'+lane['role'])
|
||||
require(policy is None or policy['policy']==lane['hcl'],'existing_policy_drift')
|
||||
require(role is None or role_matches(role,lane),'existing_role_drift')
|
||||
boundary=read_optional(BOUNDARY); require(boundary is not None,'coding_agent_boundary_required')
|
||||
current=boundary['policy']; additions=''
|
||||
for lane in lanes:
|
||||
for path in (lane['kv'],lane['metadata']):
|
||||
if '"'+path+'"' in current:
|
||||
require(re.search(r'path\s+"'+re.escape(path)+r'"\s*\{\s*capabilities\s*=\s*\["deny"\]\s*\}',current),'boundary_path_drift')
|
||||
else:
|
||||
additions+=f'path "{path}" {{ capabilities = ["deny"] }}\n'
|
||||
if additions:
|
||||
require(read_optional(BOUNDARY)['policy']==current,'boundary_revision_changed')
|
||||
bao(['write',BOUNDARY,'-'],payload={'policy':current+'\n'+additions})
|
||||
require(read_optional(BOUNDARY)['policy']==current+'\n'+additions,'boundary_readback_failed')
|
||||
for lane in lanes:
|
||||
bao(['write','sys/policies/acl/'+lane['policy'],'-'],payload={'policy':lane['hcl']})
|
||||
bao(['write','auth/kubernetes/role/'+lane['role'],'-'],payload=lane['role_payload'])
|
||||
require(read_optional('sys/policies/acl/'+lane['policy'])['policy']==lane['hcl']
|
||||
and role_matches(read_optional('auth/kubernetes/role/'+lane['role']),lane),'metadata_readback_failed')
|
||||
|
||||
|
||||
def main():
|
||||
parser=argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('action',choices=['plan','receiver-check','seed'])
|
||||
parser.add_argument('--kubeconfig'); parser.add_argument('--server')
|
||||
parser.add_argument('--expected-receiver-image'); parser.add_argument('--confirm')
|
||||
parser.add_argument('--receipt',type=Path); parser.add_argument('--resume',action='store_true')
|
||||
args=parser.parse_args()
|
||||
receipt={'schema':'platform.factory-audit-custody.v1','status':'refused','credential_values_emitted':False,
|
||||
'started_at':datetime.now(timezone.utc).isoformat()}
|
||||
fd=None
|
||||
try:
|
||||
lanes=contracts(approved=args.action=='seed')
|
||||
receipt['lanes']=[{k:v for k,v in x.items() if k in ('ccr','name','kv','store','secret','secret_key','source_sha256')} for x in lanes]
|
||||
if args.action=='plan':
|
||||
receipt['status']='proposed'; print(json.dumps(receipt,indent=2)); return 0
|
||||
require(args.kubeconfig and args.expected_receiver_image and args.receipt,'receiver_inputs_required')
|
||||
fd=os.open(args.receipt,os.O_RDWR|os.O_CREAT|os.O_EXCL|os.O_NOFOLLOW,0o600)
|
||||
def save():
|
||||
os.lseek(fd,0,os.SEEK_SET);os.ftruncate(fd,0)
|
||||
os.write(fd,(json.dumps(receipt,indent=2)+'\n').encode());os.fsync(fd)
|
||||
kube=['kubectl','--kubeconfig',args.kubeconfig,'--request-timeout=20s']
|
||||
if args.server:kube+=['--server',args.server]
|
||||
receipt['receiver']=receiver_check(kube,args.expected_receiver_image)
|
||||
if args.action=='receiver-check':
|
||||
receipt['status']='receiver_contract_supported';save();return 0
|
||||
require(args.confirm==CONFIRM,'exact_confirmation_required')
|
||||
require(Path.home().parent.name=='.warden-attended-login'
|
||||
and not os.environ.get('BAO_TOKEN') and not os.environ.get('VAULT_TOKEN'),'attended_warden_envelope_required')
|
||||
identity=data(bao(['token','lookup','-format=json']))['data']
|
||||
require('platform-admin' in identity['policies'] and 'root' not in identity['policies'],'attended_platform_admin_required')
|
||||
for lane in lanes:
|
||||
command(kube+['get','namespace',lane['name'],'-o','name'])
|
||||
# Refuse malformed registry before changing policy/auth metadata.
|
||||
snapshot()
|
||||
receipt['phase']='metadata';save();prepare_metadata(lanes)
|
||||
seed(lanes,receipt,save,resume=args.resume)
|
||||
return 0
|
||||
except Exception as exc:
|
||||
receipt['status']='refused'
|
||||
receipt['error']=str(exc) if isinstance(exc,LaneError) else 'contained_operation_failed'
|
||||
if fd is not None:
|
||||
save()
|
||||
print(json.dumps({'status':'refused','error':receipt['error']}))
|
||||
return 1
|
||||
finally:
|
||||
if fd is not None:os.close(fd)
|
||||
|
||||
|
||||
if __name__=='__main__':raise SystemExit(main())
|
||||
227
tests/test_factory_audit_custody.py
Normal file
227
tests/test_factory_audit_custody.py
Normal file
|
|
@ -0,0 +1,227 @@
|
|||
"""Disposable OpenBao and exact receiver compatibility tests; no native credentials."""
|
||||
import copy
|
||||
import importlib.util
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import secrets
|
||||
import shutil
|
||||
import socket
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
import yaml
|
||||
|
||||
ROOT=Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0,str(ROOT/'scripts'))
|
||||
import factory_audit_custody as lane
|
||||
|
||||
|
||||
class Contracts(unittest.TestCase):
|
||||
def test_proposed_requests_cannot_seed(self):
|
||||
self.assertEqual(len(lane.contracts()),2)
|
||||
with patch.object(lane,'bao',side_effect=AssertionError('must not contact Bao')):
|
||||
with self.assertRaisesRegex(lane.LaneError,'approved_ccrs_required'):
|
||||
lane.contracts(approved=True)
|
||||
|
||||
def test_each_named_owner_approval_is_required(self):
|
||||
module=lane.credential_module()
|
||||
original=module.validate_ccr
|
||||
for missing in ['platform-operator','audit-core-owner','approval-engine-owner','informed-decision-owner']:
|
||||
def candidate(path):
|
||||
c,errors,warnings=original(path);c=copy.deepcopy(c);c['status']='approved'
|
||||
owners=['platform-operator','audit-core-owner',c['target']['workload']+'-owner']
|
||||
c['review']['comments']=[{'reviewer':owner,'decision':'approved'} for owner in owners if owner!=missing]
|
||||
return c,errors,warnings
|
||||
with patch.object(module,'validate_ccr',side_effect=candidate),patch.object(lane,'credential_module',return_value=module):
|
||||
with self.assertRaisesRegex(lane.LaneError,'named_owner_reviews_required'):lane.contracts(approved=True)
|
||||
|
||||
def test_altered_credential_coordinates_refuse_even_with_approvals(self):
|
||||
module=lane.credential_module();original=module.validate_ccr
|
||||
def candidate(path):
|
||||
c,errors,warnings=original(path);c=copy.deepcopy(c);c['status']='approved'
|
||||
c['review']['comments']=[{'reviewer':owner,'decision':'approved'} for owner in ['platform-operator','audit-core-owner',c['target']['workload']+'-owner']]
|
||||
c['openbao']['kv_path']='platform/workloads/audit-core/senders'
|
||||
return c,errors,warnings
|
||||
with patch.object(module,'validate_ccr',side_effect=candidate),patch.object(lane,'credential_module',return_value=module):
|
||||
with self.assertRaisesRegex(lane.LaneError,'exact_contract_required'):lane.contracts(approved=True)
|
||||
|
||||
def test_projection_only_delivers_one_field_per_exact_namespace(self):
|
||||
objects=list(yaml.safe_load_all((ROOT/'manifests/factory-audit-senders.yaml').read_text()))
|
||||
self.assertEqual([x['kind'] for x in objects],['ClusterSecretStore','ExternalSecret']*2)
|
||||
for contract,store,es in zip(lane.contracts(),objects[::2],objects[1::2]):
|
||||
self.assertEqual(store['spec']['conditions'],[{'namespaces':[contract['name']]}])
|
||||
self.assertEqual(es['metadata']['namespace'],contract['name'])
|
||||
self.assertEqual(es['spec']['data'],[{'secretKey':contract['secret_key'],'remoteRef':{'key':contract['kv'].removeprefix('platform/data/'),'property':'AUDIT_TOKEN'}}])
|
||||
self.assertEqual(es['spec']['secretStoreRef']['name'],store['metadata']['name'])
|
||||
self.assertNotIn('senders.json',json.dumps(es))
|
||||
|
||||
def test_current_receiver_source_supports_exact_contract(self):
|
||||
source=os.environ.get('AUDIT_CORE_SOURCE')
|
||||
if not source:self.skipTest('set AUDIT_CORE_SOURCE for actual receiver contract')
|
||||
result=subprocess.run([sys.executable,'-c',lane.RECEIVER_PROBE],cwd=source,capture_output=True,text=True,check=True)
|
||||
self.assertTrue(all(json.loads(result.stdout).values()))
|
||||
|
||||
def test_legacy_receiver_is_refused_before_credentials(self):
|
||||
dep={'metadata':{'uid':'d','generation':1,'resourceVersion':'1'},'spec':{'replicas':1,'template':{'spec':{'containers':[{'name':'audit-core','image':'forgejo.coulomb.social/coulomb/audit-core@sha256:'+'a'*64}]}}},'status':{'observedGeneration':1,'readyReplicas':1}}
|
||||
pod={'metadata':{'name':'audit-core-test','uid':'p'},'spec':dep['spec']['template']['spec'],'status':{'containerStatuses':[{'ready':True}]}}
|
||||
responses=[dep,{'items':[pod]},{'load_bearing':False,'redact':True,'write_only':True,'source_exact':True,'tenant_exact':True}]
|
||||
with patch.object(lane,'assert_cluster'),patch.object(lane,'command',side_effect=[subprocess.CompletedProcess([],0,json.dumps(x).encode(),b'') for x in responses]),patch.object(lane,'bao',side_effect=AssertionError('no credentials')):
|
||||
with self.assertRaisesRegex(lane.LaneError,'receiver_lacks_sender_contract'):
|
||||
lane.receiver_check(['kubectl'],dep['spec']['template']['spec']['containers'][0]['image'])
|
||||
|
||||
|
||||
class OpenBaoExercise(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
if not shutil.which('bao'):raise unittest.SkipTest('local OpenBao binary required')
|
||||
with socket.socket() as s:s.bind(('127.0.0.1',0));port=s.getsockname()[1]
|
||||
token=secrets.token_urlsafe(32)
|
||||
env=dict(os.environ,BAO_ADDR=f'http://127.0.0.1:{port}',VAULT_ADDR=f'http://127.0.0.1:{port}',BAO_TOKEN=token,VAULT_TOKEN=token,BAO_DEV_ROOT_TOKEN_ID=token)
|
||||
cls.server=subprocess.Popen(['bao','server','-dev','-dev-no-store-token',f'-dev-listen-address=127.0.0.1:{port}'],env=env,stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL)
|
||||
cls.environment=patch.dict(os.environ,env,clear=True);cls.environment.start()
|
||||
try:
|
||||
for _ in range(60):
|
||||
if lane.bao(['status'],allow_failure=True).returncode==0:break
|
||||
time.sleep(.1)
|
||||
else:raise RuntimeError('local server not ready')
|
||||
lane.bao(['secrets','enable','-path=platform','kv-v2'])
|
||||
lane.bao(['auth','enable','kubernetes'])
|
||||
cls.lanes=lane.contracts()
|
||||
except BaseException:
|
||||
cls.environment.stop();cls.server.terminate();cls.server.wait(timeout=10);raise
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
cls.environment.stop();cls.server.terminate();cls.server.wait(timeout=10)
|
||||
|
||||
def setUp(self):
|
||||
# These deletes target only the test's own loopback dev server.
|
||||
for item in self.lanes:lane.bao(['delete',item['metadata']],allow_failure=True)
|
||||
lane.bao(['delete',lane.REGISTRY.replace('/data/','/metadata/')],allow_failure=True)
|
||||
self.original=[{'name':'existing','tokens':[secrets.token_urlsafe(48)],'sources':['existing'],'tenants':['*'],'may_read':True,'may_write':False,'secret_policy':'reject'}]
|
||||
self.body={'senders.json':json.dumps(self.original),'unrelated-metadata':'preserve me'}
|
||||
lane.bao(['write',lane.REGISTRY,'-'],payload={'options':{'cas':0},'data':self.body})
|
||||
self.receipt={};self.saved=[]
|
||||
|
||||
def run_seed(self,**kwargs):
|
||||
lane.seed(self.lanes,self.receipt,lambda:self.saved.append(copy.deepcopy(self.receipt)),**kwargs)
|
||||
|
||||
def token(self,item):
|
||||
return lane.data(lane.bao(['read','-format=json',item['kv']]))['data']['data']['AUDIT_TOKEN']
|
||||
|
||||
def test_first_provision_and_idempotent_resume(self):
|
||||
self.run_seed()
|
||||
before=[self.token(x) for x in self.lanes];self.assertNotEqual(*before)
|
||||
version,body,rows,_=lane.snapshot()
|
||||
self.assertEqual(rows[0],self.original[0]);self.assertEqual(body['unrelated-metadata'],'preserve me')
|
||||
self.assertEqual(len(rows),3)
|
||||
self.run_seed(resume=True)
|
||||
self.assertEqual(lane.snapshot()[0],version)
|
||||
self.assertEqual([self.token(x) for x in self.lanes],before)
|
||||
self.assertFalse(any(t in json.dumps(self.saved) for t in before))
|
||||
with self.assertRaisesRegex(lane.LaneError,'existing_custody_requires_reviewed_resume'):self.run_seed()
|
||||
|
||||
def test_crash_after_first_write_reuses_value(self):
|
||||
real=lane.bao
|
||||
def fail_second(args,**kwargs):
|
||||
if args[:2]==['write','-format=json'] and args[2]==self.lanes[1]['kv']:raise lane.LaneError('injected_interrupt')
|
||||
return real(args,**kwargs)
|
||||
with patch.object(lane,'bao',side_effect=fail_second):
|
||||
with self.assertRaisesRegex(lane.LaneError,'injected_interrupt'):self.run_seed()
|
||||
first=self.token(self.lanes[0]);self.assertEqual(lane.snapshot()[0],1)
|
||||
self.run_seed(resume=True)
|
||||
self.assertEqual(self.token(self.lanes[0]),first);self.assertEqual(len(lane.snapshot()[2]),3)
|
||||
|
||||
def test_lost_registry_write_reply_reconciles_without_rewriting(self):
|
||||
real=lane.bao
|
||||
def lost_reply(args,**kwargs):
|
||||
result=real(args,**kwargs)
|
||||
if args[:3]==['write','-format=json',lane.REGISTRY]:raise lane.LaneError('reply_lost')
|
||||
return result
|
||||
with patch.object(lane,'bao',side_effect=lost_reply):
|
||||
with self.assertRaisesRegex(lane.LaneError,'reply_lost'):self.run_seed()
|
||||
before=[self.token(x) for x in self.lanes];version=lane.snapshot()[0]
|
||||
self.run_seed(resume=True)
|
||||
self.assertEqual(lane.snapshot()[0],version);self.assertEqual([self.token(x) for x in self.lanes],before)
|
||||
|
||||
def test_cas_conflict_preserves_concurrent_writer(self):
|
||||
real=lane.bao
|
||||
extra={'name':'concurrent','tokens':[secrets.token_urlsafe(48)],'sources':['concurrent']}
|
||||
def race(args,**kwargs):
|
||||
if args[:3]==['write','-format=json',lane.REGISTRY]:
|
||||
body=dict(self.body,**{'senders.json':json.dumps(self.original+[extra])})
|
||||
real(['write',lane.REGISTRY,'-'],payload={'options':{'cas':1},'data':body})
|
||||
return real(args,**kwargs)
|
||||
with patch.object(lane,'bao',side_effect=race):
|
||||
with self.assertRaises(lane.LaneError):self.run_seed()
|
||||
self.assertEqual(lane.snapshot()[2],self.original+[extra])
|
||||
self.run_seed(resume=True);self.assertIn(extra,lane.snapshot()[2])
|
||||
|
||||
def test_wrong_provenance_refuses_resume(self):
|
||||
x=self.lanes[0]
|
||||
lane.bao(['write',x['kv'],'-'],payload={'options':{'cas':0},'data':{'AUDIT_TOKEN':secrets.token_urlsafe(48),'CUSTODY_REQUEST':'different-request'}})
|
||||
with self.assertRaisesRegex(lane.LaneError,'custody_provenance_mismatch'):self.run_seed(resume=True)
|
||||
self.assertIsNone(lane.read_optional(self.lanes[1]['metadata']))
|
||||
self.assertEqual(lane.snapshot()[0],1)
|
||||
|
||||
def test_scope_drift_is_never_overwritten(self):
|
||||
self.run_seed();version,body,rows,_=lane.snapshot();rows[-1]['may_read']=True
|
||||
lane.bao(['write',lane.REGISTRY,'-'],payload={'options':{'cas':version},'data':dict(body,**{'senders.json':json.dumps(rows)})})
|
||||
with self.assertRaisesRegex(lane.LaneError,'registered_sender_drift'):self.run_seed(resume=True)
|
||||
self.assertTrue(lane.snapshot()[2][-1]['may_read'])
|
||||
|
||||
def test_duplicate_names_refuse_before_token_creation(self):
|
||||
lane.bao(['write',lane.REGISTRY,'-'],payload={'options':{'cas':1},'data':{'senders.json':json.dumps(self.original*2)}})
|
||||
with self.assertRaisesRegex(lane.LaneError,'registry_identity_invalid_or_duplicate'):self.run_seed()
|
||||
self.assertTrue(all(lane.read_optional(x['metadata']) is None for x in self.lanes))
|
||||
|
||||
def test_existing_cross_sender_token_collision_refused(self):
|
||||
row=dict(self.original[0],name='different')
|
||||
lane.bao(['write',lane.REGISTRY,'-'],payload={'options':{'cas':1},'data':{'senders.json':json.dumps(self.original+[row])}})
|
||||
with self.assertRaisesRegex(lane.LaneError,'registry_token_collision'):self.run_seed()
|
||||
|
||||
def test_metadata_preserves_boundary_and_deny_overrides_reader(self):
|
||||
original='path "protected/existing" { capabilities = ["deny"] }\n'
|
||||
lane.bao(['write',lane.BOUNDARY,'-'],payload={'policy':original})
|
||||
lane.prepare_metadata(self.lanes)
|
||||
boundary=lane.read_optional(lane.BOUNDARY)['policy']
|
||||
self.assertTrue(boundary.startswith(original))
|
||||
for item in self.lanes:
|
||||
self.assertTrue(lane.role_matches(lane.read_optional('auth/kubernetes/role/'+item['role']),item))
|
||||
self.assertIn(item['kv'],boundary);self.assertIn(item['metadata'],boundary)
|
||||
self.run_seed()
|
||||
item=self.lanes[0]
|
||||
token=lane.data(lane.bao(['token','create','-format=json','-policy='+item['policy'],'-policy=agent-high-risk-boundary','-ttl=60s']))['auth']['client_token']
|
||||
try:
|
||||
result=lane.bao(['read',item['kv']],token=token,allow_failure=True)
|
||||
self.assertNotEqual(result.returncode,0);self.assertIn(b'403',result.stderr)
|
||||
finally:lane.bao(['write','auth/token/revoke','-'],payload={'token':token})
|
||||
lane.prepare_metadata(self.lanes)
|
||||
self.assertEqual(lane.read_optional(lane.BOUNDARY)['policy'],boundary)
|
||||
|
||||
def test_metadata_policy_drift_refuses_before_boundary_change(self):
|
||||
item=self.lanes[0];original=lane.read_optional(lane.BOUNDARY)
|
||||
lane.bao(['write','sys/policies/acl/'+item['policy'],'-'],payload={'policy':'path "unrelated/data" { capabilities = ["read"] }'})
|
||||
try:
|
||||
with self.assertRaisesRegex(lane.LaneError,'existing_policy_drift'):lane.prepare_metadata(self.lanes)
|
||||
self.assertEqual(lane.read_optional(lane.BOUNDARY),original)
|
||||
self.assertTrue(all(lane.read_optional(x['metadata']) is None for x in self.lanes))
|
||||
finally:lane.bao(['delete','sys/policies/acl/'+item['policy']])
|
||||
|
||||
def test_exact_read_policies_exclude_sibling_and_registry(self):
|
||||
self.run_seed()
|
||||
for item in self.lanes:
|
||||
lane.bao(['write','sys/policies/acl/'+item['policy'],'-'],payload={'policy':item['hcl']})
|
||||
token=lane.data(lane.bao(['token','create','-format=json','-policy='+item['policy'],'-ttl=60s']))['auth']['client_token']
|
||||
try:
|
||||
self.assertEqual(lane.bao(['read',item['kv']],token=token).returncode,0)
|
||||
targets=[lane.REGISTRY,item['metadata']]+[x['kv'] for x in self.lanes if x!=item]
|
||||
for target in targets:
|
||||
result=lane.bao(['read',target],token=token,allow_failure=True)
|
||||
self.assertNotEqual(result.returncode,0);self.assertIn(b'403',result.stderr)
|
||||
finally:lane.bao(['write','auth/token/revoke','-'],payload={'token':token})
|
||||
|
||||
if __name__=='__main__':unittest.main()
|
||||
|
|
@ -7,7 +7,7 @@ repo: railiance-platform
|
|||
status: blocked
|
||||
owner: codex
|
||||
created: "2026-09-05"
|
||||
updated: "2026-09-10"
|
||||
updated: "2026-09-11"
|
||||
related:
|
||||
- RPF-WP-0032
|
||||
- RPF-WP-0033
|
||||
|
|
@ -393,6 +393,35 @@ receipts to HFACT-WP-0001-T03, SECRETS-WP-0009-T03 and APPROVAL-WP-0002-T01/T05.
|
|||
Audit receiver/sender custody remains AUDIT-WP-0009-T09 and must not be folded
|
||||
into this client-identity grant.
|
||||
|
||||
## Admit the two factory audit-sender custody lanes
|
||||
|
||||
```task
|
||||
id: RPF-WP-0035-T08
|
||||
status: progress
|
||||
priority: high
|
||||
assignee: railiance-platform
|
||||
needs_human: true
|
||||
intervention_note: "CCR-2026-0021/0022 need named platform-operator, audit-core-owner and producer-owner reviews. Receiver upgrade and namespace readiness remain separate preconditions; approval alone does not admit factory execution."
|
||||
```
|
||||
|
||||
Consume AUDIT-WP-0009-T09/T11's exact declarations for approval-engine and
|
||||
informed-decision: tenant:platform, write-only, load-bearing, redact. Source
|
||||
preparation supplies two proposed CCRs, independent exact-path policies/ESO
|
||||
projections, and the attended `factory_audit_custody.py` first-provision helper.
|
||||
Its CAS and request-provenance checks preserve unrelated senders and recover
|
||||
interrupted writes without generating replacements. Native reviews and
|
||||
provision/delivery/receiver-ingestion acceptance remain open.
|
||||
|
||||
The 2026-09-11 native read-only check found an additional deployment dependency:
|
||||
receiver image c2fe39a is 1/1 Ready but does not support evidence_kind. The
|
||||
synthetic native probe now refuses before any credential read. Current receiver
|
||||
source passes. AUDIT T09/T11 retain compatible release/source-manifest rollout;
|
||||
Approval Engine's missing namespace remains its owner prerequisite.
|
||||
|
||||
[Review packet](../docs/credential-lane-designs/factory-audit-senders-review.md).
|
||||
[Preparation evidence](../docs/evidence/2026-09-11-factory-audit-custody.json).
|
||||
Do not fold these senders into CCR-2026-0019 or the cancelled CCR-2026-0020.
|
||||
|
||||
## Dependency review — 2026-09-06
|
||||
|
||||
### 2026-09-09 tenant question resolved — no change needed
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue