security: recover Forgejo scanner custody and prove restart acceptance

This commit is contained in:
codex 2026-10-13 15:07:30 +02:00
parent e570433060
commit 8f770e1eea
39 changed files with 1984 additions and 12 deletions

View file

@ -0,0 +1,320 @@
#!/usr/bin/env python3
"""Exact, additive Forgejo template deployment with value-free receipts.
Default mode is read-only observation plus Kubernetes server dry-runs. No Secret
objects or Helm values are requested. Reuses existing immutable scanner artifacts.
"""
import argparse
import copy
import datetime
import hashlib
import importlib.util
import json
from pathlib import Path
import re
import subprocess
import time
HERE = Path(__file__).parent
NAME = 'forgejo-secret-template-b1d7cf8cd13f'
VOLUME = 'secret-scan-template'
SETTING = 'GITEA__GIT_0X2E_CONFIG__INIT_0X2E_TEMPLATEDIR'
TEMPLATE = '/opt/forge-secret-template'
DEFAULT = '/usr/share/git-core/templates'
MOUNT = {'name': VOLUME, 'mountPath': TEMPLATE + '/hooks/pre-receive.d/10-custodian-secret-scan',
'subPath': 'hook', 'readOnly': True}
VOL = {'name': VOLUME, 'configMap': {'name': NAME, 'defaultMode': 365}}
def digest(value):
return hashlib.sha256(json.dumps(value, sort_keys=True, separators=(',', ':')).encode()).hexdigest()
def module(name, path):
spec = importlib.util.spec_from_file_location(name, path)
obj = importlib.util.module_from_spec(spec)
spec.loader.exec_module(obj)
return obj
native = module('template_native', HERE / 'verify_receive_native.py')
activation = module('template_activation', HERE / 'activate_receive_hooks.py')
def deployment():
return json.loads(native.command(['-n', 'forgejo', 'get', 'deployment', 'forgejo-gitea', '-o', 'json']))
def target_pod(d, original):
pods = json.loads(native.command(['-n', 'forgejo', 'get', 'pods', '-l', 'app.kubernetes.io/instance=forgejo', '-o', 'json']))['items']
candidates = [p for p in pods if not p['metadata'].get('deletionTimestamp') and
any(c['name'] == 'gitea' for c in p['spec']['containers']) and
p.get('status', {}).get('phase') == 'Running' and
any(c['type'] == 'Ready' and c['status'] == 'True' for c in p.get('status', {}).get('conditions', []))]
if len(candidates) != 1:
raise ValueError('no-unique-ready-pod')
p = candidates[0]
image = next(c['image'] for c in p['spec']['containers'] if c['name'] == 'gitea')
image_id = next(c['imageID'] for c in p['status']['containerStatuses'] if c['name'] == 'gitea')
if image != original['image'] or image_id != original['image_id']:
raise ValueError('image-changed')
if d['status'].get('observedGeneration', 0) < d['metadata']['generation'] or d['status'].get('availableReplicas') != 1:
raise ValueError('deployment-not-ready')
return dict(original, pod=p['metadata']['name'], pod_uid=p['metadata']['uid'])
def patch_for(d, rollback=False):
"""Build a conditional patch; refuse conflicts and preserve all other fields."""
spec = d['spec']['template']['spec']
patch = [{'op': 'test', 'path': '/metadata/uid', 'value': d['metadata']['uid']},
{'op': 'test', 'path': '/metadata/resourceVersion', 'value': d['metadata']['resourceVersion']}]
volumes = spec['volumes']
found = [i for i, v in enumerate(volumes) if v['name'] == VOLUME]
if rollback:
if len(found) != 1 or volumes[found[0]] != VOL:
raise ValueError('rollback-volume-changed')
patch.append({'op': 'remove', 'path': '/spec/template/spec/volumes/' + str(found[0])})
else:
if found:
raise ValueError('template-volume-preexists')
patch.append({'op': 'add', 'path': '/spec/template/spec/volumes/-', 'value': VOL})
configured = 0
for kind in ['initContainers', 'containers']:
for index, c in enumerate(spec[kind]):
base = '/spec/template/spec/' + kind + '/' + str(index)
mounts = c['volumeMounts']
matches = [i for i, v in enumerate(mounts) if v['name'] == VOLUME or v['mountPath'] == MOUNT['mountPath']]
if rollback:
if len(matches) != 1 or mounts[matches[0]] != MOUNT:
raise ValueError('rollback-mount-changed')
patch.append({'op': 'remove', 'path': base + '/volumeMounts/' + str(matches[0])})
else:
if matches:
raise ValueError('template-mount-preexists')
patch.append({'op': 'add', 'path': base + '/volumeMounts/-', 'value': MOUNT})
if kind == 'initContainers' and c['name'] == 'init-app-ini':
env = c['env']
found = [i for i, e in enumerate(env) if e['name'] == SETTING]
if rollback:
if len(found) != 1 or env[found[0]] != {'name': SETTING, 'value': TEMPLATE}:
raise ValueError('rollback-setting-changed')
patch.append({'op': 'replace', 'path': base + '/env/' + str(found[0]),
'value': {'name': SETTING, 'value': DEFAULT}})
else:
if found:
raise ValueError('template-setting-preexists')
patch.append({'op': 'add', 'path': base + '/env/-', 'value': {'name': SETTING, 'value': TEMPLATE}})
configured += 1
if configured != 1:
raise ValueError('unexpected-configuration-container')
return patch
def apply_locally(d, patch):
result = copy.deepcopy(d)
for op in patch:
parts = op['path'].strip('/').split('/')
obj = result
for part in parts[:-1]:
obj = obj[int(part)] if isinstance(obj, list) else obj[part]
last = parts[-1]
key = int(last) if isinstance(obj, list) and last != '-' else last
if op['op'] == 'test':
if obj[key] != op['value']:
raise ValueError('precondition-changed')
elif op['op'] == 'remove':
del obj[key]
elif op['op'] == 'replace':
obj[key] = copy.deepcopy(op['value'])
elif op['op'] == 'add':
if last == '-':
obj.append(copy.deepcopy(op['value']))
else:
obj[key] = copy.deepcopy(op['value'])
return result
def patch_remote(patch, dry_run=False):
args = ['-n', 'forgejo', 'patch', 'deployment', 'forgejo-gitea', '--type=json', '--patch-file=/dev/stdin', '-o', 'json']
if dry_run:
args.append('--dry-run=server')
return json.loads(native.command(args, payload=json.dumps(patch).encode()))
def selected_template(pod):
raw = native.execute(pod, 'sh', '-c',
'set +e; value=$(git config --file /data/home/.gitconfig --get-all init.templateDir 2>/dev/null); code=$?; '
'case "$code" in 0) printf "%s" "$value";; 1) printf "ABSENT";; *) exit 2;; esac')
value = raw.decode().strip()
if value not in ('ABSENT', TEMPLATE, DEFAULT):
raise ValueError('unknown-template-selection')
return value
def check_artifacts(pod):
activation.inspect(native, pod, applied=True)
for item in pod['artifacts'][:2]:
path = pod['version_directory'] + '/' + item['name']
observed = native.execute(pod, 'sha256sum', path).decode().split()[0]
if observed != item['sha256']:
raise ValueError('installed-artifact-changed')
native.execute(pod, 'test', '-x', path)
native.execute(pod, 'wget', '-qO', '/dev/null', 'http://127.0.0.1:3000/api/healthz')
def wait_ready(original, selection, timeout=240):
end = time.monotonic() + timeout
while time.monotonic() < end:
try:
d = deployment()
pod = target_pod(d, original)
if d['status'].get('updatedReplicas') != 1 or selected_template(pod) != selection:
raise ValueError('replacement-not-ready')
check_artifacts(pod)
return d, pod
except (ValueError, KeyError, subprocess.SubprocessError):
time.sleep(3)
raise ValueError('rollout-not-verified')
def verify_inheritance(pod, proposal):
directory = '/tmp/custodian-template-' + proposal['proposal_id'].lower()
created = False
try:
native.execute(pod, 'sh', '-c', 'umask 077; test ! -e "$1" && mkdir "$1" && printf "%s" "$2" > "$1/.owner"',
'sh', directory, proposal['proposal_id'])
created = True
native.execute(pod, 'sh', '-c', 'test -f "$1" && test ! -L "$1" && test -x "$1"', 'sh', MOUNT['mountPath'])
native.execute(pod, 'git', '-c', 'include.path=/data/home/.gitconfig', 'init', '--bare', '-q', directory + '/probe.git')
hook = directory + '/probe.git/hooks/pre-receive.d/10-custodian-secret-scan'
native.execute(pod, 'sh', '-c', 'test -f "$1" && test ! -L "$1" && test -x "$1"', 'sh', hook)
observed = native.execute(pod, 'sha256sum', hook).decode().split()[0]
if observed != pod['hook_sha256']:
raise ValueError('inherited-hook-differs')
raw = native.execute(pod, 'sh', '-c',
'cd "$1"; set +e; output=$(printf "malformed\\n" | hooks/pre-receive.d/10-custodian-secret-scan 2>/dev/null); '
'code=$?; test "$code" = 2 || exit 1; printf "%s" "$output"', 'sh', directory + '/probe.git')
if json.loads(raw)['status'] != 'unverified':
raise ValueError('inherited-hook-not-executable')
return {'mounted_hook_regular': True, 'inherited_hook_regular_executable': True,
'hook_sha256': observed, 'inherited_hook_malformed_input_refused': True,
'scope': 'Native Git init with actual Forgejo global configuration in an owned temporary bare repository; not production API repository creation.'}
finally:
if created:
native.execute(pod, 'sh', '-c', 'test "$(cat "$1/.owner")" = "$2" && rm -rf -- "$1" && test ! -e "$1"',
'sh', directory, proposal['proposal_id'])
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--proposal', type=Path, required=True)
parser.add_argument('--receipt', type=Path, required=True)
mode = parser.add_mutually_exclusive_group()
mode.add_argument('--apply', action='store_true')
mode.add_argument('--rollback', action='store_true')
args = parser.parse_args()
proposal = json.loads(args.proposal.read_text())
if not re.fullmatch(r'[A-Z0-9-]{1,100}', proposal['proposal_id']):
raise ValueError('unsafe-proposal-id')
for filename, expected in proposal['executor_hashes'].items():
if hashlib.sha256((HERE / filename).read_bytes()).hexdigest() != expected:
raise ValueError('executor-revision-changed')
if not args.rollback:
for filename, expected in proposal['owner_source_hashes'].items():
if hashlib.sha256(Path(filename).read_bytes()).hexdigest() != expected:
raise ValueError('owner-source-revision-changed')
isolated = json.loads(Path(proposal['isolated_receipt']).read_text())
if isolated.get('result') != 'verified' or not isolated.get('isolated_container_and_volumes_removed'):
raise ValueError('isolated-acceptance-incomplete')
original = json.loads((HERE / 'receive-hook-proposal.json').read_text())
nodes = json.loads(native.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('host-changed')
apps = json.loads(native.command(['-n', 'argocd', 'get', 'applications', '-o', 'json']))['items']
if any(a['spec'].get('destination', {}).get('namespace') == 'forgejo' for a in apps):
raise ValueError('gitops-owner-now-present')
d = deployment()
if d['metadata']['uid'] != proposal['deployment_uid'] or d['spec']['strategy']['type'] != 'Recreate' or d['spec']['replicas'] != 1:
raise ValueError('deployment-target-changed')
if not args.rollback and digest(d['spec']['template']) != proposal['original_template_sha256']:
raise ValueError('deployment-template-changed')
pod = target_pod(d, original)
check_artifacts(pod)
selection = selected_template(pod)
if selection != (TEMPLATE if args.rollback else 'ABSENT'):
raise ValueError('template-selection-changed')
cm = proposal['configmap']
if (cm.get('apiVersion') != 'v1' or cm.get('kind') != 'ConfigMap' or set(cm.get('data', {})) != {'hook'}
or cm['metadata']['name'] != NAME or cm['metadata']['namespace'] != 'forgejo' or not cm.get('immutable')):
raise ValueError('invalid-configmap-target')
if hashlib.sha256(cm['data']['hook'].encode()).hexdigest() != original['hook_sha256']:
raise ValueError('template-hook-changed')
found = native.command(['-n', 'forgejo', 'get', 'configmap', NAME, '--ignore-not-found', '-o', 'json'])
if found.strip():
existing = json.loads(found)
if existing.get('data') != cm['data'] or existing.get('immutable') is not True:
raise ValueError('template-configmap-conflict')
elif not args.rollback:
native.command(['create', '--dry-run=server', '-f', '-', '-o', 'name'], payload=json.dumps(cm).encode())
patch = patch_for(d, rollback=args.rollback)
simulated = apply_locally(d, patch)
dry = patch_remote(patch, dry_run=True)
if dry['spec']['template'] != simulated['spec']['template']:
raise ValueError('server-dry-run-unexpected-change')
receipt = {'proposal_id': proposal['proposal_id'], 'proposal_sha256': hashlib.sha256(args.proposal.read_bytes()).hexdigest(),
'observed_at': datetime.datetime.now(datetime.timezone.utc).isoformat(), 'result': 'dry-run-verified',
'old_pod_uid': pod['pod_uid'], 'old_generation': d['metadata']['generation'],
'server_dry_run_exact_match': True, 'patch': patch,
'original_template_sha256': digest(d['spec']['template']),
'resulting_template_sha256': digest(simulated['spec']['template']),
'no_secret_objects_read': True, 'mode': 'rollback' if args.rollback else 'apply' if args.apply else 'preflight'}
args.receipt.write_text(json.dumps(receipt, indent=2) + '\n')
if not args.apply and not args.rollback:
print('Exact additive patch and ConfigMap server dry-runs passed; no live mutation.')
return
submitted = False
try:
if not found.strip() and not args.rollback:
native.command(['create', '-f', '-', '-o', 'name'], payload=json.dumps(cm).encode())
receipt['configmap_created'] = True
submitted = True
receipt['patch_submission_started'] = True
changed = patch_remote(patch)
receipt['applied_generation'] = changed['metadata']['generation']
expected = DEFAULT if args.rollback else TEMPLATE
ready, newpod = wait_ready(original, expected)
if selected_template(newpod) != expected:
raise ValueError('runtime-selection-not-adopted')
if not args.rollback:
receipt['inheritance'] = verify_inheritance(newpod, proposal)
receipt.update(result='verified', new_pod_uid=newpod['pod_uid'], generation=ready['metadata']['generation'],
selected_template=expected, existing_22_profiles_preserved=True,
binaries_unchanged=True, health_after=True)
except (ValueError, KeyError, subprocess.SubprocessError):
receipt['result'] = 'unverified'
if submitted and not args.rollback:
try:
current = deployment()
observed_digest = digest(current['spec']['template'])
if observed_digest == receipt['original_template_sha256']:
receipt['rollback'] = 'patch was not applied; original deployment preserved; any created ConfigMap remains inert'
elif observed_digest == receipt['resulting_template_sha256']:
recovery = patch_for(current, rollback=True)
patch_remote(recovery)
_, recovered = wait_ready(original, DEFAULT)
receipt['rollback'] = 'default template selection restored; original 22 hooks preserved; ConfigMap retained inert'
else:
raise ValueError('concurrent-template-change-needs-owner-review')
except (ValueError, KeyError, subprocess.SubprocessError):
receipt['rollback'] = 'unverified; abort owner must inspect exact deployment and receipt'
raise
finally:
args.receipt.write_text(json.dumps(receipt, indent=2) + '\n')
print('Template ' + receipt['mode'] + ' verified; existing profiles and service health preserved.')
if __name__ == '__main__':
try:
main()
except (ValueError, OSError, KeyError, subprocess.SubprocessError):
raise SystemExit('Template operation refused or incomplete; inspect value-free receipt. Raw diagnostics suppressed.')

View file

@ -0,0 +1,121 @@
#!/usr/bin/env python3
"""Conditional startup-guard rollout, with real installed-scanner receive probes."""
import argparse
import datetime
import hashlib
import importlib.util
import json
from pathlib import Path
import time
HERE = Path(__file__).parent
s = importlib.util.spec_from_file_location('template', HERE / 'activate_forge_template.py')
t = importlib.util.module_from_spec(s)
s.loader.exec_module(t)
def native_probe(pod, suffix):
base = '/tmp/custodian-installed-custody-' + suffix
created = False
try:
t.native.execute(pod, 'sh', '-c', 'umask 077; test ! -e "$1" && mkdir "$1" && printf "%s" custody-probe > "$1/.owner"', 'sh', base)
created = True
raw = t.native.execute(pod, 'sh', '-s', '--', base, pod['existing_dispatcher'],
payload=(HERE / 'probe_forge_installed.sh').read_bytes(), timeout=90)
return json.loads(raw)
finally:
if created:
t.native.execute(pod, 'sh', '-c', 'test "$(cat "$1/.owner")" = custody-probe && rm -rf -- "$1" && test ! -e "$1"', 'sh', base)
def patch_for(d, candidate, rollback=False):
current = d['spec']['template']['spec']['initContainers']
matches = [i for i,c in enumerate(current) if c['name'] == candidate['name']]
patch = [{'op':'test', 'path':'/metadata/uid', 'value':d['metadata']['uid']},
{'op':'test', 'path':'/metadata/resourceVersion', 'value':d['metadata']['resourceVersion']}]
if rollback:
if matches != [0] or current[0] != candidate:
raise ValueError('rollback-candidate-differs')
patch.append({'op':'remove', 'path':'/spec/template/spec/initContainers/0'})
else:
if matches:
raise ValueError('candidate-already-present')
# apply_locally's JSON array add support appends only; replace exact guarded list.
patch.append({'op':'replace', 'path':'/spec/template/spec/initContainers', 'value':[candidate,*current]})
return patch
def verify(pod, suffix):
t.check_artifacts(pod)
stats = []
for item in pod['artifacts'][:2]:
stat = t.native.execute(pod, 'stat', '-c', '%a %u %g', pod['version_directory']+'/'+item['name']).decode().strip()
if stat != '755 1000 1000':
raise ValueError('custody-mode-wrong')
stats.append({'name':item['name'], 'stat':stat})
return {'artifacts':stats, 'receive_probe':native_probe(pod, suffix)}
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--proposal',type=Path,required=True)
parser.add_argument('--receipt',type=Path,required=True)
group=parser.add_mutually_exclusive_group()
group.add_argument('--apply',action='store_true')
group.add_argument('--rollback',action='store_true')
group.add_argument('--restart-proof',action='store_true')
args=parser.parse_args()
p=json.loads(args.proposal.read_text())
for file,expected in p['executor_hashes'].items():
if hashlib.sha256((HERE/file).read_bytes()).hexdigest()!=expected:
raise ValueError('executor-changed')
original=json.loads((HERE/'receive-hook-proposal.json').read_text())
c=json.loads((HERE/'forge-custody-init.json').read_text())
d=t.deployment()
expected=p['patched_template_digest'] if args.rollback or args.restart_proof else p['original_template_digest']
if d['metadata']['uid']!=p['deployment_uid'] or t.digest(d['spec']['template'])!=expected:
raise ValueError('deployment-changed')
pod=t.target_pod(d,original)
if args.restart_proof:
patch=[{'op':'test','path':'/metadata/resourceVersion','value':d['metadata']['resourceVersion']},
{'op':'add','path':'/spec/template/metadata/annotations/custodian.coulomb.social~1custody-proof','value':p['restart_marker']}]
else:
patch=patch_for(d,c,args.rollback)
local=t.apply_locally(d,patch)
dry=t.patch_remote(patch,dry_run=True)
if t.digest(local['spec']['template'])!=t.digest(dry['spec']['template']):
raise ValueError('dry-run-differs')
receipt={'proposal_id':p['proposal_id'],'revision':p['revision'],
'proposal_sha256':hashlib.sha256(args.proposal.read_bytes()).hexdigest(),
'at':datetime.datetime.now(datetime.timezone.utc).isoformat(),
'mode':'rollback' if args.rollback else 'restart-proof' if args.restart_proof else 'apply' if args.apply else 'dry-run',
'dry_run_verified':True,'before_pod_uid':pod['pod_uid'],'result':'prepared'}
args.receipt.write_text(json.dumps(receipt,indent=2)+'\n')
if not (args.apply or args.rollback or args.restart_proof):
receipt['before_probe']=verify(pod,'before')
receipt['result']='dry-run-verified'
else:
applied=t.patch_remote(patch)
receipt.update(result='rollout-started', generation=applied['metadata']['generation'])
args.receipt.write_text(json.dumps(receipt,indent=2)+'\n')
after,new=t.wait_ready(original,t.TEMPLATE)
if new['pod_uid']==pod['pod_uid'] or t.digest(after['spec']['template'])!=t.digest(local['spec']['template']):
raise ValueError('replacement-not-exact')
receipt['after_pod_uid']=new['pod_uid']
if args.rollback:
# Removing the init guard permits fsGroup to widen modes again; repair explicitly.
t.native.execute(new,'sh','-s','--','apply',payload=(HERE/'forge-custody-guard.sh').read_bytes())
else:
raw=json.loads(t.native.command(['-n','forgejo','get','pod',new['pod'],'-o','json']))
status=next(x for x in raw['status']['initContainerStatuses'] if x['name']==c['name'])
if status['state']['terminated']['exitCode']!=0:
raise ValueError('guard-not-completed')
receipt['guard_completed']=True
receipt['after_probe']=verify(new,'after-'+str(after['metadata']['generation']))
receipt['result']='verified'
args.receipt.write_text(json.dumps(receipt,indent=2)+'\n')
print(json.dumps(receipt))
if __name__=='__main__':
main()

View file

@ -0,0 +1,22 @@
#!/bin/sh
set +vx
set -eu
test "$(id -u)" = 1000
test -d /data && test ! -L /data
test -d /data/security && test ! -L /data/security
test -d /data/security/receive-secret-scan && test ! -L /data/security/receive-secret-scan
test -d /data/security/receive-secret-scan/20261012-57a0642b6251 && test ! -L /data/security/receive-secret-scan/20261012-57a0642b6251
test -f /data/security/receive-secret-scan/20261012-57a0642b6251/receive-secret-scan && test ! -L /data/security/receive-secret-scan/20261012-57a0642b6251/receive-secret-scan
test "$(stat -c %u /data/security/receive-secret-scan/20261012-57a0642b6251/receive-secret-scan)" = 1000
test "$(sha256sum /data/security/receive-secret-scan/20261012-57a0642b6251/receive-secret-scan | cut -d " " -f 1)" = 57a0642b625105440fe146fceb4d244fd2dd08a75beb316831675643e6cc27ec
case "$(stat -c %a /data/security/receive-secret-scan/20261012-57a0642b6251/receive-secret-scan)" in 755|775) ;; *) exit 21;; esac
test -f /data/security/receive-secret-scan/20261012-57a0642b6251/gitleaks && test ! -L /data/security/receive-secret-scan/20261012-57a0642b6251/gitleaks
test "$(stat -c %u /data/security/receive-secret-scan/20261012-57a0642b6251/gitleaks)" = 1000
test "$(sha256sum /data/security/receive-secret-scan/20261012-57a0642b6251/gitleaks | cut -d " " -f 1)" = 88f91962aa2f93ac6ab281d553b9e125f5197bbbce38f9f2437f7299c32e5509
case "$(stat -c %a /data/security/receive-secret-scan/20261012-57a0642b6251/gitleaks)" in 755|775) ;; *) exit 21;; esac
if [ "${1:-check}" = apply ]; then
chmod 755 /data/security/receive-secret-scan/20261012-57a0642b6251/receive-secret-scan
test "$(stat -c %a /data/security/receive-secret-scan/20261012-57a0642b6251/receive-secret-scan)" = 755
chmod 755 /data/security/receive-secret-scan/20261012-57a0642b6251/gitleaks
test "$(stat -c %a /data/security/receive-secret-scan/20261012-57a0642b6251/gitleaks)" = 755
fi

View file

@ -0,0 +1,43 @@
{
"name": "verify-secret-scanner",
"image": "code.forgejo.org/forgejo/forgejo@sha256:691657c7e64af7730a2e885e6d022cf16ae60724931c5392bd61838afadcaeba",
"imagePullPolicy": "IfNotPresent",
"command": [
"sh",
"-c",
"#!/bin/sh\nset +vx\nset -eu\ntest \"$(id -u)\" = 1000\ntest -d /data && test ! -L /data\ntest -d /data/security && test ! -L /data/security\ntest -d /data/security/receive-secret-scan && test ! -L /data/security/receive-secret-scan\ntest -d /data/security/receive-secret-scan/20261012-57a0642b6251 && test ! -L /data/security/receive-secret-scan/20261012-57a0642b6251\ntest -f /data/security/receive-secret-scan/20261012-57a0642b6251/receive-secret-scan && test ! -L /data/security/receive-secret-scan/20261012-57a0642b6251/receive-secret-scan\ntest \"$(stat -c %u /data/security/receive-secret-scan/20261012-57a0642b6251/receive-secret-scan)\" = 1000\ntest \"$(sha256sum /data/security/receive-secret-scan/20261012-57a0642b6251/receive-secret-scan | cut -d \" \" -f 1)\" = 57a0642b625105440fe146fceb4d244fd2dd08a75beb316831675643e6cc27ec\ncase \"$(stat -c %a /data/security/receive-secret-scan/20261012-57a0642b6251/receive-secret-scan)\" in 755|775) ;; *) exit 21;; esac\ntest -f /data/security/receive-secret-scan/20261012-57a0642b6251/gitleaks && test ! -L /data/security/receive-secret-scan/20261012-57a0642b6251/gitleaks\ntest \"$(stat -c %u /data/security/receive-secret-scan/20261012-57a0642b6251/gitleaks)\" = 1000\ntest \"$(sha256sum /data/security/receive-secret-scan/20261012-57a0642b6251/gitleaks | cut -d \" \" -f 1)\" = 88f91962aa2f93ac6ab281d553b9e125f5197bbbce38f9f2437f7299c32e5509\ncase \"$(stat -c %a /data/security/receive-secret-scan/20261012-57a0642b6251/gitleaks)\" in 755|775) ;; *) exit 21;; esac\nif [ \"${1:-check}\" = apply ]; then\n chmod 755 /data/security/receive-secret-scan/20261012-57a0642b6251/receive-secret-scan\n test \"$(stat -c %a /data/security/receive-secret-scan/20261012-57a0642b6251/receive-secret-scan)\" = 755\n chmod 755 /data/security/receive-secret-scan/20261012-57a0642b6251/gitleaks\n test \"$(stat -c %a /data/security/receive-secret-scan/20261012-57a0642b6251/gitleaks)\" = 755\nfi\n",
"sh",
"apply"
],
"securityContext": {
"runAsUser": 1000,
"runAsGroup": 1000,
"runAsNonRoot": true,
"allowPrivilegeEscalation": false,
"readOnlyRootFilesystem": true,
"capabilities": {
"drop": [
"ALL"
]
}
},
"resources": {
"requests": {
"cpu": "10m",
"memory": "32Mi"
},
"limits": {
"cpu": "100m",
"memory": "64Mi"
}
},
"volumeMounts": [
{
"name": "data",
"mountPath": "/data/security",
"subPath": "security"
}
],
"terminationMessagePath": "/dev/termination-log",
"terminationMessagePolicy": "File"
}

View file

@ -0,0 +1,19 @@
{
"proposal_id": "FORGE6-TEMPLATE-LIFECYCLE-001",
"revision": 2,
"original_revision": 1,
"original_digest_sha256": "7eddea55359b25c5ad307ed8ccd392dfadfe167d65cbe34b02f1955350bbcb36",
"kind": "failed-trial-recovery",
"pod_uid": "f818d3f5-5c3d-4a2a-8e78-50e10b10d2ce",
"executor_sha256": "1cf0f846fd4967e6a3a8b2a3844a7736a053c34fa3a9c54d16ea7ba3c18765ad",
"guard_sha256": "75fdd0722a8749d5038f9e575c547fe39f41f599c40344230dbfdfde489c0f1c",
"diff_ref": "forge-custody-guard.sh: fixed two admitted, regular, owned, hash-pinned files mode 0775 to 0755; check mode has no mutations",
"authorization_ref": "canon/standards/build-mode-change-authorization_v0.1.md",
"rollback": "No content or owner change. Exact inverse chmod 0775 only if restoring the failed pre-repair state is required by abort owner; that deliberately re-blocks pushes. Preferred safe recovery is retain hash-verified 0755 and stop on any artifact mismatch. No hook disabling. Durable restart protection follows separately.",
"abort_owner": "railiance-forge; executor custodian-codex",
"receipt_path": "docs/evidence/2026-10-13-netkingdom-security/forge-custody-repair.json",
"expected_result": "Both exact artifacts 0755, then ordinary SSH source push succeeds; restart durability not yet claimed.",
"cost_estimate_eur": 0,
"risk_estimate": "Only removes group write from two existing executable files. No rollout, content, account, policy scope, or spend change. Any mismatch aborts before chmod. Concurrent artifact replacement not expected during this single-agent repair.",
"promotion_evidence": false
}

View file

@ -0,0 +1,33 @@
{
"proposal_id": "FORGE6-TEMPLATE-LIFECYCLE-001",
"revision": 4,
"original_revision": 1,
"original_digest_sha256": "7eddea55359b25c5ad307ed8ccd392dfadfe167d65cbe34b02f1955350bbcb36",
"deployment_uid": "e8eff0aa-64bb-4b44-9a80-a66612986355",
"original_template_digest": "b10384c9564531bff2ca756854b998fdc520f0a25f6acf195f024f675fc3b3e6",
"executor_hashes": {
"deploy_forge_custody.py": "5125b29ac20ab2b0a9bfcf686c2bb2e6e9c143f716bf119035960b345db497b9",
"probe_forge_installed.sh": "66b571a33822cbd58a4956aef3b2ac484d07ab887c296cd2c9ece354117e9440",
"forge-custody-init.json": "cccb9999785fe0151a973d9a8b554975daf17c91432ed3e8735e432f3cd80e8f",
"forge-custody-guard.sh": "75fdd0722a8749d5038f9e575c547fe39f41f599c40344230dbfdfde489c0f1c",
"activate_forge_template.py": "ef2b38f655d5520d69dcd69cdb4997eaad4b50773f32c1d798d16331b204c789",
"verify_receive_native.py": "c9120122c6f6f0b7c41fe4fa975e5a2a340e776885d852b861b8c13b7664adcd",
"activate_receive_hooks.py": "9b01ee383e16463981112226e19156b0de59af2d03dd71b9403e144e280a9fb3",
"restart_forge_custody.py": "104ed6ce5f88739f64b6f6f62e2869252408a390b6780117a499dcc9be0496f2"
},
"authorization_ref": "canon/standards/build-mode-change-authorization_v0.1.md",
"diff_ref": "Only add owned custody-proof pod-template annotation; no config or image change.",
"rollback": "Marker may remain inert. To undo guard, conditionally remove only custody-proof annotation matching this marker, wait for replacement, then use revision3 --rollback. No workload spec reversion from a stale snapshot.",
"abort_owner": "railiance-forge; executor custodian-codex; source railiance-apps",
"receipt_path": "docs/evidence/2026-10-13-netkingdom-security/forge-custody-restart.json",
"expected_result": "New pod runs verified guard to completion; same exact image/artifacts and 22 hooks; modes755; inherited native receive clean, secret refusal, quarantine and recovery pass. Repeat with another replacement pod.",
"restart_marker": "FORGE6-TEMPLATE-LIFECYCLE-001-r4",
"cost_estimate_eur": 0,
"risk_estimate": "Recreate briefly interrupts forge. Existing init maximum100m/128Mi exceeds new10m/32Mi so no increased scheduling reservation; node86percent requests. Bound240s readiness. Mismatched artifacts deliberately block startup; rollback restores prior deployment plus verified755 repair. Mount only existing security subtree, no credentials/RBAC/policy/image changes. No spend.",
"promotion_evidence": false,
"preparation_refinement": {
"previous_revision": 3,
"failure": "Second restart dry-run refused because original local patch helper did not decode JSON Pointer ~1. No second restart occurred. New executor replaces the annotations map while preserving existing fields; server equality remains required."
},
"annotation_key": "custodian.coulomb.social/custody-proof"
}

View file

@ -0,0 +1,28 @@
{
"proposal_id": "FORGE6-TEMPLATE-LIFECYCLE-001",
"revision": 3,
"original_revision": 1,
"original_digest_sha256": "7eddea55359b25c5ad307ed8ccd392dfadfe167d65cbe34b02f1955350bbcb36",
"deployment_uid": "e8eff0aa-64bb-4b44-9a80-a66612986355",
"original_template_digest": "73f6c1ef484c60f93285d61e6df6651b4f1268fafb3e470f35f450bce4f34aba",
"patched_template_digest": "b27e5658ee3d6bcc1933c5d0f0a0acd88c16c54865096c0aae73d689e1066242",
"executor_hashes": {
"deploy_forge_custody.py": "5125b29ac20ab2b0a9bfcf686c2bb2e6e9c143f716bf119035960b345db497b9",
"probe_forge_installed.sh": "66b571a33822cbd58a4956aef3b2ac484d07ab887c296cd2c9ece354117e9440",
"forge-custody-init.json": "833c2a5428999a8f2d0cf8319ca69bf1bf8c8017809bfe73450cdf9d31d5a8d4",
"forge-custody-guard.sh": "75fdd0722a8749d5038f9e575c547fe39f41f599c40344230dbfdfde489c0f1c",
"activate_forge_template.py": "ef2b38f655d5520d69dcd69cdb4997eaad4b50773f32c1d798d16331b204c789",
"verify_receive_native.py": "c9120122c6f6f0b7c41fe4fa975e5a2a340e776885d852b861b8c13b7664adcd",
"activate_receive_hooks.py": "9b01ee383e16463981112226e19156b0de59af2d03dd71b9403e144e280a9fb3"
},
"authorization_ref": "canon/standards/build-mode-change-authorization_v0.1.md",
"diff_ref": "forge-custody-init.json plus owner helm/forgejo-values.yaml; additive first init container, no other changes",
"rollback": "Same executor --rollback under exact patched-template digest; removes only this init container, waits for Ready then verifies hashes and restores 0755 via the exact guard. After restart-proof remove only owned custody-proof annotation before rollback. Reconcile source by removing preExtraInitContainers before future Helm apply. If rollout cannot become Ready, abort owner may remove exact added init container after inspecting unchanged template; no hook bypass.",
"abort_owner": "railiance-forge; executor custodian-codex; source railiance-apps",
"receipt_path": "docs/evidence/2026-10-13-netkingdom-security/forge-custody-rollout.json",
"expected_result": "New pod runs verified guard to completion; same exact image/artifacts and 22 hooks; modes755; inherited native receive clean, secret refusal, quarantine and recovery pass. Repeat with another replacement pod.",
"restart_marker": "FORGE6-TEMPLATE-LIFECYCLE-001-r3",
"cost_estimate_eur": 0,
"risk_estimate": "Recreate briefly interrupts forge. Existing init maximum100m/128Mi exceeds new10m/32Mi so no increased scheduling reservation; node86percent requests. Bound240s readiness. Mismatched artifacts deliberately block startup; rollback restores prior deployment plus verified755 repair. Mount only existing security subtree, no credentials/RBAC/policy/image changes. No spend.",
"promotion_evidence": false
}

View file

@ -0,0 +1,32 @@
{
"proposal_id": "FORGE6-TEMPLATE-LIFECYCLE-001",
"revision": 3,
"original_revision": 1,
"original_digest_sha256": "7eddea55359b25c5ad307ed8ccd392dfadfe167d65cbe34b02f1955350bbcb36",
"deployment_uid": "e8eff0aa-64bb-4b44-9a80-a66612986355",
"original_template_digest": "73f6c1ef484c60f93285d61e6df6651b4f1268fafb3e470f35f450bce4f34aba",
"patched_template_digest": "b10384c9564531bff2ca756854b998fdc520f0a25f6acf195f024f675fc3b3e6",
"executor_hashes": {
"deploy_forge_custody.py": "5125b29ac20ab2b0a9bfcf686c2bb2e6e9c143f716bf119035960b345db497b9",
"probe_forge_installed.sh": "66b571a33822cbd58a4956aef3b2ac484d07ab887c296cd2c9ece354117e9440",
"forge-custody-init.json": "cccb9999785fe0151a973d9a8b554975daf17c91432ed3e8735e432f3cd80e8f",
"forge-custody-guard.sh": "75fdd0722a8749d5038f9e575c547fe39f41f599c40344230dbfdfde489c0f1c",
"activate_forge_template.py": "ef2b38f655d5520d69dcd69cdb4997eaad4b50773f32c1d798d16331b204c789",
"verify_receive_native.py": "c9120122c6f6f0b7c41fe4fa975e5a2a340e776885d852b861b8c13b7664adcd",
"activate_receive_hooks.py": "9b01ee383e16463981112226e19156b0de59af2d03dd71b9403e144e280a9fb3"
},
"authorization_ref": "canon/standards/build-mode-change-authorization_v0.1.md",
"diff_ref": "forge-custody-init.json plus owner helm/forgejo-values.yaml; additive first init container, no other changes",
"rollback": "Same executor --rollback under exact patched-template digest; removes only this init container, waits for Ready then verifies hashes and restores 0755 via the exact guard. After restart-proof remove only owned custody-proof annotation before rollback. Reconcile source by removing preExtraInitContainers before future Helm apply. If rollout cannot become Ready, abort owner may remove exact added init container after inspecting unchanged template; no hook bypass.",
"abort_owner": "railiance-forge; executor custodian-codex; source railiance-apps",
"receipt_path": "docs/evidence/2026-10-13-netkingdom-security/forge-custody-rollout.json",
"expected_result": "New pod runs verified guard to completion; same exact image/artifacts and 22 hooks; modes755; inherited native receive clean, secret refusal, quarantine and recovery pass. Repeat with another replacement pod.",
"restart_marker": "FORGE6-TEMPLATE-LIFECYCLE-001-r3",
"cost_estimate_eur": 0,
"risk_estimate": "Recreate briefly interrupts forge. Existing init maximum100m/128Mi exceeds new10m/32Mi so no increased scheduling reservation; node86percent requests. Bound240s readiness. Mismatched artifacts deliberately block startup; rollback restores prior deployment plus verified755 repair. Mount only existing security subtree, no credentials/RBAC/policy/image changes. No spend.",
"promotion_evidence": false,
"preparation_refinement": {
"original_candidate_ref": "forge-custody-rollout-pre-defaults.json",
"reason": "Server dry-run defaulted terminationMessagePath/Policy; candidate now declares exact defaults. No live change occurred."
}
}

View file

@ -0,0 +1,145 @@
# Forgejo template inheritance
FORGE-WP-0006-T02, coordinated by CUST-WP-0080 G07. Proposal
`forge-template-proposal.json` records the exact source, executor, target,
verification and recovery for trial `FORGE6-TEMPLATE-LIFECYCLE-001`.
Standing build-mode blue-lane authorization applies; this is not an autopilot
grant or a claim of independent owner approval.
The October 13 execution reached Deployment generation 12 Ready, preserving the
image and all 22 previously installed profiles. The live Forgejo global Git
configuration selects `/opt/forge-secret-template`. Native Git initialization
using that configuration inherited the exact regular, executable scanner hook;
malformed input was refused and the owned temporary repository was removed.
The execution receipt is
`docs/evidence/2026-10-13-netkingdom-security/forge-template-activation.json`.
## Durable source
railiance-apps commit `390a30a` owns the immutable non-secret ConfigMap manifest,
Helm volume/mount/encoded environment setting, and the Make prerequisite that
installs the ConfigMap before a future Helm deployment. A file `subPath` mount
is intentional: Git must copy a regular executable, not a ConfigMap projection
symlink. The hook still invokes the two previously admitted binaries on the
existing data PVC. Backup/restore must preserve or re-admit those exact binaries;
the template alone cannot recreate them. Missing scanner artifacts refuse
pushes, rather than disabling detection.
The live change used a conditional JSON Patch because the running Deployment
contains changes newer than its last Helm revision. Reapplying that old Helm
release was outside scope. The patch added one volume, four file mounts and one
configuration environment variable; resourceVersion/UID tests guard the write.
Images, replica/strategy/resource settings, identities, existing environment
entries and Secret objects were unchanged. Initial-only admin bootstrap and
the existing admin row were verified before the restart. Configuration conversion
uses the existing chart's environment-to-ini process.
## Recovery
Do not replay the apply operation after activation. It refuses the changed
Deployment template. To recover this exact change, after checking the current
target and the source record:
```sh
python3 docs/changes/CUST-WP-0080/activate_forge_template.py \
--proposal docs/changes/CUST-WP-0080/forge-template-proposal.json \
--receipt /tmp/forge-template-rollback.json --rollback
```
Rollback removes only the exact added mounts/volume and sets the added environment
variable to `/usr/share/git-core/templates`. Merely removing the environment
variable would leave the saved application/global-Git setting behind. The
ConfigMap remains inert; original and already inherited repository hooks remain.
Readiness, health, exact original 22 profiles, binary custody and default template
selection must pass again. Reconcile the owner Helm source to that explicit default
before another deployment; the original source otherwise intentionally re-enables
the desired control. Unknown concurrent changes cause refusal and require the
railiance-forge abort owner to review the current target, not overwrite it.
Automatic recovery after an incomplete apply is limited to a Deployment whose
template matches the exact attempted result. An uncertain submission checks
current state before deciding whether recovery is needed. The readiness budget
is 240 seconds per rollout/recovery; failure remains unverified in the receipt.
The original October 12 receive-hook activation retains its old pod binding.
Do not edit that old proposal to pretend it targets the replacement pod.
## Remaining acceptance
This closes configuration persistence and native Git inheritance for future
creation paths that use Forgejo's configured Git initialization. It does not prove
an authenticated production API/browser creation, SSO journey or imported/mirrored
content check. Imports and mirrors use clone/fetch; a receive hook is insufficient
for those paths. The refreshed inventory has 166 repositories: 22 installed
profiles and 144 builtin-only inventories. Existing repository expansion, CI
authority, private finding-to-response, historical/LFS/log/artifact coverage and
the applicable DSM requirements remain open in the existing owner plans.
Validation: 30 isolated exact-image lifecycle/configuration/rollback checks, six
conditional-patch/recovery tests, non-secret Helm rendering and matching server
dry-run. Local test containers were removed. The isolated environment used SQLite;
the deployed proof is scoped to configuration, restart, Git inheritance and the
preserved existing profiles.
## October 13 custody regression and recovery
The original generation-12 receipt proves only its listed checks. Ordinary source
push acceptance subsequently failed: Kubernetes volume processing left the
scanner binaries group-writable (0775), which the scanner correctly refuses.
The trial remains failed; later corrections are recovery revisions of the same
proposal, not new unchanged successes. Do not use the original malformed-input
check as evidence that the scanner can open and scan its admitted executable.
- Revision 2: `forge-custody-repair-proposal.json` and
`repair_forge_custody.py` restore only the two hash-pinned files to 0755.
- Revision 3: `forge-custody-rollout-proposal.json` and
`deploy_forge_custody.py` add the unprivileged first init container. The exact
Helm source is railiance-apps `50b66c4`; only the existing security subtree is
mounted. Kubernetes defaults are declared explicitly. The first startup and
real installed-scanner receive probe passed.
- Revision 4: `forge-custody-restart-proposal.json` and
`restart_forge_custody.py` repeat acceptance on another pod. Use its receipt to
determine completion. The revision-3 `--restart-proof` preparation is withdrawn:
its local JSON Pointer helper did not decode the annotation's slash. Its dry-run
refused before mutation. Original executed files and hashes remain preserved.
The guard checks all artifact hashes and owners before changing either mode. It
refuses unexpected permissions, symlinks and mismatched binaries. Missing or
changed artifacts stop startup for investigation; never disable scanning to make
that check pass. The six isolated custody checks simulate fsGroup mode changes.
Native probes use temporary repositories and the real inherited installed hook;
production builtin authorization is excluded there. A normal source sync push is
required separately for authenticated SSH acceptance.
For rollback after revision 4, first conditionally remove only the owned
`custodian.coulomb.social/custody-proof` annotation whose value is the revision-4
marker. Review the resulting template against revision 3's patched digest; wait
for that replacement to finish. Then revision 3 `--rollback` removes only the
exact guard, waits for readiness, and repairs the exact binaries to 0755 after
remount. The next unguarded restart may widen modes again, so rollback is temporary
recovery and does not establish durable acceptance. Reconcile the owner Helm
source before its next deployment. Any unexpected deployment or artifact change
stops this procedure for the named abort owner. Original template rollback is a
separate operation and must not silently remove the custody repair.
### Exact original proposal storage
`forge-template-proposal.json` and `forge-template-source-render.json` are now
lossless archival envelopes. A public ConfigMap source checksum under a filename
containing `secret` triggered the generic API-key detector. The archive keeps
paths and SHA-256 values in separate fields and retains all original metadata.
No detector ignore, exception or rule change was added. The executed original
proposal digest remains unchanged; it is the digest of the restored bytes, not
of the envelope. `forge-custody-metadata-triage.json` records this classification.
Restore the historical proposal to a new private file before using its original
executor (which remains byte-for-byte unchanged):
```sh
python3 docs/changes/CUST-WP-0080/restore_forge_proposal.py \
docs/changes/CUST-WP-0080/forge-template-proposal.json \
/tmp/forge-template-original.json
```
The helper refuses an existing destination or any reconstruction whose digest
differs. Restoration does not authorize replaying a stale live proposal.

View file

@ -0,0 +1,63 @@
{
"archival_format": "json-checksum-map-entries-v1",
"original_sha256": "7eddea55359b25c5ad307ed8ccd392dfadfe167d65cbe34b02f1955350bbcb36",
"converted_map_field": "owner_source_hashes",
"reason": "Public non-secret ConfigMap source checksum was classified as generic-api-key when its filename was used as a JSON property name. Structured path/sha256 entries preserve exact historical reconstruction without detector exceptions.",
"document": {
"proposal_id": "FORGE6-TEMPLATE-LIFECYCLE-001",
"revision": 1,
"prepared_at": "2026-10-13T11:56:51.534869+00:00",
"deployment_uid": "e8eff0aa-64bb-4b44-9a80-a66612986355",
"original_template_sha256": "18855f7b4c78f79117884684e44550406c72fc7be08c0b8e61ec16cfcb855746",
"original_generation": 11,
"executor_hashes": {
"activate_forge_template.py": "ef2b38f655d5520d69dcd69cdb4997eaad4b50773f32c1d798d16331b204c789",
"verify_receive_native.py": "c9120122c6f6f0b7c41fe4fa975e5a2a340e776885d852b861b8c13b7664adcd",
"activate_receive_hooks.py": "9b01ee383e16463981112226e19156b0de59af2d03dd71b9403e144e280a9fb3",
"receive-hook-proposal.json": "9c8ea3b7fbe16a997359432af7bcf09f429881fd23cc567188c21129e80ccf8d"
},
"owner_source_hashes": [
{
"path": "/home/worsch/railiance-apps/helm/forgejo-values.yaml",
"sha256": "e97aa62fe767f4dc55f2c68c7ae1b69e61e632826a8ba3ccb4ffaca6e302174c"
},
{
"path": "/home/worsch/railiance-apps/Makefile",
"sha256": "6bd20555631eb4c22e0729779d6c24d6888a4e6d555ab1f03083697016d42b15"
},
{
"path": "/home/worsch/railiance-apps/manifests/forgejo-secret-scan-template.yaml",
"sha256": "8fad71aff5a25d8e31289072b9c06c6ec74ac74fedafc54dde7177bfe187c96a"
}
],
"configmap": {
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": {
"name": "forgejo-secret-template-b1d7cf8cd13f",
"namespace": "forgejo",
"labels": {
"app.kubernetes.io/part-of": "forgejo",
"custodian.coulomb.social/workplan": "FORGE-WP-0006"
}
},
"immutable": true,
"data": {
"hook": "#!/bin/sh\nset +vx\nset -eu\nexec /data/security/receive-secret-scan/20261012-57a0642b6251/receive-secret-scan --scanner /data/security/receive-secret-scan/20261012-57a0642b6251/gitleaks\n"
}
},
"isolated_receipt": "docs/evidence/2026-10-13-netkingdom-security/forge-template-isolated.json",
"lane": "blue",
"authorization_ref": "canon/standards/build-mode-change-authorization_v0.1.md",
"diff_ref": "owner source patch; docs/evidence/2026-10-13-netkingdom-security/forge-template-source-render.json; exact additive JSON Patch in server dry-run receipt",
"rollback": "Same executor --rollback with original proposal. Removes only exact added mounts and volume, replaces added init setting with /usr/share/git-core/templates, waits for healthy replacement and verifies original 22 hooks/binaries. Keeps immutable ConfigMap inert and any already inherited repository hooks. If rollback used, reconcile owner source to explicit default selection before another Helm deploy. Automatic recovery only when current deployment template exactly matches the attempted change; unknown concurrent changes require abort-owner review.",
"abort_owner": "railiance-forge; executing custodian-codex; release source railiance-apps",
"receipt_path": "docs/evidence/2026-10-13-netkingdom-security/forge-template-activation.json",
"target": "railiance01 namespace forgejo: Deployment forgejo-gitea and immutable ConfigMap forgejo-secret-template-b1d7cf8cd13f",
"expected_result": "Same image and artifact hashes, Recreate replacement Ready, template selected in actual Forgejo global Git configuration, regular/executable inherited hook in temporary native Git repository, existing 22 hook inventories unchanged, health passing. No new production repository, identity, credential or publication.",
"cost_estimate_eur": 0,
"risk_estimate": "No new resource requests, spend commitments, RBAC or credential scope. Existing Recreate deployment incurs a brief forge interruption; abort after 240-second readiness budget, restore default template under conditional rollback with its own 240-second verification budget. Unknown state stops automatic recovery. New repository writes can refuse if scanner missing, overloaded or input unsupported; existing 22 profiles unchanged. Recorded node CPU requests 3460m/4000m and memory 9472Mi (59%).",
"verification_limit": "Native config and Git inheritance, not authenticated production HTTP/SSO/frontend repository creation. Initial 22 repositories remain sole existing-repository activation scope; other 143, imported/mirrored history, CI and private response remain open.",
"promotion_evidence": false
}
}

View file

@ -0,0 +1,43 @@
#!/bin/sh
# Caller owns a unique temporary directory and removes it by marker afterwards.
set +vx
set -eu
base=$1
dispatcher=$2
cd "$base"
umask 077
export HOME="$base" GIT_CONFIG_NOSYSTEM=1 GIT_CONFIG_GLOBAL=/dev/null GIT_TERMINAL_PROMPT=0
git -c include.path=/data/home/.gitconfig init -q --bare server.git
git init -q -b main client
git -C client config core.hooksPath "$base/no-hooks"
git -C client config user.name 'Custodian isolated receive probe'
git -C client config user.email 'probe@example.test'
test -x server.git/hooks/pre-receive.d/10-custodian-secret-scan
cp "$dispatcher" server.git/hooks/pre-receive
chmod 755 server.git/hooks/pre-receive
# No production database identity: native dispatcher with only the inherited scan hook.
printf 'clean baseline\n' > client/source.txt
git -C client add source.txt
git -C client commit -qm 'isolated clean baseline'
git -C client push "$base/server.git" HEAD:refs/heads/main > clean.out 2>&1
grep -q '"status":"clean"' clean.out
initial=$(git -C client rev-parse HEAD)
ssh-keygen -q -t ed25519 -N '' -f "$base/synthetic-key" >/dev/null 2>&1
cp synthetic-key client/fixture.txt
git -C client add fixture.txt
git -C client commit -qm 'isolated synthetic fixture'
rejected=$(git -C client rev-parse HEAD)
git -C client rm -q fixture.txt
git -C client commit -qm 'remove fixture from final tree'
if 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 git -C server.git cat-file -e "$rejected" >/dev/null 2>&1; then exit 24; fi
test "$(git -C server.git rev-parse refs/heads/main)" = "$initial"
git -C client checkout -qb recovery "$initial"
printf 'clean recovery\n' > client/source.txt
git -C client add source.txt
git -C client commit -qm 'isolated clean recovery'
git -C client push "$base/server.git" HEAD:refs/heads/main > recovered.out 2>&1
grep -q '"status":"clean"' recovered.out
printf '{"inherited_installed_hook":true,"clean_push":true,"intermediate_secret_refused":true,"quarantine_discarded":true,"clean_recovery":true,"builtin_authorization_excluded":true}\n'

View file

@ -0,0 +1,68 @@
#!/usr/bin/env python3
"""Guarded repair of the two admitted scanner modes; capture no child diagnostics."""
import argparse
import datetime
import hashlib
import importlib.util
import json
from pathlib import Path
HERE = Path(__file__).parent
spec = importlib.util.spec_from_file_location('template', HERE / 'activate_forge_template.py')
template = importlib.util.module_from_spec(spec)
spec.loader.exec_module(template)
def guard_script(original):
lines = ['#!/bin/sh', 'set +vx', 'set -eu', 'test "$(id -u)" = 1000']
# Validate all files before changing either. Never follow a substituted link.
for parent in ['/data', '/data/security', '/data/security/receive-secret-scan', original['version_directory']]:
lines.append(f'test -d {parent} && test ! -L {parent}')
for item in original['artifacts'][:2]:
path = original['version_directory'] + '/' + item['name']
lines.extend([f'test -f {path} && test ! -L {path}',
f'test "$(stat -c %u {path})" = 1000',
f'test "$(sha256sum {path} | cut -d " " -f 1)" = {item["sha256"]}',
f'case "$(stat -c %a {path})" in 755|775) ;; *) exit 21;; esac'])
lines.append('if [ "${1:-check}" = apply ]; then')
for item in original['artifacts'][:2]:
path = original['version_directory'] + '/' + item['name']
lines.extend([f' chmod 755 {path}', f' test "$(stat -c %a {path})" = 755'])
lines.extend(['fi', ''])
return '\n'.join(lines)
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())
original = json.loads((HERE / 'receive-hook-proposal.json').read_text())
script = guard_script(original).encode()
assert hashlib.sha256(Path(__file__).read_bytes()).hexdigest() == proposal['executor_sha256']
assert hashlib.sha256(script).hexdigest() == proposal['guard_sha256']
d = template.deployment()
pod = template.target_pod(d, original)
assert pod['pod_uid'] == proposal['pod_uid']
template.check_artifacts(pod)
mode = 'apply' if args.apply else 'check'
receipt = {'proposal_id': proposal['proposal_id'], 'revision': proposal['revision'],
'proposal_sha256': hashlib.sha256(args.proposal.read_bytes()).hexdigest(),
'pod_uid': pod['pod_uid'], 'mode': mode, 'result': 'unverified',
'at': datetime.datetime.now(datetime.timezone.utc).isoformat()}
args.receipt.write_text(json.dumps(receipt, indent=2) + '\n')
template.native.execute(pod, 'sh', '-s', '--', mode, payload=script)
observed = []
for item in original['artifacts'][:2]:
path = original['version_directory'] + '/' + item['name']
observed.append({'name': item['name'], 'stat': template.native.execute(pod, 'stat', '-c', '%a %u %g', path).decode().strip()})
receipt.update(result='modes-restored' if args.apply else 'preflight-passed', artifacts=observed,
push_acceptance='pending', restart_durability='pending')
args.receipt.write_text(json.dumps(receipt, indent=2) + '\n')
print(json.dumps(receipt))
if __name__ == '__main__':
main()

View file

@ -0,0 +1,59 @@
#!/usr/bin/env python3
"""Repeat custody acceptance on a new pod, preserving all existing annotations."""
import argparse
import datetime
import hashlib
import importlib.util
import json
from pathlib import Path
HERE = Path(__file__).parent
s=importlib.util.spec_from_file_location('custody',HERE/'deploy_forge_custody.py')
m=importlib.util.module_from_spec(s)
s.loader.exec_module(m)
t=m.t
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()
p=json.loads(args.proposal.read_text())
for file,expected in p['executor_hashes'].items():
assert hashlib.sha256((HERE/file).read_bytes()).hexdigest()==expected,'executor-changed'
d=t.deployment()
assert t.digest(d['spec']['template'])==p['original_template_digest'],'deployment-changed'
original=json.loads((HERE/'receive-hook-proposal.json').read_text())
pod=t.target_pod(d,original)
annotations=dict(d['spec']['template']['metadata'].get('annotations',{}))
assert p['annotation_key'] not in annotations,'marker-preexists'
annotations[p['annotation_key']]=p['restart_marker']
patch=[{'op':'test','path':'/metadata/uid','value':p['deployment_uid']},
{'op':'test','path':'/metadata/resourceVersion','value':d['metadata']['resourceVersion']},
{'op':'add','path':'/spec/template/metadata/annotations','value':annotations}]
local=t.apply_locally(d,patch)
dry=t.patch_remote(patch,dry_run=True)
assert t.digest(local['spec']['template'])==t.digest(dry['spec']['template']),'dry-run-differs'
receipt={'proposal_id':p['proposal_id'],'revision':p['revision'],
'proposal_sha256':hashlib.sha256(args.proposal.read_bytes()).hexdigest(),
'at':datetime.datetime.now(datetime.timezone.utc).isoformat(),
'before_pod_uid':pod['pod_uid'],'dry_run_verified':True,'result':'dry-run-verified'}
args.receipt.write_text(json.dumps(receipt,indent=2)+'\n')
if args.apply:
applied=t.patch_remote(patch)
receipt.update(generation=applied['metadata']['generation'],result='rollout-started')
args.receipt.write_text(json.dumps(receipt,indent=2)+'\n')
after,new=t.wait_ready(original,t.TEMPLATE)
assert new['pod_uid']!=pod['pod_uid'],'pod-not-replaced'
assert t.digest(after['spec']['template'])==t.digest(local['spec']['template']),'deployment-changed'
raw=json.loads(t.native.command(['-n','forgejo','get','pod',new['pod'],'-o','json']))
status=next(x for x in raw['status']['initContainerStatuses'] if x['name']=='verify-secret-scanner')
assert status['state']['terminated']['exitCode']==0,'guard-not-completed'
receipt.update(after_pod_uid=new['pod_uid'],guard_completed=True,
after_probe=m.verify(new,'restart-'+str(after['metadata']['generation'])),result='verified')
args.receipt.write_text(json.dumps(receipt,indent=2)+'\n')
print(json.dumps(receipt))
if __name__=='__main__':
main()

View file

@ -0,0 +1,41 @@
#!/usr/bin/env python3
"""Restore exact historical JSON bytes from a checksum-metadata archive.
The archive separates file paths from digest fields to avoid interpreting a
public ConfigMap checksum as an API key. No detector exception is introduced.
The original execution digest is verified before writing a new private file.
"""
import argparse
import copy
import hashlib
import json
import os
from pathlib import Path
def restore(archive):
if archive['archival_format'] != 'json-checksum-map-entries-v1':
raise ValueError('unsupported-archive-format')
document = copy.deepcopy(archive['document'])
field = archive['converted_map_field']
entries = document[field]
restored = {entry['path']: entry['sha256'] for entry in entries}
if len(restored) != len(entries):
raise ValueError('duplicate-path')
document[field] = restored
raw = (json.dumps(document, indent=2) + '\n').encode()
if hashlib.sha256(raw).hexdigest() != archive['original_sha256']:
raise ValueError('original-digest-mismatch')
return raw
if __name__ == '__main__':
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('archive', type=Path)
parser.add_argument('output', type=Path)
args = parser.parse_args()
raw = restore(json.loads(args.archive.read_text()))
fd = os.open(args.output, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
with os.fdopen(fd, 'wb') as out:
out.write(raw)
print('Exact historical bytes restored and digest verified.')

View file

@ -0,0 +1,55 @@
#!/usr/bin/env python3
"""Exercise the exact startup command in the pinned image, with synthetic volume state."""
import hashlib
import json
from pathlib import Path
import shutil
import subprocess
import tempfile
HERE = Path(__file__).parent
original = json.loads((HERE / 'receive-hook-proposal.json').read_text())
container = json.loads((HERE / 'forge-custody-init.json').read_text())
checks = {}
with tempfile.TemporaryDirectory(prefix='forge-custody-test-') as tmp:
root = Path(tmp)
version = root / 'receive-secret-scan' / Path(original['version_directory']).name
version.mkdir(parents=True)
files = [version / x['name'] for x in original['artifacts'][:2]]
for x, dest in zip(original['artifacts'], files):
shutil.copyfile('/tmp/custodian-forge-lifecycle/' + x['name'], dest)
assert hashlib.sha256(dest.read_bytes()).hexdigest() == x['sha256']
dest.chmod(0o775)
def run():
return subprocess.run(['docker', 'run', '--rm', '--network', 'none', '--read-only',
'--cap-drop', 'ALL', '--security-opt', 'no-new-privileges', '--user', '1000:1000',
'--mount', f'type=bind,src={root},dst=/data/security', '--entrypoint', 'sh',
container['image'], *container['command'][1:]], capture_output=True, timeout=45).returncode
checks['remount_0775_repaired'] = run() == 0 and all(x.stat().st_mode & 0o777 == 0o755 for x in files)
checks['already_correct_idempotent'] = run() == 0
for x in files:
x.chmod(0o775)
saved = files[1].read_bytes()
files[1].write_bytes(b'corrupt fixture')
checks['wrong_hash_refused_before_any_chmod'] = run() != 0 and all(x.stat().st_mode & 0o777 == 0o775 for x in files)
files[1].write_bytes(saved)
files[1].chmod(0o777)
checks['unexpected_world_write_refused'] = run() != 0 and files[0].stat().st_mode & 0o777 == 0o775
files[1].chmod(0o775)
target = version / 'link-target'
files[1].rename(target)
files[1].symlink_to(target.name)
checks['symlink_refused'] = run() != 0 and target.stat().st_mode & 0o777 == 0o775
files[1].unlink()
target.rename(files[1])
checks['recovery_after_refusals'] = run() == 0 and all(x.stat().st_mode & 0o777 == 0o755 for x in files)
receipt = {'result': 'verified' if all(checks.values()) else 'failed', 'checks': checks,
'scope': 'Exact startup command and pinned Forgejo image; simulated fsGroup remount modes, no production mount.',
'container_sha256': hashlib.sha256((HERE / 'forge-custody-init.json').read_bytes()).hexdigest()}
Path('docs/evidence/2026-10-13-netkingdom-security/forge-custody-isolated.json').write_text(json.dumps(receipt, indent=2) + '\n')
print(json.dumps(receipt))
assert all(checks.values())

View file

@ -0,0 +1,69 @@
import copy
import importlib.util
from pathlib import Path
import unittest
path = Path(__file__).with_name('activate_forge_template.py')
spec = importlib.util.spec_from_file_location('template', path)
t = importlib.util.module_from_spec(spec)
spec.loader.exec_module(t)
def fixture():
def container(name):
return {'name': name, 'image': 'unchanged:fixture', 'resources': {'requests': {'cpu': '100m'}},
'volumeMounts': [{'name': 'data', 'mountPath': '/data'}],
'env': [{'name': 'EXISTING_REFERENCE', 'valueFrom': {'secretKeyRef': {'name': 'existing', 'key': 'reference-only'}}}]}
return {'metadata': {'uid': 'fixed', 'resourceVersion': '17'}, 'spec': {'template': {'metadata': {'labels': {'preserved': 'yes'}},
'spec': {'volumes': [{'name': 'data', 'persistentVolumeClaim': {'claimName': 'existing'}}],
'initContainers': [container('init-directories'), container('init-app-ini'), container('configure-gitea')],
'containers': [container('gitea')]}}}}
class Tests(unittest.TestCase):
def test_rollback_preserves_existing_configuration_and_explicitly_restores_default(self):
before = fixture()
active = t.apply_locally(before, t.patch_for(before))
rolled = t.apply_locally(active, t.patch_for(active, rollback=True))
expected = copy.deepcopy(before)
expected['spec']['template']['spec']['initContainers'][1]['env'].append({'name': t.SETTING, 'value': t.DEFAULT})
self.assertEqual(rolled, expected)
self.assertEqual(before, fixture())
def test_stale_resource_version_cannot_apply(self):
before = fixture()
patch = t.patch_for(before)
before['metadata']['resourceVersion'] = '18'
with self.assertRaisesRegex(ValueError, 'precondition'):
t.apply_locally(before, patch)
def test_existing_setting_not_overwritten(self):
d = fixture()
d['spec']['template']['spec']['initContainers'][1]['env'].append({'name': t.SETTING, 'value': '/unknown'})
with self.assertRaisesRegex(ValueError, 'setting-preexists'):
t.patch_for(d)
def test_conflicting_mount_not_overwritten(self):
d = fixture()
d['spec']['template']['spec']['containers'][0]['volumeMounts'].append({'name': 'other', 'mountPath': t.MOUNT['mountPath']})
with self.assertRaisesRegex(ValueError, 'mount-preexists'):
t.patch_for(d)
def test_rollback_preserves_concurrently_modified_mount(self):
d = fixture()
d = t.apply_locally(d, t.patch_for(d))
d['spec']['template']['spec']['containers'][0]['volumeMounts'][-1]['subPath'] = 'unknown'
original = copy.deepcopy(d)
with self.assertRaisesRegex(ValueError, 'rollback-mount-changed'):
t.patch_for(d, rollback=True)
self.assertEqual(d, original)
def test_missing_configuration_container_refused(self):
d = fixture()
d['spec']['template']['spec']['initContainers'].pop(1)
with self.assertRaisesRegex(ValueError, 'configuration-container'):
t.patch_for(d)
if __name__ == '__main__':
unittest.main(verbosity=2)

View file

@ -0,0 +1,96 @@
#!/usr/bin/env python3
"""Verify the proposed file mount, encoded configuration and rollback in isolation."""
import argparse
import hashlib
import importlib.util
import json
from pathlib import Path
import subprocess
import tempfile
BASE = Path(__file__).with_name('verify_forge_lifecycle.py')
BASE_SHA256 = '88bbf4235086b7ffc40ca4d7df7fafa1d67e2e1b41d2f0055ec82421ed4840cb'
if hashlib.sha256(BASE.read_bytes()).hexdigest() != BASE_SHA256:
raise SystemExit('Lifecycle helper revision changed.')
spec = importlib.util.spec_from_file_location('lifecycle', BASE)
base = importlib.util.module_from_spec(spec)
spec.loader.exec_module(base)
SETTING = 'GITEA__GIT_0X2E_CONFIG__INIT_0X2E_TEMPLATEDIR'
TEMPLATE = '/opt/forge-secret-template'
MOUNT = TEMPLATE + '/hooks/pre-receive.d/10-custodian-secret-scan'
class Probe(base.Probe):
def __init__(self, receipt, hook_file):
super().__init__(receipt)
self.hook_file = hook_file
self.evidence['scope'] = ('Isolated exact-image instance with SQLite and no network. '
'One read-only mount of the public hook fixture simulates a Kubernetes file subPath. '
'No production data, configuration or credentials used; production Kubernetes acceptance remains separate.')
def run(self, argv, *args, **kwargs):
if argv[:2] == ['docker', 'run']:
argv = list(argv)
index = argv.index(base.IMAGE)
argv[index:index] = ['--mount', f'type=bind,src={self.hook_file},dst={MOUNT},readonly']
return super().run(argv, *args, **kwargs)
def set_template(self, value):
self.execute('env', SETTING + '=' + value, 'environment-to-ini', '--config', base.CONFIG,
'--work-path', base.ROOT, '--out', base.CONFIG)
self.run(['docker', 'restart', self.cid])
self.start_web()
selected = self.execute('git', 'config', '--file', base.ROOT + '/git-home/.gitconfig',
'--get', 'init.templateDir').stdout.decode().strip()
self.check('encoded_setting_selected_' + ('default' if value != TEMPLATE else 'template'), selected == value)
def execute(self, *args, **kwargs):
if args[:5] == ('git', 'config', '--file', base.ROOT + '/git-home/.gitconfig', 'init.templateDir'):
self.set_template(TEMPLATE)
result = super().execute('sh', '-c', 'test -f "$1" && test ! -L "$1" && test -x "$1"', 'sh', MOUNT)
self.check('mounted_hook_regular_executable', result.returncode == 0)
return result
return super().execute(*args, **kwargs)
def run_probe(self, artifact_dir):
super().run_probe(artifact_dir)
self.evidence['result'] = 'unverified'
self.phase = 'rollback-default-template'
self.set_template('/usr/share/git-core/templates')
status, _ = self.api('POST', '/api/v1/user/repos', {'name': 'rollback', 'private': True, 'auto_init': True})
self.check('rollback_new_repository_created', status == 201)
absent = self.execute('test', '-e', self.profile('rollback'), allowed=(0, 1))
self.check('rollback_restores_default_creation_behavior', absent.returncode == 1)
for repo in ['coverage', 'templated']:
digest = self.execute('sha256sum', self.profile(repo)).stdout.split()[0].decode()
self.check('rollback_preserves_existing_' + repo + '_hook', digest == hashlib.sha256(self.hook_file.read_bytes()).hexdigest())
self.evidence['result'] = 'verified'
def close(self):
super().close()
self.evidence['base_executor_sha256'] = BASE_SHA256
self.evidence['executor_sha256'] = hashlib.sha256(Path(__file__).read_bytes()).hexdigest()
self.receipt.write_text(json.dumps(self.evidence, indent=2) + '\n')
def main():
p = argparse.ArgumentParser(description=__doc__)
p.add_argument('--artifact-dir', type=Path, required=True)
p.add_argument('--receipt', type=Path, required=True)
args = p.parse_args()
with tempfile.TemporaryDirectory(prefix='forge-template-fixture-') as temporary:
hook = Path(temporary) / 'hook'
hook.write_bytes(Path(__file__).with_name('receive-profile-hook').read_bytes())
hook.chmod(0o555)
probe = Probe(args.receipt, hook)
try:
probe.run_probe(args.artifact_dir)
except (ValueError, OSError, KeyError, subprocess.SubprocessError) as error:
probe.evidence['failure'] = probe.phase + ':' + type(error).__name__
raise SystemExit('Template acceptance incomplete at phase ' + probe.phase)
finally:
probe.close()
if __name__ == '__main__':
main()