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/data/workloads/approval-engine/operator-client" { capabilities = ["deny"] }
|
||||||
path "platform/metadata/workloads/secrets-engine/approval-client" { capabilities = ["deny"] }
|
path "platform/metadata/workloads/secrets-engine/approval-client" { capabilities = ["deny"] }
|
||||||
path "platform/metadata/workloads/approval-engine/operator-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
|
status: blocked
|
||||||
owner: codex
|
owner: codex
|
||||||
created: "2026-09-05"
|
created: "2026-09-05"
|
||||||
updated: "2026-09-10"
|
updated: "2026-09-11"
|
||||||
related:
|
related:
|
||||||
- RPF-WP-0032
|
- RPF-WP-0032
|
||||||
- RPF-WP-0033
|
- 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
|
Audit receiver/sender custody remains AUDIT-WP-0009-T09 and must not be folded
|
||||||
into this client-identity grant.
|
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
|
## Dependency review — 2026-09-06
|
||||||
|
|
||||||
### 2026-09-09 tenant question resolved — no change needed
|
### 2026-09-09 tenant question resolved — no change needed
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue