feat(recovery): prepare full identity backup and bootstrap handoff
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 2s

Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a13eab-1e6a-7b33-9c37-372e90ada3a2
This commit is contained in:
tegwick 2026-10-16 01:37:11 +02:00
parent dca999222f
commit 994d11c869
29 changed files with 3277 additions and 29 deletions

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

@ -1,6 +1,6 @@
{
"schema": "netkingdom.identity-backup-profile.v1",
"status": "proposed",
"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",
@ -133,20 +133,21 @@
"historical_database": "existing verified October15 dump remains protected; new bundle must link its own exact database/key capture generation and recovery receipt"
},
"proposed_destinations": {
"primary": "s3://railiance-platform-pg-backup/platform-pg/application-archives/net-kingdom/",
"secondary": "Storage Share Backup/railiance-backups/net-kingdom-identity/",
"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": "new exact prefix-scoped owner-delivered publisher; do not borrow another workload reader or bootstrap API key",
"retention": "initial single verified generation; later seven daily/two weekly only after measured quota/overlap/headroom and separately admitted retention executor; never expire existing good backups",
"schedule": "not installed; propose daily capture after logical database backup with per-generation linkage, separate failure alert and operator-approved RPO/RTO"
"publisher": "CCR-2026-0048: new folder-only read/create public DAV share, delivered by exact ESO reader; no operator password in runtime",
"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",
"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": [
"Railiance owner accepts exact read/capture executor, immutable helper images, PVC access and task-scoped caller attribution; no production stop/restart inferred",
"NetKingdom/service owners accept full asset manifest, original key preservation, consistency interval, bounded recovery environment and native acceptance",
"Platform/reef-storage admits both new archive prefixes, immutable create/readback, scoped publisher custody and current quota/headroom; current application archive profiles do not include net-kingdom",
"Resolve every referenced credential through encrypted recovery or an independent owner reissue path; no undocumented grant propagation",
"Implement and test capture/sealing/transfer adapters against this new profile before live execution; existing bootstrap tar checker is insufficient",
"Retrieve by immutable object/version from each admitted provider; digest check, attended independent-key decryption, native service recovery, failed-key case and verified cleanup",
"Only then enable recurring capture/alerts and measured retention; preserve T02 legacy provider-credential and deferred-rotation boundaries"
"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"
]
}

View file

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

View file

@ -0,0 +1,234 @@
#!/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,5),'retention':(49,9)}
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):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:
status,_=request(client,owner,auth,'PUT',payload.stdout,{'If-None-Match':'*'})
if status!=201:raise Refused()
created=True
status,raw=request(client,public,(token,''))
if status!=200 or raw!=payload.stdout:raise Refused()
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()
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])
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.
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
status,_=request(client,offsite.BASE+'%2e%2e/r01-net-kingdom-pg/',(token,''))
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)
# 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()
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()
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()
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,208 @@
#!/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)
if (es['spec']['target']['name']!=name or es['spec']['data']!=[{'secretKey':'token','remoteRef':{
'key':'workloads/net-kingdom/identity-backup/'+action,'property':'NC_WEBDAV_TOKEN'}}]
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:
if os.geteuid()!=0 or socket.gethostname()!='railiance01':raise offsite.Refused()
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():
if os.geteuid()!=0 or socket.gethostname()!='railiance01':raise Refused()
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,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,83 @@
#!/usr/bin/env python3
"""Attended native reader/ESO verification; only closed booleans leave custody."""
import argparse
import hashlib
import importlib.util
import json
import os
from pathlib import Path
import sys
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
setup=load('identity-credential-setup');runtime=load('identity-runtime')
from state_hub_preflight_lane import bao,data,capabilities,revoke
def verify(action,expected,receipt,kubeconfig):
ccr,lane=setup.contract(action,expected)
ident=data(bao(['token','lookup','-format=json']))['data']
if 'platform-admin' not in ident.get('policies',[]) or 'root' in ident.get('policies',[]) or not ident.get('path','').startswith('auth/netkingdom/'):raise setup.Refused()
capture=runtime.capture;capture.KUBE=['kubectl','--kubeconfig',kubeconfig]
if capture.get(None,'namespace','kube-system')['metadata']['uid']!=capture.prepare.CLUSTER_UID:raise setup.Refused()
def login(sa,audience,negative=False,namespace='sso'):
jwt=capture.command(['-n',namespace,'create','token',sa,'--audience='+audience,'--duration=10m']).decode().strip()
result=bao(['write','-format=json','auth/kubernetes/login','-'],payload={'role':lane['role'],'jwt':jwt},allow_failure=negative)
if negative:
if result.returncode==0:
revoke(data(result)['auth']['client_token']);raise setup.Refused()
if b'403' not in result.stderr and b'400' not in result.stderr:raise setup.Refused()
return None
return data(result)['auth']
auth=login(lane['service_account'],'openbao');token=auth['client_token']
try:
if (auth['token_policies']!=[lane['policy']] or auth.get('identity_policies')
or auth.get('policies',auth['token_policies'])!=[lane['policy']] or not 0<auth['lease_duration']<=900):raise setup.Refused()
paths=[lane['kv_path'],lane['kv_path'].replace('/data/','/metadata/',1),lane['kv_path']+'-sibling',
'platform/data/workloads/railiance/backup/offsite-lane','auth/token/create']
cap=capabilities(token,paths)
if cap[paths[0]]!=['read'] or any(cap[p]!=['deny'] for p in paths[1:]):raise setup.Refused()
native=data(bao(['read','-format=json',lane['kv_path']],token=token))['data']
if set(native['data'])!={'NC_WEBDAV_TOKEN'} or native['data']['NC_WEBDAV_TOKEN']!=runtime.token(action):raise setup.Refused()
receipt['kv_version']=native['metadata']['version']
finally:revoke(token)
login('default','openbao',True);login(lane['service_account'],'wrong-audience',True)
proposed={'apiVersion':'v1','kind':'ServiceAccount','metadata':{'name':lane['service_account'],'namespace':'default'},'automountServiceAccountToken':False}
receipt['wrong_namespace_probe_attempted']=proposed['metadata']
test=json.loads(capture.command(['create','-f','-','-o','json'],json.dumps(proposed).encode()))
if test['metadata']['name']!=lane['service_account'] or test['metadata']['namespace']!='default':raise setup.Refused()
try:login(lane['service_account'],'openbao',True,'default')
finally:
path='/api/v1/namespaces/default/serviceaccounts/'+lane['service_account']
capture.command(['delete','--raw',path,'-f','-'],json.dumps({'apiVersion':'v1','kind':'DeleteOptions',
'preconditions':{'uid':test['metadata']['uid']}}).encode())
receipt['wrong_namespace_probe_cleanup_verified']=True
row=data(bao(['read','-format=json','auth/kubernetes/role/'+lane['role']]))['data']
setup.check_role(row,lane)
receipt.update(status='reader_and_delivery_verified',exact_namespace_binding_verified=True,
exact_policy_and_ttl_verified=True,wrong_service_account_denied=True,wrong_audience_denied=True,
wrong_namespace_denied=True,
metadata_sibling_escrow_and_token_create_denied=True,kv_write_denied=True,
eso_delivery_equality_verified=True,temporary_reader_revoked=True,secret_values_exported=False,
source_ccr=ccr['id'],ccr_sha256=expected)
def main():
p=argparse.ArgumentParser(description=__doc__);p.add_argument('action',choices=setup.PROFILES)
p.add_argument('--ccr-sha256',required=True);p.add_argument('--kubeconfig',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','source_sha256':hashlib.sha256(Path(__file__).read_bytes()).hexdigest()}
try:verify(a.action,a.ccr_sha256,r,a.kubeconfig)
except Exception:r.update(status='failed',error='native_delivery_verification_failed_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,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())