NK-WP-0056-T04: svc-provisioner bind lane into OpenBao/ESO (source only, CCR-2026-0052)
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 2s

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 673304@bnt-lap001
Assistant-Session: 959b12c7-cf2e-440b-a60d-91229cc9e96e
This commit is contained in:
tegwick 2026-10-30 22:54:25 +01:00
parent 3a9d640efb
commit c6df34c4de
4 changed files with 462 additions and 0 deletions

225
tools/service-binds/bind-lane.py Executable file
View file

@ -0,0 +1,225 @@
#!/usr/bin/env python3
"""Attended platform-custody lane for one LLDAP service bind (NK-WP-0056-T04).
setup: under an approved, sha256-bound CCR and an attended platform-admin session,
fence coding agents, write the bind from the decrypted custody copy to KV
(CAS=0, or confirm an identical existing value), then policy and role. Refuses
unless custody equals the live Secret, so no rotation happens here.
verify: after the ExternalSecret is applied, native reader login as the ESO
ServiceAccount, exact policy/TTL, denials, and KV == live Secret in memory.
Receipts hold closed booleans only; values are never printed or written.
"""
import argparse
import base64
import hashlib
import importlib.util
import json
import os
from pathlib import Path
import subprocess
import sys
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
LANES = {
'svc-provisioner': {
'ccr': 'CCR-2026-0052-net-kingdom-identity-provisioner-bind.yaml',
'custody_key': 'LLDAP_SVC_PROVISIONER_PASS',
'secret': 'identity-provisioner-ldap', 'username': 'svc-provisioner',
'external_secret': 'identity-provisioner-ldap',
},
}
FIELD = 'LDAP_BIND_PASSWORD'
BOUNDARY = 'sys/policies/acl/agent-high-risk-boundary'
APPROVERS = {'platform-owner', 'net-kingdom-owner'}
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):
spec = LANES[name]
path = PLATFORM / 'credential-change-requests' / spec['ccr']
raw = path.read_bytes()
if hashlib.sha256(raw).hexdigest() != expected: raise Refused('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('ccr_status')
approved = {r['reviewer'].removeprefix('User (').removesuffix(')') for r in ccr['review'].get('comments', [])
if r.get('decision') == 'approved' and r.get('source_ref')}
if not APPROVERS <= approved: raise Refused('ccr_approval')
role = 'net-kingdom-' + name + '-bind-eso'
kv = 'platform/workloads/net-kingdom/directory-binds/' + name
a = ccr['openbao']; auth = a['auth']; d = ccr['delivery']
if (a['kv_path'] != kv or a['fields'] != [FIELD] or a['policy_name'] != role or auth['role'] != role
or auth['policies'] != [role] or auth.get('audience') != 'openbao'
or auth['bound_claims'] != {'service_account_names': [role], 'service_account_namespaces': ['sso']}
or auth.get('token_no_default_policy') is not True or a.get('metadata_read') is not False
or any(auth.get(k) != '15m' for k in ('ttl', 'token_max_ttl', 'token_explicit_max_ttl'))
or d['namespace'] != 'sso' or d['secret'] != spec['secret'] or d['key'] != 'password'
or d['service_account'] != role):
raise Refused('ccr_shape')
lane = {'service_account': role, 'namespace': 'sso', 'role': role, 'policy': role,
'kv_path': kv.replace('platform/', 'platform/data/', 1), **spec}
policy = ('path "' + lane['kv_path'] + '" { capabilities = ["read"] }\n'
'path "auth/token/lookup-self" { capabilities = ["read"] }\n'
'path "auth/token/revoke-self" { capabilities = ["update"] }\n')
if (PLATFORM / a['policy_file']).read_text() != policy: raise Refused('policy_file')
lane['policy_text'] = policy
return ccr, lane
def kube(kubeconfig, args, payload=None):
return subprocess.run(['kubectl', '--kubeconfig', kubeconfig, *args], input=payload,
check=True, capture_output=True).stdout
def live_secret(kubeconfig, lane):
raw = json.loads(kube(kubeconfig, ['-n', 'sso', 'get', 'secret', lane['secret'], '-o', 'json']))
values = {k: base64.b64decode(v, validate=True).decode() for k, v in raw.get('data', {}).items()}
if values.get('username') != lane['username'] or not values.get('password'): raise Refused('live_secret_shape')
return values['password']
def custody_value(root, lane):
io = load('custody_io', REPO / 'sso-mfa/bootstrap/apply-service-secrets.py')
with io.custody(root, ['lldap']) as dirs:
return io.literal(io.read_env(dirs['lldap']), lane['custody_key'])
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 setup(name, expected, root, kubeconfig, receipt):
receipt['phase'] = 'contract'; ccr, lane = contract(name, expected)
receipt['phase'] = 'identity'; attended_platform_admin()
receipt['phase'] = 'custody_equals_live'
value = custody_value(root, lane)
if live_secret(kubeconfig, lane) != value: raise Refused('custody_drift')
receipt['phase'] = 'coding_agent_fence'
current = data(bao(['read', '-format=json', BOUNDARY]))['data']['policy']; added = ''
for path in (lane['kv_path'], lane['kv_path'].replace('/data/', '/metadata/', 1)):
stanza = 'path "' + path + '" { capabilities = ["deny"] }\n'
if '"' + path + '"' not in current: added += stanza
elif stanza.strip() not in current: raise Refused('fence_conflict')
if added:
if data(bao(['read', '-format=json', BOUNDARY]))['data']['policy'] != current: raise Refused('fence_race')
bao(['write', BOUNDARY, '-'], payload={'policy': current + '\n' + added})
if data(bao(['read', '-format=json', BOUNDARY]))['data']['policy'] != current + '\n' + added:
raise Refused('fence_readback')
receipt['phase'] = 'kv'
existing = bao(['read', '-format=json', lane['kv_path']], allow_failure=True)
if existing.returncode == 0:
if data(existing)['data']['data'] != {FIELD: value}: raise Refused('kv_differs')
receipt['kv_version'] = data(existing)['data']['metadata']['version']; receipt['existing_identical_custody'] = True
else:
if b'No value found' not in existing.stdout + existing.stderr: raise Refused('kv_absence_unproven')
written = data(bao(['write', '-format=json', lane['kv_path'], '-'],
payload={'options': {'cas': 0}, 'data': {FIELD: value}}))
receipt['kv_version'] = written['data']['version']
receipt['phase'] = 'policy_role'
policy = lane['policy_text']
ensure('sys/policies/acl/' + lane['policy'], {'policy': policy},
lambda actual: None if actual['policy'] == policy else (_ for _ in ()).throw(Refused('policy_drift')))
ensure('auth/kubernetes/role/' + lane['role'], role_payload(lane), lambda actual: check_role(actual, lane))
receipt['phase'] = 'fence_effective'
child = data(bao(['token', 'create', '-format=json', '-policy=' + lane['policy'], '-policy=agent-high-risk-boundary',
'-no-default-policy', '-ttl=60s']))['auth']['client_token']
try:
paths = [lane['kv_path'], lane['kv_path'].replace('/data/', '/metadata/', 1)]
if any(v != ['deny'] for v in capabilities(child, paths).values()): raise Refused('fence_ineffective')
finally:
revoke(child)
receipt.update(phase='done', status='custody_and_reader_prepared', custody_equals_live=True,
coding_agents_denied=True, source_ccr=ccr['id'], ccr_sha256=expected,
external_secret_apply_and_verify_pending=True)
def verify(name, expected, kubeconfig, receipt):
receipt['phase'] = 'contract'; ccr, lane = contract(name, expected)
receipt['phase'] = 'identity'; attended_platform_admin()
receipt['phase'] = 'external_secret_ready'
es = json.loads(kube(kubeconfig, ['-n', 'sso', 'get', 'externalsecret', lane['external_secret'], '-o', 'json']))
if not any(c.get('type') == 'Ready' and c.get('status') == 'True' for c in es.get('status', {}).get('conditions', [])):
raise Refused('external_secret_not_ready')
def login(sa, audience, negative=False):
jwt = kube(kubeconfig, ['-n', 'sso', 'create', 'token', sa, '--audience=' + audience, '--duration=10m']).decode().strip()
result = bao(['write', '-format=json', 'auth/kubernetes/login', '-'],
payload={'role': lane['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'] = 'native_reader_login'
auth = login(lane['service_account'], 'openbao'); token = auth['client_token']
try:
if (auth['token_policies'] != [lane['policy']] or auth.get('identity_policies')
or not 0 < auth['lease_duration'] <= 900):
raise Refused('reader_policy_or_ttl')
receipt['phase'] = 'reader_capabilities'
others = ['platform/data/workloads/net-kingdom/directory-binds/' + n
for n in ('svc-authelia', 'svc-keycape', 'svc-privacyidea')]
paths = [lane['kv_path'], lane['kv_path'].replace('/data/', '/metadata/', 1), *others, 'auth/token/create']
cap = capabilities(token, paths)
if cap[paths[0]] != ['read'] or any(cap[p] != ['deny'] for p in paths[1:]): raise Refused('reader_capabilities')
receipt['phase'] = 'kv_equals_live'
native = data(bao(['read', '-format=json', lane['kv_path']], token=token))['data']
if set(native['data']) != {FIELD} or native['data'][FIELD] != live_secret(kubeconfig, lane):
raise Refused('kv_live_mismatch')
receipt['kv_version'] = native['metadata']['version']
finally:
revoke(token)
receipt['phase'] = 'negative_logins'
login('default', 'openbao', True); login(lane['service_account'], 'wrong-audience', True)
check_role(data(bao(['read', '-format=json', 'auth/kubernetes/role/' + lane['role']]))['data'], lane)
receipt.update(phase='done', status='reader_and_delivery_verified', external_secret_ready=True,
exact_policy_and_ttl_verified=True, sibling_metadata_and_token_create_denied=True,
wrong_service_account_denied=True, wrong_audience_denied=True,
eso_delivery_equality_verified=True, temporary_reader_revoked=True,
source_ccr=ccr['id'], ccr_sha256=expected)
def main():
p = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
p.add_argument('action', choices=['setup', 'verify']); p.add_argument('lane', choices=sorted(LANES))
p.add_argument('--ccr-sha256', required=True); p.add_argument('--kubeconfig', required=True)
p.add_argument('--custody', help='decrypted custody root (setup only)'); p.add_argument('--receipt', required=True)
a = p.parse_args()
if (a.action == 'setup') != bool(a.custody): p.error('--custody is required for setup and refused for verify')
fd = os.open(a.receipt, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o600)
r = {'status': 'failed', 'action': a.action, 'lane': a.lane, 'values_recorded': False,
'source_sha256': hashlib.sha256(Path(__file__).read_bytes()).hexdigest()}
try:
if a.action == 'setup': setup(a.lane, a.ccr_sha256, a.custody, a.kubeconfig, r)
else: verify(a.lane, a.ccr_sha256, 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())