From 12c9b2df438f1af6119070e6c963c82fdf8c92af Mon Sep 17 00:00:00 2001 From: tegwick Date: Sat, 31 Oct 2026 19:42:40 +0100 Subject: [PATCH] NK-WP-0056-T10: set cas_required by metadata write (platform-admin has no patch) First attended setup stopped at cas_required: platform-admin holds create/read/update/delete/list on platform/*, not patch, so 'kv metadata patch' was denied. Policies were already applied; nothing after. Now a metadata write naming only cas_required (KV v2 keeps unnamed fields; checked on the pinned OpenBao image), guarded by a before/after comparison of max_versions, delete_version_after and custom_metadata. Receipt of the stopped attempt kept as evidence. Co-Authored-By: Claude Opus 5.5 Assistant: claude-code Assistant-Model: opus Assistant-Process: 5502@bnt-lap001 Assistant-Session: 68b3b02f-5d0f-4e71-8421-796f62e50196 --- .../00-setup-attempt-patch-denied.json | 8 +++++++ tests/test_executor_lanes.py | 22 +++++++++++++++---- tools/service-binds/executor-lanes.py | 21 +++++++++++++++--- 3 files changed, 44 insertions(+), 7 deletions(-) create mode 100644 docs/evidence/2026-10-31-nk56-t10-executor-lanes/00-setup-attempt-patch-denied.json diff --git a/docs/evidence/2026-10-31-nk56-t10-executor-lanes/00-setup-attempt-patch-denied.json b/docs/evidence/2026-10-31-nk56-t10-executor-lanes/00-setup-attempt-patch-denied.json new file mode 100644 index 0000000..429cf13 --- /dev/null +++ b/docs/evidence/2026-10-31-nk56-t10-executor-lanes/00-setup-attempt-patch-denied.json @@ -0,0 +1,8 @@ +{ + "action": "setup", + "phase": "cas_required", + "reason": "LaneError", + "source_sha256": "a9289918254bc98d84b4b573c7ed2895fe50d7fbc538f650167e4d84557d44ae", + "status": "failed", + "values_recorded": false +} diff --git a/tests/test_executor_lanes.py b/tests/test_executor_lanes.py index 681b31a..634bb66 100644 --- a/tests/test_executor_lanes.py +++ b/tests/test_executor_lanes.py @@ -30,7 +30,10 @@ def platform_copy(tmp_path, confirm=True): (root / 'credential-change-requests').mkdir() for name in CCRS.values(): shutil.copy(PLATFORM / 'credential-change-requests' / name, root / 'credential-change-requests' / name) - if confirm: + copy = root / 'credential-change-requests' / name + if not confirm: # the real CCRs are confirmed since 2026-10-31; undo it in the copy + copy.write_text(copy.read_text().replace('bound_claims_confirmed: true', 'bound_claims_confirmed: false')) + elif 'bound_claims_confirmed: true' not in copy.read_text(): subprocess.run([sys.executable, str(root / 'scripts/credential-change.py'), 'confirm-binding', '--reviewer', 'platform-owner', '--comment', 'synthetic test', str(root / 'credential-change-requests' / name)], check=True, capture_output=True) @@ -41,11 +44,17 @@ def digests(root): return {k: hashlib.sha256((root / 'credential-change-requests' / v).read_bytes()).hexdigest() for k, v in CCRS.items()} +def m_key(m): + return m.CAS_KEY + + class FakeBao: def __init__(self, m): self.m = m self.store = {m.BOUNDARY: {'policy': 'path "x" { capabilities = ["deny"] }\n' + FENCE}} self.cas = False + self.meta = {'max_versions': 0, 'delete_version_after': '0s', 'custom_metadata': None} + self.meta_write_clobbers = False self.identity = {'policies': ['platform-admin'], 'path': 'auth/netkingdom/oidc/callback'} self.login_ok = {('nk-bind-executor', 'openbao', m.LANES['journey']['role'])} self.wide = False @@ -56,9 +65,8 @@ class FakeBao: missing = out({}, 2, b'No value found') if args[:2] == ['token', 'lookup']: return out({'data': self.identity}) if args[:2] == ['token', 'create']: return out({'auth': {'client_token': 'child'}}) - if args[:3] == ['kv', 'metadata', 'get']: return out({'data': {'cas_required': self.cas}}) - if args[:3] == ['kv', 'metadata', 'patch']: - assert '-cas-required=true' in args; self.cas = True; return out({}) + if args[0] == 'read' and args[-1] == m_key(self.m): + return out({'data': {'cas_required': self.cas, **self.meta}}) if args[0] == 'read': path = args[-1] if path == JOURNEY and token == 'reader': @@ -78,6 +86,11 @@ class FakeBao: return out({'auth': {'client_token': 'reader', 'lease_duration': 900, 'token_policies': [self.m.LANES['journey']['policy']]}}) return out({}, 2, b'Code: 403') + if path == m_key(self.m): + assert payload == {'cas_required': True} + self.cas = True + if self.meta_write_clobbers: self.meta = {**self.meta, 'max_versions': 10} + return out({}) if path.startswith('auth/kubernetes/role/'): self.store[path] = {k: (900 if v == '15m' else v) for k, v in payload.items()}; return out({}) self.store[path] = payload; return out({}) @@ -136,6 +149,7 @@ def test_verify_after_setup(tmp_path, monkeypatch): 'write_role_exists_outside_a_window'), (lambda m, f, d: f.store.update({'sys/policies/acl/' + m.LANES['journey']['policy']: {'policy': 'path "*" {}'}}), 'net-kingdom-bind-executor-syn-journey-read:policy_drift'), + (lambda m, f, d: setattr(f, 'meta_write_clobbers', True), 'metadata_changed_beyond_cas_required'), ]) def test_setup_refusals(tmp_path, monkeypatch, mutate, reason): m, fake, d = build(tmp_path, monkeypatch) diff --git a/tools/service-binds/executor-lanes.py b/tools/service-binds/executor-lanes.py index 1b73640..0d5261d 100644 --- a/tools/service-binds/executor-lanes.py +++ b/tools/service-binds/executor-lanes.py @@ -61,7 +61,8 @@ for _lane in LANES.values(): _lane.update(role=_lane['policy'], service_account=SA, namespace=NS, data_path=_lane['kv'].replace('platform/', 'platform/data/', 1), metadata_path=_lane['kv'].replace('platform/', 'platform/metadata/', 1)) -CAS_KEY = ['-mount=platform', 'workloads/net-kingdom/directory-binds/svc-authelia'] +CAS_KEY = 'platform/metadata/workloads/net-kingdom/directory-binds/svc-authelia' +KEPT_METADATA = ('max_versions', 'delete_version_after', 'custom_metadata') class Refused(Exception): @@ -114,8 +115,22 @@ def policy_check(lane): return check +def key_metadata(): + return data(bao(['read', '-format=json', CAS_KEY]))['data'] + + def cas_required(): - return data(bao(['kv', 'metadata', 'get', '-format=json', *CAS_KEY]))['data'].get('cas_required') is True + return key_metadata().get('cas_required') is True + + +def set_cas_required(): + """A metadata write that names only cas_required. platform-admin holds update, not patch, on platform/*. + KV v2 changes only the fields a write names (checked on the pinned OpenBao, 2026-10-31); the other settings + are compared before and after anyway.""" + before = {k: key_metadata().get(k) for k in KEPT_METADATA} + bao(['write', CAS_KEY, '-'], payload={'cas_required': True}) + after = key_metadata() + if {k: after.get(k) for k in KEPT_METADATA} != before: raise Refused('metadata_changed_beyond_cas_required') def write_role_absent(): @@ -137,7 +152,7 @@ def setup(digests, receipt): ensure('sys/policies/acl/' + lane['policy'], {'policy': lane['policy_text']}, policy_check(lane)) receipt['phase'] = 'cas_required' if not cas_required(): - bao(['kv', 'metadata', 'patch', '-cas-required=true', *CAS_KEY]) + set_cas_required() if not cas_required(): raise Refused('cas_required_not_set') receipt['phase'] = 'journey_role' journey = LANES['journey']