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
242 lines
13 KiB
Python
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())
|