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 <noreply@anthropic.com>

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 5502@bnt-lap001
Assistant-Session: 68b3b02f-5d0f-4e71-8421-796f62e50196
This commit is contained in:
tegwick 2026-10-31 19:42:40 +01:00
parent 634b3a41df
commit 12c9b2df43
3 changed files with 44 additions and 7 deletions

View file

@ -0,0 +1,8 @@
{
"action": "setup",
"phase": "cas_required",
"reason": "LaneError",
"source_sha256": "a9289918254bc98d84b4b573c7ed2895fe50d7fbc538f650167e4d84557d44ae",
"status": "failed",
"values_recorded": false
}

View file

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

View file

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