From 5c456215ea2eea89015e36e48cedfc95385c304b Mon Sep 17 00:00:00 2001 From: codex Date: Sat, 17 Oct 2026 22:32:57 +0200 Subject: [PATCH] Record reviewed case source evidence (5/13) --- .../test_admit_1_3_type.before.py | 157 +++++ .../OWNER-REVIEW.md | 69 ++ .../canonical-source-readonly-state.json | 12 + .../focused-tests.json | 10 + .../focused-tests.txt | 13 + .../manifest.json | 20 + .../netkingdom-entry-point.conditional.patch | 16 + .../docs/keycape-issuer-eso-contract.md | 68 ++ .../templates/keycape-issuer-eso.yaml | 46 ++ .../scripts/keycape_issuer_eso.py | 70 ++ .../tests/test_keycape_issuer_eso.py | 129 ++++ .../platform.candidate.patch | 325 ++++++++++ .../nk48-r5-f2-implementation-r1/review.json | 66 ++ .../source-pins.json | 108 ++++ .../structural-and-patch-checks.json | 8 + .../type.before.yaml | 495 ++++++++++++++ .../type.candidate.yaml | 498 +++++++++++++++ .../nk48-r5-f2-publication-r1/README.md | 43 ++ .../canonical-validation.json | 5 + .../consistency.json | 604 ++++++++++++++++++ 20 files changed, 2762 insertions(+) create mode 100644 docs/changes/netkingdom-target-2026-10-17/nk48-r5-f1-implementation-r1/test_admit_1_3_type.before.py create mode 100644 docs/changes/netkingdom-target-2026-10-17/nk48-r5-f2-implementation-r1/OWNER-REVIEW.md create mode 100644 docs/changes/netkingdom-target-2026-10-17/nk48-r5-f2-implementation-r1/canonical-source-readonly-state.json create mode 100644 docs/changes/netkingdom-target-2026-10-17/nk48-r5-f2-implementation-r1/focused-tests.json create mode 100644 docs/changes/netkingdom-target-2026-10-17/nk48-r5-f2-implementation-r1/focused-tests.txt create mode 100644 docs/changes/netkingdom-target-2026-10-17/nk48-r5-f2-implementation-r1/manifest.json create mode 100644 docs/changes/netkingdom-target-2026-10-17/nk48-r5-f2-implementation-r1/netkingdom-entry-point.conditional.patch create mode 100644 docs/changes/netkingdom-target-2026-10-17/nk48-r5-f2-implementation-r1/platform-candidate/docs/keycape-issuer-eso-contract.md create mode 100644 docs/changes/netkingdom-target-2026-10-17/nk48-r5-f2-implementation-r1/platform-candidate/manifests/templates/keycape-issuer-eso.yaml create mode 100644 docs/changes/netkingdom-target-2026-10-17/nk48-r5-f2-implementation-r1/platform-candidate/scripts/keycape_issuer_eso.py create mode 100644 docs/changes/netkingdom-target-2026-10-17/nk48-r5-f2-implementation-r1/platform-candidate/tests/test_keycape_issuer_eso.py create mode 100644 docs/changes/netkingdom-target-2026-10-17/nk48-r5-f2-implementation-r1/platform.candidate.patch create mode 100644 docs/changes/netkingdom-target-2026-10-17/nk48-r5-f2-implementation-r1/review.json create mode 100644 docs/changes/netkingdom-target-2026-10-17/nk48-r5-f2-implementation-r1/source-pins.json create mode 100644 docs/changes/netkingdom-target-2026-10-17/nk48-r5-f2-implementation-r1/structural-and-patch-checks.json create mode 100644 docs/changes/netkingdom-target-2026-10-17/nk48-r5-f2-implementation-r1/type.before.yaml create mode 100644 docs/changes/netkingdom-target-2026-10-17/nk48-r5-f2-implementation-r1/type.candidate.yaml create mode 100644 docs/changes/netkingdom-target-2026-10-17/nk48-r5-f2-publication-r1/README.md create mode 100644 docs/changes/netkingdom-target-2026-10-17/nk48-r5-f2-publication-r1/canonical-validation.json create mode 100644 docs/changes/netkingdom-target-2026-10-17/nk48-r5-f2-publication-r1/consistency.json diff --git a/docs/changes/netkingdom-target-2026-10-17/nk48-r5-f1-implementation-r1/test_admit_1_3_type.before.py b/docs/changes/netkingdom-target-2026-10-17/nk48-r5-f1-implementation-r1/test_admit_1_3_type.before.py new file mode 100644 index 00000000..084df94b --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/nk48-r5-f1-implementation-r1/test_admit_1_3_type.before.py @@ -0,0 +1,157 @@ +"""Kernel admission against the real 1.3 successor type (NK-WP-0048-T16 candidate). + +Unlike test_admit_1_2_adapter_descriptors.py, nothing is patched into the type's +execution_descriptors: the map tested here is the one committed in +security-case-type-revisions/1.3. The type is `proposed`, so the kernel must refuse +it as it stands. Warden's BrowserInputs also pins each type version's parameter +fingerprint and knows only 1.1 and 1.2 (ops-warden case_inputs.PARAMETER_PINS), so an +acknowledged 1.3 is still refused until ops-warden adds a 1.3 pin. That is an +ops-warden owner change, and it is not made here. The tests show that 1.3's +parameters carry 1.2's pinned fingerprint, and that with only that one pin added in +memory the committed map admits exactly the composed descriptors and nothing else. +Trust is fixture-only, nothing is dispatched, and none of this is owner admission +or an acknowledgment. +""" +from dataclasses import replace +import hashlib +import importlib.util +import json +import os +from pathlib import Path +import tempfile +import unittest +from unittest import mock + +import yaml + +from warden.case_run import Adapter, Executor, Journal, digest +from warden.case_receipt_state import Refused +import warden.case_inputs as inputs + +ROOT = Path(__file__).parent +REPO = ROOT.parents[2] +spec = importlib.util.spec_from_file_location('compose_13_admit', ROOT / 'compose-1.2-descriptors.py') +c = importlib.util.module_from_spec(spec) +spec.loader.exec_module(c) + +TYPE_DIR = REPO / 'security-case-type-revisions/1.3/oidc-browser-relying-party' +TYPE_BYTES = (TYPE_DIR / 'type.yaml').read_bytes() +ARTIFACT = json.loads((ROOT / 'descriptor-composition-1.3.candidate.json').read_text()) +EVIDENCE = {'fixture-acceptance', 'fixture-materialization', 'fixture-descriptor', 'fixture-session'} + + +def unused(*_): + raise AssertionError('admission must not dispatch') + + +def parameter_fingerprint(type_bytes): + params = yaml.safe_load(type_bytes)['spec']['parameters'] + return hashlib.sha256(json.dumps(params, sort_keys=True, separators=(',', ':')).encode()).hexdigest() + + +def pinned_13(): + """Warden's parameter pins plus the one 1.3 entry ops-warden would have to add.""" + return mock.patch.dict(inputs.PARAMETER_PINS, {'1.3': parameter_fingerprint(TYPE_BYTES)}) + + +def with_status(status): + """The committed 1.3 type with only its status changed.""" + typ = yaml.safe_load(TYPE_BYTES) + typ['spec']['status'] = status + return yaml.safe_dump(typ).encode() + + +class Admit13TypeTests(unittest.TestCase): + def setUp(self): + self.temp = tempfile.TemporaryDirectory() + self.addCleanup(self.temp.cleanup) + case = yaml.safe_load((TYPE_DIR / 'examples/forgejo.yaml').read_bytes()) + case['spec'].update(status='materialized', acceptance={ + 'at': '2026-10-17', 'record': 'fixture-only acceptance, not an owner record', + 'by': [{'name': 'fixture', 'role': 'owner'}, {'name': 'fixture', 'role': 'founder'}]}) + self.case_source = yaml.safe_dump(case).encode() + self.descriptors = dict(c.adapter_descriptors(ARTIFACT['check_config_digest'], + ARTIFACT['params']['consumer_owner_repo'], + version='1.3')) + + def admit(self, desc, type_source, *, actor=None, lane=None): + desc_bytes = c.encode(desc) + adapter = Adapter(desc.adapter, desc.adapter_source, unused, unused) + executor = Executor(Journal(Path(self.temp.name) / desc.step), + {adapter.reference: adapter}, + lambda purpose, source, evidence: evidence in EVIDENCE, + browser_inputs=inputs.BrowserInputs(REPO)) + with mock.patch.dict(os.environ): + os.environ.pop('WARDEN_AGENT_ID', None) + return executor.admit(self.case_source, type_source, desc_bytes, + b'fixture-only-catalog', principal='founder', actor=actor or desc.actor, + lane=lane or desc.lane, + acceptance_evidence='fixture-acceptance', + materialization_evidence='fixture-materialization', + descriptor_evidence='fixture-descriptor', session_evidence='fixture-session') + + def test_committed_map_is_exactly_the_composed_set(self): + committed = yaml.safe_load(TYPE_BYTES)['spec']['execution_descriptors'] + composed = {s: digest(c.encode(d)) for s, d in self.descriptors.items()} + self.assertEqual(len(composed), 7) + self.assertEqual(committed, composed) + self.assertEqual(committed, ARTIFACT['execution_descriptors']) + + def test_proposed_type_is_refused_as_it_stands(self): + self.assertEqual(yaml.safe_load(TYPE_BYTES)['spec']['status'], 'proposed') + for step, desc in self.descriptors.items(): + with self.subTest(step=step), self.assertRaisesRegex(Refused, 'type is not acknowledged'): + self.admit(desc, TYPE_BYTES) + + def test_warden_has_no_1_3_parameter_pin_yet(self): + self.assertNotIn('1.3', inputs.PARAMETER_PINS) + # 1.3 keeps 1.2's parameters, so the missing pin is 1.2's fingerprint. + self.assertEqual(parameter_fingerprint(TYPE_BYTES), inputs.PARAMETER_PINS['1.2']) + acknowledged = with_status('acknowledged') + for step, desc in self.descriptors.items(): + with self.subTest(step=step), self.assertRaisesRegex( + Refused, 'failed exact-version validation'): + self.admit(desc, acknowledged) + + def test_committed_map_admits_every_descriptor_once_acknowledged_and_pinned(self): + acknowledged = with_status('acknowledged') + for step, desc in self.descriptors.items(): + with self.subTest(step=step), pinned_13(): + admitted, invocation, _ = self.admit(desc, acknowledged) + self.assertEqual(admitted, desc) + self.assertEqual((invocation.step, invocation.actor, invocation.target), + (desc.step, desc.actor, 'forgejo')) + + def test_descriptor_outside_the_map_is_refused(self): + acknowledged = with_status('acknowledged') + mint = self.descriptors['mint-secret'] + for changed in (replace(mint, lane='other-lane'), replace(mint, retry_safe=True)): + with self.subTest(changed=changed), pinned_13(), self.assertRaisesRegex( + Refused, 'no exact acknowledged machine descriptor'): + self.admit(changed, acknowledged) + + def test_mint_secret_admits_only_a_human_actor(self): + # Moved from test_admit_1_2_adapter_descriptors.py (NK48-R1-F1) in r5. + acknowledged = with_status('acknowledged') + mint = self.descriptors['mint-secret'] + self.assertEqual((mint.effect, mint.actor), ('mint', 'human')) + with pinned_13(), self.assertRaisesRegex(Refused, 'principal actor or lane mismatch'): + self.admit(mint, acknowledged, actor='agent') + + def test_wrong_lane_is_refused(self): + # Moved from test_admit_1_2_adapter_descriptors.py in r5. + acknowledged = with_status('acknowledged') + for step, desc in self.descriptors.items(): + with self.subTest(step=step), pinned_13(), self.assertRaisesRegex( + Refused, 'principal actor or lane mismatch'): + self.admit(desc, acknowledged, lane='wrong-lane') + + def test_earlier_versions_carry_no_execution_map(self): + for version in ('1.2',): + path = REPO / f'security-case-type-revisions/{version}/oidc-browser-relying-party/type.yaml' + with self.subTest(version=version): + self.assertNotIn('execution_descriptors', yaml.safe_load(path.read_bytes())['spec']) + + +if __name__ == '__main__': + unittest.main() diff --git a/docs/changes/netkingdom-target-2026-10-17/nk48-r5-f2-implementation-r1/OWNER-REVIEW.md b/docs/changes/netkingdom-target-2026-10-17/nk48-r5-f2-implementation-r1/OWNER-REVIEW.md new file mode 100644 index 00000000..34708c59 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/nk48-r5-f2-implementation-r1/OWNER-REVIEW.md @@ -0,0 +1,69 @@ +# NK48-R5-F2 issuer store implementation candidate + +The missing issuer store is now a concrete **proposed Platform source contract**, +not a discovered existing grant. Neither patch has been applied to a canonical +checkout. Independent Platform review remains necessary before the conditional +NetKingdom patch can be integrated; F2 is not declared closed here. + +The proposed reference is cluster-scoped `ClusterSecretStore +openbao-keycape-{client_id}-issuer`, with no metadata namespace and use restricted +to `sso`. For Forgejo, it binds role `external-secrets-keycape-forgejo` to the +existing CCR0041 identity `external-secrets/external-secrets`, audience `openbao`. +The exact store/provider/ExternalSecret/key/property/target source is in +`platform-candidate/manifests/templates/keycape-issuer-eso.yaml`; its hash is +recorded in `review.json` and the conditional type text. The new `-issuer` name +is an implementation proposal: it does not reuse the Apps consumer SecretStore +or claim that CCR0041 admitted this store name. It preserves type IDs ending in +`-` without changing parameters. ClusterSecretStore names use DNS subdomain +names; the existing adapter's 63-character target reference bound is retained. + +`platform.candidate.patch` adds only that explicit template, a pure metadata +render/validation helper, eight focused fixture tests and an owner contract doc. +It contains no Kubernetes/OpenBao API client, apply/delete command, credentials, +receipt issuer or catalog grant. Store provisioning is separate owner work; the +delivery lane gets only the ExternalSecret document. The template is multi-doc +review source and must not be applied through the delivery lane. Its strict spec +comparison intentionally refuses drift; installed ESO schema/defaulting and +trusted observation authenticity still belong to the real owner binding. + +`netkingdom-entry-point.conditional.patch` changes **only** the issuer +`deliver-secret.entry_point` at NetKingdom 3236d500. The prior type is byte-equal +to r5's e75fe941 reviewed type. It names the proposed exact kind/name/scope and +owner template digest. Parameters, descriptors, modes, lanes, other follow-through, +materialization, verification, acceptance, and every rollback entry are unchanged. +The root coordinator can combine this text with F3 after Platform source review, +then regenerate the existing combined artifact. No source owner acknowledgment, +case/type pin, catalog mutation or default revision switch has occurred. + +Eight tests / 42 negative subcases passed using public synthetic metadata: +independently fixed Forgejo mapping; wrong kind/name/namespace; role, server, +mount, SA/audience or allowed-namespace drift; consumer-store substitution; +remoteRef/property/target drift; malformed/unready observations; rejected caller +store/path overrides; detached ExternalSecret-only render; type-compatible trailing +dash. Both exact patches passed `git apply --check` in disposable scratch. +See `focused-tests.*` and `structural-and-patch-checks.json`. These checks are not +native delivery, signed evidence, API defaulting, issuer auth or isolation proof. + +The source basis is pinned in `source-pins.json`: r5 F2 at coordinator review +lines 429–439; Platform RPF-1.3-C1 explicitly requires a named store and the +lane definition explicitly calls for a fixed client-derived Platform constant; +CCR0041 grants the Forgejo role/SA/audience but declares no store name; +Platform's historical issuer `eso.draft.yaml` supplies ClusterSecretStore, +namespace restriction, serviceAccountRef.audiences and fixed target/remoteRef +conventions; the scaffold also renders ClusterSecretStore and mount-relative +remoteRefs but requires a caller-supplied owner config store_name. The scaffold's +historical audience location differs: the candidate follows the issuer draft, +and makes no claim about the actual installed schema. + +Bounded source search: Platform, NetKingdom and Mason public YAML/Python/JSON/MD +for `openbao-keycape-forgejo` and `external-secrets-keycape-forgejo`, plus Platform +issuer/store/scaffold files. Existing hits name the separate consumer store, +role/policy or old type text. No fixed Forgejo issuer store declaration was found. +This is a public-source finding, not a live-cluster absence claim. No Kubernetes, +SSH, Bao, credential route, Secret or private archive was accessed. + +The next source step is independent Platform review of the exact new declaration, +then coordinator integration of the accepted text with F3. RPF-1.3-T4's actual +capabilities, authentic metadata receipts, per-attempt revocation, guarded +created-only rollback and Warden catalog routing remain its existing owner work. +No new task, UUID, protocol, policy gate or human approval request is created. diff --git a/docs/changes/netkingdom-target-2026-10-17/nk48-r5-f2-implementation-r1/canonical-source-readonly-state.json b/docs/changes/netkingdom-target-2026-10-17/nk48-r5-f2-implementation-r1/canonical-source-readonly-state.json new file mode 100644 index 00000000..f0871704 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/nk48-r5-f2-implementation-r1/canonical-source-readonly-state.json @@ -0,0 +1,12 @@ +{ + "net-kingdom": { + "HEAD": "3236d5000e52e0643f33906a26a7825aa0b67465", + "status_porcelain_v1": " M .custodian-brief.md\n M WORK-RECORDS.md\n?? docs/evidence/2026-10-15-cadence-adoption/hourly-observation-capture/\n?? docs/evidence/2026-10-15-cadence-adoption/hourly-observation-inventory.jsonl\n?? docs/evidence/2026-10-15-cadence-adoption/hourly-observation-monitor.json\n?? intakes/NET-IN-0005-single-use-setup-link-mail-handoff.md\n", + "index_sha256": "47bcf3291b9613706337c2493c33a065ab089c0c5bbbe545de5f462b67b007a3" + }, + "railiance-platform": { + "HEAD": "0a9617a62d4696a8070798923b459e8405c3dbf2", + "status_porcelain_v1": "M .custodian-brief.md\n M WORK-RECORDS.md\n", + "index_sha256": "40f766948f7774c6215ab562e50b4154e9bbdcfbf3a14488625c95afc9cf6fe6" + } +} diff --git a/docs/changes/netkingdom-target-2026-10-17/nk48-r5-f2-implementation-r1/focused-tests.json b/docs/changes/netkingdom-target-2026-10-17/nk48-r5-f2-implementation-r1/focused-tests.json new file mode 100644 index 00000000..403ab1f5 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/nk48-r5-f2-implementation-r1/focused-tests.json @@ -0,0 +1,10 @@ +{ + "schema": "custodian.source-test-result.v1", + "scope": "synthetic public metadata only; no API, native, credential, receipt-authenticity or isolation proof", + "command": "python3 -B platform-candidate/tests/test_keycape_issuer_eso.py", + "tests": 8, + "subtests": 42, + "passed": true, + "python": "3.12.3", + "pyyaml": "6.0.1" +} diff --git a/docs/changes/netkingdom-target-2026-10-17/nk48-r5-f2-implementation-r1/focused-tests.txt b/docs/changes/netkingdom-target-2026-10-17/nk48-r5-f2-implementation-r1/focused-tests.txt new file mode 100644 index 00000000..013b4727 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/nk48-r5-f2-implementation-r1/focused-tests.txt @@ -0,0 +1,13 @@ +test_delivery_document_is_only_external_and_detached (fixture.FixedIssuerContract.test_delivery_document_is_only_external_and_detached) ... ok +test_external_drift_refuses (fixture.FixedIssuerContract.test_external_drift_refuses) ... ok +test_independent_forgejo_binding (fixture.FixedIssuerContract.test_independent_forgejo_binding) ... ok +test_missing_or_unready_refuses (fixture.FixedIssuerContract.test_missing_or_unready_refuses) ... ok +test_no_caller_store_or_path_override (fixture.FixedIssuerContract.test_no_caller_store_or_path_override) ... ok +test_store_drift_refuses (fixture.FixedIssuerContract.test_store_drift_refuses) ... ok +test_type_admitted_trailing_dash_is_preserved (fixture.FixedIssuerContract.test_type_admitted_trailing_dash_is_preserved) ... ok +test_valid_ready_metadata_allows_extra_controller_metadata (fixture.FixedIssuerContract.test_valid_ready_metadata_allows_extra_controller_metadata) ... ok + +---------------------------------------------------------------------- +Ran 8 tests in 0.247s + +OK diff --git a/docs/changes/netkingdom-target-2026-10-17/nk48-r5-f2-implementation-r1/manifest.json b/docs/changes/netkingdom-target-2026-10-17/nk48-r5-f2-implementation-r1/manifest.json new file mode 100644 index 00000000..432c3f57 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/nk48-r5-f2-implementation-r1/manifest.json @@ -0,0 +1,20 @@ +{ + "schema": "custodian.source-packet-manifest.v1", + "sha256": { + "OWNER-REVIEW.md": "sha256:7983fd841f09bc7584047426912c33d568c97b4c1126776f99aac42b34d2b742", + "canonical-source-readonly-state.json": "sha256:129d6926bb996e510ab4b41d30e738096b53b71de1fb2dd470fe0ac94848cbec", + "focused-tests.json": "sha256:8de5862a8d752dce0b3b96a9275e42952a99415a18d3f67c06bf9f5b6008d0e3", + "focused-tests.txt": "sha256:aab46c51d87198ad4be6dd3078f61286e2faf1ef4d76fa14298698388f78ab24", + "netkingdom-entry-point.conditional.patch": "sha256:6784a21fbd10bd62712be2e0f4929034601c94629aa74ccaa2e93fc815eecd61", + "platform-candidate/docs/keycape-issuer-eso-contract.md": "sha256:7f59e59b17c58252982931ac61fe50dd7a07340add6c437fe48af36971eafe68", + "platform-candidate/manifests/templates/keycape-issuer-eso.yaml": "sha256:e17498bd8a4c820041a4105e4efe9c60c6a50953db8f621ff54887080065c743", + "platform-candidate/scripts/keycape_issuer_eso.py": "sha256:21c5797280c2a7825dbda8ac970d34a0cdef4c6458f68bce0b1c12407f122a87", + "platform-candidate/tests/test_keycape_issuer_eso.py": "sha256:223d4c5b349062b9f93e6af79e8d5feda44377ba41a11726f6f2b762691e8308", + "platform.candidate.patch": "sha256:a490df7dee41e15351da8d2906e1342edebb533984befe3d9ae74d952dbb091b", + "review.json": "sha256:d99db9ab4f326308cde464129bbbb7573434e500fd03c130a37a590bac3643a4", + "source-pins.json": "sha256:6e896b6196a0aca01ddaf433b4d14cc041fc0296c5dd6404c3a698b941b61bd8", + "structural-and-patch-checks.json": "sha256:a3dc4cd405c412d4369986cc6736cd2d9eed02d4b86d95d62d5a9914106fdb14", + "type.before.yaml": "sha256:e75fe94110ef9fb7fb18072fe2856941b33507c99cb406d36576046481485d0a", + "type.candidate.yaml": "sha256:ef02ddae3167bdd1558193a5f6db0379fb8cc84207531706d5ef35caabc3684f" + } +} diff --git a/docs/changes/netkingdom-target-2026-10-17/nk48-r5-f2-implementation-r1/netkingdom-entry-point.conditional.patch b/docs/changes/netkingdom-target-2026-10-17/nk48-r5-f2-implementation-r1/netkingdom-entry-point.conditional.patch new file mode 100644 index 00000000..6a725fe5 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/nk48-r5-f2-implementation-r1/netkingdom-entry-point.conditional.patch @@ -0,0 +1,16 @@ +--- a/security-case-type-revisions/1.3/oidc-browser-relying-party/type.yaml ++++ b/security-case-type-revisions/1.3/oidc-browser-relying-party/type.yaml +@@ -269,8 +269,11 @@ + when: client_type == confidential + entry_point: reviewed adapter railiance-platform/keycape-deliver-secret, dispatched + by Warden's case kernel; Platform's deliver capability applies ExternalSecret +- sso/keycape-{client_id}-client (key client-secret) through the ESO store for +- role external-secrets-keycape-{client_id}, from bao:platform/workloads/keycape/{client_id} ++ sso/keycape-{client_id}-client (key client-secret) through fixed ClusterSecretStore ++ openbao-keycape-{client_id}-issuer (cluster-scoped, restricted to namespace sso), ++ defined by Platform-controlled manifests/templates/keycape-issuer-eso.yaml ++ (sha256:e17498bd8a4c820041a4105e4efe9c60c6a50953db8f621ff54887080065c743), ++ using role external-secrets-keycape-{client_id}, from bao:platform/workloads/keycape/{client_id} + auth: Platform's own delivery path on lane platform-secret-delivery, inside the human + aal2 session; ESO reads custody through role external-secrets-keycape-{client_id}; + Warden holds no Platform credential, and no value leaves custody diff --git a/docs/changes/netkingdom-target-2026-10-17/nk48-r5-f2-implementation-r1/platform-candidate/docs/keycape-issuer-eso-contract.md b/docs/changes/netkingdom-target-2026-10-17/nk48-r5-f2-implementation-r1/platform-candidate/docs/keycape-issuer-eso-contract.md new file mode 100644 index 00000000..56cfa1e6 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/nk48-r5-f2-implementation-r1/platform-candidate/docs/keycape-issuer-eso-contract.md @@ -0,0 +1,68 @@ +# Proposed fixed issuer ESO contract — RPF-1.3-T4 / NK48-R5-F2 + +Status: proposed source; not admitted, deployed, or owner-acknowledged. This is a +new Platform declaration for review, not an existing Forgejo grant discovery. + +The only substitution in `manifests/templates/keycape-issuer-eso.yaml` +(sha256:e17498bd8a4c820041a4105e4efe9c60c6a50953db8f621ff54887080065c743) is the admitted `client_id`. The proposed issuer +store is **ClusterSecretStore `openbao-keycape-{client_id}-issuer`**, cluster +scoped (no metadata namespace), restricted by conditions to ExternalSecrets in +`sso`. For Forgejo it is `openbao-keycape-forgejo-issuer`. The suffix makes the +issuer purpose explicit and keeps type-admitted IDs ending in `-` representable. +The store's DNS subdomain name can exceed 63 characters; the existing adapter's +63-character target Secret reference bound is preserved (client ID at most 48). +No type parameter is added or changed. + +The provider is `http://openbao.openbao.svc:8200`, KV v2 mount `platform`, +Kubernetes auth mount `kubernetes`, role `external-secrets-keycape-{client_id}`, +serviceAccountRef `external-secrets/external-secrets`, audience `openbao`. +CCR-2026-0041 already records that Forgejo role/identity/audience, not this new +store name or an unrestricted client family grant. Other clients still require +their own existing owner-controlled role/case admission. + +The ExternalSecret is `sso/keycape-{client_id}-client`, references precisely that +ClusterSecretStore, refreshes every 5m and targets the same name with Owner +creation and Retain remote deletion. Its only data entry maps `client-secret` +from remoteRef `workloads/keycape/{client_id}`, property `CLIENT_SECRET`. The +provider mount plus mount-relative remoteRef equals custody reference +`platform/workloads/keycape/{client_id}`; do not double-prefix `platform/`. +This is distinct from the Apps-owned consumer namespace SecretStore +`openbao-keycape-{client_id}-consumer` and its dedicated consumer identity. + +Store provisioning belongs to Platform's separate owner-controlled path. The +proposed `platform-secret-delivery` lane retains ONLY create/get/watch/delete on +the exact ExternalSecret in `sso`; this source does not delegate store creation +or Secret read/list. Never apply the multi-document template through that lane. +The pure `scripts/keycape_issuer_eso.py` module exposes only the ExternalSecret +document through `external_secret()` and validates fixed public observations +through `require_fixed_store()` and `require_ready_delivery()`. It performs no +API call and issues no evidence/receipt. It is not a real capability or admission +boundary; a caller-supplied observation alone establishes no authenticity. + +The existing RPF-1.3-T4 delivery binding must validate the case/client binding, +verify this exact preexisting store's public identity/spec/Ready state, apply or +compare the exact ExternalSecret, and wait for matching spec plus Ready=True. +The proposed helper accepts extra controller metadata but deliberately refuses +spec drift. Actual API defaulting/normalization and installed ESO schema support +must be checked in that implementation; these fixtures establish no native +compatibility or credential isolation. ESO's shared controller remains trusted. +No Secret bodies, values, arbitrary remoteRefs, or raw errors may enter receipts. + +Existing owner requirements remain: closed `netkingdom.deliver-secret.v1` +metadata receipt with authenticated before/after UID/resourceVersion provenance, +unchanged objects reported NO_CHANGE, attempt-session revocation, and created-only +rollback guarded by exact UID/resourceVersion (drift or uncertain ownership goes +to owner reconciliation). The helper neither deletes a target Secret nor creates +an OpenBao role/policy nor changes CCR-0041. These capabilities and catalog routing +remain unimplemented/unaccepted under RPF-1.3-T4. Independent Platform source +review of this declaration precedes adopting the conditional NetKingdom type +text; no descriptor acceptance, acknowledgment, pin, grant, catalog or native +permission is inferred. + +Grounding: CCR-2026-0041 exact role/identity; the approved historical +`docs/provisioning-windows/2026-10-04-1000/keycape/eso.draft.yaml` issuer +ClusterSecretStore and target pattern; `scripts/catalog_lane_scaffold.py` +ClusterSecretStore/namespace restriction and mount-relative remoteRef model; +RPF-1.3-C1 and RPF-1.3-T4 in the frozen owner review. The scaffold's historical +audience field placement differs from the issuer draft; this candidate uses +the issuer draft's serviceAccountRef.audiences, not a claim of installed support. diff --git a/docs/changes/netkingdom-target-2026-10-17/nk48-r5-f2-implementation-r1/platform-candidate/manifests/templates/keycape-issuer-eso.yaml b/docs/changes/netkingdom-target-2026-10-17/nk48-r5-f2-implementation-r1/platform-candidate/manifests/templates/keycape-issuer-eso.yaml new file mode 100644 index 00000000..406a29c7 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/nk48-r5-f2-implementation-r1/platform-candidate/manifests/templates/keycape-issuer-eso.yaml @@ -0,0 +1,46 @@ +# PROPOSED SOURCE ONLY — RPF-1.3-T4 / NK48-R5-F2; not admitted or applied. +# Platform controls this template. Only admitted client_id is substituted. +# Provision the store through Platform's separate reviewed owner path; the +# platform-secret-delivery lane applies ONLY the ExternalSecret document. +apiVersion: external-secrets.io/v1 +kind: ClusterSecretStore +metadata: + name: openbao-keycape-{client_id}-issuer +spec: + provider: + vault: + server: http://openbao.openbao.svc:8200 + path: platform + version: v2 + auth: + kubernetes: + mountPath: kubernetes + role: external-secrets-keycape-{client_id} + serviceAccountRef: + name: external-secrets + namespace: external-secrets + audiences: + - openbao + conditions: + - namespaces: + - sso +--- +apiVersion: external-secrets.io/v1 +kind: ExternalSecret +metadata: + name: keycape-{client_id}-client + namespace: sso +spec: + refreshInterval: 5m + secretStoreRef: + kind: ClusterSecretStore + name: openbao-keycape-{client_id}-issuer + target: + name: keycape-{client_id}-client + creationPolicy: Owner + deletionPolicy: Retain + data: + - secretKey: client-secret + remoteRef: + key: workloads/keycape/{client_id} + property: CLIENT_SECRET diff --git a/docs/changes/netkingdom-target-2026-10-17/nk48-r5-f2-implementation-r1/platform-candidate/scripts/keycape_issuer_eso.py b/docs/changes/netkingdom-target-2026-10-17/nk48-r5-f2-implementation-r1/platform-candidate/scripts/keycape_issuer_eso.py new file mode 100644 index 00000000..8acace38 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/nk48-r5-f2-implementation-r1/platform-candidate/scripts/keycape_issuer_eso.py @@ -0,0 +1,70 @@ +"""Proposed fixed issuer ESO source contract for RPF-1.3-T4 / NK48-R5-F2. + +Pure public metadata only. No API/credential/Secret access or apply path. This +new declaration requires independent Platform review before type integration. +The delivery lane may use only external_secret(); store provisioning is a +separate owner action and is never delegated by this module. +""" +from __future__ import annotations + +from copy import deepcopy +from pathlib import Path +import re + +import yaml + +ROOT = Path(__file__).resolve().parents[1] +TEMPLATE = ROOT / 'manifests/templates/keycape-issuer-eso.yaml' + + +class Refused(ValueError): + """Fixed issuer metadata differs; never return an owner delivery receipt.""" + + +def documents(client_id: str) -> tuple[dict, dict]: + # Match the frozen type client_id pattern and existing adapter's derived bound. + # No caller can supply a store name/kind/namespace or an OpenBao path. + if not isinstance(client_id, str) or not re.fullmatch(r'[a-z0-9][a-z0-9-]{1,62}', client_id): + raise Refused('bounded admitted client_id required') + if len('keycape-' + client_id + '-client') > 63: + raise Refused('derived issuer reference exceeds existing adapter bound') + store, external = yaml.safe_load_all(TEMPLATE.read_text().format(client_id=client_id)) + return store, external + + +def external_secret(client_id: str) -> dict: + """The ONLY document the proposed delivery capability may apply.""" + return deepcopy(documents(client_id)[1]) + + +def _matching_public_resource(observed: dict, expected: dict) -> None: + if not isinstance(observed, dict): + raise Refused('public resource observation required') + metadata = observed.get('metadata') + if not isinstance(metadata, dict): + raise Refused('resource metadata required') + expected_namespace = expected['metadata'].get('namespace') + namespace_matches = (metadata.get('namespace') == expected_namespace + if expected_namespace else metadata.get('namespace') in (None, '')) + if (observed.get('apiVersion') != expected['apiVersion'] + or observed.get('kind') != expected['kind'] + or metadata.get('name') != expected['metadata']['name'] + or not namespace_matches + or observed.get('spec') != expected['spec']): + raise Refused('fixed issuer resource differs') + status = observed.get('status') + conditions = status.get('conditions', []) if isinstance(status, dict) else [] + if not isinstance(conditions, list) or not any( + isinstance(c, dict) and c.get('type') == 'Ready' and c.get('status') == 'True' + for c in conditions): + raise Refused('fixed issuer resource not ready') + + +def require_fixed_store(observed: dict, client_id: str) -> None: + """Refuse a missing/drifted/not-ready store; never create it through delivery.""" + _matching_public_resource(observed, documents(client_id)[0]) + + +def require_ready_delivery(observed: dict, client_id: str) -> None: + """Spec equality and Ready precede a created/NO_CHANGE owner receipt.""" + _matching_public_resource(observed, documents(client_id)[1]) diff --git a/docs/changes/netkingdom-target-2026-10-17/nk48-r5-f2-implementation-r1/platform-candidate/tests/test_keycape_issuer_eso.py b/docs/changes/netkingdom-target-2026-10-17/nk48-r5-f2-implementation-r1/platform-candidate/tests/test_keycape_issuer_eso.py new file mode 100644 index 00000000..fd6e75db --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/nk48-r5-f2-implementation-r1/platform-candidate/tests/test_keycape_issuer_eso.py @@ -0,0 +1,129 @@ +"""Public metadata fixtures only: no API, credentials, or native calls.""" +import copy +import sys +import unittest +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "scripts")) +import keycape_issuer_eso as contract + + +class FixedIssuerContract(unittest.TestCase): + def observations(self): + resources = contract.documents("forgejo") + for resource in resources: + resource["metadata"].update(uid="public-fixture-uid", resourceVersion="7") + resource["status"] = {"conditions": [{"type": "Ready", "status": "True"}]} + return resources + + def test_independent_forgejo_binding(self): + # Independent owner-source example: CCR0041 role/SA/audience and the + # existing issuer ESO draft's provider/target/relative key convention. + store, external = contract.documents("forgejo") + self.assertEqual(store["kind"], "ClusterSecretStore") + self.assertEqual(store["metadata"], {"name": "openbao-keycape-forgejo-issuer"}) + self.assertEqual(store["spec"], { + "provider": {"vault": { + "server": "http://openbao.openbao.svc:8200", "path": "platform", "version": "v2", + "auth": {"kubernetes": {"mountPath": "kubernetes", + "role": "external-secrets-keycape-forgejo", + "serviceAccountRef": {"name": "external-secrets", "namespace": "external-secrets", + "audiences": ["openbao"]}}}}}, + "conditions": [{"namespaces": ["sso"]}]}) + self.assertEqual(external["metadata"], {"name": "keycape-forgejo-client", "namespace": "sso"}) + self.assertEqual(external["spec"], { + "refreshInterval": "5m", + "secretStoreRef": {"kind": "ClusterSecretStore", "name": "openbao-keycape-forgejo-issuer"}, + "target": {"name": "keycape-forgejo-client", "creationPolicy": "Owner", "deletionPolicy": "Retain"}, + "data": [{"secretKey": "client-secret", "remoteRef": { + "key": "workloads/keycape/forgejo", "property": "CLIENT_SECRET"}}]}) + + def test_valid_ready_metadata_allows_extra_controller_metadata(self): + store, external = self.observations() + contract.require_fixed_store(store, "forgejo") + contract.require_ready_delivery(external, "forgejo") + + def test_store_drift_refuses(self): + changes = [ + ("kind", "SecretStore"), + ("metadata.namespace", "sso"), + ("metadata.name", "caller-store"), + ("spec.provider.vault.server", "https://wrong.invalid"), + ("spec.provider.vault.path", "tenant"), + ("spec.provider.vault.auth.kubernetes.role", "external-secrets-keycape-sibling"), + ("spec.provider.vault.auth.kubernetes.serviceAccountRef.audiences", ["wrong"]), + ("spec.provider.vault.auth.kubernetes.serviceAccountRef.namespace", "sso"), + ("spec.conditions", [{"namespaces": ["sso", "other"]}]), + ] + for path, value in changes: + with self.subTest(path=path): + store = self.observations()[0] + mutate(store, path, value) + with self.assertRaises(contract.Refused): + contract.require_fixed_store(store, "forgejo") + + def test_external_drift_refuses(self): + changes = [ + ("metadata.namespace", ""), ("metadata.namespace", None), ("metadata.namespace", "other"), + ("metadata.name", "keycape-sibling-client"), + ("spec.secretStoreRef.kind", "SecretStore"), + ("spec.secretStoreRef.name", "openbao-keycape-forgejo-consumer"), + ("spec.target.name", "other"), ("spec.target.creationPolicy", "Merge"), + ("spec.target.deletionPolicy", "Delete"), + ("spec.data", [{"secretKey": "client-secret", "remoteRef": { + "key": "platform/workloads/keycape/forgejo", "property": "CLIENT_SECRET"}}]), + ("spec.data", [{"secretKey": "wrong", "remoteRef": { + "key": "workloads/keycape/forgejo", "property": "CLIENT_SECRET"}}]), + ("spec.data", [{"secretKey": "client-secret", "remoteRef": { + "key": "workloads/keycape/forgejo", "property": "wrong"}}]), + ] + for path, value in changes: + with self.subTest(path=path, value=value): + external = self.observations()[1] + mutate(external, path, value) + with self.assertRaises(contract.Refused): + contract.require_ready_delivery(external, "forgejo") + + def test_missing_or_unready_refuses(self): + for check, index in [(contract.require_fixed_store, 0), (contract.require_ready_delivery, 1)]: + for status in [None, {}, [], "bad", {"conditions": None}, + {"conditions": [{"type": "Ready", "status": "False"}]}]: + with self.subTest(check=check.__name__, status=status): + observed = self.observations()[index] + observed["status"] = status + with self.assertRaises(contract.Refused): + check(observed, "forgejo") + for malformed in [None, [], {}, {"metadata": None}]: + with self.assertRaises(contract.Refused): + check(malformed, "forgejo") + + def test_no_caller_store_or_path_override(self): + for invalid in ["", "f", "../forgejo", "forgejo/sibling", "Forgejo", "forgejo\n", "-forgejo", "f" * 49, None]: + with self.subTest(invalid=invalid), self.assertRaises(contract.Refused): + contract.documents(invalid) + with self.assertRaises(TypeError): + contract.external_secret("forgejo", store_name="caller-store") + + def test_type_admitted_trailing_dash_is_preserved(self): + self.assertEqual(contract.external_secret("forgejo-")["metadata"]["name"], + "keycape-forgejo--client") + self.assertEqual(contract.documents("forgejo-")[0]["metadata"]["name"], + "openbao-keycape-forgejo--issuer") + + def test_delivery_document_is_only_external_and_detached(self): + external = contract.external_secret("forgejo") + self.assertEqual(external["kind"], "ExternalSecret") + external["spec"]["secretStoreRef"]["name"] = "tampered" + self.assertEqual(contract.external_secret("forgejo")["spec"]["secretStoreRef"]["name"], + "openbao-keycape-forgejo-issuer") + + +def mutate(resource, path, value): + parts = path.split(".") + for key in parts[:-1]: + resource = resource[key] + resource[parts[-1]] = copy.deepcopy(value) + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/docs/changes/netkingdom-target-2026-10-17/nk48-r5-f2-implementation-r1/platform.candidate.patch b/docs/changes/netkingdom-target-2026-10-17/nk48-r5-f2-implementation-r1/platform.candidate.patch new file mode 100644 index 00000000..8d47282f --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/nk48-r5-f2-implementation-r1/platform.candidate.patch @@ -0,0 +1,325 @@ +--- /dev/null ++++ b/docs/keycape-issuer-eso-contract.md +@@ -0,0 +1,68 @@ ++# Proposed fixed issuer ESO contract — RPF-1.3-T4 / NK48-R5-F2 ++ ++Status: proposed source; not admitted, deployed, or owner-acknowledged. This is a ++new Platform declaration for review, not an existing Forgejo grant discovery. ++ ++The only substitution in `manifests/templates/keycape-issuer-eso.yaml` ++(sha256:e17498bd8a4c820041a4105e4efe9c60c6a50953db8f621ff54887080065c743) is the admitted `client_id`. The proposed issuer ++store is **ClusterSecretStore `openbao-keycape-{client_id}-issuer`**, cluster ++scoped (no metadata namespace), restricted by conditions to ExternalSecrets in ++`sso`. For Forgejo it is `openbao-keycape-forgejo-issuer`. The suffix makes the ++issuer purpose explicit and keeps type-admitted IDs ending in `-` representable. ++The store's DNS subdomain name can exceed 63 characters; the existing adapter's ++63-character target Secret reference bound is preserved (client ID at most 48). ++No type parameter is added or changed. ++ ++The provider is `http://openbao.openbao.svc:8200`, KV v2 mount `platform`, ++Kubernetes auth mount `kubernetes`, role `external-secrets-keycape-{client_id}`, ++serviceAccountRef `external-secrets/external-secrets`, audience `openbao`. ++CCR-2026-0041 already records that Forgejo role/identity/audience, not this new ++store name or an unrestricted client family grant. Other clients still require ++their own existing owner-controlled role/case admission. ++ ++The ExternalSecret is `sso/keycape-{client_id}-client`, references precisely that ++ClusterSecretStore, refreshes every 5m and targets the same name with Owner ++creation and Retain remote deletion. Its only data entry maps `client-secret` ++from remoteRef `workloads/keycape/{client_id}`, property `CLIENT_SECRET`. The ++provider mount plus mount-relative remoteRef equals custody reference ++`platform/workloads/keycape/{client_id}`; do not double-prefix `platform/`. ++This is distinct from the Apps-owned consumer namespace SecretStore ++`openbao-keycape-{client_id}-consumer` and its dedicated consumer identity. ++ ++Store provisioning belongs to Platform's separate owner-controlled path. The ++proposed `platform-secret-delivery` lane retains ONLY create/get/watch/delete on ++the exact ExternalSecret in `sso`; this source does not delegate store creation ++or Secret read/list. Never apply the multi-document template through that lane. ++The pure `scripts/keycape_issuer_eso.py` module exposes only the ExternalSecret ++document through `external_secret()` and validates fixed public observations ++through `require_fixed_store()` and `require_ready_delivery()`. It performs no ++API call and issues no evidence/receipt. It is not a real capability or admission ++boundary; a caller-supplied observation alone establishes no authenticity. ++ ++The existing RPF-1.3-T4 delivery binding must validate the case/client binding, ++verify this exact preexisting store's public identity/spec/Ready state, apply or ++compare the exact ExternalSecret, and wait for matching spec plus Ready=True. ++The proposed helper accepts extra controller metadata but deliberately refuses ++spec drift. Actual API defaulting/normalization and installed ESO schema support ++must be checked in that implementation; these fixtures establish no native ++compatibility or credential isolation. ESO's shared controller remains trusted. ++No Secret bodies, values, arbitrary remoteRefs, or raw errors may enter receipts. ++ ++Existing owner requirements remain: closed `netkingdom.deliver-secret.v1` ++metadata receipt with authenticated before/after UID/resourceVersion provenance, ++unchanged objects reported NO_CHANGE, attempt-session revocation, and created-only ++rollback guarded by exact UID/resourceVersion (drift or uncertain ownership goes ++to owner reconciliation). The helper neither deletes a target Secret nor creates ++an OpenBao role/policy nor changes CCR-0041. These capabilities and catalog routing ++remain unimplemented/unaccepted under RPF-1.3-T4. Independent Platform source ++review of this declaration precedes adopting the conditional NetKingdom type ++text; no descriptor acceptance, acknowledgment, pin, grant, catalog or native ++permission is inferred. ++ ++Grounding: CCR-2026-0041 exact role/identity; the approved historical ++`docs/provisioning-windows/2026-10-04-1000/keycape/eso.draft.yaml` issuer ++ClusterSecretStore and target pattern; `scripts/catalog_lane_scaffold.py` ++ClusterSecretStore/namespace restriction and mount-relative remoteRef model; ++RPF-1.3-C1 and RPF-1.3-T4 in the frozen owner review. The scaffold's historical ++audience field placement differs from the issuer draft; this candidate uses ++the issuer draft's serviceAccountRef.audiences, not a claim of installed support. +--- /dev/null ++++ b/manifests/templates/keycape-issuer-eso.yaml +@@ -0,0 +1,46 @@ ++# PROPOSED SOURCE ONLY — RPF-1.3-T4 / NK48-R5-F2; not admitted or applied. ++# Platform controls this template. Only admitted client_id is substituted. ++# Provision the store through Platform's separate reviewed owner path; the ++# platform-secret-delivery lane applies ONLY the ExternalSecret document. ++apiVersion: external-secrets.io/v1 ++kind: ClusterSecretStore ++metadata: ++ name: openbao-keycape-{client_id}-issuer ++spec: ++ provider: ++ vault: ++ server: http://openbao.openbao.svc:8200 ++ path: platform ++ version: v2 ++ auth: ++ kubernetes: ++ mountPath: kubernetes ++ role: external-secrets-keycape-{client_id} ++ serviceAccountRef: ++ name: external-secrets ++ namespace: external-secrets ++ audiences: ++ - openbao ++ conditions: ++ - namespaces: ++ - sso ++--- ++apiVersion: external-secrets.io/v1 ++kind: ExternalSecret ++metadata: ++ name: keycape-{client_id}-client ++ namespace: sso ++spec: ++ refreshInterval: 5m ++ secretStoreRef: ++ kind: ClusterSecretStore ++ name: openbao-keycape-{client_id}-issuer ++ target: ++ name: keycape-{client_id}-client ++ creationPolicy: Owner ++ deletionPolicy: Retain ++ data: ++ - secretKey: client-secret ++ remoteRef: ++ key: workloads/keycape/{client_id} ++ property: CLIENT_SECRET +--- /dev/null ++++ b/scripts/keycape_issuer_eso.py +@@ -0,0 +1,70 @@ ++"""Proposed fixed issuer ESO source contract for RPF-1.3-T4 / NK48-R5-F2. ++ ++Pure public metadata only. No API/credential/Secret access or apply path. This ++new declaration requires independent Platform review before type integration. ++The delivery lane may use only external_secret(); store provisioning is a ++separate owner action and is never delegated by this module. ++""" ++from __future__ import annotations ++ ++from copy import deepcopy ++from pathlib import Path ++import re ++ ++import yaml ++ ++ROOT = Path(__file__).resolve().parents[1] ++TEMPLATE = ROOT / 'manifests/templates/keycape-issuer-eso.yaml' ++ ++ ++class Refused(ValueError): ++ """Fixed issuer metadata differs; never return an owner delivery receipt.""" ++ ++ ++def documents(client_id: str) -> tuple[dict, dict]: ++ # Match the frozen type client_id pattern and existing adapter's derived bound. ++ # No caller can supply a store name/kind/namespace or an OpenBao path. ++ if not isinstance(client_id, str) or not re.fullmatch(r'[a-z0-9][a-z0-9-]{1,62}', client_id): ++ raise Refused('bounded admitted client_id required') ++ if len('keycape-' + client_id + '-client') > 63: ++ raise Refused('derived issuer reference exceeds existing adapter bound') ++ store, external = yaml.safe_load_all(TEMPLATE.read_text().format(client_id=client_id)) ++ return store, external ++ ++ ++def external_secret(client_id: str) -> dict: ++ """The ONLY document the proposed delivery capability may apply.""" ++ return deepcopy(documents(client_id)[1]) ++ ++ ++def _matching_public_resource(observed: dict, expected: dict) -> None: ++ if not isinstance(observed, dict): ++ raise Refused('public resource observation required') ++ metadata = observed.get('metadata') ++ if not isinstance(metadata, dict): ++ raise Refused('resource metadata required') ++ expected_namespace = expected['metadata'].get('namespace') ++ namespace_matches = (metadata.get('namespace') == expected_namespace ++ if expected_namespace else metadata.get('namespace') in (None, '')) ++ if (observed.get('apiVersion') != expected['apiVersion'] ++ or observed.get('kind') != expected['kind'] ++ or metadata.get('name') != expected['metadata']['name'] ++ or not namespace_matches ++ or observed.get('spec') != expected['spec']): ++ raise Refused('fixed issuer resource differs') ++ status = observed.get('status') ++ conditions = status.get('conditions', []) if isinstance(status, dict) else [] ++ if not isinstance(conditions, list) or not any( ++ isinstance(c, dict) and c.get('type') == 'Ready' and c.get('status') == 'True' ++ for c in conditions): ++ raise Refused('fixed issuer resource not ready') ++ ++ ++def require_fixed_store(observed: dict, client_id: str) -> None: ++ """Refuse a missing/drifted/not-ready store; never create it through delivery.""" ++ _matching_public_resource(observed, documents(client_id)[0]) ++ ++ ++def require_ready_delivery(observed: dict, client_id: str) -> None: ++ """Spec equality and Ready precede a created/NO_CHANGE owner receipt.""" ++ _matching_public_resource(observed, documents(client_id)[1]) +--- /dev/null ++++ b/tests/test_keycape_issuer_eso.py +@@ -0,0 +1,129 @@ ++"""Public metadata fixtures only: no API, credentials, or native calls.""" ++import copy ++import sys ++import unittest ++from pathlib import Path ++ ++sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "scripts")) ++import keycape_issuer_eso as contract ++ ++ ++class FixedIssuerContract(unittest.TestCase): ++ def observations(self): ++ resources = contract.documents("forgejo") ++ for resource in resources: ++ resource["metadata"].update(uid="public-fixture-uid", resourceVersion="7") ++ resource["status"] = {"conditions": [{"type": "Ready", "status": "True"}]} ++ return resources ++ ++ def test_independent_forgejo_binding(self): ++ # Independent owner-source example: CCR0041 role/SA/audience and the ++ # existing issuer ESO draft's provider/target/relative key convention. ++ store, external = contract.documents("forgejo") ++ self.assertEqual(store["kind"], "ClusterSecretStore") ++ self.assertEqual(store["metadata"], {"name": "openbao-keycape-forgejo-issuer"}) ++ self.assertEqual(store["spec"], { ++ "provider": {"vault": { ++ "server": "http://openbao.openbao.svc:8200", "path": "platform", "version": "v2", ++ "auth": {"kubernetes": {"mountPath": "kubernetes", ++ "role": "external-secrets-keycape-forgejo", ++ "serviceAccountRef": {"name": "external-secrets", "namespace": "external-secrets", ++ "audiences": ["openbao"]}}}}}, ++ "conditions": [{"namespaces": ["sso"]}]}) ++ self.assertEqual(external["metadata"], {"name": "keycape-forgejo-client", "namespace": "sso"}) ++ self.assertEqual(external["spec"], { ++ "refreshInterval": "5m", ++ "secretStoreRef": {"kind": "ClusterSecretStore", "name": "openbao-keycape-forgejo-issuer"}, ++ "target": {"name": "keycape-forgejo-client", "creationPolicy": "Owner", "deletionPolicy": "Retain"}, ++ "data": [{"secretKey": "client-secret", "remoteRef": { ++ "key": "workloads/keycape/forgejo", "property": "CLIENT_SECRET"}}]}) ++ ++ def test_valid_ready_metadata_allows_extra_controller_metadata(self): ++ store, external = self.observations() ++ contract.require_fixed_store(store, "forgejo") ++ contract.require_ready_delivery(external, "forgejo") ++ ++ def test_store_drift_refuses(self): ++ changes = [ ++ ("kind", "SecretStore"), ++ ("metadata.namespace", "sso"), ++ ("metadata.name", "caller-store"), ++ ("spec.provider.vault.server", "https://wrong.invalid"), ++ ("spec.provider.vault.path", "tenant"), ++ ("spec.provider.vault.auth.kubernetes.role", "external-secrets-keycape-sibling"), ++ ("spec.provider.vault.auth.kubernetes.serviceAccountRef.audiences", ["wrong"]), ++ ("spec.provider.vault.auth.kubernetes.serviceAccountRef.namespace", "sso"), ++ ("spec.conditions", [{"namespaces": ["sso", "other"]}]), ++ ] ++ for path, value in changes: ++ with self.subTest(path=path): ++ store = self.observations()[0] ++ mutate(store, path, value) ++ with self.assertRaises(contract.Refused): ++ contract.require_fixed_store(store, "forgejo") ++ ++ def test_external_drift_refuses(self): ++ changes = [ ++ ("metadata.namespace", ""), ("metadata.namespace", None), ("metadata.namespace", "other"), ++ ("metadata.name", "keycape-sibling-client"), ++ ("spec.secretStoreRef.kind", "SecretStore"), ++ ("spec.secretStoreRef.name", "openbao-keycape-forgejo-consumer"), ++ ("spec.target.name", "other"), ("spec.target.creationPolicy", "Merge"), ++ ("spec.target.deletionPolicy", "Delete"), ++ ("spec.data", [{"secretKey": "client-secret", "remoteRef": { ++ "key": "platform/workloads/keycape/forgejo", "property": "CLIENT_SECRET"}}]), ++ ("spec.data", [{"secretKey": "wrong", "remoteRef": { ++ "key": "workloads/keycape/forgejo", "property": "CLIENT_SECRET"}}]), ++ ("spec.data", [{"secretKey": "client-secret", "remoteRef": { ++ "key": "workloads/keycape/forgejo", "property": "wrong"}}]), ++ ] ++ for path, value in changes: ++ with self.subTest(path=path, value=value): ++ external = self.observations()[1] ++ mutate(external, path, value) ++ with self.assertRaises(contract.Refused): ++ contract.require_ready_delivery(external, "forgejo") ++ ++ def test_missing_or_unready_refuses(self): ++ for check, index in [(contract.require_fixed_store, 0), (contract.require_ready_delivery, 1)]: ++ for status in [None, {}, [], "bad", {"conditions": None}, ++ {"conditions": [{"type": "Ready", "status": "False"}]}]: ++ with self.subTest(check=check.__name__, status=status): ++ observed = self.observations()[index] ++ observed["status"] = status ++ with self.assertRaises(contract.Refused): ++ check(observed, "forgejo") ++ for malformed in [None, [], {}, {"metadata": None}]: ++ with self.assertRaises(contract.Refused): ++ check(malformed, "forgejo") ++ ++ def test_no_caller_store_or_path_override(self): ++ for invalid in ["", "f", "../forgejo", "forgejo/sibling", "Forgejo", "forgejo\n", "-forgejo", "f" * 49, None]: ++ with self.subTest(invalid=invalid), self.assertRaises(contract.Refused): ++ contract.documents(invalid) ++ with self.assertRaises(TypeError): ++ contract.external_secret("forgejo", store_name="caller-store") ++ ++ def test_type_admitted_trailing_dash_is_preserved(self): ++ self.assertEqual(contract.external_secret("forgejo-")["metadata"]["name"], ++ "keycape-forgejo--client") ++ self.assertEqual(contract.documents("forgejo-")[0]["metadata"]["name"], ++ "openbao-keycape-forgejo--issuer") ++ ++ def test_delivery_document_is_only_external_and_detached(self): ++ external = contract.external_secret("forgejo") ++ self.assertEqual(external["kind"], "ExternalSecret") ++ external["spec"]["secretStoreRef"]["name"] = "tampered" ++ self.assertEqual(contract.external_secret("forgejo")["spec"]["secretStoreRef"]["name"], ++ "openbao-keycape-forgejo-issuer") ++ ++ ++def mutate(resource, path, value): ++ parts = path.split(".") ++ for key in parts[:-1]: ++ resource = resource[key] ++ resource[parts[-1]] = copy.deepcopy(value) ++ ++ ++if __name__ == "__main__": ++ unittest.main(verbosity=2) diff --git a/docs/changes/netkingdom-target-2026-10-17/nk48-r5-f2-implementation-r1/review.json b/docs/changes/netkingdom-target-2026-10-17/nk48-r5-f2-implementation-r1/review.json new file mode 100644 index 00000000..7b81699e --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/nk48-r5-f2-implementation-r1/review.json @@ -0,0 +1,66 @@ +{ + "schema": "custodian.nk48-r5-f2-implementation-candidate.v1", + "status": "proposed-source-not-admitted", + "date": "2026-10-17", + "scope": "NK-WP-0048-T16; existing RPF-1.3-T4 implementation preparation only", + "source_owner_assent_received": false, + "finding": "NK48-R5-F2", + "owner_requirement": "RPF-1.3-C1", + "netkingdom_base": "3236d5000e52e0643f33906a26a7825aa0b67465", + "reviewed_code": "001a38ca2aa85155ce4840c4da22aca6e2346171", + "platform_base": "0a9617a62d4696a8070798923b459e8405c3dbf2", + "discovery": "No existing Forgejo issuer store declaration found in bounded public source search. No claim about live absence.", + "new_proposed_binding": { + "kind": "ClusterSecretStore", + "name": "openbao-keycape-{client_id}-issuer", + "namespace": null, + "allowed_external_secret_namespace": "sso", + "role": "external-secrets-keycape-{client_id}", + "service_account_ref": "external-secrets/external-secrets", + "audiences": [ + "openbao" + ], + "template_path": "manifests/templates/keycape-issuer-eso.yaml", + "template_sha256": "e17498bd8a4c820041a4105e4efe9c60c6a50953db8f621ff54887080065c743", + "external_secret": "sso/keycape-{client_id}-client", + "secret_key": "client-secret", + "provider_mount": "platform", + "remote_ref_key": "workloads/keycape/{client_id}", + "remote_ref_property": "CLIENT_SECRET", + "target_creation_policy": "Owner", + "remote_deletion_policy": "Retain" + }, + "candidate_patches": { + "platform.candidate.patch": "a490df7dee41e15351da8d2906e1342edebb533984befe3d9ae74d952dbb091b", + "netkingdom-entry-point.conditional.patch": "6784a21fbd10bd62712be2e0f4929034601c94629aa74ccaa2e93fc815eecd61" + }, + "checks": { + "tests_passed": 8, + "subtests_passed": 42, + "patch_dryruns_passed": true, + "only_type_structural_delta": "spec.follow_through[id=deliver-secret].entry_point", + "parameters_bytes_unchanged": true + }, + "remaining_existing_owner_work": [ + "Independent Platform review/adoption of explicit template and metadata helper under RPF-1.3-T4; no assent inferred.", + "After that review, integrate the conditional NetKingdom text with F3 and regenerate combined composition artifact through existing owner path.", + "RPF-1.3-T4 real authenticated deliver/remove/evidence/session capabilities and catalog routing acceptance remain separate existing requirements.", + "Actual approved owner execution must establish installed ESO compatibility, authentic resource observation, fixed preexisting store and receipt/rollback semantics; source fixtures prove none of these." + ], + "changes_not_made": [ + "canonical checkouts", + "workplans/tasks/UUIDs", + "descriptors", + "catalog", + "CCR0041/grants", + "pins/acknowledgments", + "default 1.1 or revision 1.2", + "native resources", + "credentials/Secret values" + ], + "knowledge": { + "version": "0.2.6", + "digest": "8d1d6a9d4d07fec7b619618de7dfe2be2254c1790f85f4198c7fe19db9751c02", + "status": "withheld-orientation-only; current source SECURITY-COMPANION and actual owner contracts reviewed; no authority inferred" + } +} diff --git a/docs/changes/netkingdom-target-2026-10-17/nk48-r5-f2-implementation-r1/source-pins.json b/docs/changes/netkingdom-target-2026-10-17/nk48-r5-f2-implementation-r1/source-pins.json new file mode 100644 index 00000000..ed00c613 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/nk48-r5-f2-implementation-r1/source-pins.json @@ -0,0 +1,108 @@ +[ + { + "repo": "net-kingdom", + "commit": "3236d5000e52e0643f33906a26a7825aa0b67465", + "path": "security-case-type-revisions/1.3/oidc-browser-relying-party/type.yaml", + "sha256": "e75fe94110ef9fb7fb18072fe2856941b33507c99cb406d36576046481485d0a", + "working_tree_equals_commit": true, + "line_anchors": [ + 14, + 17, + 269, + 270, + 273 + ] + }, + { + "repo": "net-kingdom", + "commit": "3236d5000e52e0643f33906a26a7825aa0b67465", + "path": "sso-mfa/k8s/keycape/deliver-secret-adapter.py", + "sha256": "cd1a515a1ae2def2093660ed2a810a9c66d1d4cf7126f573ee7e9c4c42afb98d", + "working_tree_equals_commit": true, + "line_anchors": [ + 47, + 60, + 94, + 98 + ] + }, + { + "repo": "railiance-platform", + "commit": "0a9617a62d4696a8070798923b459e8405c3dbf2", + "path": "credential-change-requests/CCR-2026-0041-forgejo-netkingdom-oidc-keycape-eso-reader.yaml", + "sha256": "e69077ad29166770e4a57b4c4105615e272822d8fde11df591d0c127b158ac67", + "working_tree_equals_commit": true, + "line_anchors": [ + 40, + 44, + 52, + 56, + 66, + 89 + ] + }, + { + "repo": "railiance-platform", + "commit": "0a9617a62d4696a8070798923b459e8405c3dbf2", + "path": "docs/provisioning-windows/2026-10-04-1000/keycape/eso.draft.yaml", + "sha256": "c8c334fae9d7e7e2f74ac30095d2266d626e857f7c5a85ad4e20b28543678c5d", + "working_tree_equals_commit": true, + "line_anchors": [ + 3, + 5, + 16, + 18, + 19, + 22, + 26, + 33, + 43, + 55 + ] + }, + { + "repo": "railiance-platform", + "commit": "0a9617a62d4696a8070798923b459e8405c3dbf2", + "path": "docs/credential-lane-designs/keycape-secrets-engine-openbao-issuer-input.yaml", + "sha256": "ade2e10b0f65341051b955900c68931bdd56fd3a81bf452ae81229cc2390ad63", + "working_tree_equals_commit": true, + "line_anchors": [ + 44, + 56 + ] + }, + { + "repo": "railiance-platform", + "commit": "0a9617a62d4696a8070798923b459e8405c3dbf2", + "path": "scripts/catalog_lane_scaffold.py", + "sha256": "96d60cc47aa78728f5ba96ce1fd63033adf23c05641de93ebbdda3902082d2d1", + "working_tree_equals_commit": true, + "line_anchors": [ + 137, + 144, + 145, + 149, + 154 + ] + }, + { + "repo": "railiance-platform", + "commit": "0a9617a62d4696a8070798923b459e8405c3dbf2", + "path": "argocd/platform-addons/openbao-secretstore/openbao-keycape-approval-clients.clustersecretstore.yaml", + "sha256": "9ce6533e652db5428248c37319b6096a5f8f4af82138141473de4d4ca809e692", + "working_tree_equals_commit": true, + "line_anchors": [ + 1 + ] + }, + { + "repo": "the-custodian", + "path": "docs/changes/netkingdom-target-2026-10-17/nk48-adapters-1.2-review/coordinator-review-r5.json", + "sha256": "e3016352c58178894ccbfc600b960d3f9e2b5d534a05b8df2c5b79737cb0127a" + }, + { + "repo": "the-custodian", + "path": "docs/changes/netkingdom-target-2026-10-17/nk48-1.3-owner-review/owner-review-railiance-platform.json", + "sha256": "d3f523b60924b530376b7aac4459cecda5da183848dd6e35889c1013c73439d9" + } +] diff --git a/docs/changes/netkingdom-target-2026-10-17/nk48-r5-f2-implementation-r1/structural-and-patch-checks.json b/docs/changes/netkingdom-target-2026-10-17/nk48-r5-f2-implementation-r1/structural-and-patch-checks.json new file mode 100644 index 00000000..1e5cc2b4 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/nk48-r5-f2-implementation-r1/structural-and-patch-checks.json @@ -0,0 +1,8 @@ +{ + "only_structural_delta": "spec.follow_through[id=deliver-secret].entry_point", + "parameters_raw_bytes_identical": true, + "descriptor_changes": false, + "canonical_1_1_1_2_changes": false, + "base": "3236d5000e52e0643f33906a26a7825aa0b67465", + "both_patch_dryruns_exit": 0 +} diff --git a/docs/changes/netkingdom-target-2026-10-17/nk48-r5-f2-implementation-r1/type.before.yaml b/docs/changes/netkingdom-target-2026-10-17/nk48-r5-f2-implementation-r1/type.before.yaml new file mode 100644 index 00000000..6ec75605 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/nk48-r5-f2-implementation-r1/type.before.yaml @@ -0,0 +1,495 @@ +apiVersion: netkingdom.io/security-case-type/v0.1 +kind: SecurityCaseType +spec: + id: oidc-browser-relying-party + version: '1.3' + status: proposed + title: Browser OIDC relying party for an internal service + purpose: 'Let an internal web service sign its human users in through NetKingdom + (KeyCape): an exact-redirect OIDC client, optional MFA pin, the LLDAP groups the + service maps to its own roles, and, for confidential clients, a client secret + minted straight into OpenBao custody. Replaces the per-client rollout scripts + in sso-mfa/k8s/keycape. First case: Forgejo (KEY-WP-0038).' + parameters: + - name: client_id + type: string + case_settable: true + constraints: + pattern: ^[a-z0-9][a-z0-9-]{1,62}$ + sensitivity: operational + description: OAuth2 client_id, unique in KeyCape. + - name: display_name + type: string + case_settable: true + constraints: + pattern: ^[^\n\r]{1,80}$ + sensitivity: public + description: Name shown on the sign-in page. + - name: redirect_uris + type: string + case_settable: true + constraints: + pattern: ^https://[^*?#\s,]+(,https://[^*?#\s,]+)*$ + sensitivity: security_sensitive + description: 'Comma-separated exact https redirect URIs. No wildcards, no plain + http, no query and no fragment: KeyCape refuses ''*'' and ''?'' at startup, + and OAuth 2.0 forbids fragments (key-cape type review R1).' + - name: allowed_scopes + type: string + case_settable: true + constraints: + enum: + - openid,profile,email,groups + - openid,email,groups + - openid,profile,groups + - openid,groups + sensitivity: security_sensitive + description: Scopes the client may request, openid first. Always includes groups, + because access_groups is required and KeyCape's UserInfo returns groups only + for that scope (key-cape type review R2). + - name: client_type + type: string + case_settable: true + constraints: + enum: + - confidential + - public + sensitivity: security_sensitive + description: confidential for server-side services that can hold a secret; public + for SPAs and native apps (PKCE only). + - name: mfa_required + type: string + case_settable: true + constraints: + enum: + - 'true' + - 'false' + sensitivity: security_sensitive + description: '"true" pins MFA on this client; required when the service grants + administrative or owner rights. "false" is not "no MFA": the client then carries + no mfaRequired field and KeyCape applies the per-user privacyIDEA policy. A + renderer must never emit an explicit mfaRequired false, which would skip that + policy (key-cape type review R3).' + - name: access_groups + type: string + case_settable: true + constraints: + pattern: ^[a-z][a-z0-9-]{1,62}(,[a-z][a-z0-9-]{1,62})*$ + sensitivity: operational + description: 'Comma-separated LLDAP groups that the service maps to its roles. + Created if absent. KeyCape has no per-client group gate: it issues tokens to + any authenticated user and only carries the groups claim, so the consuming service + enforces the gate itself (for Forgejo, its required-claim setting; key-cape + type review R2).' + - name: initial_members + type: string + required: false + case_settable: true + constraints: + pattern: ^[a-z][a-z0-9-]*=[a-z][a-z0-9._-]*(\+[a-z][a-z0-9._-]*)*(;[a-z][a-z0-9-]*=[a-z][a-z0-9._-]*(\+[a-z][a-z0-9._-]*)*)*$ + sensitivity: operational + description: Initial memberships as group=user+user;group=user. Usernames are + directory usernames, never secrets. + - name: consuming_service + type: string + case_settable: true + constraints: + pattern: ^[a-z0-9][a-z0-9-]{0,62}$ + sensitivity: operational + description: The service that signs users in with this client. + - name: consumer_owner_repo + type: string + case_settable: true + constraints: + pattern: ^[a-z0-9][a-z0-9-]*$ + sensitivity: operational + description: The repository that owns the consuming service's configuration. + - name: consumer_namespace + type: string + case_settable: true + constraints: + pattern: ^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$ + sensitivity: security_sensitive + description: Consumer namespace for namespaced ESO TokenRequest reader; actual + workload owner must confirm; never infer from service name. + - name: issuer + type: string + case_settable: false + default: https://kc.coulomb.social + sensitivity: security_sensitive + description: The NetKingdom issuer; fixed by the type. + composition: + authority: netkingdom + initial_trust: + - runtime_secret_trust + - runtime_identity_trust + capabilities: + - c1.lightweight-sso + providers: + c1.lightweight-sso: net-kingdom.keycape-client-registration + parameter_bindings: + net-kingdom.keycape-client-registration.client_id: client_id + net-kingdom.keycape-client-registration.display_name: display_name + net-kingdom.keycape-client-registration.redirect_uris: redirect_uris + net-kingdom.keycape-client-registration.allowed_scopes: allowed_scopes + net-kingdom.keycape-client-registration.client_type: client_type + net-kingdom.keycape-client-registration.mfa_required: mfa_required + materialization: + - id: client-secret-path + kind: kv_path + when: client_type == confidential + template: platform/workloads/keycape/{client_id} + body: field CLIENT_SECRET; minted by the credential broker, never written by hand + - id: keycape-reader-policy + kind: openbao_policy + when: client_type == confidential + template: workload-kv-read-keycape-{client_id}-eso + body: "path \"platform/data/workloads/keycape/{client_id}\" {{\n capabilities\ + \ = [\"read\"]\n}}\n" + description: KeyCape reads its client secret only through ESO. No metadata, list, + parent or sibling access. + - id: consumer-reader-policy + kind: openbao_policy + when: client_type == confidential + template: workload-kv-read-keycape-{client_id}-{consuming_service} + body: "path \"platform/data/workloads/keycape/{client_id}\" {{\n capabilities\ + \ = [\"read\"]\n}}\n" + description: The consuming service reads the same secret. No metadata, list, parent + or sibling access. + - id: eso-role + kind: openbao_role + when: client_type == confidential + template: external-secrets-keycape-{client_id} + body: 'auth: kubernetes + + bound_service_account_names: [external-secrets] + + bound_service_account_namespaces: [external-secrets] + + token_policies: [workload-kv-read-keycape-{client_id}-eso] + + token_no_default_policy: true + + token_ttl: 15m + + token_max_ttl: 15m + + ' + description: The Kubernetes-auth role deliver-secret uses; bound to the ESO service + account only. + - id: consumer-eso-role + kind: openbao_role + when: client_type == confidential + template: external-secrets-keycape-{client_id}-{consuming_service} + body: 'auth: kubernetes + + bound_service_account_names: [{consuming_service}-keycape-{client_id}-eso] + + bound_service_account_namespaces: [{consumer_namespace}] + + audience: openbao + + token_policies: [workload-kv-read-keycape-{client_id}-{consuming_service}] + + token_no_default_policy: true + + token_ttl: 15m + + token_max_ttl: 15m + + ' + description: Consumer ESO requests a token for this separate reference identity. + Common ESO controller remains trusted and can request tokens; no cryptographic + controller isolation is claimed. + - id: catalog-entry + kind: catalog_entry + template: '{consuming_service}-keycape-client' + body: 'id: {consuming_service}-keycape-client + + owner_repo: key-cape + + warden_executes: false + + risk: high + + status: active once verification passes + + workload_ref: applicable, name {consuming_service}, declaration_ref of the consumer''s + published declaration or the explicit unknown form, never inferred + + path_template: platform/workloads/keycape/{client_id} + + field: CLIENT_SECRET + + delegation: native, intended_owner key-cape + + rotation: mint-secret then verification; automatable false until a run has been + proven + + ' + description: One entry per case (ops-mason item 5, ops-warden R4'). It carries + the consumer's workload reference, which ADR-0009 needs for zone selection. + follow_through: + - id: ensure-groups + owner: net-kingdom + entry_point: reviewed adapter net-kingdom/keycape-ensure-groups, dispatched by Warden's + case kernel inside the attended session; it calls identity-provisioner + ensure_service_groups in the provisioner pod through the session's contained + cluster access (groups {access_groups}; members exactly {initial_members} as + recorded in the accepted case, nothing else) + auth: the attended human session on lane keycape-attended-case-change (actor human, + parent-session revoked per attempt); the in-pod call uses the provisioner's own + directory binding, and no provisioner or user-engine token is read or sent + lane: keycape-attended-case-change + warden_executes: false + attended: true + value_handling: none + attended_justification: The provisioner's only HTTP credential is user-engine's + runtime token, and its NetworkPolicy admits only user-engine pods. A proxy lane + would act as user-engine (ops-warden review R8; canon section 3; ops-warden + ADR-0002). Running it in the session register-client already needs adds no attended + session. It can become unattended once the provisioner has an operator credential + of its own and an admitted path. Warden's case kernel dispatches the reviewed adapter only inside the one attended human session (descriptor actor human, lane keycape-attended-case-change, parent-session revoked per attempt); the human session is the authority and the kernel is only the dispatcher. + - id: mint-secret + owner: railiance-platform + when: client_type == confidential + entry_point: 'railiance-platform credential broker: generate a client secret inside + custody at bao:platform/workloads/keycape/{client_id}, through reviewed adapter + railiance-platform/keycape-mint-secret' + auth: a human caller (net-kingdom-admins, aal2) starts it; Warden runs it unattended + through the broker lane under the caller's session, with the caller's broker grant + for the case's KV path; agents and root are refused, and the value is generated + in custody and never returned + lane: keycape-client-secret-generate + warden_executes: true + attended: false + value_handling: generated_in_custody + - id: deliver-secret + owner: railiance-platform + when: client_type == confidential + entry_point: reviewed adapter railiance-platform/keycape-deliver-secret, dispatched + by Warden's case kernel; Platform's deliver capability applies ExternalSecret + sso/keycape-{client_id}-client (key client-secret) through the ESO store for + role external-secrets-keycape-{client_id}, from bao:platform/workloads/keycape/{client_id} + auth: Platform's own delivery path on lane platform-secret-delivery, inside the human + aal2 session; ESO reads custody through role external-secrets-keycape-{client_id}; + Warden holds no Platform credential, and no value leaves custody + lane: platform-secret-delivery + warden_executes: false + attended: false + value_handling: none + ready_check: the delivery for sso/keycape-{client_id}-client reports Ready=True + (status only, no value read) + - id: bind-env + owner: net-kingdom + when: client_type == confidential + entry_point: 'reviewed adapter net-kingdom/keycape-bind-env, dispatched by Warden''s + case kernel inside the attended session: patch Deployment sso/keycape env + KEYCAPE_{client_id:env}_CLIENT_SECRET from sso/keycape-{client_id}-client, by + reference only' + auth: the attended human session on lane keycape-attended-case-change, with operator + kube credentials for namespace sso (actor human, parent-session revoked per attempt) + lane: keycape-attended-case-change + warden_executes: false + attended: true + value_handling: none + attended_justification: A change to the live identity provider's Deployment. It + runs in the same attended operator session as register-client, before it, because + KeyCape resolves a confidential client's secret at start and fails closed if + the binding is missing. Warden's case kernel dispatches the reviewed adapter only inside the one attended human session (descriptor actor human, lane keycape-attended-case-change, parent-session revoked per attempt); the human session is the authority and the kernel is only the dispatcher. + - id: register-client + owner: net-kingdom + entry_point: reviewed adapter net-kingdom/keycape-register-client, dispatched by Warden's + case kernel inside the attended session; it runs sso-mfa/k8s/keycape/keycape-client.py + apply --client-file --case-acceptance --check-config + (net-kingdom.keycape-client-registration) + auth: the attended human session on lane keycape-attended-case-change, with operator + write access to the live KeyCape client registry in namespace sso (actor human, + parent-session revoked per attempt) + lane: keycape-attended-case-change + warden_executes: false + attended: true + value_handling: none + attended_justification: 'The live KeyCape client registry is a Kubernetes Secret, + and agents do not read or write Secrets. Until KeyCape loads clients from a + non-secret registry with secrets referenced from custody, one attended operator + session applies the rendered registration. ops-warden groups all attended steps + of a run into that one session. KeyCape loads clients at startup and keeps login, + authorization and handoff state in memory, so the restart drops in-flight logins + and sessions for every client; runs go in a quiet window (key-cape type review + R4). The rendered registry must pass KeyCape''s own startup checks (keycape + check-config, the pinned checker artifact sha256:be37738fe1c08259fb4f9ca0daf0f9301bfffc237e5bb38435525dfbf11ccff5 + built from key-cape 3c71226, which keycape-client.py runs before it patches) + before the restart: a refused entry stops the single replica and every client + loses sign-in (R1). Warden''s case kernel dispatches the reviewed adapter only inside the one attended human session (descriptor actor human, lane keycape-attended-case-change, parent-session revoked per attempt); the human session is the authority and the kernel is only the dispatcher.' + - id: deliver-consumer-secret + owner: '{consumer_owner_repo}' + when: client_type == confidential + entry_point: Reviewed adapter net-kingdom/keycape-deliver-consumer-secret, dispatched + by Warden's case kernel inside the attended consumer-owner session; the owner's + apply capability creates dedicated ServiceAccount {consumer_namespace}/{consuming_service}-keycape-{client_id}-eso + (automount=false, no legacy tokens), namespace SecretStore openbao-keycape-{client_id}-consumer + with serviceAccountRef and audiences [openbao], and ExternalSecret {consuming_service}-netkingdom-oidc-client + with Owner target, Retain remote deletion and only CLIENT_SECRET from workloads/keycape/{client_id}. + Admission validates actual installed ESO TokenRequest support and common controller + RBAC before any owner-approved apply. + auth: the attended consumer-owner session on lane consumer-owner-deployment (actor + human, the consumer owner's operator deployment authority, on the founder's go + for the change); ESO authenticates to OpenBao role external-secrets-keycape-{client_id}-{consuming_service} + with a TokenRequest for the dedicated namespaced serviceAccountRef; controller + remains trusted + lane: consumer-owner-deployment + warden_executes: false + attended: true + value_handling: none + attended_justification: The consumer owner's only deployment authority in the consumer + namespace is its operator credential, and production applies need the founder's + go per change. No contained agent deployer identity exists, and the owner does + not want a standing credential an agent could trigger. It runs in one attended + consumer-owner session together with hand-over. Warden's case kernel dispatches + the reviewed adapter only inside that session (actor human, parent-session + revoked per attempt); the human session is the authority and the kernel is only + the dispatcher. + ready_check: Namespace SecretStore and ExternalSecret both report Ready=True; + dedicated SA has no legacy token refs; status and fixed metadata only, never + Secret values. + - id: hand-over + owner: '{consumer_owner_repo}' + entry_point: The consumer owner first makes its own Helm change configuring {consuming_service} + with issuer {issuer}, client {client_id} and custody bao:platform/workloads/keycape/{client_id}, + requiring one of {access_groups} in the groups claim before sign-in completes. + Then reviewed adapter net-kingdom/keycape-hand-over, dispatched by Warden's case + kernel inside the attended consumer-owner session, verifies the consumer with + four value-free probes (member-login, non-member-refused, membership-add-grants, + membership-remove-revokes) and records the hand-over receipt. + auth: the attended consumer-owner session on lane apps-consumer-handover (actor human, + parent-session revoked per attempt); the consumer configuration itself is the + owner's own deployment path + lane: apps-consumer-handover + warden_executes: false + attended: true + value_handling: none + attended_justification: The verification needs a real member login and a refused + non-member login, which only an attended session can perform, and it follows + the owner's own Helm change in the same consumer-owner session as + deliver-consumer-secret. Warden's case kernel dispatches the reviewed adapter + only inside that session (actor human, parent-session revoked per attempt); the + human session is the authority and the kernel is only the dispatcher. + verification: + - id: authorize-accepts-redirects + check: GET {issuer}/authorize with client {client_id} and each of {redirect_uris} + returns the login page + expect: present + reads_secret_value: false + - id: authorize-refuses-other-redirect + check: GET {issuer}/authorize with client {client_id} and an unregistered redirect + returns an OAuth error + expect: deny + reads_secret_value: false + - id: custody-denied-to-default + when: client_type == confidential + check: a default-only OpenBao identity has no read capability on bao:platform/workloads/keycape/{client_id} + expect: deny + reads_secret_value: false + - id: custody-allowed-to-consumer + when: client_type == confidential + check: policy workload-kv-read-keycape-{client_id}-{consuming_service} grants + read on bao:platform/workloads/keycape/{client_id} (capabilities check only) + expect: allow + reads_secret_value: false + - id: groups-present + check: LLDAP groups {access_groups} exist, with exactly the memberships of the + accepted case added + expect: present + reads_secret_value: false + - id: consumer-enforces-groups + check: '{consuming_service}''s OIDC source for {client_id} (owner {consumer_owner_repo}) + requires one of {access_groups} in the groups claim (configuration read, no + secret value)' + expect: present + reads_secret_value: false + - id: secret-delivered + when: client_type == confidential + check: the delivery for sso/keycape-{client_id}-client reports Ready=True (status + only) + expect: present + reads_secret_value: false + - id: mfa-pinned + when: mfa_required == true + check: keycape-client.py inspect for {client_id} shows the MFA pin on the registered + client (non-secret client view) + expect: present + reads_secret_value: false + - id: consumer-secret-delivered + when: client_type == confidential + check: '{consumer_namespace} SecretStore openbao-keycape-{client_id}-consumer + and ExternalSecret {consuming_service}-netkingdom-oidc-client Ready=True, exact + dedicated SA/audience role binding, fixed CLIENT_SECRET mapping; no Secret value + read' + expect: present + reads_secret_value: false + rollback: + - reverses: hand-over + action: the adapter removes only its hand-over receipt. Removing the OIDC source + for {client_id} from {consuming_service} is the owner's Helm rollback (owner + {consumer_owner_repo}), done inside deliver-consumer-secret's owner_remove + before any consumer object is removed + - reverses: deliver-consumer-secret + when: client_type == confidential + action: After reverting consumer secret reference, delete only authenticated created + ExternalSecret/owned target by UID and ownerReference, then SecretStore and + dedicated SA; preserve preexisting-matched resources and never delete a shared + target. Retain audit and ownership receipts; removing SecretStore/SA alone is + not immediate OpenBao token revocation. + - reverses: register-client + action: keycape-client.py apply --operation remove for {client_id} + - reverses: bind-env + when: client_type == confidential + action: remove env KEYCAPE_{client_id:env}_CLIENT_SECRET from Deployment sso/keycape, + after register-client is reversed + - reverses: deliver-secret + when: client_type == confidential + action: delete ExternalSecret sso/keycape-{client_id}-client and its target + - reverses: mint-secret + when: client_type == confidential + action: 'covered by client-secret-path: one metadata delete, after deliver-secret + is reversed' + - reverses: ensure-groups + action: identity-provisioner revoke_service_groups inside the provisioner pod + (attended, as ensure-groups) with the case's memberships; delete_if_empty only + if no other case uses {access_groups} + - reverses: client-secret-path + when: client_type == confidential + action: delete metadata and all versions at platform/workloads/keycape/{client_id}, + after the ExternalSecret is gone (this also reverses mint-secret) + - reverses: eso-role + when: client_type == confidential + action: delete Kubernetes-auth role external-secrets-keycape-{client_id} + - reverses: keycape-reader-policy + when: client_type == confidential + action: delete policy workload-kv-read-keycape-{client_id}-eso, after eso-role + - reverses: consumer-eso-role + when: client_type == confidential + action: Delete only this case-created role external-secrets-keycape-{client_id}-{consuming_service}, + before deleting consumer-reader-policy; revoke case-issued OpenBao tokens or + prove expiration before declaring deactivation. + - reverses: consumer-reader-policy + when: client_type == confidential + action: delete policy workload-kv-read-keycape-{client_id}-{consuming_service} + - reverses: catalog-entry + action: remove catalog entry {consuming_service}-keycape-client + acceptance: + accepted_by: + - owner + - founder + risk_class: medium + # Reviewed machine descriptors (NK-WP-0048-T16 successor candidate). Digests are + # generated by sso-mfa/k8s/keycape/compose-1.2-descriptors.py --version 1.3. + execution_descriptors: + bind-env: sha256:d5fcb20cf15bf7454890d2529180c582218e3df12584a99162e4b664b899caa0 + deliver-consumer-secret: sha256:36281df8139fb1ec64b226d94f80eb592de5addab7063ff54c691c641e275fc7 + deliver-secret: sha256:75aaa7346aebdea2a0eb0ba993995515b5248ffefc34caf8e713525e9b58da85 + ensure-groups: sha256:ebc9d5b9167d18e6015f4bbeb9699e8d396d64fbfaf195142781f84f4f47a70a + hand-over: sha256:1bce07a3229b7d0c7c730f35ce5b40fe471009a82de8dd53788ed970a9be1224 + mint-secret: sha256:c92957227235db04953809d92b260d32c8b947472a8e8fd17a258ade3ae8da70 + register-client: sha256:4ecc769f75433b66c7161f13c0fe7e5599633cf466d46cb6d8031f0456f426e7 diff --git a/docs/changes/netkingdom-target-2026-10-17/nk48-r5-f2-implementation-r1/type.candidate.yaml b/docs/changes/netkingdom-target-2026-10-17/nk48-r5-f2-implementation-r1/type.candidate.yaml new file mode 100644 index 00000000..74790225 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/nk48-r5-f2-implementation-r1/type.candidate.yaml @@ -0,0 +1,498 @@ +apiVersion: netkingdom.io/security-case-type/v0.1 +kind: SecurityCaseType +spec: + id: oidc-browser-relying-party + version: '1.3' + status: proposed + title: Browser OIDC relying party for an internal service + purpose: 'Let an internal web service sign its human users in through NetKingdom + (KeyCape): an exact-redirect OIDC client, optional MFA pin, the LLDAP groups the + service maps to its own roles, and, for confidential clients, a client secret + minted straight into OpenBao custody. Replaces the per-client rollout scripts + in sso-mfa/k8s/keycape. First case: Forgejo (KEY-WP-0038).' + parameters: + - name: client_id + type: string + case_settable: true + constraints: + pattern: ^[a-z0-9][a-z0-9-]{1,62}$ + sensitivity: operational + description: OAuth2 client_id, unique in KeyCape. + - name: display_name + type: string + case_settable: true + constraints: + pattern: ^[^\n\r]{1,80}$ + sensitivity: public + description: Name shown on the sign-in page. + - name: redirect_uris + type: string + case_settable: true + constraints: + pattern: ^https://[^*?#\s,]+(,https://[^*?#\s,]+)*$ + sensitivity: security_sensitive + description: 'Comma-separated exact https redirect URIs. No wildcards, no plain + http, no query and no fragment: KeyCape refuses ''*'' and ''?'' at startup, + and OAuth 2.0 forbids fragments (key-cape type review R1).' + - name: allowed_scopes + type: string + case_settable: true + constraints: + enum: + - openid,profile,email,groups + - openid,email,groups + - openid,profile,groups + - openid,groups + sensitivity: security_sensitive + description: Scopes the client may request, openid first. Always includes groups, + because access_groups is required and KeyCape's UserInfo returns groups only + for that scope (key-cape type review R2). + - name: client_type + type: string + case_settable: true + constraints: + enum: + - confidential + - public + sensitivity: security_sensitive + description: confidential for server-side services that can hold a secret; public + for SPAs and native apps (PKCE only). + - name: mfa_required + type: string + case_settable: true + constraints: + enum: + - 'true' + - 'false' + sensitivity: security_sensitive + description: '"true" pins MFA on this client; required when the service grants + administrative or owner rights. "false" is not "no MFA": the client then carries + no mfaRequired field and KeyCape applies the per-user privacyIDEA policy. A + renderer must never emit an explicit mfaRequired false, which would skip that + policy (key-cape type review R3).' + - name: access_groups + type: string + case_settable: true + constraints: + pattern: ^[a-z][a-z0-9-]{1,62}(,[a-z][a-z0-9-]{1,62})*$ + sensitivity: operational + description: 'Comma-separated LLDAP groups that the service maps to its roles. + Created if absent. KeyCape has no per-client group gate: it issues tokens to + any authenticated user and only carries the groups claim, so the consuming service + enforces the gate itself (for Forgejo, its required-claim setting; key-cape + type review R2).' + - name: initial_members + type: string + required: false + case_settable: true + constraints: + pattern: ^[a-z][a-z0-9-]*=[a-z][a-z0-9._-]*(\+[a-z][a-z0-9._-]*)*(;[a-z][a-z0-9-]*=[a-z][a-z0-9._-]*(\+[a-z][a-z0-9._-]*)*)*$ + sensitivity: operational + description: Initial memberships as group=user+user;group=user. Usernames are + directory usernames, never secrets. + - name: consuming_service + type: string + case_settable: true + constraints: + pattern: ^[a-z0-9][a-z0-9-]{0,62}$ + sensitivity: operational + description: The service that signs users in with this client. + - name: consumer_owner_repo + type: string + case_settable: true + constraints: + pattern: ^[a-z0-9][a-z0-9-]*$ + sensitivity: operational + description: The repository that owns the consuming service's configuration. + - name: consumer_namespace + type: string + case_settable: true + constraints: + pattern: ^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$ + sensitivity: security_sensitive + description: Consumer namespace for namespaced ESO TokenRequest reader; actual + workload owner must confirm; never infer from service name. + - name: issuer + type: string + case_settable: false + default: https://kc.coulomb.social + sensitivity: security_sensitive + description: The NetKingdom issuer; fixed by the type. + composition: + authority: netkingdom + initial_trust: + - runtime_secret_trust + - runtime_identity_trust + capabilities: + - c1.lightweight-sso + providers: + c1.lightweight-sso: net-kingdom.keycape-client-registration + parameter_bindings: + net-kingdom.keycape-client-registration.client_id: client_id + net-kingdom.keycape-client-registration.display_name: display_name + net-kingdom.keycape-client-registration.redirect_uris: redirect_uris + net-kingdom.keycape-client-registration.allowed_scopes: allowed_scopes + net-kingdom.keycape-client-registration.client_type: client_type + net-kingdom.keycape-client-registration.mfa_required: mfa_required + materialization: + - id: client-secret-path + kind: kv_path + when: client_type == confidential + template: platform/workloads/keycape/{client_id} + body: field CLIENT_SECRET; minted by the credential broker, never written by hand + - id: keycape-reader-policy + kind: openbao_policy + when: client_type == confidential + template: workload-kv-read-keycape-{client_id}-eso + body: "path \"platform/data/workloads/keycape/{client_id}\" {{\n capabilities\ + \ = [\"read\"]\n}}\n" + description: KeyCape reads its client secret only through ESO. No metadata, list, + parent or sibling access. + - id: consumer-reader-policy + kind: openbao_policy + when: client_type == confidential + template: workload-kv-read-keycape-{client_id}-{consuming_service} + body: "path \"platform/data/workloads/keycape/{client_id}\" {{\n capabilities\ + \ = [\"read\"]\n}}\n" + description: The consuming service reads the same secret. No metadata, list, parent + or sibling access. + - id: eso-role + kind: openbao_role + when: client_type == confidential + template: external-secrets-keycape-{client_id} + body: 'auth: kubernetes + + bound_service_account_names: [external-secrets] + + bound_service_account_namespaces: [external-secrets] + + token_policies: [workload-kv-read-keycape-{client_id}-eso] + + token_no_default_policy: true + + token_ttl: 15m + + token_max_ttl: 15m + + ' + description: The Kubernetes-auth role deliver-secret uses; bound to the ESO service + account only. + - id: consumer-eso-role + kind: openbao_role + when: client_type == confidential + template: external-secrets-keycape-{client_id}-{consuming_service} + body: 'auth: kubernetes + + bound_service_account_names: [{consuming_service}-keycape-{client_id}-eso] + + bound_service_account_namespaces: [{consumer_namespace}] + + audience: openbao + + token_policies: [workload-kv-read-keycape-{client_id}-{consuming_service}] + + token_no_default_policy: true + + token_ttl: 15m + + token_max_ttl: 15m + + ' + description: Consumer ESO requests a token for this separate reference identity. + Common ESO controller remains trusted and can request tokens; no cryptographic + controller isolation is claimed. + - id: catalog-entry + kind: catalog_entry + template: '{consuming_service}-keycape-client' + body: 'id: {consuming_service}-keycape-client + + owner_repo: key-cape + + warden_executes: false + + risk: high + + status: active once verification passes + + workload_ref: applicable, name {consuming_service}, declaration_ref of the consumer''s + published declaration or the explicit unknown form, never inferred + + path_template: platform/workloads/keycape/{client_id} + + field: CLIENT_SECRET + + delegation: native, intended_owner key-cape + + rotation: mint-secret then verification; automatable false until a run has been + proven + + ' + description: One entry per case (ops-mason item 5, ops-warden R4'). It carries + the consumer's workload reference, which ADR-0009 needs for zone selection. + follow_through: + - id: ensure-groups + owner: net-kingdom + entry_point: reviewed adapter net-kingdom/keycape-ensure-groups, dispatched by Warden's + case kernel inside the attended session; it calls identity-provisioner + ensure_service_groups in the provisioner pod through the session's contained + cluster access (groups {access_groups}; members exactly {initial_members} as + recorded in the accepted case, nothing else) + auth: the attended human session on lane keycape-attended-case-change (actor human, + parent-session revoked per attempt); the in-pod call uses the provisioner's own + directory binding, and no provisioner or user-engine token is read or sent + lane: keycape-attended-case-change + warden_executes: false + attended: true + value_handling: none + attended_justification: The provisioner's only HTTP credential is user-engine's + runtime token, and its NetworkPolicy admits only user-engine pods. A proxy lane + would act as user-engine (ops-warden review R8; canon section 3; ops-warden + ADR-0002). Running it in the session register-client already needs adds no attended + session. It can become unattended once the provisioner has an operator credential + of its own and an admitted path. Warden's case kernel dispatches the reviewed adapter only inside the one attended human session (descriptor actor human, lane keycape-attended-case-change, parent-session revoked per attempt); the human session is the authority and the kernel is only the dispatcher. + - id: mint-secret + owner: railiance-platform + when: client_type == confidential + entry_point: 'railiance-platform credential broker: generate a client secret inside + custody at bao:platform/workloads/keycape/{client_id}, through reviewed adapter + railiance-platform/keycape-mint-secret' + auth: a human caller (net-kingdom-admins, aal2) starts it; Warden runs it unattended + through the broker lane under the caller's session, with the caller's broker grant + for the case's KV path; agents and root are refused, and the value is generated + in custody and never returned + lane: keycape-client-secret-generate + warden_executes: true + attended: false + value_handling: generated_in_custody + - id: deliver-secret + owner: railiance-platform + when: client_type == confidential + entry_point: reviewed adapter railiance-platform/keycape-deliver-secret, dispatched + by Warden's case kernel; Platform's deliver capability applies ExternalSecret + sso/keycape-{client_id}-client (key client-secret) through fixed ClusterSecretStore + openbao-keycape-{client_id}-issuer (cluster-scoped, restricted to namespace sso), + defined by Platform-controlled manifests/templates/keycape-issuer-eso.yaml + (sha256:e17498bd8a4c820041a4105e4efe9c60c6a50953db8f621ff54887080065c743), + using role external-secrets-keycape-{client_id}, from bao:platform/workloads/keycape/{client_id} + auth: Platform's own delivery path on lane platform-secret-delivery, inside the human + aal2 session; ESO reads custody through role external-secrets-keycape-{client_id}; + Warden holds no Platform credential, and no value leaves custody + lane: platform-secret-delivery + warden_executes: false + attended: false + value_handling: none + ready_check: the delivery for sso/keycape-{client_id}-client reports Ready=True + (status only, no value read) + - id: bind-env + owner: net-kingdom + when: client_type == confidential + entry_point: 'reviewed adapter net-kingdom/keycape-bind-env, dispatched by Warden''s + case kernel inside the attended session: patch Deployment sso/keycape env + KEYCAPE_{client_id:env}_CLIENT_SECRET from sso/keycape-{client_id}-client, by + reference only' + auth: the attended human session on lane keycape-attended-case-change, with operator + kube credentials for namespace sso (actor human, parent-session revoked per attempt) + lane: keycape-attended-case-change + warden_executes: false + attended: true + value_handling: none + attended_justification: A change to the live identity provider's Deployment. It + runs in the same attended operator session as register-client, before it, because + KeyCape resolves a confidential client's secret at start and fails closed if + the binding is missing. Warden's case kernel dispatches the reviewed adapter only inside the one attended human session (descriptor actor human, lane keycape-attended-case-change, parent-session revoked per attempt); the human session is the authority and the kernel is only the dispatcher. + - id: register-client + owner: net-kingdom + entry_point: reviewed adapter net-kingdom/keycape-register-client, dispatched by Warden's + case kernel inside the attended session; it runs sso-mfa/k8s/keycape/keycape-client.py + apply --client-file --case-acceptance --check-config + (net-kingdom.keycape-client-registration) + auth: the attended human session on lane keycape-attended-case-change, with operator + write access to the live KeyCape client registry in namespace sso (actor human, + parent-session revoked per attempt) + lane: keycape-attended-case-change + warden_executes: false + attended: true + value_handling: none + attended_justification: 'The live KeyCape client registry is a Kubernetes Secret, + and agents do not read or write Secrets. Until KeyCape loads clients from a + non-secret registry with secrets referenced from custody, one attended operator + session applies the rendered registration. ops-warden groups all attended steps + of a run into that one session. KeyCape loads clients at startup and keeps login, + authorization and handoff state in memory, so the restart drops in-flight logins + and sessions for every client; runs go in a quiet window (key-cape type review + R4). The rendered registry must pass KeyCape''s own startup checks (keycape + check-config, the pinned checker artifact sha256:be37738fe1c08259fb4f9ca0daf0f9301bfffc237e5bb38435525dfbf11ccff5 + built from key-cape 3c71226, which keycape-client.py runs before it patches) + before the restart: a refused entry stops the single replica and every client + loses sign-in (R1). Warden''s case kernel dispatches the reviewed adapter only inside the one attended human session (descriptor actor human, lane keycape-attended-case-change, parent-session revoked per attempt); the human session is the authority and the kernel is only the dispatcher.' + - id: deliver-consumer-secret + owner: '{consumer_owner_repo}' + when: client_type == confidential + entry_point: Reviewed adapter net-kingdom/keycape-deliver-consumer-secret, dispatched + by Warden's case kernel inside the attended consumer-owner session; the owner's + apply capability creates dedicated ServiceAccount {consumer_namespace}/{consuming_service}-keycape-{client_id}-eso + (automount=false, no legacy tokens), namespace SecretStore openbao-keycape-{client_id}-consumer + with serviceAccountRef and audiences [openbao], and ExternalSecret {consuming_service}-netkingdom-oidc-client + with Owner target, Retain remote deletion and only CLIENT_SECRET from workloads/keycape/{client_id}. + Admission validates actual installed ESO TokenRequest support and common controller + RBAC before any owner-approved apply. + auth: the attended consumer-owner session on lane consumer-owner-deployment (actor + human, the consumer owner's operator deployment authority, on the founder's go + for the change); ESO authenticates to OpenBao role external-secrets-keycape-{client_id}-{consuming_service} + with a TokenRequest for the dedicated namespaced serviceAccountRef; controller + remains trusted + lane: consumer-owner-deployment + warden_executes: false + attended: true + value_handling: none + attended_justification: The consumer owner's only deployment authority in the consumer + namespace is its operator credential, and production applies need the founder's + go per change. No contained agent deployer identity exists, and the owner does + not want a standing credential an agent could trigger. It runs in one attended + consumer-owner session together with hand-over. Warden's case kernel dispatches + the reviewed adapter only inside that session (actor human, parent-session + revoked per attempt); the human session is the authority and the kernel is only + the dispatcher. + ready_check: Namespace SecretStore and ExternalSecret both report Ready=True; + dedicated SA has no legacy token refs; status and fixed metadata only, never + Secret values. + - id: hand-over + owner: '{consumer_owner_repo}' + entry_point: The consumer owner first makes its own Helm change configuring {consuming_service} + with issuer {issuer}, client {client_id} and custody bao:platform/workloads/keycape/{client_id}, + requiring one of {access_groups} in the groups claim before sign-in completes. + Then reviewed adapter net-kingdom/keycape-hand-over, dispatched by Warden's case + kernel inside the attended consumer-owner session, verifies the consumer with + four value-free probes (member-login, non-member-refused, membership-add-grants, + membership-remove-revokes) and records the hand-over receipt. + auth: the attended consumer-owner session on lane apps-consumer-handover (actor human, + parent-session revoked per attempt); the consumer configuration itself is the + owner's own deployment path + lane: apps-consumer-handover + warden_executes: false + attended: true + value_handling: none + attended_justification: The verification needs a real member login and a refused + non-member login, which only an attended session can perform, and it follows + the owner's own Helm change in the same consumer-owner session as + deliver-consumer-secret. Warden's case kernel dispatches the reviewed adapter + only inside that session (actor human, parent-session revoked per attempt); the + human session is the authority and the kernel is only the dispatcher. + verification: + - id: authorize-accepts-redirects + check: GET {issuer}/authorize with client {client_id} and each of {redirect_uris} + returns the login page + expect: present + reads_secret_value: false + - id: authorize-refuses-other-redirect + check: GET {issuer}/authorize with client {client_id} and an unregistered redirect + returns an OAuth error + expect: deny + reads_secret_value: false + - id: custody-denied-to-default + when: client_type == confidential + check: a default-only OpenBao identity has no read capability on bao:platform/workloads/keycape/{client_id} + expect: deny + reads_secret_value: false + - id: custody-allowed-to-consumer + when: client_type == confidential + check: policy workload-kv-read-keycape-{client_id}-{consuming_service} grants + read on bao:platform/workloads/keycape/{client_id} (capabilities check only) + expect: allow + reads_secret_value: false + - id: groups-present + check: LLDAP groups {access_groups} exist, with exactly the memberships of the + accepted case added + expect: present + reads_secret_value: false + - id: consumer-enforces-groups + check: '{consuming_service}''s OIDC source for {client_id} (owner {consumer_owner_repo}) + requires one of {access_groups} in the groups claim (configuration read, no + secret value)' + expect: present + reads_secret_value: false + - id: secret-delivered + when: client_type == confidential + check: the delivery for sso/keycape-{client_id}-client reports Ready=True (status + only) + expect: present + reads_secret_value: false + - id: mfa-pinned + when: mfa_required == true + check: keycape-client.py inspect for {client_id} shows the MFA pin on the registered + client (non-secret client view) + expect: present + reads_secret_value: false + - id: consumer-secret-delivered + when: client_type == confidential + check: '{consumer_namespace} SecretStore openbao-keycape-{client_id}-consumer + and ExternalSecret {consuming_service}-netkingdom-oidc-client Ready=True, exact + dedicated SA/audience role binding, fixed CLIENT_SECRET mapping; no Secret value + read' + expect: present + reads_secret_value: false + rollback: + - reverses: hand-over + action: the adapter removes only its hand-over receipt. Removing the OIDC source + for {client_id} from {consuming_service} is the owner's Helm rollback (owner + {consumer_owner_repo}), done inside deliver-consumer-secret's owner_remove + before any consumer object is removed + - reverses: deliver-consumer-secret + when: client_type == confidential + action: After reverting consumer secret reference, delete only authenticated created + ExternalSecret/owned target by UID and ownerReference, then SecretStore and + dedicated SA; preserve preexisting-matched resources and never delete a shared + target. Retain audit and ownership receipts; removing SecretStore/SA alone is + not immediate OpenBao token revocation. + - reverses: register-client + action: keycape-client.py apply --operation remove for {client_id} + - reverses: bind-env + when: client_type == confidential + action: remove env KEYCAPE_{client_id:env}_CLIENT_SECRET from Deployment sso/keycape, + after register-client is reversed + - reverses: deliver-secret + when: client_type == confidential + action: delete ExternalSecret sso/keycape-{client_id}-client and its target + - reverses: mint-secret + when: client_type == confidential + action: 'covered by client-secret-path: one metadata delete, after deliver-secret + is reversed' + - reverses: ensure-groups + action: identity-provisioner revoke_service_groups inside the provisioner pod + (attended, as ensure-groups) with the case's memberships; delete_if_empty only + if no other case uses {access_groups} + - reverses: client-secret-path + when: client_type == confidential + action: delete metadata and all versions at platform/workloads/keycape/{client_id}, + after the ExternalSecret is gone (this also reverses mint-secret) + - reverses: eso-role + when: client_type == confidential + action: delete Kubernetes-auth role external-secrets-keycape-{client_id} + - reverses: keycape-reader-policy + when: client_type == confidential + action: delete policy workload-kv-read-keycape-{client_id}-eso, after eso-role + - reverses: consumer-eso-role + when: client_type == confidential + action: Delete only this case-created role external-secrets-keycape-{client_id}-{consuming_service}, + before deleting consumer-reader-policy; revoke case-issued OpenBao tokens or + prove expiration before declaring deactivation. + - reverses: consumer-reader-policy + when: client_type == confidential + action: delete policy workload-kv-read-keycape-{client_id}-{consuming_service} + - reverses: catalog-entry + action: remove catalog entry {consuming_service}-keycape-client + acceptance: + accepted_by: + - owner + - founder + risk_class: medium + # Reviewed machine descriptors (NK-WP-0048-T16 successor candidate). Digests are + # generated by sso-mfa/k8s/keycape/compose-1.2-descriptors.py --version 1.3. + execution_descriptors: + bind-env: sha256:d5fcb20cf15bf7454890d2529180c582218e3df12584a99162e4b664b899caa0 + deliver-consumer-secret: sha256:36281df8139fb1ec64b226d94f80eb592de5addab7063ff54c691c641e275fc7 + deliver-secret: sha256:75aaa7346aebdea2a0eb0ba993995515b5248ffefc34caf8e713525e9b58da85 + ensure-groups: sha256:ebc9d5b9167d18e6015f4bbeb9699e8d396d64fbfaf195142781f84f4f47a70a + hand-over: sha256:1bce07a3229b7d0c7c730f35ce5b40fe471009a82de8dd53788ed970a9be1224 + mint-secret: sha256:c92957227235db04953809d92b260d32c8b947472a8e8fd17a258ade3ae8da70 + register-client: sha256:4ecc769f75433b66c7161f13c0fe7e5599633cf466d46cb6d8031f0456f426e7 diff --git a/docs/changes/netkingdom-target-2026-10-17/nk48-r5-f2-publication-r1/README.md b/docs/changes/netkingdom-target-2026-10-17/nk48-r5-f2-publication-r1/README.md new file mode 100644 index 00000000..96672385 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/nk48-r5-f2-publication-r1/README.md @@ -0,0 +1,43 @@ +# F2 proposed source publication packet + +Prepared local integration only; not executed against either canonical checkout. +`manifest.json` lists exact source hashes and absent preimages. Platform HEAD was +`0a9617a62d4696a8070798923b459e8405c3dbf2`; staged `.custodian-brief.md` and +unstaged `WORK-RECORDS.md` already existed. Preserve them. + +Check, then apply the four reviewed files: + +```bash +python3 /home/worsch/the-custodian/docs/changes/netkingdom-target-2026-10-17/nk48-r5-f2-publication-r1/integrate_platform.py +python3 /home/worsch/the-custodian/docs/changes/netkingdom-target-2026-10-17/nk48-r5-f2-publication-r1/integrate_platform.py --apply +``` + +The script checks the exact HEAD, absent tracked/worktree preimages, hashes and +symlink safety; it writes only the four manifest paths. It never stages, commits, +syncs, applies native resources or edits NetKingdom. The source remains explicitly +proposed/source-only, with no inferred owner assent. It verifies that unrelated +Git status and index bytes are unchanged. A concurrent failure may leave a +partial set of newly created files; inspect it rather than reset unrelated dirt. + +Run the focused test in Platform, then stage only these paths. To keep the +preexisting staged brief out of the source commit, use a path-scoped commit: + +```bash +git add -- docs/keycape-issuer-eso-contract.md manifests/templates/keycape-issuer-eso.yaml scripts/keycape_issuer_eso.py tests/test_keycape_issuer_eso.py +git commit --only -m "Prepare proposed fixed KeyCape issuer ESO source contract" -- docs/keycape-issuer-eso-contract.md manifests/templates/keycape-issuer-eso.yaml scripts/keycape_issuer_eso.py tests/test_keycape_issuer_eso.py +``` + +Root handles the already-authorized normal repository sync and its exact-commit +receipt. No task is marked done. `progress-paragraph.md` explains the completed +source-fit work and remaining real implementation. No legitimate Platform +workplan task was found: `RPF-1.3-T4` lives in the original owner-review JSON only. +Do not append the paragraph to an unrelated finished workplan or create a new one. + +`netkingdom-entry-point.conditional.patch` is the unchanged exact proposed-1.3 +entry text from the reviewed packet. Keep it unapplied until F3 is combined and +the existing combined artifact regenerated. It edits only the issuer delivery +entry point, pins the exact template digest and grants no adoption or activation. + +`integration-checks.json` records the isolated scratch application, preservation +of synthetic staged/unstaged dirt, rejection of non-absent preimages, and eight +passing focused tests. Tests used a minimal environment and local Git only. diff --git a/docs/changes/netkingdom-target-2026-10-17/nk48-r5-f2-publication-r1/canonical-validation.json b/docs/changes/netkingdom-target-2026-10-17/nk48-r5-f2-publication-r1/canonical-validation.json new file mode 100644 index 00000000..f5399810 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/nk48-r5-f2-publication-r1/canonical-validation.json @@ -0,0 +1,5 @@ +{ + "first_attempt": "missing /home/worsch/railiance-platform/.venv/bin/python; exit127; tests did not run", + "rerun": "env PYTHONDONTWRITEBYTECODE=1 HOME=/tmp PATH=/usr/bin:/bin python3 -B -m unittest discover -s tests -p test_keycape_issuer_eso.py -q", + "result": "8 tests passed; fixture-only metadata, no native/API calls" +} diff --git a/docs/changes/netkingdom-target-2026-10-17/nk48-r5-f2-publication-r1/consistency.json b/docs/changes/netkingdom-target-2026-10-17/nk48-r5-f2-publication-r1/consistency.json new file mode 100644 index 00000000..08e3dfd1 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/nk48-r5-f2-publication-r1/consistency.json @@ -0,0 +1,604 @@ +{ + "repo_slug": "railiance-platform", + "repo_path": "/home/worsch/railiance-platform", + "source_revision": "104ed90f42a9a86bf9fda343b49b65a85c9ee4b9", + "source_revision_reason": null, + "issues": [ + { + "severity": "WARN", + "check_id": "C-35", + "message": "Repo Manager missing: README.md: README.md is required for flavor tooling", + "file_path": "README.md", + "db_id": "", + "file_value": "", + "db_value": "", + "fixable": false + }, + { + "severity": "WARN", + "check_id": "C-40", + "message": "blocker satisfied, task still wait: 'RPF-WP-0015-T03' in workplan 'RPF-WP-0015' \u2014 decision 2fd5849c-240e-4d0a-bf07-5ebf8dfce3fd is resolved", + "file_path": "workplans/RPF-WP-0015-audit-core-custody-and-recovery-coordination.md#RPF-WP-0015-T03", + "db_id": "", + "file_value": "2fd5849c-240e-4d0a-bf07-5ebf8dfce3fd", + "db_value": "", + "fixable": false + }, + { + "severity": "WARN", + "check_id": "C-40", + "message": "blocker satisfied, task still wait: 'RPF-WP-0035-T02' in workplan 'RPF-WP-0035' \u2014 decision 62aa95e4-3ccc-4867-8806-5e3db005b483 is resolved", + "file_path": "workplans/RPF-WP-0035-credential-lane-implementation.md#RPF-WP-0035-T02", + "db_id": "", + "file_value": "62aa95e4-3ccc-4867-8806-5e3db005b483", + "db_value": "", + "fixable": false + }, + { + "severity": "WARN", + "check_id": "C-40", + "message": "blocker satisfied, task still wait: 'RPF-WP-0035-T03' in workplan 'RPF-WP-0035' \u2014 decision 62aa95e4-3ccc-4867-8806-5e3db005b483 is resolved", + "file_path": "workplans/RPF-WP-0035-credential-lane-implementation.md#RPF-WP-0035-T03", + "db_id": "", + "file_value": "62aa95e4-3ccc-4867-8806-5e3db005b483", + "db_value": "", + "fixable": false + }, + { + "severity": "WARN", + "check_id": "C-40", + "message": "blocker satisfied, task still wait: 'RPF-WP-0035-T07' in workplan 'RPF-WP-0035' \u2014 decision 1591daa1-6e85-4dfd-aadd-5a9fe1ef47fa is resolved", + "file_path": "workplans/RPF-WP-0035-credential-lane-implementation.md#RPF-WP-0035-T07", + "db_id": "", + "file_value": "1591daa1-6e85-4dfd-aadd-5a9fe1ef47fa", + "db_value": "", + "fixable": false + }, + { + "severity": "WARN", + "check_id": "C-40", + "message": "blocker satisfied, task still wait: 'RPF-WP-0035-T08' in workplan 'RPF-WP-0035' \u2014 decision ab85ae8a-12a0-4945-ae64-1d3d86d5b26f is resolved", + "file_path": "workplans/RPF-WP-0035-credential-lane-implementation.md#RPF-WP-0035-T08", + "db_id": "", + "file_value": "ab85ae8a-12a0-4945-ae64-1d3d86d5b26f", + "db_value": "", + "fixable": false + }, + { + "severity": "WARN", + "check_id": "C-40", + "message": "blocker satisfied, task still wait: 'RPF-WP-0036-T03' in workplan 'RPF-WP-0036' \u2014 decision 89f1058f-33bc-47a8-8a49-0b44d426c67e is resolved", + "file_path": "workplans/RPF-WP-0036-platform-service-assurance.md#RPF-WP-0036-T03", + "db_id": "", + "file_value": "89f1058f-33bc-47a8-8a49-0b44d426c67e", + "db_value": "", + "fixable": false + }, + { + "severity": "WARN", + "check_id": "C-39", + "message": "Wait task 'RPF-WP-0038-T04' in workplan 'RPF-WP-0038' has neither depends_on, needs_human: true, blocked_on: message-from:\u2026 nor wait_until \u2014 unqualified wait", + "file_path": "workplans/RPF-WP-0038-forgejo-scaleway-primary-coverage.md#RPF-WP-0038-T04", + "db_id": "", + "file_value": "wait", + "db_value": "", + "fixable": false + }, + { + "severity": "WARN", + "check_id": "C-40", + "message": "blocker satisfied, task still wait: 'RPF-WP-0043-T03' in workplan 'RPF-WP-0043' \u2014 decision 89f1058f-33bc-47a8-8a49-0b44d426c67e is resolved", + "file_path": "workplans/RPF-WP-0043-policy-nexus-argocd-onboarding.md#RPF-WP-0043-T03", + "db_id": "", + "file_value": "89f1058f-33bc-47a8-8a49-0b44d426c67e", + "db_value": "", + "fixable": false + }, + { + "severity": "WARN", + "check_id": "C-40", + "message": "blocker satisfied, task still wait: 'RPF-WP-0043-T04' in workplan 'RPF-WP-0043' \u2014 decision 89f1058f-33bc-47a8-8a49-0b44d426c67e is resolved", + "file_path": "workplans/RPF-WP-0043-policy-nexus-argocd-onboarding.md#RPF-WP-0043-T04", + "db_id": "", + "file_value": "89f1058f-33bc-47a8-8a49-0b44d426c67e", + "db_value": "", + "fixable": false + }, + { + "severity": "WARN", + "check_id": "C-40", + "message": "blocker satisfied, task still wait: 'RPF-WP-0044-T04' in workplan 'RPF-WP-0044' \u2014 decision 89f1058f-33bc-47a8-8a49-0b44d426c67e is resolved", + "file_path": "workplans/RPF-WP-0044-argocd-phase-b-adopt-existing-applications.md#RPF-WP-0044-T04", + "db_id": "", + "file_value": "89f1058f-33bc-47a8-8a49-0b44d426c67e", + "db_value": "", + "fixable": false + }, + { + "severity": "WARN", + "check_id": "C-40", + "message": "blocker satisfied, task still wait: 'RPF-WP-0044-T06' in workplan 'RPF-WP-0044' \u2014 decision 89f1058f-33bc-47a8-8a49-0b44d426c67e is resolved", + "file_path": "workplans/RPF-WP-0044-argocd-phase-b-adopt-existing-applications.md#RPF-WP-0044-T06", + "db_id": "", + "file_value": "89f1058f-33bc-47a8-8a49-0b44d426c67e", + "db_value": "", + "fixable": false + }, + { + "severity": "WARN", + "check_id": "C-40", + "message": "blocker satisfied, task still wait: 'RPF-WP-0044-T07' in workplan 'RPF-WP-0044' \u2014 decision 89f1058f-33bc-47a8-8a49-0b44d426c67e is resolved", + "file_path": "workplans/RPF-WP-0044-argocd-phase-b-adopt-existing-applications.md#RPF-WP-0044-T07", + "db_id": "", + "file_value": "89f1058f-33bc-47a8-8a49-0b44d426c67e", + "db_value": "", + "fixable": false + }, + { + "severity": "WARN", + "check_id": "C-40", + "message": "blocker satisfied, task still wait: 'RPF-WP-0048-T02' in workplan 'RPF-WP-0048' \u2014 decision 89f1058f-33bc-47a8-8a49-0b44d426c67e is resolved", + "file_path": "workplans/RPF-WP-0048-activity-core-gitops-adoption.md#RPF-WP-0048-T02", + "db_id": "", + "file_value": "89f1058f-33bc-47a8-8a49-0b44d426c67e", + "db_value": "", + "fixable": false + }, + { + "severity": "WARN", + "check_id": "C-23", + "message": "Lifecycle drift in 'rpf-wp-0053': workplan status 'proposed' has an active task \u2014 report only (ADR-007: do not auto-promote proposed)", + "file_path": "workplans/RPF-WP-0053-coulomb-paired-backup-admission.md", + "db_id": "cb55e474-c7a5-5c6f-bd72-ce65f853e3e3", + "file_value": "proposed", + "db_value": "proposed", + "fixable": false + }, + { + "severity": "WARN", + "check_id": "C-39", + "message": "Wait task 'RPF-WP-0058-T05' in workplan 'RPF-WP-0058' has neither depends_on, needs_human: true, blocked_on: message-from:\u2026 nor wait_until \u2014 unqualified wait", + "file_path": "workplans/RPF-WP-0058-reuse-private-source-reader.md#RPF-WP-0058-T05", + "db_id": "", + "file_value": "wait", + "db_value": "", + "fixable": false + }, + { + "severity": "WARN", + "check_id": "C-20", + "message": "Workplan dependency target 'RAILIANCE-WP-0004' is not linked to State Hub", + "file_path": "workplans/archived/260702-RPF-WP-0023-workload-kv-access-lanes.md", + "db_id": "", + "file_value": "RAILIANCE-WP-0004", + "db_value": "", + "fixable": false + }, + { + "severity": "WARN", + "check_id": "C-20", + "message": "Workplan dependency target 'RAILIANCE-WP-0005' is not linked to State Hub", + "file_path": "workplans/archived/260702-RPF-WP-0024-credential-change-approval-workflow.md", + "db_id": "", + "file_value": "RAILIANCE-WP-0005", + "db_value": "", + "fixable": false + }, + { + "severity": "WARN", + "check_id": "C-20", + "message": "Workplan dependency target 'RAILIANCE-WP-0006' is not linked to State Hub", + "file_path": "workplans/archived/260702-RPF-WP-0024-credential-change-approval-workflow.md", + "db_id": "", + "file_value": "RAILIANCE-WP-0006", + "db_value": "", + "fixable": false + }, + { + "severity": "WARN", + "check_id": "C-20", + "message": "Workplan dependency target 'RAILIANCE-WP-0005' is not linked to State Hub", + "file_path": "workplans/archived/260905-RPF-WP-0002-openbao-approved-automation-delegation.md", + "db_id": "", + "file_value": "RAILIANCE-WP-0005", + "db_value": "", + "fixable": false + }, + { + "severity": "WARN", + "check_id": "C-20", + "message": "Workplan dependency target 'RAILIANCE-WP-0006' is not linked to State Hub", + "file_path": "workplans/archived/260905-RPF-WP-0002-openbao-approved-automation-delegation.md", + "db_id": "", + "file_value": "RAILIANCE-WP-0006", + "db_value": "", + "fixable": false + }, + { + "severity": "WARN", + "check_id": "C-20", + "message": "Workplan dependency target 'RAILIANCE-WP-0007' is not linked to State Hub", + "file_path": "workplans/archived/260905-RPF-WP-0002-openbao-approved-automation-delegation.md", + "db_id": "", + "file_value": "RAILIANCE-WP-0007", + "db_value": "", + "fixable": false + }, + { + "severity": "WARN", + "check_id": "C-20", + "message": "Workplan dependency target 'RAILIANCE-WP-0004' is not linked to State Hub", + "file_path": "workplans/archived/260905-RPF-WP-0003-issue-core-runtime-ingestion-key-lane.md", + "db_id": "", + "file_value": "RAILIANCE-WP-0004", + "db_value": "", + "fixable": false + }, + { + "severity": "WARN", + "check_id": "C-20", + "message": "Workplan dependency target 'RAILIANCE-WP-0007' is not linked to State Hub", + "file_path": "workplans/archived/260905-RPF-WP-0003-issue-core-runtime-ingestion-key-lane.md", + "db_id": "", + "file_value": "RAILIANCE-WP-0007", + "db_value": "", + "fixable": false + }, + { + "severity": "WARN", + "check_id": "C-20", + "message": "Workplan dependency target 'RAILIANCE-WP-0008' is not linked to State Hub", + "file_path": "workplans/archived/260905-RPF-WP-0003-issue-core-runtime-ingestion-key-lane.md", + "db_id": "", + "file_value": "RAILIANCE-WP-0008", + "db_value": "", + "fixable": false + }, + { + "severity": "WARN", + "check_id": "C-20", + "message": "Workplan dependency target 'RAILIANCE-WP-0004' is not linked to State Hub", + "file_path": "workplans/archived/260905-RPF-WP-0004-llm-connect-openrouter-provider-key-lane.md", + "db_id": "", + "file_value": "RAILIANCE-WP-0004", + "db_value": "", + "fixable": false + }, + { + "severity": "WARN", + "check_id": "C-20", + "message": "Workplan dependency target 'RAILIANCE-WP-0007' is not linked to State Hub", + "file_path": "workplans/archived/260905-RPF-WP-0004-llm-connect-openrouter-provider-key-lane.md", + "db_id": "", + "file_value": "RAILIANCE-WP-0007", + "db_value": "", + "fixable": false + }, + { + "severity": "WARN", + "check_id": "C-20", + "message": "Workplan dependency target 'RAILIANCE-WP-0008' is not linked to State Hub", + "file_path": "workplans/archived/260905-RPF-WP-0004-llm-connect-openrouter-provider-key-lane.md", + "db_id": "", + "file_value": "RAILIANCE-WP-0008", + "db_value": "", + "fixable": false + }, + { + "severity": "WARN", + "check_id": "C-20", + "message": "Workplan dependency target 'RAILIANCE-WP-0007' is not linked to State Hub", + "file_path": "workplans/archived/260905-RPF-WP-0005-reuse-surface-runtime-secrets-openbao-lane.md", + "db_id": "", + "file_value": "RAILIANCE-WP-0007", + "db_value": "", + "fixable": false + }, + { + "severity": "WARN", + "check_id": "C-20", + "message": "Workplan dependency target 'RAILIANCE-WP-0008' is not linked to State Hub", + "file_path": "workplans/archived/260905-RPF-WP-0005-reuse-surface-runtime-secrets-openbao-lane.md", + "db_id": "", + "file_value": "RAILIANCE-WP-0008", + "db_value": "", + "fixable": false + }, + { + "severity": "INFO", + "check_id": "C-08", + "message": "Retired DB workstream 'railiance-wp-0003@retired-20260826' (id=665b3b9b\u2026, status=finished) has no backing workplan file \u2014 projection tombstone, not live work", + "file_path": "", + "db_id": "665b3b9b-608a-4be4-84b6-dcb8261ff57b", + "file_value": "", + "db_value": "", + "fixable": false + }, + { + "severity": "INFO", + "check_id": "C-08", + "message": "Retired DB workstream 'railiance-platform-railiance-wp-0004@retired-20260826' (id=e57e487b\u2026, status=finished) has no backing workplan file \u2014 projection tombstone, not live work", + "file_path": "", + "db_id": "e57e487b-8557-439d-8093-0457c73ede93", + "file_value": "", + "db_value": "", + "fixable": false + }, + { + "severity": "INFO", + "check_id": "C-08", + "message": "Retired DB workstream 'railiance-platform-railiance-wp-0005@retired-20260826' (id=2731fece\u2026, status=finished) has no backing workplan file \u2014 projection tombstone, not live work", + "file_path": "", + "db_id": "2731fece-6c49-45b8-ab8a-4ea6c04ac603", + "file_value": "", + "db_value": "", + "fixable": false + }, + { + "severity": "INFO", + "check_id": "C-08", + "message": "Retired DB workstream 'railiance-platform-railiance-wp-0006@retired-20260826' (id=96c8a93d\u2026, status=finished) has no backing workplan file \u2014 projection tombstone, not live work", + "file_path": "", + "db_id": "96c8a93d-7a5a-4fa9-8f7b-865119551da3", + "file_value": "", + "db_value": "", + "fixable": false + }, + { + "severity": "INFO", + "check_id": "C-08", + "message": "Retired DB workstream 'railiance-platform-railiance-wp-0007@retired-20260826' (id=4d7ce243\u2026, status=finished) has no backing workplan file \u2014 projection tombstone, not live work", + "file_path": "", + "db_id": "4d7ce243-f40a-4249-a46a-a24f75d6fe4c", + "file_value": "", + "db_value": "", + "fixable": false + }, + { + "severity": "INFO", + "check_id": "C-08", + "message": "Retired DB workstream 'railiance-platform-railiance-wp-0008@retired-20260826' (id=671898ef\u2026, status=finished) has no backing workplan file \u2014 projection tombstone, not live work", + "file_path": "", + "db_id": "671898ef-2378-4814-b8f6-066148cdad46", + "file_value": "", + "db_value": "", + "fixable": false + }, + { + "severity": "INFO", + "check_id": "C-08", + "message": "Retired DB workstream 'railiance-platform-railiance-wp-0009@retired-20260826' (id=b059c81d\u2026, status=finished) has no backing workplan file \u2014 projection tombstone, not live work", + "file_path": "", + "db_id": "b059c81d-96f1-451f-896f-a05cd73744a1", + "file_value": "", + "db_value": "", + "fixable": false + }, + { + "severity": "INFO", + "check_id": "C-08", + "message": "Retired DB workstream 'railiance-platform-railiance-wp-0010@retired-20260826' (id=f364d405\u2026, status=finished) has no backing workplan file \u2014 projection tombstone, not live work", + "file_path": "", + "db_id": "f364d405-a85d-4b89-b600-1964ab436cad", + "file_value": "", + "db_value": "", + "fixable": false + }, + { + "severity": "INFO", + "check_id": "C-08", + "message": "Retired DB workstream 'railiance-wp-0029@retired-20260826' (id=038bc3c0\u2026, status=blocked) has no backing workplan file \u2014 projection tombstone, not live work", + "file_path": "", + "db_id": "038bc3c0-4492-5b91-95eb-ae515ca205df", + "file_value": "", + "db_value": "", + "fixable": false + }, + { + "severity": "INFO", + "check_id": "C-08", + "message": "Retired DB workstream 'railiance-wp-0024@retired-20260826' (id=88c4ef7f\u2026, status=blocked) has no backing workplan file \u2014 projection tombstone, not live work", + "file_path": "", + "db_id": "88c4ef7f-0af8-580e-90dc-a2bae2675a4d", + "file_value": "", + "db_value": "", + "fixable": false + }, + { + "severity": "INFO", + "check_id": "C-08", + "message": "Retired DB workstream 'railiance-platform-railiance-wp-0011@retired-20260826' (id=3f9fd8de\u2026, status=finished) has no backing workplan file \u2014 projection tombstone, not live work", + "file_path": "", + "db_id": "3f9fd8de-7235-4486-aaa7-634dba2bb6fa", + "file_value": "", + "db_value": "", + "fixable": false + }, + { + "severity": "INFO", + "check_id": "C-08", + "message": "Retired DB workstream 'railiance-wp-0027@retired-20260826' (id=6f8a6cbc\u2026, status=blocked) has no backing workplan file \u2014 projection tombstone, not live work", + "file_path": "", + "db_id": "6f8a6cbc-c076-5f0a-ade2-281a7ec71360", + "file_value": "", + "db_value": "", + "fixable": false + }, + { + "severity": "INFO", + "check_id": "C-08", + "message": "Retired DB workstream 'railiance-wp-0017@retired-20260826' (id=16e8f234\u2026, status=finished) has no backing workplan file \u2014 projection tombstone, not live work", + "file_path": "", + "db_id": "16e8f234-9305-5ff3-af82-4639215a0c54", + "file_value": "", + "db_value": "", + "fixable": false + }, + { + "severity": "INFO", + "check_id": "C-08", + "message": "Retired DB workstream 'railiance-wp-0028@retired-20260826' (id=022dda1f\u2026, status=finished) has no backing workplan file \u2014 projection tombstone, not live work", + "file_path": "", + "db_id": "022dda1f-8772-57ad-862e-672924455f11", + "file_value": "", + "db_value": "", + "fixable": false + }, + { + "severity": "INFO", + "check_id": "C-08", + "message": "Retired DB workstream 'railiance-wp-0015@retired-20260826' (id=1c73af72\u2026, status=finished) has no backing workplan file \u2014 projection tombstone, not live work", + "file_path": "", + "db_id": "1c73af72-fd4b-5ab6-8ff7-a3c302bf55a5", + "file_value": "", + "db_value": "", + "fixable": false + }, + { + "severity": "INFO", + "check_id": "C-08", + "message": "Retired DB workstream 'railiance-wp-0022@retired-20260826' (id=a7977cbf\u2026, status=finished) has no backing workplan file \u2014 projection tombstone, not live work", + "file_path": "", + "db_id": "a7977cbf-ab43-56b7-b7a9-ceb52382b341", + "file_value": "", + "db_value": "", + "fixable": false + }, + { + "severity": "INFO", + "check_id": "C-08", + "message": "Retired DB workstream 'railiance-wp-0025@retired-20260826' (id=2422d4e7\u2026, status=finished) has no backing workplan file \u2014 projection tombstone, not live work", + "file_path": "", + "db_id": "2422d4e7-2125-591e-8e95-80ecc6e09da0", + "file_value": "", + "db_value": "", + "fixable": false + }, + { + "severity": "INFO", + "check_id": "C-08", + "message": "Retired DB workstream 'railiance-wp-0016@retired-20260826' (id=857ca302\u2026, status=finished) has no backing workplan file \u2014 projection tombstone, not live work", + "file_path": "", + "db_id": "857ca302-bb94-5d41-a649-1aa966368df4", + "file_value": "", + "db_value": "", + "fixable": false + }, + { + "severity": "INFO", + "check_id": "C-08", + "message": "Retired DB workstream 'railiance-wp-0026@retired-20260826' (id=73878f8d\u2026, status=finished) has no backing workplan file \u2014 projection tombstone, not live work", + "file_path": "", + "db_id": "73878f8d-d8bb-5845-8a5c-91cdd83c1066", + "file_value": "", + "db_value": "", + "fixable": false + }, + { + "severity": "INFO", + "check_id": "C-08", + "message": "Retired DB workstream 'railiance-wp-0023@retired-20260826' (id=ec52fbe6\u2026, status=finished) has no backing workplan file \u2014 projection tombstone, not live work", + "file_path": "", + "db_id": "ec52fbe6-571b-570e-884b-c7a32ff7bcc8", + "file_value": "", + "db_value": "", + "fixable": false + }, + { + "severity": "WARN", + "check_id": "C-25", + "message": "Owner-return task 'RPF-WP-0035-T08' waiting on audit-core has 9 reply/replies after its last dated note (or unread when undated) \u2014 owner action needed: 570ca050-0316-4e38-9c16-be41057c2330, 2de1c6e7-9602-48e5-868d-0048fbbef150, e7c36b17-d8f7-4d86-af09-02111289d1f5, 5fe769a7-d35b-4e37-a671-673d13cf8948, b3c564a6-93f9-4c07-872b-7feb22cccb73, ec4995b5-e1b2-4c66-9f8d-5ac8d6ceb149, 7c4019f2-348d-475e-9c96-e1ff6d0d27f5, ff43fd2c-7633-4b34-9ae6-2483ba03a271, 0d0da0d2-36a1-4ffc-b7bf-7896fad691f6", + "file_path": "workplans/RPF-WP-0035-credential-lane-implementation.md#RPF-WP-0035-T08", + "db_id": "", + "file_value": "message-from:audit-core", + "db_value": "", + "fixable": false, + "repo_slug": "railiance-platform", + "source_revision": "104ed90f42a9a86bf9fda343b49b65a85c9ee4b9", + "source_revision_reason": null, + "owner_return": { + "task_record_id": "RPF-WP-0035-T08", + "message_ids": [ + "570ca050-0316-4e38-9c16-be41057c2330", + "2de1c6e7-9602-48e5-868d-0048fbbef150", + "e7c36b17-d8f7-4d86-af09-02111289d1f5", + "5fe769a7-d35b-4e37-a671-673d13cf8948", + "b3c564a6-93f9-4c07-872b-7feb22cccb73", + "ec4995b5-e1b2-4c66-9f8d-5ac8d6ceb149", + "7c4019f2-348d-475e-9c96-e1ff6d0d27f5", + "ff43fd2c-7633-4b34-9ae6-2483ba03a271", + "0d0da0d2-36a1-4ffc-b7bf-7896fad691f6" + ] + } + }, + { + "severity": "WARN", + "check_id": "C-25", + "message": "Owner-return task 'RPF-WP-0056-T03' waiting on railiance-cluster has 9 reply/replies after its last dated note (or unread when undated) \u2014 owner action needed: 4e9d4e58-71b3-497b-8954-cedc30740529, 13acb774-8fbe-4ca3-b44d-89a4742138bd, c9435491-8545-4d48-a57a-1773f2789092, fb8f25df-57ce-40e5-ae67-0c0b392746e8, 4bb0d7af-b644-44f3-acc8-69c7321e9824, 5301d4fb-0631-4ac5-8471-531e5362b2ba, 2087b9ac-c980-4881-89f1-6b1088132f5b, 78b89fb2-ced6-45f1-9718-0670b8257d45, 110e8dea-ce81-40ff-8161-325ddb89cd67", + "file_path": "workplans/RPF-WP-0056-playwright-consumer-admission.md#RPF-WP-0056-T03", + "db_id": "", + "file_value": "message-from:railiance-cluster", + "db_value": "", + "fixable": false, + "repo_slug": "railiance-platform", + "source_revision": "104ed90f42a9a86bf9fda343b49b65a85c9ee4b9", + "source_revision_reason": null, + "owner_return": { + "task_record_id": "RPF-WP-0056-T03", + "message_ids": [ + "4e9d4e58-71b3-497b-8954-cedc30740529", + "13acb774-8fbe-4ca3-b44d-89a4742138bd", + "c9435491-8545-4d48-a57a-1773f2789092", + "fb8f25df-57ce-40e5-ae67-0c0b392746e8", + "4bb0d7af-b644-44f3-acc8-69c7321e9824", + "5301d4fb-0631-4ac5-8471-531e5362b2ba", + "2087b9ac-c980-4881-89f1-6b1088132f5b", + "78b89fb2-ced6-45f1-9718-0670b8257d45", + "110e8dea-ce81-40ff-8161-325ddb89cd67" + ] + } + }, + { + "severity": "WARN", + "check_id": "C-28", + "message": "14 unread message(s) lack thread_id for supersession tracking: the-custodian:836a8502, the-custodian:72c6a148, audit-core:0d0da0d2, rapp-postgres:6d047a08, railiance-platform:c4a80a60", + "file_path": "", + "db_id": "", + "file_value": "", + "db_value": "", + "fixable": false + }, + { + "severity": "WARN", + "check_id": "C-26", + "message": "Workplan id 'RAIL-PL-WP-0002' uses prefix 'RAIL-PL-WP' but repo canonical prefix is 'RPF-WP' \u2014 new plans must conform", + "file_path": "workplans/archived/260702-RAIL-PL-WP-0002-openbao-platform-secrets-service.md", + "db_id": "", + "file_value": "RAIL-PL-WP-0002", + "db_value": "RPF-WP", + "fixable": false + }, + { + "severity": "WARN", + "check_id": "C-26", + "message": "Workplan id 'RAIL-PL-WP-0001' uses prefix 'RAIL-PL-WP' but repo canonical prefix is 'RPF-WP' \u2014 new plans must conform", + "file_path": "workplans/archived/RAIL-PL-WP-0001-platform-baseline.md", + "db_id": "", + "file_value": "RAIL-PL-WP-0001", + "db_value": "RPF-WP", + "fixable": false + } + ], + "fixes_applied": [ + "bindings: synced 64/64", + "C-17 cleared: pushed 1 backlogged commit(s)", + "push: pushed" + ], + "summary": { + "fail": 0, + "automation_error": 0, + "warn": 35, + "info": 20 + }, + "result": "warn" +}