security: record KeyCape custody caller reconciliation and native config proof
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 2s
Reject generated-only commits / generated-commit-guard (push) Successful in 8s

This commit is contained in:
codex 2026-10-13 19:49:24 +02:00
parent 873c595bbb
commit bb3e2db07e
4 changed files with 269 additions and 0 deletions

View file

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

View file

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

View file

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

View file

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