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:
codex 2026-09-11 02:01:57 +02:00
parent d3a502b45c
commit bfe65a5b05
11 changed files with 1053 additions and 1 deletions

View file

@ -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

View file

@ -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

View 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.

View 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
}

View 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

View file

@ -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"] }

View file

@ -0,0 +1,3 @@
path "platform/data/workloads/approval-engine/audit-sender" {
capabilities = ["read"]
}

View file

@ -0,0 +1,3 @@
path "platform/data/workloads/informed-decision/audit-sender" {
capabilities = ["read"]
}

View 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())

View 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()

View file

@ -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