security: recover Forgejo scanner custody and prove restart acceptance
This commit is contained in:
parent
e570433060
commit
8f770e1eea
39 changed files with 1984 additions and 12 deletions
|
|
@ -563,6 +563,74 @@
|
|||
"executed_digest_sha256": "9c8ea3b7fbe16a997359432af7bcf09f429881fd23cc567188c21129e80ccf8d",
|
||||
"verified_at": "2026-10-12T17:32:28.543884+00:00",
|
||||
"verified_outcome": "Additive independent scanner installed on 22 exact repositories. Each actual SSH clean and synthetic-secret probe produced the expected scanner result under the temporary veto; probe refs and quarantined commits absent. Veto and staging removed, original hook inventory preserved, health passes. Normal clean acceptance remains separate."
|
||||
},
|
||||
{
|
||||
"proposal_id": "FORGE6-TEMPLATE-LIFECYCLE-001",
|
||||
"disposition": "unscored",
|
||||
"outcome": "failed",
|
||||
"original_revision": 1,
|
||||
"original_digest_sha256": "7eddea55359b25c5ad307ed8ccd392dfadfe167d65cbe34b02f1955350bbcb36",
|
||||
"proposal_ref": "docs/changes/CUST-WP-0080/forge-template-proposal.json",
|
||||
"authorization_ref": "canon/standards/build-mode-change-authorization_v0.1.md",
|
||||
"reason": "Standing founder blue-lane authorization after exact source/isolation/server-dry-run proof; not an exact-revision human approval, owner assent, autopilot grant or credential-isolation proof.",
|
||||
"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.",
|
||||
"verification_ref": "docs/evidence/2026-10-13-netkingdom-security/forge-template-activation.json",
|
||||
"dry_run_ref": "docs/evidence/2026-10-13-netkingdom-security/forge-template-dry-run.json",
|
||||
"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",
|
||||
"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%).",
|
||||
"prepared_at": "2026-10-13T11:56:51.534869+00:00",
|
||||
"promotion_evidence": false,
|
||||
"refinement_or_rescue": true,
|
||||
"executed_revision": 1,
|
||||
"executed_digest_sha256": "7eddea55359b25c5ad307ed8ccd392dfadfe167d65cbe34b02f1955350bbcb36",
|
||||
"verified_at": "2026-10-13T12:02:44.468580+00:00",
|
||||
"verified_outcome": "Subsequent ordinary railiance-forge push refused with scanner-file-refused after the restart. Initial hash/executable/malformed-input verification was insufficient. Custody-mode diagnosis and recovery in progress.",
|
||||
"initial_verification": {
|
||||
"at": "2026-10-13T12:02:44.468580+00:00",
|
||||
"outcome": "Exact additive patch reached generation 12 Ready on unchanged image; configured native Git init inherits a regular executable scanner hook and malformed input refuses. Original 22 hooks/binaries preserved, health passing, temporary repository removed. No production API repository creation or global existing-repository coverage inferred.",
|
||||
"limit": "Did not exercise ordinary source acceptance after the actual Kubernetes restart."
|
||||
},
|
||||
"adverse_outcome_observed_at": "2026-10-13T12:11:00.052645+00:00",
|
||||
"recovery_revisions": [
|
||||
{
|
||||
"revision": 2,
|
||||
"digest_sha256": "7530423d7528b63434d896dfe28e5288d4e332167db817efa58c2255defe28d3",
|
||||
"proposal_ref": "docs/changes/CUST-WP-0080/forge-custody-repair-proposal.json",
|
||||
"disposition": "unscored-standing-blue-authorization",
|
||||
"outcome": "modes-restored; ordinary queued source push accepted via statehub sync",
|
||||
"preflight_ref": "docs/evidence/2026-10-13-netkingdom-security/forge-custody-preflight.json",
|
||||
"receipt_ref": "docs/evidence/2026-10-13-netkingdom-security/forge-custody-repair.json"
|
||||
},
|
||||
{
|
||||
"revision": 3,
|
||||
"digest_sha256": "b9e8d82331ac2a7fc4b12b5524d486809d03b82f52dc96b31e8ae5e73b93c37f",
|
||||
"proposal_ref": "docs/changes/CUST-WP-0080/forge-custody-rollout-proposal.json",
|
||||
"disposition": "unscored-standing-blue-authorization",
|
||||
"outcome": "first rollout verified; second restart preparation refused JSON-pointer local/server mismatch without mutation",
|
||||
"source_commit": "railiance-apps 50b66c4",
|
||||
"preflight_ref": "docs/evidence/2026-10-13-netkingdom-security/forge-custody-rollout-dry-run.json",
|
||||
"receipt_ref": "docs/evidence/2026-10-13-netkingdom-security/forge-custody-rollout.json"
|
||||
},
|
||||
{
|
||||
"revision": 4,
|
||||
"digest_sha256": "f6060bf7a5a604781a04b853775f72ba7ada5cc05c89a1da0df4fd480779004b",
|
||||
"proposal_ref": "docs/changes/CUST-WP-0080/forge-custody-restart-proposal.json",
|
||||
"disposition": "unscored-standing-blue-authorization",
|
||||
"outcome": "verified second replacement generation14; guard completed; modes755 and clean/secret/quarantine/recovery checks pass",
|
||||
"receipt_ref": "docs/evidence/2026-10-13-netkingdom-security/forge-custody-restart.json"
|
||||
}
|
||||
],
|
||||
"recovery_outcome": "Recovered with refinement: revisions2-4 restore755 and persist startup custody; two replacement pods passed native receive probes. Original trial remains failed, not an unchanged success. Authenticated SSH source push2c11c36 accepted after final restart and remote head verified.",
|
||||
"recovery_summary_ref": "docs/evidence/2026-10-13-netkingdom-security/forge-custody-recovery-summary.json",
|
||||
"proposal_storage": {
|
||||
"format": "json-checksum-map-entries-v1",
|
||||
"restore_helper": "docs/changes/CUST-WP-0080/restore_forge_proposal.py",
|
||||
"original_digest_semantics": "SHA256 of exactly reconstructed executed JSON bytes; not the archive envelope",
|
||||
"triage_ref": "docs/evidence/2026-10-13-netkingdom-security/forge-custody-metadata-triage.json"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
320
docs/changes/CUST-WP-0080/activate_forge_template.py
Normal file
320
docs/changes/CUST-WP-0080/activate_forge_template.py
Normal 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.')
|
||||
121
docs/changes/CUST-WP-0080/deploy_forge_custody.py
Normal file
121
docs/changes/CUST-WP-0080/deploy_forge_custody.py
Normal 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()
|
||||
22
docs/changes/CUST-WP-0080/forge-custody-guard.sh
Normal file
22
docs/changes/CUST-WP-0080/forge-custody-guard.sh
Normal 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
|
||||
43
docs/changes/CUST-WP-0080/forge-custody-init.json
Normal file
43
docs/changes/CUST-WP-0080/forge-custody-init.json
Normal 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"
|
||||
}
|
||||
19
docs/changes/CUST-WP-0080/forge-custody-repair-proposal.json
Normal file
19
docs/changes/CUST-WP-0080/forge-custody-repair-proposal.json
Normal 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
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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."
|
||||
}
|
||||
}
|
||||
145
docs/changes/CUST-WP-0080/forge-template-operations.md
Normal file
145
docs/changes/CUST-WP-0080/forge-template-operations.md
Normal 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.
|
||||
63
docs/changes/CUST-WP-0080/forge-template-proposal.json
Normal file
63
docs/changes/CUST-WP-0080/forge-template-proposal.json
Normal 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
|
||||
}
|
||||
}
|
||||
43
docs/changes/CUST-WP-0080/probe_forge_installed.sh
Normal file
43
docs/changes/CUST-WP-0080/probe_forge_installed.sh
Normal 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'
|
||||
68
docs/changes/CUST-WP-0080/repair_forge_custody.py
Normal file
68
docs/changes/CUST-WP-0080/repair_forge_custody.py
Normal 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()
|
||||
59
docs/changes/CUST-WP-0080/restart_forge_custody.py
Normal file
59
docs/changes/CUST-WP-0080/restart_forge_custody.py
Normal 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()
|
||||
41
docs/changes/CUST-WP-0080/restore_forge_proposal.py
Normal file
41
docs/changes/CUST-WP-0080/restore_forge_proposal.py
Normal 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.')
|
||||
55
docs/changes/CUST-WP-0080/test_forge_custody.py
Normal file
55
docs/changes/CUST-WP-0080/test_forge_custody.py
Normal 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())
|
||||
69
docs/changes/CUST-WP-0080/test_forge_template_patch.py
Normal file
69
docs/changes/CUST-WP-0080/test_forge_template_patch.py
Normal 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)
|
||||
96
docs/changes/CUST-WP-0080/verify_forge_template.py
Normal file
96
docs/changes/CUST-WP-0080/verify_forge_template.py
Normal 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()
|
||||
|
|
@ -841,8 +841,8 @@
|
|||
"closure_evidence": [],
|
||||
"assessment": {
|
||||
"scope": "Reviewed workstation checkouts and 22 deployed Forgejo repositories; separate exact-image isolated lifecycle checks; no estate-wide conformance inferred",
|
||||
"observed": "Inherited generated-only hook allowed a synthetic private-key commit. New pinned scanner adapter detects that fixture in complete changed staged blobs with real-scanner tests. Actual configured hooks now enforce the pinned scanner on 22 exact workstation roots: clean accepted, disposable private key rejected, generated-only guard retained, unavailable scanner refused. Temporary alternate indexes/object stores preserved user state; unreviewed checkout behavior and routing stayed unchanged. Forge-owned introduced-history adapter now passes 33 source/protocol leaf cases with actual disposable pushes; exact-binary native activation is now verified on 22 Forgejo repositories. Actual SSH clean/detection probes passed under a temporary veto, with absent probe refs/quarantine and veto cleanup; fresh read-only hashes match. Ordinary source pushes to Custodian and railiance-forge succeeded. Separate exact-image isolated HTTP/API/merge, regeneration, Git-template inheritance and restart checks pass (23 assertions), with all earlier incomplete preparation attempts retained.",
|
||||
"remaining": "The other 143 inventoried repositories and other workstation clones remain uncovered. New-repository template is tested but not deployed; deployed HTTP/SSO/UI and Kubernetes lifecycle acceptance remain open. Import/mirror clone/fetch paths bypass receive-pack; historical, LFS, logs/artifacts/runtime coverage, CI authority and private detection-to-response are unverified.",
|
||||
"observed": "Inherited generated-only hook allowed a synthetic private-key commit. New pinned scanner adapter detects that fixture in complete changed staged blobs with real-scanner tests. Actual configured hooks now enforce the pinned scanner on 22 exact workstation roots: clean accepted, disposable private key rejected, generated-only guard retained, unavailable scanner refused. Temporary alternate indexes/object stores preserved user state; unreviewed checkout behavior and routing stayed unchanged. Forge-owned introduced-history adapter now passes 33 source/protocol leaf cases with actual disposable pushes; exact-binary native activation is now verified on 22 Forgejo repositories. Actual SSH clean/detection probes passed under a temporary veto, with absent probe refs/quarantine and veto cleanup; fresh read-only hashes match. Ordinary source pushes to Custodian and railiance-forge succeeded. Separate exact-image isolated HTTP/API/merge, regeneration, Git-template inheritance and restart checks pass (23 assertions), with all earlier incomplete preparation attempts retained. Exact template source was deployed on October 13: same-image generation 12 Ready, actual global Git template selection and regular/executable inheritance in a native temporary repository verified, all original 22 profiles/binaries preserved. Thirty isolated mounted-file/configuration/rollback checks and six conditional-patch regressions pass. The first live template rollout subsequently failed ordinary source acceptance: group-writable0775 scanner permissions were refused. Trial retained as failed. Recovery restored0755 and persisted a hash/owner-checked startup guard (railiance-apps50b66c4). Two Kubernetes replacements, generations13/14, completed the guard and passed inherited native clean/secret/quarantine/recovery probes. Six isolated custody checks pass; builtin authorization excluded in temporary native probes. See recovery summary for separate ordinary SSH acceptance.",
|
||||
"remaining": "Fresh inventory has 166 repositories: 22 installed profiles and 144 builtin-only; other workstation clones remain uncovered. Production authenticated HTTP/SSO/frontend creation remains open. Import/mirror clone/fetch bypass receive-pack; historical, LFS, logs/artifacts/runtime coverage, CI authority and private detection-to-response are unverified.",
|
||||
"owner_task": "RMGR-WP-0021-T01/T02/T03/T04; CUST-WP-0080-T03; FORGE-WP-0006-T02/T03",
|
||||
"verification_evidence": [
|
||||
"secret-detection.json",
|
||||
|
|
@ -851,7 +851,11 @@
|
|||
"receive-hook-activation.json",
|
||||
"../2026-10-13-netkingdom-security/receive-profile-recheck.json",
|
||||
"../2026-10-13-netkingdom-security/receive-clean-push.json",
|
||||
"../2026-10-13-netkingdom-security/forge-lifecycle-summary.json"
|
||||
"../2026-10-13-netkingdom-security/forge-lifecycle-summary.json",
|
||||
"../2026-10-13-netkingdom-security/forge-template-activation.json",
|
||||
"../2026-10-13-netkingdom-security/forge-template-isolated.json",
|
||||
"../2026-10-13-netkingdom-security/forge-hook-coverage-refresh.json",
|
||||
"../2026-10-13-netkingdom-security/forge-custody-recovery-summary.json"
|
||||
],
|
||||
"assessed_at": "2026-10-13"
|
||||
}
|
||||
|
|
@ -870,8 +874,8 @@
|
|||
"closure_evidence": [],
|
||||
"assessment": {
|
||||
"scope": "Reviewed workstation checkouts and 22 deployed Forgejo repositories; separate exact-image isolated lifecycle checks; no estate-wide conformance inferred",
|
||||
"observed": "Verified local hook wiring in 21 review repositories plus Custodian; source adapter covers changed Git index blobs only. Actual configured hooks now enforce the pinned scanner on 22 exact workstation roots: clean accepted, disposable private key rejected, generated-only guard retained, unavailable scanner refused. Temporary alternate indexes/object stores preserved user state; unreviewed checkout behavior and routing stayed unchanged. Forge-owned introduced-history adapter now passes 33 source/protocol leaf cases with actual disposable pushes; exact-binary native activation is now verified on 22 Forgejo repositories. Actual SSH clean/detection probes passed under a temporary veto, with absent probe refs/quarantine and veto cleanup; fresh read-only hashes match. Ordinary source pushes to Custodian and railiance-forge succeeded. Separate exact-image isolated HTTP/API/merge, regeneration, Git-template inheritance and restart checks pass (23 assertions), with all earlier incomplete preparation attempts retained.",
|
||||
"remaining": "The other 143 inventoried repositories and other workstation clones remain uncovered. New-repository template is tested but not deployed; deployed HTTP/SSO/UI and Kubernetes lifecycle acceptance remain open. Import/mirror clone/fetch paths bypass receive-pack; historical, LFS, logs/artifacts/runtime coverage, CI authority and private detection-to-response are unverified.",
|
||||
"observed": "Verified local hook wiring in 21 review repositories plus Custodian; source adapter covers changed Git index blobs only. Actual configured hooks now enforce the pinned scanner on 22 exact workstation roots: clean accepted, disposable private key rejected, generated-only guard retained, unavailable scanner refused. Temporary alternate indexes/object stores preserved user state; unreviewed checkout behavior and routing stayed unchanged. Forge-owned introduced-history adapter now passes 33 source/protocol leaf cases with actual disposable pushes; exact-binary native activation is now verified on 22 Forgejo repositories. Actual SSH clean/detection probes passed under a temporary veto, with absent probe refs/quarantine and veto cleanup; fresh read-only hashes match. Ordinary source pushes to Custodian and railiance-forge succeeded. Separate exact-image isolated HTTP/API/merge, regeneration, Git-template inheritance and restart checks pass (23 assertions), with all earlier incomplete preparation attempts retained. Exact template source was deployed on October 13: same-image generation 12 Ready, actual global Git template selection and regular/executable inheritance in a native temporary repository verified, all original 22 profiles/binaries preserved. Thirty isolated mounted-file/configuration/rollback checks and six conditional-patch regressions pass. The first live template rollout subsequently failed ordinary source acceptance: group-writable0775 scanner permissions were refused. Trial retained as failed. Recovery restored0755 and persisted a hash/owner-checked startup guard (railiance-apps50b66c4). Two Kubernetes replacements, generations13/14, completed the guard and passed inherited native clean/secret/quarantine/recovery probes. Six isolated custody checks pass; builtin authorization excluded in temporary native probes. See recovery summary for separate ordinary SSH acceptance.",
|
||||
"remaining": "Fresh inventory has 166 repositories: 22 installed profiles and 144 builtin-only; other workstation clones remain uncovered. Production authenticated HTTP/SSO/frontend creation remains open. Import/mirror clone/fetch bypass receive-pack; historical, LFS, logs/artifacts/runtime coverage, CI authority and private detection-to-response are unverified.",
|
||||
"owner_task": "RMGR-WP-0021-T01/T02/T03/T04; CUST-WP-0080-T03; FORGE-WP-0006-T02/T03",
|
||||
"verification_evidence": [
|
||||
"secret-detection.json",
|
||||
|
|
@ -880,7 +884,11 @@
|
|||
"receive-hook-activation.json",
|
||||
"../2026-10-13-netkingdom-security/receive-profile-recheck.json",
|
||||
"../2026-10-13-netkingdom-security/receive-clean-push.json",
|
||||
"../2026-10-13-netkingdom-security/forge-lifecycle-summary.json"
|
||||
"../2026-10-13-netkingdom-security/forge-lifecycle-summary.json",
|
||||
"../2026-10-13-netkingdom-security/forge-template-activation.json",
|
||||
"../2026-10-13-netkingdom-security/forge-template-isolated.json",
|
||||
"../2026-10-13-netkingdom-security/forge-hook-coverage-refresh.json",
|
||||
"../2026-10-13-netkingdom-security/forge-custody-recovery-summary.json"
|
||||
],
|
||||
"assessed_at": "2026-10-13"
|
||||
}
|
||||
|
|
@ -899,8 +907,8 @@
|
|||
"closure_evidence": [],
|
||||
"assessment": {
|
||||
"scope": "Reviewed workstation checkouts and 22 deployed Forgejo repositories; separate exact-image isolated lifecycle checks; no estate-wide conformance inferred",
|
||||
"observed": "A disposable secret commit succeeds through the inherited hook and is rejected through the candidate scanner hook; git commit --only and partial/alternate-index cases are tested. Actual configured hooks now enforce the pinned scanner on 22 exact workstation roots: clean accepted, disposable private key rejected, generated-only guard retained, unavailable scanner refused. Temporary alternate indexes/object stores preserved user state; unreviewed checkout behavior and routing stayed unchanged. Forge-owned introduced-history adapter now passes 33 source/protocol leaf cases with actual disposable pushes; exact-binary native activation is now verified on 22 Forgejo repositories. Actual SSH clean/detection probes passed under a temporary veto, with absent probe refs/quarantine and veto cleanup; fresh read-only hashes match. Ordinary source pushes to Custodian and railiance-forge succeeded. Separate exact-image isolated HTTP/API/merge, regeneration, Git-template inheritance and restart checks pass (23 assertions), with all earlier incomplete preparation attempts retained.",
|
||||
"remaining": "The other 143 inventoried repositories and other workstation clones remain uncovered. New-repository template is tested but not deployed; deployed HTTP/SSO/UI and Kubernetes lifecycle acceptance remain open. Import/mirror clone/fetch paths bypass receive-pack; historical, LFS, logs/artifacts/runtime coverage, CI authority and private detection-to-response are unverified.",
|
||||
"observed": "A disposable secret commit succeeds through the inherited hook and is rejected through the candidate scanner hook; git commit --only and partial/alternate-index cases are tested. Actual configured hooks now enforce the pinned scanner on 22 exact workstation roots: clean accepted, disposable private key rejected, generated-only guard retained, unavailable scanner refused. Temporary alternate indexes/object stores preserved user state; unreviewed checkout behavior and routing stayed unchanged. Forge-owned introduced-history adapter now passes 33 source/protocol leaf cases with actual disposable pushes; exact-binary native activation is now verified on 22 Forgejo repositories. Actual SSH clean/detection probes passed under a temporary veto, with absent probe refs/quarantine and veto cleanup; fresh read-only hashes match. Ordinary source pushes to Custodian and railiance-forge succeeded. Separate exact-image isolated HTTP/API/merge, regeneration, Git-template inheritance and restart checks pass (23 assertions), with all earlier incomplete preparation attempts retained. Exact template source was deployed on October 13: same-image generation 12 Ready, actual global Git template selection and regular/executable inheritance in a native temporary repository verified, all original 22 profiles/binaries preserved. Thirty isolated mounted-file/configuration/rollback checks and six conditional-patch regressions pass. The first live template rollout subsequently failed ordinary source acceptance: group-writable0775 scanner permissions were refused. Trial retained as failed. Recovery restored0755 and persisted a hash/owner-checked startup guard (railiance-apps50b66c4). Two Kubernetes replacements, generations13/14, completed the guard and passed inherited native clean/secret/quarantine/recovery probes. Six isolated custody checks pass; builtin authorization excluded in temporary native probes. See recovery summary for separate ordinary SSH acceptance.",
|
||||
"remaining": "Fresh inventory has 166 repositories: 22 installed profiles and 144 builtin-only; other workstation clones remain uncovered. Production authenticated HTTP/SSO/frontend creation remains open. Import/mirror clone/fetch bypass receive-pack; historical, LFS, logs/artifacts/runtime coverage, CI authority and private detection-to-response are unverified.",
|
||||
"owner_task": "RMGR-WP-0021-T01/T02/T03/T04; CUST-WP-0080-T03; FORGE-WP-0006-T02/T03",
|
||||
"verification_evidence": [
|
||||
"secret-detection.json",
|
||||
|
|
@ -909,7 +917,11 @@
|
|||
"receive-hook-activation.json",
|
||||
"../2026-10-13-netkingdom-security/receive-profile-recheck.json",
|
||||
"../2026-10-13-netkingdom-security/receive-clean-push.json",
|
||||
"../2026-10-13-netkingdom-security/forge-lifecycle-summary.json"
|
||||
"../2026-10-13-netkingdom-security/forge-lifecycle-summary.json",
|
||||
"../2026-10-13-netkingdom-security/forge-template-activation.json",
|
||||
"../2026-10-13-netkingdom-security/forge-template-isolated.json",
|
||||
"../2026-10-13-netkingdom-security/forge-hook-coverage-refresh.json",
|
||||
"../2026-10-13-netkingdom-security/forge-custody-recovery-summary.json"
|
||||
],
|
||||
"assessed_at": "2026-10-13"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"result": "verified",
|
||||
"checks": {
|
||||
"remount_0775_repaired": true,
|
||||
"already_correct_idempotent": true,
|
||||
"wrong_hash_refused_before_any_chmod": true,
|
||||
"unexpected_world_write_refused": true,
|
||||
"symlink_refused": true,
|
||||
"recovery_after_refusals": true
|
||||
},
|
||||
"scope": "Exact startup command and pinned Forgejo image; simulated fsGroup remount modes, no production mount.",
|
||||
"container_sha256": "cccb9999785fe0151a973d9a8b554975daf17c91432ed3e8735e432f3cd80e8f"
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"result": "classified-and-represented-as-structured-metadata",
|
||||
"findings": [
|
||||
{
|
||||
"file": "docs/changes/CUST-WP-0080/forge-template-proposal.json",
|
||||
"classification": "public ConfigMap source checksum false positive",
|
||||
"rule": "generic-api-key",
|
||||
"original_sha256": "7eddea55359b25c5ad307ed8ccd392dfadfe167d65cbe34b02f1955350bbcb36",
|
||||
"exact_original_reconstruction_verified": true
|
||||
},
|
||||
{
|
||||
"file": "docs/evidence/2026-10-13-netkingdom-security/forge-template-source-render.json",
|
||||
"classification": "public ConfigMap source checksum false positive",
|
||||
"rule": "generic-api-key",
|
||||
"original_sha256": "d3658a4dd8ea9c31bed5ce7fc99f2e1a79ac89e59d9e3dcebb3fb517409abd27",
|
||||
"exact_original_reconstruction_verified": true
|
||||
}
|
||||
],
|
||||
"credential_values_present": false,
|
||||
"detector_configuration_changed": false,
|
||||
"detector_exception_added": false,
|
||||
"original_bytes_retained": true
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"proposal_id": "FORGE6-TEMPLATE-LIFECYCLE-001",
|
||||
"revision": 2,
|
||||
"proposal_sha256": "7530423d7528b63434d896dfe28e5288d4e332167db817efa58c2255defe28d3",
|
||||
"pod_uid": "f818d3f5-5c3d-4a2a-8e78-50e10b10d2ce",
|
||||
"mode": "check",
|
||||
"result": "preflight-passed",
|
||||
"at": "2026-10-13T12:17:21.841158+00:00",
|
||||
"artifacts": [
|
||||
{
|
||||
"name": "receive-secret-scan",
|
||||
"stat": "775 1000 1000"
|
||||
},
|
||||
{
|
||||
"name": "gitleaks",
|
||||
"stat": "775 1000 1000"
|
||||
}
|
||||
],
|
||||
"push_acceptance": "pending",
|
||||
"restart_durability": "pending"
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
{
|
||||
"proposal_id": "FORGE6-TEMPLATE-LIFECYCLE-001",
|
||||
"original_outcome": "failed",
|
||||
"original_receipt_ref": "forge-template-activation.json",
|
||||
"original_receipt_sha256": "b7f84a6d2840e95d16b3f9ac6cc83d0b5e8c237f082c85f72ba84fa41d46f380",
|
||||
"failure": "Ordinary source push scanner-file-refused after generation12; artifacts0775, which scanner correctly rejects. Initial malformed-input/hash/health checks did not prove usability.",
|
||||
"recovery_result": "verified with refinement; original trial remains failed",
|
||||
"source_commit": "railiance-apps50b66c4e44d78c1fb1e89991439eda14722103b3",
|
||||
"recovery_receipts": [
|
||||
"forge-custody-repair.json",
|
||||
"forge-custody-isolated.json",
|
||||
"forge-custody-source-render.json",
|
||||
"forge-custody-rollout-dry-run.json",
|
||||
"forge-custody-rollout.json",
|
||||
"forge-custody-restart-dry-run.json",
|
||||
"forge-custody-restart.json"
|
||||
],
|
||||
"retained_preparation_failures": [
|
||||
"Revision3 initial server dry-run added omitted default termination fields; explicit defaults then matched.",
|
||||
"Revision3 restart-only dry-run refused local JSON Pointer mismatch; revision4 preserves annotations and matched. No live mutation occurred in either rejected dry-run."
|
||||
],
|
||||
"coverage": {
|
||||
"workstation_checkouts": 22,
|
||||
"server_installed_profiles": 22,
|
||||
"server_total_repositories": 166,
|
||||
"server_builtin_only": 144
|
||||
},
|
||||
"not_proven": [
|
||||
"Production authenticated HTTP/SSO/frontend creation",
|
||||
"Remaining144repository activation including gitlink compatibility",
|
||||
"Import/mirror fetch and broader history/LFS/log/artifact coverage",
|
||||
"CI change authority and private detection-to-response"
|
||||
],
|
||||
"at": "2026-10-13T12:29:15.704944+00:00",
|
||||
"promotion_evidence": false,
|
||||
"authenticated_ssh_after_final_restart": {
|
||||
"accepted": true,
|
||||
"source_commit": "2c11c366ceb6d95b4a2275e56f69a8c52d04072f",
|
||||
"route": "statehub fix-consistency --repo railiance-forge --no-writeback",
|
||||
"remote_head_verified": true,
|
||||
"sync_result": "warn",
|
||||
"sync_summary": {
|
||||
"fail": 0,
|
||||
"automation_error": 0,
|
||||
"warn": 7,
|
||||
"info": 0
|
||||
}
|
||||
},
|
||||
"remote_heads": {
|
||||
"railiance-forge": "2c11c366ceb6d95b4a2275e56f69a8c52d04072f",
|
||||
"railiance-apps": "50b66c4e44d78c1fb1e89991439eda14722103b3"
|
||||
},
|
||||
"verified_at": "2026-10-13T13:02:36.216871+00:00"
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"proposal_id": "FORGE6-TEMPLATE-LIFECYCLE-001",
|
||||
"revision": 2,
|
||||
"proposal_sha256": "7530423d7528b63434d896dfe28e5288d4e332167db817efa58c2255defe28d3",
|
||||
"pod_uid": "f818d3f5-5c3d-4a2a-8e78-50e10b10d2ce",
|
||||
"mode": "apply",
|
||||
"result": "modes-restored",
|
||||
"at": "2026-10-13T12:17:44.858130+00:00",
|
||||
"artifacts": [
|
||||
{
|
||||
"name": "receive-secret-scan",
|
||||
"stat": "755 1000 1000"
|
||||
},
|
||||
{
|
||||
"name": "gitleaks",
|
||||
"stat": "755 1000 1000"
|
||||
}
|
||||
],
|
||||
"push_acceptance": {
|
||||
"accepted": true,
|
||||
"commit": "ec1d7c591af4823ab42168fe3b6e940c8559392b",
|
||||
"route": "statehub fix-consistency --repo railiance-forge --no-writeback",
|
||||
"result": "pushed 1 backlogged commit; enforced hook unchanged"
|
||||
},
|
||||
"restart_durability": "pending"
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"proposal_id": "FORGE6-TEMPLATE-LIFECYCLE-001",
|
||||
"revision": 4,
|
||||
"proposal_sha256": "f6060bf7a5a604781a04b853775f72ba7ada5cc05c89a1da0df4fd480779004b",
|
||||
"at": "2026-10-13T12:26:48.093378+00:00",
|
||||
"before_pod_uid": "5c5b12d3-f029-4288-9803-16f56bebd5e0",
|
||||
"dry_run_verified": true,
|
||||
"result": "dry-run-verified"
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
{
|
||||
"proposal_id": "FORGE6-TEMPLATE-LIFECYCLE-001",
|
||||
"revision": 4,
|
||||
"proposal_sha256": "f6060bf7a5a604781a04b853775f72ba7ada5cc05c89a1da0df4fd480779004b",
|
||||
"at": "2026-10-13T12:27:16.105641+00:00",
|
||||
"before_pod_uid": "5c5b12d3-f029-4288-9803-16f56bebd5e0",
|
||||
"dry_run_verified": true,
|
||||
"result": "verified",
|
||||
"generation": 14,
|
||||
"after_pod_uid": "d4728052-b8df-4574-8a3e-74490a04472d",
|
||||
"guard_completed": true,
|
||||
"after_probe": {
|
||||
"artifacts": [
|
||||
{
|
||||
"name": "receive-secret-scan",
|
||||
"stat": "755 1000 1000"
|
||||
},
|
||||
{
|
||||
"name": "gitleaks",
|
||||
"stat": "755 1000 1000"
|
||||
}
|
||||
],
|
||||
"receive_probe": {
|
||||
"inherited_installed_hook": true,
|
||||
"clean_push": true,
|
||||
"intermediate_secret_refused": true,
|
||||
"quarantine_discarded": true,
|
||||
"clean_recovery": true,
|
||||
"builtin_authorization_excluded": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
{
|
||||
"proposal_id": "FORGE6-TEMPLATE-LIFECYCLE-001",
|
||||
"revision": 3,
|
||||
"proposal_sha256": "b9e8d82331ac2a7fc4b12b5524d486809d03b82f52dc96b31e8ae5e73b93c37f",
|
||||
"at": "2026-10-13T12:22:45.963893+00:00",
|
||||
"mode": "dry-run",
|
||||
"dry_run_verified": true,
|
||||
"before_pod_uid": "f818d3f5-5c3d-4a2a-8e78-50e10b10d2ce",
|
||||
"result": "dry-run-verified",
|
||||
"before_probe": {
|
||||
"artifacts": [
|
||||
{
|
||||
"name": "receive-secret-scan",
|
||||
"stat": "755 1000 1000"
|
||||
},
|
||||
{
|
||||
"name": "gitleaks",
|
||||
"stat": "755 1000 1000"
|
||||
}
|
||||
],
|
||||
"receive_probe": {
|
||||
"inherited_installed_hook": true,
|
||||
"clean_push": true,
|
||||
"intermediate_secret_refused": true,
|
||||
"quarantine_discarded": true,
|
||||
"clean_recovery": true,
|
||||
"builtin_authorization_excluded": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
{
|
||||
"proposal_id": "FORGE6-TEMPLATE-LIFECYCLE-001",
|
||||
"revision": 3,
|
||||
"proposal_sha256": "b9e8d82331ac2a7fc4b12b5524d486809d03b82f52dc96b31e8ae5e73b93c37f",
|
||||
"at": "2026-10-13T12:23:38.883450+00:00",
|
||||
"mode": "apply",
|
||||
"dry_run_verified": true,
|
||||
"before_pod_uid": "f818d3f5-5c3d-4a2a-8e78-50e10b10d2ce",
|
||||
"result": "verified",
|
||||
"generation": 13,
|
||||
"after_pod_uid": "5c5b12d3-f029-4288-9803-16f56bebd5e0",
|
||||
"guard_completed": true,
|
||||
"after_probe": {
|
||||
"artifacts": [
|
||||
{
|
||||
"name": "receive-secret-scan",
|
||||
"stat": "755 1000 1000"
|
||||
},
|
||||
{
|
||||
"name": "gitleaks",
|
||||
"stat": "755 1000 1000"
|
||||
}
|
||||
],
|
||||
"receive_probe": {
|
||||
"inherited_installed_hook": true,
|
||||
"clean_push": true,
|
||||
"intermediate_secret_refused": true,
|
||||
"quarantine_discarded": true,
|
||||
"clean_recovery": true,
|
||||
"builtin_authorization_excluded": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"result": "verified",
|
||||
"exact_init_rendered": true,
|
||||
"rollback_restores_original_template": true,
|
||||
"values_sha256": "cf87a3dccd666bf8870619755afac4af64fc0bee77b0f0ea54718d6384b70d4f"
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"observed_at": "2026-10-13T12:06:05.310048+00:00",
|
||||
"pod": "forgejo-gitea-77cdff66d5-5jpjl",
|
||||
"pod_uid": "f818d3f5-5c3d-4a2a-8e78-50e10b10d2ce",
|
||||
"repository_count": 166,
|
||||
"counts": {
|
||||
"reviewed_profile": 22,
|
||||
"builtin_only": 144,
|
||||
"other_hook_inventory": 0,
|
||||
"shallow": 0,
|
||||
"partial": 0
|
||||
},
|
||||
"private_inventory_sha256": "38476667ce2b74d8701b9d5ce165fcecba12df5cd46149996aadb19765441d46",
|
||||
"scope": "Read-only exact hook hashes and shallow/partial metadata for all current database repositories. No content scan, authorization or behavior proof for the unactivated repositories. No unknown hook replaced."
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"observed_at": "2026-10-13T12:04:04.828055+00:00",
|
||||
"host": "railiance01",
|
||||
"pod": "forgejo-gitea-77cdff66d5-5jpjl",
|
||||
"pod_uid": "f818d3f5-5c3d-4a2a-8e78-50e10b10d2ce",
|
||||
"private_inventory_sha256": "b230efc3c3b620d5104a68a9d2238049c548d032e9eaab68411f4e4c5987b618",
|
||||
"database_repositories": 166,
|
||||
"filesystem_repositories": 166,
|
||||
"missing_database_directories": 0,
|
||||
"unmapped_directories": 0,
|
||||
"private": 12,
|
||||
"mirrors": 0,
|
||||
"forks": 0,
|
||||
"archived": 0,
|
||||
"empty": 1,
|
||||
"wikis": 0,
|
||||
"protected_branch_rules": 0,
|
||||
"scope": "Metadata only; full repository identities kept in a mode-0600 local observation, not committed. No repository content or credential values read."
|
||||
}
|
||||
|
|
@ -0,0 +1,100 @@
|
|||
{
|
||||
"proposal_id": "FORGE6-TEMPLATE-LIFECYCLE-001",
|
||||
"proposal_sha256": "7eddea55359b25c5ad307ed8ccd392dfadfe167d65cbe34b02f1955350bbcb36",
|
||||
"observed_at": "2026-10-13T12:00:12.863351+00:00",
|
||||
"result": "verified",
|
||||
"old_pod_uid": "bc771b51-e9b8-456d-b59c-e7041704389d",
|
||||
"old_generation": 11,
|
||||
"server_dry_run_exact_match": true,
|
||||
"patch": [
|
||||
{
|
||||
"op": "test",
|
||||
"path": "/metadata/uid",
|
||||
"value": "e8eff0aa-64bb-4b44-9a80-a66612986355"
|
||||
},
|
||||
{
|
||||
"op": "test",
|
||||
"path": "/metadata/resourceVersion",
|
||||
"value": "67848734"
|
||||
},
|
||||
{
|
||||
"op": "add",
|
||||
"path": "/spec/template/spec/volumes/-",
|
||||
"value": {
|
||||
"name": "secret-scan-template",
|
||||
"configMap": {
|
||||
"name": "forgejo-secret-template-b1d7cf8cd13f",
|
||||
"defaultMode": 365
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"op": "add",
|
||||
"path": "/spec/template/spec/initContainers/0/volumeMounts/-",
|
||||
"value": {
|
||||
"name": "secret-scan-template",
|
||||
"mountPath": "/opt/forge-secret-template/hooks/pre-receive.d/10-custodian-secret-scan",
|
||||
"subPath": "hook",
|
||||
"readOnly": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"op": "add",
|
||||
"path": "/spec/template/spec/initContainers/1/volumeMounts/-",
|
||||
"value": {
|
||||
"name": "secret-scan-template",
|
||||
"mountPath": "/opt/forge-secret-template/hooks/pre-receive.d/10-custodian-secret-scan",
|
||||
"subPath": "hook",
|
||||
"readOnly": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"op": "add",
|
||||
"path": "/spec/template/spec/initContainers/1/env/-",
|
||||
"value": {
|
||||
"name": "GITEA__GIT_0X2E_CONFIG__INIT_0X2E_TEMPLATEDIR",
|
||||
"value": "/opt/forge-secret-template"
|
||||
}
|
||||
},
|
||||
{
|
||||
"op": "add",
|
||||
"path": "/spec/template/spec/initContainers/2/volumeMounts/-",
|
||||
"value": {
|
||||
"name": "secret-scan-template",
|
||||
"mountPath": "/opt/forge-secret-template/hooks/pre-receive.d/10-custodian-secret-scan",
|
||||
"subPath": "hook",
|
||||
"readOnly": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"op": "add",
|
||||
"path": "/spec/template/spec/containers/0/volumeMounts/-",
|
||||
"value": {
|
||||
"name": "secret-scan-template",
|
||||
"mountPath": "/opt/forge-secret-template/hooks/pre-receive.d/10-custodian-secret-scan",
|
||||
"subPath": "hook",
|
||||
"readOnly": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"original_template_sha256": "18855f7b4c78f79117884684e44550406c72fc7be08c0b8e61ec16cfcb855746",
|
||||
"resulting_template_sha256": "73f6c1ef484c60f93285d61e6df6651b4f1268fafb3e470f35f450bce4f34aba",
|
||||
"no_secret_objects_read": true,
|
||||
"mode": "apply",
|
||||
"configmap_created": true,
|
||||
"patch_submission_started": true,
|
||||
"applied_generation": 12,
|
||||
"inheritance": {
|
||||
"mounted_hook_regular": true,
|
||||
"inherited_hook_regular_executable": true,
|
||||
"hook_sha256": "b1d7cf8cd13feafe44e45876b1de3fdd9c88339d8d45791fa2b9f72ca832932b",
|
||||
"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."
|
||||
},
|
||||
"new_pod_uid": "f818d3f5-5c3d-4a2a-8e78-50e10b10d2ce",
|
||||
"generation": 12,
|
||||
"selected_template": "/opt/forge-secret-template",
|
||||
"existing_22_profiles_preserved": true,
|
||||
"binaries_unchanged": true,
|
||||
"health_after": true
|
||||
}
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
{
|
||||
"proposal_id": "FORGE6-TEMPLATE-LIFECYCLE-001",
|
||||
"proposal_sha256": "7eddea55359b25c5ad307ed8ccd392dfadfe167d65cbe34b02f1955350bbcb36",
|
||||
"observed_at": "2026-10-13T11:57:41.978702+00:00",
|
||||
"result": "dry-run-verified",
|
||||
"old_pod_uid": "bc771b51-e9b8-456d-b59c-e7041704389d",
|
||||
"old_generation": 11,
|
||||
"server_dry_run_exact_match": true,
|
||||
"patch": [
|
||||
{
|
||||
"op": "test",
|
||||
"path": "/metadata/uid",
|
||||
"value": "e8eff0aa-64bb-4b44-9a80-a66612986355"
|
||||
},
|
||||
{
|
||||
"op": "test",
|
||||
"path": "/metadata/resourceVersion",
|
||||
"value": "67848734"
|
||||
},
|
||||
{
|
||||
"op": "add",
|
||||
"path": "/spec/template/spec/volumes/-",
|
||||
"value": {
|
||||
"name": "secret-scan-template",
|
||||
"configMap": {
|
||||
"name": "forgejo-secret-template-b1d7cf8cd13f",
|
||||
"defaultMode": 365
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"op": "add",
|
||||
"path": "/spec/template/spec/initContainers/0/volumeMounts/-",
|
||||
"value": {
|
||||
"name": "secret-scan-template",
|
||||
"mountPath": "/opt/forge-secret-template/hooks/pre-receive.d/10-custodian-secret-scan",
|
||||
"subPath": "hook",
|
||||
"readOnly": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"op": "add",
|
||||
"path": "/spec/template/spec/initContainers/1/volumeMounts/-",
|
||||
"value": {
|
||||
"name": "secret-scan-template",
|
||||
"mountPath": "/opt/forge-secret-template/hooks/pre-receive.d/10-custodian-secret-scan",
|
||||
"subPath": "hook",
|
||||
"readOnly": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"op": "add",
|
||||
"path": "/spec/template/spec/initContainers/1/env/-",
|
||||
"value": {
|
||||
"name": "GITEA__GIT_0X2E_CONFIG__INIT_0X2E_TEMPLATEDIR",
|
||||
"value": "/opt/forge-secret-template"
|
||||
}
|
||||
},
|
||||
{
|
||||
"op": "add",
|
||||
"path": "/spec/template/spec/initContainers/2/volumeMounts/-",
|
||||
"value": {
|
||||
"name": "secret-scan-template",
|
||||
"mountPath": "/opt/forge-secret-template/hooks/pre-receive.d/10-custodian-secret-scan",
|
||||
"subPath": "hook",
|
||||
"readOnly": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"op": "add",
|
||||
"path": "/spec/template/spec/containers/0/volumeMounts/-",
|
||||
"value": {
|
||||
"name": "secret-scan-template",
|
||||
"mountPath": "/opt/forge-secret-template/hooks/pre-receive.d/10-custodian-secret-scan",
|
||||
"subPath": "hook",
|
||||
"readOnly": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"original_template_sha256": "18855f7b4c78f79117884684e44550406c72fc7be08c0b8e61ec16cfcb855746",
|
||||
"resulting_template_sha256": "73f6c1ef484c60f93285d61e6df6651b4f1268fafb3e470f35f450bce4f34aba",
|
||||
"no_secret_objects_read": true,
|
||||
"mode": "preflight"
|
||||
}
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
{
|
||||
"observed_at": "2026-10-13T11:48:57.834001+00:00",
|
||||
"image": "code.forgejo.org/forgejo/forgejo@sha256:691657c7e64af7730a2e885e6d022cf16ae60724931c5392bd61838afadcaeba",
|
||||
"result": "verified",
|
||||
"checks": {
|
||||
"synthetic_identity_authenticated": true,
|
||||
"native_repository_created": true,
|
||||
"new_repository_missing_profile_without_template": true,
|
||||
"http_push_clean": true,
|
||||
"http_push_blocked": true,
|
||||
"http_quarantine_discarded": true,
|
||||
"api_accepted_http_status": 201,
|
||||
"api_accepted": true,
|
||||
"api_refused_http_status": 500,
|
||||
"api_refused": true,
|
||||
"api_refusal_has_scanner_detection": true,
|
||||
"rejected_api_content_absent": true,
|
||||
"feature_branch_created": true,
|
||||
"feature_content_created": true,
|
||||
"pull_request_created": true,
|
||||
"merge_refusal_status": 409,
|
||||
"merge_message_secret_refused": true,
|
||||
"merge_refusal_has_scanner_detection": true,
|
||||
"rejected_merge_not_applied": true,
|
||||
"clean_merge_accepted": true,
|
||||
"regeneration_preserves_exact_profile": true,
|
||||
"encoded_setting_selected_template": true,
|
||||
"mounted_hook_regular_executable": true,
|
||||
"native_repository_with_template_created": true,
|
||||
"new_repository_inherits_exact_profile": true,
|
||||
"restart_preserves_coverage_profile": true,
|
||||
"restart_preserves_templated_profile": true,
|
||||
"inherited_profile_blocks_after_restart": true,
|
||||
"encoded_setting_selected_default": true,
|
||||
"rollback_new_repository_created": true,
|
||||
"rollback_restores_default_creation_behavior": true,
|
||||
"rollback_preserves_existing_coverage_hook": true,
|
||||
"rollback_preserves_existing_templated_hook": true
|
||||
},
|
||||
"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.",
|
||||
"identity_http_status": 200,
|
||||
"repository_create_http_status": 201,
|
||||
"repository_create_error_kinds": [
|
||||
"email"
|
||||
],
|
||||
"isolated_container_and_volumes_removed": true,
|
||||
"last_phase": "rollback-default-template",
|
||||
"executor_sha256": "2e7b431b5f0f270d8bce5de21fec2d0f8278a26ad9014f1f7a341c47e24ab83b",
|
||||
"base_executor_sha256": "88bbf4235086b7ffc40ca4d7df7fafa1d67e2e1b41d2f0055ec82421ed4840cb"
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
{
|
||||
"archival_format": "json-checksum-map-entries-v1",
|
||||
"original_sha256": "d3658a4dd8ea9c31bed5ce7fc99f2e1a79ac89e59d9e3dcebb3fb517409abd27",
|
||||
"converted_map_field": "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": {
|
||||
"chart": "gitea-12.5.0",
|
||||
"chart_sha256": "2d6f35f3d1ff0cfcab0f22142d20a1d6405fa974a79d7a18000904d233131823",
|
||||
"source_hashes": [
|
||||
{
|
||||
"path": "helm/forgejo-values.yaml",
|
||||
"sha256": "e97aa62fe767f4dc55f2c68c7ae1b69e61e632826a8ba3ccb4ffaca6e302174c"
|
||||
},
|
||||
{
|
||||
"path": "Makefile",
|
||||
"sha256": "6bd20555631eb4c22e0729779d6c24d6888a4e6d555ab1f03083697016d42b15"
|
||||
},
|
||||
{
|
||||
"path": "manifests/forgejo-secret-scan-template.yaml",
|
||||
"sha256": "8fad71aff5a25d8e31289072b9c06c6ec74ac74fedafc54dde7177bfe187c96a"
|
||||
}
|
||||
],
|
||||
"rendered_target": "Deployment forgejo/forgejo-gitea",
|
||||
"file_mounts_checked": [
|
||||
"gitea",
|
||||
"init-directories",
|
||||
"init-app-ini",
|
||||
"configure-gitea"
|
||||
],
|
||||
"encoded_setting": {
|
||||
"name": "GITEA__GIT_0X2E_CONFIG__INIT_0X2E_TEMPLATEDIR",
|
||||
"value": "/opt/forge-secret-template"
|
||||
},
|
||||
"scope": "Nonsecret owner source render only. No production config/Secret read. Live rollout uses a narrowly scoped patch, not full Helm reapplication."
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"observed_at": "2026-10-13T12:08:04.722236+00:00",
|
||||
"repository_count": 166,
|
||||
"default_trees_without_gitlinks": 164,
|
||||
"default_trees_with_gitlinks": 1,
|
||||
"empty_repositories": 1,
|
||||
"private_inventory_sha256": "5ea46926a62c5626111a8b2c7e1f6080baaae2af20b5da0c3bdc2a457b9ffe73",
|
||||
"scope": "Only current default-branch tree modes examined. No source blobs read, no credential scan, no claim about other refs or past history. A gitlink count flags compatibility for the current refusal behavior."
|
||||
}
|
||||
|
|
@ -15,7 +15,7 @@ No whole-estate SEC-DSM conformance or maturity level is claimed.
|
|||
| G04 | Credential inventory is incomplete and its five metadata fields do not by themselves establish full SEC-DSM lifecycle coverage. Mason's dated survey found 49 paths; coverage is not current estate-wide discovery. | MASON-WP-0004-T03/T04; RPF-WP-0035; CUST-WP-0080-T03 | Current metadata-only discovery denominator; owners/consumers/scopes; rotation and revocation procedures; unknown entries remain findings |
|
||||
| G05 | Agent effective authority and credential isolation are unproven. Glas declares no ready profile; the Custodian record grants no autopilot authority. | CUST-WP-0073-T02/T04; GLAS-WP-0012 | Actual admitted agent identity, runtime read-denial and tool-effect restrictions, task expiry/cancel cleanup, no cross-task credential accumulation |
|
||||
| G06 | Native evidence delivery, source completeness and attribution remain incomplete. Independent archive custody is not source completeness. | AUDIT-WP-0013/0014/0015; RPF-WP-0035-T08; existing FLEX-WP-0031 and NK-WP-0035/0040 residual records | Admitted sender, durable outbox, native archive readback, heartbeat/reconciliation and failure recovery. Residual work requires explicit promotion |
|
||||
| G07 | The original generated-only hook accepted a disposable private-key commit. The pinned scanner is now enforced by actual configured hooks on 22 exact workstation checkouts; clean, synthetic-secret, generated-only and unavailable-scanner probes pass. The additive forge scanner is installed on the same 22 named repositories and SSH refusal probes passed; ordinary acceptance, other write paths, broader forge/CI coverage and response remain unverified. | RMGR-WP-0021-T01/T02 source and local activation done; T03 forge/CI via FORGE-WP-0006, T04 response open; CUST-WP-0080-T03 coordinates railiance-forge ownership | Deterministic production-repo inventory; actual prevention/detection runs; scanner findings kept private; finding-to-revocation procedure |
|
||||
| G07 | The original generated-only hook accepted a disposable private-key commit. The pinned scanner is now enforced by actual configured hooks on 22 exact workstation checkouts; clean, synthetic-secret, generated-only and unavailable-scanner probes pass. The additive forge scanner is installed on the same 22 named repositories and SSH refusal probes passed; ordinary SSH acceptance is verified; other deployed write paths, broader forge/CI coverage and response remain unverified. | RMGR-WP-0021-T01/T02 source and local activation done; T03 forge/CI via FORGE-WP-0006, T04 response open; CUST-WP-0080-T03 coordinates railiance-forge ownership | Deterministic production-repo inventory; actual prevention/detection runs; scanner findings kept private; finding-to-revocation procedure |
|
||||
| G08 | Legacy root export and generation-prefix disclosure reproduced and removed in source. Bootstrap now uses a registered public recipient, explicit temporary-key resume and an encrypted handoff bound to the bundle digest. Other custody callers and native recovery acceptance remain unverified. | NK-WP-0049 T01 repaired; T02 caller reconciliation; T03 native recovery. NK-WP-0045 covers only the non-secret root-grant record; CUST-WP-0073 retains isolation/incident work | Inspect actual bootstrap callers; stop secret output reaching model/logs; owner-reviewed safe recovery procedure and tests |
|
||||
| G09 | Rotation and revocation closure are incomplete: existing exposure rotation is founder-deferred, npm migration remains open, and five-field descriptions do not prove consumer adoption. | CUST-WP-0073-T05; SECRETS-WP-0006-T06; WARDEN-WP-0037-T03; RPF-WP-0035-T07 | For each relevant credential: new consumer accepted, old credential refused, copies reconciled; explicit DSM exception if still deferred |
|
||||
| G10 | Build-mode exceptions and close-down checklist do not uniformly contain DSM-EXC requirement, risk, compensating controls and review fields. | CUST-WP-0080-T03; existing security-close-down checklist and October 11 decisions | Requirement-level exception records with real approval references and revalidation; no invented approval or retroactive conformance |
|
||||
|
|
@ -145,7 +145,33 @@ hook omission is reproduced; a Git-template candidate remedies it in isolation.
|
|||
retain all failed preparation attempts and distinguish these tests from deployed
|
||||
PostgreSQL/SSO/ingress and Kubernetes acceptance. Import/mirror clone/fetch paths
|
||||
bypass receive-pack. Remaining repository and response coverage is now also on
|
||||
close-down checklist row 21. The tested template has not been deployed.
|
||||
close-down checklist row 21. At that test stage the template had not been deployed; subsequent rollout and recovery are recorded below.
|
||||
|
||||
G07 template deployed on October 13: source railiance-apps `390a30a` and exact
|
||||
FORGE6-TEMPLATE-LIFECYCLE-001 reached generation 12 Ready using a conditional
|
||||
additive patch. Read-only file mounts avoid copying ConfigMap symlinks into Git
|
||||
hooks. Actual Forgejo Git configuration now selects the template; native Git init
|
||||
inherited the exact executable hook and malformed input refused. Original 22
|
||||
profiles and binaries remain exact. [Deployment receipt](evidence/2026-10-13-netkingdom-security/forge-template-activation.json).
|
||||
Thirty isolated configuration/lifecycle/rollback checks and six patch regressions
|
||||
passed; explicit-default rollback is documented in the linked proposal.
|
||||
[Fresh inventory](evidence/2026-10-13-netkingdom-security/forge-inventory-refresh.json)
|
||||
now has 166 repositories, with [22 installed and 144 builtin-only hook inventories](evidence/2026-10-13-netkingdom-security/forge-hook-coverage-refresh.json).
|
||||
No unknown custom hooks were found. Production API/SSO/frontend acceptance,
|
||||
existing-repository expansion and imported/mirrored content/CI/response coverage
|
||||
remain open; inherited hooks do not scan fetch or historical content.
|
||||
|
||||
G07 custody recovery on October 13: the generation-12 rollout later failed ordinary
|
||||
source acceptance because scanner permissions were 0775. The earlier health,
|
||||
hash and malformed-input checks did not prove scanner usability. The exact trial
|
||||
is retained as failed. Revisions 2–4 restored 0755 and deployed a hash/owner-checked
|
||||
startup guard in owner source `50b66c4`. Two actual Kubernetes replacements
|
||||
(generations 13 and 14) completed the guard and passed inherited native clean
|
||||
acceptance, intermediate-secret refusal, quarantine removal and clean recovery.
|
||||
Six isolated custody cases also pass. [Recovery evidence](evidence/2026-10-13-netkingdom-security/forge-custody-recovery-summary.json)
|
||||
supersedes the initial acceptance claim without discarding its original receipt.
|
||||
The temporary native probes exclude builtin authorization; ordinary source pushes
|
||||
supply separate SSH evidence. G07 and all wider coverage requirements remain open.
|
||||
|
||||
## SEC DSM requirement coverage
|
||||
|
||||
|
|
|
|||
|
|
@ -27,4 +27,4 @@ Add a row whenever you loosen, shortcut or leave unarmed a control. Remove nothi
|
|||
| 18 | Ordinary authenticated operator/PDP admission for User Engine application.register | Executed one-use exception USER-CATALOGUE-INFDEC-20261011-R1 under confirmed founder approval b859698b-5d08-4da5-9599-4760c6a799f1 (02:07:09Z). Exact executor b0dbbb97 and transport3e594e0e; packet307c4fc5, grant SHA 72ee846034809f4ae6848eac3a43f2b7390f1df7a67ede50eff7435d1b43f839. Two exact informed-decision catalogue records committed; genuine denial retained, three truthful audits, one consumed ledger and unsent/unclaimed registered event verified. Private grant removed. Ordinary management admission remains unmet; no IAM/human claim/future registration/targeted replay authority. Execution deadline2026-10-12T23:59:59Z does not erase facts. [Outcome](evidence/2026-10-10-security-consolidation/user-catalogue-independent-postapply-verification.json). | Before customer production, supported authenticated operator management with genuine ordinary allow/deny/attribution and unavailable refusal; retire bootstrap use, preserve/reconcile ledger/audits/events. Broader USER-WP-0026-T03/USER-WP-0028-T02 remain live; no human journey completion inferred. | user-engine, access-engine; Custodian tracks | 2026-10-11 |
|
||||
| 19 | Fresh informed-decision readback during a failed T05 rollout | Exact new trial INFD-WP-0009-T05/zlib85091-r1 approved under founder acceptance b5ce337c-5d7d-44fa-864d-706b54039f7c at2026-10-11T10:40:55Z. Image f558/source312ab97 verified Ready and actual canonical lifecycle HTTP200 at11:20:25Z, with latest database preserved. Proposed bounded30minute manual outage recovery was expressly accepted but never invoked; trial completed. Original proposed packet and pre-execution refusals remain immutable. [Outcome](evidence/2026-10-10-security-consolidation/zlib-native-completed/canonical-provider-verification.json). | Completed2026-10-11. Ordinary fresh-authority gate retained; no standing cached authority, autonomy promotion, other-trial or customer-production reuse. Any future standing recovery mechanism needs independently obtainable authority and separate review. | informed-decision, railiance-platform; Custodian tracks | 2026-10-11 |
|
||||
| 20 | Unattended origin publication for assembly-economics checkpoints | rein-aharness approach `assembly-checkpoint` pushes to `coulomb/assembly-economics` origin without an attended session, limited to `checkpoints/`, fast-forward only, rebase-once, never force (mirrors the FI brief grant). Founder-approved 2026-10-12: assembly-economics `docs/decisions/2026-10-12-checkpoint-origin-publication.md`. Runs on legacy profile-absent routing until 2026-12-31. | Publication through rein-aharness's governed publish lane with a per-repo, path-scoped grant and a governed execution profile; legacy route removed. | rein-aharness / assembly-economics | 2026-10-12 |
|
||||
| 21 | Independent repository secret detection and response (CUST-WP-0080 G07) | Scanner active on 22 workstation checkouts and 22 Forgejo repositories; 143 other repositories remain uncovered. New repositories lack the hook by default. Exact-image HTTP/API/merge and regeneration/restart checks pass in isolation; Git-template inheritance is tested but not deployed. Import/mirror fetch, historical/LFS/log/artifact coverage and private finding response remain open. [Evidence](evidence/2026-10-13-netkingdom-security/forge-lifecycle-summary.json). | Complete FORGE-WP-0006 and RMGR-WP-0021: admit the full repository denominator, persist/reconcile hooks across creation and lifecycle, verify deployed write paths and unsupported input handling, and prove private detection-to-response with existing custody owners. Rejection alone does not revoke prior exposure; CUST-WP-0073-T05 remains founder-deferred. | railiance-forge, repo-manager; Custodian coordinates | 2026-10-13 |
|
||||
| 21 | Independent repository secret detection and response (CUST-WP-0080 G07) | Scanner active on 22 workstation checkouts and 22 Forgejo repositories; 144 of the refreshed 166 repositories remain builtin-only. Template inheritance deployed October 13; its first rollout broke scanner custody permissions and is retained as failed. Recovery adds a pinned startup guard; two Kubernetes replacements (generations 13/14) pass 0755 custody and native clean/secret/quarantine/recovery checks. Thirty isolated configuration/lifecycle/rollback checks pass; authenticated production API/SSO/frontend acceptance remains open. Import/mirror fetch, historical/LFS/log/artifact coverage and private finding response remain open. [Recovery evidence](evidence/2026-10-13-netkingdom-security/forge-custody-recovery-summary.json). | Complete FORGE-WP-0006 and RMGR-WP-0021: admit the full repository denominator, persist/reconcile hooks across creation and lifecycle, verify deployed write paths and unsupported input handling, and prove private detection-to-response with existing custody owners. Rejection alone does not revoke prior exposure; CUST-WP-0073-T05 remains founder-deferred. | railiance-forge, repo-manager; Custodian coordinates | 2026-10-13 |
|
||||
|
|
|
|||
|
|
@ -234,6 +234,55 @@ scanner passes the revised staged source; no suppression or exception was added.
|
|||
`scanner-triage.json` retains value-free classification and resolution evidence;
|
||||
this does not close RMGR-WP-0021-T04's credential-response requirement.
|
||||
|
||||
2026-10-13 G07 template deployment: source railiance-apps `390a30a` adds the
|
||||
immutable non-secret ConfigMap, read-only file subPath mounts, encoded Git
|
||||
configuration and deployment prerequisite. Thirty isolated file-mount/config/
|
||||
rollback checks and six conditional-patch regressions pass. Exact proposal
|
||||
FORGE6-TEMPLATE-LIFECYCLE-001 passed the server dry-run and its additive live patch
|
||||
reached generation 12 Ready on the unchanged image. Actual Forgejo Git config
|
||||
selects the template; a native temporary Git repository inherited the regular,
|
||||
executable exact hook and refused malformed input. All 22 original hook inventories
|
||||
and both binaries remain unchanged; health passes and probe data was removed.
|
||||
Existing admin identity and initial-only password mode were checked before restart.
|
||||
No credential, identity, resource request or policy scope was changed. The first
|
||||
owner-source sync hit the restart window; retry pushed `390a30a` successfully.
|
||||
Receipt: `docs/evidence/2026-10-13-netkingdom-security/forge-template-activation.json`;
|
||||
proposal, guarded rollback and source reconciliation instructions are in
|
||||
`docs/changes/CUST-WP-0080/forge-template-operations.md`.
|
||||
|
||||
Fresh metadata now reconciles 166 database repositories and directories (one more
|
||||
than October 12): all hook inventories are known; 22 have the admitted profile,
|
||||
144 are builtin-only. No mirrors, forks or wikis are present; no protected-branch
|
||||
rules are configured. Full identity inventories remain in mode-0600 local files;
|
||||
committed receipts contain aggregates only. Production API/SSO/frontend creation,
|
||||
existing-repository expansion, import/mirror content paths, CI and response
|
||||
acceptance remain open; T03/T04 and the owner plan are not closed by this rollout.
|
||||
|
||||
2026-10-13 adverse acceptance and recovery: the initial generation-12 health,
|
||||
hash and malformed-input checks missed a real restart failure. The next ordinary
|
||||
Forge owner push refused with `scanner-file-refused`; both installed artifacts
|
||||
had become group-writable 0775, consistent with Kubernetes fsGroup volume
|
||||
processing. The original trial remains failed in supervision; its original
|
||||
proposal and receipt are retained, not rewritten as an unchanged success.
|
||||
Revision 2 verified exact hashes/ownership and restored only 0755. The queued
|
||||
Forge owner source push `ec1d7c5` then succeeded through the enforced hook.
|
||||
Revision 3 adds an exact-image unprivileged startup guard, mounted only to the
|
||||
existing security subtree, persisted in railiance-apps `50b66c4`. Six isolated
|
||||
checks cover remount permissions, idempotence, wrong hashes, unsafe modes,
|
||||
symlinks and recovery. Server dry-run and rendered source match; rollback removes
|
||||
only that init container and explicitly repairs modes after remount.
|
||||
Generation 13 completed the guard and passed real inherited native receive
|
||||
checks: clean acceptance, intermediate-secret refusal, quarantine removal and
|
||||
clean recovery. Builtin authorization is excluded in the temporary native probe;
|
||||
the ordinary source push separately exercises authenticated SSH. A second
|
||||
Kubernetes replacement reached generation 14 and repeated all those checks with
|
||||
a different pod UID; both startup guards exited zero and both binaries remained
|
||||
0755. Authenticated SSH source acceptance after that replacement is recorded
|
||||
separately in the recovery summary.
|
||||
Evidence: `forge-custody-repair.json`, `forge-custody-rollout.json` and
|
||||
`forge-custody-restart.json` in the October 13 evidence directory. This resolves
|
||||
an availability regression without weakening scanner refusal or widening access.
|
||||
|
||||
## Verify native closure against every requirement
|
||||
|
||||
```task
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue