diff --git a/.kaizen/agents/custodian-codex/supervision.json b/.kaizen/agents/custodian-codex/supervision.json index 057b5250..d9acae44 100644 --- a/.kaizen/agents/custodian-codex/supervision.json +++ b/.kaizen/agents/custodian-codex/supervision.json @@ -516,6 +516,53 @@ "verified_at": "2026-10-11T23:23:28.882335+00:00", "verified_outcome": "Exact source ConfigMap, generation 11 ready; health 200 and unauthenticated 403. Attended positive preview remains open.", "refinement_or_rescue": false + }, + { + "proposal_id": "FORGE6-NATIVE-PROBE-001", + "disposition": "unscored", + "outcome": "succeeded", + "original_revision": 1, + "original_digest_sha256": "b6fe6f55354f7f09837b94f4d75e79f39ac0a4b868f7ac377e2ca6590e340b42", + "proposal_ref": "docs/changes/CUST-WP-0080/receive-native-proposal.json", + "authorization_ref": "canon/standards/build-mode-change-authorization_v0.1.md", + "target": "railiance01 forgejo/forgejo-gitea-7d94988c79-8k76l new disposable /tmp probe only", + "expected_result": "Exact candidate binary on deployed image/Git accepts clean temporary pushes, rejects intermediate synthetic private key, discards quarantine, refuses unavailable scanner, recovers clean acceptance, retains dispatcher/additional-hook behavior; health remains passing and temporary area removed.", + "verification_ref": "docs/evidence/2026-10-12-netkingdom-security/receive-native-probe.json", + "rollback": "Remove only the new /tmp/custodian-receive-native-20261012-001 directory after matching .probe-owner. No existing repository/hook/config changes. Executor finally performs cleanup on success/failure.", + "abort_owner": "railiance-forge; executing custodian-codex", + "cost_estimate_eur": 0, + "risk_estimate": "Temporary approximately 27 MiB artifact storage plus small disposable Git fixtures and brief CPU use on the existing container; no new resource allocation/spend, no real credentials or production repository/config writes. Preflight requires 256 MiB free temporary space and passing health; execution has timeout and owned-path cleanup.", + "promotion_evidence": false, + "reason": "Existing standing founder authorization; not an exact-revision human approval, autopilot grant or credential-isolation proof.", + "executed_revision": 1, + "executed_digest_sha256": "b6fe6f55354f7f09837b94f4d75e79f39ac0a4b868f7ac377e2ca6590e340b42", + "verified_at": "2026-10-12T17:13:10.480151+00:00", + "verified_outcome": "Exact binaries passed disposable native push/detection/quarantine/failure/recovery/dispatcher checks in live image; existing service health passed, temporary directory removed. Production auth stubbed and hooks unchanged.", + "refinement_or_rescue": false + }, + { + "proposal_id": "FORGE6-RECEIVE-ACTIVATION-001", + "authorization_ref": "canon/standards/build-mode-change-authorization_v0.1.md", + "expected_result": "22 existing upstream dispatchers gain independent pinned secret scanner; SSH clean/blocked results observed under independently tested temporary veto, rejected probe refs and quarantines absent, veto removed, existing hooks preserved and health passing. Subsequent authorized ordinary source push separately proves clean acceptance.", + "rollback": "Run the same executor with this proposal and --rollback. Removes only the two additive files after exact hash verification; verifies original eight-file inventory on all 22 repositories. Durable admitted binaries remain inert. Automatic rollback on failed activation verification.", + "abort_owner": "railiance-forge; executing custodian-codex", + "cost_estimate_eur": 0, + "risk_estimate": "Existing infrastructure, approximately 27 MiB durable storage and transient transfer copy, sequential small probes. Possible false refusals or bounded push latency on 22 repositories until rollback. No new spend/resources or credential scope; no restart. Temporary exact-ref veto prevents synthetic fixture publication. Scanner bounds: 90 seconds, 2000 objects, 8 MiB each, 64 MiB aggregate; larger pushes require reviewed remediation rather than bypass.", + "promotion_evidence": false, + "disposition": "unscored", + "outcome": "succeeded", + "original_revision": 1, + "original_digest_sha256": "9c8ea3b7fbe16a997359432af7bcf09f429881fd23cc567188c21129e80ccf8d", + "proposal_ref": "docs/changes/CUST-WP-0080/receive-hook-proposal.json", + "target": "railiance01 existing forgejo pod; additive pre-receive.d hooks on exactly 22 listed repositories", + "verification_ref": "docs/evidence/2026-10-12-netkingdom-security/receive-hook-activation.json", + "reason": "Existing standing founder blue-lane authorization; not an exact-revision human approval or autopilot grant.", + "prepared_at": "2026-10-12T17:28:10.890040+00:00", + "refinement_or_rescue": false, + "executed_revision": 1, + "executed_digest_sha256": "9c8ea3b7fbe16a997359432af7bcf09f429881fd23cc567188c21129e80ccf8d", + "verified_at": "2026-10-12T17:32:28.543884+00:00", + "verified_outcome": "Additive independent scanner installed on 22 exact repositories. Each actual SSH clean and synthetic-secret probe produced the expected scanner result under the temporary veto; probe refs and quarantined commits absent. Veto and staging removed, original hook inventory preserved, health passes. Normal clean acceptance remains separate." } ] } diff --git a/docs/changes/CUST-WP-0080/activate_receive_hooks.py b/docs/changes/CUST-WP-0080/activate_receive_hooks.py new file mode 100644 index 00000000..a6275aba --- /dev/null +++ b/docs/changes/CUST-WP-0080/activate_receive_hooks.py @@ -0,0 +1,237 @@ +#!/usr/bin/env python3 +"""Activate exact reviewed additive Forgejo hook profile and verify SSH refusals. + +Synthetic pushes are always vetoed by a temporary independent hook. Existing +Forgejo hooks are hash-checked and preserved. No real credential is read and no +synthetic ref is retained. Native frontend/HTTP/new-repo coverage stays separate. +""" +import argparse +import hashlib +import importlib.util +import json +import os +from pathlib import Path +import re +import shlex +import subprocess +import tempfile + + +def sha(path): + return hashlib.sha256(Path(path).read_bytes()).hexdigest() + + +def helper(proposal): + path = Path(__file__).with_name('verify_receive_native.py') + if sha(__file__) != proposal['executor_sha256'] or sha(path) != proposal['native_helper_sha256']: + raise ValueError('executor-revision-changed') + spec = importlib.util.spec_from_file_location('native_receive_support', path) + module = importlib.util.module_from_spec(spec); spec.loader.exec_module(module) + return module + + +def shell(native, proposal, script, payload=None, timeout=60): + return native.execute(proposal, 'bash', '-c', 'set -euo pipefail\n' + script, payload=payload, timeout=timeout) + + +def repo_script(proposal, body): + names = proposal['repositories'] + if len(names) != 22 or len(set(names)) != len(names) or any(not re.fullmatch(r'[a-z0-9-]+', n) for n in names): + raise ValueError('scope-refused') + return 'for name in ' + ' '.join(names) + '; do\nrepo=/data/git/gitea-repositories/coulomb/$name.git\n' + body + '\ndone\n' + + +def validate(proposal): + for name in [proposal['hook_name'], proposal['veto_name']]: + if not re.fullmatch(r'[a-z0-9-]+', name): raise ValueError('unsafe-hook-name') + if not re.fullmatch(r'/data/security/receive-secret-scan/[a-z0-9-]+', proposal['version_directory']): + raise ValueError('unsafe-version-directory') + if not re.fullmatch(r'refs/heads/custodian-g07-probe-[a-z0-9-]+', proposal['probe_ref']): + raise ValueError('unsafe-probe-ref') + + +def inspect(native, proposal, applied=False, sentinel=False): + validate(proposal) + prefix = proposal['hook_name']; veto = proposal['veto_name'] + expected = dict(proposal['existing_hook_hashes']) + if applied: expected['pre-receive.d/' + prefix] = proposal['hook_sha256'] + if sentinel: expected['pre-receive.d/' + veto] = proposal['veto_sha256'] + body = '''test -d "$repo" && test ! -L "$repo" && test ! -L "$repo/hooks" && test ! -L "$repo/hooks/pre-receive.d" +test -z "$(find "$repo/hooks" -maxdepth 2 -type l -print -quit)" +printf 'REPO %s\n' "$name" +find "$repo/hooks" -maxdepth 2 -type f ! -name '*.sample' -exec sha256sum {} \\; +''' + if applied: + body += 'test -x "$repo/hooks/pre-receive.d/' + proposal['hook_name'] + '"\n' + if sentinel: + body += 'test -x "$repo/hooks/pre-receive.d/' + proposal['veto_name'] + '"\n' + raw = shell(native, proposal, repo_script(proposal, body)).decode() + rows = {}; current = None + for line in raw.splitlines(): + if line.startswith('REPO '): current = line[5:]; rows[current] = {} + else: + digest, name = line.split(' ', 1) + if not re.fullmatch(r'[a-f0-9]{64}', digest): raise ValueError('invalid-hook-digest') + prefix_path = '/data/git/gitea-repositories/coulomb/' + current + '.git/hooks/' + if not name.startswith(prefix_path): raise ValueError('unexpected-hook-path') + rows[current][name[len(prefix_path):]] = digest + if set(rows) != set(proposal['repositories']) or any(v != expected for v in rows.values()): + raise ValueError('hook-inventory-changed') + return rows + + +def rollback(native, proposal): + validate(proposal) + for name, digest in [(proposal['hook_name'], proposal['hook_sha256']), (proposal['veto_name'], proposal['veto_sha256'])]: + body = 'path="$repo/hooks/pre-receive.d/' + name + '"\n' + body += 'if test -e "$path"; then test ! -L "$path"; test "$(sha256sum "$path" | cut -d" " -f1)" = ' + shlex.quote(digest) + '; fi\n' + shell(native, proposal, repo_script(proposal, body)) + for name in [proposal['hook_name'], proposal['veto_name']]: + shell(native, proposal, repo_script(proposal, 'rm -f -- "$repo/hooks/pre-receive.d/' + name + '"')) + inspect(native, proposal) + + +def git(directory, args, payload=None, accepted=(0,)): + env = dict(os.environ) + for key in list(env): + if key.startswith('GIT_'): env.pop(key) + env.update(GIT_CONFIG_NOSYSTEM='1', GIT_CONFIG_GLOBAL='/dev/null', GIT_TERMINAL_PROMPT='0', + GIT_SSH_COMMAND='ssh -o BatchMode=yes -o ConnectTimeout=10') + r = subprocess.run(['/usr/bin/git', *args], cwd=directory, env=env, input=payload, + stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=120) + if r.returncode not in accepted: + raise ValueError('git-probe-command-failed') + return r + + +def verify_pushes(native, proposal): + fixture = Path(proposal['fixture_key']) + if sha(fixture) != proposal['fixture_sha256']: raise ValueError('fixture-changed') + rows = [] + with tempfile.TemporaryDirectory(prefix='forge-receive-push-proof-') as temporary: + directory = Path(temporary) + git(directory, ['init', '-q', '-b', 'main']) + git(directory, ['config', 'core.hooksPath', '/dev/null']) + git(directory, ['config', 'user.name', 'Custodian disposable detector probe']) + git(directory, ['config', 'user.email', 'probe@example.test']) + (directory / 'source.txt').write_text('Disposable detector probe; intended to be rejected by a temporary veto.\n') + git(directory, ['add', 'source.txt']); git(directory, ['commit', '-qm', proposal['proposal_id'] + ' clean probe']) + clean = git(directory, ['rev-parse', 'HEAD']).stdout.decode().strip() + (directory / 'fixture.txt').write_bytes(fixture.read_bytes()) + git(directory, ['add', 'fixture.txt']); git(directory, ['commit', '-qm', proposal['proposal_id'] + ' synthetic detector fixture']) + blocked = git(directory, ['rev-parse', 'HEAD']).stdout.decode().strip() + for number, name in enumerate(proposal['repositories']): + remote = 'forgejo-remote:coulomb/' + name + '.git' + if git(directory, ['ls-remote', remote, proposal['probe_ref']]).stdout.strip(): + raise ValueError('probe-ref-already-exists') + row = {'repo': name} + for oid, status in [(clean, 'clean'), (blocked, 'blocked')]: + r = git(directory, ['push', '--porcelain', remote, oid + ':' + proposal['probe_ref']], accepted=(0, 1)) + output = r.stdout + r.stderr + if r.returncode == 0: + # Only our newly created ref may be removed, with a lease. + git(directory, ['push', '--force-with-lease=' + proposal['probe_ref'] + ':' + oid, + remote, ':' + proposal['probe_ref']]) + raise ValueError('independent-veto-failed-ref-removed') + if fixture.read_bytes().strip() in output: raise ValueError('synthetic-content-disclosed') + found = [] + for line in output.splitlines(): + match = re.search(rb'\{"status":.*\}', line) + if match: + try: found.append(json.loads(match.group())) + except ValueError: raise ValueError('malformed-control-receipt') + if len(found) != 1 or found[0].get('status') != status or b'CUSTODIAN_G07_PROBE_VETO' not in output: + raise ValueError('native-push-proof-incomplete') + row[status + '_scanner_result'] = status + if git(directory, ['ls-remote', remote, proposal['probe_ref']]).stdout.strip(): + raise ValueError('unexpected-probe-ref-retained') + raw = native.execute(proposal, '/usr/bin/git', '--git-dir=/data/git/gitea-repositories/coulomb/' + name + '.git', + 'cat-file', '--batch-check', payload=(blocked + '\n').encode()).decode().strip() + if raw != blocked + ' missing': raise ValueError('quarantine-object-retained') + row.update(temporary_veto_observed=True, probe_ref_absent=True, quarantined_commit_absent=True) + rows.append(row) + if (number + 1) % 5 == 0: print('Verified SSH refusal probes:', number + 1, 'of', len(proposal['repositories']), flush=True) + return rows + + +def main(): + p = argparse.ArgumentParser(description=__doc__) + p.add_argument('--proposal', type=Path, required=True); p.add_argument('--receipt', type=Path, required=True) + modes = p.add_mutually_exclusive_group() + modes.add_argument('--apply', action='store_true'); modes.add_argument('--rollback', action='store_true') + args = p.parse_args(); proposal = json.loads(args.proposal.read_text()); native = helper(proposal) + # Reuse only the read-only target/artifact preflight with its own pinned revision. + validate(proposal) + support = dict(proposal); support['executor_sha256'] = proposal['native_helper_sha256'] + if args.rollback: support['artifacts'] = [] + observation = native.preflight(support) + if args.rollback: + rollback(native, proposal); print('Prior hook inventory restored; admitted tools remain inert.'); return + inspect(native, proposal) + ref = shlex.quote(proposal['probe_ref']) + shell(native, proposal, repo_script(proposal, 'test -z "$(git --git-dir="$repo" for-each-ref --format=\'%(refname)\' ' + ref + ')"')) + if not args.apply: + print(json.dumps({'status': 'preflight-verified', 'repositories': len(proposal['repositories']), **observation})); return + temporary = '/tmp/' + proposal['directory_name']; version = proposal['version_directory'] + receipt = {'proposal_id': proposal['proposal_id'], 'proposal_sha256': sha(args.proposal), 'result': 'unverified', + 'observation': observation, 'phase': 'stage-tools', 'no_live_credentials_used': True, + 'executed_revision': proposal['revision']} + created = changed = False + try: + native.execute(proposal, 'sh', '-c', 'umask 077; test ! -e "$1" && mkdir "$1" && printf "%s" "$2" > "$1/.probe-owner"', + 'sh', temporary, proposal['proposal_id']); created = True + native.execute(proposal, 'tar', '-xmf', '-', '-C', temporary, payload=native.archive(proposal), timeout=90) + lines = ['cd ' + shlex.quote(temporary), 'sha256sum -c SHA256SUMS >/dev/null 2>&1'] + for parent in ['/data/security', '/data/security/receive-secret-scan', version]: + lines.append('test ! -L ' + shlex.quote(parent)) + lines.append('mkdir -p ' + shlex.quote(version)) + for artifact in proposal['artifacts']: + if artifact['name'] not in ['receive-secret-scan', 'gitleaks']: continue + target = version + '/' + artifact['name']; quoted = shlex.quote(target) + lines.append('if test -e ' + quoted + '; then test ! -L ' + quoted + '; test "$(sha256sum ' + quoted + ' | cut -d" " -f1)" = ' + shlex.quote(artifact['sha256']) + '; else cp ' + shlex.quote(artifact['name']) + ' ' + quoted + '; fi') + lines.append('chmod 755 ' + quoted) + shell(native, proposal, '\n'.join(lines)) + receipt['phase'] = 'install-hooks'; changed = True + body = '' + for field, source in [('veto_name', 'probe-veto'), ('hook_name', 'profile-hook')]: + dest = '"$repo/hooks/pre-receive.d/' + proposal[field] + '"' + body += 'test ! -e ' + dest + ' && test ! -L ' + dest + '\n' + body += 'staged=$(mktemp \"$repo/hooks/.custodian-install-XXXXXX\")\n' + body += 'cp ' + shlex.quote(temporary + '/' + source) + ' \"$staged\"\nchmod 755 \"$staged\"\nmv -n \"$staged\" ' + dest + '\n' + body += 'test ! -e \"$staged\"\n' + shell(native, proposal, repo_script(proposal, body)) + inspect(native, proposal, applied=True, sentinel=True) + receipt['phase'] = 'verify-veto' + body = 'set +e\noutput=$(printf "%040d %040d %s\\n" 0 1 ' + ref + ' | "$repo/hooks/pre-receive.d/' + proposal['veto_name'] + '" 2>&1)\ncode=$?\nset -e\ntest "$code" = 1 && test "$output" = CUSTODIAN_G07_PROBE_VETO' + shell(native, proposal, repo_script(proposal, body)) + receipt['phase'] = 'verify-ssh-pushes' + receipt['repositories'] = verify_pushes(native, proposal) + receipt['phase'] = 'remove-temporary-veto' + body = 'path="$repo/hooks/pre-receive.d/' + proposal['veto_name'] + '"\ntest "$(sha256sum "$path" | cut -d" " -f1)" = ' + shlex.quote(proposal['veto_sha256']) + '\nrm -- "$path"' + shell(native, proposal, repo_script(proposal, body)) + inspect(native, proposal, applied=True) + native.execute(proposal, 'wget', '-qO', '/dev/null', 'http://127.0.0.1:3000/api/healthz') + receipt.update(result='verified', phase='active', temporary_veto_removed=True, health_after=True, + verification_limit='Synthetic pushes deliberately vetoed; scanner clean/detection results and quarantine absence verified. Normal clean acceptance needs the subsequent authorized source push; HTTP/frontend/lifecycle/outside-scope remain separate.') + except BaseException: + if changed: + try: rollback(native, proposal); receipt['rollback'] = 'prior hook inventory restored' + except BaseException: receipt['rollback'] = 'unverified; inspect exact target hashes' + raise + finally: + if created: + try: + native.execute(proposal, 'sh', '-c', 'test "$(cat "$1/.probe-owner")" = "$2" && rm -rf -- "$1" && test ! -e "$1"', + 'sh', temporary, proposal['proposal_id']) + receipt['temporary_directory_removed'] = True + except BaseException: + receipt['temporary_directory_removed'] = False; receipt['result'] = 'unverified' + args.receipt.write_text(json.dumps(receipt, indent=2) + '\n') + if receipt['result'] != 'verified': raise ValueError('cleanup-incomplete') + print('Receive-path profile active for 22 repositories; SSH detector/veto/quarantine proofs verified.') + + +if __name__ == '__main__': + try: main() + except (ValueError, OSError, KeyError, subprocess.SubprocessError): + raise SystemExit('Activation refused or incomplete; inspect value-free receipt. Raw diagnostics suppressed.') diff --git a/docs/changes/CUST-WP-0080/observe_forge_inventory.py b/docs/changes/CUST-WP-0080/observe_forge_inventory.py new file mode 100644 index 00000000..de257c2e --- /dev/null +++ b/docs/changes/CUST-WP-0080/observe_forge_inventory.py @@ -0,0 +1,34 @@ +import json, subprocess, re, datetime + +def command(args, payload=None): + r=subprocess.run(['kubectl','--request-timeout=15s',*args],input=payload,capture_output=True,text=True,timeout=35) + if r.returncode: raise SystemExit('Metadata observation failed; raw output suppressed.') + return r.stdout + +def ready(p): + return p.get('status',{}).get('phase')=='Running' and any(c.get('type')=='Ready' and c.get('status')=='True' for c in p.get('status',{}).get('conditions',[])) +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 SystemExit('Unexpected target host.') +pods=json.loads(command(['-n','forgejo','get','pods','-o','json']))['items'] +apps=[p for p in pods if ready(p) and any(c['name']=='gitea' for c in p['spec']['containers'])] +if len(apps)!=1: raise SystemExit('Ambiguous forge pod.') +app=apps[0] +cluster=json.loads(command(['-n','databases','get','cluster','forgejo-db','-o','json'])) +primary=cluster.get('status',{}).get('currentPrimary') +if not primary or not re.fullmatch(r'forgejo-db-[0-9]+',primary): raise SystemExit('No unambiguous database primary.') +query='BEGIN READ ONLY;\nSET LOCAL statement_timeout = \'5s\';\nSELECT json_build_object(\n \'repositories\',(SELECT json_agg(row_to_json(x) ORDER BY x.id) FROM\n (SELECT r.id,u.lower_name AS owner,r.lower_name AS name,r.is_private,r.is_empty,r.is_archived,r.is_mirror,r.is_fork,r.object_format_name,r.git_size,r.lfs_size,\n (SELECT count(*) FROM protected_branch b WHERE b.repo_id=r.id) AS protected_branch_rules,\n (SELECT json_agg(ru.type ORDER BY ru.type) FROM repo_unit ru WHERE ru.repo_id=r.id) AS unit_types\n FROM repository r JOIN "user" u ON u.id=r.owner_id) x)\n);\nROLLBACK;' +raw=command(['-n','databases','exec','-i',primary,'-c','postgres','--','psql','-X','-U','postgres','-d','forgejo','-Atq','-v','ON_ERROR_STOP=1'],query) +metadata=json.loads(raw) +versions=command(['-n','forgejo','exec',app['metadata']['name'],'-c','gitea','--','/usr/bin/git','--version']).strip() +directories=command(['-n','forgejo','exec',app['metadata']['name'],'-c','gitea','--','find','/data/git/gitea-repositories','-mindepth','2','-maxdepth','2','-type','d','-name','*.git']).splitlines() +relative=[] +for directory in directories: + path=directory.removeprefix('/data/git/gitea-repositories/') + if not re.fullmatch(r'[a-z0-9_.-]+/[a-z0-9_.-]+\.git',path): raise SystemExit('Unrecognized repository path; output suppressed.') + relative.append(path) +for repo in metadata['repositories']: + if not re.fullmatch(r'[a-z0-9_.-]+',repo['owner']) or not re.fullmatch(r'[a-z0-9_.-]+',repo['name']): raise SystemExit('Unrecognized repository identity; output suppressed.') + repo['main_directory_present']=repo['owner']+'/'+repo['name']+'.git' in relative + repo['wiki_directory_present']=repo['owner']+'/'+repo['name']+'.wiki.git' in relative +result={'observed_at':datetime.datetime.now(datetime.timezone.utc).isoformat(),'host':'railiance01','forge_pod':app['metadata']['name'],'forge_pod_uid':app['metadata']['uid'],'containers':[{'name':c['name'],'image':c['image'],'resources':c.get('resources',{})} for c in app['spec']['containers']],'database_primary':primary,'git_version':versions,'repositories':metadata['repositories'],'repository_directories':sorted(relative),'scope':'Read-only metadata: DB identifiers/flags/sizes/unit types/protected-rule counts and repository-directory presence. No descriptions, URLs, credentials, repository contents or logs read.'} +print(json.dumps(result,indent=2)) diff --git a/docs/changes/CUST-WP-0080/probe_receive_native.sh b/docs/changes/CUST-WP-0080/probe_receive_native.sh new file mode 100644 index 00000000..8b05fabb --- /dev/null +++ b/docs/changes/CUST-WP-0080/probe_receive_native.sh @@ -0,0 +1,63 @@ +#!/bin/sh +# Disposable bare repositories only; caller owns admission and exact cleanup. +set +vx +set -eu +base=$1 +cd "$base" +umask 077 +export HOME="$base/home" GIT_CONFIG_NOSYSTEM=1 GIT_CONFIG_GLOBAL=/dev/null GIT_TERMINAL_PROMPT=0 +mkdir home +trap 'chmod 755 "$base/gitleaks" >/dev/null 2>&1 || :' 0 +sha256sum -c SHA256SUMS >/dev/null 2>&1 +chmod 755 receive-secret-scan gitleaks +/usr/bin/git init -q --bare server.git +/usr/bin/git init -q -b main client +/usr/bin/git -C server.git config core.hooksPath "$base/server.git/hooks" +/usr/bin/git -C client config core.hooksPath "$base/no-client-hooks" +/usr/bin/git -C client config user.name 'Custodian disposable probe' +/usr/bin/git -C client config user.email 'probe@example.test' +mkdir server.git/hooks/pre-receive.d +cp pre-receive.fixture server.git/hooks/pre-receive +chmod 755 server.git/hooks/pre-receive +cat > server.git/hooks/pre-receive.d/gitea </dev/null +printf 'invoked\n' >> '$base/stub-calls' +STUB +cat > server.git/hooks/pre-receive.d/secret-scan < client/source.txt +/usr/bin/git -C client add source.txt +/usr/bin/git -C client commit -qm 'disposable clean baseline' +if ! /usr/bin/git -C client push "$base/server.git" HEAD:refs/heads/main > clean.out 2>&1; then exit 21; fi +grep -q '"status":"clean"' clean.out +initial=$(/usr/bin/git -C client rev-parse HEAD) +cp fixture-key client/secret.txt +/usr/bin/git -C client add secret.txt +/usr/bin/git -C client commit -qm 'disposable detector fixture' +rejected=$(/usr/bin/git -C client rev-parse HEAD) +/usr/bin/git -C client rm -q secret.txt +/usr/bin/git -C client commit -qm 'remove fixture from final tree' +if /usr/bin/git -C client push "$base/server.git" HEAD:refs/heads/main > blocked.out 2>&1; then exit 22; fi +grep -q '"status":"blocked"' blocked.out +if grep -q 'BEGIN OPENSSH PRIVATE KEY' blocked.out; then exit 23; fi +if /usr/bin/git -C server.git cat-file -e "$rejected" >/dev/null 2>&1; then exit 24; fi +test "$(/usr/bin/git -C server.git rev-parse refs/heads/main)" = "$initial" +/usr/bin/git -C client checkout -qb clean-retry "$initial" +printf 'clean recovery\n' > client/source.txt +/usr/bin/git -C client add source.txt +/usr/bin/git -C client commit -qm 'disposable clean recovery' +chmod 644 gitleaks +if /usr/bin/git -C client push "$base/server.git" HEAD:refs/heads/main > unavailable.out 2>&1; then exit 25; fi +grep -q '"status":"unverified"' unavailable.out +grep -q '"reason":"scanner-file-refused"' unavailable.out +chmod 755 gitleaks +if ! /usr/bin/git -C client push "$base/server.git" HEAD:refs/heads/main > recovered.out 2>&1; then exit 26; fi +grep -q '"status":"clean"' recovered.out +test "$(/usr/bin/git -C server.git rev-parse refs/heads/main)" = "$(/usr/bin/git -C client rev-parse HEAD)" +test "$(wc -l < stub-calls | tr -d ' ')" = 4 +cmp pre-receive.fixture server.git/hooks/pre-receive +printf '{"clean_push":true,"intermediate_secret_refused":true,"quarantine_discarded":true,"scanner_unavailable_refused":true,"clean_after_recovery":true,"dispatcher_preserved":true,"other_hook_invocations":4,"builtin_authorization_stubbed":true}\n' diff --git a/docs/changes/CUST-WP-0080/receive-hook-operations.md b/docs/changes/CUST-WP-0080/receive-hook-operations.md new file mode 100644 index 00000000..dcec7d60 --- /dev/null +++ b/docs/changes/CUST-WP-0080/receive-hook-operations.md @@ -0,0 +1,71 @@ +# Initial forge receive-hook activation + +Owner work: FORGE-WP-0006-T02, coordinated through RMGR-WP-0021-T03 and +CUST-WP-0080 G07. The exact proposed action is `receive-hook-proposal.json`; +its executor and artifact hashes are immutable for that trial. The standing +build-mode authorization supplies the reversible-change authority; this is not +an independent owner approval, autopilot promotion, or credential-isolation proof. + +## Composition and scope + +The proposal enumerates 22 existing repositories, their eight original hook-file +hashes, the exact running pod/image, and two admitted Linux executables. Only an +additional `pre-receive.d/10-custodian-secret-scan` is retained. Generated Forgejo +dispatchers and authorization hooks remain byte-for-byte unchanged. Tool storage +is `/data/security/receive-secret-scan/20261012-57a0642b6251`, on the existing +persistent data volume. No user-authored arbitrary hook facility is enabled. + +Metadata reconciliation found 165 repositories; the other 143 are uncovered, +not exempt. Actions enabled and branch-protection metadata do not prove required +CI checks, credential scope, or frontend lifecycle enforcement. + +## Execution and rollback + +From the Custodian checkout, default invocation performs read-only preflight: + +```sh +python3 docs/changes/CUST-WP-0080/activate_receive_hooks.py \ + --proposal docs/changes/CUST-WP-0080/receive-hook-proposal.json \ + --receipt docs/evidence/2026-10-12-netkingdom-security/receive-hook-activation.json +``` + +`--apply` performs the exact mutation and writes its receipt; do not reapply to +an already installed profile. On failure after hook writes, automatic rollback +removes only the known added hook files and verifies the original inventory. +`--rollback` performs that same removal explicitly and does not require temporary +build artifacts. It retains the admitted tools as inert files. Explicit rollback +still verifies the pinned live target and health; if the pod has changed or health +is failing, the abort owner must review the current target before recovery. Never +edit the original proposal to disguise a changed execution revision. + +Unknown hook modifications cause rollback refusal rather than deletion. Inspect +the receipt and exact path hashes; preserve evidence and resolve the concurrent +change with the forge owner. Temporary staging cleanup requires a matching owner +marker. No existing repository data, config, ref, account, or credential is removed +by ordinary rollback. + +## Verification boundaries + +The isolated native probe tests clean acceptance, intermediate-history detection, +scanner unavailability, quarantine discard and recovery using the exact deployed +image and binaries. Its Forgejo authorization hook is stubbed and the receipt +explicitly says so. + +Activation uses the actual SSH transport and existing authorization hooks. A +separate, directly tested temporary hook vetoes the exact synthetic probe ref on +all 22 targets. Both clean and disposable-secret probes must therefore fail the +push, while the scanner must separately report `clean` and `blocked`. Ref absence +and quarantined-commit absence are checked. The temporary veto is removed only +after all probes pass. No synthetic key is valid for authentication or retained in +Git. Native probe logs and fixture material are not published. + +These forced rejections alone do not prove normal clean acceptance. The next +ordinary authorized source push supplies that evidence, scoped to its repository +and commit. HTTP pushes, frontend edits/merges/imports, restart/hook regeneration, +new-repository reconciliation, historical/LFS/log/artifact coverage, and governed +finding-to-response acceptance remain open in the owner plan. + +The installer rehearsal uses local stand-ins for transport and scanner, proving +installation/rollback behavior only. It covers partial installation recovery, +missing build artifacts during rollback, executable mode checks, and preservation +of unknown changes. It is not a substitute for either native receipt. diff --git a/docs/changes/CUST-WP-0080/receive-hook-proposal.json b/docs/changes/CUST-WP-0080/receive-hook-proposal.json new file mode 100644 index 00000000..82a0a706 --- /dev/null +++ b/docs/changes/CUST-WP-0080/receive-hook-proposal.json @@ -0,0 +1,92 @@ +{ + "pod": "forgejo-gitea-7d94988c79-8k76l", + "pod_uid": "bc771b51-e9b8-456d-b59c-e7041704389d", + "image": "code.forgejo.org/forgejo/forgejo:11.0.3-rootless", + "image_id": "code.forgejo.org/forgejo/forgejo@sha256:691657c7e64af7730a2e885e6d022cf16ae60724931c5392bd61838afadcaeba", + "existing_dispatcher": "/data/git/gitea-repositories/coulomb/the-custodian.git/hooks/pre-receive", + "dispatcher_sha256": "38f2197d49b27f315e3487664802e56bc888d4afaec9e30b3bcc6bd461554aef", + "proposal_id": "FORGE6-RECEIVE-ACTIVATION-001", + "revision": 1, + "directory_name": "custodian-receive-native-activation-20261012-001", + "executor_sha256": "9b01ee383e16463981112226e19156b0de59af2d03dd71b9403e144e280a9fb3", + "native_helper_sha256": "c9120122c6f6f0b7c41fe4fa975e5a2a340e776885d852b861b8c13b7664adcd", + "repositories": [ + "access-engine", + "approval-engine", + "audit-core", + "gate-house", + "glas-harness", + "key-cape", + "kings-guard", + "maturity-engine", + "net-kingdom", + "ops-mason", + "ops-warden", + "policy-nexus", + "railiance-forge", + "railiance-platform", + "repo-manager", + "role-engine", + "secrets-engine", + "security-canon", + "tenant-engine", + "the-custodian", + "user-engine", + "zone-engine" + ], + "existing_hook_hashes": { + "proc-receive.d/gitea": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "post-receive.d/gitea": "a3a87fd152133783f14859bfa738eb3fcc38d3090147ad8df81724ed62fa97a5", + "pre-receive": "38f2197d49b27f315e3487664802e56bc888d4afaec9e30b3bcc6bd461554aef", + "proc-receive": "ca4eb127414b2c85164b9366a3311218ed7b5a2f6cac93c2d654da97e1ba4748", + "update": "a53caf562b0cdd47b08d20c27c63059ba41cd410c3264a61cda1a531f1810ae6", + "update.d/gitea": "d32f9f382dc1511c93e9e2ce61a60c068ac83a933376368b474faad1399be79f", + "post-receive": "38f2197d49b27f315e3487664802e56bc888d4afaec9e30b3bcc6bd461554aef", + "pre-receive.d/gitea": "2572c08b2aa9ec9db475f03e6ed43bf64d18f64f9a2364c5341ad277e31149fc" + }, + "hook_name": "10-custodian-secret-scan", + "veto_name": "99-custodian-g07-probe-veto", + "hook_sha256": "b1d7cf8cd13feafe44e45876b1de3fdd9c88339d8d45791fa2b9f72ca832932b", + "veto_sha256": "a1c4b14f405fe687f32a36158d70fef8d6602cd0df0dad17a58edf48e67bc47b", + "version_directory": "/data/security/receive-secret-scan/20261012-57a0642b6251", + "probe_ref": "refs/heads/custodian-g07-probe-20261012-001", + "fixture_key": "/tmp/netkingdom-forge-admission/local-probe/fixture-key", + "fixture_sha256": "7f3c3a80af30e41ab13a7ce124e58b3863186a3fd68e3a9f78a274cf4de2d108", + "artifacts": [ + { + "name": "receive-secret-scan", + "source": "/tmp/netkingdom-forge-admission/local-probe/receive-secret-scan", + "sha256": "57a0642b625105440fe146fceb4d244fd2dd08a75beb316831675643e6cc27ec", + "mode": 493 + }, + { + "name": "gitleaks", + "source": "/tmp/netkingdom-forge-admission/local-probe/gitleaks", + "sha256": "88f91962aa2f93ac6ab281d553b9e125f5197bbbce38f9f2437f7299c32e5509", + "mode": 493 + }, + { + "name": "profile-hook", + "source": "/home/worsch/the-custodian/docs/changes/CUST-WP-0080/receive-profile-hook", + "sha256": "b1d7cf8cd13feafe44e45876b1de3fdd9c88339d8d45791fa2b9f72ca832932b", + "mode": 493 + }, + { + "name": "probe-veto", + "source": "/home/worsch/the-custodian/docs/changes/CUST-WP-0080/receive-probe-veto", + "sha256": "a1c4b14f405fe687f32a36158d70fef8d6602cd0df0dad17a58edf48e67bc47b", + "mode": 493 + } + ], + "lane": "blue", + "authorization_ref": "canon/standards/build-mode-change-authorization_v0.1.md", + "diff_ref": "docs/changes/CUST-WP-0080/activate_receive_hooks.py; receive-profile-hook; receive-probe-veto; exact hashes in this proposal", + "rollback": "Run the same executor with this proposal and --rollback. Removes only the two additive files after exact hash verification; verifies original eight-file inventory on all 22 repositories. Durable admitted binaries remain inert. Automatic rollback on failed activation verification.", + "abort_owner": "railiance-forge; executing custodian-codex", + "receipt_path": "docs/evidence/2026-10-12-netkingdom-security/receive-hook-activation.json", + "expected_result": "22 existing upstream dispatchers gain independent pinned secret scanner; SSH clean/blocked results observed under independently tested temporary veto, rejected probe refs and quarantines absent, veto removed, existing hooks preserved and health passing. Subsequent authorized ordinary source push separately proves clean acceptance.", + "cost_estimate_eur": 0, + "risk_estimate": "Existing infrastructure, approximately 27 MiB durable storage and transient transfer copy, sequential small probes. Possible false refusals or bounded push latency on 22 repositories until rollback. No new spend/resources or credential scope; no restart. Temporary exact-ref veto prevents synthetic fixture publication. Scanner bounds: 90 seconds, 2000 objects, 8 MiB each, 64 MiB aggregate; larger pushes require reviewed remediation rather than bypass.", + "verification_limit": "Initial reviewed 22 of 165 repositories only; HTTP/frontend/new-repository/restart/reconciliation and response workflow remain open. No historic content scan, live credential, or synthetic public ref publication authorized.", + "promotion_evidence": false +} diff --git a/docs/changes/CUST-WP-0080/receive-native-proposal.json b/docs/changes/CUST-WP-0080/receive-native-proposal.json new file mode 100644 index 00000000..504ed3a1 --- /dev/null +++ b/docs/changes/CUST-WP-0080/receive-native-proposal.json @@ -0,0 +1,65 @@ +{ + "proposal_id": "FORGE6-NATIVE-PROBE-001", + "revision": 1, + "pod": "forgejo-gitea-7d94988c79-8k76l", + "pod_uid": "bc771b51-e9b8-456d-b59c-e7041704389d", + "image": "code.forgejo.org/forgejo/forgejo:11.0.3-rootless", + "image_id": "code.forgejo.org/forgejo/forgejo@sha256:691657c7e64af7730a2e885e6d022cf16ae60724931c5392bd61838afadcaeba", + "directory_name": "custodian-receive-native-20261012-001", + "executor_sha256": "c9120122c6f6f0b7c41fe4fa975e5a2a340e776885d852b861b8c13b7664adcd", + "artifacts": [ + { + "name": "receive-secret-scan", + "source": "/tmp/netkingdom-forge-admission/local-probe/receive-secret-scan", + "sha256": "57a0642b625105440fe146fceb4d244fd2dd08a75beb316831675643e6cc27ec", + "mode": 493 + }, + { + "name": "gitleaks", + "source": "/tmp/netkingdom-forge-admission/local-probe/gitleaks", + "sha256": "88f91962aa2f93ac6ab281d553b9e125f5197bbbce38f9f2437f7299c32e5509", + "mode": 493 + }, + { + "name": "probe.sh", + "source": "/tmp/netkingdom-forge-admission/local-probe/probe.sh", + "sha256": "0fcd8a582c7545bbc417310fbef9fc6eeb3e592db176cbcff7e19a58ee97963c", + "mode": 384 + }, + { + "name": "fixture-key", + "source": "/tmp/netkingdom-forge-admission/local-probe/fixture-key", + "sha256": "7f3c3a80af30e41ab13a7ce124e58b3863186a3fd68e3a9f78a274cf4de2d108", + "mode": 384 + }, + { + "name": "pre-receive.fixture", + "source": "/tmp/netkingdom-forge-admission/local-probe/pre-receive.fixture", + "sha256": "38f2197d49b27f315e3487664802e56bc888d4afaec9e30b3bcc6bd461554aef", + "mode": 384 + } + ], + "existing_dispatcher": "/data/git/gitea-repositories/coulomb/the-custodian.git/hooks/pre-receive", + "dispatcher_sha256": "38f2197d49b27f315e3487664802e56bc888d4afaec9e30b3bcc6bd461554aef", + "lane": "blue", + "authorization_ref": "canon/standards/build-mode-change-authorization_v0.1.md", + "diff_ref": "docs/changes/CUST-WP-0080/verify_receive_native.py and probe_receive_native.sh; locally rehearsed with exact artifact hashes", + "rollback": "Remove only the new /tmp/custodian-receive-native-20261012-001 directory after matching .probe-owner. No existing repository/hook/config changes. Executor finally performs cleanup on success/failure.", + "abort_owner": "railiance-forge; executing custodian-codex", + "receipt_path": "docs/evidence/2026-10-12-netkingdom-security/receive-native-probe.json", + "expected_result": "Exact candidate binary on deployed image/Git accepts clean temporary pushes, rejects intermediate synthetic private key, discards quarantine, refuses unavailable scanner, recovers clean acceptance, retains dispatcher/additional-hook behavior; health remains passing and temporary area removed.", + "verification_limit": "Builtin Forgejo authorization is a stub in an isolated unregistered bare repository. Does not prove installed SSH/HTTP/frontend/new-repository production enforcement.", + "cost_estimate_eur": 0, + "risk_estimate": "Temporary approximately 27 MiB artifact storage plus small disposable Git fixtures and brief CPU use on the existing container; no new resource allocation/spend, no real credentials or production repository/config writes. Preflight requires 256 MiB free temporary space and passing health; execution has timeout and owned-path cleanup.", + "local_rehearsal": { + "clean_push": true, + "intermediate_secret_refused": true, + "quarantine_discarded": true, + "scanner_unavailable_refused": true, + "clean_after_recovery": true, + "dispatcher_preserved": true, + "other_hook_invocations": 4, + "builtin_authorization_stubbed": true + }, + "promotion_evidence": false +} diff --git a/docs/changes/CUST-WP-0080/receive-probe-veto b/docs/changes/CUST-WP-0080/receive-probe-veto new file mode 100644 index 00000000..96bf5ac8 --- /dev/null +++ b/docs/changes/CUST-WP-0080/receive-probe-veto @@ -0,0 +1,11 @@ +#!/bin/sh +set +vx +set -eu +blocked=0 +while read -r old new ref; do + if [ "$ref" = "refs/heads/custodian-g07-probe-20261012-001" ]; then blocked=1; fi +done +if [ "$blocked" = 1 ]; then + echo CUSTODIAN_G07_PROBE_VETO >&2 + exit 1 +fi diff --git a/docs/changes/CUST-WP-0080/receive-profile-hook b/docs/changes/CUST-WP-0080/receive-profile-hook new file mode 100644 index 00000000..0714e490 --- /dev/null +++ b/docs/changes/CUST-WP-0080/receive-profile-hook @@ -0,0 +1,4 @@ +#!/bin/sh +set +vx +set -eu +exec /data/security/receive-secret-scan/20261012-57a0642b6251/receive-secret-scan --scanner /data/security/receive-secret-scan/20261012-57a0642b6251/gitleaks diff --git a/docs/changes/CUST-WP-0080/test_receive_activation.py b/docs/changes/CUST-WP-0080/test_receive_activation.py new file mode 100644 index 00000000..fcf72c8a --- /dev/null +++ b/docs/changes/CUST-WP-0080/test_receive_activation.py @@ -0,0 +1,81 @@ +import copy +import importlib.util +import io +import json +from pathlib import Path +import subprocess +import sys +import tarfile +import tempfile +import unittest +from unittest.mock import patch + +SOURCE=Path(__file__).with_name('activate_receive_hooks.py').resolve() +spec=importlib.util.spec_from_file_location('activation',SOURCE) +a=importlib.util.module_from_spec(spec);spec.loader.exec_module(a) +BASE=json.loads(SOURCE.with_name('receive-hook-proposal.json').read_text()) + +class Native: + def __init__(self, root, proposal, fail=False): self.root=root;self.p=proposal;self.fail=fail + def preflight(self,p): + for x in p['artifacts']: + if not Path(x['source']).is_file(): raise ValueError('missing-artifact') + return {'rehearsal':True} + def archive(self,p): + out=io.BytesIO();checks=[] + with tarfile.open(fileobj=out,mode='w') as t: + for item in p['artifacts']: + content=Path(item['source']).read_bytes();i=tarfile.TarInfo(item['name']);i.size=len(content);i.mode=item['mode'];t.addfile(i,io.BytesIO(content));checks.append(item['sha256']+' '+item['name']+'\n') + content=''.join(checks).encode();i=tarfile.TarInfo('SHA256SUMS');i.size=len(content);t.addfile(i,io.BytesIO(content)) + return out.getvalue() + def execute(self,p,*args,payload=None,timeout=45): + if args[0]=='wget': return b'' + mapped=[x.replace('/data/',str(self.root/'data')+'/').replace('/tmp/'+p['directory_name'],str(self.root/'stage')) for x in args] + if self.fail and args[0]=='bash' and 'mktemp' in args[-1]: + self.fail=False + path=self.root/'data/git/gitea-repositories/coulomb'/ (p['repositories'][0]+'.git/hooks/pre-receive.d') + for field,source in [('hook_name','receive-profile-hook'),('veto_name','receive-probe-veto')]: + dest=path/p[field];dest.write_bytes(SOURCE.with_name(source).read_bytes());dest.chmod(0o755) + raise ValueError('injected-partial-install-failure') + r=subprocess.run(mapped,input=payload,capture_output=True,timeout=timeout) + if r.returncode: raise ValueError('local-command-refused') + return r.stdout.replace((str(self.root/'data')+'/').encode(),b'/data/') + +class Tests(unittest.TestCase): + def setUp(self): + self.tmp=tempfile.TemporaryDirectory();self.root=Path(self.tmp.name);self.p=copy.deepcopy(BASE) + # Small inert tools are sufficient for installer/rollback protocol testing. + binary=self.root/'tool';binary.write_text('#!/bin/sh\nexit 0\n') + for item in self.p['artifacts'][:2]: item.update(source=str(binary),sha256=a.sha(binary)) + self.p['existing_hook_hashes']={k:a.hashlib.sha256(b'#!/bin/sh\nexit 0\n').hexdigest() for k in self.p['existing_hook_hashes']} + for name in self.p['repositories']: + repo=self.root/'data/git/gitea-repositories/coulomb'/(name+'.git') + subprocess.run(['git','init','--bare','-q',str(repo)],check=True) + for rel in self.p['existing_hook_hashes']: + f=repo/'hooks'/rel;f.parent.mkdir(exist_ok=True);f.write_bytes(binary.read_bytes());f.chmod(0o755) + self.native=Native(self.root,self.p);self.proposal=self.root/'proposal.json';self.receipt=self.root/'receipt.json' + def tearDown(self):self.tmp.cleanup() + def runmain(self,*args): + self.proposal.write_text(json.dumps(self.p)) + with patch.object(a,'helper',return_value=self.native),patch.object(a,'verify_pushes',return_value=[{'rehearsal_only':True}]),patch.object(sys,'argv',['activate','--proposal',str(self.proposal),'--receipt',str(self.receipt),*args]):a.main() + def test_activation_and_rollback_without_build_files(self): + self.runmain('--apply');a.inspect(self.native,self.p,applied=True) + self.assertEqual(json.loads(self.receipt.read_text())['result'],'verified') + for x in self.p['artifacts']:x['source']='/missing/build/artifact' + self.runmain('--rollback');a.inspect(self.native,self.p) + def test_partial_install_rolls_back_all_added_hooks(self): + self.native.fail=True + with self.assertRaises(ValueError):self.runmain('--apply') + a.inspect(self.native,self.p) + r=json.loads(self.receipt.read_text());self.assertEqual(r['rollback'],'prior hook inventory restored');self.assertTrue(r['temporary_directory_removed']);self.assertEqual(r['result'],'unverified') + def test_non_executable_hook_refused(self): + self.runmain('--apply') + f=self.root/'data/git/gitea-repositories/coulomb'/(self.p['repositories'][0]+'.git/hooks/pre-receive.d')/self.p['hook_name'];f.chmod(0o644) + with self.assertRaises(ValueError):a.inspect(self.native,self.p,applied=True) + def test_rollback_preserves_unknown_modification(self): + self.runmain('--apply') + f=self.root/'data/git/gitea-repositories/coulomb'/(self.p['repositories'][0]+'.git/hooks/pre-receive.d')/self.p['hook_name'];f.write_text('unknown change\n') + with self.assertRaises(ValueError):a.rollback(self.native,self.p) + self.assertEqual(f.read_text(),'unknown change\n') + +if __name__=='__main__':unittest.main(verbosity=2) diff --git a/docs/changes/CUST-WP-0080/verify_receive_native.py b/docs/changes/CUST-WP-0080/verify_receive_native.py new file mode 100644 index 00000000..682391f9 --- /dev/null +++ b/docs/changes/CUST-WP-0080/verify_receive_native.py @@ -0,0 +1,145 @@ +#!/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.') diff --git a/docs/evidence/2026-10-12-netkingdom-security/forge-repository-compatibility.json b/docs/evidence/2026-10-12-netkingdom-security/forge-repository-compatibility.json new file mode 100644 index 00000000..5c592319 --- /dev/null +++ b/docs/evidence/2026-10-12-netkingdom-security/forge-repository-compatibility.json @@ -0,0 +1,159 @@ +{ + "observed_at": "2026-10-12T17:14:48.732230+00:00", + "repositories": [ + { + "repo": "access-engine", + "shallow": false, + "object_format": "sha1", + "partial_config_keys": 0, + "head_gitlinks": 0 + }, + { + "repo": "approval-engine", + "shallow": false, + "object_format": "sha1", + "partial_config_keys": 0, + "head_gitlinks": 0 + }, + { + "repo": "audit-core", + "shallow": false, + "object_format": "sha1", + "partial_config_keys": 0, + "head_gitlinks": 0 + }, + { + "repo": "gate-house", + "shallow": false, + "object_format": "sha1", + "partial_config_keys": 0, + "head_gitlinks": 0 + }, + { + "repo": "glas-harness", + "shallow": false, + "object_format": "sha1", + "partial_config_keys": 0, + "head_gitlinks": 0 + }, + { + "repo": "key-cape", + "shallow": false, + "object_format": "sha1", + "partial_config_keys": 0, + "head_gitlinks": 0 + }, + { + "repo": "kings-guard", + "shallow": false, + "object_format": "sha1", + "partial_config_keys": 0, + "head_gitlinks": 0 + }, + { + "repo": "maturity-engine", + "shallow": false, + "object_format": "sha1", + "partial_config_keys": 0, + "head_gitlinks": 0 + }, + { + "repo": "net-kingdom", + "shallow": false, + "object_format": "sha1", + "partial_config_keys": 0, + "head_gitlinks": 0 + }, + { + "repo": "ops-mason", + "shallow": false, + "object_format": "sha1", + "partial_config_keys": 0, + "head_gitlinks": 0 + }, + { + "repo": "ops-warden", + "shallow": false, + "object_format": "sha1", + "partial_config_keys": 0, + "head_gitlinks": 0 + }, + { + "repo": "policy-nexus", + "shallow": false, + "object_format": "sha1", + "partial_config_keys": 0, + "head_gitlinks": 0 + }, + { + "repo": "railiance-forge", + "shallow": false, + "object_format": "sha1", + "partial_config_keys": 0, + "head_gitlinks": 0 + }, + { + "repo": "railiance-platform", + "shallow": false, + "object_format": "sha1", + "partial_config_keys": 0, + "head_gitlinks": 0 + }, + { + "repo": "repo-manager", + "shallow": false, + "object_format": "sha1", + "partial_config_keys": 0, + "head_gitlinks": 0 + }, + { + "repo": "role-engine", + "shallow": false, + "object_format": "sha1", + "partial_config_keys": 0, + "head_gitlinks": 0 + }, + { + "repo": "secrets-engine", + "shallow": false, + "object_format": "sha1", + "partial_config_keys": 0, + "head_gitlinks": 0 + }, + { + "repo": "security-canon", + "shallow": false, + "object_format": "sha1", + "partial_config_keys": 0, + "head_gitlinks": 0 + }, + { + "repo": "tenant-engine", + "shallow": false, + "object_format": "sha1", + "partial_config_keys": 0, + "head_gitlinks": 0 + }, + { + "repo": "the-custodian", + "shallow": false, + "object_format": "sha1", + "partial_config_keys": 0, + "head_gitlinks": 0 + }, + { + "repo": "user-engine", + "shallow": false, + "object_format": "sha1", + "partial_config_keys": 0, + "head_gitlinks": 0 + }, + { + "repo": "zone-engine", + "shallow": false, + "object_format": "sha1", + "partial_config_keys": 0, + "head_gitlinks": 0 + } + ] +} diff --git a/docs/evidence/2026-10-12-netkingdom-security/forge-repository-inventory.json b/docs/evidence/2026-10-12-netkingdom-security/forge-repository-inventory.json new file mode 100644 index 00000000..0ff33576 --- /dev/null +++ b/docs/evidence/2026-10-12-netkingdom-security/forge-repository-inventory.json @@ -0,0 +1,188 @@ +{ + "observed_at": "2026-10-12T16:57:39.762249+00:00", + "host": "railiance01", + "pod": "forgejo-gitea-7d94988c79-8k76l", + "pod_uid": "bc771b51-e9b8-456d-b59c-e7041704389d", + "image": "code.forgejo.org/forgejo/forgejo:11.0.3-rootless", + "git_version": "git version 2.47.3", + "source_inventory_sha256": "e3ab33bd977544436532a804509cb94024abb5b0fa059a212992fb72f0eb9fac", + "counts": { + "database_repositories": 165, + "filesystem_directories": 165, + "public": 153, + "private": 12, + "archived": 0, + "mirrors": 0, + "forks": 0, + "empty": 0, + "wikis": 0, + "missing_main_directories": 0, + "repositories_with_protected_branch_rules": 0, + "actions_unit_enabled": 165 + }, + "reviewed_scope": [ + { + "repository_id": 2, + "repo": "glas-harness", + "main_directory_present": true, + "protected_branch_rules": 0, + "actions_unit_enabled": true + }, + { + "repository_id": 3, + "repo": "key-cape", + "main_directory_present": true, + "protected_branch_rules": 0, + "actions_unit_enabled": true + }, + { + "repository_id": 9, + "repo": "railiance-platform", + "main_directory_present": true, + "protected_branch_rules": 0, + "actions_unit_enabled": true + }, + { + "repository_id": 16, + "repo": "ops-warden", + "main_directory_present": true, + "protected_branch_rules": 0, + "actions_unit_enabled": true + }, + { + "repository_id": 26, + "repo": "audit-core", + "main_directory_present": true, + "protected_branch_rules": 0, + "actions_unit_enabled": true + }, + { + "repository_id": 42, + "repo": "access-engine", + "main_directory_present": true, + "protected_branch_rules": 0, + "actions_unit_enabled": true + }, + { + "repository_id": 60, + "repo": "railiance-forge", + "main_directory_present": true, + "protected_branch_rules": 0, + "actions_unit_enabled": true + }, + { + "repository_id": 67, + "repo": "net-kingdom", + "main_directory_present": true, + "protected_branch_rules": 0, + "actions_unit_enabled": true + }, + { + "repository_id": 71, + "repo": "secrets-engine", + "main_directory_present": true, + "protected_branch_rules": 0, + "actions_unit_enabled": true + }, + { + "repository_id": 76, + "repo": "the-custodian", + "main_directory_present": true, + "protected_branch_rules": 0, + "actions_unit_enabled": true + }, + { + "repository_id": 77, + "repo": "user-engine", + "main_directory_present": true, + "protected_branch_rules": 0, + "actions_unit_enabled": true + }, + { + "repository_id": 92, + "repo": "tenant-engine", + "main_directory_present": true, + "protected_branch_rules": 0, + "actions_unit_enabled": true + }, + { + "repository_id": 93, + "repo": "kings-guard", + "main_directory_present": true, + "protected_branch_rules": 0, + "actions_unit_enabled": true + }, + { + "repository_id": 102, + "repo": "ops-mason", + "main_directory_present": true, + "protected_branch_rules": 0, + "actions_unit_enabled": true + }, + { + "repository_id": 108, + "repo": "repo-manager", + "main_directory_present": true, + "protected_branch_rules": 0, + "actions_unit_enabled": true + }, + { + "repository_id": 113, + "repo": "role-engine", + "main_directory_present": true, + "protected_branch_rules": 0, + "actions_unit_enabled": true + }, + { + "repository_id": 124, + "repo": "policy-nexus", + "main_directory_present": true, + "protected_branch_rules": 0, + "actions_unit_enabled": true + }, + { + "repository_id": 131, + "repo": "zone-engine", + "main_directory_present": true, + "protected_branch_rules": 0, + "actions_unit_enabled": true + }, + { + "repository_id": 137, + "repo": "gate-house", + "main_directory_present": true, + "protected_branch_rules": 0, + "actions_unit_enabled": true + }, + { + "repository_id": 138, + "repo": "approval-engine", + "main_directory_present": true, + "protected_branch_rules": 0, + "actions_unit_enabled": true + }, + { + "repository_id": 139, + "repo": "maturity-engine", + "main_directory_present": true, + "protected_branch_rules": 0, + "actions_unit_enabled": true + }, + { + "repository_id": 157, + "repo": "security-canon", + "main_directory_present": true, + "protected_branch_rules": 0, + "actions_unit_enabled": true + } + ], + "scope_disposition": { + "reviewed_initial_scope": 22, + "outside_initial_scope_not_exempt": 143, + "production_software_denominator_admitted": false, + "no_exemptions_inferred_from_archive_or_privacy_flags": true, + "production_gate_activated": false + }, + "unit_type_source": "https://codeberg.org/forgejo/forgejo/src/tag/v11.0.3/models/unit/unit.go", + "limits": "Detailed unreviewed/private repository identities and size data remain in a local private observation; committed evidence contains aggregate counts and already-reviewed identities only. This metadata establishes no scanner enforcement or complete production denominator." +} diff --git a/docs/evidence/2026-10-12-netkingdom-security/receive-activation-rehearsal.json b/docs/evidence/2026-10-12-netkingdom-security/receive-activation-rehearsal.json new file mode 100644 index 00000000..fe9459d8 --- /dev/null +++ b/docs/evidence/2026-10-12-netkingdom-security/receive-activation-rehearsal.json @@ -0,0 +1,21 @@ +{ + "scope": "installer rehearsal only; native transport and scanner replaced with local harness/inert binaries", + "tests_passed": 4, + "tests": [ + "activation and rollback after build artifacts removed", + "partial installation failure restores original inventory", + "non-executable installed hook refused", + "unknown hook modification preserved during rollback" + ], + "duration_seconds": 24.89, + "executor_sha256": "9b01ee383e16463981112226e19156b0de59af2d03dd71b9403e144e280a9fb3", + "test_source": "docs/changes/CUST-WP-0080/test_receive_activation.py", + "proposal_sha256": "9c8ea3b7fbe16a997359432af7bcf09f429881fd23cc567188c21129e80ccf8d", + "live_preflight": { + "result": "verified", + "repositories": 22, + "pod_uid": "bc771b51-e9b8-456d-b59c-e7041704389d", + "image_id": "code.forgejo.org/forgejo/forgejo@sha256:691657c7e64af7730a2e885e6d022cf16ae60724931c5392bd61838afadcaeba", + "existing_hooks_unchanged": true + } +} diff --git a/docs/evidence/2026-10-12-netkingdom-security/receive-hook-activation.json b/docs/evidence/2026-10-12-netkingdom-security/receive-hook-activation.json new file mode 100644 index 00000000..c550a1aa --- /dev/null +++ b/docs/evidence/2026-10-12-netkingdom-security/receive-hook-activation.json @@ -0,0 +1,197 @@ +{ + "proposal_id": "FORGE6-RECEIVE-ACTIVATION-001", + "proposal_sha256": "9c8ea3b7fbe16a997359432af7bcf09f429881fd23cc567188c21129e80ccf8d", + "result": "verified", + "observation": { + "pod_uid": "bc771b51-e9b8-456d-b59c-e7041704389d", + "image_id": "code.forgejo.org/forgejo/forgejo@sha256:691657c7e64af7730a2e885e6d022cf16ae60724931c5392bd61838afadcaeba", + "git_version": "git version 2.47.3", + "free_tmp_kib": 115227300, + "health_before": true + }, + "phase": "active", + "no_live_credentials_used": true, + "executed_revision": 1, + "repositories": [ + { + "repo": "access-engine", + "clean_scanner_result": "clean", + "blocked_scanner_result": "blocked", + "temporary_veto_observed": true, + "probe_ref_absent": true, + "quarantined_commit_absent": true + }, + { + "repo": "approval-engine", + "clean_scanner_result": "clean", + "blocked_scanner_result": "blocked", + "temporary_veto_observed": true, + "probe_ref_absent": true, + "quarantined_commit_absent": true + }, + { + "repo": "audit-core", + "clean_scanner_result": "clean", + "blocked_scanner_result": "blocked", + "temporary_veto_observed": true, + "probe_ref_absent": true, + "quarantined_commit_absent": true + }, + { + "repo": "gate-house", + "clean_scanner_result": "clean", + "blocked_scanner_result": "blocked", + "temporary_veto_observed": true, + "probe_ref_absent": true, + "quarantined_commit_absent": true + }, + { + "repo": "glas-harness", + "clean_scanner_result": "clean", + "blocked_scanner_result": "blocked", + "temporary_veto_observed": true, + "probe_ref_absent": true, + "quarantined_commit_absent": true + }, + { + "repo": "key-cape", + "clean_scanner_result": "clean", + "blocked_scanner_result": "blocked", + "temporary_veto_observed": true, + "probe_ref_absent": true, + "quarantined_commit_absent": true + }, + { + "repo": "kings-guard", + "clean_scanner_result": "clean", + "blocked_scanner_result": "blocked", + "temporary_veto_observed": true, + "probe_ref_absent": true, + "quarantined_commit_absent": true + }, + { + "repo": "maturity-engine", + "clean_scanner_result": "clean", + "blocked_scanner_result": "blocked", + "temporary_veto_observed": true, + "probe_ref_absent": true, + "quarantined_commit_absent": true + }, + { + "repo": "net-kingdom", + "clean_scanner_result": "clean", + "blocked_scanner_result": "blocked", + "temporary_veto_observed": true, + "probe_ref_absent": true, + "quarantined_commit_absent": true + }, + { + "repo": "ops-mason", + "clean_scanner_result": "clean", + "blocked_scanner_result": "blocked", + "temporary_veto_observed": true, + "probe_ref_absent": true, + "quarantined_commit_absent": true + }, + { + "repo": "ops-warden", + "clean_scanner_result": "clean", + "blocked_scanner_result": "blocked", + "temporary_veto_observed": true, + "probe_ref_absent": true, + "quarantined_commit_absent": true + }, + { + "repo": "policy-nexus", + "clean_scanner_result": "clean", + "blocked_scanner_result": "blocked", + "temporary_veto_observed": true, + "probe_ref_absent": true, + "quarantined_commit_absent": true + }, + { + "repo": "railiance-forge", + "clean_scanner_result": "clean", + "blocked_scanner_result": "blocked", + "temporary_veto_observed": true, + "probe_ref_absent": true, + "quarantined_commit_absent": true + }, + { + "repo": "railiance-platform", + "clean_scanner_result": "clean", + "blocked_scanner_result": "blocked", + "temporary_veto_observed": true, + "probe_ref_absent": true, + "quarantined_commit_absent": true + }, + { + "repo": "repo-manager", + "clean_scanner_result": "clean", + "blocked_scanner_result": "blocked", + "temporary_veto_observed": true, + "probe_ref_absent": true, + "quarantined_commit_absent": true + }, + { + "repo": "role-engine", + "clean_scanner_result": "clean", + "blocked_scanner_result": "blocked", + "temporary_veto_observed": true, + "probe_ref_absent": true, + "quarantined_commit_absent": true + }, + { + "repo": "secrets-engine", + "clean_scanner_result": "clean", + "blocked_scanner_result": "blocked", + "temporary_veto_observed": true, + "probe_ref_absent": true, + "quarantined_commit_absent": true + }, + { + "repo": "security-canon", + "clean_scanner_result": "clean", + "blocked_scanner_result": "blocked", + "temporary_veto_observed": true, + "probe_ref_absent": true, + "quarantined_commit_absent": true + }, + { + "repo": "tenant-engine", + "clean_scanner_result": "clean", + "blocked_scanner_result": "blocked", + "temporary_veto_observed": true, + "probe_ref_absent": true, + "quarantined_commit_absent": true + }, + { + "repo": "the-custodian", + "clean_scanner_result": "clean", + "blocked_scanner_result": "blocked", + "temporary_veto_observed": true, + "probe_ref_absent": true, + "quarantined_commit_absent": true + }, + { + "repo": "user-engine", + "clean_scanner_result": "clean", + "blocked_scanner_result": "blocked", + "temporary_veto_observed": true, + "probe_ref_absent": true, + "quarantined_commit_absent": true + }, + { + "repo": "zone-engine", + "clean_scanner_result": "clean", + "blocked_scanner_result": "blocked", + "temporary_veto_observed": true, + "probe_ref_absent": true, + "quarantined_commit_absent": true + } + ], + "temporary_veto_removed": true, + "health_after": true, + "verification_limit": "Synthetic pushes deliberately vetoed; scanner clean/detection results and quarantine absence verified. Normal clean acceptance needs the subsequent authorized source push; HTTP/frontend/lifecycle/outside-scope remain separate.", + "temporary_directory_removed": true +} diff --git a/docs/evidence/2026-10-12-netkingdom-security/receive-native-probe.json b/docs/evidence/2026-10-12-netkingdom-security/receive-native-probe.json new file mode 100644 index 00000000..21a26454 --- /dev/null +++ b/docs/evidence/2026-10-12-netkingdom-security/receive-native-probe.json @@ -0,0 +1,28 @@ +{ + "proposal_id": "FORGE6-NATIVE-PROBE-001", + "proposal_sha256": "b6fe6f55354f7f09837b94f4d75e79f39ac0a4b868f7ac377e2ca6590e340b42", + "executed_revision": 1, + "result": "verified", + "observation": { + "pod_uid": "bc771b51-e9b8-456d-b59c-e7041704389d", + "image_id": "code.forgejo.org/forgejo/forgejo@sha256:691657c7e64af7730a2e885e6d022cf16ae60724931c5392bd61838afadcaeba", + "git_version": "git version 2.47.3", + "free_tmp_kib": 115282740, + "health_before": true + }, + "scope": "exact binaries in disposable bare repositories inside existing Forgejo image; builtin auth stubbed", + "production_repositories_changed": false, + "live_credentials_used": false, + "verification": { + "clean_push": true, + "intermediate_secret_refused": true, + "quarantine_discarded": true, + "scanner_unavailable_refused": true, + "clean_after_recovery": true, + "dispatcher_preserved": true, + "other_hook_invocations": 4, + "builtin_authorization_stubbed": true + }, + "health_after": true, + "temporary_directory_removed": true +} diff --git a/docs/evidence/2026-10-13-netkingdom-security/receive-profile-recheck.json b/docs/evidence/2026-10-13-netkingdom-security/receive-profile-recheck.json new file mode 100644 index 00000000..b963e5c5 --- /dev/null +++ b/docs/evidence/2026-10-13-netkingdom-security/receive-profile-recheck.json @@ -0,0 +1,53 @@ +{ + "observed_at": "2026-10-13T08:47:08.601228+00:00", + "result": "verified", + "scope": "read-only deployed profile recheck; no push or mutation", + "proposal_id": "FORGE6-RECEIVE-ACTIVATION-001", + "proposal_sha256": "9c8ea3b7fbe16a997359432af7bcf09f429881fd23cc567188c21129e80ccf8d", + "observation": { + "pod_uid": "bc771b51-e9b8-456d-b59c-e7041704389d", + "image_id": "code.forgejo.org/forgejo/forgejo@sha256:691657c7e64af7730a2e885e6d022cf16ae60724931c5392bd61838afadcaeba", + "git_version": "git version 2.47.3", + "free_tmp_kib": 114612720, + "health_before": true + }, + "repositories": [ + "access-engine", + "approval-engine", + "audit-core", + "gate-house", + "glas-harness", + "key-cape", + "kings-guard", + "maturity-engine", + "net-kingdom", + "ops-mason", + "ops-warden", + "policy-nexus", + "railiance-forge", + "railiance-platform", + "repo-manager", + "role-engine", + "secrets-engine", + "security-canon", + "tenant-engine", + "the-custodian", + "user-engine", + "zone-engine" + ], + "original_hooks_preserved": true, + "installed_hooks_match": true, + "temporary_veto_absent": true, + "artifacts": [ + { + "name": "receive-secret-scan", + "sha256": "57a0642b625105440fe146fceb4d244fd2dd08a75beb316831675643e6cc27ec", + "executable": true + }, + { + "name": "gitleaks", + "sha256": "88f91962aa2f93ac6ab281d553b9e125f5197bbbce38f9f2437f7299c32e5509", + "executable": true + } + ] +} diff --git a/docs/netkingdom-security-gaps.md b/docs/netkingdom-security-gaps.md index 4cb2eab1..8faa0d11 100644 --- a/docs/netkingdom-security-gaps.md +++ b/docs/netkingdom-security-gaps.md @@ -1,6 +1,6 @@ # NetKingdom security gaps and closure sequence -Assessment date: 2026-10-12. [Blueprint](netkingdom-security-blueprint.md). +Assessment date: 2026-10-12; activation rechecked 2026-10-13. [Blueprint](netkingdom-security-blueprint.md). Coordinator: CUST-WP-0080. Existing owner work remains authoritative. Statuses below distinguish actual defects, incomplete integration and missing verification. No whole-estate SEC-DSM conformance or maturity level is claimed. @@ -15,12 +15,12 @@ No whole-estate SEC-DSM conformance or maturity level is claimed. | G04 | Credential inventory is incomplete and its five metadata fields do not by themselves establish full SEC-DSM lifecycle coverage. Mason's dated survey found 49 paths; coverage is not current estate-wide discovery. | MASON-WP-0004-T03/T04; RPF-WP-0035; CUST-WP-0080-T03 | Current metadata-only discovery denominator; owners/consumers/scopes; rotation and revocation procedures; unknown entries remain findings | | G05 | Agent effective authority and credential isolation are unproven. Glas declares no ready profile; the Custodian record grants no autopilot authority. | CUST-WP-0073-T02/T04; GLAS-WP-0012 | Actual admitted agent identity, runtime read-denial and tool-effect restrictions, task expiry/cancel cleanup, no cross-task credential accumulation | | G06 | Native evidence delivery, source completeness and attribution remain incomplete. Independent archive custody is not source completeness. | AUDIT-WP-0013/0014/0015; RPF-WP-0035-T08; existing FLEX-WP-0031 and NK-WP-0035/0040 residual records | Admitted sender, durable outbox, native archive readback, heartbeat/reconciliation and failure recovery. Residual work requires explicit promotion | -| G07 | The original generated-only hook accepted a disposable private-key commit. The pinned scanner is now enforced by actual configured hooks on 22 exact workstation checkouts; clean, synthetic-secret, generated-only and unavailable-scanner probes pass. Independent forge/CI prevention and response remain unverified. | RMGR-WP-0021-T01/T02 source and local activation done; T03 forge/CI via FORGE-WP-0006, T04 response open; CUST-WP-0080-T03 coordinates railiance-forge ownership | Deterministic production-repo inventory; actual prevention/detection runs; scanner findings kept private; finding-to-revocation procedure | +| G07 | The original generated-only hook accepted a disposable private-key commit. The pinned scanner is now enforced by actual configured hooks on 22 exact workstation checkouts; clean, synthetic-secret, generated-only and unavailable-scanner probes pass. The additive forge scanner is installed on the same 22 named repositories and SSH refusal probes passed; ordinary acceptance, other write paths, broader forge/CI coverage and response remain unverified. | RMGR-WP-0021-T01/T02 source and local activation done; T03 forge/CI via FORGE-WP-0006, T04 response open; CUST-WP-0080-T03 coordinates railiance-forge ownership | Deterministic production-repo inventory; actual prevention/detection runs; scanner findings kept private; finding-to-revocation procedure | | G08 | Legacy root export and generation-prefix disclosure reproduced and removed in source. Bootstrap now uses a registered public recipient, explicit temporary-key resume and an encrypted handoff bound to the bundle digest. Other custody callers and native recovery acceptance remain unverified. | NK-WP-0049 T01 repaired; T02 caller reconciliation; T03 native recovery. NK-WP-0045 covers only the non-secret root-grant record; CUST-WP-0073 retains isolation/incident work | Inspect actual bootstrap callers; stop secret output reaching model/logs; owner-reviewed safe recovery procedure and tests | | G09 | Rotation and revocation closure are incomplete: existing exposure rotation is founder-deferred, npm migration remains open, and five-field descriptions do not prove consumer adoption. | CUST-WP-0073-T05; SECRETS-WP-0006-T06; WARDEN-WP-0037-T03; RPF-WP-0035-T07 | For each relevant credential: new consumer accepted, old credential refused, copies reconciled; explicit DSM exception if still deferred | | G10 | Build-mode exceptions and close-down checklist do not uniformly contain DSM-EXC requirement, risk, compensating controls and review fields. | CUST-WP-0080-T03; existing security-close-down checklist and October 11 decisions | Requirement-level exception records with real approval references and revalidation; no invented approval or retroactive conformance | | G11 | Root recovery, independent custody and single-node resilience require scoped native proof. Old successful source preparation is insufficient. | WARDEN-WP-0027-T02; platform recovery records; RPF-WP-0015 residual | Fresh authorized scenario, off-host encrypted backup, independent recovery access, verified restore/return; honor build-mode single-holder decision | -| G12 | CI credential authority, Kubernetes delivery-copy lifecycle and at-rest protection lack complete estate coverage in this review. | RPF-WP-0036/0044; railiance-forge; existing CCRs; CUST-WP-0080-T03 | Pipeline writer scope and job identity, per-store namespace/path isolation, refresh/deletion semantics, consumer adoption, independently governed encryption keys | +| G12 | CI credential authority, Kubernetes delivery-copy lifecycle and at-rest protection lack complete estate coverage. Forge metadata shows Actions enabled on 165 repositories and zero protected-branch rules; this does not establish workflow execution or required checks. | RPF-WP-0036/0044; railiance-forge; existing CCRs; CUST-WP-0080-T03 | Pipeline writer scope and job identity, per-store namespace/path isolation, refresh/deletion semantics, consumer adoption, independently governed encryption keys | | G13 | PDP authenticity is separate from digest binding. FLEX-WP-0024 implements optional signatures; Secrets Engine still lacks independently anchored verification. Its reproduced redirect/proxy credential forwarding defects are now fixed in source (675 tests pass, one skipped). Native signer/consumer acceptance remains open. | SECRETS-WP-0014 T01 done; T02/T03 own trust admission, signed consumer enforcement and native verification. CUST-WP-0080-T03 tracks estate coverage | Independently trusted key or authenticated channel, configured producer, consumer verification, forged/altered/unknown-key/unsigned-downgrade refusals and key-rotation evidence | G01 source repair is deployed; authenticated preview/U08 remains open. G02 and G03 require existing owner @@ -103,12 +103,14 @@ acceptance remain open. [Activation receipt](evidence/2026-10-12-netkingdom-secu All four DSM-DETECT requirements now have incomplete assessments rather than unverified placeholders. [Receipt and exact hook inventory](evidence/2026-10-12-netkingdom-security/secret-detection.json). -G07 server baseline: the 22 reviewed repositories on railiance01 have only +G07 server baseline before activation: the 22 reviewed repositories on railiance01 had only upstream-generated receive hooks, verified by matching all observed hashes to [Forgejo v11.0.3 templates](https://codeberg.org/forgejo/forgejo/src/tag/v11.0.3/modules/repository/hooks.go). -No custom scanner hook is installed at that boundary. The 165 repository -directories are a discovery count, not a reviewed production denominator. -Actions/branch-protection and actual push refusal are still unverified. +At that observation no custom scanner hook was installed. The subsequent metadata reconciliation matches 165 database repositories to 165 +directories. All have Actions enabled and none has a protected-branch rule. The +143 outside the initial reviewed scope remain uncovered, not exempt; enabled +Actions does not prove executed or required checks. Production admission remains +incomplete. [Metadata reconciliation](evidence/2026-10-12-netkingdom-security/forge-repository-inventory.json). [Read-only inventory](evidence/2026-10-12-netkingdom-security/forge-secret-detection-baseline.json). G07 introduced-history source: FORGE-WP-0006 adds a standalone forge-owned @@ -120,6 +122,18 @@ payload scanning. No production hook is activated by this source work. Exact-binary/runtime admission, full inventory and forge frontend/transport acceptance remain open. [Source and protocol receipt](evidence/2026-10-12-netkingdom-security/receive-secret-detection.json). +G07 activation and fresh recheck: the October 12 +[activation receipt](evidence/2026-10-12-netkingdom-security/receive-hook-activation.json) +records the installed exact profile on 22 repositories. Actual SSH probes yielded +clean/blocked scanner results; the temporary independent veto prevented fixture +publication, then was removed. The October 13 +[read-only recheck](evidence/2026-10-13-netkingdom-security/receive-profile-recheck.json) +confirms all expected hook hashes, preserved original hooks, executable exact +binaries and healthy service. Ordinary clean acceptance and HTTP/frontend, +restart/regeneration/new-repository coverage remain open, as do the other 143 +repositories and finding-to-response acceptance. This supersedes the pre-activation +state above without claiming complete forge or DSM-DETECT closure. + ## SEC DSM requirement coverage Each numbered requirement in these families is in scope for assessment. The diff --git a/workplans/CUST-WP-0080-netkingdom-security-blueprint-and-gap-closure.md b/workplans/CUST-WP-0080-netkingdom-security-blueprint-and-gap-closure.md index 507dc765..3e3565b0 100644 --- a/workplans/CUST-WP-0080-netkingdom-security-blueprint-and-gap-closure.md +++ b/workplans/CUST-WP-0080-netkingdom-security-blueprint-and-gap-closure.md @@ -8,7 +8,7 @@ status: active owner: codex topic_slug: custodian created: "2026-10-12" -updated: "2026-10-12" +updated: "2026-10-13" flavor: implementation depends_on: [] state_hub_workstream_id: "dfadb19b-445e-59c6-bcd6-48e87b912c52" @@ -185,6 +185,30 @@ No production hook changed. Exact-binary admission, repository denominator, SSH/HTTP/frontend/restart/new-repository coverage and response acceptance remain open in FORGE-WP-0006-T02/T03 and RMGR-WP-0021-T03/T04. G07 stays open. +2026-10-12 G07 native admission: the exact adapter/scanner binaries passed +clean/detection/quarantine/tool-failure/recovery checks in disposable repositories +inside the running Forgejo image. Builtin authorization was stubbed only in this +isolated probe; no production hook changed during that probe. Receipt: +`receive-native-probe.json`. Metadata reconciliation now matches 165 database +repositories to 165 directories; none has a protected-branch rule. The initial +22 reviewed repositories pass shallow/partial/gitlink compatibility checks; the +remaining 143 are uncovered, not exempt. `forge-repository-inventory.json` +retains aggregates and reviewed identities only. An exact additive-hook activation +proposal and tested rollback are prepared as FORGE6-RECEIVE-ACTIVATION-001 under +the existing standing blue-lane authorization; outcome is recorded separately. + +2026-10-13 interruption recovery: retained activation receipt proves that +FORGE6-RECEIVE-ACTIVATION-001 completed on October 12 at 17:32 UTC, after the +last progress note. All 22 exact Forgejo repositories gained the additive hook; +actual SSH probes produced clean/blocked scanner results under a temporary veto, +with probe refs/quarantined commits absent and the veto removed. A fresh read-only +inspection confirms the same pod/image, exact original and added hook hashes, +executable pinned binaries and healthy service. Evidence: +`receive-hook-activation.json` in the October 12 directory and +`docs/evidence/2026-10-13-netkingdom-security/receive-profile-recheck.json`. +Ordinary clean-push acceptance, HTTP/frontend/lifecycle coverage, the other 143 +repositories and private response acceptance still need proof. G07 remains open. + ## Verify native closure against every requirement ```task