diff --git a/sso-mfa/k8s/identity-provisioner/bind-eso.yaml b/sso-mfa/k8s/identity-provisioner/bind-eso.yaml new file mode 100644 index 0000000..b5724cb --- /dev/null +++ b/sso-mfa/k8s/identity-provisioner/bind-eso.yaml @@ -0,0 +1,52 @@ +# NK-WP-0056-T04 pilot, railiance-platform CCR-2026-0052. +# Projects only the svc-provisioner bind password from platform custody into the +# existing Secret sso/identity-provisioner-ldap (created by apply-service-secrets.py). +# Merge keeps the Secret's username key and ownership; Retain leaves the working +# Secret in place if this ExternalSecret is removed. +# Apply only after `tools/service-binds/bind-lane.py setup` reports equal custody. +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: net-kingdom-svc-provisioner-bind-eso + namespace: sso +automountServiceAccountToken: false +--- +apiVersion: external-secrets.io/v1 +kind: SecretStore +metadata: + name: net-kingdom-svc-provisioner-bind + namespace: sso +spec: + provider: + vault: + server: http://openbao.openbao.svc:8200 + path: platform + version: v2 + auth: + kubernetes: + mountPath: kubernetes + role: net-kingdom-svc-provisioner-bind-eso + serviceAccountRef: + name: net-kingdom-svc-provisioner-bind-eso + audiences: [openbao] +--- +apiVersion: external-secrets.io/v1 +kind: ExternalSecret +metadata: + name: identity-provisioner-ldap + namespace: sso +spec: + refreshInterval: 1h + secretStoreRef: + kind: SecretStore + name: net-kingdom-svc-provisioner-bind + target: + name: identity-provisioner-ldap + creationPolicy: Merge + deletionPolicy: Retain + data: + - secretKey: password + remoteRef: + key: workloads/net-kingdom/directory-binds/svc-provisioner + property: LDAP_BIND_PASSWORD diff --git a/tests/test_service_bind_lane.py b/tests/test_service_bind_lane.py new file mode 100644 index 0000000..013cb3b --- /dev/null +++ b/tests/test_service_bind_lane.py @@ -0,0 +1,178 @@ +"""bind-lane.py against an in-memory OpenBao and Kubernetes; no values leave the fakes.""" +import base64 +import hashlib +import importlib.util +import json +import shutil +import subprocess +from pathlib import Path +from types import SimpleNamespace + +import pytest + +ROOT = Path(__file__).resolve().parents[1] +PLATFORM = ROOT.parent / 'railiance-platform' +CCR = 'CCR-2026-0052-net-kingdom-identity-provisioner-bind.yaml' +VALUE = 'SyntheticProvisionerBind1234567890abcdef' +OTHER = 'DifferentProvisionerBind1234567890abcdef' +KV = 'platform/data/workloads/net-kingdom/directory-binds/svc-provisioner' + +pytestmark = pytest.mark.skipif(not (PLATFORM / 'credential-change-requests' / CCR).exists(), + reason='railiance-platform checkout with CCR-2026-0052 required') + + +def load(): + spec = importlib.util.spec_from_file_location('bind_lane', ROOT / 'tools/service-binds/bind-lane.py') + module = importlib.util.module_from_spec(spec); spec.loader.exec_module(module); return module + + +class FakeBao: + def __init__(self): + self.store = {'sys/policies/acl/agent-high-risk-boundary': {'policy': 'path "x" { capabilities = ["deny"] }\n'}} + self.kv = {}; self.writes = []; self.identity = {'policies': ['platform-admin'], 'path': 'auth/netkingdom/oidc/callback'} + self.login_ok = {('net-kingdom-svc-provisioner-bind-eso', 'openbao')} + + def __call__(self, args, *, payload=None, token=None, allow_failure=False): + out = lambda d, code=0, err=b'': SimpleNamespace(returncode=code, stdout=json.dumps(d).encode(), stderr=err) + if args[:2] == ['token', 'lookup']: return out({'data': self.identity}) + if args[:2] == ['token', 'create']: return out({'auth': {'client_token': 'child'}}) + if args[0] == 'read': + path = args[-1] + if path == KV: + return out({'data': {'data': self.kv['value'], 'metadata': {'version': self.kv['version']}}}) if self.kv \ + else out({}, 2, b'No value found at ' + path.encode()) + if path in self.store: return out({'data': self.store[path]}) + return out({}, 2, b'No value found') + if args[0] == 'write': + path = args[-2] if args[-1] == '-' else args[-1] + if path == 'auth/token/revoke': return out({}) + if path == 'sys/capabilities': + caps = {p: (['read'] if p == KV and token is None and payload['token'] == 'reader' else ['deny']) + for p in payload['paths']} + return out({'data': caps}) + if path == 'auth/kubernetes/login': + sa, aud = payload['jwt'].split('|') + if (sa, aud) in self.login_ok: + return out({'auth': {'client_token': 'reader', 'token_policies': ['net-kingdom-svc-provisioner-bind-eso'], + 'lease_duration': 900}}) + return out({}, 2, b'Code: 403') + self.writes.append(path) + if path == KV: + assert payload['options'] == {'cas': 0} + self.kv = {'value': payload['data'], 'version': 1}; return out({'data': {'version': 1}}) + if path.startswith('auth/kubernetes/role/'): + stored = {k: (900 if v == '15m' else v) for k, v in payload.items()} + self.store[path] = stored; return out({}) + self.store[path] = payload; return out({}) + raise AssertionError(args) + + +@pytest.fixture +def lane(tmp_path, monkeypatch): + tool = load() + platform = tmp_path / 'platform' + (platform / 'credential-change-requests').mkdir(parents=True); (platform / 'openbao/policies').mkdir(parents=True) + (platform / 'scripts').mkdir() + shutil.copy(PLATFORM / 'scripts/credential-change.py', platform / 'scripts/credential-change.py') + shutil.copy(PLATFORM / 'openbao/policies/net-kingdom-svc-provisioner-bind-eso.hcl', platform / 'openbao/policies/') + ccr = json.loads((PLATFORM / 'credential-change-requests' / CCR).read_text()) + ccr['status'] = 'approved' + ccr['review']['comments'] = [{'reviewer': f'User ({r})', 'decision': 'approved', 'source_ref': 'statehub:decision:synthetic', + 'comment': 'synthetic', 'reviewed_at': '2026-10-30T00:00:00+00:00'} + for r in ('platform-owner', 'net-kingdom-owner')] + path = platform / 'credential-change-requests' / CCR + path.write_text(json.dumps(ccr, indent=2) + '\n') + digest = hashlib.sha256(path.read_bytes()).hexdigest() + monkeypatch.setattr(tool, 'PLATFORM', platform) + bao = FakeBao() + for name in ('bao',): + monkeypatch.setattr(tool, name, bao) + monkeypatch.setattr(tool, 'data', lambda r: json.loads(r.stdout)) + monkeypatch.setattr(tool, 'capabilities', lambda token, paths: json.loads( + bao(['write', '-format=json', 'sys/capabilities', '-'], payload={'paths': paths, 'token': token}).stdout)['data']) + monkeypatch.setattr(tool, 'revoke', lambda token: None) + + def ensure(path, wanted, check): + old = bao(['read', '-format=json', path], allow_failure=True) + if old.returncode != 0: bao(['write', path, '-'], payload=wanted) + check(json.loads(bao(['read', '-format=json', path]).stdout)['data']) + monkeypatch.setattr(tool, 'ensure', ensure) + live = {'value': VALUE, 'ready': 'True'} + + def kube(kubeconfig, args, payload=None): + verb = args[2:5] + if verb == ['get', 'secret', 'identity-provisioner-ldap']: + return json.dumps({'data': {'username': base64.b64encode(b'svc-provisioner').decode(), + 'password': base64.b64encode(live['value'].encode()).decode()}}).encode() + if verb == ['get', 'externalsecret', 'identity-provisioner-ldap']: + return json.dumps({'status': {'conditions': [{'type': 'Ready', 'status': live['ready']}]}}).encode() + if verb[:2] == ['create', 'token']: + return (args[4] + '|' + args[5].split('=', 1)[1]).encode() + raise AssertionError(args) + monkeypatch.setattr(tool, 'kube', kube) + custody = tmp_path / 'custody'; (custody / 'lldap').mkdir(parents=True, mode=0o700); custody.chmod(0o700) + env = custody / 'lldap/secrets.env'; env.write_text(f'LLDAP_SVC_PROVISIONER_PASS={VALUE}\n'); env.chmod(0o600) + return SimpleNamespace(tool=tool, bao=bao, live=live, digest=digest, custody=str(custody), ccr=path) + + +def run(ns, action): + receipt = {} + if action == 'setup': ns.tool.setup('svc-provisioner', ns.digest, ns.custody, 'kc', receipt) + else: ns.tool.verify('svc-provisioner', ns.digest, 'kc', receipt) + assert VALUE not in json.dumps(receipt) + return receipt + + +def test_setup_then_verify(lane): + r = run(lane, 'setup') + assert r['status'] == 'custody_and_reader_prepared' and r['kv_version'] == 1 + assert lane.bao.writes[0] == 'sys/policies/acl/agent-high-risk-boundary' and KV in lane.bao.writes + assert KV in lane.bao.store['sys/policies/acl/agent-high-risk-boundary']['policy'] + assert run(lane, 'setup')['existing_identical_custody'] is True # rerun is idempotent + assert run(lane, 'verify')['status'] == 'reader_and_delivery_verified' + + +def test_setup_refuses_custody_drift_before_any_write(lane): + lane.live['value'] = OTHER + with pytest.raises(lane.tool.Refused, match='custody_drift'): run(lane, 'setup') + assert lane.bao.writes == [] + + +def test_setup_refuses_unapproved_or_changed_ccr(lane): + with pytest.raises(lane.tool.Refused, match='ccr_digest'): + lane.tool.setup('svc-provisioner', '0' * 64, lane.custody, 'kc', {}) + ccr = json.loads(lane.ccr.read_text()); ccr['status'] = 'proposed'; lane.ccr.write_text(json.dumps(ccr)) + digest = hashlib.sha256(lane.ccr.read_bytes()).hexdigest() + with pytest.raises(lane.tool.Refused, match='ccr_status'): + lane.tool.setup('svc-provisioner', digest, lane.custody, 'kc', {}) + assert lane.bao.writes == [] + + +def test_setup_refuses_non_admin_session(lane): + lane.bao.identity = {'policies': ['default'], 'path': 'auth/netkingdom/oidc/callback'} + with pytest.raises(lane.tool.Refused, match='attended_platform_admin_required'): run(lane, 'setup') + + +def test_verify_refuses_unsynced_or_mismatched_delivery(lane): + run(lane, 'setup') + lane.live['ready'] = 'False' + with pytest.raises(lane.tool.Refused, match='external_secret_not_ready'): run(lane, 'verify') + lane.live.update(ready='True', value=OTHER) + with pytest.raises(lane.tool.Refused, match='kv_live_mismatch'): run(lane, 'verify') + + +def test_verify_refuses_when_wrong_service_account_can_log_in(lane): + run(lane, 'setup') + lane.bao.login_ok.add(('default', 'openbao')) + with pytest.raises(lane.tool.Refused, match='negative_login_accepted'): run(lane, 'verify') + + +def test_manifest_matches_contract(): + import yaml + docs = [d for d in yaml.safe_load_all((ROOT / 'sso-mfa/k8s/identity-provisioner/bind-eso.yaml').read_text()) if d] + sa, store, es = docs + assert sa['metadata'] == {'name': 'net-kingdom-svc-provisioner-bind-eso', 'namespace': 'sso'} + assert store['spec']['provider']['vault']['auth']['kubernetes']['role'] == 'net-kingdom-svc-provisioner-bind-eso' + assert es['spec']['target'] == {'name': 'identity-provisioner-ldap', 'creationPolicy': 'Merge', 'deletionPolicy': 'Retain'} + assert es['spec']['data'] == [{'secretKey': 'password', 'remoteRef': { + 'key': 'workloads/net-kingdom/directory-binds/svc-provisioner', 'property': 'LDAP_BIND_PASSWORD'}}] diff --git a/tools/service-binds/bind-lane.py b/tools/service-binds/bind-lane.py new file mode 100755 index 0000000..5684728 --- /dev/null +++ b/tools/service-binds/bind-lane.py @@ -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()) diff --git a/workplans/NK-WP-0056-bounded-executor-path.md b/workplans/NK-WP-0056-bounded-executor-path.md index a38ffc5..3bf2125 100644 --- a/workplans/NK-WP-0056-bounded-executor-path.md +++ b/workplans/NK-WP-0056-bounded-executor-path.md @@ -141,6 +141,13 @@ needs to be decrypted for a bind rotation. 2026-10-30:** live KeyCape value matches Authelia's hash; custody did not match before, was updated and matches after (value-free output); custody re-sealed and shredded. The reconciliation step is done; the ESO move remains. +**2026-10-30, pilot `svc-provisioner` prepared (source only, nothing applied):** railiance-platform +CCR-2026-0052 (proposed) and policy `net-kingdom-svc-provisioner-bind-eso`; `sso-mfa/k8s/identity-provisioner/bind-eso.yaml` +(ServiceAccount, SecretStore, ExternalSecret with `creationPolicy: Merge` into the existing Secret, `password` only); +`tools/service-binds/bind-lane.py setup|verify` (approved sha256-bound CCR, attended platform-admin, custody must equal +live, coding-agent fence, KV CAS=0, value-free receipts); `tests/test_service_bind_lane.py` (7). Next: owner approval of +the CCR, then the founder-run setup, ESO apply and verify. Authelia, KeyCape and privacyIDEA follow one at a time. + ## Synthetic verification instead of a founder sign-in ```task