193 lines
12 KiB
Python
193 lines
12 KiB
Python
|
|
#!/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())
|