Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a13eab-1e6a-7b33-9c37-372e90ada3a2
145 lines
8.2 KiB
Python
145 lines
8.2 KiB
Python
#!/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())
|