railiance-platform/scripts/factory_audit_delivery.py

193 lines
12 KiB
Python
Raw Normal View History

#!/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())