feat: deliver approved factory audit sender custody
Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a07ff8-19d0-7820-b4d0-1353833cb7fc
This commit is contained in:
parent
fe1b1c5fe2
commit
44314cad37
7 changed files with 411 additions and 8 deletions
|
|
@ -3,7 +3,7 @@ kind: credential-change-request
|
|||
schema_version: 1
|
||||
request_type: workload-kv-read
|
||||
title: approval-engine load-bearing audit sender custody and delivery
|
||||
status: approved
|
||||
status: applied
|
||||
created: '2026-09-11'
|
||||
updated: '2026-09-11'
|
||||
requester:
|
||||
|
|
@ -107,6 +107,19 @@ verification:
|
|||
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.
|
||||
evidence:
|
||||
- at: '2026-09-11T07:32:51+00:00'
|
||||
actor: codex via attended user platform-admin
|
||||
kind: factory_audit_initial_custody
|
||||
result: passed
|
||||
details:
|
||||
- Both independently generated sender credentials persisted at version 1 with
|
||||
exact request provenance. Shared registry CAS advanced from version 7 to 8;
|
||||
existing senders and registry fields preserved.
|
||||
- 'Attended retry completed with exit 0 and confirmed self-revocation. Receipt:
|
||||
docs/evidence/2026-09-11-factory-audit-sender-seed.json.'
|
||||
- 'Status applied: native ESO delivery, scope verification, receiver reload and
|
||||
producer acceptance remain.'
|
||||
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
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ kind: credential-change-request
|
|||
schema_version: 1
|
||||
request_type: workload-kv-read
|
||||
title: informed-decision load-bearing audit sender custody and delivery
|
||||
status: approved
|
||||
status: applied
|
||||
created: '2026-09-11'
|
||||
updated: '2026-09-11'
|
||||
requester:
|
||||
|
|
@ -108,6 +108,19 @@ verification:
|
|||
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.
|
||||
evidence:
|
||||
- at: '2026-09-11T07:32:51+00:00'
|
||||
actor: codex via attended user platform-admin
|
||||
kind: factory_audit_initial_custody
|
||||
result: passed
|
||||
details:
|
||||
- Both independently generated sender credentials persisted at version 1 with
|
||||
exact request provenance. Shared registry CAS advanced from version 7 to 8;
|
||||
existing senders and registry fields preserved.
|
||||
- 'Attended retry completed with exit 0 and confirmed self-revocation. Receipt:
|
||||
docs/evidence/2026-09-11-factory-audit-sender-seed.json.'
|
||||
- 'Status applied: native ESO delivery, scope verification, receiver reload and
|
||||
producer acceptance remain.'
|
||||
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
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# Factory audit senders: concrete custody review
|
||||
|
||||
CCR-2026-0021 and CCR-2026-0022 are **approved** by the user on 2026-09-11,
|
||||
CCR-2026-0021 and CCR-2026-0022 were **approved** by the user on 2026-09-11,
|
||||
replying "good, go on" to the explicit question naming the platform operator,
|
||||
Audit Core owner, and each producer owner. The source review comments and the
|
||||
two existing State Hub decisions record that approval. RPF-WP-0035-T08 owns
|
||||
|
|
@ -33,7 +33,7 @@ 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
|
||||
## Historical receiver finding (resolved before custody)
|
||||
|
||||
On 2026-09-11 the native receiver is 1/1 Ready on
|
||||
`forgejo.coulomb.social/coulomb/audit-core@sha256:c2fe39a0185b99be3fc0cb14d2de69772b8e66e20490097c9d11d90cc39719a6`.
|
||||
|
|
@ -52,6 +52,15 @@ 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.
|
||||
|
||||
## Current execution status
|
||||
|
||||
The compatible receiver c82e0442de0f is live and both namespaces exist. The
|
||||
attended retry seeded both independent version-1 credentials and advanced the
|
||||
registry with CAS 7→8, preserving existing senders and fields. Both CCRs are
|
||||
**applied**; Warden exited 0 and confirmed revocation for that session. See the
|
||||
[seed receipt](../evidence/2026-09-11-factory-audit-sender-seed.json). Delivery,
|
||||
receiver reload and producer acceptance remain distinct checks.
|
||||
|
||||
## Executable sequence
|
||||
|
||||
Review the source and exact policies:
|
||||
|
|
@ -102,9 +111,41 @@ 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.
|
||||
operator reader. The delivery helper automates delivery, scope checks and receiver reload; it
|
||||
does not deploy producers or automate their outbox acceptance.
|
||||
Do not mark either CCR verified/active until those receipts exist.
|
||||
|
||||
## Attended delivery helper
|
||||
|
||||
After the seed receipt exists, run this silent command inside a fresh Warden
|
||||
attended platform-admin envelope (do not rerun seed or reuse a revoked session):
|
||||
|
||||
```sh
|
||||
python3 scripts/factory_audit_delivery.py \
|
||||
--kubeconfig /operator/railiance-kubeconfig \
|
||||
--server https://127.0.0.1:16444 \
|
||||
--expected-receiver-image forgejo.coulomb.social/coulomb/audit-core@sha256:c82e0442de0fd181342916ae9cd5d6de41d859e1efda637bd93936c67873afa5 \
|
||||
--confirm 'DELIVER CCR-2026-0021 CCR-2026-0022' \
|
||||
--receipt /operator/unique-delivery-receipt.json
|
||||
```
|
||||
|
||||
The helper pins the reviewed manifest bytes and native cluster/image. It
|
||||
verifies exact policy, role, version-1 custody and provenance; independently
|
||||
logs in as each native ESO reader; proves sibling, full-registry, metadata,
|
||||
listing, wrong-SA and coding-agent-boundary denials; and revokes temporary
|
||||
reader tokens. It checks exact projected token equality and ESO ownership in
|
||||
captured memory, forces receiver-registry reconciliation and compares the
|
||||
complete JSON before restarting the receiver. A uniquely owned temporary
|
||||
namespace proves wrong-namespace login and explicit ESO namespace restriction
|
||||
with no Secret created. Cleanup uses the namespace UID precondition.
|
||||
|
||||
The metadata-only receipt advances by phase. Its successful status is
|
||||
`delivered_and_scope_verified_pending_producer_acceptance`. Failures preserve
|
||||
completed phases for diagnosis; they do not rotate or delete permanent custody.
|
||||
Receiver accepted/duplicate and negative HTTP checks, real producer outbox
|
||||
recovery and bearer revocation remain owner acceptance work. Do not mistake
|
||||
short-lived ESO-reader revocation for receiver bearer revocation.
|
||||
|
||||
## Revocation and limits
|
||||
|
||||
ESO login expiry does **not** expire an audit bearer. Stop the affected producer,
|
||||
|
|
|
|||
63
docs/evidence/2026-09-11-factory-audit-sender-seed.json
Normal file
63
docs/evidence/2026-09-11-factory-audit-sender-seed.json
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
{
|
||||
"schema": "platform.factory-audit-custody.v1",
|
||||
"status": "custody_seeded_pending_delivery_and_receiver_reload",
|
||||
"credential_values_emitted": false,
|
||||
"started_at": "2026-09-11T05:37:49.432171+00:00",
|
||||
"lanes": [
|
||||
{
|
||||
"ccr": "CCR-2026-0021",
|
||||
"name": "approval-engine",
|
||||
"kv": "platform/data/workloads/approval-engine/audit-sender",
|
||||
"store": "openbao-approval-engine-audit",
|
||||
"secret": "approval-engine-audit",
|
||||
"secret_key": "audit-token",
|
||||
"source_sha256": "ba69ecc8227c5bf887c2cd0ac4c50518c019bebfdfb6d49628edee1ce570da7d"
|
||||
},
|
||||
{
|
||||
"ccr": "CCR-2026-0022",
|
||||
"name": "informed-decision",
|
||||
"kv": "platform/data/workloads/informed-decision/audit-sender",
|
||||
"store": "openbao-informed-decision-audit",
|
||||
"secret": "informed-decision-audit",
|
||||
"secret_key": "token",
|
||||
"source_sha256": "2f7f7f214d1e21fd79275197edd03ee7e154ef451d62a182017926250515a6aa"
|
||||
}
|
||||
],
|
||||
"receiver": {
|
||||
"image": "forgejo.coulomb.social/coulomb/audit-core@sha256:c82e0442de0fd181342916ae9cd5d6de41d859e1efda637bd93936c67873afa5",
|
||||
"deployment_uid": "b85fe3d0-75c9-4e0d-8c34-c6f1df0881bb",
|
||||
"deployment_resource_version": "59751057",
|
||||
"pod_uid": "6642c414-a942-4b11-b048-267423e12da8",
|
||||
"capabilities": {
|
||||
"load_bearing": true,
|
||||
"redact": true,
|
||||
"write_only": true,
|
||||
"source_exact": true,
|
||||
"tenant_exact": true
|
||||
},
|
||||
"synthetic_probe_only": true,
|
||||
"credential_reads": 0
|
||||
},
|
||||
"phase": "registry_cas",
|
||||
"registry_version_before": 7,
|
||||
"seeded": [
|
||||
{
|
||||
"ccr": "CCR-2026-0021",
|
||||
"version": 1,
|
||||
"request_id": "cf6dd167-9d37-fbcf-cc3d-6d54c0f988b9"
|
||||
},
|
||||
{
|
||||
"ccr": "CCR-2026-0022",
|
||||
"version": 1,
|
||||
"request_id": "2f373e63-058b-7a6e-07c4-365317dffa73"
|
||||
}
|
||||
],
|
||||
"registry_write_request_id": "91fdb393-80ba-4e06-2dde-3982763d0ae5",
|
||||
"registry_version_after": 8,
|
||||
"unchanged_other_senders": true,
|
||||
"unchanged_other_registry_fields": true,
|
||||
"credentials_reused": false,
|
||||
"attended_wrapper_exit": 0,
|
||||
"attended_session_self_revocation": "confirmed_by_successful_wrapper",
|
||||
"prior_failed_login": "not asserted resolved by this new session"
|
||||
}
|
||||
192
scripts/factory_audit_delivery.py
Normal file
192
scripts/factory_audit_delivery.py
Normal file
|
|
@ -0,0 +1,192 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Attended delivery and native scope checks for the approved factory audit senders.
|
||||
|
||||
No token values leave captured subprocess memory. The receipt is metadata only.
|
||||
This does not deploy producers or claim their outbox/recovery acceptance.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import argparse
|
||||
import base64
|
||||
from datetime import datetime, timezone
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import time
|
||||
import yaml
|
||||
|
||||
from factory_audit_custody import contracts, desired, receiver_check, snapshot, require, REGISTRY
|
||||
from keycape_approval_custody import require_denied, read_optional, role_matches
|
||||
from state_hub_preflight_lane import ROOT, LaneError, command, bao, data, revoke
|
||||
|
||||
CONFIRM='DELIVER CCR-2026-0021 CCR-2026-0022'
|
||||
MANIFEST_SHA256='bca0f6b318baf6bccebf7faec48dd5a69d686cfc7ba9f4c769c7a60b9dc72cc9'
|
||||
|
||||
|
||||
def custody_value(lane, raw):
|
||||
require(raw['metadata']['version']==1 and set(raw['data'])=={'AUDIT_TOKEN','CUSTODY_REQUEST'}
|
||||
and raw['data']['CUSTODY_REQUEST']==lane['ccr'], 'custody_provenance_mismatch')
|
||||
token=raw['data']['AUDIT_TOKEN']
|
||||
import re
|
||||
require(isinstance(token,str) and re.fullmatch(r'[A-Za-z0-9_-]{64}',token), 'custody_token_shape')
|
||||
return token
|
||||
|
||||
|
||||
def verify_readers(kube, lanes, receipt, save):
|
||||
values={}
|
||||
for lane,row in zip(lanes,receipt['lanes']):
|
||||
receipt['check']=lane['name']+':reader_metadata';save()
|
||||
require(read_optional('sys/policies/acl/'+lane['policy'])['policy']==lane['hcl']
|
||||
and role_matches(read_optional('auth/kubernetes/role/'+lane['role']),lane),'native_metadata_drift')
|
||||
receipt['check']=lane['name']+':reader_login';save()
|
||||
jwt=command(kube+['-n','external-secrets','create','token','external-secrets','--duration=10m']).stdout.decode().strip()
|
||||
auth=data(bao(['write','-format=json','auth/kubernetes/login','-'],payload={'role':lane['role'],'jwt':jwt}))['auth']
|
||||
token=auth['client_token']
|
||||
try:
|
||||
require(set(auth['token_policies'])=={lane['policy'],'default'} and 0<auth['lease_duration']<=900,'reader_scope_or_ttl_mismatch')
|
||||
raw=data(bao(['read','-format=json',lane['kv']],token=token))['data']
|
||||
values[lane['name']]=custody_value(lane,raw)
|
||||
for path in [REGISTRY,lane['metadata'],next(x['kv'] for x in lanes if x!=lane)]:
|
||||
require_denied(bao(['read','-format=json',path],token=token,allow_failure=True))
|
||||
require_denied(bao(['list','-format=json','platform/metadata/workloads'],token=token,allow_failure=True))
|
||||
row.update(reader_exact_path=True,registry_and_sibling_denied=True,metadata_and_listing_denied=True,reader_ttl=auth['lease_duration'])
|
||||
finally: revoke(token)
|
||||
require_denied(bao(['token','lookup','-format=json'],token=token,allow_failure=True))
|
||||
row['reader_revocation_verified']=True
|
||||
receipt['check']=lane['name']+':wrong_service_account';save()
|
||||
bad_jwt=command(kube+['-n','external-secrets','create','token','default','--duration=10m']).stdout.decode().strip()
|
||||
bad=bao(['write','-format=json','auth/kubernetes/login','-'],payload={'role':lane['role'],'jwt':bad_jwt},allow_failure=True)
|
||||
if bad.returncode==0:
|
||||
revoke(data(bad)['auth']['client_token']);raise LaneError('wrong_service_account_authenticated')
|
||||
require(b'403' in bad.stderr and b'service account' in bad.stderr.lower(),'service_account_denial_inconclusive')
|
||||
row['wrong_service_account_denied']=True
|
||||
receipt['check']=lane['name']+':coding_agent_boundary';save()
|
||||
child=data(bao(['token','create','-format=json','-policy='+lane['policy'],'-policy=agent-high-risk-boundary','-no-default-policy','-ttl=60s']))['auth']['client_token']
|
||||
try:
|
||||
for path in [lane['kv'],lane['metadata']]:require_denied(bao(['read','-format=json',path],token=child,allow_failure=True))
|
||||
finally:revoke(child)
|
||||
row['coding_agent_deny_wins']=True;save()
|
||||
require(len(set(values.values()))==len(lanes),'sender_token_collision')
|
||||
return values
|
||||
|
||||
|
||||
def projection_matches(secret, external, lane, value):
|
||||
require(set(secret['data'])=={lane['secret_key']}
|
||||
and base64.b64decode(secret['data'][lane['secret_key']],validate=True).decode()==value,'projection_value_mismatch')
|
||||
require(any(x['uid']==external['metadata']['uid'] and x.get('controller') is True
|
||||
for x in secret['metadata'].get('ownerReferences',[])),'projection_owner_mismatch')
|
||||
|
||||
|
||||
def guarded_manifest():
|
||||
path=ROOT/'manifests/factory-audit-senders.yaml'
|
||||
require(hashlib.sha256(path.read_bytes()).hexdigest()==MANIFEST_SHA256,'reviewed_manifest_changed')
|
||||
return path,list(yaml.safe_load_all(path.read_text()))
|
||||
|
||||
|
||||
def preflight_projection(kube, objects):
|
||||
for obj in objects:
|
||||
meta=obj['metadata'];cmd=kube+(['-n',meta['namespace']] if 'namespace' in meta else [])
|
||||
result=command(cmd+['get',obj['kind'],meta['name'],'-o','json'],allow_failure=True)
|
||||
if result.returncode:
|
||||
require(b'NotFound' in result.stderr,'projection_presence_unknown');continue
|
||||
live=data(result)
|
||||
# Ignore API default fields, but never redirect an existing delivery lane.
|
||||
require(all(live['spec'].get(k)==v for k,v in obj['spec'].items()),'existing_projection_drift')
|
||||
require(not live['spec'].get('dataFrom') and not live['spec'].get('target',{}).get('template'),'unexpected_projection_template')
|
||||
|
||||
|
||||
def deliver(kube, lanes, values, receipt, save):
|
||||
path,objects=guarded_manifest();preflight_projection(kube,objects)
|
||||
receipt['check']='apply_reviewed_projection';save()
|
||||
command(kube+['apply','-f',str(path)])
|
||||
for lane,row in zip(lanes,receipt['lanes']):
|
||||
receipt['check']=lane['name']+':projection_readiness';save()
|
||||
command(kube+['wait','--for=condition=Ready','clustersecretstore/'+lane['store'],'--timeout=45s'])
|
||||
command(kube+['-n',lane['name'],'wait','--for=condition=Ready','externalsecret/'+lane['secret'],'--timeout=45s'])
|
||||
es=data(command(kube+['-n',lane['name'],'get','externalsecret',lane['secret'],'-o','json']))
|
||||
secret=data(command(kube+['-n',lane['name'],'get','secret',lane['secret'],'-o','json']))
|
||||
projection_matches(secret,es,lane,values[lane['name']])
|
||||
row.update(external_secret_ready=True,projection_matches=True,secret_uid=secret['metadata']['uid'],secret_resource_version=secret['metadata']['resourceVersion']);save()
|
||||
receipt['check']='receiver_registry_delivery';save()
|
||||
version,body,rows,_=snapshot()
|
||||
require(all(desired(l,values[l['name']]) in rows for l in lanes),'registry_scope_changed')
|
||||
command(kube+['-n','audit-core','annotate','externalsecret','audit-core-senders','force-sync='+str(time.time_ns()),'--overwrite'])
|
||||
for _ in range(25):
|
||||
secret=data(command(kube+['-n','audit-core','get','secret','audit-core-senders','-o','json']))
|
||||
if json.loads(base64.b64decode(secret['data']['senders.json']))==json.loads(body['senders.json']):break
|
||||
time.sleep(2)
|
||||
else:raise LaneError('receiver_registry_delivery_mismatch')
|
||||
receipt.update(registry_version=version,receiver_registry_matches=True,receiver_secret_resource_version=secret['metadata']['resourceVersion']);save()
|
||||
receipt['check']='receiver_reload';save()
|
||||
command(kube+['-n','audit-core','rollout','restart','deployment/audit-core'])
|
||||
command(kube+['-n','audit-core','rollout','status','deployment/audit-core','--timeout=50s'])
|
||||
for _ in range(20):
|
||||
try:
|
||||
receipt['receiver_after']=receiver_check(kube,receipt['receiver_before']['image']);break
|
||||
except LaneError as e:
|
||||
if str(e)!='receiver_rollout_in_progress':raise
|
||||
time.sleep(2)
|
||||
else:raise LaneError('receiver_rollout_in_progress')
|
||||
receipt['receiver_reloaded']=True;save()
|
||||
|
||||
|
||||
def namespace_checks(kube, lanes, receipt, save):
|
||||
import secrets
|
||||
ns='factory-audit-check-'+secrets.token_hex(5)
|
||||
created=data(command(kube+['create','namespace',ns,'-o','json']))
|
||||
try:
|
||||
receipt['temporary_namespace']={'name':ns,'uid':created['metadata']['uid']};save()
|
||||
command(kube+['-n',ns,'create','serviceaccount','external-secrets'])
|
||||
jwt=command(kube+['-n',ns,'create','token','external-secrets','--duration=10m']).stdout.decode().strip()
|
||||
for lane,row in zip(lanes,receipt['lanes']):
|
||||
receipt['check']=lane['name']+':wrong_namespace';save()
|
||||
result=bao(['write','-format=json','auth/kubernetes/login','-'],payload={'role':lane['role'],'jwt':jwt},allow_failure=True)
|
||||
if result.returncode==0:
|
||||
revoke(data(result)['auth']['client_token']);raise LaneError('wrong_namespace_authenticated')
|
||||
require(b'403' in result.stderr and b'namespace' in result.stderr.lower(),'namespace_denial_inconclusive')
|
||||
obj={'apiVersion':'external-secrets.io/v1','kind':'ExternalSecret','metadata':{'name':lane['secret'],'namespace':ns},'spec':{'refreshInterval':'15s','secretStoreRef':{'kind':'ClusterSecretStore','name':lane['store']},'target':{'name':lane['secret'],'creationPolicy':'Owner'},'data':[{'secretKey':lane['secret_key'],'remoteRef':{'key':lane['kv'].removeprefix('platform/data/'),'property':'AUDIT_TOKEN'}}]}}
|
||||
command(kube+['create','-f','-'],payload=obj)
|
||||
for _ in range(20):
|
||||
live=data(command(kube+['-n',ns,'get','externalsecret',lane['secret'],'-o','json']))
|
||||
events=data(command(kube+['-n',ns,'get','events','--field-selector=involvedObject.uid='+live['metadata']['uid'],'-o','json']))
|
||||
if any('not allowed' in e.get('message','').lower() and 'namespace' in e.get('message','').lower() for e in events['items']):break
|
||||
time.sleep(2)
|
||||
else:raise LaneError('store_namespace_denial_inconclusive')
|
||||
absent=command(kube+['-n',ns,'get','secret',lane['secret'],'-o','json'],allow_failure=True)
|
||||
require(absent.returncode!=0 and b'NotFound' in absent.stderr,'outside_namespace_secret_not_absent')
|
||||
row.update(wrong_namespace_denied=True,store_namespace_denied=True);save()
|
||||
finally:
|
||||
command(kube+['delete','--raw=/api/v1/namespaces/'+ns,'-f','/dev/stdin'],payload={'apiVersion':'v1','kind':'DeleteOptions','preconditions':{'uid':created['metadata']['uid']}})
|
||||
receipt['namespace_cleanup_requested']=True;save()
|
||||
|
||||
|
||||
def main():
|
||||
p=argparse.ArgumentParser(description=__doc__)
|
||||
p.add_argument('--kubeconfig',required=True);p.add_argument('--server',required=True)
|
||||
p.add_argument('--expected-receiver-image',required=True);p.add_argument('--confirm',required=True)
|
||||
p.add_argument('--receipt',type=Path,required=True)
|
||||
a=p.parse_args();receipt={'schema':'platform.factory-audit-delivery.v1','status':'refused','started_at':datetime.now(timezone.utc).isoformat(),'credential_values_emitted':False};fd=None
|
||||
try:
|
||||
lanes=contracts(approved=True);guarded_manifest();require(a.confirm==CONFIRM,'exact_confirmation_required')
|
||||
require(Path.home().parent.name=='.warden-attended-login' and not os.getenv('BAO_TOKEN') and not os.getenv('VAULT_TOKEN'),'attended_warden_envelope_required')
|
||||
fd=os.open(a.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',a.kubeconfig,'--server',a.server,'--request-timeout=20s']
|
||||
receipt['receiver_before']=receiver_check(kube,a.expected_receiver_image)
|
||||
identity=data(bao(['token','lookup','-format=json']))['data']
|
||||
require('platform-admin' in identity['policies'] and 'root' not in identity['policies'],'attended_platform_admin_required')
|
||||
receipt['lanes']=[{k:x[k] for k in ['ccr','name','kv','store','secret','secret_key']} for x in lanes]
|
||||
receipt['phase']='reader_verification';save();values=verify_readers(kube,lanes,receipt,save)
|
||||
receipt['phase']='delivery';save();deliver(kube,lanes,values,receipt,save)
|
||||
receipt['phase']='namespace_verification';save();namespace_checks(kube,lanes,receipt,save)
|
||||
receipt.update(status='delivered_and_scope_verified_pending_producer_acceptance',completed_at=datetime.now(timezone.utc).isoformat());save();return 0
|
||||
except Exception as exc:
|
||||
receipt.update(status='refused',error=str(exc) if isinstance(exc,LaneError) else 'contained_delivery_failed')
|
||||
if fd is not None:save()
|
||||
return 1
|
||||
finally:
|
||||
if fd is not None:os.close(fd)
|
||||
|
||||
|
||||
if __name__=='__main__':raise SystemExit(main())
|
||||
70
tests/test_factory_audit_delivery.py
Normal file
70
tests/test_factory_audit_delivery.py
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
"""Delivery refuses custody/projection drift before changing a live receiver."""
|
||||
import base64
|
||||
import copy
|
||||
import json
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
sys.path.insert(0,str(Path(__file__).resolve().parents[1]/'scripts'))
|
||||
import factory_audit_delivery as delivery
|
||||
|
||||
|
||||
class DeliveryContracts(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.lane=delivery.contracts()[0]
|
||||
self.value='fixture_only_'+('x'*51)
|
||||
self.raw={'metadata':{'version':1},'data':{'AUDIT_TOKEN':self.value,'CUSTODY_REQUEST':self.lane['ccr']}}
|
||||
|
||||
def test_version_one_exact_provenance_is_required(self):
|
||||
self.assertEqual(delivery.custody_value(self.lane,self.raw),self.value)
|
||||
for raw in [dict(self.raw,metadata={'version':2}),dict(self.raw,data=dict(self.raw['data'],CUSTODY_REQUEST='different-request')),dict(self.raw,data=dict(self.raw['data'],extra='unexpected'))]:
|
||||
with self.assertRaisesRegex(delivery.LaneError,'custody_provenance_mismatch'):delivery.custody_value(self.lane,raw)
|
||||
|
||||
def test_bad_token_shape_never_appears_in_the_diagnostic(self):
|
||||
raw=copy.deepcopy(self.raw);raw['data']['AUDIT_TOKEN']='sensitive fixture with spaces'
|
||||
with self.assertRaisesRegex(delivery.LaneError,'^custody_token_shape$'):delivery.custody_value(self.lane,raw)
|
||||
|
||||
def test_projection_must_match_value_and_controller(self):
|
||||
es={'metadata':{'uid':'expected-controller'}}
|
||||
secret={'data':{self.lane['secret_key']:base64.b64encode(self.value.encode()).decode()},'metadata':{'ownerReferences':[{'uid':'expected-controller','controller':True}]}}
|
||||
delivery.projection_matches(secret,es,self.lane,self.value)
|
||||
with self.assertRaisesRegex(delivery.LaneError,'projection_value_mismatch'):delivery.projection_matches(secret,es,self.lane,'wrong-value')
|
||||
secret['metadata']['ownerReferences'][0]['controller']=False
|
||||
with self.assertRaisesRegex(delivery.LaneError,'projection_owner_mismatch'):delivery.projection_matches(secret,es,self.lane,self.value)
|
||||
|
||||
def test_manifest_byte_change_refuses_before_apply(self):
|
||||
path,objects=delivery.guarded_manifest();self.assertEqual(len(objects),4)
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
target=Path(d)/'manifests';target.mkdir();(target/path.name).write_bytes(path.read_bytes()+b'\n')
|
||||
with patch.object(delivery,'ROOT',Path(d)):
|
||||
with self.assertRaisesRegex(delivery.LaneError,'reviewed_manifest_changed'):delivery.guarded_manifest()
|
||||
|
||||
def test_existing_projection_must_not_be_redirected(self):
|
||||
_,objects=delivery.guarded_manifest();expected=objects[1];actual=copy.deepcopy(expected)
|
||||
actual['spec']['secretStoreRef']['name']='unrelated-store'
|
||||
def result(argv, **kwargs):return subprocess.CompletedProcess(argv,0,json.dumps(actual).encode(),b'')
|
||||
with patch.object(delivery,'command',side_effect=result) as command:
|
||||
with self.assertRaisesRegex(delivery.LaneError,'existing_projection_drift'):delivery.preflight_projection(['kubectl'],[expected])
|
||||
self.assertTrue(all('apply' not in call.args[0] for call in command.call_args_list))
|
||||
|
||||
def test_presence_errors_are_not_treated_as_absence(self):
|
||||
_,objects=delivery.guarded_manifest()
|
||||
with patch.object(delivery,'command',return_value=subprocess.CompletedProcess([],1,b'',b'Forbidden')):
|
||||
with self.assertRaisesRegex(delivery.LaneError,'projection_presence_unknown'):delivery.preflight_projection(['kubectl'],objects)
|
||||
|
||||
def test_unexpected_reader_grant_is_revoked_and_not_used(self):
|
||||
lanes=delivery.contracts();lane=lanes[0]
|
||||
def read(path):
|
||||
if path.startswith('sys/policies'):return {'policy':lane['hcl']}
|
||||
return {}
|
||||
auth={'auth':{'client_token':'test-reader','token_policies':['root'],'lease_duration':900}}
|
||||
with patch.object(delivery,'read_optional',side_effect=read),patch.object(delivery,'role_matches',return_value=True),patch.object(delivery,'command',return_value=subprocess.CompletedProcess([],0,b'fixture-jwt',b'')),patch.object(delivery,'bao',return_value=subprocess.CompletedProcess([],0,json.dumps(auth).encode(),b'')) as bao,patch.object(delivery,'revoke') as revoke:
|
||||
with self.assertRaisesRegex(delivery.LaneError,'reader_scope_or_ttl_mismatch'):delivery.verify_readers(['kubectl'],lanes,{'lanes':[{} for x in lanes]},lambda:None)
|
||||
revoke.assert_called_once_with('test-reader')
|
||||
self.assertEqual(bao.call_count,1)
|
||||
|
||||
|
||||
if __name__=='__main__':unittest.main()
|
||||
|
|
@ -397,11 +397,11 @@ into this client-identity grant.
|
|||
|
||||
```task
|
||||
id: RPF-WP-0035-T08
|
||||
status: wait
|
||||
status: progress
|
||||
priority: high
|
||||
assignee: railiance-platform
|
||||
needs_human: true
|
||||
intervention_note: "All named CCR-2026-0021/0022 reviews are approved. Compatible receiver and namespaces are ready. Attended OIDC failed before owner-command handoff; Warden could not confirm remote session revocation. Await operator browser/MFA feedback and login/session resolution before custody retry. Do not repeat sender approval."
|
||||
needs_human: false
|
||||
intervention_note: null
|
||||
state_hub_task_id: "0ef52c26-2669-5cd1-8149-b1f6fcdda6cb"
|
||||
```
|
||||
|
||||
|
|
@ -498,3 +498,14 @@ resolution. Approvals remain valid; T08 waits for the identity act, not review.
|
|||
The receiver check now selects component=receiver, so retained attestation-job
|
||||
pods cannot falsely block a stable receiver. Eighteen custody tests pass.
|
||||
[Native return](../docs/evidence/2026-09-11-factory-receiver-live.json).
|
||||
|
||||
### Attended custody retry succeeded — 2026-09-11
|
||||
|
||||
The user requested a retry and completed the attended identity act. Both CCRs
|
||||
are applied: independent version-1 custody and registry CAS 7→8 succeeded,
|
||||
preserving other senders/fields. Warden exited 0 with self-revocation confirmed
|
||||
for this session. This does not retrospectively attest the earlier failed
|
||||
login's remote session state. The old login wait is cleared; T08 is progress
|
||||
for ESO delivery, exact native reader/namespace refusals, receiver reload and
|
||||
producer acceptance. No token is reseeded or rotated on continuation.
|
||||
[Seed receipt](../docs/evidence/2026-09-11-factory-audit-sender-seed.json).
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue