docs: preserve reviewed NetKingdom session artifacts
Some checks failed
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 2s
Reject generated-only commits / generated-commit-guard (push) Has been cancelled

This commit is contained in:
codex 2026-11-01 23:06:45 +01:00
parent bdf71998e9
commit b07fbc60b1
46 changed files with 8683 additions and 0 deletions

View file

@ -0,0 +1,243 @@
import base64
import importlib.util
import json
import io
import os
import tempfile
import signal
import subprocess
import sys
import time
from pathlib import Path
import unittest
from unittest.mock import patch
SOURCE=Path(__file__).parents[1]/'tools/bootstrap-recovery/isolated-keycape-session.py'
spec=importlib.util.spec_from_file_location('session',SOURCE)
s=importlib.util.module_from_spec(spec);spec.loader.exec_module(s)
class SessionTests(unittest.TestCase):
def test_tmpfs_plan_has_four_exact_original_mounts(self):
session=s.plan_session()
self.assertEqual({v['target'] for v in session['volumes'].values()},set(s.RUNROOTS))
self.assertTrue(all(v['options']==s.VOLUME_OPTIONS for v in session['volumes'].values()))
self.assertEqual(session['images'][session['issuer']],s.adapter.IMAGE)
def test_non_tmpfs_driver_options_refused(self):
session=s.plan_session();name,spec=next(iter(session['volumes'].items()))
wrong=[{'Name':name,'Driver':'local','Scope':'local','Options':{'type':'none','device':'/host/data'},'Labels':{s.adapter.LABEL:session['run_id']}}]
with patch.object(s,'run',return_value=json.dumps(wrong).encode()):
with self.assertRaises(s.adapter.Refused):s.verify_volume(name,spec,session['run_id'])
def test_owner_secret_is_not_docker_argv_and_import_is_after_inspection(self):
session=s.plan_session();events=[]
payload={'files':{'/etc/keycape/config.yaml':base64.b64encode(b'original').decode()},'env':{'KEYCAPE_TEST_SECRET':'fixture-sensitive'}}
def run(argv,**kwargs):
events.append(('run',argv,kwargs))
if argv[-1]=='{{.Id}}':return b'anchor'
return b''
with patch.object(s,'run',side_effect=run),patch.object(s,'verify_volume'),patch.object(s.adapter,'inspect_session',side_effect=lambda *args,**kwargs:events.append(('inspect',args,kwargs))):
s.provision(session,payload,{'/recovery-tls/trust.pem':b'original-trust'},'https://issuer.example')
joined=' '.join(' '.join(e[1]) for e in events if e[0]=='run')
self.assertNotIn('fixture-sensitive',joined)
private_index=next(i for i,e in enumerate(events) if e[0]=='run' and s.IMPORT in e[1])
inspect_index=next(i for i,e in enumerate(events) if e[0]=='inspect')
self.assertLess(inspect_index,private_index)
issuer_call=next(e for e in events if e[0]=='run' and e[1][-1]==s.adapter.IMAGE)
self.assertEqual(issuer_call[2]['env']['KEYCAPE_TEST_SECRET'],'fixture-sensitive')
self.assertIn('KEYCAPE_TEST_SECRET',issuer_call[1])
self.assertNotIn('--entrypoint',issuer_call[1])
def test_non_original_https_port_refused_before_any_mutation(self):
with patch.object(s,'run') as run:
with self.assertRaises(s.adapter.Refused):s.provision(s.plan_session(),{'files':{},'env':{}},{},'http://issuer.example')
run.assert_not_called()
def test_container_failure_still_attempts_each_volume_cleanup(self):
session=s.plan_session();receipt={};removed=[]
def run(argv,**kwargs):
if argv[1:3]==['volume','rm']:removed.append(argv[-1])
if argv[1:3]==['volume','ls']:
name=next(x for x in argv if x.startswith('name=^')).removeprefix('name=^').removesuffix('$')
return (name+'\n').encode() if name not in removed else b''
return b''
with patch.object(s.adapter,'cleanup',side_effect=s.adapter.Refused),patch.object(s,'verify_volume'),patch.object(s,'run',side_effect=run):
with self.assertRaises(s.adapter.Refused):s.cleanup_all(session,receipt)
self.assertEqual(set(removed),set(session['volumes']))
self.assertFalse(receipt['container_cleanup_verified'])
self.assertEqual(len(receipt['tmpfs_volume_cleanup_obligations']),4)
def test_reaper_inherits_no_custody_or_service_environment(self):
session=s.plan_session();staging={'path':'/dev/shm/registered','run_id':session['run_id']}
with patch.object(s.subprocess,'Popen') as spawn,patch.object(s,'verify_execution'),patch.object(s.os,'pidfd_open',return_value=99),patch.object(s.os,'close'),patch.object(s,'register_workdir',return_value=staging),patch.object(s.select,'select',side_effect=lambda *args:([spawn.return_value.stdout],[],[])):
spawn.return_value.stdout.readline.return_value=b'registered\n'
s.start_reaper(session,staging,{'pid':123})
kwargs=spawn.call_args.kwargs
self.assertEqual(set(kwargs['env']),{'PATH','DOCKER_HOST'})
self.assertTrue(kwargs['close_fds']);self.assertTrue(kwargs['start_new_session']);self.assertEqual(kwargs['pass_fds'],(99,))
class TargetBindingTests(unittest.TestCase):
def test_every_direct_docker_command_is_explicitly_bound(self):
with patch.dict('os.environ',{},clear=True),patch.object(s.adapter.subprocess,'run',return_value=__import__('subprocess').CompletedProcess([],0,b'ok')) as run:
self.assertEqual(s.run(['docker','info']),b'ok')
argv=run.call_args.args[0]
self.assertEqual(argv[:3],['docker','--host',s.adapter.ENDPOINT])
self.assertEqual(run.call_args.kwargs['env']['DOCKER_HOST'],s.adapter.ENDPOINT)
def test_nondefault_unix_socket_is_refused_before_any_docker_call(self):
with patch.dict('os.environ',{'DOCKER_HOST':'unix:///tmp/other-daemon.sock'},clear=True),patch.object(s.adapter.subprocess,'run') as run:
with self.assertRaises(s.adapter.Refused):s.run(['docker','info'])
run.assert_not_called()
def test_conflicting_context_is_refused(self):
with patch.dict('os.environ',{'DOCKER_CONTEXT':'production'},clear=True),patch.object(s.adapter.subprocess,'run') as run:
with self.assertRaises(s.adapter.Refused):s.run(['docker','info'])
run.assert_not_called()
def test_conflicting_per_call_host_is_refused(self):
with patch.dict('os.environ',{},clear=True),patch.object(s.adapter.subprocess,'run') as run:
with self.assertRaises(s.adapter.Refused):s.run(['docker','info'],env={'DOCKER_HOST':'unix:///tmp/other.sock'})
run.assert_not_called()
def test_reused_preflight_uses_same_bound_command(self):
self.assertIs(s.local.command,s.adapter.command)
def test_probe_is_registered_before_session_creation(self):
session=s.plan_session()
self.assertEqual(session['public_image_probe'],'nk49-kc-'+session['run_id']+'-image-probe')
self.assertEqual(session['docker_endpoint'],s.adapter.ENDPOINT)
class PlaintextLifecycleTests(unittest.TestCase):
def setUp(self):
self.tmp=tempfile.TemporaryDirectory();self.addCleanup(self.tmp.cleanup)
self.root=Path(self.tmp.name)
self.override=patch.object(s,'STAGING_ROOT',self.root);self.override.start();self.addCleanup(self.override.stop)
self.run_id='a'*32
self.work=self.root/('nk49-keycape-'+self.run_id+'-abcdefgh');self.work.mkdir(mode=0o700)
self.staging=s.register_workdir(self.work,self.run_id)
def test_exact_plaintext_directory_removed_and_recorded(self):
(self.work/'archive.tar').write_bytes(b'synthetic-private-archive')
restored=self.work/'restored';restored.mkdir(mode=0o700);(restored/'fixture').write_bytes(b'synthetic')
receipt={};s.cleanup_workdir(self.staging,receipt)
self.assertFalse(self.work.exists());self.assertTrue(receipt['temporary_plaintext_cleanup_verified'])
self.assertEqual(receipt['plaintext_cleanup_obligation']['status'],'absence_verified')
def test_docker_failure_does_not_skip_plaintext_cleanup(self):
(self.work/'archive.tar').write_bytes(b'synthetic')
receipt={}
with patch.object(s,'cleanup_all',side_effect=s.adapter.Refused):
with self.assertRaises(s.adapter.Refused):s.recover_obligations({},self.staging,receipt)
self.assertFalse(self.work.exists());self.assertTrue(receipt['temporary_plaintext_cleanup_verified'])
def test_replacement_inode_is_not_removed(self):
# Preserve original inode while replacing its registered pathname.
old=self.root/'retained-original';self.work.rename(old);self.work.mkdir(mode=0o700)
(self.work/'replacement').write_bytes(b'not-owned-by-this-obligation');receipt={}
with self.assertRaises(s.adapter.Refused):s.cleanup_workdir(self.staging,receipt)
self.assertTrue((self.work/'replacement').exists());self.assertTrue(old.exists())
self.assertEqual(receipt['plaintext_cleanup_obligation']['status'],'identity_mismatch_not_removed')
def test_symlink_root_is_not_followed(self):
self.work.rmdir();outside=self.root/'outside';outside.mkdir();(outside/'kept').write_bytes(b'keep')
self.work.symlink_to(outside,target_is_directory=True)
with self.assertRaises(s.adapter.Refused):s.cleanup_workdir(self.staging,{})
self.assertTrue((outside/'kept').exists())
def test_descendant_symlink_is_not_followed(self):
outside=self.root/'kept';outside.write_bytes(b'keep');(self.work/'link').symlink_to(outside)
with self.assertRaises(s.adapter.Refused):s.cleanup_workdir(self.staging,{})
self.assertEqual(outside.read_bytes(),b'keep')
def test_wrong_owner_identity_is_not_removed(self):
changed={**self.staging,'owner_uid':os.getuid()+1}
with self.assertRaises(s.adapter.Refused):s.cleanup_workdir(changed,{})
self.assertTrue(self.work.exists())
def test_unregistered_path_refused(self):
with self.assertRaises(s.adapter.Refused):s.cleanup_workdir({**self.staging,'path':str(self.root)}, {})
@unittest.skipUnless(hasattr(os,'pidfd_open') and hasattr(signal,'pidfd_send_signal'),'Linux pidfd required')
class ExecutionDeadlineTests(unittest.TestCase):
def start_synthetic_owner(self,root):
late=str(root/'late-creation')
code="""import importlib.util,json,subprocess,sys,time
spec=importlib.util.spec_from_file_location('session',sys.argv[1]);s=importlib.util.module_from_spec(spec);spec.loader.exec_module(s)
execution=s.isolate_execution()
child=subprocess.Popen([sys.executable,'-c',"import pathlib,sys,time;time.sleep(2);pathlib.Path(sys.argv[1]).write_text('late')",sys.argv[2]])
print(json.dumps({'execution':execution,'child_pid':child.pid}),flush=True)
time.sleep(20)
"""
parent=subprocess.Popen([sys.executable,'-c',code,str(SOURCE),late],stdout=subprocess.PIPE,stderr=subprocess.DEVNULL)
message=json.loads(parent.stdout.readline());parent.stdout.close()
def finish():
if parent.poll() is None:parent.kill()
parent.wait(timeout=5)
try:
before=s.process_facts(message['child_pid'])
if before is None or before['sid']!=message['execution']['sid'] or before['pgid']!=message['execution']['pgid']:return
fd=os.pidfd_open(message['child_pid'])
try:
after=s.process_facts(message['child_pid'])
if after is not None and all(after[k]==before[k] for k in ('pid','sid','pgid','start_ticks','uid')):
signal.pidfd_send_signal(fd,signal.SIGKILL)
except ProcessLookupError:pass
finally:os.close(fd)
except ProcessLookupError:pass
self.addCleanup(finish)
return parent,message['execution'],Path(late)
def test_deadline_quiesces_only_fresh_owner_group_before_cleanup(self):
with tempfile.TemporaryDirectory() as directory:
parent,execution,late=self.start_synthetic_owner(Path(directory));fd=os.pidfd_open(parent.pid)
try:
receipt={};s.quiesce_execution(execution,fd,receipt)
self.assertTrue(receipt['execution_quiescence_verified'])
self.assertFalse(s.group_members(execution));self.assertFalse(late.exists())
finally:os.close(fd)
def test_hard_parent_loss_stops_surviving_marker_creator(self):
with tempfile.TemporaryDirectory() as directory:
parent,execution,late=self.start_synthetic_owner(Path(directory));fd=os.pidfd_open(parent.pid)
try:
signal.pidfd_send_signal(fd,signal.SIGKILL)
poll=__import__('select').poll();poll.register(fd,__import__('select').POLLIN);self.assertTrue(poll.poll(2000))
receipt={};s.quiesce_execution(execution,fd,receipt)
self.assertTrue(receipt['execution_quiescence_verified']);self.assertFalse(late.exists())
self.assertFalse(s.group_members(execution))
finally:os.close(fd)
def test_expired_execution_cannot_start_any_docker_command(self):
binding={'pid':os.getpid(),'sid':os.getsid(0),'pgid':os.getpgrp(),'deadline':time.monotonic()-1,'staging':'/tmp/not-created'}
with patch.object(s.adapter,'EXECUTION',binding),patch.object(s.adapter.subprocess,'run') as run:
with self.assertRaises(s.adapter.Refused):s.run(['docker','create','fixed-image'])
run.assert_not_called()
def test_reused_or_foreign_session_refuses_signals(self):
binding={'pid':123,'sid':123,'pgid':123,'start_ticks':10,'uid':os.getuid()}
wrong={**binding,'sid':456,'state':'S'}
with patch.object(s,'process_facts',return_value=wrong),patch.object(s.signal,'pidfd_send_signal') as send:
with self.assertRaises(s.adapter.Refused):s.quiesce_execution(binding,99,{})
send.assert_not_called()
def test_inflight_uncertainty_survives_plaintext_directory_removal(self):
with tempfile.TemporaryDirectory() as directory:
path=Path(directory)/'owned-stage';path.mkdir();marker=Path(str(path)+'-docker-inflight');marker.touch()
staging={'path':str(path)};self.assertTrue(s.daemon_uncertain(staging));path.rmdir()
self.assertTrue(s.daemon_uncertain(staging))
class ReaperOrderingTests(unittest.TestCase):
def exercise(self,quiescence_fails=False):
events=[];records=[]
def quiet(*args):
events.append('quiesce')
if quiescence_fails:raise s.adapter.Refused()
args[-1]['execution_quiescence_verified']=True
def recover(*args):events.append('cleanup')
with patch.object(s,'verify_execution'),patch.object(s,'pidfd_dead',return_value=False),patch.object(s.select,'poll') as poll,patch.object(s,'quiesce_execution',side_effect=quiet),patch.object(s,'daemon_uncertain',return_value=True),patch.object(s,'recover_obligations',side_effect=recover),patch.object(s.os,'close'),patch.object(s.os,'open',return_value=99),patch.object(s.os,'fdopen'),patch.object(s.json,'dump',side_effect=lambda r,*args:records.append(dict(r))),patch('sys.stdout',io.StringIO()):
poll.return_value.poll.return_value=[]
s.reaper_main({}, {'path':'/dev/shm/exact-staging'}, {'deadline':time.monotonic()-1},77)
return events,records[0]
def test_daemon_uncertainty_survives_verified_resource_readback(self):
events,receipt=self.exercise();self.assertEqual(events,['quiesce','cleanup'])
self.assertTrue(receipt['execution_quiescence_verified']);self.assertTrue(receipt['in_flight_daemon_request_uncertain'])
self.assertEqual(receipt['status'],'bounded_cleanup_uncertain')
def test_unverified_quiescence_prevents_any_cleanup(self):
events,receipt=self.exercise(True);self.assertEqual(events,['quiesce'])
self.assertFalse(receipt['execution_quiescence_verified']);self.assertEqual(receipt['status'],'bounded_cleanup_uncertain')
def test_checkpoint_rewrites_one_valid_json_document(self):
with tempfile.TemporaryFile() as stream:
s.persist_receipt(stream.fileno(),{'status':'registered','long':'x'*100})
s.persist_receipt(stream.fileno(),{'status':'failed'})
stream.seek(0);self.assertEqual(json.load(stream),{'status':'failed'})
def test_docker_timeout_retains_independent_uncertainty_marker(self):
with tempfile.TemporaryDirectory() as directory:
staging=str(Path(directory)/'stage');Path(staging).mkdir()
binding={'pid':os.getpid(),'sid':os.getsid(0),'pgid':os.getpgrp(),'deadline':time.monotonic()+10,'staging':staging}
def timeout(*args,**kwargs):
self.assertTrue(Path(staging+'-docker-inflight').exists())
raise subprocess.TimeoutExpired('synthetic Docker',1)
with patch.object(s.adapter,'EXECUTION',binding),patch.object(s.adapter.subprocess,'run',side_effect=timeout):
with self.assertRaises(subprocess.TimeoutExpired):s.run(['docker','create','fixed-image'])
self.assertFalse(Path(staging+'-docker-inflight').exists())
self.assertTrue(Path(staging+'-docker-uncertain').exists());self.assertTrue(s.daemon_uncertain({'path':staging}))
if __name__=='__main__':unittest.main()

View file

@ -0,0 +1,156 @@
#!/usr/bin/env python3
"""Restore the fetched PI database in an isolated, disposable local PostgreSQL.
Run only through the attended custody helper. No cluster, production database,
network, ports, host data mounts, IAM login or token decryption is involved.
"""
import argparse
from datetime import datetime, timezone
import hashlib
import importlib.util
import json
import os
from pathlib import Path
import signal
import subprocess
import sys
import tempfile
import time
import uuid
ROOT=Path(__file__).resolve().parents[2]
spec=importlib.util.spec_from_file_location('custody',ROOT/'sso-mfa/bootstrap/sops-private.py')
custody=importlib.util.module_from_spec(spec);spec.loader.exec_module(custody)
RECIPIENT='age1zvryunvjhvpkmasskauga2heeg0ztnte9ymgppvjge36ekumk50syr3tsz'
IMAGE='postgres@sha256:ff23cdce56cac62ada6f66013e1a50864c0abbe79d132d40b6e05bd80f378a70'
OBJECT='r01-net-kingdom-pg/net-kingdom-pg-privacyidea_db-20261015T023000Z.dump.age'
SIZE=89129309
class Refused(Exception):pass
def run(args,**kwargs):
result=subprocess.run(args,stdout=subprocess.PIPE,stderr=subprocess.DEVNULL,timeout=600,**kwargs)
if result.returncode:raise Refused()
return result.stdout
def transfer_input(receipt):
r=json.loads(receipt.read_text())
if (r.get('schema')!='netkingdom.privacyidea-offhost-transfer.v1' or
r.get('status')!='ciphertext_fetched' or r.get('object')!=OBJECT or
r.get('ciphertext_bytes')!=SIZE or r.get('object_unchanged_during_transfer') is not True):raise Refused()
path=Path(r['ciphertext_path'])
if not path.is_absolute() or path.parent.parent!=Path('/tmp') or not path.parent.name.startswith('nk49-offhost-'):raise Refused()
if any(p.is_symlink() for p in (path,*path.parents)):raise Refused()
st=path.stat()
if st.st_uid!=os.getuid() or st.st_mode&0o077 or st.st_nlink!=1 or not path.is_file() or st.st_size!=SIZE:raise Refused()
blob=path.read_bytes()
if len(blob)!=SIZE or hashlib.sha256(blob).hexdigest()!=r['ciphertext_sha256']:raise Refused()
return blob,r['ciphertext_sha256']
def restore(dump,container):
# Require the exact image already present; do not pull through a mutable tag.
image_id=run(['docker','image','inspect','--format','{{.Id}}',IMAGE]).decode().strip()
run(['docker','run','--detach','--pull=never','--name',container,'--network','none',
'--security-opt','no-new-privileges','--memory','2g','--cpus','2',
'--tmpfs','/var/lib/postgresql/data:rw,nosuid,nodev,size=1073741824',
'--env','POSTGRES_HOST_AUTH_METHOD=trust','--env','POSTGRES_DB=recovery',
IMAGE,'postgres','-c','listen_addresses='])
for _ in range(60):
# The image starts a temporary server during initdb. Wait until the
# entrypoint has exec'd the final server before attempting the restore.
pid1=subprocess.run(['docker','exec',container,'cat','/proc/1/comm'],
stdout=subprocess.PIPE,stderr=subprocess.DEVNULL,timeout=10)
if pid1.returncode or pid1.stdout.strip()!=b'postgres':
time.sleep(1);continue
ready=subprocess.run(['docker','exec',container,'pg_isready','-U','postgres','-d','recovery'],
stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL,timeout=10)
if ready.returncode==0:break
time.sleep(1)
else:raise Refused()
version=run(['docker','exec',container,'pg_restore','--version']).decode().strip()
with dump.open('rb') as source:
run(['docker','exec','-i',container,'pg_restore','-U','postgres','-d','recovery',
'--no-owner','--no-privileges','--exit-on-error'],stdin=source)
# Fixed aggregate queries only. No identity, resolver config or token values.
counts={}
for table in ('token','resolver','realm'):
raw=run(['docker','exec',container,'psql','-X','-U','postgres','-d','recovery','-At',
'-v','ON_ERROR_STOP=1','-c',f'SELECT count(*) FROM public.{table};']).strip()
if not raw.isdigit():raise Refused()
counts[table]=int(raw)
return {'image':IMAGE,'image_id':image_id,'pg_restore_version':version,'aggregate_row_counts':counts}
def main():
p=argparse.ArgumentParser(description=__doc__)
p.add_argument('--transfer-receipt',required=True);p.add_argument('--receipt',required=True)
a=p.parse_args();os.umask(0o077)
# Reserve the receipt before any recovery action; never overwrite evidence.
fd=os.open(a.receipt,os.O_WRONLY|os.O_CREAT|os.O_EXCL|os.O_NOFOLLOW,0o600)
r={'schema':'netkingdom.privacyidea-database-drill.v1','status':'failed',
'started_at':datetime.now(timezone.utc).isoformat(),'source_sha256':hashlib.sha256(Path(__file__).read_bytes()).hexdigest(),
'production_contacted':False,'secret_values_exported':False,'native_identity_recovery_proven':False,
'token_decryption_proven':False,'audit_signature_continuity_proven':False,
'full_identity_file_coverage_proven':False,'container_cleanup_verified':False,
'temporary_plaintext_cleanup_verified':False}
container='nk49-pi-recovery-'+uuid.uuid4().hex
keyfd=None;work=None
try:
blob,digest=transfer_input(Path(a.transfer_receipt));r['ciphertext_sha256']=digest
keyfd=custody.private_key_fd()
derived=run(['age-keygen','-y',f'/proc/self/fd/{keyfd}'],pass_fds=(keyfd,)).strip()
if derived!=RECIPIENT.encode():raise Refused()
r['registered_recipient_match']=True
with tempfile.TemporaryDirectory(prefix='nk49-pi-drill-',dir='/tmp') as temporary:
work=Path(temporary)
env={'PATH':os.environ.get('PATH',os.defpath),'HOME':temporary,'XDG_CONFIG_HOME':temporary}
wrong=work/'wrong.key'
run(['age-keygen','-o',str(wrong)],env=env)
negative=subprocess.run(['age','-d','-i',str(wrong)],input=blob,stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,timeout=60,env=env)
if negative.returncode==0:raise Refused()
r['wrong_key_refused']=True
dump=work/'privacyidea.dump'
with dump.open('xb') as output:
result=subprocess.run(['age','-d','-i',f'/proc/self/fd/{keyfd}'],input=blob,
stdout=output,stderr=subprocess.DEVNULL,timeout=120,pass_fds=(keyfd,),env=env)
if result.returncode or not 5<dump.stat().st_size<SIZE:raise Refused()
with dump.open('rb') as source:
if source.read(5)!=b'PGDMP':raise Refused()
r['offhost_ciphertext_decryption_verified']=True
try:
r.update(restore(dump,container));r['database_restore_and_readback_passed']=True
finally:
removed=subprocess.run(['docker','rm','--force','--volumes',container],stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,timeout=60)
# A removal error is a failed ceremony even if the restore passed.
if removed.returncode:raise Refused()
absent=subprocess.run(['docker','container','inspect',container],stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,timeout=15)
# Confirm the daemon remains reachable, so inspect failure means absent.
run(['docker','info','--format','{{.ServerVersion}}'])
if absent.returncode==0:raise Refused()
r['container_cleanup_verified']=True
if work.exists():raise Refused()
r['temporary_plaintext_cleanup_verified']=True;r['status']='isolated_database_drill_passed'
except Exception:
r['error']='database_drill_failed_diagnostics_suppressed'
finally:
if keyfd is not None:os.close(keyfd)
if work is not None and not work.exists():r['temporary_plaintext_cleanup_verified']=True
r['finished_at']=datetime.now(timezone.utc).isoformat()
with os.fdopen(fd,'w') as out:json.dump(r,out,indent=2);out.write('\n')
if r['status']=='isolated_database_drill_passed':
print('OK: off-host database decrypted, isolated restore/readback passed, disposable data removed. Full identity recovery remains open.')
return 0
print('ERROR: database drill failed; private diagnostics suppressed. Retain the safe receipt for review.',file=sys.stderr)
return 1
if __name__=='__main__':
for sig in (signal.SIGHUP,signal.SIGTERM,signal.SIGINT):signal.signal(sig,custody.interrupted)
raise SystemExit(main())

View file

@ -0,0 +1,158 @@
{
"schema": "netkingdom.identity-backup-profile.v1",
"status": "implemented_pending_owner_admission",
"owner": "net-kingdom with railiance-platform/rapp-postgres runtime and storage admission",
"public_recipient": "age1zvryunvjhvpkmasskauga2heeg0ztnte9ymgppvjge36ekumk50syr3tsz",
"archive_profile": "netkingdom-identity-v1; new profile, not the bootstrap secrets-only tar profile",
"components": [
{
"namespace": "sso",
"deployment": "lldap",
"deployment_uid": "743a86bd-5cb0-4ca0-9c56-a3438f4746a8",
"pvc_refs": [
{
"name": "lldap-data",
"uid": "0a06b65a-e7b3-42d3-916e-e74dbe5fc7da"
}
],
"secret_refs": [
"lldap-secrets"
],
"configmap_refs": [],
"required_assets": [
"SQLite online-backup snapshot of /data/users.db with integrity check",
"remaining non-backup durable /data files, including any private identity material",
"current deployment configuration and exact lldap-secrets fields"
],
"native_acceptance": [
"directory user/group/membership preservation",
"existing-user password authentication in isolated environment"
]
},
{
"namespace": "sso",
"deployment": "authelia",
"deployment_uid": "dc40a925-2306-4a3b-89c0-e714187503c3",
"pvc_refs": [
{
"name": "authelia-data",
"uid": "86826554-b014-4df4-8b28-866aa64f6413"
}
],
"secret_refs": [
"authelia-secrets",
"authelia-smtp"
],
"configmap_refs": [
"authelia-config"
],
"required_assets": [
"SQLite online-backup snapshot of /var/authelia/data/db.sqlite3 with integrity check",
"current configuration.yml and all Secret-mounted encryption/OIDC/session fields",
"explicit disposition for notifier credentials and remaining durable files"
],
"native_acceptance": [
"storage decryption",
"same OIDC signing identity and preserved client configuration",
"isolated authentication flow with outbound notification disabled"
]
},
{
"namespace": "sso",
"deployment": "keycape",
"deployment_uid": "99ddd83c-cb3f-4847-bcf8-35f1aa87627f",
"pvc_refs": [
{
"name": "keycape-authentication-policy",
"uid": "423f558f-8c11-4edf-956b-243559df4a8e"
}
],
"secret_refs": [
"keycape-approval-engine-operator-client",
"keycape-config",
"keycape-factor-read",
"keycape-informed-decision-sitting-requester-client",
"keycape-rapp-qonto-client",
"keycape-secrets-engine-approval-client",
"keycape-secrets-engine-openbao-client",
"keycape-secrets-engine-requester-client"
],
"configmap_refs": [],
"required_assets": [
"exact current keycape-config config.yaml and key.pem",
"complete stable policy/receipt PVC snapshot",
"current deployment and owner disposition for every referenced credential: encrypted recovery or independently recoverable provider reissue path"
],
"native_acceptance": [
"same issuer/JWKS signing identity",
"registry, authentication policy and receipt continuity",
"current consumer positive/negative authentication under isolated test endpoints"
]
},
{
"namespace": "mfa",
"deployment": "privacyidea",
"deployment_uid": "58c7f96d-61cb-4dd4-bca2-54661c0ac375",
"pvc_refs": [
{
"name": "privacyidea-data",
"uid": "65650fc1-02d8-4c0f-84fc-e0015c8f3ed9"
},
{
"name": "privacyidea-logs",
"uid": "39fa90d9-bc54-4a03-a0d6-ae85165e0cb9"
}
],
"secret_refs": [
"privacyidea-config"
],
"configmap_refs": [
"factor-recovery-01119187d90d7b76",
"keycape-onboarding-9465e5e0b98f7986",
"privacyidea-cfg"
],
"required_assets": [
"original /etc/privacyidea/enckey, private.pem and public.pem with matching RSA pair validation",
"current privacyidea-config and mounted configuration/scripts",
"bounded native PostgreSQL custom dump with exact database identity and key-generation linkage",
"explicit disposition for service logs; audit database/signatures must be preserved"
],
"native_acceptance": [
"decrypt existing retained token material without exporting it",
"verify pre-backup audit signatures and create/verify a new isolated audit entry",
"existing-token MFA and negative-key refusal"
]
}
],
"capture_contract": {
"plaintext": "private task staging outside Git; never process output, State Hub or logs; verified cleanup after encryption",
"consistency": "native SQLite online-backup and PostgreSQL logical snapshot; explicit capture interval per asset; no global atomic snapshot claim; before/after source identities/config versions and directory inventories must match",
"key_policy": "preserve original service keys; no age private key in the payload, no replacement generation, no root/unseal collection",
"encryption": "public-recipient-only streaming archive to age; private fresh ciphertext, no overwrite; manifest inside ciphertext with source identities, times, complete required paths, digests and omissions",
"limits": "measure first; proposed 512 MiB encrypted admission ceiling, no silent truncation/exclusions",
"historical_database": "existing verified October15 dump remains protected; new bundle must link its own exact database/key capture generation and recovery receipt"
},
"proposed_destinations": {
"initial": "Storage Share Backup/railiance-backups/net-kingdom-identity/",
"future_secondary": "s3://railiance-platform-pg-backup/platform-pg/application-archives/net-kingdom/; not implemented/admitted in this rollout",
"admitted": false,
"publisher": "CCR-2026-0048: new folder-only read/create public DAV share, delivered by exact ESO reader; no operator password in runtime; explicitly owner-approved provider-added re-sharing (permissions21)",
"retention": "CCR-2026-0049: separate folder-only read/delete share and process; newest plus seven daily/two weekly and independently recovered pinned generation; unknown/unindexed points never deleted; explicitly owner-approved provider-added re-sharing (permissions25)",
"schedule": "source prepared: daily 03:45 UTC plus up to five minutes jitter, hourly 25-hour freshness check; installation leaves timers disabled until independent archive recovery",
"quota": "512 MiB ceiling, two GiB fresh provider headroom; initial 10 GiB account quota checked; unsupported public-share quota fails closed"
},
"admission_gates": [
"Approve exact CCR-2026-0048/0049 grants and node runtime packet; three required owner reviews, confirmed native SA/namespace/audience boundaries before acceptance",
"Exclude helper capture label from existing broad DNS allow policies; validate read-only helper manifests and cluster/PVC/source bindings before private capture",
"Preserve every referenced credential encrypted with original keys; fresh native SQLite/PG snapshots and exact manifest, with documented omissions and no global atomicity claim",
"Create fresh ciphertext and exact Storage Share object, read back ciphertext and receipt by strong ETag/hash, independently retrieve using operator custody",
"Attended independent age-key proof: wrong-key denial, complete archive hashes, SQLite/RSA checks, disposable native PostgreSQL restore and verified cleanup before timer activation",
"Full NK49 closure separately requires native directory/issuer/MFA authentication, retained token decryption, audit signature continuity, current callers and legacy credential dispositions; archive acceptance does not close those gates",
"Host-local health cannot report a dead host; external outage monitoring and second-provider copy remain separate coverage gaps"
],
"runtime_host": {
"ssh_alias": "railiance01",
"hostname": "239.62.205.92.host.secureserver.net",
"machine_id_sha256": "fe7367a60951922122af7ba18ed2cde65c7e0a232bbedfb045cb575de2094c2f"
}
}

View file

@ -0,0 +1,211 @@
#!/usr/bin/env python3
"""Strict data-bearing identity archive format, native checks and private sealing.
No custody key is used to create a bundle. Its manifest and plaintext digests
stay inside encryption. Public receipts expose only ciphertext provenance.
"""
import base64
from datetime import datetime,timezone
import hashlib
import importlib.util
import io
import json
import os
from pathlib import Path,PurePosixPath
import re
import sqlite3
import stat
import subprocess
import tarfile
import tempfile
ROOT=Path(__file__).resolve().parents[2]
PROFILE=ROOT/'tools/bootstrap-recovery/identity-backup-profile.json'
LIMIT=512*1024*1024
FILE_LIMIT=256*1024*1024
NAME=re.compile(r'[A-Za-z0-9_.-]{1,160}\Z')
class Refused(Exception):pass
def profile():return json.loads(PROFILE.read_text())
def private(path,directory=False,max_bytes=FILE_LIMIT):
if any(p.is_symlink() for p in (path,*path.parents)):raise Refused()
s=path.lstat()
if s.st_uid!=os.getuid() or s.st_mode&0o077:raise Refused()
if directory:
if not stat.S_ISDIR(s.st_mode):raise Refused()
elif not stat.S_ISREG(s.st_mode) or s.st_nlink!=1 or s.st_size>max_bytes:raise Refused()
def allowed(name,profile_data=None):
p=PurePosixPath(name)
if p.is_absolute() or any(not NAME.fullmatch(x) or x in {'.','..'} for x in p.parts):return False
if len(p.parts)>=3 and p.parts[:2] in [('data','lldap'),('data','authelia'),('data','keycape'),('data','privacyidea')]:return True
if name=='databases/privacyidea.dump':return True
for c in (profile_data or profile())['components']:
prefix=f"resources/{c['namespace']}/"
if name==prefix+'deployment/'+c['deployment']+'.json':return True
if any(name==prefix+'secret/'+n+'.json' for n in c['secret_refs']):return True
if any(name==prefix+'configmap/'+n+'.json' for n in c['configmap_refs']):return True
return False
def save(root,name,value,profile_data=None):
if not allowed(name,profile_data) or len(value)>FILE_LIMIT or b'AGE-SECRET-KEY-' in value:raise Refused()
target=root/name;target.parent.mkdir(mode=0o700,parents=True,exist_ok=True)
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(value)
def import_files(raw,root,mode):
names=set();total=0
with tarfile.open(fileobj=io.BytesIO(raw),mode='r:') as archive:
for member in archive:
expected=('data/privacyidea/',) if mode=='mfa' else ('data/lldap/','data/authelia/','data/keycape/')
if (not member.isfile() or not member.name.startswith(expected) or member.name in names
or member.size<0 or member.size>FILE_LIMIT or len(names)>=2048):raise Refused()
names.add(member.name);total+=member.size
if total>LIMIT:raise Refused()
data=archive.extractfile(member).read()
if len(data)!=member.size:raise Refused()
save(root,member.name,data)
def resource(root,ns,kind,name):
p=root/f'resources/{ns}/{kind}/{name}.json'
data=json.loads(p.read_bytes())
if data['metadata']['name']!=name or data['metadata']['namespace']!=ns:raise Refused()
return data
def checks(root,profile_data=None):
# Checks describe stored formats/integrity, not current login validity.
for c in (profile_data or profile())['components']:
deployment=resource(root,c['namespace'],'deployment',c['deployment'])
if deployment.get('kind')!='Deployment' or deployment['metadata']['uid']!=c['deployment_uid']:raise Refused()
for name in c['secret_refs']:
data=resource(root,c['namespace'],'secret',name)
if data.get('kind')!='Secret' or not isinstance(data.get('data'),dict) or not data['data']:raise Refused()
for value in data['data'].values():
if b'AGE-SECRET-KEY-' in base64.b64decode(value,validate=True):raise Refused()
for name in c['configmap_refs']:
if resource(root,c['namespace'],'configmap',name).get('kind')!='ConfigMap':raise Refused()
for name,file in [('lldap','users.db'),('authelia','db.sqlite3')]:
db=root/f'data/{name}/{file}'
with sqlite3.connect(db.as_uri()+'?mode=ro',uri=True) as con:
if con.execute('PRAGMA integrity_check').fetchall()!=[('ok',)]:raise Refused()
with (root/'databases/privacyidea.dump').open('rb') as source:
if source.read(5)!=b'PGDMP':raise Refused()
spec=importlib.util.spec_from_file_location('pi_keys',ROOT/'sso-mfa/k8s/privacyidea/key-custody-remote.py')
keys=importlib.util.module_from_spec(spec);spec.loader.exec_module(keys)
keys.validate({name:(root/'data/privacyidea'/name).read_bytes() for name in ('enckey','private.pem','public.pem')})
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import rsa
config=resource(root,'sso','secret','keycape-config')['data']
if not base64.b64decode(config['config.yaml'],validate=True):raise Refused()
signing=serialization.load_pem_private_key(base64.b64decode(config['key.pem'],validate=True),password=None)
if not isinstance(signing,rsa.RSAPrivateKey) or signing.key_size<2048:raise Refused()
# Policy must exist, even for an empty set of receipts; do not silently
# restore an issuer without its current authentication policy.
policy=json.loads((root/'data/keycape/policy.json').read_bytes())
if not isinstance(policy,dict):raise Refused()
return {'sqlite_integrity_passed':True,'privacyidea_key_format_and_pair_passed':True,
'keycape_signing_key_format_passed':True,'policy_json_present':True,
'database_custom_format_present':True,'native_identity_recovery_proven':False}
def inventory(root):
private(root,True);entries=[];total=0
for path in sorted(root.rglob('*')):
if path.is_dir():private(path,True);continue
private(path);name=path.relative_to(root).as_posix()
if not allowed(name):raise Refused()
with path.open('rb') as source:value=source.read(FILE_LIMIT+1)
if len(value)>FILE_LIMIT or b'AGE-SECRET-KEY-' in value:raise Refused()
total+=len(value)
if total>LIMIT or len(entries)>=4096:raise Refused()
entries.append({'path':name,'bytes':len(value),'sha256':hashlib.sha256(value).hexdigest()})
if not entries:raise Refused()
return entries
def seal(root,output,capture):
output=output.absolute();private(output.parent,True)
if output.exists() or output.is_symlink():raise Refused()
checked=checks(root);entries=inventory(root)
manifest={'schema':'netkingdom.identity-archive.v1','profile_sha256':hashlib.sha256(PROFILE.read_bytes()).hexdigest(),
'profile':profile(),
'captured_at':datetime.now(timezone.utc).isoformat(),'capture':capture,'files':entries,'checks':checked}
with tempfile.TemporaryDirectory(prefix='.identity-seal-',dir=output.parent) as temporary:
ciphertext=Path(temporary)/'ciphertext'
fd=os.open(ciphertext,os.O_WRONLY|os.O_CREAT|os.O_EXCL,0o600)
proc=subprocess.Popen(['age','-r',profile()['public_recipient']],stdin=subprocess.PIPE,stdout=fd,stderr=subprocess.DEVNULL)
os.close(fd)
try:
with tarfile.open(fileobj=proc.stdin,mode='w|') as archive:
raw=json.dumps(manifest,sort_keys=True).encode();m=tarfile.TarInfo('manifest.json');m.size=len(raw);m.mode=0o600
archive.addfile(m,io.BytesIO(raw))
for entry in entries:
with (root/entry['path']).open('rb') as source:
value=source.read(FILE_LIMIT+1)
if hashlib.sha256(value).hexdigest()!=entry['sha256']:raise Refused()
m=tarfile.TarInfo(entry['path']);m.size=len(value);m.mode=0o600
archive.addfile(m,io.BytesIO(value))
proc.stdin.close()
if proc.wait(timeout=120) or not 0<ciphertext.stat().st_size<=LIMIT:raise Refused()
# Check source completeness after encryption too; changed/new files
# refuse publication. This is not same-UID adversary isolation.
if inventory(root)!=entries:raise Refused()
with ciphertext.open('rb') as source:
os.fsync(source.fileno());digest=hashlib.file_digest(source,'sha256').hexdigest()
os.link(ciphertext,output)
finally:
if proc.poll() is None:proc.kill();proc.wait()
return {'schema':'netkingdom.identity-capture.v1','status':'encrypted_bundle_created',
'ciphertext_sha256':digest,'ciphertext_bytes':output.stat().st_size,'ciphertext_path':str(output),
'profile_sha256':manifest['profile_sha256'],'file_count':len(entries),
'checks':checked,'offsite_recovery_proven':False,'secret_values_exported':False}
def restore(archive_path,root):
private(root,True)
with tarfile.open(archive_path,mode='r:') as archive:
first=archive.next()
if first is None or first.name!='manifest.json' or not first.isfile() or not 0<first.size<2*1024*1024:raise Refused()
manifest=json.load(archive.extractfile(first))
if manifest.get('schema')!='netkingdom.identity-archive.v1':raise Refused()
captured=manifest['profile']
if captured.get('schema')!='netkingdom.identity-backup-profile.v1':raise Refused()
if len(captured['components'])!=4:raise Refused()
if {(c['namespace'],c['deployment']) for c in captured['components']}!={('sso','lldap'),('sso','authelia'),('sso','keycape'),('mfa','privacyidea')}:raise Refused()
for c in captured['components']:
for key in ('secret_refs','configmap_refs'):
if len(set(c[key]))!=len(c[key]) or any(not NAME.fullmatch(n) or n in {'.','..'} for n in c[key]):raise Refused()
entries=manifest['files'];expected={e['path']:e for e in entries}
if len(expected)!=len(entries) or len(entries)>4096:raise Refused()
seen=set();total=0
# Iteration starts with the already-read first member; explicitly skip
# only that member. A later duplicate manifest is never accepted.
for index,member in enumerate(archive):
if index==0 and member is first:continue
name=member.name
if (not member.isfile() or name in seen or name not in expected or not allowed(name,captured)
or not 0<=member.size<=FILE_LIMIT or member.size!=expected[name]['bytes']):raise Refused()
seen.add(name);total+=member.size
if total>LIMIT:raise Refused()
raw=archive.extractfile(member).read()
if len(raw)!=member.size or hashlib.sha256(raw).hexdigest()!=expected[name]['sha256']:raise Refused()
save(root,name,raw,captured)
if seen!=set(expected):raise Refused()
return manifest,checks(root,captured)
def require_runtime_host():
import socket
host=profile()['runtime_host']
if (os.geteuid()!=0 or socket.gethostname()!=host['hostname']
or hashlib.sha256(Path('/etc/machine-id').read_bytes()).hexdigest()!=host['machine_id_sha256']):raise Refused()

View file

@ -0,0 +1,124 @@
#!/usr/bin/env python3
"""Folder-scoped Storage Share ciphertext publication and verified readback.
Runtime gets only a dedicated read/create share token. Account administration,
deletion and age private keys have no route in this publisher.
"""
import base64
from datetime import datetime,timezone
import hashlib
import json
import os
from pathlib import Path
import re
import tempfile
import urllib.error
import urllib.request
import xml.etree.ElementTree as ET
HOST='https://nx4069.your-storageshare.de'
BASE=HOST+'/public.php/webdav/'
NAME=re.compile(r'net-kingdom-identity-\d{8}T\d{6}Z-[a-f0-9]{12}\.tar\.age\Z')
LIMIT=512*1024*1024
HEADROOM=2*1024**3
class Refused(Exception):pass
class NoRedirect(urllib.request.HTTPRedirectHandler):
def redirect_request(self,*args,**kwargs):raise Refused()
def opener():return urllib.request.build_opener(urllib.request.ProxyHandler({}),NoRedirect())
def headers(token):
if not isinstance(token,str) or not re.fullmatch('[A-Za-z0-9]{10,128}',token):raise Refused()
return {'Authorization':'Basic '+base64.b64encode((token+':').encode()).decode(),
'X-Requested-With':'XMLHttpRequest','Accept-Encoding':'identity'}
def digest(path):
with path.open('rb') as source:return hashlib.file_digest(source,'sha256').hexdigest()
def quota(client,token):
body=b'<d:propfind xmlns:d="DAV:"><d:prop><d:quota-available-bytes/></d:prop></d:propfind>'
h=headers(token);h.update(Depth='0');h['Content-Type']='application/xml'
with client.open(urllib.request.Request(BASE,data=body,method='PROPFIND',headers=h),timeout=30) as response:
if response.status!=207 or response.url!=BASE:raise Refused()
raw=response.read(1024*1024+1)
if len(raw)>1024*1024:raise Refused()
amount=ET.fromstring(raw).findtext('.//{DAV:}quota-available-bytes') or ''
if not amount.isdigit():raise Refused()
return int(amount)
def etag(response,url,size):
value=response.headers.get('ETag','')
if (response.status!=200 or response.url!=url or response.headers.get('Content-Length')!=str(size)
or not re.fullmatch(r'"[^"\x00-\x20]{1,180}"',value)):raise Refused()
return value
def readback(client,token,name,size,expected,target):
valid=NAME.fullmatch(name) or (name.endswith('.receipt.json') and NAME.fullmatch(name.removesuffix('.receipt.json')) and size<=65536)
if not valid or not 0<size<=LIMIT:raise Refused()
url=BASE+name;h=headers(token)
with client.open(urllib.request.Request(url,method='HEAD',headers=h),timeout=30) as response:version=etag(response,url,size)
h['If-Match']=version;count=0;sha=hashlib.sha256()
with client.open(urllib.request.Request(url,headers=h),timeout=180) as response:
if etag(response,url,size)!=version:raise Refused()
fd=os.open(target,os.O_WRONLY|os.O_CREAT|os.O_EXCL|os.O_NOFOLLOW,0o600)
with os.fdopen(fd,'wb') as out:
while block:=response.read(1024*1024):
count+=len(block)
if count>size:raise Refused()
sha.update(block);out.write(block)
out.flush();os.fsync(out.fileno())
if count!=size or sha.hexdigest()!=expected:raise Refused()
with client.open(urllib.request.Request(url,method='HEAD',headers=h),timeout=30) as response:
if etag(response,url,size)!=version:raise Refused()
return version
def publish(client,token,source,capture,target):
if (not NAME.fullmatch(source.name) or source.is_symlink() or source.stat().st_mode&0o077
or source.stat().st_uid!=os.getuid() or source.stat().st_nlink!=1 or not source.is_file()
or source.stat().st_size>LIMIT or capture.get('status')!='encrypted_bundle_created'
or capture.get('helper_cleanup_verified') is not True or capture.get('temporary_plaintext_cleanup_verified') is not True
or digest(source)!=capture.get('ciphertext_sha256') or source.stat().st_size!=capture.get('ciphertext_bytes')):raise Refused()
with source.open('rb') as stream:
if stream.read(22)!=b'age-encryption.org/v1\n':raise Refused()
available=quota(client,token)
if available<source.stat().st_size+HEADROOM:raise Refused()
url=BASE+source.name;h=headers(token)
try:
with client.open(urllib.request.Request(url,method='HEAD',headers=h),timeout=30):raise Refused()
except urllib.error.HTTPError as error:
if error.code!=404:raise Refused() from None
h.update({'If-None-Match':'*','Content-Type':'application/octet-stream','Content-Length':str(source.stat().st_size)})
# An unknown upload outcome is never retried under this name automatically.
with source.open('rb') as stream:
with client.open(urllib.request.Request(url,data=stream,method='PUT',headers=h),timeout=600) as response:
if response.status!=201 or response.url!=url:raise Refused()
version=readback(client,token,source.name,source.stat().st_size,capture['ciphertext_sha256'],target)
index={'schema':'netkingdom.identity-offsite-index.v1','status':'readback_verified',
'artifact':source.name,'ciphertext_sha256':capture['ciphertext_sha256'],
'ciphertext_bytes':source.stat().st_size,'captured_at':capture['finished_at']}
raw=json.dumps(index,sort_keys=True).encode();index_name=source.name+'.receipt.json';index_url=BASE+index_name
h=headers(token);h.update({'If-None-Match':'*','Content-Type':'application/json','Content-Length':str(len(raw))})
with client.open(urllib.request.Request(index_url,data=raw,method='PUT',headers=h),timeout=30) as response:
if response.status!=201 or response.url!=index_url:raise Refused()
with tempfile.TemporaryDirectory(prefix='.identity-index-',dir=target.parent) as temporary:
readback(client,token,index_name,len(raw),hashlib.sha256(raw).hexdigest(),Path(temporary)/'index.json')
return {'schema':'netkingdom.identity-offsite.v1','status':'readback_verified',
'observed_at':datetime.now(timezone.utc).isoformat(),'provider':'Storage Share',
'folder':'Backup/railiance-backups/net-kingdom-identity','artifact':source.name,
'ciphertext_sha256':capture['ciphertext_sha256'],'ciphertext_bytes':source.stat().st_size,
'fetched_path':str(target),'etag':version,'quota_available_before':available,
'new_object_created':True,'downloaded_digest_verified':True,
'verified_index_published':True,'index_name':index_name,
'age_private_key_used':False,'secret_values_exported':False,'retained_objects_deleted':False,
'native_identity_recovery_proven':False}

View file

@ -0,0 +1,487 @@
#!/usr/bin/env python3
"""Owner-run T18 session assembly using the existing archive/containment pipeline.
Candidate only: native pinned-image tests and review are required before use.
Shared Docker local-driver tmpfs is required because Docker cp does not support
private tmpfs/mount paths, and the original KeyCape image is distroless.
"""
import argparse
import base64
from datetime import datetime, timezone
import hashlib
import importlib.util
import json
import os
from pathlib import Path
import signal
import re
import stat
import select
import subprocess
import sys
import tempfile
import time
import uuid
HERE=Path(__file__).resolve().parent
def load(name):
spec=importlib.util.spec_from_file_location(name,HERE/(name+'.py'))
module=importlib.util.module_from_spec(spec);spec.loader.exec_module(module);return module
adapter=load('isolated-keycape')
local=load('isolated-lldap')
bundle=local.bundle
# Bind the reused preflight itself, not only this module's direct Docker calls.
local.command=adapter.command
_original_preflight=local.preflight
def preflight():
contexts=json.loads(adapter.command(['docker','context','inspect']))
if len(contexts)!=1 or contexts[0]['Endpoints']['docker']['Host']!=adapter.ENDPOINT:raise adapter.Refused()
result=_original_preflight();result['docker_endpoint']=adapter.ENDPOINT;return result
local.preflight=preflight
WORKER=local.CLIENT
STAGING_ROOT=Path('/dev/shm')
RUNROOTS=('/etc/keycape','/var/lib/keycape-policy','/etc/keycape-factor','/recovery-tls')
VOLUME_OPTIONS={'type':'tmpfs','device':'tmpfs','o':'size=33554432,uid=1000,gid=1000,mode=0700,nosuid,nodev,noexec'}
IMPORT=r'''
import base64,json,os,sys
from pathlib import Path
os.umask(0o077)
p=json.load(sys.stdin)
allowed=('/etc/keycape/','/var/lib/keycape-policy/','/etc/keycape-factor/','/recovery-tls/')
for name,encoded in p.items():
f=Path(name)
if not name.startswith(allowed) or '..' in f.parts or any(x.is_symlink() for x in (f,*f.parents)):raise ValueError()
f.parent.mkdir(mode=0o700,parents=True,exist_ok=True)
fd=os.open(f,os.O_WRONLY|os.O_CREAT|os.O_EXCL|os.O_NOFOLLOW,0o600)
with os.fdopen(fd,'wb') as out:out.write(base64.b64decode(encoded,validate=True))
'''
# Small transport inside the already pinned helper; it has only loopback routes.
# No token handling, auth bypass, dependency imitation, reset or notifier path.
TLS=r'''
import select,socket,socketserver,ssl,threading
context=ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
context.minimum_version=ssl.TLSVersion.TLSv1_2
context.load_cert_chain('/recovery-tls/server.pem','/recovery-tls/server-key.pem')
class Handler(socketserver.BaseRequestHandler):
def handle(self):
try:
with context.wrap_socket(self.request,server_side=True) as incoming:
with socket.create_connection(('127.0.0.1',8080),timeout=10) as outgoing:
incoming.settimeout(15);outgoing.settimeout(15)
while True:
ready,_,_=select.select([incoming,outgoing],[],[],15)
if not ready:return
for source in ready:
data=source.recv(65536)
if not data:return
(outgoing if source is incoming else incoming).sendall(data)
except Exception:pass
class Server(socketserver.ThreadingTCPServer):
allow_reuse_address=True
daemon_threads=True
with Server(('127.0.0.1',443),Handler) as server:server.serve_forever()
'''
def run(argv,**kwargs):return adapter.command(argv,**kwargs)
def plan_session():
run_id=uuid.uuid4().hex
stem='nk49-kc-'+run_id
worker=stem+'-transport';issuer=stem+'-issuer'
volumes={stem+'-v'+str(i):{'target':root,'options':dict(VOLUME_OPTIONS)} for i,root in enumerate(RUNROOTS)}
return {'run_id':run_id,'containers':[worker,issuer], 'worker':worker,'issuer':issuer,
'images':{worker:WORKER,issuer:adapter.IMAGE},'volumes':volumes,
'public_image_probe':stem+'-image-probe','docker_endpoint':adapter.ENDPOINT}
def verify_volume(name,spec,run_id):
obj=json.loads(run(['docker','volume','inspect',name]))
if (len(obj)!=1 or obj[0]['Name']!=name or obj[0]['Driver']!='local' or obj[0].get('Scope')!='local'
or obj[0].get('Options')!=VOLUME_OPTIONS or spec['options']!=VOLUME_OPTIONS
or obj[0].get('Labels',{}).get(adapter.LABEL)!=run_id):raise adapter.Refused()
def base_args(session,name,network):
return ['docker','run','--detach','--pull=never','--name',name,'--network',network,
'--label',adapter.LABEL+'='+session['run_id'],'--read-only','--cap-drop','ALL',
'--security-opt','no-new-privileges','--user','1000:1000','--memory','512m','--cpus','1',
'--pids-limit','64','--log-driver','none','--restart','no',
'--tmpfs','/tmp:rw,nosuid,nodev,noexec,size=16777216,uid=1000,gid=1000,mode=0700']
def mounts(session,readonly=False):
args=[]
for name,spec in session['volumes'].items():
args+=['--mount','type=volume,source='+name+',target='+spec['target']+(',readonly' if readonly else '')]
return args
def provision(session,payload,tls,issuer):
"""Inspect containment and driver options before the first private import."""
from urllib.parse import urlsplit
address=urlsplit(issuer)
if address.scheme!='https' or not address.hostname or address.port not in (None,443) or address.path not in ('','/'):
raise adapter.Refused()
for name,spec in session['volumes'].items():
run(['docker','volume','create','--driver','local','--label',adapter.LABEL+'='+session['run_id'],
'--opt','type=tmpfs','--opt','device=tmpfs','--opt','o='+VOLUME_OPTIONS['o'],name])
verify_volume(name,spec,session['run_id'])
args=base_args(session,session['worker'],'none')+mounts(session)
args+=['--sysctl','net.ipv4.ip_unprivileged_port_start=0','--add-host',address.hostname+':127.0.0.1',
'--entrypoint','python3',WORKER,'-c','import time;time.sleep(1800)']
run(args)
# Check helper and every private filesystem before touching the ciphertext's
# private contents. Issuer is created only after originals are present.
adapter.inspect_session({**session,'containers':[session['worker']],'issuer':session['worker']},worker_only=True)
files={**payload['files'],**{k:base64.b64encode(v).decode() for k,v in tls.items()}}
run(['docker','exec','-i',session['worker'],'python3','-c',IMPORT],input=json.dumps(files).encode())
anchor=run(['docker','container','inspect',session['worker'],'--format','{{.Id}}']).decode().strip()
args=base_args(session,session['issuer'],'container:'+anchor)+mounts(session,readonly=True)
# CLI argv carries environment names only. Original credentials stay in the
# owner subprocess environment and Docker's private container configuration.
env={'PATH':os.environ.get('PATH',os.defpath),'DOCKER_HOST':'unix:///var/run/docker.sock',**payload['env'],
'SSL_CERT_FILE':'/recovery-tls/trust.pem'}
for name in (*payload['env'],'SSL_CERT_FILE'):args+=['--env',name]
args+=[adapter.IMAGE]
run(args,env=env)
adapter.inspect_session(session)
run(['docker','exec','--detach',session['worker'],'python3','-c',TLS])
def cleanup_all(session,receipt):
failed=False
try:adapter.cleanup({**session,'containers':session['containers']+[session['public_image_probe']]},receipt)
except Exception:failed=True
rows=[]
for name,spec in session['volumes'].items():
row={'volume':name,'status':'unverified'};rows.append(row)
try:
exists=run(['docker','volume','ls','--filter','name=^'+name+'$','--format','{{.Name}}']).decode().splitlines()
if not exists:
row['status']='absence_verified'
continue
verify_volume(name,spec,session['run_id'])
run(['docker','volume','rm',name])
remaining=run(['docker','volume','ls','--filter','name=^'+name+'$','--format','{{.Name}}']).decode().splitlines()
if remaining:row['status']='still_present';failed=True
else:row['status']='absence_verified'
except Exception:failed=True
receipt['tmpfs_volume_cleanup_obligations']=rows
receipt['container_cleanup_verified']=not failed
if failed:raise adapter.Refused()
def register_workdir(work, run_id):
if work.parent!=STAGING_ROOT or not re.fullmatch('nk49-keycape-'+run_id+'-[a-z0-9_]{8}',work.name):raise adapter.Refused()
if any(p.is_symlink() for p in (work,*work.parents)):raise adapter.Refused()
info=work.lstat()
if not stat.S_ISDIR(info.st_mode) or info.st_uid!=os.getuid() or stat.S_IMODE(info.st_mode)!=0o700:raise adapter.Refused()
return {'path':str(work),'run_id':run_id,'device':info.st_dev,'inode':info.st_ino,'owner_uid':info.st_uid}
def cleanup_workdir(staging, receipt):
"""Delete only the registered inode below actual /dev/shm, never a replacement."""
row={'path':staging['path'],'device':staging['device'],'inode':staging['inode'],'status':'unverified'}
receipt['plaintext_cleanup_obligation']=row
work=Path(staging['path'])
if work.parent!=STAGING_ROOT or not re.fullmatch('nk49-keycape-'+staging['run_id']+'-[a-z0-9_]{8}',work.name):raise adapter.Refused()
if not any(line.split()[1:3]==['/dev/shm','tmpfs'] for line in Path('/proc/mounts').read_text().splitlines()):raise adapter.Refused()
if any(p.is_symlink() for p in work.parents):raise adapter.Refused()
parentfd=os.open(work.parent,os.O_RDONLY|os.O_DIRECTORY|os.O_NOFOLLOW)
try:
try:info=os.stat(work.name,dir_fd=parentfd,follow_symlinks=False)
except FileNotFoundError:
row['status']='absence_verified';receipt['temporary_plaintext_cleanup_verified']=True;return
if (not stat.S_ISDIR(info.st_mode) or info.st_uid!=staging['owner_uid'] or info.st_uid!=os.getuid()
or info.st_dev!=staging['device'] or info.st_ino!=staging['inode'] or stat.S_IMODE(info.st_mode)!=0o700):
row['status']='identity_mismatch_not_removed';raise adapter.Refused()
def remove(parent,name,expected):
fd=os.open(name,os.O_RDONLY|os.O_DIRECTORY|os.O_NOFOLLOW,dir_fd=parent)
try:
current=os.fstat(fd)
if (current.st_dev,current.st_ino)!=(expected.st_dev,expected.st_ino):raise adapter.Refused()
for child in os.listdir(fd):
before=os.stat(child,dir_fd=fd,follow_symlinks=False)
if before.st_uid!=staging['owner_uid'] or before.st_dev!=staging['device']:raise adapter.Refused()
if stat.S_ISDIR(before.st_mode):remove(fd,child,before)
elif stat.S_ISREG(before.st_mode) and before.st_nlink==1:os.unlink(child,dir_fd=fd)
else:raise adapter.Refused() # symlinks/special files/hardlinks are uncertain, never followed
finally:os.close(fd)
after=os.stat(name,dir_fd=parent,follow_symlinks=False)
if (after.st_dev,after.st_ino)!=(expected.st_dev,expected.st_ino):raise adapter.Refused()
os.rmdir(name,dir_fd=parent)
remove(parentfd,work.name,info)
try:os.stat(work.name,dir_fd=parentfd,follow_symlinks=False)
except FileNotFoundError:
row['status']='absence_verified';receipt['temporary_plaintext_cleanup_verified']=True;return
raise adapter.Refused()
finally:os.close(parentfd)
def recover_obligations(session, staging, receipt):
failed=False
try:cleanup_all(session,receipt)
except Exception:failed=True
# The plaintext obligation is attempted even when Docker is unreachable.
try:cleanup_workdir(staging,receipt)
except Exception:failed=True
if failed:raise adapter.Refused()
def process_facts(pid):
raw=Path('/proc')/str(pid)/'stat'
try:
fields=raw.read_text().rsplit(')',1)[1].split()
return {'pid':pid,'state':fields[0],'pgid':int(fields[2]),'sid':int(fields[3]),
'start_ticks':int(fields[19]),'uid':raw.parent.stat().st_uid}
except FileNotFoundError:return None
def isolate_execution():
# Called by main before reading TLS/key inputs. Never reuse an inherited
# shell, custody-helper, user or unrelated process group.
os.setsid()
pid=os.getpid();facts=process_facts(pid)
if os.getsid(0)!=pid or os.getpgrp()!=pid or facts['sid']!=pid or facts['pgid']!=pid:raise adapter.Refused()
return {k:facts[k] for k in ('pid','pgid','sid','start_ticks','uid')}|{'deadline':time.monotonic()+1800}
def verify_execution(execution):
if execution['pid']!=execution['sid'] or execution['pid']!=execution['pgid'] or execution['uid']!=os.getuid():raise adapter.Refused()
current=process_facts(execution['pid'])
if current is not None and any(current[k]!=execution[k] for k in ('pid','pgid','sid','start_ticks','uid')):
# Numeric leader PID reuse is not authority to signal a new group.
raise adapter.Refused()
def group_members(execution):
verify_execution(execution)
members=[]
for path in Path('/proc').iterdir():
if not path.name.isdecimal():continue
facts=process_facts(int(path.name))
if facts is None or facts['pgid']!=execution['pgid']:continue
if facts['sid']!=execution['sid'] or facts['uid']!=execution['uid'] or facts['start_ticks']<execution['start_ticks']:raise adapter.Refused()
if facts['state'] not in ('Z','X'):members.append(facts)
return members
def pidfd_dead(pidfd):
poller=select.poll();poller.register(pidfd,select.POLLIN)
return bool(poller.poll(0))
def quiesce_execution(execution,parent_pidfd,receipt):
"""Stop only this fresh execution session; cleanup needs no live creator.
Exact parent pidfd prevents leader reuse. Each remaining session member gets
its own pidfd after metadata comparison; no inherited/user-group kill occurs.
Unknown membership or uninterruptible survivors refuse cleanup altogether.
"""
verify_execution(execution)
if not pidfd_dead(parent_pidfd):
signal.pidfd_send_signal(parent_pidfd,signal.SIGTERM)
poller=select.poll();poller.register(parent_pidfd,select.POLLIN);poller.poll(2000)
if not pidfd_dead(parent_pidfd):signal.pidfd_send_signal(parent_pidfd,signal.SIGKILL)
for attempt in range(50):
members=group_members(execution)
if pidfd_dead(parent_pidfd) and not members:
receipt['execution_quiescence_verified']=True;return
for original in members:
try:fd=os.pidfd_open(original['pid'])
except ProcessLookupError:continue
try:
current=process_facts(original['pid'])
if current is None or pidfd_dead(fd):continue
if current!=original: # state may naturally change; bind stable identity below
if any(current[k]!=original[k] for k in ('pid','pgid','sid','start_ticks','uid')):raise adapter.Refused()
verify_execution(execution)
signal.pidfd_send_signal(fd,signal.SIGKILL)
except ProcessLookupError:pass
finally:os.close(fd)
time.sleep(.05)
receipt['execution_quiescence_verified']=False
raise adapter.Refused()
def daemon_uncertain(staging):
return any(Path(staging['path']+suffix).exists() or Path(staging['path']+suffix).is_symlink() for suffix in ('-docker-inflight','-docker-uncertain'))
def reaper_main(session,staging,execution,parent_pidfd):
receipt={'status':'failed','execution_quiescence_verified':False}
verify_execution(execution)
if pidfd_dead(parent_pidfd):raise adapter.Refused()
print('registered',flush=True)
poller=select.poll();poller.register(parent_pidfd,select.POLLIN)
remaining=max(0,int((execution['deadline']-time.monotonic())*1000))
parent_lost=bool(poller.poll(remaining))
receipt['cleanup_trigger']='parent_exit' if parent_lost else 'execution_deadline'
try:
# No deletion races a live creator: first stop/wait this exact owner
# session and its children. A daemon request already accepted can still
# complete after CLI loss; retain that uncertainty even after readback.
quiesce_execution(execution,parent_pidfd,receipt)
uncertain=daemon_uncertain(staging)
receipt['in_flight_daemon_request_uncertain']=uncertain
recover_obligations(session,staging,receipt)
receipt['status']='bounded_cleanup_uncertain' if uncertain else 'bounded_cleanup_verified'
except Exception:receipt['status']='bounded_cleanup_uncertain'
finally:
os.close(parent_pidfd)
try:
fd=os.open(staging['path']+'-reaper.json',os.O_WRONLY|os.O_CREAT|os.O_EXCL|os.O_NOFOLLOW,0o600)
with os.fdopen(fd,'w') as out:json.dump(receipt,out)
except Exception:pass
def start_reaper(session, staging, execution):
adapter.docker_environment()
verify_execution(execution)
if session.get('docker_endpoint')!=adapter.ENDPOINT or staging.get('run_id')!=session['run_id']:raise adapter.Refused()
if register_workdir(Path(staging['path']),session['run_id'])!=staging:raise adapter.Refused()
parent_pidfd=os.pidfd_open(execution['pid'])
code="""import importlib.util,json,sys
spec=importlib.util.spec_from_file_location('session',sys.argv[1]);m=importlib.util.module_from_spec(spec);spec.loader.exec_module(m)
m.reaper_main(json.loads(sys.argv[2]),json.loads(sys.argv[3]),json.loads(sys.argv[4]),int(sys.argv[5]))
"""
try:
child=subprocess.Popen([sys.executable,'-c',code,str(Path(__file__).resolve()),json.dumps(session),json.dumps(staging),json.dumps(execution),str(parent_pidfd)],
stdin=subprocess.DEVNULL,stdout=subprocess.PIPE,stderr=subprocess.DEVNULL,
env={'PATH':os.environ.get('PATH',os.defpath),'DOCKER_HOST':adapter.ENDPOINT},
close_fds=True,pass_fds=(parent_pidfd,),start_new_session=True)
finally:os.close(parent_pidfd)
ready,_,_=select.select([child.stdout],[],[],10)
if not ready or child.stdout.readline()!=b'registered\n':
child.terminate();child.wait(timeout=5);raise adapter.Refused()
child.stdout.close()
return child
def execute(transfer,admission,tls,receipt,execution,checkpoint=None):
"""Existing private custody/transfer/archive APIs; no second archive format."""
verify_execution(execution)
if execution['pid']!=os.getpid():raise adapter.Refused()
target=local.preflight()
for image in (adapter.IMAGE,):run(['docker','image','inspect',image])
local.offline.validate(transfer)
if transfer.get('independent_owner_retrieval_verified') is not True:raise adapter.Refused()
path=Path(transfer['fetched_path']);bundle.private(path,max_bytes=bundle.LIMIT)
if (path.stat().st_size!=transfer['ciphertext_bytes'] or local.offline.offsite.digest(path)!=transfer['ciphertext_sha256']
or admission['ciphertext_sha256']!=transfer['ciphertext_sha256']):raise adapter.Refused()
if hashlib.sha256(tls['/recovery-tls/trust.pem']).hexdigest()!=admission['trust_sha256']:raise adapter.Refused()
if hashlib.sha256(tls['/recovery-tls/server.pem']).hexdigest()!=admission['tls_cert_sha256']:raise adapter.Refused()
# Validate exact private certificate/key pair and original issuer name before
# opening service containers; do not generate replacement trust or keys.
from cryptography import x509
from cryptography.hazmat.primitives import serialization
cert=x509.load_pem_x509_certificate(tls['/recovery-tls/server.pem'])
key=serialization.load_pem_private_key(tls['/recovery-tls/server-key.pem'],password=None)
if cert.public_key().public_numbers()!=key.public_key().public_numbers():raise adapter.Refused()
keyfd=local.verify.custody.private_key_fd();work=None;session=None;reaper=None;staging=None
receipt.update(status='failed',target=target,session_source_sha256=hashlib.sha256(Path(__file__).read_bytes()).hexdigest(),
source_files_sha256={name:hashlib.sha256((HERE/name).read_bytes()).hexdigest() for name in ('isolated-keycape.py','isolated-keycape-session.py','run-isolated-keycape.sh','isolated-lldap.py','identity-bundle.py','verify-identity.py','retrieve-identity-offline.py','identity-offsite.py')},
temporary_plaintext_cleanup_verified=False,container_cleanup_verified=False,
downstream_consumer_reconnect_proven=False,human_mfa_proven=False)
try:
if run(['age-keygen','-y',f'/proc/self/fd/{keyfd}'],pass_fds=(keyfd,)).strip()!=bundle.profile()['public_recipient'].encode():raise adapter.Refused()
session=plan_session()
work=Path(tempfile.mkdtemp(prefix='nk49-keycape-'+session['run_id']+'-',dir='/dev/shm'))
staging=register_workdir(work,session['run_id'])
reaper=start_reaper(session,staging,execution)
adapter.EXECUTION={**execution,'staging':str(work)}
receipt.update(execution_binding=execution,bounded_cleanup_reaper_started=True,plaintext_cleanup_obligation={**staging,'status':'registered_before_plaintext'},docker_endpoint=adapter.ENDPOINT,
reaper_outcome_path=staging['path']+'-reaper.json',
registered_cleanup={'containers':session['containers']+[session['public_image_probe']],'tmpfs_volumes':list(session['volumes'])})
if checkpoint is not None:checkpoint(receipt)
archive=work/'archive.tar'
with archive.open('xb') as output:
p=adapter.process(['age','-d','-i',f'/proc/self/fd/{keyfd}',str(path)],stdout=output,stderr=subprocess.DEVNULL,pass_fds=(keyfd,),timeout=180)
if p.returncode or archive.stat().st_size>bundle.LIMIT+4*1024*1024:raise adapter.Refused()
root=work/'restored';root.mkdir(mode=0o700)
manifest,_=bundle.restore(archive,root)
payload=adapter.private_payload(root,manifest,admission['manifest_sha256'])
import yaml
cfg=yaml.safe_load(base64.b64decode(payload['files']['/etc/keycape/config.yaml']))
if cfg.get('port')!=8080 or cfg.get('issuer')!=admission['issuer']:raise adapter.Refused()
# Public executable readback uses the daemon archive API only on the
# original image rootfs, never a tmpfs or private user-created mount.
probe=session['public_image_probe']
try:
run(['docker','create','--name',probe,'--network','none','--read-only','--label',adapter.LABEL+'='+session['run_id'],adapter.IMAGE])
binary=adapter.container_file(probe,'/keycape')
if hashlib.sha256(binary).hexdigest()!=admission['verifier_sha256']:raise adapter.Refused()
finally:
receipt['public_image_probe_cleanup_obligations']=[{'container':probe,'status':'unverified'}]
run(['docker','rm',probe])
if run(['docker','container','ls','--all','--filter','name=^/'+probe+'$','--format','{{.Names}}']).strip():raise adapter.Refused()
receipt['public_image_probe_cleanup_obligations'][0]['status']='absence_verified'
provision(session,payload,tls,admission['issuer'])
receipt.update(ciphertext_sha256=transfer['ciphertext_sha256'],all_manifest_hashes_verified=True,manifest_file_count=len(manifest['files']))
checked_admission={**admission,'session':session,'trust_path':'/recovery-tls/trust.pem'}
# Native checker consumes no service secret argument or new grant.
# Retry only bounded public readiness; no account/factor attempt.
for attempt in range(40):
try:
numbers=serialization.load_pem_private_key(base64.b64decode(payload['files']['/etc/keycape/key.pem']),password=None).public_key().public_numbers()
adapter.check_jwks(session,checked_admission,numbers);break
except Exception:
if attempt==39:raise
time.sleep(.25)
adapter.native_checks(root,manifest,checked_admission,receipt,cleanup_session=False)
finally:
os.close(keyfd)
try:
if session is not None and staging is not None:
uncertain=daemon_uncertain(staging)
receipt['in_flight_daemon_request_uncertain']=uncertain
recover_obligations(session,staging,receipt)
if uncertain:raise adapter.Refused()
if reaper is not None:
reaper.terminate();reaper.wait(timeout=10)
except Exception:
receipt['status']='failed'
raise
finally:
adapter.EXECUTION=None
if work is not None and not work.exists():receipt['temporary_plaintext_cleanup_verified']=True
def persist_receipt(fd,receipt):
raw=(json.dumps(receipt,indent=2)+'\n').encode()
os.lseek(fd,0,os.SEEK_SET);os.ftruncate(fd,0)
with os.fdopen(os.dup(fd),'wb') as output:
output.write(raw);output.flush();os.fsync(output.fileno())
def main():
p=argparse.ArgumentParser(description=__doc__)
for name in ('transfer-receipt','admission','admission-sha256','tls-cert','tls-key','trust','receipt'):p.add_argument('--'+name,required=True)
args=p.parse_args();os.umask(0o077)
fd=os.open(args.receipt,os.O_WRONLY|os.O_CREAT|os.O_EXCL|os.O_NOFOLLOW,0o600)
receipt={'schema':'netkingdom.isolated-keycape-recovery.v1','status':'failed','production_contacted':False,'secret_values_exported':False}
try:
execution=isolate_execution()
raw=Path(args.admission).read_bytes()
if hashlib.sha256(raw).hexdigest()!=args.admission_sha256:raise adapter.Refused()
admission=json.loads(raw)
tls={}
for name,path in (('server.pem',args.tls_cert),('server-key.pem',args.tls_key),('trust.pem',args.trust)):
source=Path(path);bundle.private(source,max_bytes=1024*1024);tls['/recovery-tls/'+name]=source.read_bytes()
execute(json.loads(Path(args.transfer_receipt).read_text()),admission,tls,receipt,execution,checkpoint=lambda r:persist_receipt(fd,r))
except (Exception,KeyboardInterrupt):receipt.update(status='failed',error='isolated_keycape_failed_private_diagnostics_suppressed')
finally:
receipt['finished_at']=datetime.now(timezone.utc).isoformat()
try:persist_receipt(fd,receipt)
finally:os.close(fd)
print('OK: isolated KeyCape native checks and cleanup passed.' if receipt['status']=='isolated_keycape_native_registration_checked' else 'ERROR: isolated KeyCape stopped; review safe receipt.')
return int(receipt['status']!='isolated_keycape_native_registration_checked')
if __name__=='__main__':
for sig in (signal.SIGHUP,signal.SIGTERM,signal.SIGINT):signal.signal(sig,local.offline.interrupted)
raise SystemExit(main())

View file

@ -0,0 +1,441 @@
#!/usr/bin/env python3
"""T18 native adapter for an owner-admitted, already restored isolated session.
Called after identity-bundle.restore, never with an unvalidated directory. This
module does not provision TLS, grant credentials, or claim downstream acceptance.
The existing decrypt/tmpfs wrapper remains responsible for archive cleanup.
"""
import base64
import hashlib
import importlib.util
import json
import io
import os
import secrets
import tarfile
import time
import re
import subprocess
from pathlib import Path
HERE = Path(__file__).resolve().parent
IMAGE = 'forgejo.coulomb.social/coulomb/key-cape@sha256:cdec9dd7c515f7e0a6169fc3fa560ee2cc47bf40d899755dec808c362f5ce010'
LABEL = 'net-kingdom.recovery-run'
class Refused(Exception):
pass
def load_bundle():
spec = importlib.util.spec_from_file_location('identity_bundle', HERE / 'identity-bundle.py')
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
def private_payload(root, manifest, expected_manifest_sha256):
"""Bind the complete validated archive before selecting KeyCape material.
Private hashes never enter the public receipt. The digest is supplied by the
existing private archive admission, not inferred from an untrusted manifest.
"""
encoded = json.dumps(manifest, sort_keys=True, separators=(',', ':')).encode()
if hashlib.sha256(encoded).hexdigest() != expected_manifest_sha256:
raise Refused()
entries = manifest['files']
if len(entries) != len({e['path'] for e in entries}):
raise Refused()
values = {}
bundle = load_bundle()
for entry in entries:
if not bundle.allowed(entry['path'], manifest['profile']):
raise Refused()
path = root / entry['path']
bundle.private(path)
value = path.read_bytes()
if len(value) != entry['bytes'] or hashlib.sha256(value).hexdigest() != entry['sha256']:
raise Refused()
values[entry['path']] = value
deployment = json.loads(values['resources/sso/deployment/keycape.json'])
main = next(c for c in deployment['spec']['template']['spec']['containers'] if c['name'] == 'keycape')
if main.get('command') or main.get('args') or main.get('envFrom'):
raise Refused()
if main['image'] != IMAGE:
# Mutable-tag/historical mismatches need a separate explicit owner admission.
raise Refused()
secret = json.loads(values['resources/sso/secret/keycape-config.json'])['data']
files = {'/etc/keycape/' + name: base64.b64decode(secret[name], validate=True)
for name in ('config.yaml', 'key.pem')}
for path, value in values.items():
if path.startswith('data/keycape/'):
relative = path.removeprefix('data/keycape/')
files['/var/lib/keycape-policy/' + relative] = value
if '/var/lib/keycape-policy/policy.json' not in files:
raise Refused()
if 'resources/sso/secret/keycape-factor-read.json' in values:
factor=json.loads(values['resources/sso/secret/keycape-factor-read.json'])['data']
files['/etc/keycape-factor/admin-token']=base64.b64decode(factor['admin-token'],validate=True)
# Resolve original delivered environment, including all referenced secrets.
env = {}
for item in main.get('env', []):
if not re.fullmatch(r'KEYCAPE_[A-Z0-9_]+',item['name']):raise Refused()
if item['name'] in env:
raise Refused()
if 'value' in item:
env[item['name']] = item['value']
else:
ref = item['valueFrom']['secretKeyRef']
source = json.loads(values['resources/sso/secret/' + ref['name'] + '.json'])['data']
env[item['name']] = base64.b64decode(source[ref['key']], validate=True).decode()
if env.get('KEYCAPE_CONFIG') != '/etc/keycape/config.yaml' or env.get('KEYCAPE_POLICY_PATH') != '/var/lib/keycape-policy/policy.json':
raise Refused()
return {'files': {p: base64.b64encode(v).decode() for p, v in files.items()}, 'env': env}
ENDPOINT = 'unix:///var/run/docker.sock'
EXECUTION = None
def docker_environment(environment=None):
# Even an admitted daemon exposed by another Unix socket is outside this
# exact session binding. Conflicting ambient routing is refused, not ignored.
for source in (os.environ, environment or {}):
if source.get('DOCKER_HOST') not in (None,'',ENDPOINT):raise Refused()
if source.get('DOCKER_CONTEXT') not in (None,'','default'):raise Refused()
if any(source.get(k) for k in ('DOCKER_TLS_VERIFY','DOCKER_CERT_PATH')):raise Refused()
env=dict(os.environ if environment is None else environment)
for key in ('DOCKER_CONTEXT','DOCKER_TLS_VERIFY','DOCKER_CERT_PATH'):env.pop(key,None)
env['DOCKER_HOST']=ENDPOINT
return env
def process(argv, **kwargs):
marker=None
if EXECUTION is not None and (kwargs.get('start_new_session') or kwargs.get('process_group') not in (None,-1)):raise Refused()
if argv[0]=='docker':
argv=['docker','--host',ENDPOINT,*argv[1:]]
kwargs['env']=docker_environment(kwargs.get('env'))
if EXECUTION is not None:
if (os.getpid()!=EXECUTION['pid'] or os.getsid(0)!=EXECUTION['sid'] or os.getpgrp()!=EXECUTION['pgid']
or time.monotonic()>=EXECUTION['deadline']):raise Refused()
marker=Path(EXECUTION['staging']+'-docker-inflight')
fd=os.open(marker,os.O_WRONLY|os.O_CREAT|os.O_EXCL|os.O_NOFOLLOW,0o600)
os.close(fd)
try:
result=subprocess.run(argv,**kwargs)
return result
except BaseException:
# Parent loss leaves .docker-inflight; caught timeout/transport failures
# retain a sticky uncertainty marker even if later cleanup can run.
if marker is not None:
fd=os.open(Path(EXECUTION['staging']+'-docker-uncertain'),os.O_WRONLY|os.O_CREAT|os.O_NOFOLLOW,0o600);os.close(fd)
raise
finally:
if marker is not None:
try:marker.unlink()
except FileNotFoundError:pass
def command(argv, **kwargs):
maximum = kwargs.pop('max_output', 32768)
result = process(argv, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL,
timeout=kwargs.pop('timeout', 180), **kwargs)
if result.returncode or len(result.stdout) > maximum:
raise Refused()
return result.stdout
def inspect_session(session, worker_only=False):
"""No credential environment is read by Docker inspect or returned to caller."""
if not re.fullmatch(r'[a-f0-9]{32}', session['run_id']):
raise Refused()
names = session['containers']
if not names or len(names) > 12 or len(set(names)) != len(names):
raise Refused()
for name in names:
if not re.fullmatch(r'nk49-kc-' + session['run_id'] + r'-[a-z0-9-]{1,24}', name):
raise Refused()
if session['worker'] not in names or session['issuer'] not in names:
raise Refused()
import os
contexts=json.loads(command(['docker','context','inspect']))
endpoints=[contexts[0]['Endpoints']['docker']['Host'],os.environ.get('DOCKER_HOST') or ENDPOINT]
if not all(x==ENDPOINT for x in endpoints):raise Refused()
info = json.loads(command(['docker', 'info', '--format', '{"ID":{{json .ID}},"Name":{{json .Name}},"OSType":{{json .OSType}}}']))
if info != {'ID': '74f93783-6cef-43a8-a3c4-f62586cd3bc1', 'Name': 'bnt-lap001', 'OSType': 'linux'}:
raise Refused()
# The worker is the anchor; all dependencies/TLS/issuer share its network-none namespace.
anchor = command(['docker', 'container', 'inspect', session['worker'], '--format', '{{.Id}}']).decode().strip()
for name,spec in session.get('volumes',{}).items():
volume=json.loads(command(['docker','volume','inspect',name]))
expected={'type':'tmpfs','device':'tmpfs','o':'size=33554432,uid=1000,gid=1000,mode=0700,nosuid,nodev,noexec'}
if len(volume)!=1 or volume[0]['Driver']!='local' or volume[0].get('Scope')!='local' or volume[0].get('Options')!=expected or spec['options']!=expected or volume[0].get('Labels',{}).get(LABEL)!=session['run_id']:raise Refused()
for name in names:
obj = json.loads(command(['docker', 'container', 'inspect', name, '--format',
'{"host":{{json .HostConfig}},"mounts":{{json .Mounts}},"labels":{{json .Config.Labels}},"image":{{json .Config.Image}},"user":{{json .Config.User}},"entrypoint":{{json .Config.Entrypoint}},"cmd":{{json .Config.Cmd}}}']))
host = obj['host']
mounts=obj['mounts']
destinations=[m['Destination'] for m in mounts]
if len(destinations)!=len(set(destinations)):raise Refused()
for volume,expected_spec in session.get('volumes',{}).items():
matches=[m for m in mounts if m['Destination']==expected_spec['target']]
if len(matches)!=1 or matches[0]['Type']!='volume' or matches[0]['Name']!=volume or matches[0]['RW']!=(name==session['worker']):raise Refused()
if (obj['labels'].get(LABEL) != session['run_id'] or not obj['user'] or obj['user'].split(':')[0] in ('0', 'root')
or not host['ReadonlyRootfs'] or host['Privileged'] or host.get('Binds') or host.get('PortBindings')
or host.get('PidMode') or host.get('IpcMode') == 'host'
or host.get('UTSMode') or host.get('UsernsMode') == 'host' or host.get('CgroupnsMode') == 'host'
or host.get('Sysctls',{}) not in ({},{'net.ipv4.ip_unprivileged_port_start':'0'} if name==session['worker'] else {})
or host.get('CapAdd') or host.get('Devices') or host.get('DeviceRequests') or host.get('VolumesFrom')
or host.get('CapDrop') != ['ALL']
or host.get('SecurityOpt') not in (['no-new-privileges'], ['no-new-privileges:true'])
or any(m['Type']!='tmpfs' and not (m['Type']=='volume' and m['Name'] in session.get('volumes',{}) and m['Destination']==session['volumes'][m['Name']]['target']) for m in obj['mounts'])
or host['NetworkMode'] != ('none' if name == session['worker'] else 'container:' + anchor)
or obj['image'] != session['images'][name] or '@sha256:' not in obj['image']):
raise Refused()
if worker_only:return
if session['images'][session['issuer']] != IMAGE:
raise Refused()
# Known admitted archive has no command/args override; image default /keycape
# must be the actual issuer executable, not an alternate same-image command.
launch=json.loads(command(['docker','container','inspect',session['issuer'],'--format',
'{"entrypoint":{{json .Config.Entrypoint}},"cmd":{{json .Config.Cmd}},"path":{{json .Path}},"args":{{json .Args}}}']))
if launch != {'entrypoint':['/keycape'],'cmd':None,'path':'/keycape','args':[]}:
raise Refused()
def container_file(container, path):
# Docker streams one tar member into memory; never materialize private files.
raw = command(['docker', 'cp', container + ':' + path, '-'], max_output=128*1024*1024)
with tarfile.open(fileobj=io.BytesIO(raw), mode='r:') as archive:
members = archive.getmembers()
if len(members) != 1 or not members[0].isfile() or members[0].size > 96*1024*1024:
raise Refused()
return archive.extractfile(members[0]).read()
JWKS_PROBE = r"""
import base64,ipaddress,json,socket,ssl,sys,urllib.request
from urllib.parse import urlsplit
p=json.load(sys.stdin);issuer=p['issuer'];origin=urlsplit(issuer)
def require(condition):
if not condition:raise ValueError('public_jwks_binding_refused')
require(origin.scheme=='https')
resolved=socket.getaddrinfo(origin.hostname,origin.port or 443,type=socket.SOCK_STREAM)
require(bool(resolved) and all(ipaddress.ip_address(x[4][0]).is_loopback for x in resolved))
class NoRedirect(urllib.request.HTTPRedirectHandler):
def redirect_request(self,*args,**kwargs):return None
opener=urllib.request.build_opener(urllib.request.ProxyHandler({}),NoRedirect(),
urllib.request.HTTPSHandler(context=ssl.create_default_context(cadata=p['trust'])))
def get(url):
u=urlsplit(url)
require((u.scheme,u.hostname,u.port or 443)==(origin.scheme,origin.hostname,origin.port or 443))
require(not u.username and not u.password and not u.fragment)
with opener.open(url,timeout=10) as response:
raw=response.read(1024*1024+1);require(len(raw)<=1024*1024)
return json.loads(raw)
d=get(issuer.rstrip('/')+'/.well-known/openid-configuration');require(d['issuer']==issuer)
keys=get(d['jwks_uri'])['keys']
def integer(value):return int.from_bytes(base64.urlsafe_b64decode(value+'='*(-len(value)%4)),'big')
# The exact admitted owner release constructs one key, kid key-1. Reject every
# extra key, including irrelevant keys: a native successful exchange can then
# only have used the original signer through the existing verifier.
require(isinstance(keys,list) and len(keys)==1)
k=keys[0]
require(k.get('kty')=='RSA' and k.get('kid')=='key-1' and k.get('use')=='sig' and k.get('alg')=='RS256')
require(integer(k['n'])==int(p['n']) and integer(k['e'])==int(p['e']))
print('{"original_public_signing_key_in_live_jwks":true}')
"""
def check_jwks(session, admission, public_numbers):
trust=runtime_file(session,admission['trust_path']).decode()
request={'issuer':admission['issuer'],'trust':trust,'n':str(public_numbers.n),'e':str(public_numbers.e)}
result=json.loads(command(['docker','exec','-i',session['worker'],'python3','-c',JWKS_PROBE],input=json.dumps(request).encode()))
if result != {'original_public_signing_key_in_live_jwks': True}:raise Refused()
RUNTIME_FILES = r"""
import base64,json,sys
from pathlib import Path
p=json.load(sys.stdin)
roots=('/etc/keycape/','/var/lib/keycape-policy/','/etc/keycape-factor/','/recovery-tls/')
result={}
paths=[]
if p.get('tree'):
paths=list(Path('/var/lib/keycape-policy').rglob('*'))
else:paths=[Path(x) for x in p['paths']]
for f in paths:
if f.is_dir():continue
if not str(f).startswith(roots) or not f.is_file() or any(x.is_symlink() for x in (f,*f.parents)) or f.stat().st_size>8*1024*1024:raise ValueError()
result[str(f)]=base64.b64encode(f.read_bytes()).decode()
if len(result)>4096:raise ValueError()
print(json.dumps(result))
"""
def runtime_files(session, paths=(), tree=False):
raw=command(['docker','exec','-i',session['worker'],'python3','-c',RUNTIME_FILES],
input=json.dumps({'paths':list(paths),'tree':tree}).encode(),max_output=48*1024*1024)
return {k:base64.b64decode(v,validate=True) for k,v in json.loads(raw).items()}
def runtime_file(session,path):
return runtime_files(session,[path])[path]
def run_verifier(session, admission, payload):
issuer = session['issuer']
if {v['target'] for v in session.get('volumes',{}).values()}!={'/etc/keycape','/var/lib/keycape-policy','/etc/keycape-factor','/recovery-tls'}:raise Refused()
from urllib.parse import urlsplit
import ipaddress
address = urlsplit(admission['issuer'])
if (address.scheme != 'https' or not address.hostname or address.username or address.password
or address.query or address.fragment):
raise Refused()
if hashlib.sha256(container_file(issuer, '/keycape')).hexdigest() != admission['verifier_sha256']:
raise Refused()
if hashlib.sha256(runtime_file(session, admission['trust_path'])).hexdigest() != admission['trust_sha256']:
raise Refused()
# Capture private environment only in owner process memory, never receipt.
environment = json.loads(command(['docker','container','inspect',issuer,'--format','{{json .Config.Env}}']))
env = dict(item.split('=',1) for item in environment)
if len(env)!=len(environment):raise Refused()
if set(env)-set(payload['env'])-{'PATH','HOME','HOSTNAME','SSL_CERT_FILE'}:raise Refused()
if env.get('SSL_CERT_FILE') != admission['trust_path'] or any(k.lower().endswith('_proxy') and v for k,v in env.items()):
raise Refused()
if {k:v for k,v in env.items() if k.startswith('KEYCAPE_')}!={k:v for k,v in payload['env'].items() if k.startswith('KEYCAPE_')}:raise Refused()
for name,value in payload['env'].items():
if env.get(name) != value:
raise Refused()
files = {k:base64.b64decode(v,validate=True) for k,v in payload['files'].items()}
def equal():
if runtime_files(session,files)!=files:raise Refused()
durable=runtime_files(session,tree=True)
if durable!={k:v for k,v in files.items() if k.startswith('/var/lib/keycape-policy/')}:
raise Refused()
equal()
# Reading YAML is data parsing only; no archived executable is evaluated.
import yaml
cfg = yaml.safe_load(files['/etc/keycape/config.yaml'])
if cfg['issuer'] != admission['issuer']:
raise Refused()
consumer = admission['consumer']
client = next(x for x in cfg['clients'] if x['clientId']==consumer['client_id'])
if (client['secretRef']!='env:'+consumer['secret_env'] or client['clientType']!='confidential'
or 'client_credentials' not in client['grantTypes'] or not env.get(consumer['secret_env'])):
raise Refused()
from cryptography.hazmat.primitives import serialization
key=serialization.load_pem_private_key(files['/etc/keycape/key.pem'],password=None)
check_jwks(session,admission,key.public_key().public_numbers())
wrong = secrets.token_urlsafe(48)
if wrong == env[consumer['secret_env']]:raise Refused()
args = ['docker','exec','--env','NK49_UNRELATED_NEGATIVE_SECRET='+wrong,issuer,'/keycape','verify-client',
'-issuer',admission['issuer'],'-previous-secret-env','NK49_UNRELATED_NEGATIVE_SECRET']
flags = {'client_id':'client-id','secret_env':'secret-env','audience':'audience','scope':'scope',
'subject':'expect-subject','tenant':'expect-tenant','roles':'expect-roles','deny_scope':'deny-scope'}
for name,flag in flags.items():args+=['-'+flag,consumer[name]]
# Native command reports on stderr. Do not return or print its raw response.
result = process(args,stdout=subprocess.PIPE,stderr=subprocess.PIPE,timeout=150)
raw = (result.stdout+result.stderr).decode()
required = ['discovery and endpoint origin','exchange and JWKS signature for granted scopes','access-token claim bindings',
'principal_type is service','sub matches the registration','tenant matches the registration','roles present',
'granted scope carries no excess grant','predecessor secret refused']
required += ['excess scope refused: '+scope for scope in consumer['deny_scope'].split()]
if result.returncode or any('PASS '+name+'\n' not in raw for name in required) or 'FAIL ' in raw:
raise Refused()
equal()
from cryptography.hazmat.primitives import serialization
key=serialization.load_pem_private_key(files['/etc/keycape/key.pem'],password=None)
check_jwks(session,admission,key.public_key().public_numbers())
public=key.public_key().public_bytes(serialization.Encoding.DER,serialization.PublicFormat.SubjectPublicKeyInfo)
return {'native_service_registration_verified':True,'wrong_unrelated_secret_explicitly_refused':True,
'denied_scopes_refused':True,'original_config_registry_policy_receipts_equal':True,
'public_signing_spki_sha256':hashlib.sha256(public).hexdigest(),
'original_https_issuer_and_trust_preserved':True,'original_public_signing_key_in_live_jwks':True}
def cleanup(session, receipt=None):
"""Attempt every owned cleanup even when one obligation fails.
Preserve exact safe container names and outcomes so the owner can recover an
uncertain/partial cleanup. An inspect/daemon failure never means absence.
"""
obligations = []
failed = False
for name in reversed(session['containers']):
row = {'container': name, 'status': 'unverified'}
obligations.append(row)
try:
present=command(['docker','container','ls','--all','--filter','name=^/'+name+'$','--format','{{.Names}}']).decode().splitlines()
if not present:
row['status']='absence_verified'
continue
label = command(['docker', 'container', 'inspect', name, '--format',
'{{index .Config.Labels "' + LABEL + '"}}']).decode().strip()
if label != session['run_id']:
row['status'] = 'ownership_mismatch_not_removed'
failed = True
continue
# A failed remove is followed by a successful explicit inventory;
# interrupted remove may actually have completed.
try:
command(['docker', 'rm', '--force', '--volumes', name])
except Exception:
pass
remaining = command(['docker','container','ls','--all','--filter',
'name=^/' + name + '$','--format','{{.Names}}']).decode().splitlines()
if remaining:
row['status'] = 'still_present'
failed = True
else:
row['status'] = 'absence_verified'
except Exception:
failed = True
if receipt is not None:
receipt['container_cleanup_obligations'] = obligations
if failed:
raise Refused()
def native_checks(root, manifest, admission, receipt, cleanup_session=True):
"""Owner wrapper provides a reviewed session after existing restore pipeline.
Admission must bind ciphertext, canonical private manifest digest, executable
and trust bytes, exact image/target list, original issuer and selected consumer.
None of these admission fields grant authority or carry a client secret.
"""
session = admission['session']
# Validate ownership/containment before permitting either exec or cleanup.
inspect_session(session)
receipt.update(status='failed', source_sha256=hashlib.sha256(Path(__file__).read_bytes()).hexdigest(),
ciphertext_sha256=admission['ciphertext_sha256'], production_contacted=False,
human_mfa_proven=False, downstream_consumer_reconnect_proven=False,
container_cleanup_verified=False)
try:
for field in ('ciphertext_sha256', 'manifest_sha256', 'verifier_sha256', 'trust_sha256'):
if not re.fullmatch('[a-f0-9]{64}', admission[field]):
raise Refused()
for field in ('client_id','secret_env','audience','scope','subject','tenant','roles','deny_scope'):
if not isinstance(admission['consumer'].get(field), str) or not admission['consumer'][field].strip():
raise Refused()
payload = private_payload(root, manifest, admission['manifest_sha256'])
result = run_verifier(session, admission, payload)
required = {'native_service_registration_verified','wrong_unrelated_secret_explicitly_refused',
'denied_scopes_refused','original_config_registry_policy_receipts_equal',
'original_https_issuer_and_trust_preserved','original_public_signing_key_in_live_jwks'}
if any(result.get(k) is not True for k in required) or not re.fullmatch('[a-f0-9]{64}', result.get('public_signing_spki_sha256','')):
raise Refused()
# Closed receipt allowlist: never copy arbitrary worker output.
receipt.update({k: True for k in required})
receipt['public_signing_spki_sha256'] = result['public_signing_spki_sha256']
receipt['status'] = 'isolated_keycape_native_registration_checked'
finally:
try:
if cleanup_session:
cleanup(session, receipt)
receipt['container_cleanup_verified'] = True
except Exception:
receipt['status'] = 'failed'
raise

View file

@ -0,0 +1,343 @@
#!/usr/bin/env python3
"""Owner-run native LLDAP restore from the verified identity bundle.
Private inputs remain in the owner process/pipe. Decrypted staging uses Linux
tmpfs. Containers have no network, ports, host mounts or production API access.
"""
import argparse
import base64
from datetime import datetime, timezone
import hashlib
import importlib.util
import io
import json
import os
from pathlib import Path, PurePosixPath
import re
import signal
import sqlite3
import subprocess
import tarfile
import tempfile
import time
import tomllib
import uuid
HERE = Path(__file__).resolve().parent
def load(name):
spec = importlib.util.spec_from_file_location(name, HERE / (name + '.py'))
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
verify = load('verify-identity')
offline = load('retrieve-identity-offline')
bundle = verify.bundle
IMAGE = 'docker.io/lldap/lldap@sha256:9e605a66c02514bfcffd1b67cafb1e98d50992216bb2871d7ae44622047dd09d'
CLIENT = 'ghcr.io/gpappsoft/privacyidea-docker@sha256:af7841adad262f129e0c1d4f553af13f21cb2f4dc713533f316cfe43ed0b4473'
ENGINE_ID = '74f93783-6cef-43a8-a3c4-f62586cd3bc1'
ENGINE_NAME = 'bnt-lap001'
def command(args, **kwargs):
result = subprocess.run(args, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL,
timeout=kwargs.pop('timeout', 90), **kwargs)
if result.returncode:
raise bundle.Refused()
return result.stdout
def preflight():
contexts = json.loads(command(['docker', 'context', 'inspect']))
endpoints = [contexts[0]['Endpoints']['docker']['Host']]
if os.environ.get('DOCKER_HOST'):
endpoints.append(os.environ['DOCKER_HOST'])
if not all(url.startswith('unix://') for url in endpoints):
raise bundle.Refused()
facts = json.loads(command(['docker', 'info', '--format',
'{"ID":{{json .ID}},"Name":{{json .Name}},"OSType":{{json .OSType}}}']))
if facts['ID'] != ENGINE_ID or facts['Name'] != ENGINE_NAME or facts['OSType'] != 'linux':
raise bundle.Refused()
# /dev/shm must actually be tmpfs, not a similarly named disk directory.
mounts = Path('/proc/mounts').read_text().splitlines()
if not any(line.split()[1:3] == ['/dev/shm', 'tmpfs'] for line in mounts):
raise bundle.Refused()
images = []
for image in (IMAGE, CLIENT):
image_id = command(['docker', 'image', 'inspect', '--format', '{{.Id}}', image]).decode().strip()
images.append({'reference': image, 'image_id': image_id})
return {'engine_id': facts['ID'], 'engine_name': facts['Name'], 'images': images,
'production_contacted': False, 'network_mode': 'none', 'host_ports_published': False}
def snapshot(path):
# Include all fields of durable identity tables, including stored password
# hashes. Only counts and the equality boolean leave this process.
result = {}
with sqlite3.connect(path.as_uri() + '?mode=ro', uri=True) as db:
if db.execute('PRAGMA integrity_check').fetchall() != [('ok',)]:
raise bundle.Refused()
tables = {row[0] for row in db.execute("SELECT name FROM sqlite_master WHERE type='table'")}
for name in ('users', 'groups', 'memberships', 'user_attributes', 'group_attributes',
'user_attribute_values', 'group_attribute_values',
'user_attribute_schema', 'group_attribute_schema'):
if name in tables:
rows = db.execute('SELECT * FROM "' + name + '"').fetchall()
encoded = sorted(repr(row) for row in rows)
result[name] = {'count': len(rows), 'digest': hashlib.sha256('\n'.join(encoded).encode()).digest()}
if not {'users', 'groups', 'memberships'} <= result.keys():
raise bundle.Refused()
return result
def configuration(root):
data = root / 'data/lldap'
existing = data / 'lldap_config.toml'
config = tomllib.loads(existing.read_text()) if existing.is_file() else {}
deployment = bundle.resource(root, 'sso', 'deployment', 'lldap')
main = next(c for c in deployment['spec']['template']['spec']['containers'] if c['name'] == 'lldap')
env = {e['name']: e['value'] for e in main.get('env', []) if 'value' in e}
secret = bundle.resource(root, 'sso', 'secret', 'lldap-secrets')['data']
jwt = base64.b64decode(secret['LLDAP_JWT_SECRET'], validate=True)
password = base64.b64decode(secret['LLDAP_LDAP_USER_PASS'], validate=True)
if not jwt or not password or b'\n' in jwt or b'\n' in password:
raise bundle.Refused()
base = env.get('LLDAP_LDAP_BASE_DN', config.get('ldap_base_dn'))
user = env.get('LLDAP_LDAP_USER_DN', config.get('ldap_user_dn', 'admin'))
if not isinstance(base, str) or not base or not isinstance(user, str):
raise bundle.Refused()
chosen = {'database_url': 'sqlite:///data/users.db?mode=rwc',
'ldap_base_dn': base, 'ldap_user_dn': user,
'ldap_host': '127.0.0.1', 'http_host': '127.0.0.1',
'ldap_port': 3890, 'http_port': 17170,
'force_ldap_user_pass_reset': False, 'force_update_private_key': False}
seed = env.get('LLDAP_KEY_SEED', config.get('key_seed'))
if seed:
if not isinstance(seed, str):
raise bundle.Refused()
chosen['key_seed'] = seed
else:
value = env.get('LLDAP_KEY_FILE', config.get('key_file', '/data/private_key'))
key = PurePosixPath(value)
if not key.is_absolute():
key = PurePosixPath('/data') / key
if key.parent != PurePosixPath('/data') or not (data / key.name).is_file():
raise bundle.Refused()
chosen['key_file'] = str(key)
# JSON strings are valid TOML basic strings for these service inputs.
raw = '\n'.join(k + ' = ' + json.dumps(v) for k, v in chosen.items())
raw += '\n[smtp_options]\nenable_password_reset = false\n[ldaps_options]\nenabled = false\n'
return raw.encode(), jwt, password, base
def archive_files(files):
out = io.BytesIO()
with tarfile.open(fileobj=out, mode='w') as archive:
for name, value in files.items():
if PurePosixPath(name).is_absolute() or '..' in PurePosixPath(name).parts:
raise bundle.Refused()
entry = tarfile.TarInfo(name)
entry.size = len(value)
entry.mode = 0o600
entry.uid = entry.gid = 1000
archive.addfile(entry, io.BytesIO(value))
return out.getvalue()
PROBE = r'''
import contextlib,io,json,logging,socket,sys,time
logging.disable(logging.CRITICAL)
result={'status':'failed'}
stage='network_isolation'
try:
with contextlib.redirect_stdout(io.StringIO()),contextlib.redirect_stderr(io.StringIO()):
from pathlib import Path
assert {p.name for p in Path('/sys/class/net').iterdir()}=={'lo'}
assert len(Path('/proc/net/route').read_text().splitlines())==1
stage='native_client_import'
from ldap3 import Server,Connection,NONE
from ldap3.utils.dn import escape_rdn
values=json.load(sys.stdin)
stage='directory_startup'
for _ in range(60):
try:
with socket.create_connection(('127.0.0.1',3890),timeout=1):pass
break
except OSError:time.sleep(.25)
else:raise RuntimeError()
dn='uid='+escape_rdn(values['user'])+',ou=people,'+values['base']
server=Server('127.0.0.1',port=3890,get_info=NONE,connect_timeout=3)
stage='existing_user_bind'
connection=Connection(server,user=dn,password=values['password'],auto_bind=True,raise_exceptions=True)
assert connection.bound
connection.unbind()
stage='wrong_password_bind'
wrong=Connection(server,user=dn,password=values['wrong_password'],auto_bind=False,raise_exceptions=False)
refused=not wrong.bind() and wrong.result.get('result')==49
wrong.unbind()
assert refused
result={'status':'verified','existing_user_native_ldap_bind':True,'wrong_password_refused':True,
'only_loopback_interface_verified':True,'external_routes_absent':True}
except Exception as error:result={'status':'failed','failed_stage':stage,'failure_type':type(error).__name__}
print(json.dumps(result))
'''
def restore_lldap(root, user, password, receipt):
before = snapshot(root / 'data/lldap/users.db')
config, jwt, admin_password, base = configuration(root)
name = 'nk49-lldap-' + uuid.uuid4().hex
client_name = name + '-client'
try:
command(['docker', 'run', '--detach', '--pull=never', '--name', name,
'--network', 'none', '--read-only', '--cap-drop', 'ALL',
'--security-opt', 'no-new-privileges', '--user', '1000:1000',
'--memory', '256m', '--cpus', '1', '--pids-limit', '128',
'--tmpfs', '/data:rw,nosuid,nodev,noexec,size=268435456,uid=1000,gid=1000,mode=0700',
'--tmpfs', '/run/recovery:rw,nosuid,nodev,noexec,size=1048576,uid=1000,gid=1000,mode=0700',
'--tmpfs', '/tmp:rw,nosuid,nodev,noexec,size=16777216,uid=1000,gid=1000',
'--env', 'LLDAP_JWT_SECRET_FILE=/run/recovery/jwt',
'--env', 'LLDAP_LDAP_USER_PASS_FILE=/run/recovery/admin-password',
'--entrypoint', '/bin/sh', IMAGE, '-c', 'sleep 1800'])
guard = json.loads(command(['docker', 'container', 'inspect', name, '--format',
'{"host":{{json .HostConfig}},"mounts":{{json .Mounts}},"user":{{json .Config.User}}}']))
host = guard['host']
if (host['NetworkMode'] != 'none' or not host['ReadonlyRootfs'] or host['Privileged']
or host.get('Binds') or host.get('PortBindings') or 'ALL' not in host.get('CapDrop', [])
or 'no-new-privileges' not in host.get('SecurityOpt', [])
or guard['user'] != '1000:1000'
or any(mount['Type'] != 'tmpfs' for mount in guard['mounts'])):
raise bundle.Refused()
receipt['container_isolation_verified_before_private_import'] = True
payload = {p.relative_to(root / 'data/lldap').as_posix(): p.read_bytes()
for p in (root / 'data/lldap').rglob('*') if p.is_file()}
command(['docker', 'exec', '-i', name, 'tar', '-xf', '-', '-C', '/data'], input=archive_files(payload))
command(['docker', 'exec', '-i', name, 'tar', '-xf', '-', '-C', '/run/recovery'],
input=archive_files({'lldap.toml': config, 'jwt': jwt, 'admin-password': admin_password}))
# No credential values in Docker configuration, environment or argv.
command(['docker', 'exec', '--detach', name, '/bin/sh', '-c',
'echo $$ > /run/recovery/server.pid; exec /app/lldap run --config-file /run/recovery/lldap.toml > /dev/null 2>&1'])
raw = command(['docker', 'run', '--interactive', '--rm', '--name', client_name, '--pull=never', '--network', 'container:' + name,
'--read-only', '--cap-drop', 'ALL', '--security-opt', 'no-new-privileges',
'--memory', '256m', '--cpus', '1', '--pids-limit', '64',
'--entrypoint', 'python3', CLIENT, '-c', PROBE],
input=json.dumps({'user': user, 'password': password, 'base': base,
'wrong_password': 'nk49-wrong-' + uuid.uuid4().hex}).encode())
result = json.loads(raw)
receipt.update(result)
if result.get('status') != 'verified':
raise bundle.Refused()
command(['docker', 'exec', name, '/bin/sh', '-c', 'kill -TERM "$(cat /run/recovery/server.pid)"'])
for _ in range(60):
running = subprocess.run(['docker', 'exec', name, '/bin/sh', '-c',
'kill -0 "$(cat /run/recovery/server.pid)"'],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=10)
if running.returncode:
break
time.sleep(.1)
else:
raise bundle.Refused()
raw = command(['docker', 'exec', name, '/bin/sh', '-c',
'test ! -s /data/users.db-wal && cat /data/users.db'])
after_path = root / 'post-restore-users.db'
after_path.write_bytes(raw)
if snapshot(after_path) != before:
raise bundle.Refused()
receipt.update(directory_identity_rows_preserved=True,
directory_comparison_scope=sorted(before),
directory_table_counts={k: v['count'] for k, v in before.items()})
finally:
# A failed create can still have created a container (e.g. lost reply).
for owned in (client_name, name):
found = subprocess.run(['docker', 'container', 'inspect', owned], stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL, timeout=15)
if found.returncode == 0:
command(['docker', 'rm', '--force', '--volumes', owned])
absent = subprocess.run(['docker', 'container', 'inspect', owned], stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL, timeout=15)
command(['docker', 'info', '--format', '{{.ID}}'])
if absent.returncode == 0:
raise bundle.Refused()
receipt['container_cleanup_verified'] = True
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--transfer-receipt', required=True)
parser.add_argument('--receipt', required=True)
parser.add_argument('--user', required=True)
args = parser.parse_args()
os.umask(0o077)
fd = os.open(args.receipt, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o600)
result = {'schema': 'netkingdom.isolated-lldap-recovery.v1', 'status': 'failed',
'production_contacted': False, 'secret_values_exported': False,
'container_cleanup_verified': False, 'temporary_plaintext_cleanup_verified': False,
'source_sha256': hashlib.sha256(Path(__file__).read_bytes()).hexdigest()}
keyfd = None
work = None
stage = 'target_preflight'
try:
result['target'] = preflight()
stage = 'input_validation'
if not re.fullmatch(r'[A-Za-z0-9_.-]{1,128}', args.user):
raise bundle.Refused()
transfer = json.loads(Path(args.transfer_receipt).read_text())
offline.validate(transfer)
if transfer.get('independent_owner_retrieval_verified') is not True:
raise bundle.Refused()
path = Path(transfer['fetched_path'])
bundle.private(path, max_bytes=bundle.LIMIT)
if path.stat().st_size != transfer['ciphertext_bytes'] or offline.offsite.digest(path) != transfer['ciphertext_sha256']:
raise bundle.Refused()
keyfd = verify.custody.private_key_fd()
derived = command(['age-keygen', '-y', f'/proc/self/fd/{keyfd}'], pass_fds=(keyfd,)).strip()
if derived != bundle.profile()['public_recipient'].encode():
raise bundle.Refused()
stage = 'private_existing_password_prompt'
password = offline.private_prompt('Existing account password for the isolated LLDAP check: ')
with tempfile.TemporaryDirectory(prefix='nk49-service-recovery-', dir='/dev/shm') as temporary:
work = Path(temporary)
archive = work / 'archive.tar'
stage = 'archive_decryption'
with archive.open('xb') as output:
proc = subprocess.run(['age', '-d', '-i', f'/proc/self/fd/{keyfd}', str(path)],
stdout=output, stderr=subprocess.DEVNULL, pass_fds=(keyfd,), timeout=180)
if proc.returncode or archive.stat().st_size > bundle.LIMIT + 4 * 1024 * 1024:
raise bundle.Refused()
restored = work / 'restored'
restored.mkdir(mode=0o700)
stage = 'archive_validation'
manifest, _ = bundle.restore(archive, restored)
result.update(ciphertext_sha256=transfer['ciphertext_sha256'],
manifest_file_count=len(manifest['files']), all_manifest_hashes_verified=True)
stage = 'native_directory_check'
restore_lldap(restored, args.user, password, result)
password = None
if work.exists():
raise bundle.Refused()
result.update(status='isolated_lldap_verified', temporary_plaintext_cleanup_verified=True)
except (Exception, KeyboardInterrupt):
result.update(status='failed', error='isolated_lldap_check_failed_private_diagnostics_suppressed')
result.setdefault('failed_stage', stage)
finally:
if keyfd is not None:
os.close(keyfd)
if work is not None and not work.exists():
result['temporary_plaintext_cleanup_verified'] = True
result['finished_at'] = datetime.now(timezone.utc).isoformat()
with os.fdopen(fd, 'w') as output:
json.dump(result, output, indent=2)
output.write('\n')
success = result['status'] == 'isolated_lldap_verified'
print('OK: restored LLDAP existing-user bind, wrong-password refusal, directory preservation and cleanup passed.'
if success else 'ERROR: isolated LLDAP recovery refused; review the safe receipt. No production change.')
return 0 if success else 1
if __name__ == '__main__':
for sig in (signal.SIGINT, signal.SIGTERM, signal.SIGHUP):
signal.signal(sig, offline.interrupted)
raise SystemExit(main())

View file

@ -0,0 +1,164 @@
#!/usr/bin/env python3
"""Download one reviewed ciphertext using privately entered provider access.
No OpenBao, SSO, Kubernetes, credential discovery, provider writes or age key.
Run by the operator in their own terminal, never through a recorded agent PTY.
"""
import argparse
import base64
from datetime import datetime, timezone
import getpass
import hashlib
import importlib.util
import json
import os
from pathlib import Path
import re
import shutil
import signal
import sys
import tempfile
import urllib.request
import urllib.error
import warnings
HERE = Path(__file__).resolve().parent
spec = importlib.util.spec_from_file_location('offsite', HERE / 'identity-offsite.py')
offsite = importlib.util.module_from_spec(spec)
spec.loader.exec_module(offsite)
ACCOUNT_BASE = offsite.HOST + '/remote.php/dav/files/Backup/railiance-backups/net-kingdom-identity/'
def private_prompt(label):
# getpass opens the terminal with os.open, which does not require seeking.
# Reject redirected input and turn its echoing fallback warning into an
# exception before it can read a credential without echo suppression.
if not sys.stdin.isatty() or not sys.stderr.isatty():
raise offsite.Refused()
with warnings.catch_warnings():
warnings.simplefilter('error', getpass.GetPassWarning)
return getpass.getpass(label)
def failure_metadata(error):
# No exception messages, response bodies, URLs or headers enter receipts.
if isinstance(error, urllib.error.HTTPError):
return {'failure_kind': 'provider_http_error', 'http_status': int(error.code)}
if isinstance(error, urllib.error.URLError):
return {'failure_kind': 'provider_transport_error'}
if isinstance(error, getpass.GetPassWarning):
return {'failure_kind': 'terminal_echo_suppression_unavailable'}
if isinstance(error, offsite.Refused):
return {'failure_kind': 'scope_or_integrity_refusal'}
return {'failure_kind': 'local_operation_failed'}
def validate(record):
if (record.get('schema') != 'netkingdom.identity-offsite.v1'
or record.get('status') != 'readback_verified'
or record.get('provider') != 'Storage Share'
or record.get('folder') != 'Backup/railiance-backups/net-kingdom-identity'
or not offsite.NAME.fullmatch(record.get('artifact', ''))
or not re.fullmatch('[a-f0-9]{64}', record.get('ciphertext_sha256', ''))
or type(record.get('ciphertext_bytes')) is not int
or not 0 < record['ciphertext_bytes'] <= offsite.LIMIT
or not re.fullmatch(r'"[^"\x00-\x20]{1,180}"', record.get('etag', ''))):
raise offsite.Refused()
def retrieve(record, secret, mode, client=None):
validate(record)
if mode not in {'account', 'folder-share'} or not secret or any(c in secret for c in '\r\n\x00'):
raise offsite.Refused()
auth = offsite.headers(secret) if mode == 'folder-share' else {
'Authorization': 'Basic ' + base64.b64encode(('Backup:' + secret).encode()).decode(),
'Accept-Encoding': 'identity'}
url = (offsite.BASE if mode == 'folder-share' else ACCOUNT_BASE) + record['artifact']
client = client or offsite.opener()
directory = None
complete = False
try:
with client.open(urllib.request.Request(url, method='HEAD', headers=auth), timeout=30) as response:
if offsite.etag(response, url, record['ciphertext_bytes']) != record['etag']:
raise offsite.Refused()
directory = Path(tempfile.mkdtemp(prefix='nk49-identity-fetched-', dir='/tmp'))
target = directory / record['artifact']
auth['If-Match'] = record['etag']
count = 0
digest = hashlib.sha256()
with client.open(urllib.request.Request(url, headers=auth), timeout=180) as response:
if offsite.etag(response, url, record['ciphertext_bytes']) != record['etag']:
raise offsite.Refused()
fd = os.open(target, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o600)
with os.fdopen(fd, 'wb') as output:
while block := response.read(1024 * 1024):
count += len(block)
if count > record['ciphertext_bytes']:
raise offsite.Refused()
digest.update(block)
output.write(block)
output.flush()
os.fsync(output.fileno())
if count != record['ciphertext_bytes'] or digest.hexdigest() != record['ciphertext_sha256']:
raise offsite.Refused()
with client.open(urllib.request.Request(url, method='HEAD', headers=auth), timeout=30) as response:
if offsite.etag(response, url, record['ciphertext_bytes']) != record['etag']:
raise offsite.Refused()
complete = True
# Closed allowlist: never copy arbitrary source-receipt fields to output.
result = {k: record[k] for k in ('schema', 'provider', 'folder', 'artifact',
'ciphertext_bytes', 'ciphertext_sha256', 'etag')}
result.update(status='readback_verified', fetched_path=str(target),
independent_owner_retrieval_verified=True,
credential_source='independently_held_provider_access',
provider_auth_mode=mode, openbao_contacted=False, sso_contacted=False,
provider_writes_performed=False, age_private_key_used=False,
secret_values_exported=False, object_unchanged_during_transfer=True,
downloaded_at=datetime.now(timezone.utc).isoformat())
return result
finally:
if directory is not None and not complete:
shutil.rmtree(directory)
def interrupted(signum, frame):
raise InterruptedError()
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--source-receipt', required=True)
parser.add_argument('--receipt', required=True)
parser.add_argument('--auth', choices=('account', 'folder-share'), default='account')
args = parser.parse_args()
os.umask(0o077)
fd = os.open(args.receipt, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o600)
result = {'status': 'failed', 'error': 'offline_retrieval_failed_private_diagnostics_suppressed'}
stage = 'source_receipt_validation'
try:
record = json.loads(Path(args.source_receipt).read_text())
validate(record)
# getpass must never fall back to echoed stdin (e.g. a captured agent tool).
stage = 'private_provider_prompt'
secret = private_prompt('Storage Share Backup app password or exact folder-share token: ')
try:
stage = 'provider_download'
result = retrieve(record, secret, args.auth)
finally:
secret = None
except (Exception, KeyboardInterrupt) as error:
result.update(failed_stage=stage, **failure_metadata(error))
finally:
with os.fdopen(fd, 'w') as output:
json.dump(result, output, indent=2)
output.write('\n')
ok = result['status'] == 'readback_verified'
print('OK: ciphertext retrieved without OpenBao or SSO; ETag, size and SHA256 verified.' if ok
else 'ERROR: retrieval refused; private diagnostics suppressed. Use a new receipt path to retry.')
return 0 if ok else 1
if __name__ == '__main__':
for sig in (signal.SIGINT, signal.SIGTERM, signal.SIGHUP):
signal.signal(sig, interrupted)
sys.exit(main())

View file

@ -0,0 +1,71 @@
#!/usr/bin/env python3
"""Silent attended owner retrieval of an existing identity ciphertext.
Uses Backup-account recovery custody only in memory; no private age key, upload,
share, grant, deletion or production restore operation.
"""
import argparse
import base64
from datetime import datetime,timezone
import hashlib
import importlib.util
import json
import os
from pathlib import Path
import re
import tempfile
import urllib.request
HERE=Path(__file__).resolve().parent
def load(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
inventory=load('inventory-storage-share');offsite=load('identity-offsite')
def retrieve(record,out):
if (record.get('schema')!='netkingdom.identity-offsite.v1' or record.get('status')!='readback_verified'
or not offsite.NAME.fullmatch(record.get('artifact','')) or not re.fullmatch('[a-f0-9]{64}',record.get('ciphertext_sha256',''))
or not 0<record.get('ciphertext_bytes',0)<=offsite.LIMIT):raise offsite.Refused()
identity,credentials=inventory.caller_credentials()
auth=base64.b64encode((credentials['BACKUP_USERNAME']+':'+credentials['BACKUP_PASSWORD']).encode()).decode()
url=offsite.HOST+'/remote.php/dav/files/Backup/railiance-backups/net-kingdom-identity/'+record['artifact']
h={'Authorization':'Basic '+auth,'Accept-Encoding':'identity'}
client=offsite.opener()
with client.open(urllib.request.Request(url,method='HEAD',headers=h),timeout=30) as response:
version=offsite.etag(response,url,record['ciphertext_bytes'])
if version!=record['etag']:raise offsite.Refused()
h['If-Match']=version
directory=Path(tempfile.mkdtemp(prefix='nk49-identity-fetched-',dir='/tmp'))
target=directory/record['artifact'];count=0;digest=hashlib.sha256()
with client.open(urllib.request.Request(url,headers=h),timeout=180) as response:
if offsite.etag(response,url,record['ciphertext_bytes'])!=version:raise offsite.Refused()
with target.open('xb') as output:
while block:=response.read(1024*1024):
count+=len(block)
if count>record['ciphertext_bytes']:raise offsite.Refused()
digest.update(block);output.write(block)
if count!=record['ciphertext_bytes'] or digest.hexdigest()!=record['ciphertext_sha256']:raise offsite.Refused()
with client.open(urllib.request.Request(url,method='HEAD',headers=h),timeout=30) as response:
if offsite.etag(response,url,record['ciphertext_bytes'])!=version:raise offsite.Refused()
out.update(record);out.update(status='readback_verified',fetched_path=str(target),
independent_owner_retrieval_verified=True,caller_entity_id=identity.get('entity_id'),
age_private_key_used=False,secret_values_exported=False,downloaded_at=datetime.now(timezone.utc).isoformat())
def main():
p=argparse.ArgumentParser(description=__doc__);p.add_argument('--source-receipt',required=True);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','source_sha256':hashlib.sha256(Path(__file__).read_bytes()).hexdigest()}
try:retrieve(json.loads(Path(a.source_receipt).read_text()),r)
except Exception:r.update(status='failed',error='contained_owner_retrieval_failed')
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())

View file

@ -0,0 +1,40 @@
#!/usr/bin/env bash
# Candidate owner launcher. Review and native fixture proof required before use.
set +x
set -euo pipefail
ROOT="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)"
cd -- "$ROOT"
umask 077
[[ $# == 6 ]] || { printf '%s\n' 'Usage: run-isolated-keycape.sh VERIFIED_TRANSFER_RECEIPT ADMISSION ADMISSION_SHA256 TLS_CERT TLS_KEY TRUST'; exit 1; }
[[ "$3" =~ ^[a-f0-9]{64}$ ]] || { printf '%s\n' 'ERROR: exact reviewed admission digest required.'; exit 1; }
for index in 1 2 4 5 6; do
[[ -f "${!index}" && ! -L "${!index}" ]] || { printf '%s\n' 'ERROR: provide regular owner-controlled input files.'; exit 1; }
done
[[ -t 0 && -t 1 && -t 2 ]] || { printf '%s\n' 'ERROR: run in your own interactive terminal.'; exit 1; }
for tool in python3 age age-keygen docker; do
command -v "$tool" >/dev/null || { printf 'ERROR: required tool unavailable: %s\n' "$tool"; exit 1; }
done
python3 - <<'PY'
import importlib.util
from pathlib import Path
p=Path('tools/bootstrap-recovery/isolated-keycape-session.py')
spec=importlib.util.spec_from_file_location('session',p)
m=importlib.util.module_from_spec(spec);spec.loader.exec_module(m)
try:
m.local.preflight()
m.run(['docker','image','inspect',m.adapter.IMAGE])
except Exception:
raise SystemExit('ERROR: exact local Docker/images/tmpfs preflight failed; no key requested.')
print('OK: fixed local target and cached pinned images available.')
PY
NK49_KC_RUN="$(mktemp -d /tmp/nk49-isolated-keycape-XXXXXXXX)"
printf 'Safe evidence directory: %s\n' "$NK49_KC_RUN"
printf '%s\n' 'Owner-reviewed candidate: isolated original issuer/trust and current registered service-client checks only.'
printf '%s\n' 'No browser, downstream consumer or retained-factor MFA acceptance is claimed.'
printf '%s\n' 'Enter only the independently held backup age key at the hidden prompt.'
bash sso-mfa/bootstrap/sops-custody-unlock.sh \
--expected-recipient age1zvryunvjhvpkmasskauga2heeg0ztnte9ymgppvjge36ekumk50syr3tsz \
--prompt -- python3 tools/bootstrap-recovery/isolated-keycape-session.py \
--transfer-receipt "$1" --admission "$2" --admission-sha256 "$3" \
--tls-cert "$4" --tls-key "$5" --trust "$6" --receipt "$NK49_KC_RUN/recovery.json"
printf 'Safe receipt: %s/recovery.json\n' "$NK49_KC_RUN"

View file

@ -0,0 +1,90 @@
#!/usr/bin/env python3
"""Attended independent-key recovery of the new data-bearing off-host bundle."""
import argparse
from datetime import datetime,timezone
import hashlib
import importlib.util
import json
import os
from pathlib import Path
import signal
import subprocess
import tempfile
import uuid
HERE=Path(__file__).resolve().parent
def load(name,path=None):
spec=importlib.util.spec_from_file_location(name,path or HERE/(name+'.py'))
m=importlib.util.module_from_spec(spec);spec.loader.exec_module(m);return m
bundle=load('identity-bundle');pg=load('check-privacyidea-database')
custody=load('sops_private',HERE.parents[1]/'sso-mfa/bootstrap/sops-private.py')
def verify(transfer,keyfd,receipt):
if (transfer.get('schema')!='netkingdom.identity-offsite.v1' or transfer.get('status')!='readback_verified'
or transfer.get('independent_owner_retrieval_verified') is not True
or transfer.get('ciphertext_bytes',0)>bundle.LIMIT):raise bundle.Refused()
path=Path(transfer['fetched_path']);bundle.private(path,max_bytes=bundle.LIMIT)
with path.open('rb') as source:digest=hashlib.file_digest(source,'sha256').hexdigest()
if path.stat().st_size!=transfer['ciphertext_bytes'] or digest!=transfer['ciphertext_sha256']:raise bundle.Refused()
derived=pg.run(['age-keygen','-y',f'/proc/self/fd/{keyfd}'],pass_fds=(keyfd,)).strip()
if derived!=bundle.profile()['public_recipient'].encode():raise bundle.Refused()
receipt.update(ciphertext_sha256=digest,artifact=transfer['artifact'],registered_recipient_match=True)
with tempfile.TemporaryDirectory(prefix='nk49-identity-restore-',dir='/tmp') as temporary:
work=Path(temporary);env={'PATH':os.environ.get('PATH',os.defpath),'HOME':temporary,'XDG_CONFIG_HOME':temporary}
wrong=work/'wrong.key';pg.run(['age-keygen','-o',str(wrong)],env=env)
negative=subprocess.run(['age','-d','-i',str(wrong),str(path)],stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL,env=env,timeout=120)
if negative.returncode==0:raise bundle.Refused()
receipt['wrong_key_refused']=True
plain=work/'archive.tar'
with plain.open('xb') as output:
result=subprocess.run(['age','-d','-i',f'/proc/self/fd/{keyfd}',str(path)],stdout=output,
stderr=subprocess.DEVNULL,env=env,pass_fds=(keyfd,),timeout=180)
if result.returncode or plain.stat().st_size>bundle.LIMIT+4*1024*1024:raise bundle.Refused()
restored=work/'restored';restored.mkdir(mode=0o700)
manifest,checks=bundle.restore(plain,restored)
receipt.update(checks=checks,manifest_file_count=len(manifest['files']),all_manifest_hashes_verified=True,
profile_sha256=manifest['profile_sha256'])
container='nk49-identity-db-'+uuid.uuid4().hex
try:
receipt['database_restore']=pg.restore(restored/'databases/privacyidea.dump',container)
finally:
pg.run(['docker','rm','--force','--volumes',container])
absent=subprocess.run(['docker','container','inspect',container],stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL,timeout=15)
pg.run(['docker','info','--format','{{.ServerVersion}}'])
if absent.returncode==0:raise bundle.Refused()
receipt['container_cleanup_verified']=True
if work.exists():raise bundle.Refused()
receipt.update(temporary_plaintext_cleanup_verified=True,status='identity_archive_verified',
full_service_authentication_proven=False,existing_token_decryption_proven=False,audit_signature_continuity_proven=False)
def main():
p=argparse.ArgumentParser(description=__doc__);p.add_argument('--transfer-receipt',required=True);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={'schema':'netkingdom.identity-recovery.v1','status':'failed','started_at':datetime.now(timezone.utc).isoformat(),
'production_contacted':False,'secret_values_exported':False,'container_cleanup_verified':False,
'temporary_plaintext_cleanup_verified':False,'source_sha256':hashlib.sha256(Path(__file__).read_bytes()).hexdigest()}
keyfd=None
try:
transfer=json.loads(Path(a.transfer_receipt).read_text());keyfd=custody.private_key_fd()
verify(transfer,keyfd,r)
except Exception:r.update(status='failed',error='identity_recovery_failed_private_diagnostics_suppressed')
finally:
if keyfd is not None:os.close(keyfd)
r['finished_at']=datetime.now(timezone.utc).isoformat()
with os.fdopen(fd,'w') as out:json.dump(r,out,indent=2);out.write('\n')
if r['status']=='identity_archive_verified':
print('OK: off-host identity bundle decrypted; complete manifest, key pairs, SQLite integrity and PostgreSQL restore passed; disposable data removed. Service authentication remains separate acceptance.')
return 0
print('ERROR: identity recovery failed; review safe receipt. Private diagnostics suppressed.');return 1
if __name__=='__main__':
for sig in (signal.SIGHUP,signal.SIGTERM,signal.SIGINT):signal.signal(sig,custody.interrupted)
raise SystemExit(main())

View file

@ -0,0 +1,92 @@
# Scoped native fixture proposal, revision5 — prepared, not executed
Review source, then start with the single positive case. Neither command below
was executed. Default `--plan` has been inspected and performs no Docker calls,
key generation or private material creation.
Frozen fixture manifest SHA256: `6a4e3f47fc9a647d813dfbe75851d1ef62ebac7cfc7a7818b587ad56bf919c7a`.
```sh
python3 /home/worsch/the-custodian/docs/changes/netkingdom-target-2026-10-16/identity-restore-adapter-next/revisions/r5/test-root/tests/run_isolated_keycape_native_fixture.py --plan
```
Proposed **positive-only** command, after independent review:
```sh
python3 /home/worsch/the-custodian/docs/changes/netkingdom-target-2026-10-16/identity-restore-adapter-next/revisions/r5/test-root/tests/run_isolated_keycape_native_fixture.py --execute-reviewed-fixture --expected-manifest-sha256 6a4e3f47fc9a647d813dfbe75851d1ef62ebac7cfc7a7818b587ad56bf919c7a --case positive
```
Separately proposed bounded extended cases, after the positive result and review:
```sh
python3 /home/worsch/the-custodian/docs/changes/netkingdom-target-2026-10-16/identity-restore-adapter-next/revisions/r5/test-root/tests/run_isolated_keycape_native_fixture.py --execute-reviewed-fixture --expected-manifest-sha256 6a4e3f47fc9a647d813dfbe75851d1ef62ebac7cfc7a7818b587ad56bf919c7a --case all
```
The runner requires every source/dependency hash in its manifest to match before
preflight, and binds `unix:///var/run/docker.sock` to engine
`74f93783-6cef-43a8-a3c4-f62586cd3bc1`, `bnt-lap001`, Linux. Images are the existing
exact KeyCape `cdec9dd7…` and privacyIDEA helper `af7841ad…` references listed in
the manifest. No pulls/builds/pushes or production APIs/routes are permitted.
It reuses `test_identity_bundle.stage` unchanged to create the complete format/
integrity archive skeleton, then configures only synthetic KeyCape registrations,
original synthetic signing key, valid retained policy/history and lockfile,
generated synthetic factor-reader delivery, fresh synthetic age recipient and
fresh self-signed trusted certificate for `https://nk49-fixture.invalid`. Existing
archive sealing/restore and the candidate `execute()` perform the actual checks.
The other service skeletons are format fixtures, not native PI/LLDAP/Authelia
recoveries; no acceptance for those lanes follows. No actual age/TLS/client key,
provider account, existing user or retained factor is requested.
Resources are sequential and exact-label-owned: at most two live service
containers (512MiB/oneCPU each), four 32MiB local-driver tmpfs volumes and one
short-lived stopped public executable-image probe. Container/volume names are
`nk49-kc-<fresh-run-id>-…`, labeled
`net-kingdom.recovery-run=<fresh-run-id>`; names are recorded in the candidate's
pre-decrypt checkpoint. The initial fixture public-image readback probe is also
removed in a finally block and never starts. Host fixture materials are private
under `/dev/shm/nk49-keycape-fixture-…`; candidate staging has its separate exact
registered inode obligation. Safe case receipts and cleanup inventories are
retained in `/tmp/nk49-keycape-native-fixture-review-…/result.json`.
The candidate itself performs shared-tmpfs import, default original-image
`/keycape` launch, loopback HTTPS transport, existing native `verify-client`,
byte/tree comparisons and quiescent container/volume/plaintext cleanup. Only the
policy mount is writable because owner `policy.Open` requires its retained lock
O_RDWR; configuration/signing key, factor and TLS mounts remain readonly to the
issuer. No browser/MFA/upstream readiness is imitated.
**Positive oracle:** the actual issuer-container native command must succeed for
the original fixture hostname/trust, original single `key-1` signer and selected
fixture registration; explicit wrong secret/excess scopes must be denied; all
original durable bytes and receipts must remain identical; every exact cleanup
obligation must pass. A helper-only DNS/JWKS success is insufficient. If Docker
container-network sharing does not supply issuer hostname resolution, this case
fails; the runner does not rewrite issuer or bypass trust to hide it.
**Extended oracle:** hard-parent-loss pauses only after candidate provision has
imported synthetic originals and launched the original process, then the driver
SIGKILLs the exact checkpointed owner via pidfd. Deadline similarly pauses after
import with a tightened20-second source execution deadline. The independent
candidate reaper must quiesce that fresh invocation and children before exact
cleanup. A150-second driver ceiling per case is an outer observation bound, not
a reason to claim cleanup on timeout. Other owner execution deadlines are120s.
The `nonzero-after-accept` hook runs the real first Docker service-container
creation, then replaces its successful CLI response with returncode1. It changes
no recovery implementation. The candidate must retain sticky daemon uncertainty,
refuse recovery success and report `bounded_cleanup_uncertain` even if all named
resources subsequently read absent. Every generated name is in the candidate's
registered cleanup inventory before these operations.
On any unexpected outcome, stop the fixture and retain the safe result/checkpoint
and reaper outcome. Do not retry uncertainty as success or delete by wildcard.
Manual recovery is restricted to the exact recorded names after checking their
run label and exact tmpfs driver/options, using the fixed explicit endpoint;
plaintext cleanup must use its registered device/inode/owner guards. The source
reaper remains responsible for bounded recovery after owner loss.
No execution or integration is approved by this proposal.67 unit/synthetic tests
pass; two use actual disposable local processes, while all Docker/native service
outcomes remain untested. Frozen revision4 retains the original counterexample
and its source bytes.

View file

@ -0,0 +1,184 @@
# T18 KeyCape recovery adapter candidate, revision 5
**Pending root review and native fixture proof. T18 remains open.** Source and
67 passing synthetic tests are prepared in `/tmp/nk49-keycape-adapter-next` and
copied here. No canonical owner file, credential, actual recovery, Docker run,
production route or secret-bearing archive was touched. `candidate.patch` adds
five files to NetKingdom; exact digests/check provenance are in `preparation.json`.
Files:
- `isolated-keycape.py`: archive/source/containment/native acceptance checks.
- `isolated-keycape-session.py`: existing-pipeline decryption, private import,
isolated original-image session, HTTPS transport, checks and cleanup.
- `run-isolated-keycape.sh`: attended owner launcher through the existing hidden
custody helper, with target preflight before the backup-key prompt.
- Two focused test files: check-stage negatives and session/cleanup ordering.
The wrapper uses existing `isolated-lldap`, offline transfer validation,
`verify-identity.custody` and `identity-bundle.restore` APIs. It checks the exact
independently retrieved ciphertext length/digest, custody recipient, complete
archive manifest and original immutable archived KeyCape image. Decryption and
archive restore remain on `/dev/shm`. The private manifest digest comes from
owner admission; it is never emitted publicly. Archive commands/args/envFrom and
non-KeyCape environment overrides are refused. Runtime environment permits only
original KeyCape deliveries, PATH/HOME/HOSTNAME and the exact admitted
SSL_CERT_FILE; duplicate or additional security environment (including
SSL_CERT_DIR/GODEBUG/PYTHONOPTIMIZE/proxy overrides) is refused. Original config/complete static
registry, signing key, policy/receipt tree, factor delivery and original delivered
environment are preserved. Secrets reach Docker by environment **names** only,
with values in the owner subprocess environment, never argv or printed output.
The original KeyCape image is distroless. Docker's documented
[cp corner cases](https://docs.docker.com/reference/cli/docker/container/cp/#corner-cases)
exclude tmpfs and user-created mounts. Consequently the candidate shares exactly
four Docker **local-driver tmpfs** volumes between the existing pinned Python
helper and original KeyCape issuer. Driver, scope, exact options, owner label,
source, target and read/write flags are inspected before private import. There
are no host directory mounts, disk volumes, new image, JWT verifier or execution
service. The helper's private import and comparison occur inside those exact
mounts; daemon `cp` is used only for public `/keycape` executable readback from
the original image root filesystem. This volume extension still needs native
pinned-image fixture verification before approval.
Both containers use the fixed local Docker engine, no network interfaces beyond
loopback, readonly root, nonroot UID, no capabilities/devices/privilege/host
namespaces or unadmitted security options. Peers share the exact network-none
anchor. Only namespace-local unprivileged-port sysctl `0` is admitted so the
existing nonroot Python helper can present original HTTPS on loopback443 and
forward to the original KeyCape process on loopback8080. The issuer uses its exact
original image default `/keycape` launch: Entrypoint/Cmd and actual Path/Args are
inspected; the original file/environment bytes and executable digest are bound.
Production dependency addresses remain unreachable. No readiness, LDAP,
Authelia, MFA, tenant-engine or notification endpoint is imitated. A selected
service registration can be checked without claiming `/readyz` passed.
The owner supplies an existing trusted issuer certificate/private key and trust
bundle through private regular files. No certificate, CA, signing key, client
registration or credential is generated/reissued. Original HTTPS hostname, trust
and same-origin discovery/JWKS are retained, without redirect/proxy/insecure
bypass. The public JWKS probe requires exactly the original owner's single RSA
signing key with `use:sig`, `alg:RS256`, `kid:key-1`, matching the original key's
public numbers. It checks before and after the native exchange. Every security
condition uses explicit refusal, including when Python optimization is enabled.
This probe only compares public discovery/JWKS data; JWT/exchange/claim/scope and
wrong-secret verification exclusively use shipped `/keycape verify-client`.
The negative check supplies a generated unrelated random secret through the
existing `-previous-secret-env` refusal mechanism. The bounded receipt says
**wrong unrelated secret refused**, never rotation. An absent exact PASS,
nonzero exit, TLS/discovery error, malformed output or any FAIL refuses evidence.
All durable private files and the full receipt tree are compared before/after;
extra receipt files refuse continuity. Public receipts expose only booleans,
public signing SPKI fingerprint, ciphertext/source hashes and exact safe cleanup
obligations. No config, env value, private digest, credential, JWT, response body,
subject or client ID is copied into receipts.
Normal/failure cleanup attempts every safely owned container and tmpfs volume,
records exact verified/uncertain obligations and refuses success if any removal
is uncertain. Before the first plaintext write, the wrapper registers the exact
private `/dev/shm` directory inode/device/owner/mode and every future container,
volume and public-image-probe obligation; it requires a reaper ACK and fsyncs a
safe receipt checkpoint. Plaintext removal uses directory FDs and refuses
replacement inode, owner mismatch, symlink, hardlink or special-file traversal.
Docker failure does not skip this separate plaintext obligation.
Revision4 creates a **fresh exclusive execution session/process group** before
reading TLS or custody inputs. It never reuses a shell/custody/user group. The
independent reaper receives only the exact parent pidfd, public identifiers and
fixed endpoint, with no custody descriptor or service environment. Parent loss
or the 1800-second deadline first quiesces the owner invocation and its children:
parent identity is bound by pidfd; each remaining same-SID/PGID/UID member is
compared by stable process start time and signaled through its own pidfd. Unknown
membership, PID reuse or uninterruptible survivors refuses cleanup. No broad
user/inherited-group kill is used. Only after no live creator remains may the
reaper remove resources. Every Docker call also refuses an expired or mismatched
execution binding; host subprocesses cannot request a new session/group escape.
A CLI can die after submitting a daemon request. Public exact sibling
`-docker-inflight`/`-docker-uncertain` markers survive plaintext removal, so even
clear later resource readback cannot turn an in-flight request into proven
cleanup. The reaper writes an exclusive bounded `-reaper.json` outcome next to
the registered staging directory; its exact path is in the pre-decrypt receipt.
Normal verified cleanup cancels the reaper. These are scoped invocation processes,
not standing jobs. This revision's real disposable process tests prove the
local shutdown/quiescence behavior only; Docker tmpfs/import/issuer behavior
still requires independent native fixture proof.
## Exact owner inputs and invocation, after approval/native fixture proof
The launcher takes six paths/public arguments:
```
bash tools/bootstrap-recovery/run-isolated-keycape.sh \
VERIFIED_TRANSFER_RECEIPT PRIVATE_ADMISSION ADMISSION_SHA256 \
PRIVATE_EXISTING_TLS_CERT PRIVATE_EXISTING_TLS_KEY PRIVATE_ORIGINAL_TRUST
```
Do not run this candidate until root review and synthetic pinned-image runtime
proof admit the exact source. It prompts only for the independently held age key
using the existing custody helper. Current client credentials come only from the
original encrypted archive's already delivered registration/environment; no key,
password, factor or TLS private input is passed to the agent.
The hash-bound owner admission JSON has `ciphertext_sha256`, `manifest_sha256`,
`verifier_sha256` (original image `/keycape` binary), `trust_sha256`,
`tls_cert_sha256`, original `issuer`, and `consumer` containing existing exact
`client_id`, `secret_env`, `audience`, `scope`, `subject`, `tenant`, `roles`,
`deny_scope`. Private `manifest_sha256` is SHA256 of
`json.dumps(manifest, sort_keys=True, separators=(',', ':')).encode()` from the
admitted original archive. The wrapper checks it against the validated manifest,
not an arbitrary directory. Trust path/session names are derived internally;
operator input cannot select a remote Docker engine or arbitrary container set.
The selected original issuer configuration must use HTTPS443 and local service
port8080, as the admitted owner release does. Other configurations fail closed
and need explicit source review, rather than silent rewrite.
## Remaining proof and retained review findings
The 67 tests use synthetic data and mocked Docker/native outcomes. They exercise
archive integrity/symlink/lineage drift, optimizer-safe JWKS wrong-key/extra-key/
wrong-kid/cross-origin/DNS negatives, default launch binding, capabilities/devices/
namespace/security overrides, transport-vs-wrong-secret distinction, private
import ordering, secret-free argv, shared-tmpfs planning, and partial/uncertain
cleanup. Two additional real disposable process tests exercise only synthetic owner/
grandchild shutdown and hard parent loss, without Docker. The suite does
**not** prove the actual pinned image supports the complete
session or that the selected current credential/factor works.
Next: root review; native disposable pinned-image fixture with original hostname
and a synthetic trusted TLS pair; positive/negative service behavior and hard
interruption cleanup; then an admitted owner-run actual recovery. Actual T18
still needs selected downstream-consumer reconnect and owner acceptance. Human/
browser flow uses the existing `keycape login`; retained-factor MFA stays T19.
Both remain explicitly unproven in the receipt.
Prior exact source snapshots are retained under `revisions/r1/`, `revisions/r2/`
and `revisions/r3/`; frozen test roots/closure manifests accompany R3 and R4. Root's findings were added
as regression tests: added caps/devices/security exceptions; cleanup failure
incorrectly preserving success or abandoning later obligations; Python assert
optimization; extra JWKS signer ambiguity; same-image alternate launch; and
unsupported private tmpfs daemon-copy assumptions. Revision4 addresses these in source, plus the later lifecycle and target
findings. See `LIFECYCLE-R4.md` for the remaining native fixture oracle. No root approval or native acceptance is fabricated.
Knowledge orientation: emergent-security0.2.6, digest
`8d1d6a9d4d07fec7b619618de7dfe2254c1790f85f4198c7fe19db9751c02`,
withheld/changed-source status; current owner AGENTS/companion/recovery contract
and verifier source were reviewed. Supervised assignment; no autopilot or
privileged execution claimed.
## Revision5 fixture proposal
Root reproduced Docker returncode1 after daemon acceptance; R5 records sticky
uncertainty for nonzero results before clearing the inflight marker. A regression
retains that counterexample. Public readiness failures are closed false data,
while a failing Docker CLI remains uncertain. Source review also found that
`policy.Open` requires an O_RDWR lock: only the policy tmpfs is writable in the
issuer, with exact original policy/history/lock bytes/tree still compared.
`FIXTURE-R5.md` contains concrete positive-first and optional extended commands,
resource limits, cleanup and oracles. The hash-bound runner invokes the candidate
with the existing complete synthetic archive fixture factory; it does not accept
real custody or identity input. Its default plan was inspected without Docker.
67 synthetic tests pass, including the earlier two real disposable process tests;
no native Docker compatibility or owner acceptance is claimed. R4 remains frozen.

View file

@ -0,0 +1,210 @@
#!/usr/bin/env python3
"""Age-only SOPS recovery to a new private file; no plaintext process output."""
from __future__ import annotations
import json
import os
from pathlib import Path
import re
import shutil
import signal
import stat
import subprocess
import sys
import tempfile
import uuid
ROOT = Path(__file__).resolve().parents[2]
RECIPIENT = re.compile(r'age1[023456789acdefghjklmnpqrstuvwxyz]{58}\Z')
class Refused(Exception):
pass
def public_recipient():
lines = (ROOT / 'keys/age.pub').read_text().splitlines()
values = [line.strip() for line in lines if line.strip() and not line.startswith('#')]
if len(values) != 1 or not RECIPIENT.fullmatch(values[0]):
raise Refused('registered public recipient is invalid')
result = subprocess.run(['age', '-r', values[0]], input=b'', stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL, timeout=10)
if result.returncode:
raise Refused('registered public recipient is invalid')
return values[0]
def unique_object(pairs):
result = {}
for key, value in pairs:
if key in result:
raise Refused('duplicate encrypted document key')
result[key] = value
return result
def parse_document(raw, suffix):
if suffix == '.json':
data = json.loads(raw, object_pairs_hook=unique_object)
kind = 'json'
elif suffix in {'.yaml', '.yml'}:
import yaml
class UniqueLoader(yaml.SafeLoader):
def construct_mapping(self, node, deep=False):
self.flatten_mapping(node)
return unique_object((self.construct_object(key, deep=deep),
self.construct_object(value, deep=deep))
for key, value in node.value)
try:
data = yaml.load(raw, Loader=UniqueLoader)
except yaml.YAMLError:
raise Refused('invalid encrypted YAML input') from None
kind = 'yaml'
else:
raise Refused('age recovery supports SOPS JSON/YAML input')
return data, kind
def age_document(source, recipient, *, raw=None):
# Inspect ciphertext metadata before giving a credential to any subprocess.
# This repository's admitted SOPS profile is age-only, not arbitrary cloud
# KMS, key services, SSH identities or executable age plugins.
if raw is None:
with source.open("rb") as encrypted:
raw = encrypted.read(8 * 1024 * 1024 + 1)
if len(raw) > 8 * 1024 * 1024:
raise Refused('encrypted input is too large')
data, kind = parse_document(raw, source.suffix)
metadata = data.get('sops') if isinstance(data, dict) else None
if not isinstance(metadata, dict):
raise Refused('SOPS metadata is missing')
known = {'age', 'kms', 'gcp_kms', 'azure_kv', 'hc_vault', 'hckms', 'pgp',
'key_groups', 'shamir_threshold', 'lastmodified', 'mac', 'version',
'unencrypted_suffix', 'encrypted_suffix', 'unencrypted_regex',
'encrypted_regex', 'unencrypted_comment_regex', 'encrypted_comment_regex',
'mac_only_encrypted'}
if set(metadata) - known or metadata.get('key_groups'):
raise Refused('unsupported SOPS custody profile')
if any(metadata.get(key) for key in ('kms', 'gcp_kms', 'azure_kv', 'hc_vault', 'hckms', 'pgp')):
raise Refused('recovery requires age-only metadata')
recipients = metadata.get('age')
if not isinstance(recipients, list) or not recipients:
raise Refused('age recipient metadata is missing')
if any(not isinstance(row, dict) or not RECIPIENT.fullmatch(str(row.get('recipient', '')))
for row in recipients):
raise Refused('unsupported age recipient')
if recipient not in [row['recipient'] for row in recipients]:
raise Refused('document does not include the registered recipient')
return raw, kind
def private_key_fd():
name = os.environ.get('NETKINGDOM_CUSTODY_AGE_KEY_FILE', '')
if not name:
raise Refused('explicit temporary custody key required')
path = Path(os.path.abspath(name))
if any(p.is_symlink() for p in (path, *path.parents)):
raise Refused('custody key path must not contain symlinks')
fd = os.open(path, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK)
info = os.fstat(fd)
if not stat.S_ISREG(info.st_mode) or info.st_uid != os.getuid() or info.st_mode & 0o077 or info.st_nlink != 1 or info.st_size > 16384:
os.close(fd)
raise Refused('custody key must be an owned private regular file')
return fd
def decrypt(recipient):
source_name = os.environ.get('NK_SOPS_FILE', '')
output_name = os.environ.get('NK_SOPS_OUTPUT', '')
if not source_name or not output_name:
raise Refused('supply FILE and OUTPUT; plaintext stdout is disabled')
source = Path(source_name).resolve(strict=True)
output = Path(output_name).absolute()
parent = output.parent.resolve(strict=True)
if any((p / '.git').is_file() or (p / '.git/HEAD').is_file()
or ((p / 'HEAD').is_file() and (p / 'objects').is_dir() and (p / 'refs').is_dir())
for p in [parent, *parent.parents]):
raise Refused('plaintext output must be outside Git worktrees')
raw, kind = age_document(source, recipient)
binary = shutil.which('sops')
if binary is None:
raise Refused('sops executable is unavailable')
directory = os.open(parent, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW)
temporary = '.netkingdom-sops-' + uuid.uuid4().hex
keyfd = None
try:
info = os.fstat(directory)
if info.st_uid != os.getuid() or info.st_mode & 0o077:
raise Refused('OUTPUT requires an owned private parent directory')
try:
os.stat(output.name, dir_fd=directory, follow_symlinks=False)
except FileNotFoundError:
pass
else:
raise Refused('OUTPUT already exists')
keyfd = private_key_fd()
derived = subprocess.run(['age-keygen', '-y', f'/proc/self/fd/{keyfd}'],
stdout=subprocess.PIPE, stderr=subprocess.DEVNULL,
pass_fds=(keyfd,), timeout=10)
if derived.returncode or derived.stdout.strip() != recipient.encode():
raise Refused('custody key does not match the registered recipient')
fd = os.open(temporary, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW,
0o600, dir_fd=directory)
with os.fdopen(fd, 'wb') as plain, tempfile.TemporaryDirectory(prefix='netkingdom-sops-home-') as isolated:
# HOME/XDG isolation matters: SOPS loads ambient keys in addition to
# an explicit key file. No inline keys, SSH keys, key commands,
# proxies or remote keyservice configuration are inherited.
env = {'PATH': os.environ.get('PATH', os.defpath), 'HOME': isolated,
'XDG_CONFIG_HOME': isolated, 'GNUPGHOME': isolated,
'SOPS_AGE_KEY_FILE': f'/proc/self/fd/{keyfd}',
'AWS_EC2_METADATA_DISABLED': 'true'}
result = subprocess.run([binary, '--decrypt', '--input-type', kind,
'--output-type', kind, '/dev/stdin'],
input=raw, stdout=plain, stderr=subprocess.DEVNULL,
env=env, cwd=isolated, pass_fds=(keyfd,), timeout=30)
if result.returncode:
raise Refused('SOPS decryption failed; no plaintext published')
plain.flush()
os.fsync(plain.fileno())
# Atomic no-overwrite publication within the private directory.
os.link(temporary, output.name, src_dir_fd=directory, dst_dir_fd=directory,
follow_symlinks=False)
os.unlink(temporary, dir_fd=directory)
os.fsync(directory)
finally:
if keyfd is not None:
os.close(keyfd)
try:
os.unlink(temporary, dir_fd=directory)
except FileNotFoundError:
pass
os.close(directory)
print('Decrypted to the requested private file. Remove it after the approved operation.')
def interrupted(_signum, _frame):
raise Refused('operation interrupted')
def main():
for sig in (signal.SIGTERM, signal.SIGHUP, signal.SIGINT):
signal.signal(sig, interrupted)
try:
if len(sys.argv) != 2 or sys.argv[1] not in {'setup', 'decrypt'}:
raise Refused('unsupported custody operation')
recipient = public_recipient()
if sys.argv[1] == 'setup':
print('Registered public recipient validated. No private key was read, copied or generated.')
else:
decrypt(recipient)
return 0
except Refused as exc:
print('ERROR: ' + str(exc), file=sys.stderr)
except (OSError, ValueError, TypeError, ImportError, subprocess.SubprocessError):
# Parser, filesystem and tool errors can include user data. Never echo.
print('ERROR: custody operation failed; input and tool diagnostics suppressed.', file=sys.stderr)
return 1
if __name__ == '__main__':
raise SystemExit(main())

View file

@ -0,0 +1,138 @@
#!/usr/bin/env python3
"""In-pod key transport. Only invoke through key-custody.py (captured output)."""
import base64
import fcntl
import json
import os
from pathlib import Path
import secrets
import stat
import sys
FILES = ('enckey', 'private.pem', 'public.pem')
LIMIT = 65536
class Refused(Exception):
pass
def validate(values):
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import rsa
if set(values) != set(FILES) or len(values['enckey']) != 96:
raise Refused()
if any(not value or len(value) > LIMIT for value in values.values()):
raise Refused()
private = serialization.load_pem_private_key(values['private.pem'], password=None)
public = serialization.load_pem_public_key(values['public.pem'])
if (not isinstance(private, rsa.RSAPrivateKey) or private.key_size < 2048
or not isinstance(public, rsa.RSAPublicKey)
or private.public_key().public_numbers() != public.public_numbers()):
raise Refused()
def read(directory, name):
fd = os.open(name, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK, dir_fd=directory)
try:
info = os.fstat(fd)
forbidden = 0o022 if name == 'public.pem' else 0o027
if not stat.S_ISREG(info.st_mode) or info.st_nlink != 1 or info.st_mode & forbidden:
raise Refused()
with os.fdopen(os.dup(fd), 'rb') as stream:
data = stream.read(LIMIT + 1)
after = os.stat(name, dir_fd=directory, follow_symlinks=False)
if len(data) > LIMIT or any(getattr(info, k) != getattr(after, k)
for k in ('st_dev', 'st_ino', 'st_size', 'st_mtime_ns', 'st_ctime_ns')):
raise Refused()
return data
finally:
os.close(fd)
def install(directory, values, *, uid=0, gid=999):
"""Verify every existing file before creating anything; never replace."""
missing = []
for name, value in values.items():
try:
if read(directory, name) != value:
raise Refused()
except FileNotFoundError:
missing.append(name)
for name in missing:
temp = '.key-custody-' + secrets.token_hex(12)
fd = os.open(temp, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW,
0o600, dir_fd=directory)
try:
with os.fdopen(fd, 'wb') as stream:
stream.write(values[name]); stream.flush()
os.fchown(stream.fileno(), uid, gid)
os.fchmod(stream.fileno(), 0o644 if name == 'public.pem' else 0o640)
os.fsync(stream.fileno())
os.link(temp, name, src_dir_fd=directory, dst_dir_fd=directory, follow_symlinks=False)
os.unlink(temp, dir_fd=directory)
os.fsync(directory)
finally:
try:
os.unlink(temp, dir_fd=directory)
except FileNotFoundError:
pass
if any(read(directory, name) != value for name, value in values.items()):
raise Refused()
def operate(request, root=Path('/etc/privacyidea'), *, uid=0, gid=999):
if any(p.is_symlink() for p in (root, *root.parents)):
raise Refused()
directory = os.open(root, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW)
try:
# The admitted service uses fsGroup 999, which may make its PVC group-writable.
if os.fstat(directory).st_mode & 0o002:
raise Refused()
fcntl.flock(directory, fcntl.LOCK_EX | fcntl.LOCK_NB)
if request == {'action': 'initialize-audit'}:
# This explicit first-bootstrap action never creates/replaces enckey.
enckey = read(directory, 'enckey')
if len(enckey) != 96:
raise Refused()
for name in ('private.pem', 'public.pem'):
try:
os.stat(name, dir_fd=directory, follow_symlinks=False)
except FileNotFoundError:
continue
raise Refused()
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import rsa
private = rsa.generate_private_key(public_exponent=65537, key_size=2048)
values = {'enckey': enckey,
'private.pem': private.private_bytes(serialization.Encoding.PEM,
serialization.PrivateFormat.TraditionalOpenSSL, serialization.NoEncryption()),
'public.pem': private.public_key().public_bytes(serialization.Encoding.PEM,
serialization.PublicFormat.SubjectPublicKeyInfo)}
validate(values)
install(directory, values, uid=uid, gid=gid)
return {'result': 'initialized'}
if request == {'action': 'export'}:
values = {name: read(directory, name) for name in FILES}
validate(values)
return {name: base64.b64encode(value).decode() for name, value in values.items()}
if set(request) == {'action', 'keys'} and request['action'] == 'restore':
values = {name: base64.b64decode(value, validate=True) for name, value in request['keys'].items()}
validate(values)
install(directory, values, uid=uid, gid=gid)
return {'result': 'restored'}
raise Refused()
finally:
os.close(directory)
if __name__ == '__main__':
try:
raw = sys.stdin.buffer.read(4 * LIMIT + 1)
if len(raw) > 4 * LIMIT:
raise Refused()
print(json.dumps(operate(json.loads(raw))))
except Exception:
# Transport may carry key bytes; diagnostics must never serialize them.
print('Key custody operation refused or failed.', file=sys.stderr)
sys.exit(1)

View file

@ -0,0 +1,219 @@
#!/usr/bin/env python3
"""Prepare/run the exact reviewed T18 candidate on disposable synthetic material.
Default --plan performs no Docker calls and generates no key/material. Execution
requires the reviewed fixture manifest hash and source closure. This runner calls
the candidate itself; hooks only pause or inject a post-response transport fault.
"""
import argparse
import base64
from datetime import datetime,timedelta,timezone
import hashlib
import importlib.util
import io
import json
import os
from pathlib import Path
import signal
import subprocess
import sys
import tarfile
import tempfile
import time
import uuid
ROOT=Path(__file__).resolve().parents[1]
MANIFEST=ROOT/'tests/isolated-keycape-native-fixture-manifest.json'
ISSUER='https://nk49-fixture.invalid'
CASES=('positive','hard-parent-loss','deadline','nonzero-after-accept')
def load(name,path):
spec=importlib.util.spec_from_file_location(name,path)
module=importlib.util.module_from_spec(spec);spec.loader.exec_module(module);return module
s=load('session',ROOT/'tools/bootstrap-recovery/isolated-keycape-session.py')
def require(condition):
if not condition:raise RuntimeError('synthetic_fixture_refused')
def reviewed_manifest(expected):
raw=MANIFEST.read_bytes();require(hashlib.sha256(raw).hexdigest()==expected)
manifest=json.loads(raw)
for name,digest in manifest['source_sha256'].items():
require(hashlib.sha256((ROOT/name).read_bytes()).hexdigest()==digest)
return manifest
def private_write(path,value):
path.parent.mkdir(mode=0o700,parents=True,exist_ok=True)
fd=os.open(path,os.O_WRONLY|os.O_CREAT|os.O_EXCL|os.O_NOFOLLOW,0o600)
with os.fdopen(fd,'wb') as out:out.write(value)
def materials(root):
"""Reuse the existing full archive fixture factory, no real archive accepted."""
from cryptography import x509
from cryptography.hazmat.primitives import hashes,serialization
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.x509.oid import NameOID
import yaml
existing=load('existing_bundle_fixture',ROOT/'tests/test_identity_bundle.py')
payload=existing.stage.__wrapped__(root)
config={'issuer':ISSUER,'port':8080,'privateKeyPem':'/etc/keycape/key.pem','environment':'dev','tokenLifetime':'5m',
'lldap':{'url':'ldap://127.0.0.1:1','bindDN':'cn=fixture','bindPW':'synthetic-only','baseDN':'dc=fixture'},
'authelia':{'baseURL':'http://127.0.0.1:1','clientId':'synthetic','clientSecret':'synthetic','redirectURI':ISSUER+'/authorize/callback'},
'privacyidea':{'baseURL':'http://127.0.0.1:1'},
'clients':[{'clientId':'nk49-fixture-consumer','clientType':'confidential','audience':'nk49-fixture',
'grantTypes':['client_credentials'],'allowedScopes':['fixture:read'],'secretRef':'env:KEYCAPE_FIXTURE_CLIENT_SECRET',
'serviceSubject':'service:nk49-fixture','tenant':'tenant:fixture','roles':['fixture-reader']}]+
[{'clientId':name,'displayName':name,'clientType':'public','grantTypes':['authorization_code'],
'redirectUris':['https://nk49-fixture.invalid/callback'],'allowedScopes':['openid'],'mfaRequired':True}
for name in ('vergabe-demo-company','user-engine-portal')]}
secret_path=payload/'resources/sso/secret/keycape-config.json';secret=json.loads(secret_path.read_text())
secret['data']['config.yaml']=base64.b64encode(yaml.safe_dump(config).encode()).decode();secret_path.write_text(json.dumps(secret))
client_path=payload/'resources/sso/secret/keycape-rapp-qonto-client.json';client=json.loads(client_path.read_text())
client['data']={'client-secret':base64.b64encode(b'synthetic-fixture-client-secret').decode()};client_path.write_text(json.dumps(client))
factor_path=payload/'resources/sso/secret/keycape-factor-read.json';factor=json.loads(factor_path.read_text())
factor['data']={'admin-token':base64.b64encode(b'synthetic-unused-factor-reader').decode()};factor_path.write_text(json.dumps(factor))
dep_path=payload/'resources/sso/deployment/keycape.json';dep=json.loads(dep_path.read_text())
dep['spec']={'template':{'spec':{'containers':[{'name':'keycape','image':s.adapter.IMAGE,'env':[
{'name':'KEYCAPE_CONFIG','value':'/etc/keycape/config.yaml'},
{'name':'KEYCAPE_POLICY_PATH','value':'/var/lib/keycape-policy/policy.json'},
{'name':'KEYCAPE_FIXTURE_CLIENT_SECRET','valueFrom':{'secretKeyRef':{'name':'keycape-rapp-qonto-client','key':'client-secret'}}}]}]}}};dep_path.write_text(json.dumps(dep))
policy={'Revision':1,'Modes':{k:'mandatory' for k in ('vergabe-demo-company','user-engine-portal')},'History':[
{'reference':'synthetic-retained-receipt','actor':'service:fixture','client':'user-engine-portal','before':'mandatory','after':'mandatory','revision':1,'at':1}]}
# JSON tags in the owner store are lowercase; retain an existing lock inode
# representation as part of the archived tree, never synthesize it after import.
policy={k.lower():v for k,v in policy.items()};(payload/'data/keycape/policy.json').write_text(json.dumps(policy))
private_write(payload/'data/keycape/policy.json.lock',b'')
key=rsa.generate_private_key(public_exponent=65537,key_size=2048)
subject=x509.Name([x509.NameAttribute(NameOID.COMMON_NAME,'nk49-fixture.invalid')]);now=datetime.now(timezone.utc)
cert=(x509.CertificateBuilder().subject_name(subject).issuer_name(subject).public_key(key.public_key()).serial_number(x509.random_serial_number())
.not_valid_before(now-timedelta(minutes=5)).not_valid_after(now+timedelta(days=1))
.add_extension(x509.SubjectAlternativeName([x509.DNSName('nk49-fixture.invalid')]),critical=False)
.add_extension(x509.BasicConstraints(ca=True,path_length=None),critical=True).sign(key,hashes.SHA256()))
tls={'/recovery-tls/server.pem':cert.public_bytes(serialization.Encoding.PEM),
'/recovery-tls/server-key.pem':key.private_bytes(serialization.Encoding.PEM,serialization.PrivateFormat.PKCS8,serialization.NoEncryption()),
'/recovery-tls/trust.pem':cert.public_bytes(serialization.Encoding.PEM)}
for path,value in tls.items():private_write(root/Path(path).name,value)
age=root/'synthetic-age.key';s.run(['age-keygen','-o',str(age)])
recipient=s.run(['age-keygen','-y',str(age)]).decode().strip();profile=s.bundle.profile();profile['public_recipient']=recipient
s.bundle.profile=lambda:profile
artifact=root/'net-kingdom-identity-20261016T063159Z-0123456789ab.tar.age';s.bundle.seal(payload,artifact,{'synthetic_native_keycape_fixture':True})
raw=s.run(['age','-d','-i',str(age),str(artifact)],max_output=s.bundle.LIMIT)
with tarfile.open(fileobj=io.BytesIO(raw)) as archive:manifest=json.load(archive.extractfile('manifest.json'))
transfer={'schema':'netkingdom.identity-offsite.v1','status':'readback_verified','provider':'Storage Share',
'folder':'Backup/railiance-backups/net-kingdom-identity','artifact':artifact.name,'ciphertext_bytes':artifact.stat().st_size,
'ciphertext_sha256':hashlib.sha256(artifact.read_bytes()).hexdigest(),'etag':'"synthetic"','fetched_path':str(artifact),
'independent_owner_retrieval_verified':True}
# Derive public executable identity only from the exact cached admitted image.
probe=s.plan_session();name=probe['public_image_probe']
try:
s.run(['docker','create','--pull=never','--name',name,'--network','none','--read-only','--label',s.adapter.LABEL+'='+probe['run_id'],s.adapter.IMAGE])
binary=s.adapter.container_file(name,'/keycape')
finally:s.adapter.cleanup({**probe,'containers':[name]}, {})
admission={'issuer':ISSUER,'ciphertext_sha256':transfer['ciphertext_sha256'],
'manifest_sha256':hashlib.sha256(json.dumps(manifest,sort_keys=True,separators=(',',':')).encode()).hexdigest(),
'verifier_sha256':hashlib.sha256(binary).hexdigest(),'trust_sha256':hashlib.sha256(tls['/recovery-tls/trust.pem']).hexdigest(),
'tls_cert_sha256':hashlib.sha256(tls['/recovery-tls/server.pem']).hexdigest(),
'consumer':{'client_id':'nk49-fixture-consumer','secret_env':'KEYCAPE_FIXTURE_CLIENT_SECRET','audience':'nk49-fixture','scope':'fixture:read',
'subject':'service:nk49-fixture','tenant':'tenant:fixture','roles':'fixture-reader','deny_scope':'fixture:write'}}
private_write(root/'fixture.json',json.dumps({'schema':'nk49.generated-keycape-fixture.v1','transfer':transfer,'admission':admission,'recipient':recipient}).encode())
def worker(root,case):
execution=s.isolate_execution();execution['deadline']=time.monotonic()+(20 if case=='deadline' else 120)
data=json.loads((root/'fixture.json').read_text());require(data['schema']=='nk49.generated-keycape-fixture.v1')
profile=s.bundle.profile();profile['public_recipient']=data['recipient'];s.bundle.profile=lambda:profile
s.local.verify.custody.private_key_fd=lambda:os.open(root/'synthetic-age.key',os.O_RDONLY|os.O_NOFOLLOW)
tls={'/recovery-tls/'+name:(root/name).read_bytes() for name in ('server.pem','server-key.pem','trust.pem')}
receipt={};output=root/(case+'.json');checkpoint=lambda r:output.write_text(json.dumps(r))
provision=s.provision
def paused(*args):
provision(*args)
if case in ('hard-parent-loss','deadline'):
private_write(root/(case+'.barrier'),b'private-import-and-default-launch-complete')
time.sleep(150)
s.provision=paused
if case=='nonzero-after-accept':
original=s.adapter.subprocess.run;injected=False
def fault(argv,**kwargs):
nonlocal injected
result=original(argv,**kwargs)
if argv[:3]==['docker','--host',s.adapter.ENDPOINT] and 'run' in argv and not injected and result.returncode==0:
injected=True
return subprocess.CompletedProcess(argv,1,b'',b'')
return result
s.adapter.subprocess.run=fault
try:s.execute(data['transfer'],data['admission'],tls,receipt,execution,checkpoint=checkpoint)
except BaseException:receipt['status']='failed'
finally:output.write_text(json.dumps(receipt))
return int(receipt.get('status')!='isolated_keycape_native_registration_checked')
def run_cases(root,summary,cases):
for case in cases:
child=subprocess.Popen([sys.executable,str(Path(__file__).resolve()),'--worker-root',str(root),'--worker-case',case,
'--execute-reviewed-fixture','--expected-manifest-sha256',summary['manifest_sha256']],stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL)
try:
end=time.monotonic()+150
if case=='hard-parent-loss':
while not (root/(case+'.barrier')).exists() and child.poll() is None and time.monotonic()<end:time.sleep(.05)
require((root/(case+'.barrier')).exists())
execution=json.loads((root/(case+'.json')).read_text())['execution_binding'];require(execution['pid']==child.pid)
fd=os.pidfd_open(child.pid)
try:signal.pidfd_send_signal(fd,signal.SIGKILL)
finally:os.close(fd)
child.wait(timeout=max(1,end-time.monotonic()))
receipt=json.loads((root/(case+'.json')).read_text())
summary['receipts'][case]=receipt
if case=='deadline':require((root/(case+'.barrier')).exists())
if case=='positive':
require(receipt.get('status')=='isolated_keycape_native_registration_checked' and receipt.get('container_cleanup_verified') and receipt.get('temporary_plaintext_cleanup_verified'))
require(receipt.get('native_service_registration_verified') and receipt.get('original_public_signing_key_in_live_jwks'))
summary['actual_issuer_original_hostname_tls_verified']=True
else:
require(receipt.get('status')!='isolated_keycape_native_registration_checked')
outcome=Path(receipt['reaper_outcome_path'])
while not outcome.exists() and time.monotonic()<end:time.sleep(.05)
require(outcome.exists());cleanup=json.loads(outcome.read_text())
require(cleanup.get('execution_quiescence_verified') and cleanup.get('temporary_plaintext_cleanup_verified'))
if case=='nonzero-after-accept':require(cleanup.get('status')=='bounded_cleanup_uncertain' and cleanup.get('in_flight_daemon_request_uncertain'))
else:require(cleanup.get('status')=='bounded_cleanup_verified')
summary['cases'][case]='passed'
finally:
if child.poll() is None:
fd=os.pidfd_open(child.pid)
try:signal.pidfd_send_signal(fd,signal.SIGKILL)
finally:os.close(fd)
child.wait(timeout=5)
def main():
p=argparse.ArgumentParser(description=__doc__);p.add_argument('--plan',action='store_true')
p.add_argument('--execute-reviewed-fixture',action='store_true');p.add_argument('--expected-manifest-sha256')
p.add_argument('--worker-root');p.add_argument('--worker-case',choices=CASES);p.add_argument('--case',choices=(*CASES,'all'),default='positive');args=p.parse_args()
if not args.execute_reviewed_fixture:
print(MANIFEST.read_text());return 0
require(not args.plan);manifest=reviewed_manifest(args.expected_manifest_sha256);os.umask(0o077)
if args.worker_root:
root=Path(args.worker_root);require(root.parent==Path('/dev/shm') and root.name.startswith('nk49-keycape-fixture-'))
s.bundle.private(root,True);return worker(root,args.worker_case)
require(not args.worker_case)
s.local.preflight();s.run(['docker','image','inspect',s.adapter.IMAGE,'--format','{{.Id}}'])
safe=Path(tempfile.mkdtemp(prefix='nk49-keycape-native-fixture-review-',dir='/tmp'))
summary={'schema':'nk49.synthetic-keycape-native-fixture.v1','status':'failed','manifest_sha256':args.expected_manifest_sha256,
'cases':{},'receipts':{},'actual_credentials_used':False,'actual_recovery_proven':False,'docker_executed':True}
print('Safe synthetic fixture result: '+str(safe/'result.json'),flush=True)
try:
with tempfile.TemporaryDirectory(prefix='nk49-keycape-fixture-',dir='/dev/shm') as temporary:
root=Path(temporary);materials(root);run_cases(root,summary,CASES if args.case=='all' else (args.case,))
summary['status']='synthetic_native_fixture_passed'
finally:(safe/'result.json').write_text(json.dumps(summary,indent=2)+'\n')
print('Safe synthetic fixture result: '+str(safe/'result.json'))
return int(summary['status']!='synthetic_native_fixture_passed')
if __name__=='__main__':raise SystemExit(main())

View file

@ -0,0 +1,225 @@
"""Actual SQLite/age/RSA fixtures and hostile archive/custody boundaries."""
import base64
import importlib.util
import io
import json
import os
from pathlib import Path
import sqlite3
import subprocess
import tarfile
import time
import uuid
from copy import deepcopy
import pytest
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import rsa
ROOT=Path(__file__).resolve().parents[1]
def load(name):
spec=importlib.util.spec_from_file_location(name,ROOT/f'tools/bootstrap-recovery/{name}.py')
m=importlib.util.module_from_spec(spec);spec.loader.exec_module(m);return m
b=load('identity-bundle');files=load('identity-files');capture=load('capture-identity')
@pytest.fixture
def stage(tmp_path):
os.umask(0o077);root=tmp_path/'payload';root.mkdir(mode=0o700)
key=rsa.generate_private_key(public_exponent=65537,key_size=2048)
private=key.private_bytes(serialization.Encoding.PEM,serialization.PrivateFormat.TraditionalOpenSSL,serialization.NoEncryption())
public=key.public_key().public_bytes(serialization.Encoding.PEM,serialization.PublicFormat.SubjectPublicKeyInfo)
for c in b.profile()['components']:
ns=c['namespace'];name=c['deployment']
d={'kind':'Deployment','metadata':{'name':name,'namespace':ns,'uid':c['deployment_uid']},'spec':{}}
b.save(root,f'resources/{ns}/deployment/{name}.json',json.dumps(d).encode())
for name in c['secret_refs']:
data={'fixture':base64.b64encode(b'synthetic-private-value-canary').decode()}
if name=='keycape-config':data={'key.pem':base64.b64encode(private).decode(),'config.yaml':base64.b64encode(b'issuer: https://isolated.invalid').decode()}
b.save(root,f'resources/{ns}/secret/{name}.json',json.dumps({'kind':'Secret','metadata':{'name':name,'namespace':ns},'data':data}).encode())
for name in c['configmap_refs']:
b.save(root,f'resources/{ns}/configmap/{name}.json',json.dumps({'kind':'ConfigMap','metadata':{'name':name,'namespace':ns},'data':{'fixture':'synthetic'}}).encode())
for component,name in [('lldap','users.db'),('authelia','db.sqlite3')]:
path=root/f'data/{component}/{name}';path.parent.mkdir(mode=0o700,parents=True)
with sqlite3.connect(path) as db:db.execute('CREATE TABLE retained(value TEXT)');db.execute("INSERT INTO retained VALUES ('fixture-preserved')")
path.chmod(0o600)
b.save(root,'data/keycape/policy.json',b'{"version":1}')
for name,value in [('enckey',os.urandom(96)),('private.pem',private),('public.pem',public)]:
b.save(root,'data/privacyidea/'+name,value)
b.save(root,'databases/privacyidea.dump',b'PGDMP-synthetic-format-only')
return root
def test_complete_bundle_has_native_integrity_checks(stage):
r=b.checks(stage)
assert r['sqlite_integrity_passed'] and r['privacyidea_key_format_and_pair_passed']
assert r['native_identity_recovery_proven'] is False
def test_real_age_seal_manifest_is_encrypted_and_plaintext_never_prints(stage,tmp_path,monkeypatch,capsys):
key=tmp_path/'fixture-age.key'
subprocess.run(['age-keygen','-o',str(key)],check=True,capture_output=True)
recipient=subprocess.run(['age-keygen','-y',str(key)],check=True,capture_output=True).stdout.decode().strip()
p=b.profile();p['public_recipient']=recipient;monkeypatch.setattr(b,'profile',lambda:p)
artifact=tmp_path/'identity.tar.age';r=b.seal(stage,artifact,{'test_only':True})
assert artifact.stat().st_mode&0o077==0
assert b'synthetic-private-value-canary' not in artifact.read_bytes()
result=subprocess.run(['age','-d','-i',str(key),str(artifact)],check=True,capture_output=True)
with tarfile.open(fileobj=io.BytesIO(result.stdout)) as archive:
manifest=json.load(archive.extractfile('manifest.json'))
assert len(manifest['files'])==r['file_count']
assert 'data/privacyidea/enckey' in archive.getnames()
assert 'data/lldap/users.db' in archive.getnames()
plain=tmp_path/'decrypted.tar';plain.write_bytes(result.stdout)
recovered=tmp_path/'recovered';recovered.mkdir(mode=0o700)
restored,checks=b.restore(plain,recovered)
assert checks['privacyidea_key_format_and_pair_passed']
assert (recovered/'data/privacyidea/enckey').read_bytes()==(stage/'data/privacyidea/enckey').read_bytes()
assert 'synthetic-private-value-canary' not in json.dumps(r)+capsys.readouterr().out
with pytest.raises(b.Refused):b.seal(stage,artifact,{})
@pytest.mark.parametrize('defect',['missing-key','bad-sqlite','bad-audit-pair','wrong-deployment','private-age-key','symlink','hardlink','unknown-payload'])
def test_incomplete_unsafe_and_mismatched_inputs_refuse(stage,tmp_path,defect):
if defect=='missing-key':(stage/'data/privacyidea/enckey').unlink()
if defect=='bad-sqlite':(stage/'data/lldap/users.db').write_bytes(b'broken')
if defect=='bad-audit-pair':
key=rsa.generate_private_key(public_exponent=65537,key_size=2048)
(stage/'data/privacyidea/public.pem').write_bytes(key.public_key().public_bytes(serialization.Encoding.PEM,serialization.PublicFormat.SubjectPublicKeyInfo))
if defect=='wrong-deployment':
p=stage/'resources/sso/deployment/keycape.json';r=json.loads(p.read_text());r['metadata']['uid']='replacement';p.write_text(json.dumps(r))
if defect=='private-age-key':(stage/'data/keycape/poison').write_bytes(b'AGE-SECRET-KEY-' + b'fixture')
if defect=='symlink':(stage/'data/keycape/linked').symlink_to(stage/'data/privacyidea/enckey')
if defect=='hardlink':os.link(stage/'data/privacyidea/enckey',stage/'data/keycape/linked')
if defect=='unknown-payload':(stage/'unreviewed').write_bytes(b'not-allowed')
with pytest.raises(Exception):
b.checks(stage);b.inventory(stage)
@pytest.mark.parametrize('name,kind',[('../escape','file'),('/absolute','file'),('data/keycape/valid','symlink')])
def test_tar_import_refuses_traversal_and_links(tmp_path,name,kind):
raw=io.BytesIO()
with tarfile.open(fileobj=raw,mode='w') as archive:
member=tarfile.TarInfo(name);member.size=1
if kind=='symlink':member.type=tarfile.SYMTYPE;member.linkname='/etc/passwd'
archive.addfile(member,io.BytesIO(b'x'))
with pytest.raises(b.Refused):b.import_files(raw.getvalue(),tmp_path,'sso')
def test_native_sqlite_snapshot_keeps_committed_wal_rows_and_source_unchanged(tmp_path):
root=tmp_path/'sources';root.mkdir();scratch=tmp_path/'scratch';scratch.mkdir()
for component in ['lldap','authelia','keycape']:(root/component).mkdir()
for component,name in [('lldap','users.db'),('authelia','db.sqlite3')]:
with sqlite3.connect(root/component/name) as con:
con.execute('CREATE TABLE retained(value TEXT)');con.execute("INSERT INTO retained VALUES ('native-snapshot')")
(root/'keycape/policy.json').write_text('{}')
live=sqlite3.connect(root/'lldap/users.db');live.execute('PRAGMA journal_mode=WAL')
live.execute("INSERT INTO retained VALUES ('committed-wal')");live.commit()
try:
result=files.capture('sso',root,scratch)
restored=tmp_path/'restored.sqlite';restored.write_bytes(result['data/lldap/users.db'])
with sqlite3.connect(restored) as con:assert con.execute('SELECT count(*) FROM retained').fetchone()==(2,)
assert list(scratch.iterdir())==[]
assert not any(name.endswith(('-wal','-shm')) for name in result)
finally:live.close()
def test_helper_manifest_has_no_writable_service_mount_or_credentials():
objects=capture.manifests('fixture')
for obj in objects:
if obj['kind']=='NetworkPolicy':
assert obj['spec']['egress']==[] and obj['spec']['ingress']==[]
else:
s=obj['spec'];assert s['automountServiceAccountToken'] is False
assert 'fsGroup' not in s['securityContext']
assert s['activeDeadlineSeconds']==900
assert all(v['persistentVolumeClaim']['readOnly'] for v in s['volumes'] if 'persistentVolumeClaim' in v)
assert not any('secret' in v for v in s['volumes'])
def test_cleanup_delete_carries_exact_uid_precondition(monkeypatch):
calls=[];monkeypatch.setattr(capture,'command',lambda args,payload=None,**kw:calls.append((args,payload)) or b'')
capture.delete_created({'kind':'Pod','metadata':{'namespace':'sso','name':'nk49-capture-fixture','uid':'exact-created-uid'}})
assert json.loads(calls[0][1])['preconditions']=={'uid':'exact-created-uid'}
@pytest.mark.parametrize('defect',['writable-mount','extra-container','service-token','different-image','host-network'])
def test_mutated_helper_is_refused_before_private_exec(defect):
wanted=next(o for o in capture.manifests('fixture') if o['kind']=='Pod')
actual=deepcopy(wanted);spec=actual['spec']
if defect=='writable-mount':spec['containers'][0]['volumeMounts'][0]['readOnly']=False
if defect=='extra-container':spec['containers'].append(deepcopy(spec['containers'][0]))
if defect=='service-token':spec['automountServiceAccountToken']=True
if defect=='different-image':spec['containers'][0]['image']='unreviewed:latest'
if defect=='host-network':spec['hostNetwork']=True
with pytest.raises(capture.Refused):capture.validate_helper(actual,wanted)
def test_namespace_dns_allow_is_excluded_only_for_capture_helpers():
selector={'matchExpressions':[{'key':'net-kingdom/capture','operator':'DoesNotExist'}]}
assert capture.selects(selector,{'app.kubernetes.io/name':'authelia'})
assert not capture.selects(selector,{'net-kingdom/capture':'fixture'})
def test_unknown_create_outcome_is_checkpointed_and_never_claimed_clean(tmp_path,monkeypatch):
monkeypatch.setattr(capture.bundle,'profile',lambda:{'components':[]})
monkeypatch.setattr(capture,'command',lambda *args,**kwargs:(_ for _ in ()).throw(TimeoutError()))
r={'started_at':'fixture'};checkpoints=[]
p={'run_id':'fixture','create_resources':[capture.manifests('fixture')[0]]}
with pytest.raises(capture.Refused):capture.capture(p,tmp_path/'unused.age',r,'fixture',lambda:checkpoints.append(deepcopy(r)))
assert r['helper_cleanup_verified'] is False and r['unverified_create_outcomes']
assert any(c.get('unverified_create_outcomes') for c in checkpoints)
@pytest.mark.skipif(os.environ.get('NK49_RUN_DISPOSABLE_POSTGRES_TEST')!='1',reason='explicit local Docker fixture opt-in')
def test_full_new_archive_decrypt_extract_native_database_restore_and_cleanup(stage,tmp_path,monkeypatch):
verify=load('verify-identity');fixture='nk49-bundle-fixture-'+uuid.uuid4().hex
def command(args,**kwargs):
return subprocess.run(args,check=True,stdout=subprocess.PIPE,stderr=subprocess.PIPE,timeout=120,**kwargs).stdout
try:
command(['docker','run','--detach','--pull=never','--name',fixture,'--network','none',
'--tmpfs','/var/lib/postgresql/data:rw,size=268435456','--env','POSTGRES_HOST_AUTH_METHOD=trust',
verify.pg.IMAGE,'postgres','-c','listen_addresses='])
for _ in range(60):
probe=subprocess.run(['docker','exec',fixture,'pg_isready','-U','postgres'],capture_output=True)
if probe.returncode==0:break
time.sleep(1)
command(['docker','exec','-i',fixture,'psql','-X','-U','postgres','-v','ON_ERROR_STOP=1'],
input=b'CREATE TABLE token(id integer); INSERT INTO token VALUES (1),(2); CREATE TABLE resolver(id integer); CREATE TABLE realm(id integer);')
(stage/'databases/privacyidea.dump').write_bytes(command(['docker','exec',fixture,'pg_dump','-U','postgres','-Fc','postgres']))
key=tmp_path/'disposable-age.key';command(['age-keygen','-o',str(key)])
recipient=command(['age-keygen','-y',str(key)]).decode().strip()
p=b.profile();p['public_recipient']=recipient
monkeypatch.setattr(b,'profile',lambda:p);monkeypatch.setattr(verify.bundle,'profile',lambda:p)
artifact=tmp_path/'net-kingdom-identity-20261016T000000Z-0123456789ab.tar.age'
c=b.seal(stage,artifact,{'test_only':True})
transfer={'schema':'netkingdom.identity-offsite.v1','status':'readback_verified',
'independent_owner_retrieval_verified':True,'fetched_path':str(artifact),'artifact':artifact.name,
'ciphertext_sha256':c['ciphertext_sha256'],'ciphertext_bytes':c['ciphertext_bytes']}
r={};fd=os.open(key,os.O_RDONLY)
try:verify.verify(transfer,fd,r)
finally:os.close(fd)
assert r['status']=='identity_archive_verified'
assert r['database_restore']['aggregate_row_counts']['token']==2
assert r['all_manifest_hashes_verified'] and r['wrong_key_refused']
assert r['container_cleanup_verified'] and r['temporary_plaintext_cleanup_verified']
assert not r['full_service_authentication_proven']
finally:subprocess.run(['docker','rm','--force','--volumes',fixture],stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL,timeout=30)
def test_runtime_host_requires_owner_and_exact_machine_binding(monkeypatch):
import hashlib,socket
module=load('identity-bundle')
current=module.profile();current['runtime_host']={'hostname':'verified-node','machine_id_sha256':hashlib.sha256(b'fixture-machine').hexdigest()}
monkeypatch.setattr(module,'profile',lambda:current)
monkeypatch.setattr(module.os,'geteuid',lambda:0);monkeypatch.setattr(socket,'gethostname',lambda:'verified-node')
original=Path.read_bytes
monkeypatch.setattr(Path,'read_bytes',lambda p:b'fixture-machine' if str(p)=='/etc/machine-id' else original(p))
module.require_runtime_host()
monkeypatch.setattr(socket,'gethostname',lambda:'foreign-node')
with pytest.raises(module.Refused):module.require_runtime_host()
monkeypatch.setattr(socket,'gethostname',lambda:'verified-node');monkeypatch.setattr(module.os,'geteuid',lambda:1000)
with pytest.raises(module.Refused):module.require_runtime_host()

View file

@ -0,0 +1,35 @@
import contextlib
import hashlib
import importlib.util
import io
import json
from pathlib import Path
import sys
import unittest
from unittest.mock import patch
ROOT=Path(__file__).resolve().parents[1]
spec=importlib.util.spec_from_file_location('fixture_runner',ROOT/'tests/run_isolated_keycape_native_fixture.py')
f=importlib.util.module_from_spec(spec);spec.loader.exec_module(f)
class FixturePreparationTests(unittest.TestCase):
def test_default_plan_neither_runs_docker_nor_generates_materials(self):
output=io.StringIO()
with patch.object(sys,'argv',['fixture','--plan']),patch.object(f.s,'run') as run,patch.object(f,'materials') as materials,patch.object(f.s.local,'preflight') as preflight,contextlib.redirect_stdout(output):
self.assertEqual(f.main(),0)
run.assert_not_called();materials.assert_not_called();preflight.assert_not_called()
manifest=json.loads(output.getvalue())
self.assertEqual(manifest['status'],'prepared_not_executed_not_approved')
self.assertEqual(manifest['fixed_endpoint'],'unix:///var/run/docker.sock')
def test_wrong_reviewed_manifest_hash_stops_before_preflight_or_generation(self):
with patch.object(sys,'argv',['fixture','--execute-reviewed-fixture','--expected-manifest-sha256','0'*64]),patch.object(f.s,'run') as run,patch.object(f,'materials') as materials,patch.object(f.s.local,'preflight') as preflight:
with self.assertRaises(RuntimeError):f.main()
run.assert_not_called();materials.assert_not_called();preflight.assert_not_called()
def test_manifest_binds_current_candidate_and_existing_fixture_factory(self):
digest=hashlib.sha256(f.MANIFEST.read_bytes()).hexdigest()
manifest=f.reviewed_manifest(digest)
self.assertIn('tests/test_identity_bundle.py',manifest['source_sha256'])
self.assertIn('tools/bootstrap-recovery/isolated-keycape-session.py',manifest['source_sha256'])
self.assertIn('sso-mfa/k8s/privacyidea/key-custody-remote.py',manifest['source_sha256'])
if __name__=='__main__':unittest.main()

View file

@ -0,0 +1,230 @@
import base64
import hashlib
import importlib.util
import io
import json
import subprocess
import tarfile
import tempfile
import unittest
from pathlib import Path
from unittest.mock import patch
SOURCE=Path(__file__).parents[1]/'tools/bootstrap-recovery/isolated-keycape.py'
spec=importlib.util.spec_from_file_location('adapter',SOURCE)
a=importlib.util.module_from_spec(spec);spec.loader.exec_module(a)
class ArchiveTests(unittest.TestCase):
def setUp(self):
self.tmp=tempfile.TemporaryDirectory();self.addCleanup(self.tmp.cleanup)
self.root=Path(self.tmp.name)
config={'data':{k:base64.b64encode(v).decode() for k,v in {'config.yaml':b'issuer: https://issuer.example','key.pem':b'fixture-key'}.items()}}
self.values={'resources/sso/secret/keycape-config.json':json.dumps(config).encode(),
'resources/sso/deployment/keycape.json':json.dumps({'spec':{'template':{'spec':{'containers':[{'name':'keycape','image':a.IMAGE,'env':[
{'name':'KEYCAPE_CONFIG','value':'/etc/keycape/config.yaml'},
{'name':'KEYCAPE_POLICY_PATH','value':'/var/lib/keycape-policy/policy.json'},
{'name':'KEYCAPE_ORIGINAL_SECRET','valueFrom':{'secretKeyRef':{'name':'client','key':'secret'}}}]}]}}}}).encode(),
'resources/sso/secret/client.json':json.dumps({'data':{'secret':base64.b64encode(b'fixture-private').decode()}}).encode(),
'data/keycape/policy.json':b'{}','data/keycape/receipts/one.json':b'{"original":true}'}
for name,value in self.values.items():
p=self.root/name;p.parent.mkdir(parents=True,exist_ok=True);p.write_bytes(value);p.chmod(0o600)
self.manifest={'profile':{'components':[{'namespace':'sso','deployment':'keycape','secret_refs':['keycape-config','client'],'configmap_refs':[]}]},
'files':[{'path':k,'bytes':len(v),'sha256':hashlib.sha256(v).hexdigest()} for k,v in self.values.items()]}
def payload(self):
digest=hashlib.sha256(json.dumps(self.manifest,sort_keys=True,separators=(',',':')).encode()).hexdigest()
return a.private_payload(self.root,self.manifest,digest)
def test_original_state_and_secret_delivery_retained(self):
p=self.payload();self.assertEqual(p['env']['KEYCAPE_ORIGINAL_SECRET'],'fixture-private')
self.assertEqual(base64.b64decode(p['files']['/var/lib/keycape-policy/receipts/one.json']),b'{"original":true}')
def test_archive_digest_refused(self):
with self.assertRaises(a.Refused):a.private_payload(self.root,self.manifest,'0'*64)
def test_changed_file_refused(self):
(self.root/'data/keycape/policy.json').write_text('{"changed":true}')
with self.assertRaises(a.Refused):self.payload()
def test_symlink_refused(self):
p=self.root/'data/keycape/policy.json';p.unlink();p.symlink_to('/etc/hostname')
with self.assertRaises(Exception):self.payload()
def test_duplicate_manifest_refused(self):
self.manifest['files'].append(self.manifest['files'][0])
with self.assertRaises(a.Refused):self.payload()
def test_image_mismatch_refused(self):
d=json.loads(self.values['resources/sso/deployment/keycape.json']);d['spec']['template']['spec']['containers'][0]['image']='keycape:latest'
raw=json.dumps(d).encode();p=self.root/'resources/sso/deployment/keycape.json';p.write_bytes(raw)
for row in self.manifest['files']:
if row['path'].endswith('/deployment/keycape.json'):row.update(bytes=len(raw),sha256=hashlib.sha256(raw).hexdigest())
with self.assertRaises(a.Refused):self.payload()
class NativeTests(unittest.TestCase):
def setUp(self):
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import rsa
self.key=rsa.generate_private_key(public_exponent=65537,key_size=2048).private_bytes(serialization.Encoding.PEM,serialization.PrivateFormat.PKCS8,serialization.NoEncryption())
self.cfg=b'issuer: https://issuer.example\nclients:\n- clientId: existing\n clientType: confidential\n secretRef: env:KEYCAPE_ORIGINAL_SECRET\n grantTypes: [client_credentials]\n'
self.files={'/etc/keycape/config.yaml':self.cfg,'/etc/keycape/key.pem':self.key,'/var/lib/keycape-policy/policy.json':b'{}'}
self.payload={'env':{'KEYCAPE_ORIGINAL_SECRET':'fixture-private'},'files':{k:base64.b64encode(v).decode() for k,v in self.files.items()}}
self.admission={'issuer':'https://issuer.example','verifier_sha256':hashlib.sha256(b'binary').hexdigest(),
'trust_path':'/trust.pem','trust_sha256':hashlib.sha256(b'original-trust').hexdigest(),
'consumer':{'client_id':'existing','secret_env':'KEYCAPE_ORIGINAL_SECRET','scope':'read','audience':'consumer',
'subject':'service:existing','tenant':'tenant:platform','roles':'reader','deny_scope':'write'}}
self.session={'issuer':'isolated','volumes':{x:{'target':x} for x in ('/etc/keycape','/var/lib/keycape-policy','/etc/keycape-factor','/recovery-tls')}}
self.output=''.join('PASS '+x+'\n' for x in ['discovery and endpoint origin','exchange and JWKS signature for granted scopes','access-token claim bindings','principal_type is service','sub matches the registration','tenant matches the registration','roles present','granted scope carries no excess grant','predecessor secret refused','excess scope refused: write'])
def file(self,container,path):
return {**self.files,'/etc/hosts':b'127.0.0.1 issuer.example\n','/keycape':b'binary','/trust.pem':b'original-trust'}[path]
def command(self,argv,**kwargs):
if argv[1:3]==['container','inspect']:return json.dumps(['KEYCAPE_ORIGINAL_SECRET=fixture-private','SSL_CERT_FILE=/trust.pem']).encode()
output=io.BytesIO()
with tarfile.open(fileobj=output,mode='w') as t:
m=tarfile.TarInfo('policy.json');m.size=2;t.addfile(m,io.BytesIO(b'{}'))
return output.getvalue()
def verify(self,code=0,output=None):
with patch.object(a,'check_jwks'),patch.object(a,'runtime_file',return_value=b'original-trust'),patch.object(a,'runtime_files',side_effect=lambda session,paths=(),tree=False:({k:v for k,v in self.files.items() if k.startswith('/var/lib/keycape-policy/')} if tree else dict(self.files))),patch.object(a,'container_file',side_effect=self.file),patch.object(a,'command',side_effect=self.command),patch.object(a.subprocess,'run',return_value=subprocess.CompletedProcess([],code,b'',(self.output if output is None else output).encode())) as run:
result=a.run_verifier(self.session,self.admission,self.payload)
argv=run.call_args.args[0]
self.assertNotIn('fixture-private',' '.join(argv))
self.assertIn('-previous-secret-env',argv)
return result
def test_original_registration_and_unrelated_secret(self):
r=self.verify();self.assertTrue(r['wrong_unrelated_secret_explicitly_refused']);self.assertNotIn('fixture-private',json.dumps(r))
def test_transport_failure_never_counts_as_wrong_secret(self):
with self.assertRaises(a.Refused):self.verify(1,'FAIL discovery and endpoint origin: TLS failure\n')
def test_missing_explicit_rejection_never_counts(self):
with self.assertRaises(a.Refused):self.verify(0,self.output.replace('PASS predecessor secret refused\n',''))
def test_fail_line_refused_even_zero_exit(self):
with self.assertRaises(a.Refused):self.verify(0,self.output+'FAIL transport\n')
def test_security_environment_override_refused(self):
original=self.command
def altered(argv,**kwargs):
if argv[1:3]==['container','inspect']:return json.dumps(['KEYCAPE_ORIGINAL_SECRET=fixture-private','SSL_CERT_FILE=/trust.pem','SSL_CERT_DIR=/unreviewed']).encode()
return original(argv,**kwargs)
with patch.object(self,'command',side_effect=altered):
with self.assertRaises(a.Refused):self.verify()
def test_http_issuer_refused(self):
self.admission['issuer']='http://issuer.example'
with self.assertRaises(a.Refused):self.verify()
def test_changed_trust_refused(self):
self.admission['trust_sha256']='0'*64
with self.assertRaises(a.Refused):self.verify()
def test_config_drift_refused(self):
self.payload['files']['/etc/keycape/config.yaml']=base64.b64encode(b'changed').decode()
with self.assertRaises(a.Refused):self.verify()
class PublicReadinessTransportTests(unittest.TestCase):
def test_provider_probe_failure_is_closed_false_data_not_failed_docker_exec(self):
output=io.StringIO()
with patch('sys.stdin',io.StringIO('{}')),patch('sys.stdout',output):
exec(compile(a.JWKS_TRANSPORT,'<public-readiness>','exec',optimize=2),{})
self.assertEqual(json.loads(output.getvalue()),{'original_public_signing_key_in_live_jwks':False})
class BoundaryTests(unittest.TestCase):
def test_remote_docker_refused_before_info(self):
session={'run_id':'a'*32,'containers':['nk49-kc-'+('a'*32)+'-worker'],'worker':'nk49-kc-'+('a'*32)+'-worker','issuer':'nk49-kc-'+('a'*32)+'-worker'}
with patch.object(a,'command',return_value=b'[{"Endpoints":{"docker":{"Host":"tcp://remote:2375"}}}]') as command:
with self.assertRaises(a.Refused):a.inspect_session(session)
self.assertEqual(command.call_count,1)
def test_failed_verifier_cleanup_is_attempted(self):
receipt={};admission={'session':{},'ciphertext_sha256':'a'*64,'manifest_sha256':'b'*64,'verifier_sha256':'c'*64,'trust_sha256':'d'*64,'consumer':{x:'yes' for x in ('client_id','secret_env','audience','scope','subject','tenant','roles','deny_scope')}}
with patch.object(a,'inspect_session'),patch.object(a,'private_payload',return_value={}),patch.object(a,'run_verifier',side_effect=a.Refused),patch.object(a,'cleanup') as cleanup:
with self.assertRaises(a.Refused):a.native_checks(Path('/unused'),{},admission,receipt)
cleanup.assert_called_once();self.assertTrue(receipt['container_cleanup_verified']);self.assertEqual(receipt['status'],'failed')
def test_cleanup_failure_overrides_success(self):
admission={'session':{},'ciphertext_sha256':'a'*64,'manifest_sha256':'b'*64,'verifier_sha256':'c'*64,'trust_sha256':'d'*64,'consumer':{x:'yes' for x in ('client_id','secret_env','audience','scope','subject','tenant','roles','deny_scope')}}
result={x:True for x in ('native_service_registration_verified','wrong_unrelated_secret_explicitly_refused','denied_scopes_refused','original_config_registry_policy_receipts_equal','original_https_issuer_and_trust_preserved','original_public_signing_key_in_live_jwks')}
result['public_signing_spki_sha256']='e'*64;receipt={}
with patch.object(a,'inspect_session'),patch.object(a,'private_payload',return_value={}),patch.object(a,'run_verifier',return_value=result),patch.object(a,'cleanup',side_effect=a.Refused):
with self.assertRaises(a.Refused):a.native_checks(Path('/unused'),{},admission,receipt)
self.assertEqual(receipt['status'],'failed');self.assertFalse(receipt['container_cleanup_verified'])
class ContainmentTests(unittest.TestCase):
def inspect(self,overrides=None,launch=None):
name='nk49-kc-'+('a'*32)+'-issuer'
session={'run_id':'a'*32,'containers':[name],'issuer':name,'worker':name,'images':{name:a.IMAGE}}
host={'NetworkMode':'none','ReadonlyRootfs':True,'Privileged':False,'CapDrop':['ALL'],'SecurityOpt':['no-new-privileges']}
host.update(overrides or {})
def command(argv,**kwargs):
if argv[1:3]==['context','inspect']:return b'[{"Endpoints":{"docker":{"Host":"unix:///var/run/docker.sock"}}}]'
if argv[1]=='info':return b'{"ID":"74f93783-6cef-43a8-a3c4-f62586cd3bc1","Name":"bnt-lap001","OSType":"linux"}'
if argv[-1]=='{{.Id}}':return b'anchor-id'
if '"path"' in argv[-1]:return json.dumps(launch or {'entrypoint':['/keycape'],'cmd':None,'path':'/keycape','args':[]}).encode()
return json.dumps({'host':host,'mounts':[],'labels':{a.LABEL:'a'*32},'image':a.IMAGE,'user':'1000:1000'}).encode()
with patch.object(a,'command',side_effect=command),patch.dict('os.environ',{'DOCKER_HOST':'unix:///var/run/docker.sock'}):a.inspect_session(session)
def test_minimal_network_none_admitted(self):self.inspect()
def test_alternate_same_image_command_refused(self):
with self.assertRaises(a.Refused):self.inspect(launch={'entrypoint':['/keycape'],'cmd':['check-config'],'path':'/keycape','args':['check-config']})
def test_cap_add_refused(self):
with self.assertRaises(a.Refused):self.inspect({'CapAdd':['NET_ADMIN']})
def test_devices_refused(self):
with self.assertRaises(a.Refused):self.inspect({'Devices':[{'PathOnHost':'/dev/mem'}]})
def test_device_requests_refused(self):
with self.assertRaises(a.Refused):self.inspect({'DeviceRequests':[{'Driver':'nvidia'}]})
def test_host_namespace_refused(self):
for field in ('PidMode','IpcMode','UTSMode','UsernsMode','CgroupnsMode'):
with self.subTest(field=field),self.assertRaises(a.Refused):self.inspect({field:'host'})
def test_security_override_refused(self):
with self.assertRaises(a.Refused):self.inspect({'SecurityOpt':['no-new-privileges','seccomp=unconfined']})
class CleanupTests(unittest.TestCase):
def test_partial_cleanup_attempts_every_owned_container(self):
session={'run_id':'a'*32,'containers':['first','second','third']};receipt={}
removed=[]
def command(argv,**kwargs):
if argv[1:3]==['container','inspect']:return ('a'*32).encode()
if argv[1]=='rm':
removed.append(argv[-1])
if argv[-1]=='third':raise a.Refused()
return b''
if argv[1:3]==['container','ls']:
name=next(x for x in argv if x.startswith('name=^/')).removeprefix('name=^/').removesuffix('$')
return (name+'\n').encode() if name=='third' or name not in removed else b''
return b''
with patch.object(a,'command',side_effect=command):
with self.assertRaises(a.Refused):a.cleanup(session,receipt)
self.assertEqual(removed,['third','second','first'])
self.assertEqual([r['status'] for r in receipt['container_cleanup_obligations']],['still_present','absence_verified','absence_verified'])
def test_ownership_mismatch_does_not_remove_and_continues(self):
session={'run_id':'a'*32,'containers':['first','second']};receipt={};removed=[]
def command(argv,**kwargs):
if argv[1:3]==['container','inspect']:return (('b' if argv[3]=='second' else 'a')*32).encode()
if argv[1]=='rm':removed.append(argv[-1])
if argv[1:3]==['container','ls']:
name=next(x for x in argv if x.startswith('name=^/')).removeprefix('name=^/').removesuffix('$')
return (name+'\n').encode() if name not in removed else b''
return b''
with patch.object(a,'command',side_effect=command):
with self.assertRaises(a.Refused):a.cleanup(session,receipt)
self.assertEqual(removed,['first'])
self.assertEqual(receipt['container_cleanup_obligations'][0]['status'],'ownership_mismatch_not_removed')
def test_inspect_failure_retains_uncertain_obligation(self):
receipt={}
with patch.object(a,'command',side_effect=a.Refused):
with self.assertRaises(a.Refused):a.cleanup({'run_id':'a'*32,'containers':['exact-owned-name']},receipt)
self.assertEqual(receipt['container_cleanup_obligations'],[{'container':'exact-owned-name','status':'unverified'}])
class JWKSTests(unittest.TestCase):
def execute(self,n='123',endpoint='https://issuer.example/jwks',resolved='127.0.0.1',extra=False,kid='key-1'):
request={'issuer':'https://issuer.example','trust':'public-certificate','n':'123','e':'65537'}
def enc(i):return base64.urlsafe_b64encode(int(i).to_bytes((int(i).bit_length()+7)//8,'big')).decode().rstrip('=')
responses=[{'issuer':'https://issuer.example','jwks_uri':endpoint},{'keys':[{'kty':'RSA','kid':kid,'use':'sig','alg':'RS256','n':enc(n),'e':enc(65537)}]}]
if extra:responses[1]['keys'].append(dict(responses[1]['keys'][0],kid='key-2'))
class Response:
def __init__(self,value):self.value=value
def __enter__(self):return self
def __exit__(self,*args):pass
def read(self,*args):return json.dumps(self.value).encode()
class Opener:
def open(self,*args,**kwargs):return Response(responses.pop(0))
with patch('sys.stdin',io.StringIO(json.dumps(request))),patch('sys.stdout',io.StringIO()),patch('socket.getaddrinfo',return_value=[(2,1,6,'',(resolved,443))]),patch('ssl.create_default_context'),patch('urllib.request.build_opener',return_value=Opener()):
exec(compile(a.JWKS_PROBE,'<jwks-probe>','exec',optimize=2),{})
def test_exact_original_public_key(self):self.execute()
def test_extra_signing_key_refused_under_optimization(self):
with self.assertRaises(ValueError):self.execute(extra=True)
def test_wrong_kid_refused_under_optimization(self):
with self.assertRaises(ValueError):self.execute(kid='replacement')
def test_wrong_live_signing_key_refused(self):
with self.assertRaises(ValueError):self.execute(n='124')
def test_cross_origin_jwks_refused(self):
with self.assertRaises(ValueError):self.execute(endpoint='https://production.example/jwks')
def test_production_dns_refused_before_fetch(self):
with self.assertRaises(ValueError):self.execute(resolved='192.0.2.1')
if __name__=='__main__':unittest.main()

View file

@ -0,0 +1,258 @@
import base64
import importlib.util
import json
import io
import os
import tempfile
import signal
import subprocess
import sys
import time
from pathlib import Path
import unittest
from unittest.mock import patch
SOURCE=Path(__file__).parents[1]/'tools/bootstrap-recovery/isolated-keycape-session.py'
spec=importlib.util.spec_from_file_location('session',SOURCE)
s=importlib.util.module_from_spec(spec);spec.loader.exec_module(s)
class SessionTests(unittest.TestCase):
def test_tmpfs_plan_has_four_exact_original_mounts(self):
session=s.plan_session()
self.assertEqual({v['target'] for v in session['volumes'].values()},set(s.RUNROOTS))
self.assertTrue(all(v['options']==s.VOLUME_OPTIONS for v in session['volumes'].values()))
self.assertEqual(session['images'][session['issuer']],s.adapter.IMAGE)
def test_non_tmpfs_driver_options_refused(self):
session=s.plan_session();name,spec=next(iter(session['volumes'].items()))
wrong=[{'Name':name,'Driver':'local','Scope':'local','Options':{'type':'none','device':'/host/data'},'Labels':{s.adapter.LABEL:session['run_id']}}]
with patch.object(s,'run',return_value=json.dumps(wrong).encode()):
with self.assertRaises(s.adapter.Refused):s.verify_volume(name,spec,session['run_id'])
def test_owner_secret_is_not_docker_argv_and_import_is_after_inspection(self):
session=s.plan_session();events=[]
payload={'files':{'/etc/keycape/config.yaml':base64.b64encode(b'original').decode()},'env':{'KEYCAPE_TEST_SECRET':'fixture-sensitive'}}
def run(argv,**kwargs):
events.append(('run',argv,kwargs))
if argv[-1]=='{{.Id}}':return b'anchor'
return b''
with patch.object(s,'run',side_effect=run),patch.object(s,'verify_volume'),patch.object(s.adapter,'inspect_session',side_effect=lambda *args,**kwargs:events.append(('inspect',args,kwargs))):
s.provision(session,payload,{'/recovery-tls/trust.pem':b'original-trust'},'https://issuer.example')
joined=' '.join(' '.join(e[1]) for e in events if e[0]=='run')
self.assertNotIn('fixture-sensitive',joined)
private_index=next(i for i,e in enumerate(events) if e[0]=='run' and s.IMPORT in e[1])
inspect_index=next(i for i,e in enumerate(events) if e[0]=='inspect')
self.assertLess(inspect_index,private_index)
issuer_call=next(e for e in events if e[0]=='run' and e[1][-1]==s.adapter.IMAGE)
self.assertEqual(issuer_call[2]['env']['KEYCAPE_TEST_SECRET'],'fixture-sensitive')
self.assertIn('KEYCAPE_TEST_SECRET',issuer_call[1])
self.assertNotIn('--entrypoint',issuer_call[1])
def test_non_original_https_port_refused_before_any_mutation(self):
with patch.object(s,'run') as run:
with self.assertRaises(s.adapter.Refused):s.provision(s.plan_session(),{'files':{},'env':{}},{},'http://issuer.example')
run.assert_not_called()
def test_container_failure_still_attempts_each_volume_cleanup(self):
session=s.plan_session();receipt={};removed=[]
def run(argv,**kwargs):
if argv[1:3]==['volume','rm']:removed.append(argv[-1])
if argv[1:3]==['volume','ls']:
name=next(x for x in argv if x.startswith('name=^')).removeprefix('name=^').removesuffix('$')
return (name+'\n').encode() if name not in removed else b''
return b''
with patch.object(s.adapter,'cleanup',side_effect=s.adapter.Refused),patch.object(s,'verify_volume'),patch.object(s,'run',side_effect=run):
with self.assertRaises(s.adapter.Refused):s.cleanup_all(session,receipt)
self.assertEqual(set(removed),set(session['volumes']))
self.assertFalse(receipt['container_cleanup_verified'])
self.assertEqual(len(receipt['tmpfs_volume_cleanup_obligations']),4)
def test_reaper_inherits_no_custody_or_service_environment(self):
session=s.plan_session();staging={'path':'/dev/shm/registered','run_id':session['run_id']}
with patch.object(s.subprocess,'Popen') as spawn,patch.object(s,'verify_execution'),patch.object(s.os,'pidfd_open',return_value=99),patch.object(s.os,'close'),patch.object(s,'register_workdir',return_value=staging),patch.object(s.select,'select',side_effect=lambda *args:([spawn.return_value.stdout],[],[])):
spawn.return_value.stdout.readline.return_value=b'registered\n'
s.start_reaper(session,staging,{'pid':123})
kwargs=spawn.call_args.kwargs
self.assertEqual(set(kwargs['env']),{'PATH','DOCKER_HOST'})
self.assertTrue(kwargs['close_fds']);self.assertTrue(kwargs['start_new_session']);self.assertEqual(kwargs['pass_fds'],(99,))
class TargetBindingTests(unittest.TestCase):
def test_every_direct_docker_command_is_explicitly_bound(self):
with patch.dict('os.environ',{},clear=True),patch.object(s.adapter.subprocess,'run',return_value=__import__('subprocess').CompletedProcess([],0,b'ok')) as run:
self.assertEqual(s.run(['docker','info']),b'ok')
argv=run.call_args.args[0]
self.assertEqual(argv[:3],['docker','--host',s.adapter.ENDPOINT])
self.assertEqual(run.call_args.kwargs['env']['DOCKER_HOST'],s.adapter.ENDPOINT)
def test_nondefault_unix_socket_is_refused_before_any_docker_call(self):
with patch.dict('os.environ',{'DOCKER_HOST':'unix:///tmp/other-daemon.sock'},clear=True),patch.object(s.adapter.subprocess,'run') as run:
with self.assertRaises(s.adapter.Refused):s.run(['docker','info'])
run.assert_not_called()
def test_conflicting_context_is_refused(self):
with patch.dict('os.environ',{'DOCKER_CONTEXT':'production'},clear=True),patch.object(s.adapter.subprocess,'run') as run:
with self.assertRaises(s.adapter.Refused):s.run(['docker','info'])
run.assert_not_called()
def test_conflicting_per_call_host_is_refused(self):
with patch.dict('os.environ',{},clear=True),patch.object(s.adapter.subprocess,'run') as run:
with self.assertRaises(s.adapter.Refused):s.run(['docker','info'],env={'DOCKER_HOST':'unix:///tmp/other.sock'})
run.assert_not_called()
def test_reused_preflight_uses_same_bound_command(self):
self.assertIs(s.local.command,s.adapter.command)
def test_probe_is_registered_before_session_creation(self):
session=s.plan_session()
self.assertEqual(session['public_image_probe'],'nk49-kc-'+session['run_id']+'-image-probe')
self.assertEqual(session['docker_endpoint'],s.adapter.ENDPOINT)
class PlaintextLifecycleTests(unittest.TestCase):
def setUp(self):
self.tmp=tempfile.TemporaryDirectory();self.addCleanup(self.tmp.cleanup)
self.root=Path(self.tmp.name)
self.override=patch.object(s,'STAGING_ROOT',self.root);self.override.start();self.addCleanup(self.override.stop)
self.run_id='a'*32
self.work=self.root/('nk49-keycape-'+self.run_id+'-abcdefgh');self.work.mkdir(mode=0o700)
self.staging=s.register_workdir(self.work,self.run_id)
def test_exact_plaintext_directory_removed_and_recorded(self):
(self.work/'archive.tar').write_bytes(b'synthetic-private-archive')
restored=self.work/'restored';restored.mkdir(mode=0o700);(restored/'fixture').write_bytes(b'synthetic')
receipt={};s.cleanup_workdir(self.staging,receipt)
self.assertFalse(self.work.exists());self.assertTrue(receipt['temporary_plaintext_cleanup_verified'])
self.assertEqual(receipt['plaintext_cleanup_obligation']['status'],'absence_verified')
def test_docker_failure_does_not_skip_plaintext_cleanup(self):
(self.work/'archive.tar').write_bytes(b'synthetic')
receipt={}
with patch.object(s,'cleanup_all',side_effect=s.adapter.Refused):
with self.assertRaises(s.adapter.Refused):s.recover_obligations({},self.staging,receipt)
self.assertFalse(self.work.exists());self.assertTrue(receipt['temporary_plaintext_cleanup_verified'])
def test_replacement_inode_is_not_removed(self):
# Preserve original inode while replacing its registered pathname.
old=self.root/'retained-original';self.work.rename(old);self.work.mkdir(mode=0o700)
(self.work/'replacement').write_bytes(b'not-owned-by-this-obligation');receipt={}
with self.assertRaises(s.adapter.Refused):s.cleanup_workdir(self.staging,receipt)
self.assertTrue((self.work/'replacement').exists());self.assertTrue(old.exists())
self.assertEqual(receipt['plaintext_cleanup_obligation']['status'],'identity_mismatch_not_removed')
def test_symlink_root_is_not_followed(self):
self.work.rmdir();outside=self.root/'outside';outside.mkdir();(outside/'kept').write_bytes(b'keep')
self.work.symlink_to(outside,target_is_directory=True)
with self.assertRaises(s.adapter.Refused):s.cleanup_workdir(self.staging,{})
self.assertTrue((outside/'kept').exists())
def test_descendant_symlink_is_not_followed(self):
outside=self.root/'kept';outside.write_bytes(b'keep');(self.work/'link').symlink_to(outside)
with self.assertRaises(s.adapter.Refused):s.cleanup_workdir(self.staging,{})
self.assertEqual(outside.read_bytes(),b'keep')
def test_wrong_owner_identity_is_not_removed(self):
changed={**self.staging,'owner_uid':os.getuid()+1}
with self.assertRaises(s.adapter.Refused):s.cleanup_workdir(changed,{})
self.assertTrue(self.work.exists())
def test_unregistered_path_refused(self):
with self.assertRaises(s.adapter.Refused):s.cleanup_workdir({**self.staging,'path':str(self.root)}, {})
@unittest.skipUnless(hasattr(os,'pidfd_open') and hasattr(signal,'pidfd_send_signal'),'Linux pidfd required')
class ExecutionDeadlineTests(unittest.TestCase):
def start_synthetic_owner(self,root):
late=str(root/'late-creation')
code="""import importlib.util,json,subprocess,sys,time
spec=importlib.util.spec_from_file_location('session',sys.argv[1]);s=importlib.util.module_from_spec(spec);spec.loader.exec_module(s)
execution=s.isolate_execution()
child=subprocess.Popen([sys.executable,'-c',"import pathlib,sys,time;time.sleep(2);pathlib.Path(sys.argv[1]).write_text('late')",sys.argv[2]])
print(json.dumps({'execution':execution,'child_pid':child.pid}),flush=True)
time.sleep(20)
"""
parent=subprocess.Popen([sys.executable,'-c',code,str(SOURCE),late],stdout=subprocess.PIPE,stderr=subprocess.DEVNULL)
message=json.loads(parent.stdout.readline());parent.stdout.close()
def finish():
if parent.poll() is None:parent.kill()
parent.wait(timeout=5)
try:
before=s.process_facts(message['child_pid'])
if before is None or before['sid']!=message['execution']['sid'] or before['pgid']!=message['execution']['pgid']:return
fd=os.pidfd_open(message['child_pid'])
try:
after=s.process_facts(message['child_pid'])
if after is not None and all(after[k]==before[k] for k in ('pid','sid','pgid','start_ticks','uid')):
signal.pidfd_send_signal(fd,signal.SIGKILL)
except ProcessLookupError:pass
finally:os.close(fd)
except ProcessLookupError:pass
self.addCleanup(finish)
return parent,message['execution'],Path(late)
def test_deadline_quiesces_only_fresh_owner_group_before_cleanup(self):
with tempfile.TemporaryDirectory() as directory:
parent,execution,late=self.start_synthetic_owner(Path(directory));fd=os.pidfd_open(parent.pid)
try:
receipt={};s.quiesce_execution(execution,fd,receipt)
self.assertTrue(receipt['execution_quiescence_verified'])
self.assertFalse(s.group_members(execution));self.assertFalse(late.exists())
finally:os.close(fd)
def test_hard_parent_loss_stops_surviving_marker_creator(self):
with tempfile.TemporaryDirectory() as directory:
parent,execution,late=self.start_synthetic_owner(Path(directory));fd=os.pidfd_open(parent.pid)
try:
signal.pidfd_send_signal(fd,signal.SIGKILL)
poll=__import__('select').poll();poll.register(fd,__import__('select').POLLIN);self.assertTrue(poll.poll(2000))
receipt={};s.quiesce_execution(execution,fd,receipt)
self.assertTrue(receipt['execution_quiescence_verified']);self.assertFalse(late.exists())
self.assertFalse(s.group_members(execution))
finally:os.close(fd)
def test_expired_execution_cannot_start_any_docker_command(self):
binding={'pid':os.getpid(),'sid':os.getsid(0),'pgid':os.getpgrp(),'deadline':time.monotonic()-1,'staging':'/tmp/not-created'}
with patch.object(s.adapter,'EXECUTION',binding),patch.object(s.adapter.subprocess,'run') as run:
with self.assertRaises(s.adapter.Refused):s.run(['docker','create','fixed-image'])
run.assert_not_called()
def test_reused_or_foreign_session_refuses_signals(self):
binding={'pid':123,'sid':123,'pgid':123,'start_ticks':10,'uid':os.getuid()}
wrong={**binding,'sid':456,'state':'S'}
with patch.object(s,'process_facts',return_value=wrong),patch.object(s.signal,'pidfd_send_signal') as send:
with self.assertRaises(s.adapter.Refused):s.quiesce_execution(binding,99,{})
send.assert_not_called()
def test_inflight_uncertainty_survives_plaintext_directory_removal(self):
with tempfile.TemporaryDirectory() as directory:
path=Path(directory)/'owned-stage';path.mkdir();marker=Path(str(path)+'-docker-inflight');marker.touch()
staging={'path':str(path)};self.assertTrue(s.daemon_uncertain(staging));path.rmdir()
self.assertTrue(s.daemon_uncertain(staging))
class ReaperOrderingTests(unittest.TestCase):
def exercise(self,quiescence_fails=False):
events=[];records=[]
def quiet(*args):
events.append('quiesce')
if quiescence_fails:raise s.adapter.Refused()
args[-1]['execution_quiescence_verified']=True
def recover(*args):events.append('cleanup')
with patch.object(s,'verify_execution'),patch.object(s,'pidfd_dead',return_value=False),patch.object(s.select,'poll') as poll,patch.object(s,'quiesce_execution',side_effect=quiet),patch.object(s,'daemon_uncertain',return_value=True),patch.object(s,'recover_obligations',side_effect=recover),patch.object(s.os,'close'),patch.object(s.os,'open',return_value=99),patch.object(s.os,'fdopen'),patch.object(s.json,'dump',side_effect=lambda r,*args:records.append(dict(r))),patch('sys.stdout',io.StringIO()):
poll.return_value.poll.return_value=[]
s.reaper_main({}, {'path':'/dev/shm/exact-staging'}, {'deadline':time.monotonic()-1},77)
return events,records[0]
def test_daemon_uncertainty_survives_verified_resource_readback(self):
events,receipt=self.exercise();self.assertEqual(events,['quiesce','cleanup'])
self.assertTrue(receipt['execution_quiescence_verified']);self.assertTrue(receipt['in_flight_daemon_request_uncertain'])
self.assertEqual(receipt['status'],'bounded_cleanup_uncertain')
def test_unverified_quiescence_prevents_any_cleanup(self):
events,receipt=self.exercise(True);self.assertEqual(events,['quiesce'])
self.assertFalse(receipt['execution_quiescence_verified']);self.assertEqual(receipt['status'],'bounded_cleanup_uncertain')
def test_checkpoint_rewrites_one_valid_json_document(self):
with tempfile.TemporaryFile() as stream:
s.persist_receipt(stream.fileno(),{'status':'registered','long':'x'*100})
s.persist_receipt(stream.fileno(),{'status':'failed'})
stream.seek(0);self.assertEqual(json.load(stream),{'status':'failed'})
def test_nonzero_docker_result_retains_uncertainty_before_inflight_clear(self):
with tempfile.TemporaryDirectory() as directory:
staging=str(Path(directory)/'stage');Path(staging).mkdir()
binding={'pid':os.getpid(),'sid':os.getsid(0),'pgid':os.getpgrp(),'deadline':time.monotonic()+10,'staging':staging}
def returned(*args,**kwargs):
self.assertTrue(Path(staging+'-docker-inflight').exists())
return subprocess.CompletedProcess(args[0],1,b'')
with patch.object(s.adapter,'EXECUTION',binding),patch.object(s.adapter.subprocess,'run',side_effect=returned):
with self.assertRaises(s.adapter.Refused):s.run(['docker','create','synthetic-already-accepted'])
self.assertFalse(Path(staging+'-docker-inflight').exists());self.assertTrue(s.daemon_uncertain({'path':staging}))
def test_only_policy_mount_is_writable_in_issuer(self):
plan=s.plan_session();args=s.mounts(plan,readonly=True)
mounts=args[1::2]
for value in mounts:
self.assertEqual(not value.endswith(',readonly'),'target=/var/lib/keycape-policy' in value)
def test_docker_timeout_retains_independent_uncertainty_marker(self):
with tempfile.TemporaryDirectory() as directory:
staging=str(Path(directory)/'stage');Path(staging).mkdir()
binding={'pid':os.getpid(),'sid':os.getsid(0),'pgid':os.getpgrp(),'deadline':time.monotonic()+10,'staging':staging}
def timeout(*args,**kwargs):
self.assertTrue(Path(staging+'-docker-inflight').exists())
raise subprocess.TimeoutExpired('synthetic Docker',1)
with patch.object(s.adapter,'EXECUTION',binding),patch.object(s.adapter.subprocess,'run',side_effect=timeout):
with self.assertRaises(subprocess.TimeoutExpired):s.run(['docker','create','fixed-image'])
self.assertFalse(Path(staging+'-docker-inflight').exists())
self.assertTrue(Path(staging+'-docker-uncertain').exists());self.assertTrue(s.daemon_uncertain({'path':staging}))
if __name__=='__main__':unittest.main()

View file

@ -0,0 +1,296 @@
#!/usr/bin/env python3
"""Owner-run identity capture: metadata plan first, exact bounded execution later.
Use the admitted Railiance caller. Flags bind reviewed source/targets; they do
not grant authority. Private child output and provider diagnostics never print.
"""
import argparse
from datetime import datetime,timezone
import hashlib
import importlib.util
import json
import os
from pathlib import Path
import signal
import subprocess
import tempfile
import uuid
HERE=Path(__file__).resolve().parent
def load(name):
spec=importlib.util.spec_from_file_location(name,HERE/(name+'.py'))
module=importlib.util.module_from_spec(spec);spec.loader.exec_module(module);return module
bundle=load('identity-bundle');prepare=load('prepare-identity-coverage')
IMAGE='ghcr.io/gpappsoft/privacyidea-docker@sha256:af7841adad262f129e0c1d4f553af13f21cb2f4dc713533f316cfe43ed0b4473'
SOURCES=['capture-identity.py','identity-files.py','identity-bundle.py','prepare-identity-coverage.py','identity-backup-profile.json']
KUBE=['kubectl']
class Refused(Exception):pass
def command(args,payload=None,timeout=60):
result=subprocess.run(KUBE+['--request-timeout=30s']+args,input=payload,
stdout=subprocess.PIPE,stderr=subprocess.DEVNULL,timeout=timeout)
if result.returncode:raise Refused()
return result.stdout
def get(ns,kind,name):
scope=['-n',ns] if ns else []
return json.loads(command(scope+['get',kind,name,'-o','json']))
def selects(selector,labels):
if any(labels.get(k)!=v for k,v in selector.get('matchLabels',{}).items()):return False
for expression in selector.get('matchExpressions',[]):
key=expression['key'];op=expression['operator'];values=expression.get('values',[])
if op=='In' and labels.get(key) not in values:return False
if op=='NotIn' and key in labels and labels[key] in values:return False
if op=='Exists' and key not in labels:return False
if op=='DoesNotExist' and key in labels:return False
if op not in {'In','NotIn','Exists','DoesNotExist'}:raise Refused()
return True
def network_review(run_id,actual_labels=None):
rows=[]
for ns in ('sso','mfa'):
policies=json.loads(command(['-n',ns,'get','networkpolicies','-o','json']))['items']
for obj in policies:
m=obj['metadata'];s=obj['spec']
labels=(actual_labels or {}).get(ns,{'net-kingdom/capture':run_id})
if selects(s['podSelector'],labels) and (s.get('ingress') or s.get('egress')):
rows.append({'namespace':ns,'name':m['name'],'uid':m['uid'],
'spec_sha256':hashlib.sha256(json.dumps(s,sort_keys=True).encode()).hexdigest()})
return rows
def validate_helper(actual,wanted):
a=actual['spec'];w=wanted['spec']
if (any(a.get(k) for k in ('hostNetwork','hostPID','hostIPC','initContainers','ephemeralContainers'))
or a.get('automountServiceAccountToken') is not False or a.get('activeDeadlineSeconds')!=900
or len(a['containers'])!=1 or a['volumes']!=w['volumes']):raise Refused()
c=a['containers'][0];expected=w['containers'][0]
if any(c.get(k)!=expected[k] for k in ('name','image','command','volumeMounts')):raise Refused()
if c.get('env') or c.get('envFrom') or c.get('securityContext',{}).get('privileged'):raise Refused()
if any(c.get('securityContext',{}).get(k)!=v for k,v in expected['securityContext'].items()):raise Refused()
if any(a.get('securityContext',{}).get(k)!=v for k,v in w['securityContext'].items()):raise Refused()
def manifests(run_id):
result=[]
for ns,claims in [('sso',[('lldap','lldap-data'),('authelia','authelia-data'),('keycape','keycape-authentication-policy')]),
('mfa',[('privacyidea','privacyidea-data')])]:
label={'net-kingdom/capture':run_id};name='nk49-capture-'+run_id
result.append({'apiVersion':'networking.k8s.io/v1','kind':'NetworkPolicy',
'metadata':{'name':name,'namespace':ns},
'spec':{'podSelector':{'matchLabels':label},'policyTypes':['Ingress','Egress'],'ingress':[],'egress':[]}})
result.append({'apiVersion':'v1','kind':'Pod','metadata':{'name':name,'namespace':ns,'labels':label},
'spec':{'restartPolicy':'Never','activeDeadlineSeconds':900,'automountServiceAccountToken':False,
'securityContext':{'runAsUser':0,'runAsGroup':0},
'containers':[{'name':'capture','image':IMAGE,'imagePullPolicy':'IfNotPresent',
'command':['python3','-c','import time; time.sleep(900)'],
'securityContext':{'allowPrivilegeEscalation':False,'readOnlyRootFilesystem':True,
'capabilities':{'drop':['ALL'],'add':['DAC_OVERRIDE']}},
'resources':{'requests':{'cpu':'50m','memory':'128Mi'},'limits':{'cpu':'1','memory':'2Gi'}},
'volumeMounts':[{'name':key,'mountPath':'/source/'+key,'readOnly':True} for key,_ in claims]+
[{'name':'scratch','mountPath':'/scratch'}]}],
'volumes':[{'name':key,'persistentVolumeClaim':{'claimName':claim,'readOnly':True}} for key,claim in claims]+
[{'name':'scratch','emptyDir':{'medium':'Memory','sizeLimit':'512Mi'}}]}})
return result
def plan():
p=bundle.profile();binding=prepare.prepare(p,get=get)
cluster=get('databases','cluster','net-kingdom-pg')
primary=cluster['status']['currentPrimary'];pod=get('databases','pod',primary)
if (not any(o.get('uid')==cluster['metadata']['uid'] for o in pod['metadata'].get('ownerReferences',[]))
or not any(c['type']=='Ready' and c['status']=='True' for c in pod['status'].get('conditions',[]))):raise Refused()
run_id=uuid.uuid4().hex[:12]
return {'schema':'netkingdom.identity-capture-plan.v1','status':'requires_owner_execution_admission',
'created_at':datetime.now(timezone.utc).isoformat(),'run_id':run_id,'binding':binding,'kubeconfig':KUBE[2],
'sources':[{'path':name,'sha256':hashlib.sha256((HERE/name).read_bytes()).hexdigest()} for name in SOURCES],
'database':{'namespace':'databases','cluster':'net-kingdom-pg','cluster_uid':cluster['metadata']['uid'],
'pod':primary,'pod_uid':pod['metadata']['uid'],'database':'privacyidea_db'},
'create_resources':manifests(run_id),'service_changes':False,'credential_grants':False,
'matching_network_allow_policies':network_review(run_id),
'network_policy_enforcement_requires_owner_confirmation':True,
'offsite_delivery_included':False,'global_atomic_snapshot_claimed':False}
def checked_plan(path,expected):
raw=path.read_bytes()
if hashlib.sha256(raw).hexdigest()!=expected:raise Refused()
p=json.loads(raw)
if p.get('schema')!='netkingdom.identity-capture-plan.v1':raise Refused()
if p.get('kubeconfig')!=KUBE[2]:raise Refused()
if network_review(p['run_id']):raise Refused()
if p['sources']!=[{'path':name,'sha256':hashlib.sha256((HERE/name).read_bytes()).hexdigest()} for name in SOURCES]:raise Refused()
if p['create_resources']!=manifests(p['run_id']):raise Refused()
now=datetime.now(timezone.utc)
if not 0<=(now-datetime.fromisoformat(p['created_at'])).total_seconds()<3600:raise Refused()
# Recheck the exact immutable source/target bindings before private reads.
current=prepare.prepare(bundle.profile(),get=get)
if current['bindings']!=p['binding']['bindings'] or current['profile_sha256']!=p['binding']['profile_sha256']:raise Refused()
return p
def delete_created(obj):
m=obj['metadata'];ns=m['namespace'];name=m['name'];uid=m['uid']
if obj['kind']=='Pod':path=f'/api/v1/namespaces/{ns}/pods/{name}'
elif obj['kind']=='NetworkPolicy':path=f'/apis/networking.k8s.io/v1/namespaces/{ns}/networkpolicies/{name}'
else:raise Refused()
payload=json.dumps({'apiVersion':'v1','kind':'DeleteOptions','preconditions':{'uid':uid},'gracePeriodSeconds':1}).encode()
command(['delete','--raw',path,'-f','-'],payload)
if obj['kind']=='Pod':command(['-n',ns,'wait','--for=delete','pod/'+name,'--timeout=90s'],timeout=100)
def bounded_file(args,path,limit):
# Stream binary dump to private staging, with a byte bound and suppressed
# diagnostics. API command argv contains only public resource/database names.
proc=subprocess.Popen(KUBE+['--request-timeout=600s']+args,stdout=subprocess.PIPE,stderr=subprocess.DEVNULL)
try:
count=0
with path.open('xb') as out:
while block:=proc.stdout.read(1024*1024):
count+=len(block)
if count>limit:raise Refused()
out.write(block)
if proc.wait(timeout=600):raise Refused()
finally:
if proc.poll() is None:proc.kill();proc.wait()
def capture(p,output,receipt,decision,checkpoint=lambda:None):
created=[];receipt.update(status='capture_started',decision_ref=decision,plan_run_id=p['run_id'],
created_resources=[],unverified_create_outcomes=[],helper_cleanup_verified=False,temporary_plaintext_cleanup_verified=False)
checkpoint()
with tempfile.TemporaryDirectory(prefix='nk49-identity-',dir='/tmp') as temporary:
receipt['temporary_plaintext_directory']=temporary
checkpoint()
root=Path(temporary)/'payload';root.mkdir(mode=0o700)
held={}
try:
for c in bundle.profile()['components']:
for kind,names in [('deployment',[c['deployment']]),('secret',c['secret_refs']),('configmap',c['configmap_refs'])]:
for name in names:
obj=get(c['namespace'],kind,name)
# Keep reconstructable deployed resources inside encryption,
# without unrelated stale last-applied annotations.
m=obj['metadata'];obj['metadata']={k:m[k] for k in ['name','namespace','uid','resourceVersion']}
obj.pop('status',None)
raw=json.dumps(obj,sort_keys=True).encode();slot=f"resources/{c['namespace']}/{kind}/{name}.json"
held[(c['namespace'],kind,name)]=(m['uid'],m['resourceVersion'])
bundle.save(root,slot,raw)
for proposed in p['create_resources']:
# create, never adopt/apply an existing resource. Unknown create
# outcomes stop; only returned exact UIDs are eligible for cleanup.
pending={'kind':proposed['kind'],**proposed['metadata']}
receipt['unverified_create_outcomes'].append(pending)
checkpoint()
obj=json.loads(command(['create','-f','-','-o','json'],json.dumps(proposed).encode()))
if (obj['kind']!=proposed['kind'] or obj['metadata']['name']!=proposed['metadata']['name']
or obj['metadata']['namespace']!=proposed['metadata']['namespace'] or not obj['metadata'].get('uid')):raise Refused()
created.append(obj)
receipt['unverified_create_outcomes'].remove(pending)
receipt['created_resources'].append({'namespace':obj['metadata']['namespace'],
'kind':obj['kind'],'name':obj['metadata']['name'],'uid':obj['metadata']['uid']})
checkpoint()
for ns in ('sso','mfa'):
name='nk49-capture-'+p['run_id']
command(['-n',ns,'wait','--for=condition=Ready','pod/'+name,'--timeout=180s'],timeout=190)
live=get(ns,'pod',name)
expected=next(o for o in created if o['kind']=='Pod' and o['metadata']['namespace']==ns)
if live['metadata']['uid']!=expected['metadata']['uid']:raise Refused()
wanted=next(o for o in p['create_resources'] if o['kind']=='Pod' and o['metadata']['namespace']==ns)
validate_helper(live,wanted)
if network_review(p['run_id'],{ns:live['metadata']['labels']}):raise Refused()
raw=command(['-n',ns,'exec','-i',name,'-c','capture','--','python3','-c',
(HERE/'identity-files.py').read_text(),ns],timeout=600)
if len(raw)>bundle.LIMIT+2*1024*1024:raise Refused()
bundle.import_files(raw,root,ns)
if get(ns,'pod',name)['metadata']['uid']!=expected['metadata']['uid']:raise Refused()
db=p['database'];current=get('databases','cluster','net-kingdom-pg')
if current['metadata']['uid']!=db['cluster_uid'] or current['status']['currentPrimary']!=db['pod']:raise Refused()
if get('databases','pod',db['pod'])['metadata']['uid']!=db['pod_uid']:raise Refused()
(root/'databases').mkdir(mode=0o700)
bounded_file(['-n','databases','exec',db['pod'],'-c','postgres','--','pg_dump','-U','postgres',
'-d','privacyidea_db','-Fc','--no-owner','--no-acl'],root/'databases/privacyidea.dump',bundle.FILE_LIMIT)
if get('databases','pod',db['pod'])['metadata']['uid']!=db['pod_uid']:raise Refused()
for (ns,kind,name),binding in held.items():
m=get(ns,kind,name)['metadata']
if (m['uid'],m['resourceVersion'])!=binding:raise Refused()
# Re-read original keys after the database dump. No rotation is
# inferred from file presence; a changed key set refuses publication.
raw=command(['-n','mfa','exec','-i','nk49-capture-'+p['run_id'],'-c','capture','--','python3','-c',
(HERE/'identity-files.py').read_text(),'mfa'],timeout=60)
with tempfile.TemporaryDirectory(prefix='nk49-key-recheck-',dir='/tmp') as keytemp:
again=Path(keytemp);bundle.import_files(raw,again,'mfa')
for name in ('enckey','private.pem','public.pem'):
if (root/'data/privacyidea'/name).read_bytes()!=(again/'data/privacyidea'/name).read_bytes():raise Refused()
receipt.update(bundle.seal(root,output,{'started_at':receipt['started_at'],
'finished_at':datetime.now(timezone.utc).isoformat(),'database':db,'decision_ref':decision,
'cluster_uid':prepare.CLUSTER_UID,'global_atomic_snapshot_claimed':False,
'omissions':['historical local backups','SQLite WAL/SHM/journal represented by native snapshots',
'Authelia notification.txt','privacyIDEA service logs; audit data retained in database',
'OpenBao root/unseal/age private key custody remains independently held']}))
finally:
failures=[]
for obj in reversed(created):
try:delete_created(obj)
except Exception:failures.append(obj['metadata']['uid'])
receipt['helper_cleanup_verified']=not failures and not receipt['unverified_create_outcomes']
checkpoint()
if not receipt['helper_cleanup_verified']:raise Refused()
receipt['temporary_plaintext_cleanup_verified']=not Path(temporary).exists()
checkpoint()
if not receipt['temporary_plaintext_cleanup_verified']:raise Refused()
def main():
global KUBE
parser=argparse.ArgumentParser(description=__doc__);parser.add_argument('action',choices=['plan','capture'])
parser.add_argument('--kubeconfig',required=True)
parser.add_argument('--receipt',required=True);parser.add_argument('--plan');parser.add_argument('--plan-sha256')
parser.add_argument('--output');parser.add_argument('--decision-id');a=parser.parse_args();os.umask(0o077)
KUBE=['kubectl','--kubeconfig',str(Path(a.kubeconfig).absolute())]
fd=os.open(a.receipt,os.O_WRONLY|os.O_CREAT|os.O_EXCL|os.O_NOFOLLOW,0o600)
os.close(fd)
r={'status':'failed','started_at':datetime.now(timezone.utc).isoformat(),'secret_values_exported':False}
def checkpoint():
fd,temp=tempfile.mkstemp(prefix='.capture-receipt-',dir=Path(a.receipt).absolute().parent)
try:
with os.fdopen(fd,'w') as out:json.dump(r,out,indent=2);out.write('\n');out.flush();os.fsync(out.fileno())
os.replace(temp,a.receipt)
finally:Path(temp).unlink(missing_ok=True)
code=1
try:
if a.action=='plan':
if any([a.plan,a.plan_sha256,a.output,a.decision_id]):raise Refused()
r=plan();code=0
else:
if not all([a.plan,a.plan_sha256,a.output,a.decision_id]):raise Refused()
uuid.UUID(a.decision_id)
p=checked_plan(Path(a.plan),a.plan_sha256)
capture(p,Path(a.output),r,a.decision_id,checkpoint);code=0
except Exception:
r.update(status='failed',error='capture_refused_or_failed_private_diagnostics_suppressed')
if r.get('temporary_plaintext_directory') and not Path(r['temporary_plaintext_directory']).exists():r['temporary_plaintext_cleanup_verified']=True
finally:
r['finished_at']=datetime.now(timezone.utc).isoformat()
checkpoint()
print('OK: identity '+a.action+' complete; see safe receipt.' if code==0 else 'ERROR: identity operation failed; inspect safe receipt before retry.')
return code
def interrupted(*args):raise Refused()
if __name__=='__main__':
for sig in (signal.SIGHUP,signal.SIGTERM,signal.SIGINT):signal.signal(sig,interrupted)
raise SystemExit(main())

View file

@ -0,0 +1,156 @@
#!/usr/bin/env python3
"""Restore the fetched PI database in an isolated, disposable local PostgreSQL.
Run only through the attended custody helper. No cluster, production database,
network, ports, host data mounts, IAM login or token decryption is involved.
"""
import argparse
from datetime import datetime, timezone
import hashlib
import importlib.util
import json
import os
from pathlib import Path
import signal
import subprocess
import sys
import tempfile
import time
import uuid
ROOT=Path(__file__).resolve().parents[2]
spec=importlib.util.spec_from_file_location('custody',ROOT/'sso-mfa/bootstrap/sops-private.py')
custody=importlib.util.module_from_spec(spec);spec.loader.exec_module(custody)
RECIPIENT='age1zvryunvjhvpkmasskauga2heeg0ztnte9ymgppvjge36ekumk50syr3tsz'
IMAGE='postgres@sha256:ff23cdce56cac62ada6f66013e1a50864c0abbe79d132d40b6e05bd80f378a70'
OBJECT='r01-net-kingdom-pg/net-kingdom-pg-privacyidea_db-20261015T023000Z.dump.age'
SIZE=89129309
class Refused(Exception):pass
def run(args,**kwargs):
result=subprocess.run(args,stdout=subprocess.PIPE,stderr=subprocess.DEVNULL,timeout=600,**kwargs)
if result.returncode:raise Refused()
return result.stdout
def transfer_input(receipt):
r=json.loads(receipt.read_text())
if (r.get('schema')!='netkingdom.privacyidea-offhost-transfer.v1' or
r.get('status')!='ciphertext_fetched' or r.get('object')!=OBJECT or
r.get('ciphertext_bytes')!=SIZE or r.get('object_unchanged_during_transfer') is not True):raise Refused()
path=Path(r['ciphertext_path'])
if not path.is_absolute() or path.parent.parent!=Path('/tmp') or not path.parent.name.startswith('nk49-offhost-'):raise Refused()
if any(p.is_symlink() for p in (path,*path.parents)):raise Refused()
st=path.stat()
if st.st_uid!=os.getuid() or st.st_mode&0o077 or st.st_nlink!=1 or not path.is_file() or st.st_size!=SIZE:raise Refused()
blob=path.read_bytes()
if len(blob)!=SIZE or hashlib.sha256(blob).hexdigest()!=r['ciphertext_sha256']:raise Refused()
return blob,r['ciphertext_sha256']
def restore(dump,container):
# Require the exact image already present; do not pull through a mutable tag.
image_id=run(['docker','image','inspect','--format','{{.Id}}',IMAGE]).decode().strip()
run(['docker','run','--detach','--pull=never','--name',container,'--network','none',
'--security-opt','no-new-privileges','--memory','2g','--cpus','2',
'--tmpfs','/var/lib/postgresql/data:rw,nosuid,nodev,size=1073741824',
'--env','POSTGRES_HOST_AUTH_METHOD=trust','--env','POSTGRES_DB=recovery',
IMAGE,'postgres','-c','listen_addresses='])
for _ in range(60):
# The image starts a temporary server during initdb. Wait until the
# entrypoint has exec'd the final server before attempting the restore.
pid1=subprocess.run(['docker','exec',container,'cat','/proc/1/comm'],
stdout=subprocess.PIPE,stderr=subprocess.DEVNULL,timeout=10)
if pid1.returncode or pid1.stdout.strip()!=b'postgres':
time.sleep(1);continue
ready=subprocess.run(['docker','exec',container,'pg_isready','-U','postgres','-d','recovery'],
stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL,timeout=10)
if ready.returncode==0:break
time.sleep(1)
else:raise Refused()
version=run(['docker','exec',container,'pg_restore','--version']).decode().strip()
with dump.open('rb') as source:
run(['docker','exec','-i',container,'pg_restore','-U','postgres','-d','recovery',
'--no-owner','--no-privileges','--exit-on-error'],stdin=source)
# Fixed aggregate queries only. No identity, resolver config or token values.
counts={}
for table in ('token','resolver','realm'):
raw=run(['docker','exec',container,'psql','-X','-U','postgres','-d','recovery','-At',
'-v','ON_ERROR_STOP=1','-c',f'SELECT count(*) FROM public.{table};']).strip()
if not raw.isdigit():raise Refused()
counts[table]=int(raw)
return {'image':IMAGE,'image_id':image_id,'pg_restore_version':version,'aggregate_row_counts':counts}
def main():
p=argparse.ArgumentParser(description=__doc__)
p.add_argument('--transfer-receipt',required=True);p.add_argument('--receipt',required=True)
a=p.parse_args();os.umask(0o077)
# Reserve the receipt before any recovery action; never overwrite evidence.
fd=os.open(a.receipt,os.O_WRONLY|os.O_CREAT|os.O_EXCL|os.O_NOFOLLOW,0o600)
r={'schema':'netkingdom.privacyidea-database-drill.v1','status':'failed',
'started_at':datetime.now(timezone.utc).isoformat(),'source_sha256':hashlib.sha256(Path(__file__).read_bytes()).hexdigest(),
'production_contacted':False,'secret_values_exported':False,'native_identity_recovery_proven':False,
'token_decryption_proven':False,'audit_signature_continuity_proven':False,
'full_identity_file_coverage_proven':False,'container_cleanup_verified':False,
'temporary_plaintext_cleanup_verified':False}
container='nk49-pi-recovery-'+uuid.uuid4().hex
keyfd=None;work=None
try:
blob,digest=transfer_input(Path(a.transfer_receipt));r['ciphertext_sha256']=digest
keyfd=custody.private_key_fd()
derived=run(['age-keygen','-y',f'/proc/self/fd/{keyfd}'],pass_fds=(keyfd,)).strip()
if derived!=RECIPIENT.encode():raise Refused()
r['registered_recipient_match']=True
with tempfile.TemporaryDirectory(prefix='nk49-pi-drill-',dir='/tmp') as temporary:
work=Path(temporary)
env={'PATH':os.environ.get('PATH',os.defpath),'HOME':temporary,'XDG_CONFIG_HOME':temporary}
wrong=work/'wrong.key'
run(['age-keygen','-o',str(wrong)],env=env)
negative=subprocess.run(['age','-d','-i',str(wrong)],input=blob,stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,timeout=60,env=env)
if negative.returncode==0:raise Refused()
r['wrong_key_refused']=True
dump=work/'privacyidea.dump'
with dump.open('xb') as output:
result=subprocess.run(['age','-d','-i',f'/proc/self/fd/{keyfd}'],input=blob,
stdout=output,stderr=subprocess.DEVNULL,timeout=120,pass_fds=(keyfd,),env=env)
if result.returncode or not 5<dump.stat().st_size<SIZE:raise Refused()
with dump.open('rb') as source:
if source.read(5)!=b'PGDMP':raise Refused()
r['offhost_ciphertext_decryption_verified']=True
try:
r.update(restore(dump,container));r['database_restore_and_readback_passed']=True
finally:
removed=subprocess.run(['docker','rm','--force','--volumes',container],stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,timeout=60)
# A removal error is a failed ceremony even if the restore passed.
if removed.returncode:raise Refused()
absent=subprocess.run(['docker','container','inspect',container],stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,timeout=15)
# Confirm the daemon remains reachable, so inspect failure means absent.
run(['docker','info','--format','{{.ServerVersion}}'])
if absent.returncode==0:raise Refused()
r['container_cleanup_verified']=True
if work.exists():raise Refused()
r['temporary_plaintext_cleanup_verified']=True;r['status']='isolated_database_drill_passed'
except Exception:
r['error']='database_drill_failed_diagnostics_suppressed'
finally:
if keyfd is not None:os.close(keyfd)
if work is not None and not work.exists():r['temporary_plaintext_cleanup_verified']=True
r['finished_at']=datetime.now(timezone.utc).isoformat()
with os.fdopen(fd,'w') as out:json.dump(r,out,indent=2);out.write('\n')
if r['status']=='isolated_database_drill_passed':
print('OK: off-host database decrypted, isolated restore/readback passed, disposable data removed. Full identity recovery remains open.')
return 0
print('ERROR: database drill failed; private diagnostics suppressed. Retain the safe receipt for review.',file=sys.stderr)
return 1
if __name__=='__main__':
for sig in (signal.SIGHUP,signal.SIGTERM,signal.SIGINT):signal.signal(sig,custody.interrupted)
raise SystemExit(main())

View file

@ -0,0 +1,88 @@
#!/usr/bin/env python3
"""Fetch one fixed ciphertext through Platform's attended Backup-account lane.
No key read, plaintext, redirects, proxies, grants, deletion or production writes.
The object and byte count are bound to the reviewed inventory. Provider diagnostics
and credentials remain private. Output is a fresh private directory under /tmp.
"""
import argparse
import base64
from datetime import datetime, timezone
import hashlib
import importlib.util
import json
import os
from pathlib import Path
import tempfile
import urllib.request
HERE=Path(__file__).resolve().parent
spec=importlib.util.spec_from_file_location('inventory',HERE/'inventory-storage-share.py')
owner=importlib.util.module_from_spec(spec);spec.loader.exec_module(owner)
OBJECT='r01-net-kingdom-pg/net-kingdom-pg-privacyidea_db-20261015T023000Z.dump.age'
SIZE=89129309
def metadata(response,url):
etag=response.headers.get('ETag','')
if (response.status!=200 or response.url!=url or
response.headers.get('Content-Length')!=str(SIZE) or
len(etag)>200 or not etag.startswith('"') or not etag.endswith('"') or
any(ord(c)<32 for c in etag)):
raise owner.Refused()
return etag
def transfer(opener,auth,destination):
url=owner.HOST+owner.BASE+OBJECT
headers={'Authorization':'Basic '+auth,'Accept-Encoding':'identity'}
with opener.open(urllib.request.Request(url,method='HEAD',headers=headers),timeout=30) as response:
etag=metadata(response,url)
headers['If-Match']=etag
digest=hashlib.sha256();count=0
with opener.open(urllib.request.Request(url,headers=headers),timeout=60) as response:
if metadata(response,url)!=etag:raise owner.Refused()
fd=os.open(destination,os.O_WRONLY|os.O_CREAT|os.O_EXCL|os.O_NOFOLLOW,0o600)
with os.fdopen(fd,'wb') as out:
while chunk:=response.read(1024*1024):
count+=len(chunk)
if count>SIZE:raise owner.Refused()
digest.update(chunk);out.write(chunk)
out.flush();os.fsync(out.fileno())
if count!=SIZE:raise owner.Refused()
with opener.open(urllib.request.Request(url,method='HEAD',headers=headers),timeout=30) as response:
if metadata(response,url)!=etag:raise owner.Refused()
# ETag used for request binding only; it is not asserted to be a content hash.
return {'ciphertext_bytes':count,'ciphertext_sha256':digest.hexdigest(),
'object_unchanged_during_transfer':True}
def main():
p=argparse.ArgumentParser(description=__doc__);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={'schema':'netkingdom.privacyidea-offhost-transfer.v1','status':'failed',
'started_at':datetime.now(timezone.utc).isoformat(),'provider':'Storage Share',
'object':OBJECT,'age_keys_read':False,'plaintext_created':False,
'production_writes':False,'source_sha256':hashlib.sha256(Path(__file__).read_bytes()).hexdigest(),
'owner_accessor_sha256':hashlib.sha256((HERE/'inventory-storage-share.py').read_bytes()).hexdigest()}
destination=None
try:
identity,credentials=owner.caller_credentials()
r['caller_auth_path']=identity['path'];r['caller_entity_id']=identity.get('entity_id')
auth=base64.b64encode((credentials['BACKUP_USERNAME']+':'+credentials['BACKUP_PASSWORD']).encode()).decode()
destination=Path(tempfile.mkdtemp(prefix='nk49-offhost-',dir='/tmp'))/'privacyidea.dump.age'
opener=urllib.request.build_opener(urllib.request.ProxyHandler({}),owner.NoRedirect())
r.update(transfer(opener,auth,destination))
r.update(status='ciphertext_fetched',ciphertext_path=str(destination))
except Exception:
r['error']='bounded_transfer_failed_diagnostics_suppressed'
if destination is not None:
destination.unlink(missing_ok=True);destination.parent.rmdir()
finally:
r['finished_at']=datetime.now(timezone.utc).isoformat()
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())

View file

@ -0,0 +1,158 @@
{
"schema": "netkingdom.identity-backup-profile.v1",
"status": "implemented_pending_owner_admission",
"owner": "net-kingdom with railiance-platform/rapp-postgres runtime and storage admission",
"public_recipient": "age1zvryunvjhvpkmasskauga2heeg0ztnte9ymgppvjge36ekumk50syr3tsz",
"archive_profile": "netkingdom-identity-v1; new profile, not the bootstrap secrets-only tar profile",
"components": [
{
"namespace": "sso",
"deployment": "lldap",
"deployment_uid": "743a86bd-5cb0-4ca0-9c56-a3438f4746a8",
"pvc_refs": [
{
"name": "lldap-data",
"uid": "0a06b65a-e7b3-42d3-916e-e74dbe5fc7da"
}
],
"secret_refs": [
"lldap-secrets"
],
"configmap_refs": [],
"required_assets": [
"SQLite online-backup snapshot of /data/users.db with integrity check",
"remaining non-backup durable /data files, including any private identity material",
"current deployment configuration and exact lldap-secrets fields"
],
"native_acceptance": [
"directory user/group/membership preservation",
"existing-user password authentication in isolated environment"
]
},
{
"namespace": "sso",
"deployment": "authelia",
"deployment_uid": "dc40a925-2306-4a3b-89c0-e714187503c3",
"pvc_refs": [
{
"name": "authelia-data",
"uid": "86826554-b014-4df4-8b28-866aa64f6413"
}
],
"secret_refs": [
"authelia-secrets",
"authelia-smtp"
],
"configmap_refs": [
"authelia-config"
],
"required_assets": [
"SQLite online-backup snapshot of /var/authelia/data/db.sqlite3 with integrity check",
"current configuration.yml and all Secret-mounted encryption/OIDC/session fields",
"explicit disposition for notifier credentials and remaining durable files"
],
"native_acceptance": [
"storage decryption",
"same OIDC signing identity and preserved client configuration",
"isolated authentication flow with outbound notification disabled"
]
},
{
"namespace": "sso",
"deployment": "keycape",
"deployment_uid": "99ddd83c-cb3f-4847-bcf8-35f1aa87627f",
"pvc_refs": [
{
"name": "keycape-authentication-policy",
"uid": "423f558f-8c11-4edf-956b-243559df4a8e"
}
],
"secret_refs": [
"keycape-approval-engine-operator-client",
"keycape-config",
"keycape-factor-read",
"keycape-informed-decision-sitting-requester-client",
"keycape-rapp-qonto-client",
"keycape-secrets-engine-approval-client",
"keycape-secrets-engine-openbao-client",
"keycape-secrets-engine-requester-client"
],
"configmap_refs": [],
"required_assets": [
"exact current keycape-config config.yaml and key.pem",
"complete stable policy/receipt PVC snapshot",
"current deployment and owner disposition for every referenced credential: encrypted recovery or independently recoverable provider reissue path"
],
"native_acceptance": [
"same issuer/JWKS signing identity",
"registry, authentication policy and receipt continuity",
"current consumer positive/negative authentication under isolated test endpoints"
]
},
{
"namespace": "mfa",
"deployment": "privacyidea",
"deployment_uid": "58c7f96d-61cb-4dd4-bca2-54661c0ac375",
"pvc_refs": [
{
"name": "privacyidea-data",
"uid": "65650fc1-02d8-4c0f-84fc-e0015c8f3ed9"
},
{
"name": "privacyidea-logs",
"uid": "39fa90d9-bc54-4a03-a0d6-ae85165e0cb9"
}
],
"secret_refs": [
"privacyidea-config"
],
"configmap_refs": [
"factor-recovery-01119187d90d7b76",
"keycape-onboarding-9465e5e0b98f7986",
"privacyidea-cfg"
],
"required_assets": [
"original /etc/privacyidea/enckey, private.pem and public.pem with matching RSA pair validation",
"current privacyidea-config and mounted configuration/scripts",
"bounded native PostgreSQL custom dump with exact database identity and key-generation linkage",
"explicit disposition for service logs; audit database/signatures must be preserved"
],
"native_acceptance": [
"decrypt existing retained token material without exporting it",
"verify pre-backup audit signatures and create/verify a new isolated audit entry",
"existing-token MFA and negative-key refusal"
]
}
],
"capture_contract": {
"plaintext": "private task staging outside Git; never process output, State Hub or logs; verified cleanup after encryption",
"consistency": "native SQLite online-backup and PostgreSQL logical snapshot; explicit capture interval per asset; no global atomic snapshot claim; before/after source identities/config versions and directory inventories must match",
"key_policy": "preserve original service keys; no age private key in the payload, no replacement generation, no root/unseal collection",
"encryption": "public-recipient-only streaming archive to age; private fresh ciphertext, no overwrite; manifest inside ciphertext with source identities, times, complete required paths, digests and omissions",
"limits": "measure first; proposed 512 MiB encrypted admission ceiling, no silent truncation/exclusions",
"historical_database": "existing verified October15 dump remains protected; new bundle must link its own exact database/key capture generation and recovery receipt"
},
"proposed_destinations": {
"initial": "Storage Share Backup/railiance-backups/net-kingdom-identity/",
"future_secondary": "s3://railiance-platform-pg-backup/platform-pg/application-archives/net-kingdom/; not implemented/admitted in this rollout",
"admitted": false,
"publisher": "CCR-2026-0048: new folder-only read/create public DAV share, delivered by exact ESO reader; no operator password in runtime; explicitly owner-approved provider-added re-sharing (permissions21)",
"retention": "CCR-2026-0049: separate folder-only read/delete share and process; newest plus seven daily/two weekly and independently recovered pinned generation; unknown/unindexed points never deleted; explicitly owner-approved provider-added re-sharing (permissions25)",
"schedule": "source prepared: daily 03:45 UTC plus up to five minutes jitter, hourly 25-hour freshness check; installation leaves timers disabled until independent archive recovery",
"quota": "512 MiB ceiling, two GiB fresh provider headroom; initial 10 GiB account quota checked; unsupported public-share quota fails closed"
},
"admission_gates": [
"Approve exact CCR-2026-0048/0049 grants and node runtime packet; three required owner reviews, confirmed native SA/namespace/audience boundaries before acceptance",
"Exclude helper capture label from existing broad DNS allow policies; validate read-only helper manifests and cluster/PVC/source bindings before private capture",
"Preserve every referenced credential encrypted with original keys; fresh native SQLite/PG snapshots and exact manifest, with documented omissions and no global atomicity claim",
"Create fresh ciphertext and exact Storage Share object, read back ciphertext and receipt by strong ETag/hash, independently retrieve using operator custody",
"Attended independent age-key proof: wrong-key denial, complete archive hashes, SQLite/RSA checks, disposable native PostgreSQL restore and verified cleanup before timer activation",
"Full NK49 closure separately requires native directory/issuer/MFA authentication, retained token decryption, audit signature continuity, current callers and legacy credential dispositions; archive acceptance does not close those gates",
"Host-local health cannot report a dead host; external outage monitoring and second-provider copy remain separate coverage gaps"
],
"runtime_host": {
"ssh_alias": "railiance01",
"hostname": "239.62.205.92.host.secureserver.net",
"machine_id_sha256": "fe7367a60951922122af7ba18ed2cde65c7e0a232bbedfb045cb575de2094c2f"
}
}

View file

@ -0,0 +1,211 @@
#!/usr/bin/env python3
"""Strict data-bearing identity archive format, native checks and private sealing.
No custody key is used to create a bundle. Its manifest and plaintext digests
stay inside encryption. Public receipts expose only ciphertext provenance.
"""
import base64
from datetime import datetime,timezone
import hashlib
import importlib.util
import io
import json
import os
from pathlib import Path,PurePosixPath
import re
import sqlite3
import stat
import subprocess
import tarfile
import tempfile
ROOT=Path(__file__).resolve().parents[2]
PROFILE=ROOT/'tools/bootstrap-recovery/identity-backup-profile.json'
LIMIT=512*1024*1024
FILE_LIMIT=256*1024*1024
NAME=re.compile(r'[A-Za-z0-9_.-]{1,160}\Z')
class Refused(Exception):pass
def profile():return json.loads(PROFILE.read_text())
def private(path,directory=False,max_bytes=FILE_LIMIT):
if any(p.is_symlink() for p in (path,*path.parents)):raise Refused()
s=path.lstat()
if s.st_uid!=os.getuid() or s.st_mode&0o077:raise Refused()
if directory:
if not stat.S_ISDIR(s.st_mode):raise Refused()
elif not stat.S_ISREG(s.st_mode) or s.st_nlink!=1 or s.st_size>max_bytes:raise Refused()
def allowed(name,profile_data=None):
p=PurePosixPath(name)
if p.is_absolute() or any(not NAME.fullmatch(x) or x in {'.','..'} for x in p.parts):return False
if len(p.parts)>=3 and p.parts[:2] in [('data','lldap'),('data','authelia'),('data','keycape'),('data','privacyidea')]:return True
if name=='databases/privacyidea.dump':return True
for c in (profile_data or profile())['components']:
prefix=f"resources/{c['namespace']}/"
if name==prefix+'deployment/'+c['deployment']+'.json':return True
if any(name==prefix+'secret/'+n+'.json' for n in c['secret_refs']):return True
if any(name==prefix+'configmap/'+n+'.json' for n in c['configmap_refs']):return True
return False
def save(root,name,value,profile_data=None):
if not allowed(name,profile_data) or len(value)>FILE_LIMIT or b'AGE-SECRET-KEY-' in value:raise Refused()
target=root/name;target.parent.mkdir(mode=0o700,parents=True,exist_ok=True)
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(value)
def import_files(raw,root,mode):
names=set();total=0
with tarfile.open(fileobj=io.BytesIO(raw),mode='r:') as archive:
for member in archive:
expected=('data/privacyidea/',) if mode=='mfa' else ('data/lldap/','data/authelia/','data/keycape/')
if (not member.isfile() or not member.name.startswith(expected) or member.name in names
or member.size<0 or member.size>FILE_LIMIT or len(names)>=2048):raise Refused()
names.add(member.name);total+=member.size
if total>LIMIT:raise Refused()
data=archive.extractfile(member).read()
if len(data)!=member.size:raise Refused()
save(root,member.name,data)
def resource(root,ns,kind,name):
p=root/f'resources/{ns}/{kind}/{name}.json'
data=json.loads(p.read_bytes())
if data['metadata']['name']!=name or data['metadata']['namespace']!=ns:raise Refused()
return data
def checks(root,profile_data=None):
# Checks describe stored formats/integrity, not current login validity.
for c in (profile_data or profile())['components']:
deployment=resource(root,c['namespace'],'deployment',c['deployment'])
if deployment.get('kind')!='Deployment' or deployment['metadata']['uid']!=c['deployment_uid']:raise Refused()
for name in c['secret_refs']:
data=resource(root,c['namespace'],'secret',name)
if data.get('kind')!='Secret' or not isinstance(data.get('data'),dict) or not data['data']:raise Refused()
for value in data['data'].values():
if b'AGE-SECRET-KEY-' in base64.b64decode(value,validate=True):raise Refused()
for name in c['configmap_refs']:
if resource(root,c['namespace'],'configmap',name).get('kind')!='ConfigMap':raise Refused()
for name,file in [('lldap','users.db'),('authelia','db.sqlite3')]:
db=root/f'data/{name}/{file}'
with sqlite3.connect(db.as_uri()+'?mode=ro',uri=True) as con:
if con.execute('PRAGMA integrity_check').fetchall()!=[('ok',)]:raise Refused()
with (root/'databases/privacyidea.dump').open('rb') as source:
if source.read(5)!=b'PGDMP':raise Refused()
spec=importlib.util.spec_from_file_location('pi_keys',ROOT/'sso-mfa/k8s/privacyidea/key-custody-remote.py')
keys=importlib.util.module_from_spec(spec);spec.loader.exec_module(keys)
keys.validate({name:(root/'data/privacyidea'/name).read_bytes() for name in ('enckey','private.pem','public.pem')})
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import rsa
config=resource(root,'sso','secret','keycape-config')['data']
if not base64.b64decode(config['config.yaml'],validate=True):raise Refused()
signing=serialization.load_pem_private_key(base64.b64decode(config['key.pem'],validate=True),password=None)
if not isinstance(signing,rsa.RSAPrivateKey) or signing.key_size<2048:raise Refused()
# Policy must exist, even for an empty set of receipts; do not silently
# restore an issuer without its current authentication policy.
policy=json.loads((root/'data/keycape/policy.json').read_bytes())
if not isinstance(policy,dict):raise Refused()
return {'sqlite_integrity_passed':True,'privacyidea_key_format_and_pair_passed':True,
'keycape_signing_key_format_passed':True,'policy_json_present':True,
'database_custom_format_present':True,'native_identity_recovery_proven':False}
def inventory(root):
private(root,True);entries=[];total=0
for path in sorted(root.rglob('*')):
if path.is_dir():private(path,True);continue
private(path);name=path.relative_to(root).as_posix()
if not allowed(name):raise Refused()
with path.open('rb') as source:value=source.read(FILE_LIMIT+1)
if len(value)>FILE_LIMIT or b'AGE-SECRET-KEY-' in value:raise Refused()
total+=len(value)
if total>LIMIT or len(entries)>=4096:raise Refused()
entries.append({'path':name,'bytes':len(value),'sha256':hashlib.sha256(value).hexdigest()})
if not entries:raise Refused()
return entries
def seal(root,output,capture):
output=output.absolute();private(output.parent,True)
if output.exists() or output.is_symlink():raise Refused()
checked=checks(root);entries=inventory(root)
manifest={'schema':'netkingdom.identity-archive.v1','profile_sha256':hashlib.sha256(PROFILE.read_bytes()).hexdigest(),
'profile':profile(),
'captured_at':datetime.now(timezone.utc).isoformat(),'capture':capture,'files':entries,'checks':checked}
with tempfile.TemporaryDirectory(prefix='.identity-seal-',dir=output.parent) as temporary:
ciphertext=Path(temporary)/'ciphertext'
fd=os.open(ciphertext,os.O_WRONLY|os.O_CREAT|os.O_EXCL,0o600)
proc=subprocess.Popen(['age','-r',profile()['public_recipient']],stdin=subprocess.PIPE,stdout=fd,stderr=subprocess.DEVNULL)
os.close(fd)
try:
with tarfile.open(fileobj=proc.stdin,mode='w|') as archive:
raw=json.dumps(manifest,sort_keys=True).encode();m=tarfile.TarInfo('manifest.json');m.size=len(raw);m.mode=0o600
archive.addfile(m,io.BytesIO(raw))
for entry in entries:
with (root/entry['path']).open('rb') as source:
value=source.read(FILE_LIMIT+1)
if hashlib.sha256(value).hexdigest()!=entry['sha256']:raise Refused()
m=tarfile.TarInfo(entry['path']);m.size=len(value);m.mode=0o600
archive.addfile(m,io.BytesIO(value))
proc.stdin.close()
if proc.wait(timeout=120) or not 0<ciphertext.stat().st_size<=LIMIT:raise Refused()
# Check source completeness after encryption too; changed/new files
# refuse publication. This is not same-UID adversary isolation.
if inventory(root)!=entries:raise Refused()
with ciphertext.open('rb') as source:
os.fsync(source.fileno());digest=hashlib.file_digest(source,'sha256').hexdigest()
os.link(ciphertext,output)
finally:
if proc.poll() is None:proc.kill();proc.wait()
return {'schema':'netkingdom.identity-capture.v1','status':'encrypted_bundle_created',
'ciphertext_sha256':digest,'ciphertext_bytes':output.stat().st_size,'ciphertext_path':str(output),
'profile_sha256':manifest['profile_sha256'],'file_count':len(entries),
'checks':checked,'offsite_recovery_proven':False,'secret_values_exported':False}
def restore(archive_path,root):
private(root,True)
with tarfile.open(archive_path,mode='r:') as archive:
first=archive.next()
if first is None or first.name!='manifest.json' or not first.isfile() or not 0<first.size<2*1024*1024:raise Refused()
manifest=json.load(archive.extractfile(first))
if manifest.get('schema')!='netkingdom.identity-archive.v1':raise Refused()
captured=manifest['profile']
if captured.get('schema')!='netkingdom.identity-backup-profile.v1':raise Refused()
if len(captured['components'])!=4:raise Refused()
if {(c['namespace'],c['deployment']) for c in captured['components']}!={('sso','lldap'),('sso','authelia'),('sso','keycape'),('mfa','privacyidea')}:raise Refused()
for c in captured['components']:
for key in ('secret_refs','configmap_refs'):
if len(set(c[key]))!=len(c[key]) or any(not NAME.fullmatch(n) or n in {'.','..'} for n in c[key]):raise Refused()
entries=manifest['files'];expected={e['path']:e for e in entries}
if len(expected)!=len(entries) or len(entries)>4096:raise Refused()
seen=set();total=0
# Iteration starts with the already-read first member; explicitly skip
# only that member. A later duplicate manifest is never accepted.
for index,member in enumerate(archive):
if index==0 and member is first:continue
name=member.name
if (not member.isfile() or name in seen or name not in expected or not allowed(name,captured)
or not 0<=member.size<=FILE_LIMIT or member.size!=expected[name]['bytes']):raise Refused()
seen.add(name);total+=member.size
if total>LIMIT:raise Refused()
raw=archive.extractfile(member).read()
if len(raw)!=member.size or hashlib.sha256(raw).hexdigest()!=expected[name]['sha256']:raise Refused()
save(root,name,raw,captured)
if seen!=set(expected):raise Refused()
return manifest,checks(root,captured)
def require_runtime_host():
import socket
host=profile()['runtime_host']
if (os.geteuid()!=0 or socket.gethostname()!=host['hostname']
or hashlib.sha256(Path('/etc/machine-id').read_bytes()).hexdigest()!=host['machine_id_sha256']):raise Refused()

View file

@ -0,0 +1,254 @@
#!/usr/bin/env python3
"""Attended Platform/Backup owner ceremony for two exact new backup shares.
Requires approved, hash-bound CCRs. No operator password/age key is delivered to
the runtime; only a folder-scoped share token is stored with initial KV CAS=0.
"""
import argparse
import base64
import hashlib
import importlib.util
import json
import os
from pathlib import Path
import subprocess
import sys
import urllib.error
import urllib.parse
import urllib.request
import uuid
HERE=Path(__file__).resolve().parent
PLATFORM=HERE.parents[1].parent/'railiance-platform'
sys.path.insert(0,str(PLATFORM/'scripts'))
from state_hub_preflight_lane import bao,data,capabilities,revoke
from repair_eso_kubernetes_auth import role_payload,check_role
from telemetry_smtp_delivery import ensure
def load(name,path):
spec=importlib.util.spec_from_file_location(name,path);m=importlib.util.module_from_spec(spec);spec.loader.exec_module(m);return m
inventory=load('storage_inventory',HERE/'inventory-storage-share.py')
offsite=load('offsite',HERE/'identity-offsite.py')
FOLDER='/railiance-backups/net-kingdom-identity'
PROFILES={'publisher':(48,21),'retention':(49,25)}
class Refused(Exception):pass
def contract(action,expected):
number,permission=PROFILES[action]
path=PLATFORM/f'credential-change-requests/CCR-2026-{number:04d}-net-kingdom-backup-{action}.yaml'
raw=path.read_bytes()
if hashlib.sha256(raw).hexdigest()!=expected:raise Refused()
module=load('credential_change',PLATFORM/'scripts/credential-change.py')
ccr,errors,_=module.validate_ccr(path)
if errors or ccr['status'] not in {'approved','applied','verified','active'}:raise Refused()
required={'platform-owner','net-kingdom-owner','backup-share-owner'}
approved={r['reviewer'].removeprefix('User (').removesuffix(')') for r in ccr.get('review',{}).get('comments',[])
if r.get('decision')=='approved' and r.get('source_ref')}
name='net-kingdom-backup-'+action+'-eso'
expected_path='platform/workloads/net-kingdom/identity-backup/'+action
a=ccr['openbao'];provider=ccr['provider']
if (not required<=approved or a['kv_path']!=expected_path or a['fields']!=['NC_WEBDAV_TOKEN']
or a['policy_name']!=name or a['auth']['role']!=name
or a['auth']['bound_claims']!={'service_account_names':[name],'service_account_namespaces':['sso']}
or provider['folder']!=FOLDER or provider['share_permissions']!=permission or provider['share_type']!=3
or provider['label']!='net-kingdom-identity-'+action
or provider.get('provider_added_resharing_approved') is not True):raise Refused()
if (a.get('metadata_read') is not False or a.get('token_self_lifecycle') is not True
or a['auth'].get('audience')!='openbao' or a['auth'].get('token_no_default_policy') is not True
or any(a['auth'].get(k)!='15m' for k in ['ttl','token_max_ttl','token_explicit_max_ttl'])
or a['auth'].get('policies')!=[name] or provider.get('host')!='nx4069.your-storageshare.de'):raise Refused()
lane={'service_account':name,'namespace':'sso','role':name,'policy':name,
'kv_path':expected_path.replace('platform/','platform/data/',1)}
wanted=('path "'+lane['kv_path']+'" { capabilities = ["read"] }\n'
'path "auth/token/lookup-self" { capabilities = ["read"] }\n'
'path "auth/token/revoke-self" { capabilities = ["update"] }\n')
if (PLATFORM/a['policy_file']).read_text()!=wanted:raise Refused()
return ccr,lane
def request(client,url,auth,method='GET',body=None,extra=None):
if not url.startswith(offsite.HOST+'/'):raise Refused()
h={'Authorization':'Basic '+base64.b64encode((auth[0]+':'+auth[1]).encode()).decode(),
'X-Requested-With':'XMLHttpRequest'};h.update(extra or {})
try:
with client.open(urllib.request.Request(url,data=body,method=method,headers=h),timeout=60) as response:
if response.url!=url:raise Refused()
raw=response.read(1024*1024+1)
if len(raw)>1024*1024:raise Refused()
return response.status,raw
except urllib.error.HTTPError as error:return error.code,b''
def ocs(client,auth,suffix='',fields=None):
url=offsite.HOST+'/ocs/v2.php/apps/files_sharing/api/v1/shares'+suffix
body=urllib.parse.urlencode(fields).encode() if fields is not None else None
status,raw=request(client,url,auth,'POST' if fields is not None else 'GET',body,
{'OCS-APIRequest':'true','Accept':'application/json','Content-Type':'application/x-www-form-urlencoded'})
if status not in (200,201):raise Refused()
parsed=json.loads(raw)['ocs']
if parsed['meta']['statuscode'] not in (100,200):raise Refused()
return parsed['data']
def share(client,auth,action,receipt):
owner=offsite.HOST+'/remote.php/dav/files/Backup'+FOLDER
status,_=request(client,owner,auth,'MKCOL')
if status not in (201,405):raise Refused()
receipt['folder_created']=status==201
matches=[s for s in ocs(client,auth,'?path='+urllib.parse.quote(FOLDER,safe=''))
if s.get('label')=='net-kingdom-identity-'+action]
if len(matches)>1:raise Refused()
if matches:result=matches[0];receipt['share_created']=False
else:
result=ocs(client,auth,fields={'path':FOLDER,'shareType':3,'permissions':PROFILES[action][1],
'label':'net-kingdom-identity-'+action});receipt['share_created']=True
if (result.get('path')!=FOLDER or int(result.get('permissions',-1))!=PROFILES[action][1]
or int(result.get('share_type',-1))!=3 or not result.get('token')):raise Refused()
offsite.headers(result['token'])
receipt.update(share_id=str(result['id']),share_permissions=int(result['permissions']),folder=FOLDER)
return result['token']
def boundary(client,auth,token,action,receipt):
name='nk49-boundary-'+uuid.uuid4().hex+'.age'
payload=subprocess.run(['age','-r',json.loads((HERE/'identity-backup-profile.json').read_text())['public_recipient']],
input=b'NK49 disposable provider boundary fixture',stdout=subprocess.PIPE,stderr=subprocess.DEVNULL,timeout=10)
if payload.returncode:raise Refused()
owner=offsite.HOST+'/remote.php/dav/files/Backup'+FOLDER+'/'+name
public=offsite.BASE+name;created=False;deleted=False
receipt['provider_test_artifacts']=[name]
receipt['boundary_fixture_cleanup_verified']=False
try:
receipt['boundary_phase']='create_owner_fixture'
status,_=request(client,owner,auth,'PUT',payload.stdout,{'If-None-Match':'*'})
if status!=201:raise Refused()
created=True
receipt['boundary_phase']='read_public_fixture'
status,raw=request(client,public,(token,''))
if status!=200 or raw!=payload.stdout:raise Refused()
receipt['boundary_phase']='deny_public_overwrite'
status,_=request(client,public,(token,''),'PUT',payload.stdout)
if status not in (401,403):raise Refused()
denied_name='nk49-absent-'+uuid.uuid4().hex+'.age'
if action=='retention':receipt['provider_test_artifacts'].append(denied_name)
status,_=request(client,offsite.BASE+denied_name,(token,''),'PUT',payload.stdout,{'If-None-Match':'*'}) if action=='retention' else (403,b'')
if status not in (401,403):raise Refused()
receipt['boundary_phase']='public_delete'
status,_=request(client,public,(token,''),'DELETE')
if action=='publisher':
if status not in (401,403):raise Refused()
new=offsite.BASE+'nk49-boundary-'+uuid.uuid4().hex+'.age'
receipt['provider_test_artifacts'].append(new.rsplit('/',1)[-1])
receipt['boundary_phase']='create_public_fixture'
code,_=request(client,new,(token,''),'PUT',payload.stdout,{'If-None-Match':'*'})
if code!=201:raise Refused()
# This fixture belongs to the current ceremony, not retained data.
receipt['boundary_phase']='cleanup_public_fixture'
remove_fixture(client,offsite.HOST+'/remote.php/dav/files/Backup'+FOLDER+'/'+new.rsplit('/',1)[-1],auth,payload.stdout)
else:
if status!=204:raise Refused()
deleted=True
receipt['boundary_phase']='deny_parent_path'
status,_=request(client,offsite.BASE+'%2e%2e/r01-net-kingdom-pg/',(token,''))
receipt['parent_probe_status']=status
if status not in (400,401,403,404):raise Refused()
receipt['parent_path_rejected']=True
# A malformed-path400 proves request rejection, not authorization.
# Separately probe a known existing account folder with only this share
# token: it must not authenticate as the Backup account.
receipt['boundary_phase']='deny_other_account_folder'
status,_=request(client,offsite.HOST+'/remote.php/dav/files/Backup/r01-net-kingdom-pg/',(token,''))
receipt['other_account_folder_status']=status
if status not in (401,403,404):raise Refused()
receipt['provider_boundary_verified']=True
finally:
if created and not deleted:
remove_fixture(client,owner,auth,payload.stdout)
receipt['owner_fixture_cleanup_verified']=True
# A rejected operation may have had an unexpected side effect. Only a
# successful complete boundary test claims cleanup; names remain in the
# receipt for owner reconciliation on every failure.
receipt['boundary_fixture_cleanup_verified']=receipt.get('provider_boundary_verified',False)
def remove_fixture(client,url,auth,expected):
h={'Authorization':'Basic '+base64.b64encode((auth[0]+':'+auth[1]).encode()).decode()}
with client.open(urllib.request.Request(url,method='HEAD',headers=h),timeout=30) as response:
version=offsite.etag(response,url,len(expected))
code,raw=request(client,url,auth,extra={'If-Match':version})
if code!=200 or raw!=expected:raise Refused()
code,_=request(client,url,auth,'DELETE',extra={'If-Match':version})
if code!=204:raise Refused()
def run(action,expected,receipt):
ccr,lane=contract(action,expected)
identity,credentials=inventory.caller_credentials()
receipt['caller_entity_id']=identity.get('entity_id');receipt['caller_auth_path']=identity['path']
client=offsite.opener();auth=(credentials['BACKUP_USERNAME'],credentials['BACKUP_PASSWORD'])
# Use the owner quota accessor only with proxies removed from its environment.
quota=load('owner_quota',PLATFORM/'scripts/migrate_nextcloud_backup_account.py')
quota.request=lambda url,method='GET',body=None,auth=None,headers=None:request(client,url,auth,method,body,headers)
receipt['quota_before']=quota.quota(auth)
if receipt['quota_before']['available_bytes']<offsite.HEADROOM+offsite.LIMIT:raise Refused()
token=share(client,auth,action,receipt)
boundary(client,auth,token,action,receipt)
if action=='publisher':
receipt['publisher_quota_available']=offsite.quota(client,token)
if receipt['publisher_quota_available']<offsite.HEADROOM+offsite.LIMIT:raise Refused()
# Fence agent reads before any initial secret publication.
boundary_path='sys/policies/acl/agent-high-risk-boundary'
current=data(bao(['read','-format=json',boundary_path]))['data']['policy'];added=''
for path in [lane['kv_path'],lane['kv_path'].replace('/data/','/metadata/',1)]:
stanza='path "'+path+'" { capabilities = ["deny"] }\n'
if '"'+path+'"' not in current:added+=stanza
elif stanza.strip() not in current:raise Refused()
if added:
if data(bao(['read','-format=json',boundary_path]))['data']['policy']!=current:raise Refused()
bao(['write',boundary_path,'-'],payload={'policy':current+'\n'+added})
if data(bao(['read','-format=json',boundary_path]))['data']['policy']!=current+'\n'+added:raise Refused()
receipt['coding_agent_fence_verified_before_publication']=True
existing=bao(['read','-format=json',lane['kv_path']],allow_failure=True)
if existing.returncode==0:
native=data(existing)['data']
if native['data']!={'NC_WEBDAV_TOKEN':token}:raise Refused()
receipt['kv_version']=native['metadata']['version'];receipt['existing_identical_custody']=True
else:
if b'No value found' not in existing.stdout+existing.stderr:raise Refused()
native=data(bao(['write','-format=json',lane['kv_path'],'-'],payload={'options':{'cas':0},'data':{'NC_WEBDAV_TOKEN':token}}))
receipt['kv_version']=native['data']['version']
policy=(PLATFORM/ccr['openbao']['policy_file']).read_text()
expected_policy=('path "'+lane['kv_path']+'" { capabilities = ["read"] }\n'
'path "auth/token/lookup-self" { capabilities = ["read"] }\n'
'path "auth/token/revoke-self" { capabilities = ["update"] }\n')
if policy!=expected_policy:raise Refused()
ensure('sys/policies/acl/'+lane['policy'],{'policy':policy},lambda actual: None if actual['policy']==policy else (_ for _ in ()).throw(Refused()))
ensure('auth/kubernetes/role/'+lane['role'],role_payload(lane),lambda actual:check_role(actual,lane))
child=data(bao(['token','create','-format=json','-policy='+lane['policy'],'-policy=agent-high-risk-boundary','-no-default-policy','-ttl=60s']))['auth']['client_token']
try:
if any(v!=['deny'] for v in capabilities(child,[lane['kv_path'],lane['kv_path'].replace('/data/','/metadata/',1)]).values()):raise Refused()
finally:revoke(child)
receipt.update(status='provider_and_reader_prepared',coding_agents_denied=True,escrow_read=False,
operator_password_delivered=False,retained_backups_deleted=False,source_ccr=ccr['id'],ccr_sha256=expected,
eso_delivery_and_native_login_verification_pending=True)
def main():
p=argparse.ArgumentParser(description=__doc__);p.add_argument('action',choices=PROFILES)
p.add_argument('--ccr-sha256',required=True);p.add_argument('--receipt',required=True);a=p.parse_args()
fd=os.open(a.receipt,os.O_WRONLY|os.O_CREAT|os.O_EXCL|os.O_NOFOLLOW,0o600)
r={'status':'failed','action':a.action,'source_sha256':hashlib.sha256(Path(__file__).read_bytes()).hexdigest()}
try:run(a.action,a.ccr_sha256,r)
except Exception:r.update(status='failed',error='contained_setup_failed_inspect_partial_state_before_retry')
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())

View file

@ -0,0 +1,144 @@
{
"apiVersion": "v1",
"kind": "List",
"items": [
{
"apiVersion": "v1",
"kind": "ServiceAccount",
"metadata": {
"name": "net-kingdom-backup-publisher-eso",
"namespace": "sso"
},
"automountServiceAccountToken": false
},
{
"apiVersion": "external-secrets.io/v1",
"kind": "SecretStore",
"metadata": {
"name": "net-kingdom-backup-publisher",
"namespace": "sso"
},
"spec": {
"provider": {
"vault": {
"server": "http://openbao.openbao.svc:8200",
"path": "platform",
"version": "v2",
"auth": {
"kubernetes": {
"mountPath": "kubernetes",
"role": "net-kingdom-backup-publisher-eso",
"serviceAccountRef": {
"name": "net-kingdom-backup-publisher-eso",
"audiences": [
"openbao"
]
}
}
}
}
}
}
},
{
"apiVersion": "external-secrets.io/v1",
"kind": "ExternalSecret",
"metadata": {
"name": "net-kingdom-backup-publisher",
"namespace": "sso"
},
"spec": {
"refreshInterval": "1h",
"secretStoreRef": {
"kind": "SecretStore",
"name": "net-kingdom-backup-publisher"
},
"target": {
"name": "net-kingdom-backup-publisher",
"creationPolicy": "Owner",
"template": {
"type": "Opaque"
}
},
"data": [
{
"secretKey": "token",
"remoteRef": {
"key": "workloads/net-kingdom/identity-backup/publisher",
"property": "NC_WEBDAV_TOKEN"
}
}
]
}
},
{
"apiVersion": "v1",
"kind": "ServiceAccount",
"metadata": {
"name": "net-kingdom-backup-retention-eso",
"namespace": "sso"
},
"automountServiceAccountToken": false
},
{
"apiVersion": "external-secrets.io/v1",
"kind": "SecretStore",
"metadata": {
"name": "net-kingdom-backup-retention",
"namespace": "sso"
},
"spec": {
"provider": {
"vault": {
"server": "http://openbao.openbao.svc:8200",
"path": "platform",
"version": "v2",
"auth": {
"kubernetes": {
"mountPath": "kubernetes",
"role": "net-kingdom-backup-retention-eso",
"serviceAccountRef": {
"name": "net-kingdom-backup-retention-eso",
"audiences": [
"openbao"
]
}
}
}
}
}
}
},
{
"apiVersion": "external-secrets.io/v1",
"kind": "ExternalSecret",
"metadata": {
"name": "net-kingdom-backup-retention",
"namespace": "sso"
},
"spec": {
"refreshInterval": "1h",
"secretStoreRef": {
"kind": "SecretStore",
"name": "net-kingdom-backup-retention"
},
"target": {
"name": "net-kingdom-backup-retention",
"creationPolicy": "Owner",
"template": {
"type": "Opaque"
}
},
"data": [
{
"secretKey": "token",
"remoteRef": {
"key": "workloads/net-kingdom/identity-backup/retention",
"property": "NC_WEBDAV_TOKEN"
}
}
]
}
}
]
}

View file

@ -0,0 +1,109 @@
#!/usr/bin/env python3
"""Private in-helper PVC capture. Stdout is a secret-bearing tar: parent MUST capture.
Sources are mounted read-only; SQLite uses its online backup API, never raw copy
or immutable=1. No production file is generated, replaced, chmod'd or removed.
"""
import hashlib
import io
import os
from pathlib import Path
import signal
import sqlite3
import stat
import sys
import tarfile
import tempfile
MAX_FILE=256*1024*1024
MAX_TOTAL=512*1024*1024
MAX_COUNT=2048
class Refused(Exception):pass
def read(path):
if any(p.is_symlink() for p in (path,*path.parents)):raise Refused()
fd=os.open(path,os.O_RDONLY|os.O_NOFOLLOW|os.O_NONBLOCK)
try:
before=os.fstat(fd)
if not stat.S_ISREG(before.st_mode) or before.st_nlink!=1 or before.st_size>MAX_FILE:raise Refused()
with os.fdopen(os.dup(fd),'rb') as stream:data=stream.read(MAX_FILE+1)
after=os.stat(path,follow_symlinks=False)
if (len(data)>MAX_FILE or b'AGE-SECRET-KEY-' in data or
any(getattr(before,k)!=getattr(after,k) for k in ('st_dev','st_ino','st_size','st_mtime_ns','st_ctime_ns'))):raise Refused()
return data
finally:os.close(fd)
def tree(root,database=None,omit_notification=False,omit_backups=True):
result={}
for directory,dirs,files in os.walk(root,followlinks=False):
path=Path(directory)
if path.is_symlink():raise Refused()
# Historical local snapshots are redundant; do not recursively include
# accumulated backups. WAL/SHM are represented by the native snapshot.
if omit_backups:dirs[:]=[name for name in dirs if name!='backups']
for name in dirs:
if (path/name).is_symlink():raise Refused()
for name in files:
relative=(path/name).relative_to(root).as_posix()
if database and relative in {database,database+'-wal',database+'-shm',database+'-journal'}:continue
if omit_notification and relative=='notification.txt':continue
result[relative]=read(path/name)
if len(result)>MAX_COUNT or sum(map(len,result.values()))>MAX_TOTAL:raise Refused()
return result
def snapshot(source,target):
if source.is_symlink() or not source.is_file():raise Refused()
# A read-only URI intentionally fails if the engine needs a write to its
# production WAL/SHM. Never fall back to a raw copy or mutate the mount.
with sqlite3.connect(source.as_uri()+'?mode=ro',uri=True,timeout=10) as origin:
with sqlite3.connect(target) as dest:
origin.backup(dest,pages=128,sleep=0.05)
if dest.execute('PRAGMA integrity_check').fetchall()!=[('ok',)]:raise Refused()
return read(target)
def capture(mode,root=Path('/source'),scratch=Path('/scratch')):
values={}
with tempfile.TemporaryDirectory(prefix='snapshot-',dir=scratch) as temporary:
if mode=='sso':
for name,database in [('lldap','users.db'),('authelia','db.sqlite3'),('keycape',None)]:
source=root/name
if source.is_symlink() or not source.is_dir():raise Refused()
first=tree(source,database,name=='authelia',name!='keycape')
if database:first[database]=snapshot(source/database,Path(temporary)/(name+'.sqlite'))
second=tree(source,database,name=='authelia',name!='keycape')
if {k:v for k,v in first.items() if k!=database}!=second:raise Refused()
values.update({f'data/{name}/{k}':v for k,v in first.items()})
elif mode=='mfa':
first=tree(root/'privacyidea')
if not {'enckey','private.pem','public.pem'}<=set(first) or first!=tree(root/'privacyidea'):raise Refused()
values={f'data/privacyidea/{name}':value for name,value in first.items()}
else:raise Refused()
if len(values)>MAX_COUNT or sum(map(len,values.values()))>MAX_TOTAL:raise Refused()
return values
def emit(values,destination):
with tarfile.open(fileobj=destination,mode='w|') as archive:
for name,value in sorted(values.items()):
member=tarfile.TarInfo(name);member.size=len(value);member.mode=0o600
archive.addfile(member,io.BytesIO(value))
def interrupted(*args):raise Refused()
if __name__=='__main__':
for sig in (signal.SIGTERM,signal.SIGHUP,signal.SIGINT):signal.signal(sig,interrupted)
try:
os.umask(0o077)
if len(sys.argv)!=2:raise Refused()
emit(capture(sys.argv[1]),sys.stdout.buffer)
except Exception:
print('ERROR: private file capture refused; diagnostics suppressed.',file=sys.stderr)
raise SystemExit(1)

View file

@ -0,0 +1,65 @@
#!/usr/bin/env python3
"""Plan two exact DNS-selector exclusions; apply only an owner-admitted hash-bound plan."""
import argparse
import hashlib
import importlib.util
import json
import os
from pathlib import Path
HERE=Path(__file__).resolve().parent
spec=importlib.util.spec_from_file_location('capture',HERE/'capture-identity.py')
capture=importlib.util.module_from_spec(spec);spec.loader.exec_module(capture)
SELECTOR={'matchExpressions':[{'key':'net-kingdom/capture','operator':'DoesNotExist'}]}
def plan():
if capture.get(None,'namespace','kube-system')['metadata']['uid']!=capture.prepare.CLUSTER_UID:raise capture.Refused()
rows=[]
for ns in ('sso','mfa'):
obj=capture.get(ns,'networkpolicy','allow-egress-dns');s=obj['spec'];m=obj['metadata']
if s['podSelector'] not in ({},SELECTOR):raise capture.Refused()
pods=json.loads(capture.command(['-n',ns,'get','pods','-o','json']))['items']
if any('net-kingdom/capture' in p['metadata'].get('labels',{}) for p in pods):raise capture.Refused()
# Preserve every peer, port and policy type. Existing unlabelled service
# pods retain their exact selector match. UID/RV/spec tests guard drift.
patch=[{'op':'test','path':'/metadata/uid','value':m['uid']},
{'op':'test','path':'/metadata/resourceVersion','value':m['resourceVersion']},
{'op':'test','path':'/spec','value':s},
{'op':'replace','path':'/spec/podSelector','value':SELECTOR}]
rows.append({'namespace':ns,'name':m['name'],'uid':m['uid'],'patch':patch})
return {'schema':'netkingdom.identity-isolation.v1','status':'prepared_not_applied',
'kubeconfig':capture.KUBE[2],'source_sha256':hashlib.sha256(Path(__file__).read_bytes()).hexdigest(),'policies':rows}
def apply(packet,receipt,dry_run=False):
if packet!=plan():raise capture.Refused()
for row in packet['policies']:
args=['-n',row['namespace'],'patch','networkpolicy',row['name'],'--type=json','-p',json.dumps(row['patch']),'-o','json']
if dry_run:args+=['--dry-run=server']
receipt['attempted'].append({'namespace':row['namespace'],'name':row['name'],'uid':row['uid']})
actual=json.loads(capture.command(args));expected=row['patch'][2]['value'].copy();expected['podSelector']=SELECTOR
if actual['metadata']['uid']!=row['uid'] or actual['spec']!=expected:raise capture.Refused()
receipt['verified'].append({'namespace':row['namespace'],'name':row['name'],'uid':row['uid']})
receipt['status']='server_dry_run_passed' if dry_run else 'capture_dns_exclusions_verified'
def main():
p=argparse.ArgumentParser(description=__doc__);p.add_argument('action',choices=['plan','dry-run','apply'])
p.add_argument('--kubeconfig',required=True);p.add_argument('--plan');p.add_argument('--plan-sha256');p.add_argument('--receipt',required=True)
a=p.parse_args();os.umask(0o077);capture.KUBE=['kubectl','--kubeconfig',a.kubeconfig]
fd=os.open(a.receipt,os.O_WRONLY|os.O_CREAT|os.O_EXCL|os.O_NOFOLLOW,0o600)
r={'status':'failed','attempted':[],'verified':[]}
try:
if a.action=='plan':r=plan()
else:
raw=Path(a.plan).read_bytes()
if hashlib.sha256(raw).hexdigest()!=a.plan_sha256:raise capture.Refused()
apply(json.loads(raw),r,a.action=='dry-run')
except Exception:r.update(status='failed',error='isolation_refused_review_partial_attempts')
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())

View file

@ -0,0 +1,124 @@
#!/usr/bin/env python3
"""Folder-scoped Storage Share ciphertext publication and verified readback.
Runtime gets only a dedicated read/create share token. Account administration,
deletion and age private keys have no route in this publisher.
"""
import base64
from datetime import datetime,timezone
import hashlib
import json
import os
from pathlib import Path
import re
import tempfile
import urllib.error
import urllib.request
import xml.etree.ElementTree as ET
HOST='https://nx4069.your-storageshare.de'
BASE=HOST+'/public.php/webdav/'
NAME=re.compile(r'net-kingdom-identity-\d{8}T\d{6}Z-[a-f0-9]{12}\.tar\.age\Z')
LIMIT=512*1024*1024
HEADROOM=2*1024**3
class Refused(Exception):pass
class NoRedirect(urllib.request.HTTPRedirectHandler):
def redirect_request(self,*args,**kwargs):raise Refused()
def opener():return urllib.request.build_opener(urllib.request.ProxyHandler({}),NoRedirect())
def headers(token):
if not isinstance(token,str) or not re.fullmatch('[A-Za-z0-9]{10,128}',token):raise Refused()
return {'Authorization':'Basic '+base64.b64encode((token+':').encode()).decode(),
'X-Requested-With':'XMLHttpRequest','Accept-Encoding':'identity'}
def digest(path):
with path.open('rb') as source:return hashlib.file_digest(source,'sha256').hexdigest()
def quota(client,token):
body=b'<d:propfind xmlns:d="DAV:"><d:prop><d:quota-available-bytes/></d:prop></d:propfind>'
h=headers(token);h.update(Depth='0');h['Content-Type']='application/xml'
with client.open(urllib.request.Request(BASE,data=body,method='PROPFIND',headers=h),timeout=30) as response:
if response.status!=207 or response.url!=BASE:raise Refused()
raw=response.read(1024*1024+1)
if len(raw)>1024*1024:raise Refused()
amount=ET.fromstring(raw).findtext('.//{DAV:}quota-available-bytes') or ''
if not amount.isdigit():raise Refused()
return int(amount)
def etag(response,url,size):
value=response.headers.get('ETag','')
if (response.status!=200 or response.url!=url or response.headers.get('Content-Length')!=str(size)
or not re.fullmatch(r'"[^"\x00-\x20]{1,180}"',value)):raise Refused()
return value
def readback(client,token,name,size,expected,target):
valid=NAME.fullmatch(name) or (name.endswith('.receipt.json') and NAME.fullmatch(name.removesuffix('.receipt.json')) and size<=65536)
if not valid or not 0<size<=LIMIT:raise Refused()
url=BASE+name;h=headers(token)
with client.open(urllib.request.Request(url,method='HEAD',headers=h),timeout=30) as response:version=etag(response,url,size)
h['If-Match']=version;count=0;sha=hashlib.sha256()
with client.open(urllib.request.Request(url,headers=h),timeout=180) as response:
if etag(response,url,size)!=version:raise Refused()
fd=os.open(target,os.O_WRONLY|os.O_CREAT|os.O_EXCL|os.O_NOFOLLOW,0o600)
with os.fdopen(fd,'wb') as out:
while block:=response.read(1024*1024):
count+=len(block)
if count>size:raise Refused()
sha.update(block);out.write(block)
out.flush();os.fsync(out.fileno())
if count!=size or sha.hexdigest()!=expected:raise Refused()
with client.open(urllib.request.Request(url,method='HEAD',headers=h),timeout=30) as response:
if etag(response,url,size)!=version:raise Refused()
return version
def publish(client,token,source,capture,target):
if (not NAME.fullmatch(source.name) or source.is_symlink() or source.stat().st_mode&0o077
or source.stat().st_uid!=os.getuid() or source.stat().st_nlink!=1 or not source.is_file()
or source.stat().st_size>LIMIT or capture.get('status')!='encrypted_bundle_created'
or capture.get('helper_cleanup_verified') is not True or capture.get('temporary_plaintext_cleanup_verified') is not True
or digest(source)!=capture.get('ciphertext_sha256') or source.stat().st_size!=capture.get('ciphertext_bytes')):raise Refused()
with source.open('rb') as stream:
if stream.read(22)!=b'age-encryption.org/v1\n':raise Refused()
available=quota(client,token)
if available<source.stat().st_size+HEADROOM:raise Refused()
url=BASE+source.name;h=headers(token)
try:
with client.open(urllib.request.Request(url,method='HEAD',headers=h),timeout=30):raise Refused()
except urllib.error.HTTPError as error:
if error.code!=404:raise Refused() from None
h.update({'If-None-Match':'*','Content-Type':'application/octet-stream','Content-Length':str(source.stat().st_size)})
# An unknown upload outcome is never retried under this name automatically.
with source.open('rb') as stream:
with client.open(urllib.request.Request(url,data=stream,method='PUT',headers=h),timeout=600) as response:
if response.status!=201 or response.url!=url:raise Refused()
version=readback(client,token,source.name,source.stat().st_size,capture['ciphertext_sha256'],target)
index={'schema':'netkingdom.identity-offsite-index.v1','status':'readback_verified',
'artifact':source.name,'ciphertext_sha256':capture['ciphertext_sha256'],
'ciphertext_bytes':source.stat().st_size,'captured_at':capture['finished_at']}
raw=json.dumps(index,sort_keys=True).encode();index_name=source.name+'.receipt.json';index_url=BASE+index_name
h=headers(token);h.update({'If-None-Match':'*','Content-Type':'application/json','Content-Length':str(len(raw))})
with client.open(urllib.request.Request(index_url,data=raw,method='PUT',headers=h),timeout=30) as response:
if response.status!=201 or response.url!=index_url:raise Refused()
with tempfile.TemporaryDirectory(prefix='.identity-index-',dir=target.parent) as temporary:
readback(client,token,index_name,len(raw),hashlib.sha256(raw).hexdigest(),Path(temporary)/'index.json')
return {'schema':'netkingdom.identity-offsite.v1','status':'readback_verified',
'observed_at':datetime.now(timezone.utc).isoformat(),'provider':'Storage Share',
'folder':'Backup/railiance-backups/net-kingdom-identity','artifact':source.name,
'ciphertext_sha256':capture['ciphertext_sha256'],'ciphertext_bytes':source.stat().st_size,
'fetched_path':str(target),'etag':version,'quota_available_before':available,
'new_object_created':True,'downloaded_digest_verified':True,
'verified_index_published':True,'index_name':index_name,
'age_private_key_used':False,'secret_values_exported':False,'retained_objects_deleted':False,
'native_identity_recovery_proven':False}

View file

@ -0,0 +1,106 @@
#!/usr/bin/env python3
"""Separate folder-scoped read/delete executor for verified new identity backups.
Unknown/unindexed files are never eligible. The recovered point, newest point,
seven daily and two weekly points remain protected. No grant or account login.
"""
from datetime import datetime,timezone
import hashlib
import importlib.util
import json
from pathlib import Path
import re
import tempfile
import urllib.parse
import urllib.request
import xml.etree.ElementTree as ET
spec=importlib.util.spec_from_file_location('offsite',Path(__file__).with_name('identity-offsite.py'))
offsite=importlib.util.module_from_spec(spec);spec.loader.exec_module(offsite)
def listing(client,token):
h=offsite.headers(token);h.update({'Depth':'1','Content-Type':'application/xml'})
body=b'<d:propfind xmlns:d="DAV:"><d:prop><d:resourcetype/><d:getcontentlength/><d:getetag/></d:prop></d:propfind>'
with client.open(urllib.request.Request(offsite.BASE,data=body,method='PROPFIND',headers=h),timeout=30) as response:
if response.status!=207 or response.url!=offsite.BASE:raise offsite.Refused()
raw=response.read(4*1024*1024+1)
if len(raw)>4*1024*1024:raise offsite.Refused()
result={}
for response in ET.fromstring(raw).findall('{DAV:}response'):
href=urllib.parse.urlsplit(response.findtext('{DAV:}href') or '')
if href.netloc and href.netloc!=urllib.parse.urlsplit(offsite.HOST).netloc:raise offsite.Refused()
name=urllib.parse.unquote(href.path.rstrip('/').rsplit('/',1)[-1])
for row in response.findall('{DAV:}propstat'):
if ' 200 ' not in (row.findtext('{DAV:}status') or ''):continue
p=row.find('{DAV:}prop')
if p is None or p.find('{DAV:}resourcetype/{DAV:}collection') is not None:continue
is_index=name.endswith('.receipt.json') and offsite.NAME.fullmatch(name.removesuffix('.receipt.json'))
if not offsite.NAME.fullmatch(name) and not is_index:continue
size=p.findtext('{DAV:}getcontentlength') or '';version=p.findtext('{DAV:}getetag') or ''
if not size.isdigit() or not re.fullmatch(r'"[^"\x00-\x20]{1,180}"',version) or name in result:raise offsite.Refused()
result[name]={'bytes':int(size),'etag':version}
if len(result)>10000:raise offsite.Refused()
return result
def generations(client,token,objects):
result=[]
for name,meta in objects.items():
if not offsite.NAME.fullmatch(name) or name+'.receipt.json' not in objects:continue
index=objects[name+'.receipt.json']
if not 0<index['bytes']<=65536:raise offsite.Refused()
url=offsite.BASE+name+'.receipt.json';h=offsite.headers(token);h['If-Match']=index['etag']
with client.open(urllib.request.Request(url,headers=h),timeout=30) as response:
if offsite.etag(response,url,index['bytes'])!=index['etag']:raise offsite.Refused()
raw=response.read(65537)
if len(raw)!=index['bytes']:raise offsite.Refused()
r=json.loads(raw)
if (r.get('schema')!='netkingdom.identity-offsite-index.v1' or r.get('status')!='readback_verified'
or r.get('artifact')!=name or r.get('ciphertext_bytes')!=meta['bytes'] or not 0<meta['bytes']<=offsite.LIMIT
or not re.fullmatch('[a-f0-9]{64}',r.get('ciphertext_sha256',''))):raise offsite.Refused()
stamp=datetime.strptime(name.removeprefix('net-kingdom-identity-')[:16],'%Y%m%dT%H%M%SZ').replace(tzinfo=timezone.utc)
captured=datetime.fromisoformat(r['captured_at'])
if captured.tzinfo is None or abs((captured-stamp).total_seconds())>3600:raise offsite.Refused()
if stamp>datetime.now(timezone.utc):raise offsite.Refused()
result.append({**r,'stamp':stamp,'etag':meta['etag'],'index_etag':index['etag']})
return sorted(result,key=lambda r:r['stamp'],reverse=True)
def select(rows,protected):
if not rows or protected not in {r['artifact'] for r in rows}:raise offsite.Refused()
keep={protected,rows[0]['artifact']};days=set();weeks=set()
for row in rows:
day=row['stamp'].date();week=row['stamp'].isocalendar()[:2]
if day not in days and len(days)<7:keep.add(row['artifact']);days.add(day)
if week not in weeks and len(weeks)<2:keep.add(row['artifact']);weeks.add(week)
return [r for r in rows if r['artifact'] not in keep],keep
def run(client,token,accepted,receipt,execute=False,checkpoint=lambda:None):
if (accepted.get('status')!='identity_archive_verified' or accepted.get('container_cleanup_verified') is not True
or accepted.get('temporary_plaintext_cleanup_verified') is not True or not offsite.NAME.fullmatch(accepted.get('artifact',''))):raise offsite.Refused()
objects=listing(client,token);rows=generations(client,token,objects)
candidates,keep=select(rows,accepted['artifact'])
recovered=next(r for r in rows if r['artifact']==accepted['artifact'])
if recovered['ciphertext_sha256']!=accepted['ciphertext_sha256']:raise offsite.Refused()
receipt.update(status='retention_preview',protected=accepted['artifact'],keep=sorted(keep),
candidates=[r['artifact'] for r in candidates],deleted=[],unknown_objects_untouched=True)
checkpoint()
# Validate retained known-good and newest ciphertext before any deletion.
with tempfile.TemporaryDirectory(prefix='nk49-retained-check-',dir='/tmp') as temporary:
for index,row in enumerate({r['artifact']:r for r in [recovered,rows[0]]}.values()):
offsite.readback(client,token,row['artifact'],row['ciphertext_bytes'],row['ciphertext_sha256'],Path(temporary)/str(index))
if not execute:return
for index,row in enumerate(candidates):
version=offsite.readback(client,token,row['artifact'],row['ciphertext_bytes'],row['ciphertext_sha256'],Path(temporary)/('expire-'+str(index)))
for name,etag in [(row['artifact'],version),(row['artifact']+'.receipt.json',row['index_etag'])]:
h=offsite.headers(token);h['If-Match']=etag
receipt['deletion_in_progress']=name
checkpoint()
with client.open(urllib.request.Request(offsite.BASE+name,method='DELETE',headers=h),timeout=30) as response:
if response.status!=204 or response.url!=offsite.BASE+name:raise offsite.Refused()
receipt['deleted'].append(name)
checkpoint()
receipt.pop('deletion_in_progress',None)
receipt['status']='retention_applied'

View file

@ -0,0 +1,215 @@
#!/usr/bin/env python3
"""Root-owned Railiance backup job. No operator password or age private key.
Publisher and retention are separate processes with separate scoped delivery.
Scheduled execution is gated by an independently recovered archive receipt.
"""
import argparse
import base64
from datetime import datetime,timezone
import fcntl
import importlib.util
import json
import os
from pathlib import Path
import signal
import shutil
import socket
import tempfile
import urllib.request
HERE=Path(__file__).resolve().parent
def load(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
capture=load('capture-identity');offsite=load('identity-offsite');retention=load('identity-retention')
def write(path,value):
fd,name=tempfile.mkstemp(prefix='.identity-receipt-',dir=path.parent)
try:
with os.fdopen(fd,'w') as out:json.dump(value,out,indent=2);out.write('\n');out.flush();os.fsync(out.fileno())
os.replace(name,path)
finally:Path(name).unlink(missing_ok=True)
def token(action):
name='net-kingdom-backup-'+action
es=capture.get('sso','externalsecret',name)
entries=[]
for entry in es['spec'].get('data',[]):
item=dict(entry);ref=dict(item.get('remoteRef',{}))
for key,default in [('conversionStrategy','Default'),('decodingStrategy','None'),('metadataPolicy','None')]:
if ref.pop(key,default)!=default:raise offsite.Refused()
item['remoteRef']=ref;entries.append(item)
if (es['spec']['target']['name']!=name or entries!=[{'secretKey':'token','remoteRef':{
'key':'workloads/net-kingdom/identity-backup/'+action,'property':'NC_WEBDAV_TOKEN'}}]
or es['spec'].get('dataFrom') or es['spec'].get('secretStoreRef')!={'kind':'SecretStore','name':name}
or not any(c['type']=='Ready' and c['status']=='True' for c in es['status'].get('conditions',[]))):raise offsite.Refused()
secret=capture.get('sso','secret',name)
if (set(secret['data'])!={'token'} or not any(o['uid']==es['metadata']['uid'] for o in secret['metadata'].get('ownerReferences',[]))):raise offsite.Refused()
value=base64.b64decode(secret['data']['token'],validate=True).decode();offsite.headers(value)
return value
def accepted(path):
capture.bundle.private(path)
r=json.loads(path.read_text())
if (r.get('schema')!='netkingdom.identity-recovery.v1' or r.get('status')!='identity_archive_verified'
or r.get('all_manifest_hashes_verified') is not True or r.get('container_cleanup_verified') is not True
or r.get('temporary_plaintext_cleanup_verified') is not True or not offsite.NAME.fullmatch(r.get('artifact',''))):raise offsite.Refused()
return r
def post_event(state,summary):
# Non-secret durable reporting: replay retained notes at the next run.
directory=state/'event-outbox';directory.mkdir(mode=0o700,exist_ok=True)
import uuid
path=directory/(uuid.uuid4().hex+'.json')
write(path,{'summary':summary,'event_type':'note','author':'net-kingdom-identity-backup',
'workplan_id':'17d315f0-829e-57c7-85cc-3a7914152804','task_id':'d68cd4d3-1ba3-530e-88f9-26ebb8dd6eac'})
for pending in sorted(directory.glob('*.json'))[:64]:
request=urllib.request.Request('http://10.43.68.154:8000/progress/',data=pending.read_bytes(),
headers={'Content-Type':'application/json'},method='POST')
try:
with urllib.request.build_opener(urllib.request.ProxyHandler({})).open(request,timeout=10) as response:
if response.status!=201:break
pending.unlink()
except Exception:break
def backup(state,decision,first=False):
if not first:accepted(state/'accepted-recovery.json')
if (state/'capture-in-progress.json').exists():raise offsite.Refused()
publisher=token('publisher')
client=offsite.opener()
if offsite.quota(client,publisher)<offsite.HEADROOM+offsite.LIMIT:raise offsite.Refused()
if any(shutil.disk_usage(path).free<2*offsite.LIMIT+offsite.HEADROOM for path in [state,Path('/tmp')]):raise offsite.Refused()
p=capture.plan();stamp=datetime.now(timezone.utc).strftime('%Y%m%dT%H%M%SZ')
name='net-kingdom-identity-'+stamp+'-'+p['run_id']+'.tar.age'
directory=state/'runs'/name;directory.mkdir(mode=0o700,parents=True)
plan_path=directory/'plan.json';write(plan_path,p)
import hashlib
capture.checked_plan(plan_path,hashlib.sha256(plan_path.read_bytes()).hexdigest())
source=directory/name;r={'started_at':datetime.now(timezone.utc).isoformat(),'status':'capture_started'}
record=directory/'capture.json';write(record,r)
write(state/'capture-in-progress.json',{'receipt':str(record),'artifact':name})
try:capture.capture(p,source,r,decision,lambda:write(record,r))
except Exception:
if r.get('temporary_plaintext_directory') and not Path(r['temporary_plaintext_directory']).exists():r['temporary_plaintext_cleanup_verified']=True
r['status']='failed';write(record,r);raise
finally:
if r.get('helper_cleanup_verified') and r.get('temporary_plaintext_cleanup_verified'):
(state/'capture-in-progress.json').unlink()
write(record,r)
r['finished_at']=datetime.now(timezone.utc).isoformat();write(record,r)
write(directory/'transfer-attempt.json',{'status':'pending','artifact':name,'capture_receipt':'capture.json',
'ciphertext_sha256':r['ciphertext_sha256'],'ciphertext_bytes':r['ciphertext_bytes']})
remote=offsite.publish(client,publisher,source,r,directory/'fetched.tar.age')
write(directory/'offsite.json',remote)
# The original private local ciphertext and safe receipts remain. Every
# successful fetched duplicate is unnecessary after first acceptance and
# may be removed by the owner under a separate local-cache policy.
write(state/'last-success.json',{'finished_at':datetime.now(timezone.utc).isoformat(),
'artifact':name,'directory':str(directory),'ciphertext_sha256':remote['ciphertext_sha256'],
'offsite_verified':True})
post_event(state,'Identity backup completed: encrypted full coverage captured, Storage Share upload and downloaded digest verified. Native service authentication remains separate evidence.')
def expire(state,execute=False):
if (state/'retention-blocked.json').exists():raise offsite.Refused()
proof=accepted(state/'accepted-recovery.json')
r={'status':'pending','started_at':datetime.now(timezone.utc).isoformat()}
stamp=datetime.now(timezone.utc).strftime('%Y%m%dT%H%M%S%fZ')
record=state/('retention-'+stamp+'.json')
write(record,r)
try:
retention.run(offsite.opener(),token('retention'),proof,r,execute,lambda:write(record,r))
if execute:r['local_generations_removed']=prune_local(state,proof)
except Exception:
r['status']='failed';write(record,r)
# Stop future automatic expiration after an unknown/partial outcome.
# Owner review must explicitly clear this marker; no blind retries.
if execute:write(state/'retention-blocked.json',{'receipt':str(record),'requires_owner_reconciliation':True})
raise
write(record,r)
def prune_local(state,proof):
root=state/'runs';rows=[];known={}
if not root.exists():return []
capture.bundle.private(root,True)
for directory in root.iterdir():
if directory.is_symlink() or not directory.is_dir() or not offsite.NAME.fullmatch(directory.name):continue
capture.bundle.private(directory,True)
allowed={'plan.json','capture.json','transfer-attempt.json','offsite.json','fetched.tar.age',directory.name}
if {p.name for p in directory.iterdir()}-allowed:continue
try:
remote=json.loads((directory/'offsite.json').read_text());local=json.loads((directory/'capture.json').read_text())
if (remote.get('status')!='readback_verified' or remote.get('verified_index_published') is not True
or remote.get('artifact')!=directory.name or remote.get('ciphertext_sha256')!=local.get('ciphertext_sha256')
or local.get('helper_cleanup_verified') is not True or local.get('temporary_plaintext_cleanup_verified') is not True):continue
for path in directory.iterdir():capture.bundle.private(path,max_bytes=offsite.LIMIT)
if offsite.digest(directory/directory.name)!=remote['ciphertext_sha256']:continue
stamp=datetime.strptime(directory.name.removeprefix('net-kingdom-identity-')[:16],'%Y%m%dT%H%M%SZ').replace(tzinfo=timezone.utc)
rows.append({'artifact':directory.name,'stamp':stamp});known[directory.name]=directory
except Exception:continue
rows.sort(key=lambda r:r['stamp'],reverse=True)
if not rows:return []
protected=proof['artifact'] if proof['artifact'] in known else rows[0]['artifact']
candidates,_=retention.select(rows,protected)
removed=[]
for row in candidates:
directory=known[row['artifact']]
for path in directory.iterdir():path.unlink()
directory.rmdir();removed.append(row['artifact'])
return removed
def main():
p=argparse.ArgumentParser(description=__doc__)
p.add_argument('action',choices=['first-capture','scheduled','retention-preview','retention','status'])
p.add_argument('--kubeconfig',required=True);p.add_argument('--state',default='/var/lib/net-kingdom-backup')
p.add_argument('--decision-id');a=p.parse_args();os.umask(0o077)
state=Path(a.state).absolute()
ready=False
try:
capture.bundle.require_runtime_host()
capture.KUBE=['kubectl','--kubeconfig',a.kubeconfig]
uid=capture.get(None,'namespace','kube-system')['metadata']['uid']
if uid!=capture.prepare.CLUSTER_UID:raise offsite.Refused()
state.mkdir(mode=0o700,parents=True,exist_ok=True);capture.bundle.private(state,True);ready=True
fd=None
try:
if a.action!='status':
fd=os.open(state/'.lock',os.O_RDWR|os.O_CREAT|os.O_NOFOLLOW,0o600)
fcntl.flock(fd,fcntl.LOCK_EX|fcntl.LOCK_NB)
if a.action in {'first-capture','scheduled'}:
import uuid
uuid.UUID(a.decision_id or '')
backup(state,a.decision_id,a.action=='first-capture')
elif a.action.startswith('retention'):expire(state,a.action=='retention')
else:
last=json.loads((state/'last-success.json').read_text())
age=(datetime.now(timezone.utc)-datetime.fromisoformat(last['finished_at'])).total_seconds()
if not 0<=age<=25*3600:raise offsite.Refused()
finally:
if fd is not None:os.close(fd)
except Exception:
if ready:
try:post_event(state,'Identity backup '+a.action+' failed or refused. Private diagnostics suppressed; inspect root-owned receipts and reconcile partial state before retry.')
except Exception:pass
print('ERROR: identity backup operation failed; inspect private owner receipts. No secret diagnostics emitted.');return 1
print('OK: identity backup '+a.action+' completed.');return 0
def interrupted(*args):raise offsite.Refused()
if __name__=='__main__':
for sig in (signal.SIGHUP,signal.SIGTERM,signal.SIGINT):signal.signal(sig,interrupted)
raise SystemExit(main())

View file

@ -0,0 +1,145 @@
#!/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())

View file

@ -0,0 +1,104 @@
#!/usr/bin/env python3
"""Read-only bounded S3 backup metadata through existing scoped runtime delivery.
No bootstrap API key, new grant, object content read, age key, provider write or
retention action. The caller must already have admitted cluster read authority.
"""
import argparse
import base64
from datetime import datetime, timezone
import hashlib
import json
import os
from pathlib import Path
import re
import subprocess
BUCKET='railiance-platform-pg-backup'
ENDPOINT='https://s3.nl-ams.scw.cloud'
PREFIX='platform-pg/'
CLUSTER='a553c742-0115-43d4-99a4-a5ca56fe0786'
class Refused(Exception):pass
def kube(args):
p=subprocess.run(['kubectl',*args,'-o','json'],stdout=subprocess.PIPE,stderr=subprocess.DEVNULL,timeout=30)
if p.returncode:raise Refused()
return json.loads(p.stdout)
def list_prefix(client,prefix):
if not prefix.startswith(PREFIX) or not re.fullmatch(r'[A-Za-z0-9._/-]+',prefix) or '..' in prefix.split('/'):raise Refused()
directories=[];files=[];continuation=None
for _ in range(20):
args={'Bucket':BUCKET,'Prefix':prefix,'Delimiter':'/','MaxKeys':1000}
if continuation:args['ContinuationToken']=continuation
page=client.list_objects_v2(**args)
for row in page.get('CommonPrefixes',[]):
value=row['Prefix']
if not value.startswith(prefix) or not value.endswith('/') or not re.fullmatch(r'[A-Za-z0-9._/-]+',value) or '..' in value.split('/'):raise Refused()
directories.append(value)
for row in page.get('Contents',[]):
name=row['Key']
if not name.startswith(prefix) or not re.fullmatch(r'[A-Za-z0-9._/-]+',name) or '..' in name.split('/'):raise Refused()
if name.endswith(('.age','.json','.yaml')):
size=row['Size']
if type(size) is not int or size<0:raise Refused()
files.append({'key':name,'bytes':size})
if not page.get('IsTruncated'):break
continuation=page.get('NextContinuationToken')
if not continuation:raise Refused()
else:raise Refused()
return {'directories':sorted(set(directories)),'files':sorted(files,key=lambda x:x['key'])}
def inventory(client):
pending=[(PREFIX,0)];result={}
while pending:
prefix,depth=pending.pop(0)
if prefix in result:raise Refused()
if len(result)>=64:raise Refused()
row=list_prefix(client,prefix);result[prefix]=row
if depth<3:
for child in row['directories']:
# Native database WAL/base trees are not application/key-file
# archives. Record their presence without enumerating every WAL.
if any(x in child.split('/') for x in ['wals','base']):continue
pending.append((child,depth+1))
return result
def main():
p=argparse.ArgumentParser(description=__doc__);p.add_argument('--receipt',required=True)
a=p.parse_args();fd=os.open(a.receipt,os.O_WRONLY|os.O_CREAT|os.O_EXCL|os.O_NOFOLLOW,0o600)
r={'schema':'netkingdom.scaleway-backup-inventory.v1','status':'failed',
'started_at':datetime.now(timezone.utc).isoformat(),'read_only':True,
'provider':'Scaleway','bucket':BUCKET,'prefix':PREFIX,'source_sha256':hashlib.sha256(Path(__file__).read_bytes()).hexdigest(),
'bootstrap_api_key_used':False,'age_keys_read':False,'object_contents_downloaded':False,
'secret_values_exported':False,'scope':'Bounded prefix metadata, at most three child levels; WAL/base trees not enumerated; other buckets/prefixes unassessed'}
try:
if kube(['get','namespace','kube-system'])['metadata']['uid']!=CLUSTER:raise Refused()
delivered=kube(['-n','databases','get','secret','platform-pg-backup-s3'])
if delivered['metadata']['namespace']!='databases' or delivered['metadata']['name']!='platform-pg-backup-s3':raise Refused()
values=delivered['data']
# Same governed runtime delivery used by Platform's archive verifier;
# explicit credentials avoid SDK ambient/profile/metadata discovery.
import boto3
from botocore.config import Config
client=boto3.client('s3',endpoint_url=ENDPOINT,region_name='nl-ams',
aws_access_key_id=base64.b64decode(values['ACCESS_KEY_ID'],validate=True).decode(),
aws_secret_access_key=base64.b64decode(values['ACCESS_SECRET_KEY'],validate=True).decode(),
config=Config(signature_version='s3v4',proxies={},connect_timeout=15,read_timeout=30,
retries={'max_attempts':1},s3={'addressing_style':'path'}))
r['prefixes']=inventory(client);r['status']='metadata_inventory_complete'
r['runtime_delivery_uid']=delivered['metadata']['uid']
except Exception:r['error']='inventory_failed_diagnostics_suppressed'
finally:
r['finished_at']=datetime.now(timezone.utc).isoformat()
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())

View file

@ -0,0 +1,114 @@
#!/usr/bin/env python3
"""Silent read-only inventory in the existing Platform Backup-account procedure.
Runs only inside the attended platform-admin envelope. Reuses Platform's owner
custody accessor and fixed provider/account contract; no age key is read and no
backup is downloaded, uploaded, deleted, rotated or granted.
"""
import argparse
import base64
from datetime import datetime, timezone
import hashlib
import json
import os
from pathlib import Path
import re
import sys
import urllib.parse
import urllib.request
import xml.etree.ElementTree as ET
ROOT=Path(__file__).resolve().parents[2]
OWNER=ROOT.parent/'railiance-platform/scripts'
sys.path.insert(0,str(OWNER))
from state_hub_preflight_lane import bao, data
from migrate_nextcloud_backup_account import HOST, OPERATOR_PATH
BASE='/remote.php/dav/files/Backup/railiance-backups/'
LIMIT=4*1024*1024
NAME=re.compile(r'[A-Za-z0-9._-]{1,160}\Z')
class Refused(Exception):pass
class NoRedirect(urllib.request.HTTPRedirectHandler):
def redirect_request(self,*args,**kwargs):raise Refused()
def parse_listing(raw, relative):
if len(raw)>LIMIT:raise Refused()
rows=[]
for item in ET.fromstring(raw).findall('{DAV:}response'):
href=urllib.parse.urlsplit(item.findtext('{DAV:}href') or '')
if href.netloc and href.netloc!=urllib.parse.urlsplit(HOST).netloc:raise Refused()
path=urllib.parse.unquote(href.path)
if not path.startswith(BASE+relative):raise Refused()
name=path[len(BASE+relative):].rstrip('/')
if not name:continue
if not NAME.fullmatch(name) or name in {'.','..'}:raise Refused()
for row in item.findall('{DAV:}propstat'):
if ' 200 ' not in (row.findtext('{DAV:}status') or ''):continue
prop=row.find('{DAV:}prop')
if prop is None:continue
folder=prop.find('{DAV:}resourcetype/{DAV:}collection') is not None
size=prop.findtext('{DAV:}getcontentlength')
if size is not None and not size.isdigit():raise Refused()
# Export only known-safe operational names/size, not arbitrary DAV
# properties, URLs, account metadata or raw provider responses.
if folder or name.endswith(('.age','.json','.yaml')):
rows.append({'name':name,'directory':folder,'bytes':int(size) if size else None})
if len(rows)>10000 or len({r['name'] for r in rows})!=len(rows):raise Refused()
return sorted(rows,key=lambda x:x['name'])
def listing(relative,credentials):
if relative and (not relative.endswith('/') or not NAME.fullmatch(relative[:-1])):raise Refused()
auth=base64.b64encode((credentials['BACKUP_USERNAME']+':'+credentials['BACKUP_PASSWORD']).encode()).decode()
req=urllib.request.Request(HOST+BASE+relative,
data=b'<d:propfind xmlns:d="DAV:"><d:prop><d:resourcetype/><d:getcontentlength/></d:prop></d:propfind>',
method='PROPFIND',headers={'Authorization':'Basic '+auth,'Depth':'1','Content-Type':'application/xml'})
opener=urllib.request.build_opener(urllib.request.ProxyHandler({}),NoRedirect())
with opener.open(req,timeout=30) as response:
if response.status!=207 or response.url!=HOST+BASE+relative:raise Refused()
raw=response.read(LIMIT+1)
return parse_listing(raw,relative)
def caller_credentials():
# Lookup payload contains a token: retain only safe caller/role facts.
identity=data(bao(['token','lookup','-format=json']))['data']
if 'platform-admin' not in identity.get('policies',[]) or 'root' in identity.get('policies',[]):raise Refused()
if not str(identity.get('path','')).startswith('auth/netkingdom/'):raise Refused()
credentials=data(bao(['read','-format=json',OPERATOR_PATH]))['data']['data']
if credentials.get('BACKUP_USERNAME')!='Backup' or not isinstance(credentials.get('BACKUP_PASSWORD'),str) or not credentials['BACKUP_PASSWORD']:raise Refused()
return identity,credentials
def inventory():
identity,credentials=caller_credentials()
root=listing('',credentials)
folders={}
for row in root:
if row['directory'] and re.match(r'^(?:r01-|core-|identity|sso|bootstrap|net-kingdom|openbao)',row['name']):
if len(folders)>=32:raise Refused()
folders[row['name']]=listing(row['name']+'/',credentials)
return {'root':root,'folders':folders,'caller_auth_path':identity['path'],
'caller_entity_id':identity.get('entity_id'),'scope':'Backup/railiance-backups root and bounded identity/database subfolders; other accounts/folders unassessed'}
def main():
p=argparse.ArgumentParser(description=__doc__);p.add_argument('--receipt',required=True)
a=p.parse_args();fd=os.open(a.receipt,os.O_WRONLY|os.O_CREAT|os.O_EXCL|os.O_NOFOLLOW,0o600)
r={'schema':'netkingdom.storage-share-inventory.v1','status':'failed','started_at':datetime.now(timezone.utc).isoformat(),
'read_only':True,'age_keys_read':False,'backup_contents_downloaded':False,'secret_values_exported':False,
'source_sha256':hashlib.sha256(Path(__file__).read_bytes()).hexdigest(),
'owner_sources':[{'path':str(OWNER/name),'sha256':hashlib.sha256((OWNER/name).read_bytes()).hexdigest()} for name in ['state_hub_preflight_lane.py','migrate_nextcloud_backup_account.py']]}
try:r.update(inventory());r['status']='metadata_inventory_complete'
except Exception:r['error']='inventory_failed_diagnostics_suppressed'
finally:
r['finished_at']=datetime.now(timezone.utc).isoformat()
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())

View file

@ -0,0 +1,487 @@
#!/usr/bin/env python3
"""Owner-run T18 session assembly using the existing archive/containment pipeline.
Candidate only: native pinned-image tests and review are required before use.
Shared Docker local-driver tmpfs is required because Docker cp does not support
private tmpfs/mount paths, and the original KeyCape image is distroless.
"""
import argparse
import base64
from datetime import datetime, timezone
import hashlib
import importlib.util
import json
import os
from pathlib import Path
import signal
import re
import stat
import select
import subprocess
import sys
import tempfile
import time
import uuid
HERE=Path(__file__).resolve().parent
def load(name):
spec=importlib.util.spec_from_file_location(name,HERE/(name+'.py'))
module=importlib.util.module_from_spec(spec);spec.loader.exec_module(module);return module
adapter=load('isolated-keycape')
local=load('isolated-lldap')
bundle=local.bundle
# Bind the reused preflight itself, not only this module's direct Docker calls.
local.command=adapter.command
_original_preflight=local.preflight
def preflight():
contexts=json.loads(adapter.command(['docker','context','inspect']))
if len(contexts)!=1 or contexts[0]['Endpoints']['docker']['Host']!=adapter.ENDPOINT:raise adapter.Refused()
result=_original_preflight();result['docker_endpoint']=adapter.ENDPOINT;return result
local.preflight=preflight
WORKER=local.CLIENT
STAGING_ROOT=Path('/dev/shm')
RUNROOTS=('/etc/keycape','/var/lib/keycape-policy','/etc/keycape-factor','/recovery-tls')
VOLUME_OPTIONS={'type':'tmpfs','device':'tmpfs','o':'size=33554432,uid=1000,gid=1000,mode=0700,nosuid,nodev,noexec'}
IMPORT=r'''
import base64,json,os,sys
from pathlib import Path
os.umask(0o077)
p=json.load(sys.stdin)
allowed=('/etc/keycape/','/var/lib/keycape-policy/','/etc/keycape-factor/','/recovery-tls/')
for name,encoded in p.items():
f=Path(name)
if not name.startswith(allowed) or '..' in f.parts or any(x.is_symlink() for x in (f,*f.parents)):raise ValueError()
f.parent.mkdir(mode=0o700,parents=True,exist_ok=True)
fd=os.open(f,os.O_WRONLY|os.O_CREAT|os.O_EXCL|os.O_NOFOLLOW,0o600)
with os.fdopen(fd,'wb') as out:out.write(base64.b64decode(encoded,validate=True))
'''
# Small transport inside the already pinned helper; it has only loopback routes.
# No token handling, auth bypass, dependency imitation, reset or notifier path.
TLS=r'''
import select,socket,socketserver,ssl,threading
context=ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
context.minimum_version=ssl.TLSVersion.TLSv1_2
context.load_cert_chain('/recovery-tls/server.pem','/recovery-tls/server-key.pem')
class Handler(socketserver.BaseRequestHandler):
def handle(self):
try:
with context.wrap_socket(self.request,server_side=True) as incoming:
with socket.create_connection(('127.0.0.1',8080),timeout=10) as outgoing:
incoming.settimeout(15);outgoing.settimeout(15)
while True:
ready,_,_=select.select([incoming,outgoing],[],[],15)
if not ready:return
for source in ready:
data=source.recv(65536)
if not data:return
(outgoing if source is incoming else incoming).sendall(data)
except Exception:pass
class Server(socketserver.ThreadingTCPServer):
allow_reuse_address=True
daemon_threads=True
with Server(('127.0.0.1',443),Handler) as server:server.serve_forever()
'''
def run(argv,**kwargs):return adapter.command(argv,**kwargs)
def plan_session():
run_id=uuid.uuid4().hex
stem='nk49-kc-'+run_id
worker=stem+'-transport';issuer=stem+'-issuer'
volumes={stem+'-v'+str(i):{'target':root,'options':dict(VOLUME_OPTIONS)} for i,root in enumerate(RUNROOTS)}
return {'run_id':run_id,'containers':[worker,issuer], 'worker':worker,'issuer':issuer,
'images':{worker:WORKER,issuer:adapter.IMAGE},'volumes':volumes,
'public_image_probe':stem+'-image-probe','docker_endpoint':adapter.ENDPOINT}
def verify_volume(name,spec,run_id):
obj=json.loads(run(['docker','volume','inspect',name]))
if (len(obj)!=1 or obj[0]['Name']!=name or obj[0]['Driver']!='local' or obj[0].get('Scope')!='local'
or obj[0].get('Options')!=VOLUME_OPTIONS or spec['options']!=VOLUME_OPTIONS
or obj[0].get('Labels',{}).get(adapter.LABEL)!=run_id):raise adapter.Refused()
def base_args(session,name,network):
return ['docker','run','--detach','--pull=never','--name',name,'--network',network,
'--label',adapter.LABEL+'='+session['run_id'],'--read-only','--cap-drop','ALL',
'--security-opt','no-new-privileges','--user','1000:1000','--memory','512m','--cpus','1',
'--pids-limit','64','--log-driver','none','--restart','no',
'--tmpfs','/tmp:rw,nosuid,nodev,noexec,size=16777216,uid=1000,gid=1000,mode=0700']
def mounts(session,readonly=False):
args=[]
for name,spec in session['volumes'].items():
args+=['--mount','type=volume,source='+name+',target='+spec['target']+(',readonly' if readonly and spec['target']!='/var/lib/keycape-policy' else '')]
return args
def provision(session,payload,tls,issuer):
"""Inspect containment and driver options before the first private import."""
from urllib.parse import urlsplit
address=urlsplit(issuer)
if address.scheme!='https' or not address.hostname or address.port not in (None,443) or address.path not in ('','/'):
raise adapter.Refused()
for name,spec in session['volumes'].items():
run(['docker','volume','create','--driver','local','--label',adapter.LABEL+'='+session['run_id'],
'--opt','type=tmpfs','--opt','device=tmpfs','--opt','o='+VOLUME_OPTIONS['o'],name])
verify_volume(name,spec,session['run_id'])
args=base_args(session,session['worker'],'none')+mounts(session)
args+=['--sysctl','net.ipv4.ip_unprivileged_port_start=0','--add-host',address.hostname+':127.0.0.1',
'--entrypoint','python3',WORKER,'-c','import time;time.sleep(1800)']
run(args)
# Check helper and every private filesystem before touching the ciphertext's
# private contents. Issuer is created only after originals are present.
adapter.inspect_session({**session,'containers':[session['worker']],'issuer':session['worker']},worker_only=True)
files={**payload['files'],**{k:base64.b64encode(v).decode() for k,v in tls.items()}}
run(['docker','exec','-i',session['worker'],'python3','-c',IMPORT],input=json.dumps(files).encode())
anchor=run(['docker','container','inspect',session['worker'],'--format','{{.Id}}']).decode().strip()
args=base_args(session,session['issuer'],'container:'+anchor)+mounts(session,readonly=True)
# CLI argv carries environment names only. Original credentials stay in the
# owner subprocess environment and Docker's private container configuration.
env={'PATH':os.environ.get('PATH',os.defpath),'DOCKER_HOST':'unix:///var/run/docker.sock',**payload['env'],
'SSL_CERT_FILE':'/recovery-tls/trust.pem'}
for name in (*payload['env'],'SSL_CERT_FILE'):args+=['--env',name]
args+=[adapter.IMAGE]
run(args,env=env)
adapter.inspect_session(session)
run(['docker','exec','--detach',session['worker'],'python3','-c',TLS])
def cleanup_all(session,receipt):
failed=False
try:adapter.cleanup({**session,'containers':session['containers']+[session['public_image_probe']]},receipt)
except Exception:failed=True
rows=[]
for name,spec in session['volumes'].items():
row={'volume':name,'status':'unverified'};rows.append(row)
try:
exists=run(['docker','volume','ls','--filter','name=^'+name+'$','--format','{{.Name}}']).decode().splitlines()
if not exists:
row['status']='absence_verified'
continue
verify_volume(name,spec,session['run_id'])
run(['docker','volume','rm',name])
remaining=run(['docker','volume','ls','--filter','name=^'+name+'$','--format','{{.Name}}']).decode().splitlines()
if remaining:row['status']='still_present';failed=True
else:row['status']='absence_verified'
except Exception:failed=True
receipt['tmpfs_volume_cleanup_obligations']=rows
receipt['container_cleanup_verified']=not failed
if failed:raise adapter.Refused()
def register_workdir(work, run_id):
if work.parent!=STAGING_ROOT or not re.fullmatch('nk49-keycape-'+run_id+'-[a-z0-9_]{8}',work.name):raise adapter.Refused()
if any(p.is_symlink() for p in (work,*work.parents)):raise adapter.Refused()
info=work.lstat()
if not stat.S_ISDIR(info.st_mode) or info.st_uid!=os.getuid() or stat.S_IMODE(info.st_mode)!=0o700:raise adapter.Refused()
return {'path':str(work),'run_id':run_id,'device':info.st_dev,'inode':info.st_ino,'owner_uid':info.st_uid}
def cleanup_workdir(staging, receipt):
"""Delete only the registered inode below actual /dev/shm, never a replacement."""
row={'path':staging['path'],'device':staging['device'],'inode':staging['inode'],'status':'unverified'}
receipt['plaintext_cleanup_obligation']=row
work=Path(staging['path'])
if work.parent!=STAGING_ROOT or not re.fullmatch('nk49-keycape-'+staging['run_id']+'-[a-z0-9_]{8}',work.name):raise adapter.Refused()
if not any(line.split()[1:3]==['/dev/shm','tmpfs'] for line in Path('/proc/mounts').read_text().splitlines()):raise adapter.Refused()
if any(p.is_symlink() for p in work.parents):raise adapter.Refused()
parentfd=os.open(work.parent,os.O_RDONLY|os.O_DIRECTORY|os.O_NOFOLLOW)
try:
try:info=os.stat(work.name,dir_fd=parentfd,follow_symlinks=False)
except FileNotFoundError:
row['status']='absence_verified';receipt['temporary_plaintext_cleanup_verified']=True;return
if (not stat.S_ISDIR(info.st_mode) or info.st_uid!=staging['owner_uid'] or info.st_uid!=os.getuid()
or info.st_dev!=staging['device'] or info.st_ino!=staging['inode'] or stat.S_IMODE(info.st_mode)!=0o700):
row['status']='identity_mismatch_not_removed';raise adapter.Refused()
def remove(parent,name,expected):
fd=os.open(name,os.O_RDONLY|os.O_DIRECTORY|os.O_NOFOLLOW,dir_fd=parent)
try:
current=os.fstat(fd)
if (current.st_dev,current.st_ino)!=(expected.st_dev,expected.st_ino):raise adapter.Refused()
for child in os.listdir(fd):
before=os.stat(child,dir_fd=fd,follow_symlinks=False)
if before.st_uid!=staging['owner_uid'] or before.st_dev!=staging['device']:raise adapter.Refused()
if stat.S_ISDIR(before.st_mode):remove(fd,child,before)
elif stat.S_ISREG(before.st_mode) and before.st_nlink==1:os.unlink(child,dir_fd=fd)
else:raise adapter.Refused() # symlinks/special files/hardlinks are uncertain, never followed
finally:os.close(fd)
after=os.stat(name,dir_fd=parent,follow_symlinks=False)
if (after.st_dev,after.st_ino)!=(expected.st_dev,expected.st_ino):raise adapter.Refused()
os.rmdir(name,dir_fd=parent)
remove(parentfd,work.name,info)
try:os.stat(work.name,dir_fd=parentfd,follow_symlinks=False)
except FileNotFoundError:
row['status']='absence_verified';receipt['temporary_plaintext_cleanup_verified']=True;return
raise adapter.Refused()
finally:os.close(parentfd)
def recover_obligations(session, staging, receipt):
failed=False
try:cleanup_all(session,receipt)
except Exception:failed=True
# The plaintext obligation is attempted even when Docker is unreachable.
try:cleanup_workdir(staging,receipt)
except Exception:failed=True
if failed:raise adapter.Refused()
def process_facts(pid):
raw=Path('/proc')/str(pid)/'stat'
try:
fields=raw.read_text().rsplit(')',1)[1].split()
return {'pid':pid,'state':fields[0],'pgid':int(fields[2]),'sid':int(fields[3]),
'start_ticks':int(fields[19]),'uid':raw.parent.stat().st_uid}
except FileNotFoundError:return None
def isolate_execution():
# Called by main before reading TLS/key inputs. Never reuse an inherited
# shell, custody-helper, user or unrelated process group.
os.setsid()
pid=os.getpid();facts=process_facts(pid)
if os.getsid(0)!=pid or os.getpgrp()!=pid or facts['sid']!=pid or facts['pgid']!=pid:raise adapter.Refused()
return {k:facts[k] for k in ('pid','pgid','sid','start_ticks','uid')}|{'deadline':time.monotonic()+1800}
def verify_execution(execution):
if execution['pid']!=execution['sid'] or execution['pid']!=execution['pgid'] or execution['uid']!=os.getuid():raise adapter.Refused()
current=process_facts(execution['pid'])
if current is not None and any(current[k]!=execution[k] for k in ('pid','pgid','sid','start_ticks','uid')):
# Numeric leader PID reuse is not authority to signal a new group.
raise adapter.Refused()
def group_members(execution):
verify_execution(execution)
members=[]
for path in Path('/proc').iterdir():
if not path.name.isdecimal():continue
facts=process_facts(int(path.name))
if facts is None or facts['pgid']!=execution['pgid']:continue
if facts['sid']!=execution['sid'] or facts['uid']!=execution['uid'] or facts['start_ticks']<execution['start_ticks']:raise adapter.Refused()
if facts['state'] not in ('Z','X'):members.append(facts)
return members
def pidfd_dead(pidfd):
poller=select.poll();poller.register(pidfd,select.POLLIN)
return bool(poller.poll(0))
def quiesce_execution(execution,parent_pidfd,receipt):
"""Stop only this fresh execution session; cleanup needs no live creator.
Exact parent pidfd prevents leader reuse. Each remaining session member gets
its own pidfd after metadata comparison; no inherited/user-group kill occurs.
Unknown membership or uninterruptible survivors refuse cleanup altogether.
"""
verify_execution(execution)
if not pidfd_dead(parent_pidfd):
signal.pidfd_send_signal(parent_pidfd,signal.SIGTERM)
poller=select.poll();poller.register(parent_pidfd,select.POLLIN);poller.poll(2000)
if not pidfd_dead(parent_pidfd):signal.pidfd_send_signal(parent_pidfd,signal.SIGKILL)
for attempt in range(50):
members=group_members(execution)
if pidfd_dead(parent_pidfd) and not members:
receipt['execution_quiescence_verified']=True;return
for original in members:
try:fd=os.pidfd_open(original['pid'])
except ProcessLookupError:continue
try:
current=process_facts(original['pid'])
if current is None or pidfd_dead(fd):continue
if current!=original: # state may naturally change; bind stable identity below
if any(current[k]!=original[k] for k in ('pid','pgid','sid','start_ticks','uid')):raise adapter.Refused()
verify_execution(execution)
signal.pidfd_send_signal(fd,signal.SIGKILL)
except ProcessLookupError:pass
finally:os.close(fd)
time.sleep(.05)
receipt['execution_quiescence_verified']=False
raise adapter.Refused()
def daemon_uncertain(staging):
return any(Path(staging['path']+suffix).exists() or Path(staging['path']+suffix).is_symlink() for suffix in ('-docker-inflight','-docker-uncertain'))
def reaper_main(session,staging,execution,parent_pidfd):
receipt={'status':'failed','execution_quiescence_verified':False}
verify_execution(execution)
if pidfd_dead(parent_pidfd):raise adapter.Refused()
print('registered',flush=True)
poller=select.poll();poller.register(parent_pidfd,select.POLLIN)
remaining=max(0,int((execution['deadline']-time.monotonic())*1000))
parent_lost=bool(poller.poll(remaining))
receipt['cleanup_trigger']='parent_exit' if parent_lost else 'execution_deadline'
try:
# No deletion races a live creator: first stop/wait this exact owner
# session and its children. A daemon request already accepted can still
# complete after CLI loss; retain that uncertainty even after readback.
quiesce_execution(execution,parent_pidfd,receipt)
uncertain=daemon_uncertain(staging)
receipt['in_flight_daemon_request_uncertain']=uncertain
recover_obligations(session,staging,receipt)
receipt['status']='bounded_cleanup_uncertain' if uncertain else 'bounded_cleanup_verified'
except Exception:receipt['status']='bounded_cleanup_uncertain'
finally:
os.close(parent_pidfd)
try:
fd=os.open(staging['path']+'-reaper.json',os.O_WRONLY|os.O_CREAT|os.O_EXCL|os.O_NOFOLLOW,0o600)
with os.fdopen(fd,'w') as out:json.dump(receipt,out)
except Exception:pass
def start_reaper(session, staging, execution):
adapter.docker_environment()
verify_execution(execution)
if session.get('docker_endpoint')!=adapter.ENDPOINT or staging.get('run_id')!=session['run_id']:raise adapter.Refused()
if register_workdir(Path(staging['path']),session['run_id'])!=staging:raise adapter.Refused()
parent_pidfd=os.pidfd_open(execution['pid'])
code="""import importlib.util,json,sys
spec=importlib.util.spec_from_file_location('session',sys.argv[1]);m=importlib.util.module_from_spec(spec);spec.loader.exec_module(m)
m.reaper_main(json.loads(sys.argv[2]),json.loads(sys.argv[3]),json.loads(sys.argv[4]),int(sys.argv[5]))
"""
try:
child=subprocess.Popen([sys.executable,'-c',code,str(Path(__file__).resolve()),json.dumps(session),json.dumps(staging),json.dumps(execution),str(parent_pidfd)],
stdin=subprocess.DEVNULL,stdout=subprocess.PIPE,stderr=subprocess.DEVNULL,
env={'PATH':os.environ.get('PATH',os.defpath),'DOCKER_HOST':adapter.ENDPOINT},
close_fds=True,pass_fds=(parent_pidfd,),start_new_session=True)
finally:os.close(parent_pidfd)
ready,_,_=select.select([child.stdout],[],[],10)
if not ready or child.stdout.readline()!=b'registered\n':
child.terminate();child.wait(timeout=5);raise adapter.Refused()
child.stdout.close()
return child
def execute(transfer,admission,tls,receipt,execution,checkpoint=None):
"""Existing private custody/transfer/archive APIs; no second archive format."""
verify_execution(execution)
if execution['pid']!=os.getpid():raise adapter.Refused()
target=local.preflight()
for image in (adapter.IMAGE,):run(['docker','image','inspect',image])
local.offline.validate(transfer)
if transfer.get('independent_owner_retrieval_verified') is not True:raise adapter.Refused()
path=Path(transfer['fetched_path']);bundle.private(path,max_bytes=bundle.LIMIT)
if (path.stat().st_size!=transfer['ciphertext_bytes'] or local.offline.offsite.digest(path)!=transfer['ciphertext_sha256']
or admission['ciphertext_sha256']!=transfer['ciphertext_sha256']):raise adapter.Refused()
if hashlib.sha256(tls['/recovery-tls/trust.pem']).hexdigest()!=admission['trust_sha256']:raise adapter.Refused()
if hashlib.sha256(tls['/recovery-tls/server.pem']).hexdigest()!=admission['tls_cert_sha256']:raise adapter.Refused()
# Validate exact private certificate/key pair and original issuer name before
# opening service containers; do not generate replacement trust or keys.
from cryptography import x509
from cryptography.hazmat.primitives import serialization
cert=x509.load_pem_x509_certificate(tls['/recovery-tls/server.pem'])
key=serialization.load_pem_private_key(tls['/recovery-tls/server-key.pem'],password=None)
if cert.public_key().public_numbers()!=key.public_key().public_numbers():raise adapter.Refused()
keyfd=local.verify.custody.private_key_fd();work=None;session=None;reaper=None;staging=None
receipt.update(status='failed',target=target,session_source_sha256=hashlib.sha256(Path(__file__).read_bytes()).hexdigest(),
source_files_sha256={name:hashlib.sha256((HERE/name).read_bytes()).hexdigest() for name in ('isolated-keycape.py','isolated-keycape-session.py','run-isolated-keycape.sh','isolated-lldap.py','identity-bundle.py','verify-identity.py','retrieve-identity-offline.py','identity-offsite.py')},
temporary_plaintext_cleanup_verified=False,container_cleanup_verified=False,
downstream_consumer_reconnect_proven=False,human_mfa_proven=False)
try:
if run(['age-keygen','-y',f'/proc/self/fd/{keyfd}'],pass_fds=(keyfd,)).strip()!=bundle.profile()['public_recipient'].encode():raise adapter.Refused()
session=plan_session()
work=Path(tempfile.mkdtemp(prefix='nk49-keycape-'+session['run_id']+'-',dir='/dev/shm'))
staging=register_workdir(work,session['run_id'])
reaper=start_reaper(session,staging,execution)
adapter.EXECUTION={**execution,'staging':str(work)}
receipt.update(execution_binding=execution,bounded_cleanup_reaper_started=True,plaintext_cleanup_obligation={**staging,'status':'registered_before_plaintext'},docker_endpoint=adapter.ENDPOINT,
reaper_outcome_path=staging['path']+'-reaper.json',
registered_cleanup={'containers':session['containers']+[session['public_image_probe']],'tmpfs_volumes':list(session['volumes'])})
if checkpoint is not None:checkpoint(receipt)
archive=work/'archive.tar'
with archive.open('xb') as output:
p=adapter.process(['age','-d','-i',f'/proc/self/fd/{keyfd}',str(path)],stdout=output,stderr=subprocess.DEVNULL,pass_fds=(keyfd,),timeout=180)
if p.returncode or archive.stat().st_size>bundle.LIMIT+4*1024*1024:raise adapter.Refused()
root=work/'restored';root.mkdir(mode=0o700)
manifest,_=bundle.restore(archive,root)
payload=adapter.private_payload(root,manifest,admission['manifest_sha256'])
import yaml
cfg=yaml.safe_load(base64.b64decode(payload['files']['/etc/keycape/config.yaml']))
if cfg.get('port')!=8080 or cfg.get('issuer')!=admission['issuer']:raise adapter.Refused()
# Public executable readback uses the daemon archive API only on the
# original image rootfs, never a tmpfs or private user-created mount.
probe=session['public_image_probe']
try:
run(['docker','create','--pull=never','--name',probe,'--network','none','--read-only','--label',adapter.LABEL+'='+session['run_id'],adapter.IMAGE])
binary=adapter.container_file(probe,'/keycape')
if hashlib.sha256(binary).hexdigest()!=admission['verifier_sha256']:raise adapter.Refused()
finally:
receipt['public_image_probe_cleanup_obligations']=[{'container':probe,'status':'unverified'}]
run(['docker','rm',probe])
if run(['docker','container','ls','--all','--filter','name=^/'+probe+'$','--format','{{.Names}}']).strip():raise adapter.Refused()
receipt['public_image_probe_cleanup_obligations'][0]['status']='absence_verified'
provision(session,payload,tls,admission['issuer'])
receipt.update(ciphertext_sha256=transfer['ciphertext_sha256'],all_manifest_hashes_verified=True,manifest_file_count=len(manifest['files']))
checked_admission={**admission,'session':session,'trust_path':'/recovery-tls/trust.pem'}
# Native checker consumes no service secret argument or new grant.
# Retry only bounded public readiness; no account/factor attempt.
for attempt in range(40):
try:
numbers=serialization.load_pem_private_key(base64.b64decode(payload['files']['/etc/keycape/key.pem']),password=None).public_key().public_numbers()
adapter.check_jwks(session,checked_admission,numbers);break
except Exception:
if attempt==39:raise
time.sleep(.25)
adapter.native_checks(root,manifest,checked_admission,receipt,cleanup_session=False)
finally:
os.close(keyfd)
try:
if session is not None and staging is not None:
uncertain=daemon_uncertain(staging)
receipt['in_flight_daemon_request_uncertain']=uncertain
recover_obligations(session,staging,receipt)
if uncertain:raise adapter.Refused()
if reaper is not None:
reaper.terminate();reaper.wait(timeout=10)
except Exception:
receipt['status']='failed'
raise
finally:
adapter.EXECUTION=None
if work is not None and not work.exists():receipt['temporary_plaintext_cleanup_verified']=True
def persist_receipt(fd,receipt):
raw=(json.dumps(receipt,indent=2)+'\n').encode()
os.lseek(fd,0,os.SEEK_SET);os.ftruncate(fd,0)
with os.fdopen(os.dup(fd),'wb') as output:
output.write(raw);output.flush();os.fsync(output.fileno())
def main():
p=argparse.ArgumentParser(description=__doc__)
for name in ('transfer-receipt','admission','admission-sha256','tls-cert','tls-key','trust','receipt'):p.add_argument('--'+name,required=True)
args=p.parse_args();os.umask(0o077)
fd=os.open(args.receipt,os.O_WRONLY|os.O_CREAT|os.O_EXCL|os.O_NOFOLLOW,0o600)
receipt={'schema':'netkingdom.isolated-keycape-recovery.v1','status':'failed','production_contacted':False,'secret_values_exported':False}
try:
execution=isolate_execution()
raw=Path(args.admission).read_bytes()
if hashlib.sha256(raw).hexdigest()!=args.admission_sha256:raise adapter.Refused()
admission=json.loads(raw)
tls={}
for name,path in (('server.pem',args.tls_cert),('server-key.pem',args.tls_key),('trust.pem',args.trust)):
source=Path(path);bundle.private(source,max_bytes=1024*1024);tls['/recovery-tls/'+name]=source.read_bytes()
execute(json.loads(Path(args.transfer_receipt).read_text()),admission,tls,receipt,execution,checkpoint=lambda r:persist_receipt(fd,r))
except (Exception,KeyboardInterrupt):receipt.update(status='failed',error='isolated_keycape_failed_private_diagnostics_suppressed')
finally:
receipt['finished_at']=datetime.now(timezone.utc).isoformat()
try:persist_receipt(fd,receipt)
finally:os.close(fd)
print('OK: isolated KeyCape native checks and cleanup passed.' if receipt['status']=='isolated_keycape_native_registration_checked' else 'ERROR: isolated KeyCape stopped; review safe receipt.')
return int(receipt['status']!='isolated_keycape_native_registration_checked')
if __name__=='__main__':
for sig in (signal.SIGHUP,signal.SIGTERM,signal.SIGINT):signal.signal(sig,local.offline.interrupted)
raise SystemExit(main())

View file

@ -0,0 +1,452 @@
#!/usr/bin/env python3
"""T18 native adapter for an owner-admitted, already restored isolated session.
Called after identity-bundle.restore, never with an unvalidated directory. This
module does not provision TLS, grant credentials, or claim downstream acceptance.
The existing decrypt/tmpfs wrapper remains responsible for archive cleanup.
"""
import base64
import hashlib
import importlib.util
import json
import io
import os
import secrets
import tarfile
import time
import re
import subprocess
from pathlib import Path
HERE = Path(__file__).resolve().parent
IMAGE = 'forgejo.coulomb.social/coulomb/key-cape@sha256:cdec9dd7c515f7e0a6169fc3fa560ee2cc47bf40d899755dec808c362f5ce010'
LABEL = 'net-kingdom.recovery-run'
class Refused(Exception):
pass
def load_bundle():
spec = importlib.util.spec_from_file_location('identity_bundle', HERE / 'identity-bundle.py')
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
def private_payload(root, manifest, expected_manifest_sha256):
"""Bind the complete validated archive before selecting KeyCape material.
Private hashes never enter the public receipt. The digest is supplied by the
existing private archive admission, not inferred from an untrusted manifest.
"""
encoded = json.dumps(manifest, sort_keys=True, separators=(',', ':')).encode()
if hashlib.sha256(encoded).hexdigest() != expected_manifest_sha256:
raise Refused()
entries = manifest['files']
if len(entries) != len({e['path'] for e in entries}):
raise Refused()
values = {}
bundle = load_bundle()
for entry in entries:
if not bundle.allowed(entry['path'], manifest['profile']):
raise Refused()
path = root / entry['path']
bundle.private(path)
value = path.read_bytes()
if len(value) != entry['bytes'] or hashlib.sha256(value).hexdigest() != entry['sha256']:
raise Refused()
values[entry['path']] = value
deployment = json.loads(values['resources/sso/deployment/keycape.json'])
main = next(c for c in deployment['spec']['template']['spec']['containers'] if c['name'] == 'keycape')
if main.get('command') or main.get('args') or main.get('envFrom'):
raise Refused()
if main['image'] != IMAGE:
# Mutable-tag/historical mismatches need a separate explicit owner admission.
raise Refused()
secret = json.loads(values['resources/sso/secret/keycape-config.json'])['data']
files = {'/etc/keycape/' + name: base64.b64decode(secret[name], validate=True)
for name in ('config.yaml', 'key.pem')}
for path, value in values.items():
if path.startswith('data/keycape/'):
relative = path.removeprefix('data/keycape/')
files['/var/lib/keycape-policy/' + relative] = value
if '/var/lib/keycape-policy/policy.json' not in files:
raise Refused()
if 'resources/sso/secret/keycape-factor-read.json' in values:
factor=json.loads(values['resources/sso/secret/keycape-factor-read.json'])['data']
files['/etc/keycape-factor/admin-token']=base64.b64decode(factor['admin-token'],validate=True)
# Resolve original delivered environment, including all referenced secrets.
env = {}
for item in main.get('env', []):
if not re.fullmatch(r'KEYCAPE_[A-Z0-9_]+',item['name']):raise Refused()
if item['name'] in env:
raise Refused()
if 'value' in item:
env[item['name']] = item['value']
else:
ref = item['valueFrom']['secretKeyRef']
source = json.loads(values['resources/sso/secret/' + ref['name'] + '.json'])['data']
env[item['name']] = base64.b64decode(source[ref['key']], validate=True).decode()
if env.get('KEYCAPE_CONFIG') != '/etc/keycape/config.yaml' or env.get('KEYCAPE_POLICY_PATH') != '/var/lib/keycape-policy/policy.json':
raise Refused()
return {'files': {p: base64.b64encode(v).decode() for p, v in files.items()}, 'env': env}
ENDPOINT = 'unix:///var/run/docker.sock'
EXECUTION = None
def docker_environment(environment=None):
# Even an admitted daemon exposed by another Unix socket is outside this
# exact session binding. Conflicting ambient routing is refused, not ignored.
for source in (os.environ, environment or {}):
if source.get('DOCKER_HOST') not in (None,'',ENDPOINT):raise Refused()
if source.get('DOCKER_CONTEXT') not in (None,'','default'):raise Refused()
if any(source.get(k) for k in ('DOCKER_TLS_VERIFY','DOCKER_CERT_PATH')):raise Refused()
env=dict(os.environ if environment is None else environment)
for key in ('DOCKER_CONTEXT','DOCKER_TLS_VERIFY','DOCKER_CERT_PATH'):env.pop(key,None)
env['DOCKER_HOST']=ENDPOINT
return env
def process(argv, **kwargs):
marker=None
if EXECUTION is not None and (kwargs.get('start_new_session') or kwargs.get('process_group') not in (None,-1)):raise Refused()
if argv[0]=='docker':
argv=['docker','--host',ENDPOINT,*argv[1:]]
kwargs['env']=docker_environment(kwargs.get('env'))
if EXECUTION is not None:
if (os.getpid()!=EXECUTION['pid'] or os.getsid(0)!=EXECUTION['sid'] or os.getpgrp()!=EXECUTION['pgid']
or time.monotonic()>=EXECUTION['deadline']):raise Refused()
marker=Path(EXECUTION['staging']+'-docker-inflight')
fd=os.open(marker,os.O_WRONLY|os.O_CREAT|os.O_EXCL|os.O_NOFOLLOW,0o600)
os.close(fd)
try:
result=subprocess.run(argv,**kwargs)
if marker is not None and result.returncode!=0:
fd=os.open(Path(EXECUTION['staging']+'-docker-uncertain'),os.O_WRONLY|os.O_CREAT|os.O_NOFOLLOW,0o600);os.close(fd)
return result
except BaseException:
# Parent loss leaves .docker-inflight; caught timeout/transport failures
# retain a sticky uncertainty marker even if later cleanup can run.
if marker is not None:
fd=os.open(Path(EXECUTION['staging']+'-docker-uncertain'),os.O_WRONLY|os.O_CREAT|os.O_NOFOLLOW,0o600);os.close(fd)
raise
finally:
if marker is not None:
try:marker.unlink()
except FileNotFoundError:pass
def command(argv, **kwargs):
maximum = kwargs.pop('max_output', 32768)
result = process(argv, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL,
timeout=kwargs.pop('timeout', 180), **kwargs)
if result.returncode or len(result.stdout) > maximum:
raise Refused()
return result.stdout
def inspect_session(session, worker_only=False):
"""No credential environment is read by Docker inspect or returned to caller."""
if not re.fullmatch(r'[a-f0-9]{32}', session['run_id']):
raise Refused()
names = session['containers']
if not names or len(names) > 12 or len(set(names)) != len(names):
raise Refused()
for name in names:
if not re.fullmatch(r'nk49-kc-' + session['run_id'] + r'-[a-z0-9-]{1,24}', name):
raise Refused()
if session['worker'] not in names or session['issuer'] not in names:
raise Refused()
import os
contexts=json.loads(command(['docker','context','inspect']))
endpoints=[contexts[0]['Endpoints']['docker']['Host'],os.environ.get('DOCKER_HOST') or ENDPOINT]
if not all(x==ENDPOINT for x in endpoints):raise Refused()
info = json.loads(command(['docker', 'info', '--format', '{"ID":{{json .ID}},"Name":{{json .Name}},"OSType":{{json .OSType}}}']))
if info != {'ID': '74f93783-6cef-43a8-a3c4-f62586cd3bc1', 'Name': 'bnt-lap001', 'OSType': 'linux'}:
raise Refused()
# The worker is the anchor; all dependencies/TLS/issuer share its network-none namespace.
anchor = command(['docker', 'container', 'inspect', session['worker'], '--format', '{{.Id}}']).decode().strip()
for name,spec in session.get('volumes',{}).items():
volume=json.loads(command(['docker','volume','inspect',name]))
expected={'type':'tmpfs','device':'tmpfs','o':'size=33554432,uid=1000,gid=1000,mode=0700,nosuid,nodev,noexec'}
if len(volume)!=1 or volume[0]['Driver']!='local' or volume[0].get('Scope')!='local' or volume[0].get('Options')!=expected or spec['options']!=expected or volume[0].get('Labels',{}).get(LABEL)!=session['run_id']:raise Refused()
for name in names:
obj = json.loads(command(['docker', 'container', 'inspect', name, '--format',
'{"host":{{json .HostConfig}},"mounts":{{json .Mounts}},"labels":{{json .Config.Labels}},"image":{{json .Config.Image}},"user":{{json .Config.User}},"entrypoint":{{json .Config.Entrypoint}},"cmd":{{json .Config.Cmd}}}']))
host = obj['host']
mounts=obj['mounts']
destinations=[m['Destination'] for m in mounts]
if len(destinations)!=len(set(destinations)):raise Refused()
for volume,expected_spec in session.get('volumes',{}).items():
matches=[m for m in mounts if m['Destination']==expected_spec['target']]
if len(matches)!=1 or matches[0]['Type']!='volume' or matches[0]['Name']!=volume or matches[0]['RW']!=(name==session['worker'] or expected_spec['target']=='/var/lib/keycape-policy'):raise Refused()
if (obj['labels'].get(LABEL) != session['run_id'] or not obj['user'] or obj['user'].split(':')[0] in ('0', 'root')
or not host['ReadonlyRootfs'] or host['Privileged'] or host.get('Binds') or host.get('PortBindings')
or host.get('PidMode') or host.get('IpcMode') == 'host'
or host.get('UTSMode') or host.get('UsernsMode') == 'host' or host.get('CgroupnsMode') == 'host'
or host.get('Sysctls',{}) not in ({},{'net.ipv4.ip_unprivileged_port_start':'0'} if name==session['worker'] else {})
or host.get('CapAdd') or host.get('Devices') or host.get('DeviceRequests') or host.get('VolumesFrom')
or host.get('CapDrop') != ['ALL']
or host.get('SecurityOpt') not in (['no-new-privileges'], ['no-new-privileges:true'])
or any(m['Type']!='tmpfs' and not (m['Type']=='volume' and m['Name'] in session.get('volumes',{}) and m['Destination']==session['volumes'][m['Name']]['target']) for m in obj['mounts'])
or host['NetworkMode'] != ('none' if name == session['worker'] else 'container:' + anchor)
or obj['image'] != session['images'][name] or '@sha256:' not in obj['image']):
raise Refused()
if worker_only:return
if session['images'][session['issuer']] != IMAGE:
raise Refused()
# Known admitted archive has no command/args override; image default /keycape
# must be the actual issuer executable, not an alternate same-image command.
launch=json.loads(command(['docker','container','inspect',session['issuer'],'--format',
'{"entrypoint":{{json .Config.Entrypoint}},"cmd":{{json .Config.Cmd}},"path":{{json .Path}},"args":{{json .Args}}}']))
if launch != {'entrypoint':['/keycape'],'cmd':None,'path':'/keycape','args':[]}:
raise Refused()
def container_file(container, path):
# Docker streams one tar member into memory; never materialize private files.
raw = command(['docker', 'cp', container + ':' + path, '-'], max_output=128*1024*1024)
with tarfile.open(fileobj=io.BytesIO(raw), mode='r:') as archive:
members = archive.getmembers()
if len(members) != 1 or not members[0].isfile() or members[0].size > 96*1024*1024:
raise Refused()
return archive.extractfile(members[0]).read()
JWKS_PROBE = r"""
import base64,ipaddress,json,socket,ssl,sys,urllib.request
from urllib.parse import urlsplit
p=json.load(sys.stdin);issuer=p['issuer'];origin=urlsplit(issuer)
def require(condition):
if not condition:raise ValueError('public_jwks_binding_refused')
require(origin.scheme=='https')
resolved=socket.getaddrinfo(origin.hostname,origin.port or 443,type=socket.SOCK_STREAM)
require(bool(resolved) and all(ipaddress.ip_address(x[4][0]).is_loopback for x in resolved))
class NoRedirect(urllib.request.HTTPRedirectHandler):
def redirect_request(self,*args,**kwargs):return None
opener=urllib.request.build_opener(urllib.request.ProxyHandler({}),NoRedirect(),
urllib.request.HTTPSHandler(context=ssl.create_default_context(cadata=p['trust'])))
def get(url):
u=urlsplit(url)
require((u.scheme,u.hostname,u.port or 443)==(origin.scheme,origin.hostname,origin.port or 443))
require(not u.username and not u.password and not u.fragment)
with opener.open(url,timeout=10) as response:
raw=response.read(1024*1024+1);require(len(raw)<=1024*1024)
return json.loads(raw)
d=get(issuer.rstrip('/')+'/.well-known/openid-configuration');require(d['issuer']==issuer)
keys=get(d['jwks_uri'])['keys']
def integer(value):return int.from_bytes(base64.urlsafe_b64decode(value+'='*(-len(value)%4)),'big')
# The exact admitted owner release constructs one key, kid key-1. Reject every
# extra key, including irrelevant keys: a native successful exchange can then
# only have used the original signer through the existing verifier.
require(isinstance(keys,list) and len(keys)==1)
k=keys[0]
require(k.get('kty')=='RSA' and k.get('kid')=='key-1' and k.get('use')=='sig' and k.get('alg')=='RS256')
require(integer(k['n'])==int(p['n']) and integer(k['e'])==int(p['e']))
print('{"original_public_signing_key_in_live_jwks":true}')
"""
# A failed public readiness probe is closed data, not a failing Docker RPC.
# Transport/CLI nonzero remains uncertain; no deliberately failing exec is
# retried as if daemon acceptance had been established.
JWKS_TRANSPORT = ("import contextlib,io\ntry:\n"
" with contextlib.redirect_stdout(io.StringIO()):\n"
" exec(" + repr(JWKS_PROBE) + ")\n"
"except Exception:\n print('{\"original_public_signing_key_in_live_jwks\":false}')\n"
"else:\n print('{\"original_public_signing_key_in_live_jwks\":true}')\n")
def check_jwks(session, admission, public_numbers):
trust=runtime_file(session,admission['trust_path']).decode()
request={'issuer':admission['issuer'],'trust':trust,'n':str(public_numbers.n),'e':str(public_numbers.e)}
result=json.loads(command(['docker','exec','-i',session['worker'],'python3','-c',JWKS_TRANSPORT],input=json.dumps(request).encode()))
if result != {'original_public_signing_key_in_live_jwks': True}:raise Refused()
RUNTIME_FILES = r"""
import base64,json,sys
from pathlib import Path
p=json.load(sys.stdin)
roots=('/etc/keycape/','/var/lib/keycape-policy/','/etc/keycape-factor/','/recovery-tls/')
result={}
paths=[]
if p.get('tree'):
paths=list(Path('/var/lib/keycape-policy').rglob('*'))
else:paths=[Path(x) for x in p['paths']]
for f in paths:
if f.is_dir():continue
if not str(f).startswith(roots) or not f.is_file() or any(x.is_symlink() for x in (f,*f.parents)) or f.stat().st_size>8*1024*1024:raise ValueError()
result[str(f)]=base64.b64encode(f.read_bytes()).decode()
if len(result)>4096:raise ValueError()
print(json.dumps(result))
"""
def runtime_files(session, paths=(), tree=False):
raw=command(['docker','exec','-i',session['worker'],'python3','-c',RUNTIME_FILES],
input=json.dumps({'paths':list(paths),'tree':tree}).encode(),max_output=48*1024*1024)
return {k:base64.b64decode(v,validate=True) for k,v in json.loads(raw).items()}
def runtime_file(session,path):
return runtime_files(session,[path])[path]
def run_verifier(session, admission, payload):
issuer = session['issuer']
if {v['target'] for v in session.get('volumes',{}).values()}!={'/etc/keycape','/var/lib/keycape-policy','/etc/keycape-factor','/recovery-tls'}:raise Refused()
from urllib.parse import urlsplit
import ipaddress
address = urlsplit(admission['issuer'])
if (address.scheme != 'https' or not address.hostname or address.username or address.password
or address.query or address.fragment):
raise Refused()
if hashlib.sha256(container_file(issuer, '/keycape')).hexdigest() != admission['verifier_sha256']:
raise Refused()
if hashlib.sha256(runtime_file(session, admission['trust_path'])).hexdigest() != admission['trust_sha256']:
raise Refused()
# Capture private environment only in owner process memory, never receipt.
environment = json.loads(command(['docker','container','inspect',issuer,'--format','{{json .Config.Env}}']))
env = dict(item.split('=',1) for item in environment)
if len(env)!=len(environment):raise Refused()
if set(env)-set(payload['env'])-{'PATH','HOME','HOSTNAME','SSL_CERT_FILE'}:raise Refused()
if env.get('SSL_CERT_FILE') != admission['trust_path'] or any(k.lower().endswith('_proxy') and v for k,v in env.items()):
raise Refused()
if {k:v for k,v in env.items() if k.startswith('KEYCAPE_')}!={k:v for k,v in payload['env'].items() if k.startswith('KEYCAPE_')}:raise Refused()
for name,value in payload['env'].items():
if env.get(name) != value:
raise Refused()
files = {k:base64.b64decode(v,validate=True) for k,v in payload['files'].items()}
def equal():
if runtime_files(session,files)!=files:raise Refused()
durable=runtime_files(session,tree=True)
if durable!={k:v for k,v in files.items() if k.startswith('/var/lib/keycape-policy/')}:
raise Refused()
equal()
# Reading YAML is data parsing only; no archived executable is evaluated.
import yaml
cfg = yaml.safe_load(files['/etc/keycape/config.yaml'])
if cfg['issuer'] != admission['issuer']:
raise Refused()
consumer = admission['consumer']
client = next(x for x in cfg['clients'] if x['clientId']==consumer['client_id'])
if (client['secretRef']!='env:'+consumer['secret_env'] or client['clientType']!='confidential'
or 'client_credentials' not in client['grantTypes'] or not env.get(consumer['secret_env'])):
raise Refused()
from cryptography.hazmat.primitives import serialization
key=serialization.load_pem_private_key(files['/etc/keycape/key.pem'],password=None)
check_jwks(session,admission,key.public_key().public_numbers())
wrong = secrets.token_urlsafe(48)
if wrong == env[consumer['secret_env']]:raise Refused()
args = ['docker','exec','--env','NK49_UNRELATED_NEGATIVE_SECRET='+wrong,issuer,'/keycape','verify-client',
'-issuer',admission['issuer'],'-previous-secret-env','NK49_UNRELATED_NEGATIVE_SECRET']
flags = {'client_id':'client-id','secret_env':'secret-env','audience':'audience','scope':'scope',
'subject':'expect-subject','tenant':'expect-tenant','roles':'expect-roles','deny_scope':'deny-scope'}
for name,flag in flags.items():args+=['-'+flag,consumer[name]]
# Native command reports on stderr. Do not return or print its raw response.
result = process(args,stdout=subprocess.PIPE,stderr=subprocess.PIPE,timeout=150)
raw = (result.stdout+result.stderr).decode()
required = ['discovery and endpoint origin','exchange and JWKS signature for granted scopes','access-token claim bindings',
'principal_type is service','sub matches the registration','tenant matches the registration','roles present',
'granted scope carries no excess grant','predecessor secret refused']
required += ['excess scope refused: '+scope for scope in consumer['deny_scope'].split()]
if result.returncode or any('PASS '+name+'\n' not in raw for name in required) or 'FAIL ' in raw:
raise Refused()
equal()
from cryptography.hazmat.primitives import serialization
key=serialization.load_pem_private_key(files['/etc/keycape/key.pem'],password=None)
check_jwks(session,admission,key.public_key().public_numbers())
public=key.public_key().public_bytes(serialization.Encoding.DER,serialization.PublicFormat.SubjectPublicKeyInfo)
return {'native_service_registration_verified':True,'wrong_unrelated_secret_explicitly_refused':True,
'denied_scopes_refused':True,'original_config_registry_policy_receipts_equal':True,
'public_signing_spki_sha256':hashlib.sha256(public).hexdigest(),
'original_https_issuer_and_trust_preserved':True,'original_public_signing_key_in_live_jwks':True}
def cleanup(session, receipt=None):
"""Attempt every owned cleanup even when one obligation fails.
Preserve exact safe container names and outcomes so the owner can recover an
uncertain/partial cleanup. An inspect/daemon failure never means absence.
"""
obligations = []
failed = False
for name in reversed(session['containers']):
row = {'container': name, 'status': 'unverified'}
obligations.append(row)
try:
present=command(['docker','container','ls','--all','--filter','name=^/'+name+'$','--format','{{.Names}}']).decode().splitlines()
if not present:
row['status']='absence_verified'
continue
label = command(['docker', 'container', 'inspect', name, '--format',
'{{index .Config.Labels "' + LABEL + '"}}']).decode().strip()
if label != session['run_id']:
row['status'] = 'ownership_mismatch_not_removed'
failed = True
continue
# A failed remove is followed by a successful explicit inventory;
# interrupted remove may actually have completed.
try:
command(['docker', 'rm', '--force', '--volumes', name])
except Exception:
pass
remaining = command(['docker','container','ls','--all','--filter',
'name=^/' + name + '$','--format','{{.Names}}']).decode().splitlines()
if remaining:
row['status'] = 'still_present'
failed = True
else:
row['status'] = 'absence_verified'
except Exception:
failed = True
if receipt is not None:
receipt['container_cleanup_obligations'] = obligations
if failed:
raise Refused()
def native_checks(root, manifest, admission, receipt, cleanup_session=True):
"""Owner wrapper provides a reviewed session after existing restore pipeline.
Admission must bind ciphertext, canonical private manifest digest, executable
and trust bytes, exact image/target list, original issuer and selected consumer.
None of these admission fields grant authority or carry a client secret.
"""
session = admission['session']
# Validate ownership/containment before permitting either exec or cleanup.
inspect_session(session)
receipt.update(status='failed', source_sha256=hashlib.sha256(Path(__file__).read_bytes()).hexdigest(),
ciphertext_sha256=admission['ciphertext_sha256'], production_contacted=False,
human_mfa_proven=False, downstream_consumer_reconnect_proven=False,
container_cleanup_verified=False)
try:
for field in ('ciphertext_sha256', 'manifest_sha256', 'verifier_sha256', 'trust_sha256'):
if not re.fullmatch('[a-f0-9]{64}', admission[field]):
raise Refused()
for field in ('client_id','secret_env','audience','scope','subject','tenant','roles','deny_scope'):
if not isinstance(admission['consumer'].get(field), str) or not admission['consumer'][field].strip():
raise Refused()
payload = private_payload(root, manifest, admission['manifest_sha256'])
result = run_verifier(session, admission, payload)
required = {'native_service_registration_verified','wrong_unrelated_secret_explicitly_refused',
'denied_scopes_refused','original_config_registry_policy_receipts_equal',
'original_https_issuer_and_trust_preserved','original_public_signing_key_in_live_jwks'}
if any(result.get(k) is not True for k in required) or not re.fullmatch('[a-f0-9]{64}', result.get('public_signing_spki_sha256','')):
raise Refused()
# Closed receipt allowlist: never copy arbitrary worker output.
receipt.update({k: True for k in required})
receipt['public_signing_spki_sha256'] = result['public_signing_spki_sha256']
receipt['status'] = 'isolated_keycape_native_registration_checked'
finally:
try:
if cleanup_session:
cleanup(session, receipt)
receipt['container_cleanup_verified'] = True
except Exception:
receipt['status'] = 'failed'
raise

View file

@ -0,0 +1,343 @@
#!/usr/bin/env python3
"""Owner-run native LLDAP restore from the verified identity bundle.
Private inputs remain in the owner process/pipe. Decrypted staging uses Linux
tmpfs. Containers have no network, ports, host mounts or production API access.
"""
import argparse
import base64
from datetime import datetime, timezone
import hashlib
import importlib.util
import io
import json
import os
from pathlib import Path, PurePosixPath
import re
import signal
import sqlite3
import subprocess
import tarfile
import tempfile
import time
import tomllib
import uuid
HERE = Path(__file__).resolve().parent
def load(name):
spec = importlib.util.spec_from_file_location(name, HERE / (name + '.py'))
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
verify = load('verify-identity')
offline = load('retrieve-identity-offline')
bundle = verify.bundle
IMAGE = 'docker.io/lldap/lldap@sha256:9e605a66c02514bfcffd1b67cafb1e98d50992216bb2871d7ae44622047dd09d'
CLIENT = 'ghcr.io/gpappsoft/privacyidea-docker@sha256:af7841adad262f129e0c1d4f553af13f21cb2f4dc713533f316cfe43ed0b4473'
ENGINE_ID = '74f93783-6cef-43a8-a3c4-f62586cd3bc1'
ENGINE_NAME = 'bnt-lap001'
def command(args, **kwargs):
result = subprocess.run(args, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL,
timeout=kwargs.pop('timeout', 90), **kwargs)
if result.returncode:
raise bundle.Refused()
return result.stdout
def preflight():
contexts = json.loads(command(['docker', 'context', 'inspect']))
endpoints = [contexts[0]['Endpoints']['docker']['Host']]
if os.environ.get('DOCKER_HOST'):
endpoints.append(os.environ['DOCKER_HOST'])
if not all(url.startswith('unix://') for url in endpoints):
raise bundle.Refused()
facts = json.loads(command(['docker', 'info', '--format',
'{"ID":{{json .ID}},"Name":{{json .Name}},"OSType":{{json .OSType}}}']))
if facts['ID'] != ENGINE_ID or facts['Name'] != ENGINE_NAME or facts['OSType'] != 'linux':
raise bundle.Refused()
# /dev/shm must actually be tmpfs, not a similarly named disk directory.
mounts = Path('/proc/mounts').read_text().splitlines()
if not any(line.split()[1:3] == ['/dev/shm', 'tmpfs'] for line in mounts):
raise bundle.Refused()
images = []
for image in (IMAGE, CLIENT):
image_id = command(['docker', 'image', 'inspect', '--format', '{{.Id}}', image]).decode().strip()
images.append({'reference': image, 'image_id': image_id})
return {'engine_id': facts['ID'], 'engine_name': facts['Name'], 'images': images,
'production_contacted': False, 'network_mode': 'none', 'host_ports_published': False}
def snapshot(path):
# Include all fields of durable identity tables, including stored password
# hashes. Only counts and the equality boolean leave this process.
result = {}
with sqlite3.connect(path.as_uri() + '?mode=ro', uri=True) as db:
if db.execute('PRAGMA integrity_check').fetchall() != [('ok',)]:
raise bundle.Refused()
tables = {row[0] for row in db.execute("SELECT name FROM sqlite_master WHERE type='table'")}
for name in ('users', 'groups', 'memberships', 'user_attributes', 'group_attributes',
'user_attribute_values', 'group_attribute_values',
'user_attribute_schema', 'group_attribute_schema'):
if name in tables:
rows = db.execute('SELECT * FROM "' + name + '"').fetchall()
encoded = sorted(repr(row) for row in rows)
result[name] = {'count': len(rows), 'digest': hashlib.sha256('\n'.join(encoded).encode()).digest()}
if not {'users', 'groups', 'memberships'} <= result.keys():
raise bundle.Refused()
return result
def configuration(root):
data = root / 'data/lldap'
existing = data / 'lldap_config.toml'
config = tomllib.loads(existing.read_text()) if existing.is_file() else {}
deployment = bundle.resource(root, 'sso', 'deployment', 'lldap')
main = next(c for c in deployment['spec']['template']['spec']['containers'] if c['name'] == 'lldap')
env = {e['name']: e['value'] for e in main.get('env', []) if 'value' in e}
secret = bundle.resource(root, 'sso', 'secret', 'lldap-secrets')['data']
jwt = base64.b64decode(secret['LLDAP_JWT_SECRET'], validate=True)
password = base64.b64decode(secret['LLDAP_LDAP_USER_PASS'], validate=True)
if not jwt or not password or b'\n' in jwt or b'\n' in password:
raise bundle.Refused()
base = env.get('LLDAP_LDAP_BASE_DN', config.get('ldap_base_dn'))
user = env.get('LLDAP_LDAP_USER_DN', config.get('ldap_user_dn', 'admin'))
if not isinstance(base, str) or not base or not isinstance(user, str):
raise bundle.Refused()
chosen = {'database_url': 'sqlite:///data/users.db?mode=rwc',
'ldap_base_dn': base, 'ldap_user_dn': user,
'ldap_host': '127.0.0.1', 'http_host': '127.0.0.1',
'ldap_port': 3890, 'http_port': 17170,
'force_ldap_user_pass_reset': False, 'force_update_private_key': False}
seed = env.get('LLDAP_KEY_SEED', config.get('key_seed'))
if seed:
if not isinstance(seed, str):
raise bundle.Refused()
chosen['key_seed'] = seed
else:
value = env.get('LLDAP_KEY_FILE', config.get('key_file', '/data/private_key'))
key = PurePosixPath(value)
if not key.is_absolute():
key = PurePosixPath('/data') / key
if key.parent != PurePosixPath('/data') or not (data / key.name).is_file():
raise bundle.Refused()
chosen['key_file'] = str(key)
# JSON strings are valid TOML basic strings for these service inputs.
raw = '\n'.join(k + ' = ' + json.dumps(v) for k, v in chosen.items())
raw += '\n[smtp_options]\nenable_password_reset = false\n[ldaps_options]\nenabled = false\n'
return raw.encode(), jwt, password, base
def archive_files(files):
out = io.BytesIO()
with tarfile.open(fileobj=out, mode='w') as archive:
for name, value in files.items():
if PurePosixPath(name).is_absolute() or '..' in PurePosixPath(name).parts:
raise bundle.Refused()
entry = tarfile.TarInfo(name)
entry.size = len(value)
entry.mode = 0o600
entry.uid = entry.gid = 1000
archive.addfile(entry, io.BytesIO(value))
return out.getvalue()
PROBE = r'''
import contextlib,io,json,logging,socket,sys,time
logging.disable(logging.CRITICAL)
result={'status':'failed'}
stage='network_isolation'
try:
with contextlib.redirect_stdout(io.StringIO()),contextlib.redirect_stderr(io.StringIO()):
from pathlib import Path
assert {p.name for p in Path('/sys/class/net').iterdir()}=={'lo'}
assert len(Path('/proc/net/route').read_text().splitlines())==1
stage='native_client_import'
from ldap3 import Server,Connection,NONE
from ldap3.utils.dn import escape_rdn
values=json.load(sys.stdin)
stage='directory_startup'
for _ in range(60):
try:
with socket.create_connection(('127.0.0.1',3890),timeout=1):pass
break
except OSError:time.sleep(.25)
else:raise RuntimeError()
dn='uid='+escape_rdn(values['user'])+',ou=people,'+values['base']
server=Server('127.0.0.1',port=3890,get_info=NONE,connect_timeout=3)
stage='existing_user_bind'
connection=Connection(server,user=dn,password=values['password'],auto_bind=True,raise_exceptions=True)
assert connection.bound
connection.unbind()
stage='wrong_password_bind'
wrong=Connection(server,user=dn,password=values['wrong_password'],auto_bind=False,raise_exceptions=False)
refused=not wrong.bind() and wrong.result.get('result')==49
wrong.unbind()
assert refused
result={'status':'verified','existing_user_native_ldap_bind':True,'wrong_password_refused':True,
'only_loopback_interface_verified':True,'external_routes_absent':True}
except Exception as error:result={'status':'failed','failed_stage':stage,'failure_type':type(error).__name__}
print(json.dumps(result))
'''
def restore_lldap(root, user, password, receipt):
before = snapshot(root / 'data/lldap/users.db')
config, jwt, admin_password, base = configuration(root)
name = 'nk49-lldap-' + uuid.uuid4().hex
client_name = name + '-client'
try:
command(['docker', 'run', '--detach', '--pull=never', '--name', name,
'--network', 'none', '--read-only', '--cap-drop', 'ALL',
'--security-opt', 'no-new-privileges', '--user', '1000:1000',
'--memory', '256m', '--cpus', '1', '--pids-limit', '128',
'--tmpfs', '/data:rw,nosuid,nodev,noexec,size=268435456,uid=1000,gid=1000,mode=0700',
'--tmpfs', '/run/recovery:rw,nosuid,nodev,noexec,size=1048576,uid=1000,gid=1000,mode=0700',
'--tmpfs', '/tmp:rw,nosuid,nodev,noexec,size=16777216,uid=1000,gid=1000',
'--env', 'LLDAP_JWT_SECRET_FILE=/run/recovery/jwt',
'--env', 'LLDAP_LDAP_USER_PASS_FILE=/run/recovery/admin-password',
'--entrypoint', '/bin/sh', IMAGE, '-c', 'sleep 1800'])
guard = json.loads(command(['docker', 'container', 'inspect', name, '--format',
'{"host":{{json .HostConfig}},"mounts":{{json .Mounts}},"user":{{json .Config.User}}}']))
host = guard['host']
if (host['NetworkMode'] != 'none' or not host['ReadonlyRootfs'] or host['Privileged']
or host.get('Binds') or host.get('PortBindings') or 'ALL' not in host.get('CapDrop', [])
or 'no-new-privileges' not in host.get('SecurityOpt', [])
or guard['user'] != '1000:1000'
or any(mount['Type'] != 'tmpfs' for mount in guard['mounts'])):
raise bundle.Refused()
receipt['container_isolation_verified_before_private_import'] = True
payload = {p.relative_to(root / 'data/lldap').as_posix(): p.read_bytes()
for p in (root / 'data/lldap').rglob('*') if p.is_file()}
command(['docker', 'exec', '-i', name, 'tar', '-xf', '-', '-C', '/data'], input=archive_files(payload))
command(['docker', 'exec', '-i', name, 'tar', '-xf', '-', '-C', '/run/recovery'],
input=archive_files({'lldap.toml': config, 'jwt': jwt, 'admin-password': admin_password}))
# No credential values in Docker configuration, environment or argv.
command(['docker', 'exec', '--detach', name, '/bin/sh', '-c',
'echo $$ > /run/recovery/server.pid; exec /app/lldap run --config-file /run/recovery/lldap.toml > /dev/null 2>&1'])
raw = command(['docker', 'run', '--interactive', '--rm', '--name', client_name, '--pull=never', '--network', 'container:' + name,
'--read-only', '--cap-drop', 'ALL', '--security-opt', 'no-new-privileges',
'--memory', '256m', '--cpus', '1', '--pids-limit', '64',
'--entrypoint', 'python3', CLIENT, '-c', PROBE],
input=json.dumps({'user': user, 'password': password, 'base': base,
'wrong_password': 'nk49-wrong-' + uuid.uuid4().hex}).encode())
result = json.loads(raw)
receipt.update(result)
if result.get('status') != 'verified':
raise bundle.Refused()
command(['docker', 'exec', name, '/bin/sh', '-c', 'kill -TERM "$(cat /run/recovery/server.pid)"'])
for _ in range(60):
running = subprocess.run(['docker', 'exec', name, '/bin/sh', '-c',
'kill -0 "$(cat /run/recovery/server.pid)"'],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=10)
if running.returncode:
break
time.sleep(.1)
else:
raise bundle.Refused()
raw = command(['docker', 'exec', name, '/bin/sh', '-c',
'test ! -s /data/users.db-wal && cat /data/users.db'])
after_path = root / 'post-restore-users.db'
after_path.write_bytes(raw)
if snapshot(after_path) != before:
raise bundle.Refused()
receipt.update(directory_identity_rows_preserved=True,
directory_comparison_scope=sorted(before),
directory_table_counts={k: v['count'] for k, v in before.items()})
finally:
# A failed create can still have created a container (e.g. lost reply).
for owned in (client_name, name):
found = subprocess.run(['docker', 'container', 'inspect', owned], stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL, timeout=15)
if found.returncode == 0:
command(['docker', 'rm', '--force', '--volumes', owned])
absent = subprocess.run(['docker', 'container', 'inspect', owned], stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL, timeout=15)
command(['docker', 'info', '--format', '{{.ID}}'])
if absent.returncode == 0:
raise bundle.Refused()
receipt['container_cleanup_verified'] = True
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--transfer-receipt', required=True)
parser.add_argument('--receipt', required=True)
parser.add_argument('--user', required=True)
args = parser.parse_args()
os.umask(0o077)
fd = os.open(args.receipt, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o600)
result = {'schema': 'netkingdom.isolated-lldap-recovery.v1', 'status': 'failed',
'production_contacted': False, 'secret_values_exported': False,
'container_cleanup_verified': False, 'temporary_plaintext_cleanup_verified': False,
'source_sha256': hashlib.sha256(Path(__file__).read_bytes()).hexdigest()}
keyfd = None
work = None
stage = 'target_preflight'
try:
result['target'] = preflight()
stage = 'input_validation'
if not re.fullmatch(r'[A-Za-z0-9_.-]{1,128}', args.user):
raise bundle.Refused()
transfer = json.loads(Path(args.transfer_receipt).read_text())
offline.validate(transfer)
if transfer.get('independent_owner_retrieval_verified') is not True:
raise bundle.Refused()
path = Path(transfer['fetched_path'])
bundle.private(path, max_bytes=bundle.LIMIT)
if path.stat().st_size != transfer['ciphertext_bytes'] or offline.offsite.digest(path) != transfer['ciphertext_sha256']:
raise bundle.Refused()
keyfd = verify.custody.private_key_fd()
derived = command(['age-keygen', '-y', f'/proc/self/fd/{keyfd}'], pass_fds=(keyfd,)).strip()
if derived != bundle.profile()['public_recipient'].encode():
raise bundle.Refused()
stage = 'private_existing_password_prompt'
password = offline.private_prompt('Existing account password for the isolated LLDAP check: ')
with tempfile.TemporaryDirectory(prefix='nk49-service-recovery-', dir='/dev/shm') as temporary:
work = Path(temporary)
archive = work / 'archive.tar'
stage = 'archive_decryption'
with archive.open('xb') as output:
proc = subprocess.run(['age', '-d', '-i', f'/proc/self/fd/{keyfd}', str(path)],
stdout=output, stderr=subprocess.DEVNULL, pass_fds=(keyfd,), timeout=180)
if proc.returncode or archive.stat().st_size > bundle.LIMIT + 4 * 1024 * 1024:
raise bundle.Refused()
restored = work / 'restored'
restored.mkdir(mode=0o700)
stage = 'archive_validation'
manifest, _ = bundle.restore(archive, restored)
result.update(ciphertext_sha256=transfer['ciphertext_sha256'],
manifest_file_count=len(manifest['files']), all_manifest_hashes_verified=True)
stage = 'native_directory_check'
restore_lldap(restored, args.user, password, result)
password = None
if work.exists():
raise bundle.Refused()
result.update(status='isolated_lldap_verified', temporary_plaintext_cleanup_verified=True)
except (Exception, KeyboardInterrupt):
result.update(status='failed', error='isolated_lldap_check_failed_private_diagnostics_suppressed')
result.setdefault('failed_stage', stage)
finally:
if keyfd is not None:
os.close(keyfd)
if work is not None and not work.exists():
result['temporary_plaintext_cleanup_verified'] = True
result['finished_at'] = datetime.now(timezone.utc).isoformat()
with os.fdopen(fd, 'w') as output:
json.dump(result, output, indent=2)
output.write('\n')
success = result['status'] == 'isolated_lldap_verified'
print('OK: restored LLDAP existing-user bind, wrong-password refusal, directory preservation and cleanup passed.'
if success else 'ERROR: isolated LLDAP recovery refused; review the safe receipt. No production change.')
return 0 if success else 1
if __name__ == '__main__':
for sig in (signal.SIGINT, signal.SIGTERM, signal.SIGHUP):
signal.signal(sig, offline.interrupted)
raise SystemExit(main())

View file

@ -0,0 +1,309 @@
#!/usr/bin/env python3
"""Private native privacyIDEA token-material and audit recovery checks.
This does not authenticate a user's MFA code. It restores only a disposable
database, decrypts token material internally and verifies stored audit rows.
"""
import argparse
import ast
import base64
from datetime import datetime, timezone
import hashlib
import json
import os
from pathlib import Path
import signal
import subprocess
import tempfile
import time
import uuid
# Import the already reviewed local target/transport boundary without changing
# the accepted LLDAP executor or its source digest.
import importlib.util
HERE = Path(__file__).resolve().parent
spec = importlib.util.spec_from_file_location('isolated_lldap', HERE / 'isolated-lldap.py')
local = importlib.util.module_from_spec(spec)
spec.loader.exec_module(local)
bundle = local.bundle
IMAGE = local.CLIENT
POSTGRES = local.verify.pg.IMAGE
def run(args, **kwargs):
return local.command(args, **kwargs)
def preflight():
target = local.preflight()
target['images'].append({'reference': POSTGRES,
'image_id': run(['docker', 'image', 'inspect', '--format', '{{.Id}}', POSTGRES]).decode().strip()})
return target
def isolation(name, expected_network, user):
info = json.loads(run(['docker', 'container', 'inspect', name, '--format',
'{"host":{{json .HostConfig}},"mounts":{{json .Mounts}},"user":{{json .Config.User}}}']))
host = info['host']
if (host['NetworkMode'] != expected_network or not host['ReadonlyRootfs'] or host['Privileged']
or host.get('Binds') or host.get('PortBindings') or 'ALL' not in host.get('CapDrop', [])
or 'no-new-privileges' not in host.get('SecurityOpt', []) or info['user'] != user
or any(m['Type'] != 'tmpfs' for m in info['mounts'])):
raise bundle.Refused()
def cleanup(names):
for name in names:
present = subprocess.run(['docker', 'container', 'inspect', name],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=15)
if present.returncode == 0:
run(['docker', 'rm', '--force', '--volumes', name])
absent = subprocess.run(['docker', 'container', 'inspect', name],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=15)
run(['docker', 'info', '--format', '{{.ID}}'])
if absent.returncode == 0:
raise bundle.Refused()
def start_database(name):
run(['docker', 'run', '--detach', '--pull=never', '--name', name, '--network', 'none',
'--read-only', '--cap-drop', 'ALL', '--security-opt', 'no-new-privileges',
'--user', '999:999', '--memory', '2g', '--cpus', '2', '--pids-limit', '128',
'--tmpfs', '/var/lib/postgresql/data:rw,nosuid,nodev,noexec,size=1073741824,uid=999,gid=999,mode=0700',
'--tmpfs', '/var/run/postgresql:rw,nosuid,nodev,noexec,size=16777216,uid=999,gid=999',
'--tmpfs', '/tmp:rw,nosuid,nodev,noexec,size=16777216,uid=999,gid=999',
'--env', 'POSTGRES_HOST_AUTH_METHOD=trust', '--env', 'POSTGRES_DB=recovery',
'--entrypoint', '/usr/bin/timeout', POSTGRES, '--kill-after=15s', '1800s',
'/usr/local/bin/docker-entrypoint.sh', 'postgres', '-c', 'listen_addresses=127.0.0.1'])
isolation(name, 'none', '999:999')
for _ in range(80):
# The entrypoint's temporary init server has TCP disabled. Require TCP
# readiness so we cannot mistake it for the final local-only server.
ready = subprocess.run(['docker', 'exec', name, 'pg_isready', '-h', '127.0.0.1', '-U', 'postgres', '-d', 'recovery'],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=10)
if ready.returncode == 0:
return
time.sleep(.25)
raise bundle.Refused()
def start_worker(name, database, root):
secret = bundle.resource(root, 'mfa', 'secret', 'privacyidea-config')['data']
config = {'SQLALCHEMY_DATABASE_URI': 'postgresql+psycopg2://postgres@127.0.0.1:5432/recovery',
'PI_AUDIT_SQL_URI': 'postgresql+psycopg2://postgres@127.0.0.1:5432/recovery',
'SECRET_KEY': base64.b64decode(secret['PI_SECRET_KEY'], validate=True).decode(),
'PI_PEPPER': base64.b64decode(secret['PI_PEPPER'], validate=True).decode(),
'PI_ENCFILE': '/recovery/enckey', 'PI_AUDIT_KEY_PRIVATE': '/recovery/private.pem',
'PI_AUDIT_KEY_PUBLIC': '/recovery/public.pem', 'PI_AUDIT_NO_SIGN': False,
'PI_CHECK_OLD_SIGNATURES': legacy_setting(root), 'PI_NO_RESPONSE_SIGN': True,
'PI_LOGFILE': '/recovery/runtime.log', 'PI_LOGLEVEL': 50,
'PI_UUID_FILE': '/recovery/node-uuid',
'PI_TRUSTED_JWT': []}
# This deliberately does not execute archived pi.cfg or reuse its live DB URI.
payload = {'pi.cfg': '\n'.join(k + '=' + repr(v) for k, v in config.items()).encode()}
for key in ('enckey', 'private.pem', 'public.pem'):
payload[key] = (root / 'data/privacyidea' / key).read_bytes()
run(['docker', 'run', '--detach', '--pull=never', '--name', name,
'--network', 'container:' + database, '--read-only', '--cap-drop', 'ALL',
'--security-opt', 'no-new-privileges', '--user', '1000:1000',
'--memory', '1g', '--cpus', '2', '--pids-limit', '128',
'--tmpfs', '/recovery:rw,nosuid,nodev,noexec,size=33554432,uid=1000,gid=1000,mode=0700',
'--tmpfs', '/tmp:rw,nosuid,nodev,noexec,size=16777216,uid=1000,gid=1000',
'--tmpfs', '/privacyidea/etc/persistent:rw,nosuid,nodev,noexec,size=1048576,uid=1000,gid=1000,mode=0700',
'--env', 'PRIVACYIDEA_CONFIGFILE=/recovery/pi.cfg',
'--entrypoint', 'python3', IMAGE, '-c', 'import time;time.sleep(1800)'])
# Docker normalizes container:<name> to container:<full ID> in HostConfig.
database_id = run(['docker', 'container', 'inspect', database, '--format', '{{.Id}}']).decode().strip()
isolation(name, 'container:' + database_id, '1000:1000')
run(['docker', 'exec', '-i', name, 'tar', '-xf', '-', '-C', '/recovery'],
input=local.archive_files(payload))
def legacy_setting(root):
path = root / 'resources/mfa/configmap/privacyidea-cfg.json'
if not path.exists():
return False # Only the lower-level synthetic fixture omits ConfigMaps.
raw = bundle.resource(root, 'mfa', 'configmap', 'privacyidea-cfg')['data']['pi.cfg']
selected = False
for node in ast.parse(raw).body:
if isinstance(node, ast.Assign) and any(isinstance(t, ast.Name) and t.id == 'PI_CHECK_OLD_SIGNATURES' for t in node.targets):
if not isinstance(node.value, ast.Constant) or type(node.value.value) is not bool:
raise bundle.Refused()
selected = node.value.value
return selected
PROBE = r'''
import contextlib,io,json,logging,os,sys
logging.disable(logging.CRITICAL)
os.umask(0o077)
result={'status':'failed'}
stage='network_isolation'
try:
with contextlib.redirect_stdout(io.StringIO()),contextlib.redirect_stderr(io.StringIO()):
from pathlib import Path
assert {p.name for p in Path('/sys/class/net').iterdir()}=={'lo'}
assert len(Path('/proc/net/route').read_text().splitlines())==1
stage='native_application_configuration'
from privacyidea.app import create_app
app=create_app(config_name='testing',config_file='/recovery/pi.cfg',silent=True)
from privacyidea.models import db,Token
from privacyidea.lib.security.default import DefaultSecurityModule
from privacyidea.lib.auditmodules.sqlaudit import Audit,LogEntry
from privacyidea.lib.crypto import Sign
import binascii
with app.app_context():
assert str(db.engine.url)=='postgresql+psycopg2://postgres@127.0.0.1:5432/recovery'
stage='retained_token_decryption'
security=DefaultSecurityModule({'file':'/recovery/enckey'})
wrong_path=Path('/recovery/wrong-enckey');wrong_path.write_bytes(os.urandom(96))
wrong=DefaultSecurityModule({'file':str(wrong_path)})
total=Token.query.count()
assert 0<total<=10000
checked=0;negative=0
for token in Token.query.yield_per(100):
if not token.key_enc:continue
encrypted=binascii.unhexlify(token.key_enc);iv=binascii.unhexlify(token.key_iv)
material=security.decrypt(encrypted,iv)
assert isinstance(material,bytes) and 0<len(material)<=4096
try:
invalid=wrong.decrypt(encrypted,iv)
assert invalid!=material
except (ValueError,binascii.Error):pass
negative+=1;checked+=1
del material
assert checked>0
result.update(retained_tokens_total=total,retained_tokens_with_native_decryption=checked,
different_key_cannot_reproduce_material_count=negative,
retained_token_material_decrypted_inside_process=True,existing_token_mfa_proven=False)
stage='historical_audit_verification'
audit=Audit(app.config)
allow_legacy=bool(app.config['PI_CHECK_OLD_SIGNATURES'])
historical=audit.session.query(LogEntry).count()
assert 0<historical<=2000000
signed=0;unsigned=0;invalid=0;legacy=0
for entry in audit.session.query(LogEntry).yield_per(1000):
if not entry.signature:
unsigned+=1;continue
signed+=1
if not str(entry.signature).startswith(Sign.sig_ver+':'):legacy+=1
if not audit.sign_object.verify(audit._log_to_string(entry),entry.signature,allow_legacy):invalid+=1
result.update(historical_audit_rows=historical,historical_signed_rows=signed,
historical_unsigned_rows=unsigned,historical_invalid_signatures=invalid,
historical_legacy_signatures=legacy,all_stored_audit_rows_checked=True,
legacy_signature_verification_enabled=allow_legacy,
event_completeness_claimed=False)
stage='new_isolated_audit_entry'
audit.log({'action':'NK49 isolated recovery verification','success':True,
'info':'disposable restored database only','client':'127.0.0.1'})
audit.finalize_log()
fresh=Audit(app.config)
assert fresh.session.query(LogEntry).count()==historical+1
entry=fresh.session.query(LogEntry).order_by(LogEntry.id.desc()).first()
message=fresh._log_to_string(entry)
assert fresh.sign_object.verify(message,entry.signature,False)
assert not fresh.sign_object.verify(message+' tampered',entry.signature,False)
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.primitives import serialization
public=rsa.generate_private_key(public_exponent=65537,key_size=2048).public_key().public_bytes(
serialization.Encoding.PEM,serialization.PublicFormat.SubjectPublicKeyInfo)
assert not Sign(public_key=public).verify(message,entry.signature,False)
result.update(new_audit_entry_signed_verified=True,tampered_entry_refused=True,
wrong_audit_public_key_refused=True,
audit_signature_continuity_proven=signed==historical and invalid==0,
only_loopback_interface_verified=True,external_routes_absent=True,
status='native_checks_completed')
except Exception as error:
result.update(status='failed',failed_stage=stage,failure_type=type(error).__name__)
print(json.dumps(result))
'''
def native_checks(root, receipt):
stem = 'nk49-pi-check-' + uuid.uuid4().hex
database = stem + '-db'
worker = stem + '-worker'
try:
start_database(database)
with (root / 'databases/privacyidea.dump').open('rb') as dump:
run(['docker', 'exec', '-i', database, 'pg_restore', '-U', 'postgres', '-d', 'recovery',
'--no-owner', '--no-privileges', '--exit-on-error'], stdin=dump, timeout=600)
start_worker(worker, database, root)
receipt['container_isolation_verified_before_private_import'] = True
result = json.loads(run(['docker', 'exec', worker, 'python3', '-c', PROBE], timeout=600))
receipt.update(result)
if result.get('status') != 'native_checks_completed':
raise bundle.Refused()
finally:
cleanup([worker, database])
receipt['container_cleanup_verified'] = True
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--transfer-receipt', required=True)
parser.add_argument('--receipt', required=True)
args = parser.parse_args()
os.umask(0o077)
fd = os.open(args.receipt, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o600)
result = {'schema': 'netkingdom.isolated-privacyidea-recovery.v1', 'status': 'failed',
'production_contacted': False, 'secret_values_exported': False,
'existing_token_mfa_proven': False, 'audit_signature_continuity_proven': False,
'container_cleanup_verified': False, 'temporary_plaintext_cleanup_verified': False,
'source_sha256': hashlib.sha256(Path(__file__).read_bytes()).hexdigest()}
keyfd = None
work = None
stage = 'target_preflight'
try:
result['target'] = preflight()
stage = 'transfer_validation'
transfer = json.loads(Path(args.transfer_receipt).read_text())
local.offline.validate(transfer)
if transfer.get('independent_owner_retrieval_verified') is not True:
raise bundle.Refused()
path = Path(transfer['fetched_path'])
bundle.private(path, max_bytes=bundle.LIMIT)
if path.stat().st_size != transfer['ciphertext_bytes'] or local.offline.offsite.digest(path) != transfer['ciphertext_sha256']:
raise bundle.Refused()
keyfd = local.verify.custody.private_key_fd()
if run(['age-keygen', '-y', f'/proc/self/fd/{keyfd}'], pass_fds=(keyfd,)).strip() != bundle.profile()['public_recipient'].encode():
raise bundle.Refused()
with tempfile.TemporaryDirectory(prefix='nk49-pi-recovery-', dir='/dev/shm') as temporary:
work = Path(temporary)
archive = work / 'archive.tar'
stage = 'archive_decryption'
with archive.open('xb') as output:
p = subprocess.run(['age', '-d', '-i', f'/proc/self/fd/{keyfd}', str(path)],
stdout=output, stderr=subprocess.DEVNULL, pass_fds=(keyfd,), timeout=180)
if p.returncode or archive.stat().st_size > bundle.LIMIT + 4 * 1024 * 1024:
raise bundle.Refused()
restored = work / 'restored';restored.mkdir(mode=0o700)
stage = 'archive_validation'
manifest, _ = bundle.restore(archive, restored)
result.update(ciphertext_sha256=transfer['ciphertext_sha256'],
manifest_file_count=len(manifest['files']), all_manifest_hashes_verified=True)
stage = 'native_privacyidea_checks'
native_checks(restored, result)
result['status'] = 'isolated_privacyidea_checked'
except (Exception, KeyboardInterrupt):
result.update(status='failed', error='isolated_privacyidea_check_failed_private_diagnostics_suppressed')
result.setdefault('failed_stage', stage)
finally:
if keyfd is not None:os.close(keyfd)
if work is not None and not work.exists():result['temporary_plaintext_cleanup_verified'] = True
result['finished_at'] = datetime.now(timezone.utc).isoformat()
with os.fdopen(fd, 'w') as output:
json.dump(result, output, indent=2);output.write('\n')
if result['status'] == 'failed':
print('ERROR: isolated privacyIDEA check failed; review safe receipt. No production change.')
return 1
if not result['audit_signature_continuity_proven']:
print('REVIEW: token-material checks completed; historical audit exceptions require owner review. Cleanup passed; see safe receipt.')
return 2
print('OK: retained token material decrypted internally; historical/new audit signatures and negative checks passed; cleanup passed. Existing-token MFA remains separate.')
return 0
if __name__ == '__main__':
for sig in (signal.SIGINT, signal.SIGTERM, signal.SIGHUP):signal.signal(sig, local.offline.interrupted)
raise SystemExit(main())

View file

@ -0,0 +1,94 @@
#!/usr/bin/env python3
"""Prepare exact identity backup coverage from read-only workload/PVC metadata.
This produces a proposal, never reads Secrets, executes in pods, changes cluster
resources, captures private files, uploads, schedules or grants credentials.
"""
import argparse
from datetime import datetime,timezone
import hashlib
import json
import os
from pathlib import Path
import subprocess
ROOT=Path(__file__).resolve().parents[2]
CLUSTER_UID='a553c742-0115-43d4-99a4-a5ca56fe0786'
PROFILE=ROOT/'tools/bootstrap-recovery/identity-backup-profile.json'
class Refused(Exception):pass
def query(namespace,kind,name):
# Closed read-only types; Secret/ConfigMap data and arbitrary API paths have
# no transport in this preparation command.
if kind not in {'namespace','deployment','pvc'}:raise Refused()
args=['kubectl','--request-timeout=20s']
if namespace:args+=['-n',namespace]
r=subprocess.run(args+['get',kind,name,'-o','json'],capture_output=True,timeout=25)
if r.returncode:raise Refused()
return json.loads(r.stdout)
def references(spec):
secrets=set();configmaps=set();pvcs=set()
for c in spec.get('containers',[])+spec.get('initContainers',[]):
for e in c.get('env',[]):
v=e.get('valueFrom',{})
if 'secretKeyRef' in v:secrets.add(v['secretKeyRef']['name'])
if 'configMapKeyRef' in v:configmaps.add(v['configMapKeyRef']['name'])
for e in c.get('envFrom',[]):
if 'secretRef' in e:secrets.add(e['secretRef']['name'])
if 'configMapRef' in e:configmaps.add(e['configMapRef']['name'])
for v in spec.get('volumes',[]):
if 'secret' in v:secrets.add(v['secret']['secretName'])
if 'configMap' in v:configmaps.add(v['configMap']['name'])
if 'persistentVolumeClaim' in v:pvcs.add(v['persistentVolumeClaim']['claimName'])
for p in v.get('projected',{}).get('sources',[]):
if 'secret' in p:secrets.add(p['secret']['name'])
if 'configMap' in p:configmaps.add(p['configMap']['name'])
return secrets,configmaps,pvcs
def prepare(profile,get=query):
if get(None,'namespace','kube-system')['metadata']['uid']!=CLUSTER_UID:raise Refused()
bindings=[]
for component in profile['components']:
ns=component['namespace'];name=component['deployment']
d=get(ns,'deployment',name);m=d['metadata']
if (m['uid']!=component['deployment_uid'] or m.get('deletionTimestamp') or
d['status'].get('observedGeneration')!=m['generation'] or d['status'].get('readyReplicas')!=1):raise Refused()
refs=references(d['spec']['template']['spec'])
expected_pvcs={p['name']:p['uid'] for p in component['pvc_refs']}
if refs[0]!=set(component['secret_refs']) or refs[1]!=set(component['configmap_refs']) or refs[2]!=set(expected_pvcs):raise Refused()
pvcs=[]
for pvc,expected in expected_pvcs.items():
obj=get(ns,'pvc',pvc)
if obj['metadata']['uid']!=expected or obj['metadata'].get('deletionTimestamp') or obj['status'].get('phase')!='Bound':raise Refused()
pvcs.append({'name':pvc,'uid':expected})
bindings.append({'namespace':ns,'deployment':name,'uid':m['uid'],'generation':m['generation'],
'pvc_bindings':pvcs,'secret_names_only':sorted(refs[0]),'configmap_names_only':sorted(refs[1])})
if get(None,'namespace','kube-system')['metadata']['uid']!=CLUSTER_UID:raise Refused()
return {'schema':'netkingdom.identity-backup-preparation.v1','status':'prepared_not_admitted',
'observed_at':datetime.now(timezone.utc).isoformat(),'cluster_uid':CLUSTER_UID,
'profile_sha256':hashlib.sha256(PROFILE.read_bytes()).hexdigest(),
'source_sha256':hashlib.sha256(Path(__file__).read_bytes()).hexdigest(),
'bindings':bindings,'secret_values_read':False,'backup_captured':False,'uploaded':False,
'runtime_changed':False,'credential_grant_created':False,
'owner_admission_required':profile['admission_gates']}
def main():
p=argparse.ArgumentParser(description=__doc__);p.add_argument('--receipt',required=True)
a=p.parse_args()
try:
r=prepare(json.loads(PROFILE.read_text()))
fd=os.open(a.receipt,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')
except Exception:
print('ERROR: coverage preparation refused; private diagnostics suppressed.');return 1
print('OK: exact coverage proposal prepared; capture, delivery and owner admission remain pending.');return 0
if __name__=='__main__':raise SystemExit(main())

View file

@ -0,0 +1,176 @@
#!/usr/bin/env python3
"""Build a secret-free offline recovery kit; optionally publish exact new files.
Publication creates only three fixed versioned objects beside identity backups,
never changes shares, credentials, existing files, retention or runtime jobs.
"""
import argparse
import base64
from datetime import datetime, timezone
import hashlib
import importlib.util
import io
import json
import os
from pathlib import Path
import sys
import urllib.error
import urllib.request
import zipfile
ROOT = Path(__file__).resolve().parents[2]
HERE = Path(__file__).resolve().parent
spec = importlib.util.spec_from_file_location('offline', HERE / 'retrieve-identity-offline.py')
offline = importlib.util.module_from_spec(spec)
spec.loader.exec_module(offline)
offsite = offline.offsite
FILES = [
'docs/disaster-recovery-storage-share.md',
'docs/identity-service-restore-packet.md',
'docs/evidence/2026-10-16-identity-backup-live/actual-offsite.json',
'docs/evidence/2026-10-16-identity-outage-recovery/image-observation.json',
'tools/bootstrap-recovery/retrieve-identity-offline.py',
'tools/bootstrap-recovery/identity-offsite.py',
'tools/bootstrap-recovery/verify-identity.py',
'tools/bootstrap-recovery/identity-bundle.py',
'tools/bootstrap-recovery/check-privacyidea-database.py',
'tools/bootstrap-recovery/identity-backup-profile.json',
'sso-mfa/bootstrap/sops-custody-unlock.sh',
'sso-mfa/bootstrap/sops-private.py',
'keys/age.pub',
]
def build():
content = {name: (ROOT / name).read_bytes() for name in FILES}
if any(b'AGE-SECRET-KEY-1' in value for name, value in content.items()
if not name.endswith(('.py', '.sh', '.md'))):
raise offsite.Refused()
sums = ''.join(hashlib.sha256(value).hexdigest() + ' ' + name + '\n'
for name, value in content.items()).encode()
output = io.BytesIO()
with zipfile.ZipFile(output, 'w', compression=zipfile.ZIP_DEFLATED) as archive:
for name, value in {**content, 'SHA256SUMS': sums}.items():
entry = zipfile.ZipInfo(name, date_time=(2026, 10, 16, 0, 0, 0))
entry.external_attr = 0o100600 << 16
entry.compress_type = zipfile.ZIP_DEFLATED
archive.writestr(entry, value)
kit = output.getvalue()
version = hashlib.sha256(kit).hexdigest()
stem = 'net-kingdom-recovery-' + version[:16]
return {
stem + '-START-HERE.md': content['docs/disaster-recovery-storage-share.md'],
stem + '.zip': kit,
stem + '.zip.sha256': (version + ' ' + stem + '.zip\n').encode(),
}
def publish(files, headers, client=None):
client = client or offsite.opener()
result = []
for name, raw in files.items():
url = offline.ACCOUNT_BASE + name
digest = hashlib.sha256(raw).hexdigest()
# A retry may accept an identical object after exact readback, but must
# never overwrite a conflicting object or retry an unknown PUT outcome.
present = False
try:
with client.open(urllib.request.Request(url, method='HEAD', headers=headers), timeout=30) as response:
version = offsite.etag(response, url, len(raw))
present = True
except urllib.error.HTTPError as error:
if error.code != 404:
raise
if not present:
h = dict(headers, **{'If-None-Match': '*', 'Content-Length': str(len(raw)),
'Content-Type': 'application/zip' if name.endswith('.zip') else 'text/plain; charset=utf-8'})
with client.open(urllib.request.Request(url, data=raw, method='PUT', headers=h), timeout=60) as response:
if response.status != 201 or response.url != url:
raise offsite.Refused()
with client.open(urllib.request.Request(url, method='HEAD', headers=headers), timeout=30) as response:
version = offsite.etag(response, url, len(raw))
h = dict(headers, **{'If-Match': version})
with client.open(urllib.request.Request(url, headers=h), timeout=60) as response:
if offsite.etag(response, url, len(raw)) != version:
raise offsite.Refused()
downloaded = response.read(len(raw) + 1)
if downloaded != raw:
raise offsite.Refused()
result.append({'name': name, 'bytes': len(raw), 'sha256': digest,
'etag': version, 'readback_verified': True})
return result
def main():
p = argparse.ArgumentParser(description=__doc__)
p.add_argument('--output-dir', required=True)
p.add_argument('--publish', action='store_true')
p.add_argument('--auth-source', choices=('prompt', 'attended-openbao'), default='prompt')
p.add_argument('--receipt', required=True)
p.add_argument('--offline-transfer-receipt', help='After prompted publication, retrieve the known recovered generation using the same independently held access')
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 = {'schema': 'netkingdom.recovery-kit.v1', 'status': 'failed',
'secret_values_exported': False, 'new_grants_created': False}
stage = 'option_validation'
try:
if a.offline_transfer_receipt and (not a.publish or a.auth_source != 'prompt'):
raise offsite.Refused()
stage = 'build_kit'
files = build()
stage = 'write_local_kit'
directory = Path(a.output_dir)
directory.mkdir(mode=0o700, exist_ok=False)
for name, raw in files.items():
(directory / name).write_bytes(raw)
r.update(status='built', files=[{'name': n, 'bytes': len(v), 'sha256': hashlib.sha256(v).hexdigest()}
for n, v in files.items()])
if a.publish:
if a.auth_source == 'prompt':
stage = 'private_provider_prompt'
secret = offline.private_prompt('Independently held Storage Share Backup app password: ')
else:
stage = 'attended_owner_custody'
spec = importlib.util.spec_from_file_location('inventory', HERE / 'inventory-storage-share.py')
owner = importlib.util.module_from_spec(spec)
spec.loader.exec_module(owner)
_, credentials = owner.caller_credentials()
secret = credentials['BACKUP_PASSWORD']
if not secret or any(c in secret for c in '\r\n\x00'):
raise offsite.Refused()
try:
headers = {'Authorization': 'Basic ' + base64.b64encode(('Backup:' + secret).encode()).decode(),
'Accept-Encoding': 'identity'}
stage = 'provider_publication'
r.update(status='published_readback_verified', files=publish(files, headers),
folder='Backup/railiance-backups/net-kingdom-identity',
provider='Storage Share', credential_source=a.auth_source)
if a.offline_transfer_receipt:
stage = 'offline_provider_download'
transfer_fd = os.open(a.offline_transfer_receipt, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o600)
transfer = {'status': 'failed', 'error': 'offline_retrieval_failed_private_diagnostics_suppressed'}
try:
record = json.loads((ROOT / FILES[2]).read_text())
transfer = offline.retrieve(record, secret, 'account')
finally:
with os.fdopen(transfer_fd, 'w') as out:
json.dump(transfer, out, indent=2)
out.write('\n')
r['offline_retrieval_verified'] = transfer['status'] == 'readback_verified'
finally:
secret = None
r['finished_at'] = datetime.now(timezone.utc).isoformat()
except (Exception, KeyboardInterrupt) as error:
r.update(status='failed', error='kit_operation_failed_private_diagnostics_suppressed',
failed_stage=stage, **offline.failure_metadata(error))
finally:
with os.fdopen(fd, 'w') as output:
json.dump(r, output, indent=2)
output.write('\n')
# Silent: safe to use in the existing captured attended-owner conduit.
return int(r['status'] == 'failed')
if __name__ == '__main__':
sys.exit(main())

View file

@ -0,0 +1,164 @@
#!/usr/bin/env python3
"""Download one reviewed ciphertext using privately entered provider access.
No OpenBao, SSO, Kubernetes, credential discovery, provider writes or age key.
Run by the operator in their own terminal, never through a recorded agent PTY.
"""
import argparse
import base64
from datetime import datetime, timezone
import getpass
import hashlib
import importlib.util
import json
import os
from pathlib import Path
import re
import shutil
import signal
import sys
import tempfile
import urllib.request
import urllib.error
import warnings
HERE = Path(__file__).resolve().parent
spec = importlib.util.spec_from_file_location('offsite', HERE / 'identity-offsite.py')
offsite = importlib.util.module_from_spec(spec)
spec.loader.exec_module(offsite)
ACCOUNT_BASE = offsite.HOST + '/remote.php/dav/files/Backup/railiance-backups/net-kingdom-identity/'
def private_prompt(label):
# getpass opens the terminal with os.open, which does not require seeking.
# Reject redirected input and turn its echoing fallback warning into an
# exception before it can read a credential without echo suppression.
if not sys.stdin.isatty() or not sys.stderr.isatty():
raise offsite.Refused()
with warnings.catch_warnings():
warnings.simplefilter('error', getpass.GetPassWarning)
return getpass.getpass(label)
def failure_metadata(error):
# No exception messages, response bodies, URLs or headers enter receipts.
if isinstance(error, urllib.error.HTTPError):
return {'failure_kind': 'provider_http_error', 'http_status': int(error.code)}
if isinstance(error, urllib.error.URLError):
return {'failure_kind': 'provider_transport_error'}
if isinstance(error, getpass.GetPassWarning):
return {'failure_kind': 'terminal_echo_suppression_unavailable'}
if isinstance(error, offsite.Refused):
return {'failure_kind': 'scope_or_integrity_refusal'}
return {'failure_kind': 'local_operation_failed'}
def validate(record):
if (record.get('schema') != 'netkingdom.identity-offsite.v1'
or record.get('status') != 'readback_verified'
or record.get('provider') != 'Storage Share'
or record.get('folder') != 'Backup/railiance-backups/net-kingdom-identity'
or not offsite.NAME.fullmatch(record.get('artifact', ''))
or not re.fullmatch('[a-f0-9]{64}', record.get('ciphertext_sha256', ''))
or type(record.get('ciphertext_bytes')) is not int
or not 0 < record['ciphertext_bytes'] <= offsite.LIMIT
or not re.fullmatch(r'"[^"\x00-\x20]{1,180}"', record.get('etag', ''))):
raise offsite.Refused()
def retrieve(record, secret, mode, client=None):
validate(record)
if mode not in {'account', 'folder-share'} or not secret or any(c in secret for c in '\r\n\x00'):
raise offsite.Refused()
auth = offsite.headers(secret) if mode == 'folder-share' else {
'Authorization': 'Basic ' + base64.b64encode(('Backup:' + secret).encode()).decode(),
'Accept-Encoding': 'identity'}
url = (offsite.BASE if mode == 'folder-share' else ACCOUNT_BASE) + record['artifact']
client = client or offsite.opener()
directory = None
complete = False
try:
with client.open(urllib.request.Request(url, method='HEAD', headers=auth), timeout=30) as response:
if offsite.etag(response, url, record['ciphertext_bytes']) != record['etag']:
raise offsite.Refused()
directory = Path(tempfile.mkdtemp(prefix='nk49-identity-fetched-', dir='/tmp'))
target = directory / record['artifact']
auth['If-Match'] = record['etag']
count = 0
digest = hashlib.sha256()
with client.open(urllib.request.Request(url, headers=auth), timeout=180) as response:
if offsite.etag(response, url, record['ciphertext_bytes']) != record['etag']:
raise offsite.Refused()
fd = os.open(target, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o600)
with os.fdopen(fd, 'wb') as output:
while block := response.read(1024 * 1024):
count += len(block)
if count > record['ciphertext_bytes']:
raise offsite.Refused()
digest.update(block)
output.write(block)
output.flush()
os.fsync(output.fileno())
if count != record['ciphertext_bytes'] or digest.hexdigest() != record['ciphertext_sha256']:
raise offsite.Refused()
with client.open(urllib.request.Request(url, method='HEAD', headers=auth), timeout=30) as response:
if offsite.etag(response, url, record['ciphertext_bytes']) != record['etag']:
raise offsite.Refused()
complete = True
# Closed allowlist: never copy arbitrary source-receipt fields to output.
result = {k: record[k] for k in ('schema', 'provider', 'folder', 'artifact',
'ciphertext_bytes', 'ciphertext_sha256', 'etag')}
result.update(status='readback_verified', fetched_path=str(target),
independent_owner_retrieval_verified=True,
credential_source='independently_held_provider_access',
provider_auth_mode=mode, openbao_contacted=False, sso_contacted=False,
provider_writes_performed=False, age_private_key_used=False,
secret_values_exported=False, object_unchanged_during_transfer=True,
downloaded_at=datetime.now(timezone.utc).isoformat())
return result
finally:
if directory is not None and not complete:
shutil.rmtree(directory)
def interrupted(signum, frame):
raise InterruptedError()
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--source-receipt', required=True)
parser.add_argument('--receipt', required=True)
parser.add_argument('--auth', choices=('account', 'folder-share'), default='account')
args = parser.parse_args()
os.umask(0o077)
fd = os.open(args.receipt, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o600)
result = {'status': 'failed', 'error': 'offline_retrieval_failed_private_diagnostics_suppressed'}
stage = 'source_receipt_validation'
try:
record = json.loads(Path(args.source_receipt).read_text())
validate(record)
# getpass must never fall back to echoed stdin (e.g. a captured agent tool).
stage = 'private_provider_prompt'
secret = private_prompt('Storage Share Backup app password or exact folder-share token: ')
try:
stage = 'provider_download'
result = retrieve(record, secret, args.auth)
finally:
secret = None
except (Exception, KeyboardInterrupt) as error:
result.update(failed_stage=stage, **failure_metadata(error))
finally:
with os.fdopen(fd, 'w') as output:
json.dump(result, output, indent=2)
output.write('\n')
ok = result['status'] == 'readback_verified'
print('OK: ciphertext retrieved without OpenBao or SSO; ETag, size and SHA256 verified.' if ok
else 'ERROR: retrieval refused; private diagnostics suppressed. Use a new receipt path to retry.')
return 0 if ok else 1
if __name__ == '__main__':
for sig in (signal.SIGINT, signal.SIGTERM, signal.SIGHUP):
signal.signal(sig, interrupted)
sys.exit(main())

View file

@ -0,0 +1,71 @@
#!/usr/bin/env python3
"""Silent attended owner retrieval of an existing identity ciphertext.
Uses Backup-account recovery custody only in memory; no private age key, upload,
share, grant, deletion or production restore operation.
"""
import argparse
import base64
from datetime import datetime,timezone
import hashlib
import importlib.util
import json
import os
from pathlib import Path
import re
import tempfile
import urllib.request
HERE=Path(__file__).resolve().parent
def load(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
inventory=load('inventory-storage-share');offsite=load('identity-offsite')
def retrieve(record,out):
if (record.get('schema')!='netkingdom.identity-offsite.v1' or record.get('status')!='readback_verified'
or not offsite.NAME.fullmatch(record.get('artifact','')) or not re.fullmatch('[a-f0-9]{64}',record.get('ciphertext_sha256',''))
or not 0<record.get('ciphertext_bytes',0)<=offsite.LIMIT):raise offsite.Refused()
identity,credentials=inventory.caller_credentials()
auth=base64.b64encode((credentials['BACKUP_USERNAME']+':'+credentials['BACKUP_PASSWORD']).encode()).decode()
url=offsite.HOST+'/remote.php/dav/files/Backup/railiance-backups/net-kingdom-identity/'+record['artifact']
h={'Authorization':'Basic '+auth,'Accept-Encoding':'identity'}
client=offsite.opener()
with client.open(urllib.request.Request(url,method='HEAD',headers=h),timeout=30) as response:
version=offsite.etag(response,url,record['ciphertext_bytes'])
if version!=record['etag']:raise offsite.Refused()
h['If-Match']=version
directory=Path(tempfile.mkdtemp(prefix='nk49-identity-fetched-',dir='/tmp'))
target=directory/record['artifact'];count=0;digest=hashlib.sha256()
with client.open(urllib.request.Request(url,headers=h),timeout=180) as response:
if offsite.etag(response,url,record['ciphertext_bytes'])!=version:raise offsite.Refused()
with target.open('xb') as output:
while block:=response.read(1024*1024):
count+=len(block)
if count>record['ciphertext_bytes']:raise offsite.Refused()
digest.update(block);output.write(block)
if count!=record['ciphertext_bytes'] or digest.hexdigest()!=record['ciphertext_sha256']:raise offsite.Refused()
with client.open(urllib.request.Request(url,method='HEAD',headers=h),timeout=30) as response:
if offsite.etag(response,url,record['ciphertext_bytes'])!=version:raise offsite.Refused()
out.update(record);out.update(status='readback_verified',fetched_path=str(target),
independent_owner_retrieval_verified=True,caller_entity_id=identity.get('entity_id'),
age_private_key_used=False,secret_values_exported=False,downloaded_at=datetime.now(timezone.utc).isoformat())
def main():
p=argparse.ArgumentParser(description=__doc__);p.add_argument('--source-receipt',required=True);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','source_sha256':hashlib.sha256(Path(__file__).read_bytes()).hexdigest()}
try:retrieve(json.loads(Path(a.source_receipt).read_text()),r)
except Exception:r.update(status='failed',error='contained_owner_retrieval_failed')
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())

View file

@ -0,0 +1,40 @@
#!/usr/bin/env bash
# Candidate owner launcher. Review and native fixture proof required before use.
set +x
set -euo pipefail
ROOT="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)"
cd -- "$ROOT"
umask 077
[[ $# == 6 ]] || { printf '%s\n' 'Usage: run-isolated-keycape.sh VERIFIED_TRANSFER_RECEIPT ADMISSION ADMISSION_SHA256 TLS_CERT TLS_KEY TRUST'; exit 1; }
[[ "$3" =~ ^[a-f0-9]{64}$ ]] || { printf '%s\n' 'ERROR: exact reviewed admission digest required.'; exit 1; }
for index in 1 2 4 5 6; do
[[ -f "${!index}" && ! -L "${!index}" ]] || { printf '%s\n' 'ERROR: provide regular owner-controlled input files.'; exit 1; }
done
[[ -t 0 && -t 1 && -t 2 ]] || { printf '%s\n' 'ERROR: run in your own interactive terminal.'; exit 1; }
for tool in python3 age age-keygen docker; do
command -v "$tool" >/dev/null || { printf 'ERROR: required tool unavailable: %s\n' "$tool"; exit 1; }
done
python3 - <<'PY'
import importlib.util
from pathlib import Path
p=Path('tools/bootstrap-recovery/isolated-keycape-session.py')
spec=importlib.util.spec_from_file_location('session',p)
m=importlib.util.module_from_spec(spec);spec.loader.exec_module(m)
try:
m.local.preflight()
m.run(['docker','image','inspect',m.adapter.IMAGE])
except Exception:
raise SystemExit('ERROR: exact local Docker/images/tmpfs preflight failed; no key requested.')
print('OK: fixed local target and cached pinned images available.')
PY
NK49_KC_RUN="$(mktemp -d /tmp/nk49-isolated-keycape-XXXXXXXX)"
printf 'Safe evidence directory: %s\n' "$NK49_KC_RUN"
printf '%s\n' 'Owner-reviewed candidate: isolated original issuer/trust and current registered service-client checks only.'
printf '%s\n' 'No browser, downstream consumer or retained-factor MFA acceptance is claimed.'
printf '%s\n' 'Enter only the independently held backup age key at the hidden prompt.'
bash sso-mfa/bootstrap/sops-custody-unlock.sh \
--expected-recipient age1zvryunvjhvpkmasskauga2heeg0ztnte9ymgppvjge36ekumk50syr3tsz \
--prompt -- python3 tools/bootstrap-recovery/isolated-keycape-session.py \
--transfer-receipt "$1" --admission "$2" --admission-sha256 "$3" \
--tls-cert "$4" --tls-key "$5" --trust "$6" --receipt "$NK49_KC_RUN/recovery.json"
printf 'Safe receipt: %s/recovery.json\n' "$NK49_KC_RUN"

View file

@ -0,0 +1,139 @@
#!/usr/bin/env python3
"""Isolated ciphertext recovery check; no cluster, credentials or values emitted.
This verifies the handoff transport and exact requested file coverage. Current
service validity, independent source custody and native consumer recovery need
separate owner evidence; this tool cannot infer them from a decrypted archive.
"""
import argparse
import hashlib
import importlib.util
import io
import json
import os
from pathlib import Path, PurePosixPath
import re
import signal
import subprocess
import sys
import tarfile
import tempfile
from datetime import datetime, timezone
ROOT = Path(__file__).resolve().parents[2]
spec = importlib.util.spec_from_file_location('private_age_recovery', ROOT / 'sso-mfa/bootstrap/sops-private.py')
custody = importlib.util.module_from_spec(spec); spec.loader.exec_module(custody)
LIMIT = 64 * 1024 * 1024
ALLOWED = {f'{c}/secrets.env' for c in ('privacyidea', 'postgres', 'lldap', 'authelia', 'keycape', 'breakglass')} | {
'privacyidea/pi.enc', 'privacyidea/private.pem', 'privacyidea/public.pem',
'keycape/key.pem', 'authelia/oidc_private_key.pem', 'openbao/init.json'}
class Refused(Exception):
pass
def recover(ciphertext, keyfd, required, work):
# Exclude ambient age keys/plugins and unrelated credential environments.
env = {'PATH': os.environ.get('PATH', os.defpath), 'HOME': str(work), 'XDG_CONFIG_HOME': str(work)}
result = subprocess.run(['age', '-d', '-i', f'/proc/self/fd/{keyfd}'], input=ciphertext,
stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, pass_fds=(keyfd,),
timeout=60, env=env, cwd=work)
if result.returncode or not result.stdout or len(result.stdout) > LIMIT:
raise Refused()
restored = set(); names = set(); total = 0
with tarfile.open(fileobj=io.BytesIO(result.stdout), mode='r:') as archive:
for index, member in enumerate(archive):
if index >= 128 or member.name in names:
raise Refused()
names.add(member.name)
path = PurePosixPath(member.name)
if (path.is_absolute() or '..' in path.parts or '.' in path.parts
or not path.parts or path.parts[0] != 'secrets'):
raise Refused()
if member.isdir():
if len(path.parts) > 2 or (len(path.parts) == 2 and path.parts[1] not in {p.split('/')[0] for p in ALLOWED}):
raise Refused()
continue
relative = '/'.join(path.parts[1:])
if not member.isfile() or relative not in ALLOWED or not 0 <= member.size <= 16 * 1024 * 1024:
raise Refused()
total += member.size
if total > LIMIT or member.size == 0:
raise Refused()
content = archive.extractfile(member).read()
if len(content) != member.size or b'AGE-SECRET-KEY-' in content:
raise Refused()
target = work / 'restored' / relative
target.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
fd = os.open(target, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o600)
with os.fdopen(fd, 'wb') as dest:
dest.write(content); dest.flush(); os.fsync(dest.fileno())
if target.read_bytes() != content:
raise Refused()
restored.add(relative)
if not required.issubset(restored):
raise Refused()
return sorted(restored)
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--bundle', required=True)
parser.add_argument('--bundle-sha256', required=True)
parser.add_argument('--required-file', action='append', required=True)
parser.add_argument('--receipt', required=True)
args = parser.parse_args()
if not re.fullmatch('[a-f0-9]{64}', args.bundle_sha256): raise Refused()
required = set(args.required_file)
if not required or required - ALLOWED or len(required) != len(args.required_file): raise Refused()
path = Path(args.bundle).absolute()
if any(p.is_symlink() for p in (path, *path.parents)): raise Refused()
with path.open('rb') as file:
data = file.read(LIMIT + 1)
if len(data) > LIMIT or hashlib.sha256(data).hexdigest() != args.bundle_sha256: raise Refused()
recipient = custody.public_recipient()
keyfd = custody.private_key_fd()
try:
derived = subprocess.run(['age-keygen', '-y', f'/proc/self/fd/{keyfd}'],
pass_fds=(keyfd,), stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, timeout=10)
if derived.returncode or derived.stdout.strip() != recipient.encode(): raise Refused()
# Check the negative path with a disposable unrelated key. It never
# replaces the registered key, leaves temporary custody, or contacts IAM.
with tempfile.TemporaryDirectory(prefix='netkingdom-recovery-', dir='/tmp') as temporary:
work = Path(temporary)
disposable = work / 'wrong.key'
created = subprocess.run(['age-keygen', '-o', str(disposable)], stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL, timeout=10)
if created.returncode: raise Refused()
wrong = subprocess.run(['age', '-d', '-i', str(disposable)], input=data,
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=60,
env={'PATH': os.environ.get('PATH', os.defpath), 'HOME': temporary,
'XDG_CONFIG_HOME': temporary}, cwd=temporary)
if wrong.returncode == 0: raise Refused()
restored = recover(data, keyfd, required, work)
if work.exists(): raise Refused()
finally:
os.close(keyfd)
receipt = {'schema': 'netkingdom.bootstrap-recovery-transport.v1',
'observed_at': datetime.now(timezone.utc).isoformat(), 'bundle_sha256': args.bundle_sha256,
'registered_recipient_match': True, 'wrong_key_refused': True,
'required_files': sorted(required), 'restored_files': restored,
'isolated_restore_readback_passed': True, 'temporary_plaintext_cleanup_verified': True,
'secret_values_exported': False, 'cluster_contacted': False,
'independent_custody_proven': False, 'current_service_validity_proven': False,
'native_consumer_recovery_proven': False}
# Publish only the closed, safe receipt shape; preserve existing receipts.
fd = os.open(args.receipt, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o600)
with os.fdopen(fd, 'w') as dest:
json.dump(receipt, dest, indent=2); dest.write('\n')
print('Ciphertext transport/isolated restore checks passed. Independent custody and current service recovery remain separate owner evidence.')
if __name__ == '__main__':
try:
for sig in (signal.SIGHUP, signal.SIGTERM, signal.SIGINT): signal.signal(sig, custody.interrupted)
main()
except (Refused, custody.Refused, OSError, ValueError, TypeError, KeyError, tarfile.TarError, subprocess.SubprocessError):
print('ERROR: recovery transport check refused or failed; no acceptance receipt. Private diagnostics suppressed.', file=sys.stderr)
raise SystemExit(1)