From bb3e2db07ef945a4ab74b7f432284b80678701f3 Mon Sep 17 00:00:00 2001 From: codex Date: Tue, 13 Oct 2026 19:49:24 +0200 Subject: [PATCH] security: record KeyCape custody caller reconciliation and native config proof --- .../keycape_bootstrap_native_test.go | 23 +++ .../CUST-WP-0080/verify_keycape_bootstrap.py | 69 ++++++++ .../keycape-caller-reconciliation.json | 160 ++++++++++++++++++ ...gdom-security-blueprint-and-gap-closure.md | 17 ++ 4 files changed, 269 insertions(+) create mode 100644 docs/changes/CUST-WP-0080/keycape_bootstrap_native_test.go create mode 100644 docs/changes/CUST-WP-0080/verify_keycape_bootstrap.py create mode 100644 docs/evidence/2026-10-13-netkingdom-security/keycape-caller-reconciliation.json diff --git a/docs/changes/CUST-WP-0080/keycape_bootstrap_native_test.go b/docs/changes/CUST-WP-0080/keycape_bootstrap_native_test.go new file mode 100644 index 00000000..9027af6f --- /dev/null +++ b/docs/changes/CUST-WP-0080/keycape_bootstrap_native_test.go @@ -0,0 +1,23 @@ +package config + +import "testing" + +func TestCustodyBootstrapNativeLoader(t *testing.T) { + cfg, err := Load("../../bootstrap-fixture.json") + if err != nil { + t.Fatal("bootstrap output failed native config parsing") + } + if cfg.PrivacyIDEA.AdminToken != "" || cfg.PrivacyIDEA.AdminTokenFile != "/etc/keycape-factor/admin-token" { + t.Fatal("credential source mismatch") + } + if cfg.PrivacyIDEA.Realm != "coulomb" || !cfg.PrivacyIDEA.RequireForAll { + t.Fatal("factor policy mismatch") + } + clients, err := cfg.Registrations() + if err != nil || len(clients) != 5 { + t.Fatal("bootstrap registrations did not survive parsing") + } + if cfg.LLDAP.BindPW == "" || cfg.Authelia.ClientSecret == "" { + t.Fatal("bootstrap credential fields missing") + } +} diff --git a/docs/changes/CUST-WP-0080/verify_keycape_bootstrap.py b/docs/changes/CUST-WP-0080/verify_keycape_bootstrap.py new file mode 100644 index 00000000..fc47a3ee --- /dev/null +++ b/docs/changes/CUST-WP-0080/verify_keycape_bootstrap.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python3 +"""Verify the synthetic bootstrap output with KeyCape's current native loader. + +Only local source, synthetic credentials and disposable files are used. Requires +pytest, PyYAML, OpenSSL and Go with dependencies already cached. No kubectl from +PATH is executed: the fixture installs its recording stub in a private PATH. +""" +import argparse +import base64 +import hashlib +import importlib.util +import json +import os +from pathlib import Path +import shutil +import subprocess +import tempfile + + +def digest(path): + return hashlib.sha256(path.read_bytes()).hexdigest() + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument('--net-kingdom', type=Path, required=True) + parser.add_argument('--key-cape', type=Path, required=True) + args = parser.parse_args() + fixture = args.net_kingdom / 'tests/test_keycape_bootstrap.py' + spec = importlib.util.spec_from_file_location('bootstrap_fixture', fixture) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + with tempfile.TemporaryDirectory(prefix='keycape-bootstrap-native-') as directory: + root = Path(directory) + case_root = root / 'fixture'; case_root.mkdir() + case = module.setup.__wrapped__(case_root) + result = module.invoke(case) + if result.returncode: + raise RuntimeError('synthetic bootstrap failed') + writes = [c for c in module.calls(case) if c['args'][0] == 'create'] + if len(writes) != 1: + raise RuntimeError('unexpected synthetic operation count') + manifest = json.loads(writes[0]['stdin']) + checkout = root / 'consumer'; checkout.mkdir() + shutil.copytree(args.key_cape / 'src', checkout / 'src', + ignore=shutil.ignore_patterns('.git', '__pycache__', '.pytest_cache')) + # Existing native config tests read only these public example fixtures. + (checkout / 'config').mkdir() + for name in ('dev-config.yaml', 'service-clients.example.yaml'): + shutil.copyfile(args.key_cape / 'config' / name, checkout / 'config' / name) + (checkout / 'src/bootstrap-fixture.json').write_bytes(base64.b64decode(manifest['data']['config.yaml'])) + native_test = Path(__file__).with_name('keycape_bootstrap_native_test.go') + shutil.copyfile(native_test, checkout / 'src/internal/config/custody_bootstrap_test.go') + tested = subprocess.run(['go', 'test', '-count=1', './internal/config', './internal/adapters/privacyidea'], + cwd=checkout / 'src', capture_output=True, timeout=120, + env={**os.environ, 'GOPROXY': 'off', 'GOTOOLCHAIN': 'auto'}) + if tested.returncode: + raise RuntimeError('native loader/adapter verification failed') + sources = ['src/internal/config/config.go', 'src/internal/adapters/privacyidea/config.go', + 'src/internal/adapters/privacyidea/adapter.go', + 'src/internal/adapters/privacyidea/credential_file_test.go'] + print(json.dumps({'result': 'passed', 'bootstrap_fixture_sha256': digest(fixture), + 'native_test_sha256': digest(native_test), + 'consumer_sources': [{'path': p, 'sha256': digest(args.key_cape / p)} for p in sources], + 'scope': 'Generated synthetic YAML parsed by native loader; local config and adapter suites. No live cluster or provider.'}, indent=2)) + + +if __name__ == '__main__': + main() diff --git a/docs/evidence/2026-10-13-netkingdom-security/keycape-caller-reconciliation.json b/docs/evidence/2026-10-13-netkingdom-security/keycape-caller-reconciliation.json new file mode 100644 index 00000000..6ff79489 --- /dev/null +++ b/docs/evidence/2026-10-13-netkingdom-security/keycape-caller-reconciliation.json @@ -0,0 +1,160 @@ +{ + "observed_at": "2026-10-13T17:45:42.711908+00:00", + "result": "source-repair-verified; native-deployment-unverified", + "workplan": "NK-WP-0049", + "task": "NK-WP-0049-T02", + "baseline_commit": "19a1c75d309991de8654c4b7a9eeffea8fb90b18", + "owner_evidence": { + "factor_custody": "railiance-platform/workplans/RPF-WP-0040-keycape-factor-credential-custody.md (finished; historical native receipts)", + "contract": "railiance-platform/docs/net-kingdom-credential-custody-contract.md", + "consumer_manifest": "net-kingdom/sso-mfa/k8s/keycape/deployment.yaml", + "fresh_live_state_checked": false + }, + "knowledge": { + "version": "0.2.6", + "package_digest": "8d1d6a9d4d07fec7b619618de7dfe2be2254c1790f85f4198c7fe19db9751c02", + "cards": [ + "start", + "local-work" + ], + "source_status": "withheld; changed sources; orientation only", + "reviewed_current_sources": [ + "net-kingdom/AGENTS.md", + "net-kingdom/SECURITY-COMPANION.md", + "emergent-security/specs/emergent-security-0.8.md", + "the-custodian/docs/agent-environment-orientation.md", + "the-custodian/docs/agent-autonomy-decision.md", + "railiance-platform factor custody contract and workplan", + "key-cape native configuration and credential-file adapter" + ], + "authority": "User-authorized source repair only; no live credential or cluster operation" + }, + "changes": [ + "Retire personal-admin token mint and live refresh entry points before input or effects; use established scoped renewal lane.", + "First-bootstrap generator uses mounted adminTokenFile, private literal inputs, captured subprocess output, stdin Secret payload and create-only semantics.", + "Generate a missing RSA key into a private atomic no-replace file; preserve valid existing key and retain it after API failure.", + "Refuse stale RSA/client-secret replacement rotations before mutation; preserve remaining coordinated rotation requirement.", + "Update bootstrap callers, recovery guidance and portal registration contract test." + ], + "verification": { + "old_source_regressions": { + "failed": 21, + "passed": 1 + }, + "final_command": "python3 -m pytest -q tests sso-mfa/k8s/keycape --tb=short", + "passed": 277, + "skipped": 22, + "skip_reason": "21 real-SOPS tests lack SOPS; privacyIDEA 3.12.2 module unavailable. These are unchanged-surface regressions, not new caller proof.", + "shell_syntax": "passed for all changed shell scripts", + "native_consumer": { + "result": "passed", + "bootstrap_fixture_sha256": "afbf23e113f5f4118a41c3afa34c9dd9066fb7b2d8b80386ae64a466455d3414", + "native_test_sha256": "807a8c541fef5442f248d862690c0c684463b1052e0f826c7e9c3275cddd6e30", + "scope": "Generated synthetic YAML parsed by native loader; local config and adapter suites. No live cluster or provider.", + "consumer_sources": [ + { + "path": "src/internal/config/config.go", + "sha256": "7a0f46f970654ea8af66bd4e5a9e3593d7df561167e4c6ca829993bc225e0a8f" + }, + { + "path": "src/internal/adapters/privacyidea/config.go", + "sha256": "8dd769b18c04c562714794484630acdbda81b34ab1cfaf9cc7445513207e5021" + }, + { + "path": "src/internal/adapters/privacyidea/adapter.go", + "sha256": "2559c68b5aa22bc5f61961e47d696b376f8bede533be03715ffc80c0d89ed904" + }, + { + "path": "src/internal/adapters/privacyidea/credential_file_test.go", + "sha256": "9db15562c6fdf01e0806bbc478915413b17adb63c7e9ba213acfc4f9b2b2f0b6" + } + ] + }, + "native_reproduction": "python3 docs/changes/CUST-WP-0080/verify_keycape_bootstrap.py --net-kingdom /home/worsch/net-kingdom --key-cape /home/worsch/key-cape" + }, + "preparation_failures": [ + "First copied Go source omitted sibling config fixtures; two existing config tests failed. Full disposable layout corrected and both packages passed.", + "First reproducible wrapper selected local Go 1.22.2 but consumer requires 1.23.0; cached auto toolchain selected with GOPROXY=off.", + "Subsequent wrapper attempts hit sandbox read-only Go cache; same synthetic verification rerun with approved cache access and passed.", + "Custodian pre-commit correctly scanned the receipt and flagged file-digest map entries as generic-api-key candidates. Verified every match was a recomputable SHA-256; changed receipt schema to explicit path/sha256 records. No scanner rule, exclusion or hook changed." + ], + "remaining": [ + "No deployed first-bootstrap, fresh login, credential revocation or live recovery acceptance performed.", + "Legacy token/trigger objects and historical copies need owner inventory and approved revocation.", + "privacyidea/check-user-mfa-state.sh still reads legacy token Secret and uses insecure curl/argv handling; reconcile with scoped diagnostic authority.", + "Other delegated create/apply and direct SOPS edit/encrypt/rotate callers remain under T02.", + "Configuration-preserving RSA/client-secret rotation, rollback custody and multi-consumer acceptance remain open.", + "T03 exact owner-scoped off-host recovery ceremony remains pending." + ], + "source_commit": "7d4bb1653edb45abeb4e66990cd877d635424cd0", + "source_files": [ + { + "path": "sso-mfa/WORKPLAN.md", + "sha256": "2cda3d865f2bfa45f33c07abb0d31108c8aff1e7698eabcf97d3969eee9a5c5d" + }, + { + "path": "sso-mfa/bootstrap/creds-apply.sh", + "sha256": "3653ba053b94fdff590f0ebbed5ce7d296c719b36d5dba2f3c18a21cd4da0297" + }, + { + "path": "sso-mfa/bootstrap/creds-bootstrap-agent.sh", + "sha256": "e58a52a49de1299c29ecb35e0a496438f259efed6dd71ff92f1d5d01e730797c" + }, + { + "path": "sso-mfa/bootstrap/creds-rotate.sh", + "sha256": "69b1676c17668964b4b13459f225855532e7192194c181be9d9e767c37370f4f" + }, + { + "path": "sso-mfa/bootstrap/creds-status.sh", + "sha256": "f0fe0b885fd22f4789baa86cc2438272700aff8634cfa6b27d55c2c860c9f36d" + }, + { + "path": "sso-mfa/bootstrap/gen-secrets.sh", + "sha256": "fa000a765042d962eb38e4497d1f40da768d27c789d453c828d356205948121f" + }, + { + "path": "sso-mfa/k8s/backup/DR-RUNBOOK.md", + "sha256": "ad486c757fb68cb848beff538b17f8ba128679faf09c62ad95d8313a92bed745" + }, + { + "path": "sso-mfa/k8s/keycape/README.md", + "sha256": "32884fdb336e2d6f44f713ac2ae138772d661848606e969519cc44d1fe0e694b" + }, + { + "path": "sso-mfa/k8s/keycape/create-pi-token.sh", + "sha256": "2f750aea638c06eb11dfe7afcf7b3d6117ba9e964cbf2a48bb12590054178152" + }, + { + "path": "sso-mfa/k8s/keycape/create-secrets.sh", + "sha256": "f5131160d91641a59c3163a17a8dd4f9b97d228eb8a5e0d8ad04cc2f7ff51644" + }, + { + "path": "sso-mfa/k8s/keycape/refresh-pi-token-live.sh", + "sha256": "2f750aea638c06eb11dfe7afcf7b3d6117ba9e964cbf2a48bb12590054178152" + }, + { + "path": "sso-mfa/k8s/keycape/test_portal_client_rollout.py", + "sha256": "a16cd4bf278970c10de2e1b1025597b3872c9515713dd947ac6b50cb78feea1f" + }, + { + "path": "tests/test_rotation_output.py", + "sha256": "a938be803a262375f5ed7ca433ba6a859997967ed1ec1f0f60982178a7c65002" + }, + { + "path": "workplans/NK-WP-0049-bootstrap-secret-free-recovery-handoff.md", + "sha256": "f8143f639b36d28051f6fa44067d66a4a0a300065b699a4fbff2f183c02a8e00" + }, + { + "path": "sso-mfa/k8s/keycape/bootstrap-config.json", + "sha256": "fa681a4474b947e074b824710f16e80cfce9adcb130b92e6663e98ab5c3f22df" + }, + { + "path": "sso-mfa/k8s/keycape/create-secrets.py", + "sha256": "5da672fff36eb94101741ad99e2a1a96ae81fea15ed1871304c40e63ce27c787" + }, + { + "path": "tests/test_keycape_bootstrap.py", + "sha256": "afbf23e113f5f4118a41c3afa34c9dd9066fb7b2d8b80386ae64a466455d3414" + } + ] +} diff --git a/workplans/CUST-WP-0080-netkingdom-security-blueprint-and-gap-closure.md b/workplans/CUST-WP-0080-netkingdom-security-blueprint-and-gap-closure.md index 8257f0aa..ed25a810 100644 --- a/workplans/CUST-WP-0080-netkingdom-security-blueprint-and-gap-closure.md +++ b/workplans/CUST-WP-0080-netkingdom-security-blueprint-and-gap-closure.md @@ -351,6 +351,23 @@ recovery work; no repository content was fetched, changed or uploaded, and no scanner exception was introduced. Receipts: `forge-gitlink-reconciliation.json`, `forge-missing-gitlink-object-search.json`, `forge-gitlink-local-copies.json`. +2026-10-13 KeyCape custody callers reconciled with existing RPF-WP-0040: +retired personal-admin token mint/refresh entry points and updated bootstrap +configuration to the scoped renewable factor credential mount. First creation +uses private literal inputs, captured diagnostics, stdin-only Secret data and +API create without last-applied annotations or replacement of existing config. +Signing keys are published privately without overwrite and retained on failure. +Legacy RSA/client-secret replacement rotations now refuse before mutation; +configuration-preserving rotation and retained rollback custody remain required. + +The old source failed 21 of 22 new regressions. Final bootstrap/KeyCape suite: +277 passed, 22 dependency-related skips; actual KeyCape Go loader/config and +credential-file adapter suites pass against generated synthetic configuration. +Failed verification preparations are retained in the receipt. No live credential +or deployment changed. NK-WP-0049-T02 remains progress: other callers, historical +object revocation, coordinated rotation and owner-scoped native recovery remain. +Evidence: `docs/evidence/2026-10-13-netkingdom-security/keycape-caller-reconciliation.json`. + ## Verify native closure against every requirement ```task