Record archive recovery lifecycle and validate receipt provenance
Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a06ecb-456a-71c2-b41e-0755d336e883
This commit is contained in:
parent
445f1361dc
commit
a867ec269a
9 changed files with 203 additions and 11 deletions
|
|
@ -1,6 +1,7 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Silent attended decryption of the hash-verified primary archive download."""
|
||||
import argparse
|
||||
from datetime import datetime, timezone
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
|
|
@ -16,12 +17,15 @@ def digest(path):
|
|||
def main():
|
||||
p=argparse.ArgumentParser(description=__doc__)
|
||||
for name in ['source','output','transfer-receipt','receipt']: p.add_argument('--'+name,required=True,type=Path)
|
||||
a=p.parse_args();receipt={'schema':'platform.forgejo-primary-decryption.v1','status':'failed'}
|
||||
a=p.parse_args();receipt={'schema':'platform.forgejo-primary-decryption.v1','status':'failed','started_at':datetime.now(timezone.utc).isoformat()}
|
||||
fd=os.open(a.receipt,os.O_WRONLY|os.O_CREAT|os.O_EXCL,0o600)
|
||||
try:
|
||||
transfer=json.loads(a.transfer_receipt.read_text())
|
||||
if (transfer.get('status')!='primary_fetched_pending_application_restore'
|
||||
or not transfer.get('download_hash_matches')
|
||||
raw=a.transfer_receipt.read_bytes()
|
||||
transfer=json.loads(raw)
|
||||
receipt['transfer_receipt_sha256']=hashlib.sha256(raw).hexdigest()
|
||||
if (transfer.get('schema')!='platform.forgejo-primary-archive.v1'
|
||||
or transfer.get('status')!='primary_fetched_pending_application_restore'
|
||||
or transfer.get('download_hash_matches') is not True
|
||||
or digest(a.source)!=transfer.get('ciphertext_sha256')):
|
||||
raise ValueError('verified_primary_download_required')
|
||||
value=data(bao(['read','-format=json','platform/data/workloads/railiance/backup/offsite-lane']))['data']['data']['AGE_PRIVATE_KEY']
|
||||
|
|
@ -29,10 +33,15 @@ def main():
|
|||
a.output.chmod(0o600)
|
||||
result=subprocess.run(['age','-d','-i','/dev/stdin',str(a.source)],input=(value.strip()+'\n').encode(),stdout=target,stderr=subprocess.PIPE,timeout=1200)
|
||||
if result.returncode: raise ValueError('primary_decryption_failed')
|
||||
receipt.update(transfer,decrypted=True,plaintext_sha256=digest(a.output))
|
||||
# Preserve this operation's schema and times, not the transfer's.
|
||||
for key in ('destination','ciphertext_sha256','ciphertext_bytes','download_hash_matches','archive_profile'):
|
||||
if key in transfer: receipt[key]=transfer[key]
|
||||
receipt.update(status='primary_fetched_pending_application_restore',
|
||||
decrypted=True,plaintext_sha256=digest(a.output))
|
||||
except Exception:
|
||||
receipt['error']='bounded_primary_decryption_failed'
|
||||
finally:
|
||||
receipt['finished_at']=datetime.now(timezone.utc).isoformat()
|
||||
with os.fdopen(fd,'w') as output: json.dump(receipt,output,indent=2)
|
||||
return int(receipt['status']=='failed')
|
||||
if __name__=='__main__': raise SystemExit(main())
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Restore a fetched Forgejo archive on a disposable, internal Docker network."""
|
||||
import argparse
|
||||
from datetime import datetime, timezone
|
||||
import configparser
|
||||
import hashlib
|
||||
import json
|
||||
|
|
@ -189,13 +190,19 @@ def main():
|
|||
p.add_argument('--archive',required=True,type=Path);p.add_argument('--receipt',required=True)
|
||||
p.add_argument('--profile',choices=['full','essentials'],default='full')
|
||||
p.add_argument('--transfer-receipt',required=True,type=Path)
|
||||
a=p.parse_args();receipt={'schema':'platform.forgejo-isolated-restore.v1','status':'failed','images':[FORGE,POSTGRES]}
|
||||
a=p.parse_args();receipt={'schema':'platform.forgejo-isolated-restore.v1','status':'failed','started_at':datetime.now(timezone.utc).isoformat(),'images':[FORGE,POSTGRES]}
|
||||
fd=os.open(a.receipt,os.O_WRONLY|os.O_CREAT|os.O_EXCL,0o600)
|
||||
try:
|
||||
transfer=json.loads(a.transfer_receipt.read_text())
|
||||
nextcloud = (transfer.get('status') == 'offsite_fetched_pending_isolated_restore'
|
||||
raw=a.transfer_receipt.read_bytes()
|
||||
transfer=json.loads(raw)
|
||||
receipt['transfer_receipt_sha256']=hashlib.sha256(raw).hexdigest()
|
||||
nextcloud = (transfer.get('schema') == 'platform.real-offsite-recovery.v1'
|
||||
and transfer.get('decrypted') is True
|
||||
and transfer.get('upload_http_status') == 201
|
||||
and transfer.get('status') == 'offsite_fetched_pending_isolated_restore'
|
||||
and transfer.get('download_http_status') == 200)
|
||||
primary = (transfer.get('status') == 'primary_fetched_pending_application_restore'
|
||||
primary = (transfer.get('schema') in ('platform.forgejo-primary-archive.v1', 'platform.forgejo-primary-decryption.v1')
|
||||
and transfer.get('status') == 'primary_fetched_pending_application_restore'
|
||||
and transfer.get('download_hash_matches') is True and transfer.get('decrypted') is True)
|
||||
if (not (nextcloud or primary)
|
||||
or transfer.get('plaintext_sha256') != file_digest(a.archive)):
|
||||
|
|
@ -205,10 +212,13 @@ def main():
|
|||
receipt['ciphertext_sha256']=transfer['ciphertext_sha256']
|
||||
if transfer.get('archive_profile','full')!=a.profile: raise ValueError('profile_provenance_mismatch')
|
||||
run(a.archive,receipt,a.profile)
|
||||
if receipt.get('cleanup') is not True:
|
||||
raise ValueError('restore_cleanup_incomplete')
|
||||
except Exception:
|
||||
receipt['status']='failed'
|
||||
receipt['error']='isolated_restore_failed'
|
||||
finally:
|
||||
receipt['finished_at']=datetime.now(timezone.utc).isoformat()
|
||||
with os.fdopen(fd,'w') as f: json.dump(receipt,f,indent=2)
|
||||
return int(receipt['status'] not in ('restored','restored_essentials') or not receipt.get('cleanup'))
|
||||
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ def main():
|
|||
p.add_argument('--output',required=True,type=Path);p.add_argument('--receipt',required=True,type=Path)
|
||||
p.add_argument('--kubeconfig',required=True)
|
||||
a=p.parse_args()
|
||||
receipt={'schema':'platform.forgejo-primary-archive.v1','status':'running'}
|
||||
receipt={'schema':'platform.forgejo-primary-archive.v1','status':'running','started_at':datetime.now(timezone.utc).isoformat()}
|
||||
fd=os.open(a.receipt,os.O_CREAT|os.O_EXCL|os.O_WRONLY,0o600);os.close(fd)
|
||||
def checkpoint(): a.receipt.write_text(json.dumps(receipt,indent=2)+'\n')
|
||||
try:
|
||||
|
|
@ -99,6 +99,9 @@ def main():
|
|||
receipt['exception_type']=type(error).__name__
|
||||
receipt.update(status='failed',error='bounded_primary_archive_transfer_failed');checkpoint()
|
||||
return 1
|
||||
finally:
|
||||
receipt['finished_at']=datetime.now(timezone.utc).isoformat()
|
||||
checkpoint()
|
||||
return 0
|
||||
|
||||
if __name__=='__main__': raise SystemExit(main())
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Silent attended transfer of a real encrypted backup for isolated recovery."""
|
||||
import argparse
|
||||
from datetime import datetime, timezone
|
||||
import base64
|
||||
import urllib.request
|
||||
import shutil
|
||||
|
|
@ -70,10 +71,11 @@ def main():
|
|||
p.add_argument('--receipt',required=True)
|
||||
a=p.parse_args()
|
||||
fd=os.open(a.receipt,os.O_WRONLY|os.O_CREAT|os.O_EXCL,0o600)
|
||||
receipt={'schema':'platform.real-offsite-recovery.v1','status':'failed'}
|
||||
receipt={'schema':'platform.real-offsite-recovery.v1','status':'failed','started_at':datetime.now(timezone.utc).isoformat()}
|
||||
try: run(a.source,a.directory,receipt)
|
||||
except Exception as error: receipt['error']=str(error) if isinstance(error,LaneError) else 'internal_error'
|
||||
finally:
|
||||
receipt['finished_at']=datetime.now(timezone.utc).isoformat()
|
||||
with os.fdopen(fd,'w') as f: json.dump(receipt,f,indent=2)
|
||||
return int(receipt['status']=='failed')
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue