#!/usr/bin/env python3 """Render/install a source-bound node runtime; activation needs actual recovery. Install does not enable timers. Owner applies the separately reviewed delivery and capture-isolation changes, performs first capture and independent recovery, then explicitly activates. No credential values enter this package. """ import argparse import hashlib import importlib.util import json import os from pathlib import Path import shutil import socket import subprocess import uuid ROOT=Path(__file__).resolve().parents[2] HERE=Path(__file__).resolve().parent FILES=[f'tools/bootstrap-recovery/{name}' for name in ['capture-identity.py','identity-files.py','identity-bundle.py', 'identity-runtime.py','identity-offsite.py','identity-retention.py','identity-backup-profile.json', 'prepare-identity-coverage.py','install-identity-backup.py']]+['sso-mfa/k8s/privacyidea/key-custody-remote.py'] STATE='/var/lib/net-kingdom-backup' KUBECONFIG='/etc/rancher/k3s/k3s.yaml' class Refused(Exception):pass def render(decision): uuid.UUID(decision) files=[{'path':name,'sha256':hashlib.sha256((ROOT/name).read_bytes()).hexdigest()} for name in FILES] release=hashlib.sha256(json.dumps(files,sort_keys=True).encode()).hexdigest() directory='/opt/net-kingdom-backup/releases/'+release runtime=directory+'/tools/bootstrap-recovery/identity-runtime.py' common=f' --kubeconfig {KUBECONFIG} --state {STATE}' base=('User=root\nGroup=root\nUMask=0077\nStateDirectory=net-kingdom-backup\nStateDirectoryMode=0700\n' 'NoNewPrivileges=true\nPrivateTmp=true\nPrivateDevices=true\nProtectSystem=strict\nProtectHome=true\n' f'ReadWritePaths={STATE}\nCapabilityBoundingSet=\nRestrictAddressFamilies=AF_UNIX AF_INET AF_INET6\n' 'ProtectKernelTunables=true\nProtectKernelModules=true\nProtectControlGroups=true\n' 'MemoryMax=3G\nCPUQuota=200%\nTasksMax=128\nTimeoutStartSec=45min\nTimeoutStopSec=5min\n') command='/usr/bin/python3 -I '+runtime service=('[Unit]\nDescription=Encrypted full identity backup and verified Storage Share readback\nAfter=network-online.target k3s.service\n' f'ConditionPathExists={STATE}/accepted-recovery.json\n[Service]\nType=oneshot\n'+base+ f'ExecStartPre={command} retention{common}\nExecStart={command} scheduled{common} --decision-id {decision}\n' f'ExecStartPost={command} retention{common}\n') health=('[Unit]\nDescription=Identity backup age check with durable failure reporting\nAfter=network-online.target k3s.service\n' '[Service]\nType=oneshot\n'+base+f'ExecStart={command} status{common}\n') units={'net-kingdom-identity-backup.service':service, 'net-kingdom-identity-backup.timer':'[Unit]\nDescription=Daily full identity recovery bundle\n[Timer]\nOnCalendar=*-*-* 03:45:00 UTC\nPersistent=true\nRandomizedDelaySec=5min\nUnit=net-kingdom-identity-backup.service\n[Install]\nWantedBy=timers.target\n', 'net-kingdom-identity-backup-health.service':health, 'net-kingdom-identity-backup-health.timer':'[Unit]\nDescription=Hourly identity backup freshness check\n[Timer]\nOnCalendar=hourly\nPersistent=true\nUnit=net-kingdom-identity-backup-health.service\n[Install]\nWantedBy=timers.target\n'} return {'schema':'netkingdom.identity-runtime-install.v1','status':'prepared_not_installed','decision_ref':decision, 'release_id':release,'release_directory':directory,'runtime_files':files,'units':units, 'timers_automatically_enabled':False,'first_capture_command':command+' first-capture'+common+' --decision-id '+decision, 'activation_requires_independent_recovery':True,'source_contains_credentials':False} def run(args): r=subprocess.run(args,stdout=subprocess.PIPE,stderr=subprocess.DEVNULL,timeout=120) if r.returncode:raise Refused() return r.stdout def module(name): spec=importlib.util.spec_from_file_location(name,HERE/(name+'.py'));m=importlib.util.module_from_spec(spec);spec.loader.exec_module(m);return m def owner(): module('identity-bundle').require_runtime_host() capture=module('capture-identity');capture.KUBE=['kubectl','--kubeconfig',KUBECONFIG] if capture.get(None,'namespace','kube-system')['metadata']['uid']!=capture.prepare.CLUSTER_UID:raise Refused() def install(packet): owner();expected=render(packet['decision_ref']) if packet!=expected:raise Refused() destination=Path(packet['release_directory']) # Refuse differing prior units before changing files. Installation never # overwrites a running/unknown version as an implicit upgrade. for name,value in packet['units'].items(): path=Path('/etc/systemd/system')/name if path.is_symlink() or (path.exists() and path.read_text()!=value):raise Refused() destination.mkdir(mode=0o700,parents=True,exist_ok=True) for row in packet['runtime_files']: target=destination/row['path'];target.parent.mkdir(mode=0o700,parents=True,exist_ok=True) if target.exists(): if target.is_symlink() or hashlib.sha256(target.read_bytes()).hexdigest()!=row['sha256']:raise Refused() else: fd=os.open(target,os.O_WRONLY|os.O_CREAT|os.O_EXCL|os.O_NOFOLLOW,0o600) with os.fdopen(fd,'wb') as out:out.write((ROOT/row['path']).read_bytes()) if hashlib.sha256(target.read_bytes()).hexdigest()!=row['sha256']:raise Refused() for name,value in packet['units'].items(): path=Path('/etc/systemd/system')/name if not path.exists(): fd=os.open(path,os.O_WRONLY|os.O_CREAT|os.O_EXCL|os.O_NOFOLLOW,0o644) with os.fdopen(fd,'w') as out:out.write(value) run(['systemd-analyze','verify',*[str(Path('/etc/systemd/system')/name) for name in packet['units']]]) run(['systemctl','daemon-reload']) def activate(proof): owner();runtime=module('identity-runtime');r=runtime.accepted(proof) state=Path(STATE);runtime.capture.bundle.private(state,True) last=json.loads((state/'last-success.json').read_text()) if last.get('offsite_verified') is not True:raise Refused() # Restore proof names the exact protected artifact; require its local # off-site receipt too, not a synthetic JSON worksheet. path=state/'runs'/r['artifact']/'offsite.json' runtime.capture.bundle.private(path);transfer=json.loads(path.read_text()) if transfer.get('status')!='readback_verified' or transfer.get('ciphertext_sha256')!=r['ciphertext_sha256']:raise Refused() target=state/'accepted-recovery.json' if target.exists(): if target.is_symlink() or json.loads(target.read_text())!=r:raise Refused() else: fd=os.open(target,os.O_WRONLY|os.O_CREAT|os.O_EXCL|os.O_NOFOLLOW,0o600) with os.fdopen(fd,'w') as out:json.dump(r,out,indent=2);out.write('\n') run(['systemctl','start','net-kingdom-identity-backup-health.service']) run(['systemctl','enable','--now','net-kingdom-identity-backup.timer','net-kingdom-identity-backup-health.timer']) for unit in ['net-kingdom-identity-backup.timer','net-kingdom-identity-backup-health.timer']: if run(['systemctl','is-active',unit]).strip()!=b'active':raise Refused() def main(): p=argparse.ArgumentParser(description=__doc__);p.add_argument('action',choices=['render','install','activate']) p.add_argument('--decision-id');p.add_argument('--packet');p.add_argument('--packet-sha256');p.add_argument('--accepted-receipt') p.add_argument('--receipt',required=True);a=p.parse_args();os.umask(0o077) fd=os.open(a.receipt,os.O_WRONLY|os.O_CREAT|os.O_EXCL|os.O_NOFOLLOW,0o600);r={'status':'failed'} try: if a.action=='render':r=render(a.decision_id) elif a.action=='install': raw=Path(a.packet).read_bytes() if hashlib.sha256(raw).hexdigest()!=a.packet_sha256:raise Refused() packet=json.loads(raw);install(packet) r={'status':'installed_timers_disabled','release_id':packet['release_id']} else: activate(Path(a.accepted_receipt));r={'status':'timers_active_after_independent_recovery'} except Exception:r={'status':'failed','error':'owner_install_or_activation_refused_private_diagnostics_suppressed'} finally: with os.fdopen(fd,'w') as out:json.dump(r,out,indent=2);out.write('\n') return int(r['status']=='failed') if __name__=='__main__':raise SystemExit(main())