security: record KeyCape custody caller reconciliation and native config proof
This commit is contained in:
parent
873c595bbb
commit
bb3e2db07e
4 changed files with 269 additions and 0 deletions
23
docs/changes/CUST-WP-0080/keycape_bootstrap_native_test.go
Normal file
23
docs/changes/CUST-WP-0080/keycape_bootstrap_native_test.go
Normal 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")
|
||||
}
|
||||
}
|
||||
69
docs/changes/CUST-WP-0080/verify_keycape_bootstrap.py
Normal file
69
docs/changes/CUST-WP-0080/verify_keycape_bootstrap.py
Normal 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()
|
||||
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue