the-custodian/docs/changes/CUST-WP-0080/verify_receive_native.py
codex eb93eeedfd
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Reject generated-only commits / generated-commit-guard (push) Successful in 7s
security: preserve verified Forgejo scanner activation and reconcile recovery
2026-10-13 11:31:06 +02:00

145 lines
7 KiB
Python

#!/usr/bin/env python3
"""Exact-binary disposable probe in the admitted live Forgejo container.
No production repository, Git hook, credential, account, config or policy writes.
Default is read-only preflight; --apply creates only the exact new /tmp directory
and removes it after the probe, guarded by its unique owner marker.
"""
import argparse
import hashlib
import io
import json
import re
import shlex
import subprocess
import tarfile
from pathlib import Path
def sha(path):
return hashlib.sha256(Path(path).read_bytes()).hexdigest()
def command(args, payload=None, timeout=45):
result = subprocess.run(['ssh', '-o', 'BatchMode=yes', '-o', 'ConnectTimeout=10',
'railiance01', shlex.join(['kubectl', '--request-timeout=15s', *args])],
input=payload, capture_output=True, timeout=timeout)
if result.returncode:
raise ValueError('native-command-refused')
return result.stdout
def execute(proposal, *args, payload=None, timeout=45):
return command(['-n', 'forgejo', 'exec', '-i', proposal['pod'], '-c', 'gitea', '--', *args],
payload, timeout)
def preflight(proposal):
if not re.fullmatch(r'custodian-receive-native-[a-z0-9-]+', proposal['directory_name']):
raise ValueError('unsafe-probe-directory')
if proposal['executor_sha256'] != sha(__file__):
raise ValueError('executor-revision-changed')
nodes = json.loads(command(['get', 'nodes', '-o', 'json']))['items']
if len(nodes) != 1 or not any(a['address'] == '92.205.62.239' for a in nodes[0]['status']['addresses']):
raise ValueError('target-host-changed')
pod = json.loads(command(['-n', 'forgejo', 'get', 'pod', proposal['pod'], '-o', 'json']))
if pod['metadata']['uid'] != proposal['pod_uid']:
raise ValueError('target-pod-changed')
if not any(c['type'] == 'Ready' and c['status'] == 'True' for c in pod['status']['conditions']):
raise ValueError('target-pod-unready')
image = next(c['image'] for c in pod['spec']['containers'] if c['name'] == 'gitea')
if image != proposal['image']:
raise ValueError('target-image-changed')
image_id = next(c['imageID'] for c in pod['status']['containerStatuses'] if c['name'] == 'gitea')
if image_id != proposal['image_id']:
raise ValueError('target-image-digest-changed')
for item in proposal['artifacts']:
path = Path(item['source'])
if path.is_symlink() or not path.is_file() or sha(path) != item['sha256']:
raise ValueError('artifact-changed')
if not re.fullmatch(r'[a-zA-Z0-9._-]+', item['name']):
raise ValueError('artifact-name-refused')
df = execute(proposal, 'df', '-Pk', '/tmp').decode().splitlines()[-1].split()
if int(df[3]) < 256 * 1024:
raise ValueError('temporary-space-insufficient')
execute(proposal, 'wget', '-qO', '/dev/null', 'http://127.0.0.1:3000/api/healthz')
dispatcher = execute(proposal, 'sha256sum', proposal['existing_dispatcher']).decode().split()[0]
if dispatcher != proposal['dispatcher_sha256']:
raise ValueError('existing-dispatcher-changed')
return {'pod_uid': pod['metadata']['uid'], 'image_id': image_id,
'git_version': execute(proposal, '/usr/bin/git', '--version').decode().strip(),
'free_tmp_kib': int(df[3]), 'health_before': True}
def archive(proposal):
data = io.BytesIO()
checksums = []
with tarfile.open(fileobj=data, mode='w') as output:
for item in proposal['artifacts']:
content = Path(item['source']).read_bytes()
info = tarfile.TarInfo(item['name']); info.size = len(content); info.mode = item['mode']
info.uid = info.gid = 1000
output.addfile(info, io.BytesIO(content))
checksums.append(item['sha256'] + ' ' + item['name'] + '\n')
content = ''.join(checksums).encode()
info = tarfile.TarInfo('SHA256SUMS'); info.size = len(content); info.mode = 0o600
info.uid = info.gid = 1000
output.addfile(info, io.BytesIO(content))
return data.getvalue()
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--proposal', type=Path, required=True)
parser.add_argument('--receipt', type=Path, required=True)
parser.add_argument('--apply', action='store_true')
args = parser.parse_args()
proposal = json.loads(args.proposal.read_text())
observation = preflight(proposal)
if not args.apply:
print(json.dumps({'status': 'preflight-verified', **observation}))
return
directory = '/tmp/' + proposal['directory_name']
marker = proposal['proposal_id']
if not re.fullmatch(r'[A-Za-z0-9-]+', marker):
raise ValueError('unsafe-marker')
receipt = {'proposal_id': marker, 'proposal_sha256': sha(args.proposal),
'executed_revision': proposal['revision'], 'result': 'unverified', 'observation': observation,
'scope': 'exact binaries in disposable bare repositories inside existing Forgejo image; builtin auth stubbed',
'production_repositories_changed': False, 'live_credentials_used': False}
created = False
try:
execute(proposal, 'sh', '-c', 'umask 077; test ! -e "$1" && mkdir "$1" && printf "%s" "$2" > "$1/.probe-owner"',
'sh', directory, marker)
created = True
execute(proposal, 'tar', '-xmf', '-', '-C', directory, payload=archive(proposal), timeout=90)
raw = execute(proposal, 'timeout', '-k', '5', '150', 'sh', directory + '/probe.sh', directory, timeout=180)
result = json.loads(raw)
keys = ['clean_push', 'intermediate_secret_refused', 'quarantine_discarded',
'scanner_unavailable_refused', 'clean_after_recovery', 'dispatcher_preserved']
if not all(result.get(k) is True for k in keys) or result.get('other_hook_invocations') != 4:
raise ValueError('incomplete-native-proof')
receipt['verification'] = result
execute(proposal, 'wget', '-qO', '/dev/null', 'http://127.0.0.1:3000/api/healthz')
receipt['health_after'] = True
receipt['result'] = 'verified'
finally:
if created:
try:
execute(proposal, 'sh', '-c', 'test "$(cat "$1/.probe-owner")" = "$2" && rm -rf -- "$1" && test ! -e "$1"',
'sh', directory, marker)
receipt['temporary_directory_removed'] = True
except (ValueError, OSError, subprocess.SubprocessError):
receipt['temporary_directory_removed'] = False
receipt['result'] = 'unverified'
args.receipt.write_text(json.dumps(receipt, indent=2) + '\n')
if receipt['result'] != 'verified':
raise ValueError('verification-or-cleanup-incomplete')
print('Exact-binary native probe verified; temporary area removed; production hooks unchanged.')
if __name__ == '__main__':
try:
main()
except (ValueError, OSError, KeyError, StopIteration, subprocess.SubprocessError):
raise SystemExit('Native probe refused or incomplete; inspect value-free receipt. Raw diagnostics suppressed.')