net-kingdom/tools/service-binds/executor-lanes.py
tegwick 634b3a41df
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
NK-WP-0056-T10: attended OpenBao setup and proof for the executor lanes
tools/service-binds/executor-lanes.py (bind-lane.py shape): under both
sha256-bound, approved, binding-confirmed CCRs (0058 write, 0059 journey) and
an attended platform-admin, apply both policies, set cas_required on the
svc-authelia key, create the standing journey-read role, refuse if the
window-scoped write role exists, prove the agent fence; verify proves the
journey token reads only its path (<=15m) and denies wrong SA, wrong audience
and write-role login. Value-free receipts. 12 tests on a fake OpenBao (real
helpers), in make bounded-executor-test. Founder runbook:
sso-mfa/k8s/bounded-executor/T10-LANES-RUNBOOK.md.

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
2026-10-31 19:15:46 +01:00

242 lines
13 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,
never here. Also 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.
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 = ['-mount=platform', 'workloads/net-kingdom/directory-binds/svc-authelia']
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 cas_required():
return data(bao(['kv', 'metadata', 'get', '-format=json', *CAS_KEY]))['data'].get('cas_required') is True
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():
bao(['kv', 'metadata', 'patch', '-cas-required=true', *CAS_KEY])
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)
def main():
p = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
p.add_argument('action', choices=['setup', 'verify'])
p.add_argument('--write-ccr-sha256', required=True, help='sha256 of the CCR-2026-0058 file')
p.add_argument('--journey-ccr-sha256', required=True, help='sha256 of the CCR-2026-0059 file')
p.add_argument('--kubeconfig', help='admin kubeconfig; required for verify')
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')
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)
else: verify(digests, a.kubeconfig, 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())