Implement attended Railiance Clock host key delivery
Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a09cbb-87c6-7900-a145-4ce53ba9f1a6
This commit is contained in:
parent
3b11773469
commit
e70ef2f32a
7 changed files with 284 additions and 49 deletions
|
|
@ -464,10 +464,16 @@ def validate_ccr(path: Path) -> tuple[dict[str, Any], list[str], list[str]]:
|
|||
if isinstance(status, str) and status not in ALLOWED_STATUSES:
|
||||
errors.append(f"status must be one of {sorted(ALLOWED_STATUSES)}")
|
||||
request_type = ccr.get("request_type")
|
||||
if request_type != "workload-kv-read":
|
||||
errors.append("request_type must be workload-kv-read")
|
||||
else:
|
||||
if request_type == "workload-kv-read":
|
||||
validate_workload_kv_read(ccr, errors, warnings)
|
||||
elif request_type == "attended-host-key-delivery":
|
||||
import importlib.util
|
||||
spec = importlib.util.spec_from_file_location("railiance_clock_custody", REPO_DIR / "scripts/railiance_clock_custody.py")
|
||||
owner = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(owner)
|
||||
errors.extend(owner.validate_contract(ccr))
|
||||
else:
|
||||
errors.append("unsupported request_type")
|
||||
return ccr, errors, warnings
|
||||
|
||||
|
||||
|
|
@ -554,6 +560,8 @@ def render_summary(ccr: dict[str, Any], warnings: list[str]) -> str:
|
|||
|
||||
|
||||
def generated_policy_hcl(ccr: dict[str, Any]) -> str:
|
||||
if ccr.get("request_type") != "workload-kv-read":
|
||||
fail("attended host delivery uses its owner procedure; no workload policy")
|
||||
openbao = ccr["openbao"]
|
||||
mount = openbao["mount"]
|
||||
suffix = openbao["kv_path"][len(mount) + 1 :]
|
||||
|
|
|
|||
132
scripts/railiance_clock_custody.py
Normal file
132
scripts/railiance_clock_custody.py
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Attended CCR-2026-0028 initial custody and host delivery. Silent, resumable."""
|
||||
from pathlib import Path
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import shlex
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
CCR = ROOT / 'credential-change-requests/CCR-2026-0028-railiance-clock-authority-signing.yaml'
|
||||
KV = 'platform/data/workloads/railiance-clock/authority-signing'
|
||||
KID = 'railiance01-clock-20260915-v1'
|
||||
TARGET = '/var/lib/railiance-clock/signing.pem'
|
||||
|
||||
|
||||
def validate_contract(ccr):
|
||||
errors=[]
|
||||
expected={'mount':'platform','kv_path':'platform/workloads/railiance-clock/authority-signing',
|
||||
'fields':['PRIVATE_KEY_PEM','KEY_ID'],'auth':{'method':'attended-oidc','mount':'netkingdom','role':'platform-admin'},
|
||||
'runtime_access':False,'initial_cas':0}
|
||||
delivery={'surface':'attended-host-file','host':'92.205.62.239','ssh_user':'tegwick',
|
||||
'target':TARGET,'owner':'railiance-clock','mode':'0600','directory_mode':'0700',
|
||||
'procedure':'scripts/railiance_clock_custody.py'}
|
||||
if ccr.get('id')!='CCR-2026-0028' or ccr.get('request_type')!='attended-host-key-delivery':
|
||||
errors.append('unsupported attended host contract')
|
||||
if ccr.get('openbao')!=expected or ccr.get('delivery')!=delivery:
|
||||
errors.append('exact Clock custody and host binding required')
|
||||
if ccr.get('access_frontdoor',{}).get('resolvable') is not False:
|
||||
errors.append('runtime credential frontdoor must remain disabled')
|
||||
if ccr.get('status') in {'approved','applied','verified','active'}:
|
||||
comments=ccr.get('review',{}).get('comments',[])
|
||||
for role in ['platform-operator','railiance-clock-owner']:
|
||||
if not any(c.get('reviewer')==f'User ({role})' and c.get('decision')=='approved' for c in comments):
|
||||
errors.append('explicit role approval missing: '+role)
|
||||
for section,fields in {'verification':['positive','negative','activation_conditions'],
|
||||
'lifecycle':['deactivate','rotate','compromised']}.items():
|
||||
for field in fields:
|
||||
if not ccr.get(section,{}).get(field):errors.append(section+'.'+field+' required')
|
||||
return errors
|
||||
|
||||
class CustodyError(Exception): pass
|
||||
|
||||
def run_cmd(argv,payload=None):
|
||||
r=subprocess.run(argv,input=payload,capture_output=True,timeout=90)
|
||||
if r.returncode:raise CustodyError('contained_command_failed')
|
||||
return r.stdout
|
||||
|
||||
def bao(args,payload=None):
|
||||
return json.loads(run_cmd(['bao',*args],None if payload is None else json.dumps(payload).encode()))
|
||||
|
||||
REMOTE = '''import os,sys,stat,pwd,subprocess,json,hashlib
|
||||
p='/var/lib/railiance-clock/signing.pem'
|
||||
u=pwd.getpwnam('railiance-clock')
|
||||
d=os.lstat(os.path.dirname(p))
|
||||
assert stat.S_ISDIR(d.st_mode) and d.st_uid==u.pw_uid and stat.S_IMODE(d.st_mode)==0o700
|
||||
key=sys.stdin.buffer.read(8193)
|
||||
assert 0<len(key)<=8192
|
||||
# Publish the complete key atomically; initial delivery never overwrites a key.
|
||||
if not os.path.lexists(p):
|
||||
tmp=p+'.admission-'+str(os.getpid())
|
||||
fd=os.open(tmp,os.O_WRONLY|os.O_CREAT|os.O_EXCL|os.O_NOFOLLOW,0o600)
|
||||
try:
|
||||
os.fchown(fd,u.pw_uid,u.pw_gid)
|
||||
with os.fdopen(fd,'wb') as f:
|
||||
f.write(key);f.flush();os.fsync(f.fileno())
|
||||
os.link(tmp,p)
|
||||
finally:
|
||||
os.unlink(tmp)
|
||||
fd=os.open(p,os.O_RDONLY|os.O_NOFOLLOW)
|
||||
with os.fdopen(fd,'rb') as f:
|
||||
st=os.fstat(f.fileno())
|
||||
assert stat.S_ISREG(st.st_mode) and st.st_uid==u.pw_uid and stat.S_IMODE(st.st_mode)==0o600
|
||||
assert f.read(8193)==key
|
||||
r=subprocess.run(['openssl','pkey','-pubout'],input=key,capture_output=True,check=True)
|
||||
print(json.dumps({'public_key_sha256':hashlib.sha256(r.stdout).hexdigest(),'owner':'railiance-clock','mode':'0600'}))
|
||||
'''
|
||||
|
||||
def main():
|
||||
p=argparse.ArgumentParser(description=__doc__)
|
||||
p.add_argument('--receipt',required=True)
|
||||
p.add_argument('--resume-version',type=int)
|
||||
a=p.parse_args()
|
||||
fd=os.open(a.receipt,os.O_WRONLY|os.O_CREAT|os.O_EXCL|os.O_NOFOLLOW,0o600)
|
||||
receipt={'schema':'platform.railiance-clock-host-custody.v1','status':'failed','stage':'contract','ccr':'CCR-2026-0028'}
|
||||
try:
|
||||
import yaml
|
||||
from cryptography.hazmat.primitives import serialization
|
||||
from cryptography.hazmat.primitives.asymmetric import ec
|
||||
ccr=yaml.safe_load(CCR.read_text())
|
||||
if validate_contract(ccr) or ccr['status'] not in {'approved','applied','verified','active'}:
|
||||
raise CustodyError('approved_exact_contract_required')
|
||||
receipt['stage']='attended_identity'
|
||||
who=bao(['token','lookup','-format=json'])['data']
|
||||
if 'platform-admin' not in who['policies'] or 'root' in who['policies'] or not who['path'].startswith('auth/netkingdom/'):
|
||||
raise CustodyError('attended_platform_admin_required')
|
||||
if not bao(['audit','list','-format=json']):raise CustodyError('audit_required')
|
||||
receipt['stage']='custody'
|
||||
if a.resume_version is None:
|
||||
key=ec.generate_private_key(ec.SECP256R1())
|
||||
pem=key.private_bytes(serialization.Encoding.PEM,serialization.PrivateFormat.PKCS8,serialization.NoEncryption()).decode()
|
||||
result=bao(['write','-format=json',KV,'-'],{'options':{'cas':0},'data':{'PRIVATE_KEY_PEM':pem,'KEY_ID':KID}})
|
||||
version=result['data']['version']
|
||||
else:
|
||||
if a.resume_version!=1:raise CustodyError('only_initial_version_resume_supported')
|
||||
version=a.resume_version
|
||||
receipt['kv_version']=version
|
||||
native=bao(['read','-format=json',KV])['data']
|
||||
if native['metadata']['version']!=version or version!=1:raise CustodyError('custody_version_changed')
|
||||
values=native['data']
|
||||
if set(values)!={'PRIVATE_KEY_PEM','KEY_ID'} or values['KEY_ID']!=KID:raise CustodyError('custody_shape_mismatch')
|
||||
pem=values['PRIVATE_KEY_PEM'].encode()
|
||||
key=serialization.load_pem_private_key(pem,password=None)
|
||||
if not isinstance(key,ec.EllipticCurvePrivateKey) or not isinstance(key.curve,ec.SECP256R1):raise CustodyError('es256_key_required')
|
||||
public=key.public_key().public_bytes(serialization.Encoding.PEM,serialization.PublicFormat.SubjectPublicKeyInfo)
|
||||
receipt.update(kid=KID,public_key_pem=public.decode(),public_key_sha256=hashlib.sha256(public).hexdigest())
|
||||
receipt['stage']='host_delivery'
|
||||
remote=json.loads(run_cmd(['ssh','-o','BatchMode=yes','-o','StrictHostKeyChecking=yes','-o','ConnectTimeout=10',
|
||||
'tegwick@92.205.62.239','sudo -n python3 -c '+shlex.quote(REMOTE)],pem))
|
||||
if remote['public_key_sha256']!=receipt['public_key_sha256']:raise CustodyError('host_public_key_mismatch')
|
||||
receipt['host']=remote
|
||||
receipt['status']='delivered_pending_authority_acceptance'
|
||||
receipt['stage']='complete'
|
||||
except Exception as e:
|
||||
receipt['error']=str(e) if isinstance(e,CustodyError) else 'internal_error'
|
||||
finally:
|
||||
with os.fdopen(fd,'w') as f:json.dump(receipt,f,indent=2);f.write('\n')
|
||||
return 0 if receipt['status']!='failed' else 1
|
||||
|
||||
if __name__=='__main__':sys.exit(main())
|
||||
Loading…
Add table
Add a link
Reference in a new issue