- bounded-executor/prepare_admission.py: the live admission from the pilot example, pins read from the image's commit, window <= 2 h (audit-core's declared bound), validated. - executor-lanes.py window-open/window-close: CCR-2026-0058's window-scoped write role, created for one admitted open window under attended platform-admin and deleted afterwards (idempotent). - security-cases/nk-bind-executor-approval.yaml: the case of record, proposed (founder acceptance). - docs/evidence/2026-11-04-nk56-t15-audit-sender: 8 checks against audit-core 876c3f6. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Assistant: claude-code Assistant-Model: opus Assistant-Process: 523928@bnt-lap001 Assistant-Session: 6c737afb-9c14-4152-9f02-477214b9c350
321 lines
18 KiB
Python
321 lines
18 KiB
Python
#!/usr/bin/env python3
|
|
"""Attended OpenBao setup and proof for the bounded executor's two lanes (NK-WP-0056-T10, T06).
|
|
|
|
write CCR-2026-0058 policy only. Its Kubernetes role is window-scoped: created per admitted run by
|
|
window-open, deleted by window-close. setup sets cas_required on the svc-authelia key.
|
|
journey CCR-2026-0059 policy and the standing read role for sso/nk-bind-executor.
|
|
|
|
setup: under both approved, sha256-bound CCRs (binding confirmed) and an attended platform-admin session:
|
|
check that the coding-agent fence denies both paths, apply both policies, set cas_required, create the
|
|
journey role, prove that the write role does not exist, and prove that the fence beats both policies.
|
|
verify: read back policies, cas_required and the role; log in as sso/nk-bind-executor (audience openbao) and prove
|
|
that the token reads only the journey path, with exactly its policy and at most 15m. Prove that a wrong
|
|
ServiceAccount, a wrong audience and a login to the write role are all denied.
|
|
window-open --admission FILE: for one admitted run (T06): the admission is valid, names this executor and the write
|
|
lane, and its window is open now; the policy, cas_required and the fence hold; the write role is absent. Then
|
|
create the write role exactly as CCR-2026-0058 declares it.
|
|
window-close --admission FILE: after the run's receipt or at window end, whichever comes first: delete the write role
|
|
and prove it is gone. Always allowed, also late; needs no CCR digests.
|
|
Receipts hold closed booleans only. The journey values are read into memory to check their field names, and never
|
|
printed or written.
|
|
"""
|
|
import argparse
|
|
import hashlib
|
|
import importlib.util
|
|
import json
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
HERE = Path(__file__).resolve().parent
|
|
REPO = HERE.parents[1]
|
|
PLATFORM = Path(os.environ.get('NK_PLATFORM_ROOT', REPO.parent / 'railiance-platform'))
|
|
sys.path.insert(0, str(PLATFORM / 'scripts'))
|
|
from state_hub_preflight_lane import bao, data, capabilities, revoke # noqa: E402
|
|
from repair_eso_kubernetes_auth import role_payload, check_role # noqa: E402
|
|
from telemetry_smtp_delivery import ensure # noqa: E402
|
|
|
|
SA, NS = 'nk-bind-executor', 'sso'
|
|
APPROVERS = {'platform-owner', 'net-kingdom-owner'}
|
|
BOUNDARY = 'sys/policies/acl/agent-high-risk-boundary'
|
|
BINDS = 'platform/data/workloads/net-kingdom/directory-binds/'
|
|
LANES = {
|
|
'write': {
|
|
'ccr': 'CCR-2026-0058-net-kingdom-bind-executor-svc-authelia-write.yaml',
|
|
'request_type': 'workload-kv-write-next-version',
|
|
'kv': 'platform/workloads/net-kingdom/directory-binds/svc-authelia', 'fields': ['LDAP_BIND_PASSWORD'],
|
|
'policy': 'net-kingdom-bind-executor-svc-authelia-write',
|
|
'policy_text': ('path "platform/data/workloads/net-kingdom/directory-binds/svc-authelia" {\n'
|
|
' capabilities = ["read", "update"]\n allowed_parameters = {\n "data" = []\n'
|
|
' "options" = []\n }\n}\n\npath "auth/token/lookup-self" {\n capabilities = ["read"]\n}\n\n'
|
|
'path "auth/token/revoke-self" {\n capabilities = ["update"]\n}\n'),
|
|
},
|
|
'journey': {
|
|
'ccr': 'CCR-2026-0059-net-kingdom-bind-executor-syn-journey-read.yaml',
|
|
'request_type': 'workload-kv-read',
|
|
'kv': 'platform/workloads/net-kingdom/synthetic-journey/syn-journey', 'fields': ['PASSWORD', 'TOTP_SEED'],
|
|
'policy': 'net-kingdom-bind-executor-syn-journey-read',
|
|
'policy_text': ('path "platform/data/workloads/net-kingdom/synthetic-journey/syn-journey" '
|
|
'{ capabilities = ["read"] }\n'
|
|
'path "auth/token/lookup-self" { capabilities = ["read"] }\n'
|
|
'path "auth/token/revoke-self" { capabilities = ["update"] }\n'),
|
|
},
|
|
}
|
|
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 = 'platform/metadata/workloads/net-kingdom/directory-binds/svc-authelia'
|
|
KEPT_METADATA = ('max_versions', 'delete_version_after', 'custom_metadata')
|
|
|
|
|
|
class Refused(Exception):
|
|
pass
|
|
|
|
|
|
def load(name, path):
|
|
spec = importlib.util.spec_from_file_location(name, path)
|
|
module = importlib.util.module_from_spec(spec); spec.loader.exec_module(module)
|
|
return module
|
|
|
|
|
|
def contract(name, expected):
|
|
lane = LANES[name]
|
|
path = PLATFORM / 'credential-change-requests' / lane['ccr']
|
|
if hashlib.sha256(path.read_bytes()).hexdigest() != expected: raise Refused(name + ':ccr_digest')
|
|
ccr, errors, _ = load('credential_change', PLATFORM / 'scripts/credential-change.py').validate_ccr(path)
|
|
if errors or ccr['status'] not in {'approved', 'applied', 'verified', 'active'}: raise Refused(name + ':ccr_status')
|
|
approved = {r['reviewer'] for r in ccr['review'].get('comments', []) if r.get('decision') == 'approved'}
|
|
if not APPROVERS <= approved: raise Refused(name + ':ccr_approval')
|
|
a = ccr['openbao']; auth = a['auth']
|
|
if (ccr['request_type'] != lane['request_type'] or a['kv_path'] != lane['kv'] or a['fields'] != lane['fields']
|
|
or a['policy_name'] != lane['policy'] or auth['role'] != lane['role'] or auth['policies'] != [lane['policy']]
|
|
or auth.get('method') != 'kubernetes' or auth.get('audience') != 'openbao'
|
|
or auth['bound_claims'] != {'service_account_names': [SA], 'service_account_namespaces': [NS]}
|
|
or auth.get('token_no_default_policy') is not True
|
|
or any(auth.get(k) != '15m' for k in ('ttl', 'token_max_ttl', 'token_explicit_max_ttl'))):
|
|
raise Refused(name + ':ccr_shape')
|
|
if auth.get('bound_claims_confirmed') is not True: raise Refused(name + ':binding_unconfirmed')
|
|
if (PLATFORM / a['policy_file']).read_text() != lane['policy_text']: raise Refused(name + ':policy_file')
|
|
return ccr
|
|
|
|
|
|
def attended_platform_admin():
|
|
ident = data(bao(['token', 'lookup', '-format=json']))['data']
|
|
if ('platform-admin' not in ident.get('policies', []) or 'root' in ident.get('policies', [])
|
|
or not ident.get('path', '').startswith('auth/netkingdom/')):
|
|
raise Refused('attended_platform_admin_required')
|
|
|
|
|
|
def fence_present():
|
|
current = data(bao(['read', '-format=json', BOUNDARY]))['data']['policy']
|
|
return all('path "' + p + '" { capabilities = ["deny"] }' in current
|
|
for lane in LANES.values() for p in (lane['data_path'], lane['metadata_path']))
|
|
|
|
|
|
def policy_check(lane):
|
|
def check(actual):
|
|
if actual.get('policy') != lane['policy_text']: raise Refused(lane['policy'] + ':policy_drift')
|
|
return check
|
|
|
|
|
|
def key_metadata():
|
|
return data(bao(['read', '-format=json', CAS_KEY]))['data']
|
|
|
|
|
|
def cas_required():
|
|
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():
|
|
result = bao(['read', '-format=json', 'auth/kubernetes/role/' + LANES['write']['role']], allow_failure=True)
|
|
return result.returncode != 0 and b'No value found' in result.stdout + result.stderr
|
|
|
|
|
|
def contracts(digests):
|
|
return {name: contract(name, digests[name]) for name in LANES}
|
|
|
|
|
|
def setup(digests, receipt):
|
|
receipt['phase'] = 'contract'; ccrs = contracts(digests)
|
|
receipt['phase'] = 'identity'; attended_platform_admin()
|
|
receipt['phase'] = 'coding_agent_fence'
|
|
if not fence_present(): raise Refused('agent_fence_missing') # established by CCR-2026-0053 and -0057
|
|
receipt['phase'] = 'policies'
|
|
for lane in LANES.values():
|
|
ensure('sys/policies/acl/' + lane['policy'], {'policy': lane['policy_text']}, policy_check(lane))
|
|
receipt['phase'] = 'cas_required'
|
|
if not cas_required():
|
|
set_cas_required()
|
|
if not cas_required(): raise Refused('cas_required_not_set')
|
|
receipt['phase'] = 'journey_role'
|
|
journey = LANES['journey']
|
|
ensure('auth/kubernetes/role/' + journey['role'], role_payload(journey), lambda actual: check_role(actual, journey))
|
|
receipt['phase'] = 'write_role_absent'
|
|
if not write_role_absent(): raise Refused('write_role_exists_outside_a_window')
|
|
receipt['phase'] = 'fence_effective'
|
|
for lane in LANES.values():
|
|
child = data(bao(['token', 'create', '-format=json', '-policy=' + lane['policy'],
|
|
'-policy=agent-high-risk-boundary', '-no-default-policy', '-ttl=60s']))['auth']['client_token']
|
|
try:
|
|
caps = capabilities(child, [lane['data_path'], lane['metadata_path']])
|
|
if any(v != ['deny'] for v in caps.values()): raise Refused(lane['policy'] + ':fence_ineffective')
|
|
finally:
|
|
revoke(child)
|
|
receipt.update(phase='done', status='lanes_prepared', policies_applied=True, cas_required=True,
|
|
journey_role_created=True, write_role_absent=True, coding_agents_denied=True,
|
|
source_ccrs={n: c['id'] for n, c in ccrs.items()}, ccr_sha256=digests)
|
|
|
|
|
|
def kube(kubeconfig, args):
|
|
return subprocess.run(['kubectl', '--kubeconfig', kubeconfig, *args], check=True, capture_output=True).stdout
|
|
|
|
|
|
def verify(digests, kubeconfig, receipt):
|
|
receipt['phase'] = 'contract'; ccrs = contracts(digests)
|
|
receipt['phase'] = 'identity'; attended_platform_admin()
|
|
receipt['phase'] = 'read_back'
|
|
for lane in LANES.values():
|
|
policy_check(lane)(data(bao(['read', '-format=json', 'sys/policies/acl/' + lane['policy']]))['data'])
|
|
journey, write = LANES['journey'], LANES['write']
|
|
check_role(data(bao(['read', '-format=json', 'auth/kubernetes/role/' + journey['role']]))['data'], journey)
|
|
if not cas_required(): raise Refused('cas_required_not_set')
|
|
if not write_role_absent(): raise Refused('write_role_exists_outside_a_window')
|
|
if not fence_present(): raise Refused('agent_fence_missing')
|
|
|
|
def login(sa, audience, role, negative=False):
|
|
jwt = kube(kubeconfig, ['-n', NS, 'create', 'token', sa, '--audience=' + audience, '--duration=10m']).decode().strip()
|
|
result = bao(['write', '-format=json', 'auth/kubernetes/login', '-'],
|
|
payload={'role': role, 'jwt': jwt}, allow_failure=negative)
|
|
if negative:
|
|
if result.returncode == 0:
|
|
revoke(data(result)['auth']['client_token']); raise Refused('negative_login_accepted')
|
|
if b'403' not in result.stderr and b'400' not in result.stderr: raise Refused('negative_login_unclear')
|
|
return None
|
|
return data(result)['auth']
|
|
|
|
receipt['phase'] = 'journey_login'
|
|
auth = login(SA, 'openbao', journey['role']); token = auth['client_token']
|
|
try:
|
|
if (auth['token_policies'] != [journey['policy']] or auth.get('identity_policies')
|
|
or not 0 < auth['lease_duration'] <= 900):
|
|
raise Refused('journey_policy_or_ttl')
|
|
receipt['phase'] = 'journey_capabilities'
|
|
others = [BINDS + n for n in ('svc-authelia', 'svc-keycape', 'svc-privacyidea', 'svc-provisioner')]
|
|
paths = [journey['data_path'], journey['metadata_path'], write['metadata_path'], *others, 'auth/token/create']
|
|
caps = capabilities(token, paths)
|
|
if caps[paths[0]] != ['read'] or any(caps[p] != ['deny'] for p in paths[1:]):
|
|
raise Refused('journey_capabilities')
|
|
receipt['phase'] = 'journey_read'
|
|
native = data(bao(['read', '-format=json', journey['data_path']], token=token))['data']
|
|
if sorted(native['data']) != sorted(journey['fields']): raise Refused('journey_fields')
|
|
receipt['journey_kv_version'] = native['metadata']['version']
|
|
finally:
|
|
revoke(token)
|
|
receipt['phase'] = 'negative_logins'
|
|
login('default', 'openbao', journey['role'], True)
|
|
login(SA, 'wrong-audience', journey['role'], True)
|
|
login(SA, 'openbao', write['role'], True)
|
|
receipt.update(phase='done', status='lanes_verified', exact_policies_and_role=True, cas_required=True,
|
|
journey_reads_only_its_path=True, journey_lease_at_most_15m=True, write_role_absent=True,
|
|
wrong_service_account_denied=True, wrong_audience_denied=True, write_login_denied=True,
|
|
coding_agents_denied=True, temporary_token_revoked=True,
|
|
source_ccrs={n: c['id'] for n, c in ccrs.items()}, ccr_sha256=digests)
|
|
|
|
|
|
BEA = REPO / 'tools/bounded-execution-admission/validate.py'
|
|
WRITE_SCOPE = 'openbao/' + LANES['write']['kv']
|
|
|
|
|
|
def admitted_window(admission_path, now=None):
|
|
"""The admission that opens the window: valid, for this executor, naming the write lane, window open now."""
|
|
bea = load('bea_validate_lanes', BEA)
|
|
doc = bea.load(Path(admission_path))
|
|
now = now or __import__('datetime').datetime.now(__import__('datetime').UTC)
|
|
if not bea.evaluate(doc, json.loads(bea.SCHEMA.read_text()), as_of=now)['valid']: raise Refused('admission_invalid')
|
|
spec = doc['spec']
|
|
if spec['executor']['identity'] != 'service:' + SA: raise Refused('admission_other_executor')
|
|
if not any(c['mode'] == 'OPERATE' and c['scope'] == WRITE_SCOPE for c in spec['scope_ceiling']):
|
|
raise Refused('admission_does_not_name_write_lane')
|
|
start, end = (bea.parse_time(spec['window'][k]) for k in ('not_before', 'not_after'))
|
|
if not start <= now < end: raise Refused('admission_window_not_open')
|
|
return {'admission': spec['id'], 'admission_digest': bea.admission_digest(doc),
|
|
'window': dict(spec['window'])}
|
|
|
|
|
|
def window_open(digests, admission_path, receipt, now=None):
|
|
"""Create the write role for one admitted window (CCR-2026-0058 lifecycle.role_window). Refuses if it exists."""
|
|
receipt['phase'] = 'contract'; ccrs = contracts(digests)
|
|
receipt['phase'] = 'admission'; receipt.update(admitted_window(admission_path, now))
|
|
receipt['phase'] = 'identity'; attended_platform_admin()
|
|
receipt['phase'] = 'preconditions'
|
|
write = LANES['write']
|
|
policy_check(write)(data(bao(['read', '-format=json', 'sys/policies/acl/' + write['policy']]))['data'])
|
|
if not cas_required(): raise Refused('cas_required_not_set')
|
|
if not fence_present(): raise Refused('agent_fence_missing')
|
|
if not write_role_absent(): raise Refused('write_role_already_open')
|
|
receipt['phase'] = 'create_role'
|
|
ensure('auth/kubernetes/role/' + write['role'], role_payload(write), lambda actual: check_role(actual, write))
|
|
receipt.update(phase='done', status='window_opened', write_role_created=True, exact_role=True,
|
|
close_by=receipt['window']['not_after'], source_ccrs={n: c['id'] for n, c in ccrs.items()},
|
|
ccr_sha256=digests)
|
|
|
|
|
|
def window_close(admission_path, receipt):
|
|
"""Delete the write role after the run's receipt or at window end (whichever comes first) and prove it is gone.
|
|
Needs no open window: closing late must always be possible."""
|
|
bea = load('bea_validate_lanes', BEA)
|
|
spec = bea.load(Path(admission_path))['spec']
|
|
receipt.update(admission=spec['id'])
|
|
receipt['phase'] = 'identity'; attended_platform_admin()
|
|
receipt['phase'] = 'delete_role'
|
|
was_open = not write_role_absent()
|
|
if was_open:
|
|
bao(['delete', 'auth/kubernetes/role/' + LANES['write']['role']])
|
|
if not write_role_absent(): raise Refused('write_role_still_present')
|
|
receipt.update(phase='done', status='window_closed', write_role_was_present=was_open, write_role_absent=True)
|
|
|
|
|
|
def main():
|
|
p = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
p.add_argument('action', choices=['setup', 'verify', 'window-open', 'window-close'])
|
|
p.add_argument('--write-ccr-sha256', help='sha256 of the CCR-2026-0058 file (all but window-close)')
|
|
p.add_argument('--journey-ccr-sha256', help='sha256 of the CCR-2026-0059 file (all but window-close)')
|
|
p.add_argument('--kubeconfig', help='admin kubeconfig; required for verify')
|
|
p.add_argument('--admission', help='the admitted BEA file; required for window-open and window-close')
|
|
p.add_argument('--receipt', required=True)
|
|
a = p.parse_args()
|
|
if a.action == 'verify' and not a.kubeconfig: p.error('--kubeconfig is required for verify')
|
|
if a.action.startswith('window-') and not a.admission: p.error('--admission is required for ' + a.action)
|
|
if a.action != 'window-close' and not (a.write_ccr_sha256 and a.journey_ccr_sha256):
|
|
p.error('--write-ccr-sha256 and --journey-ccr-sha256 are required for ' + a.action)
|
|
digests = {'write': a.write_ccr_sha256, 'journey': a.journey_ccr_sha256}
|
|
fd = os.open(a.receipt, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o600)
|
|
r = {'status': 'failed', 'action': a.action, 'values_recorded': False,
|
|
'source_sha256': hashlib.sha256(Path(__file__).read_bytes()).hexdigest()}
|
|
try:
|
|
if a.action == 'setup': setup(digests, r)
|
|
elif a.action == 'verify': verify(digests, a.kubeconfig, r)
|
|
elif a.action == 'window-open': window_open(digests, a.admission, r)
|
|
else: window_close(a.admission, r)
|
|
except Exception as error:
|
|
r.update(status='failed', reason=str(error) if isinstance(error, Refused) else type(error).__name__)
|
|
finally:
|
|
with os.fdopen(fd, 'w') as out: json.dump(r, out, indent=2, sort_keys=True); out.write('\n')
|
|
print(json.dumps({k: r[k] for k in ('status', 'phase', 'reason') if k in r}))
|
|
return int(r['status'] == 'failed')
|
|
|
|
|
|
if __name__ == '__main__':
|
|
raise SystemExit(main())
|