security: preserve verified Forgejo scanner activation and reconcile recovery
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

This commit is contained in:
codex 2026-10-13 11:31:06 +02:00
parent 78081dabf2
commit eb93eeedfd
19 changed files with 1542 additions and 8 deletions

View file

@ -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.')

View file

@ -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))

View file

@ -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 <<STUB
#!/bin/sh
cat >/dev/null
printf 'invoked\n' >> '$base/stub-calls'
STUB
cat > server.git/hooks/pre-receive.d/secret-scan <<HOOK
#!/bin/sh
exec '$base/receive-secret-scan' --scanner '$base/gitleaks'
HOOK
chmod 755 server.git/hooks/pre-receive.d/gitea server.git/hooks/pre-receive.d/secret-scan
printf 'clean baseline\n' > 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'

View file

@ -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.

View file

@ -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
}

View file

@ -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
}

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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.')