NK-WP-0056-T04: svc-provisioner bind lane into OpenBao/ESO (source only, CCR-2026-0052)
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:
parent
3a9d640efb
commit
c6df34c4de
4 changed files with 462 additions and 0 deletions
52
sso-mfa/k8s/identity-provisioner/bind-eso.yaml
Normal file
52
sso-mfa/k8s/identity-provisioner/bind-eso.yaml
Normal file
|
|
@ -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
|
||||
178
tests/test_service_bind_lane.py
Normal file
178
tests/test_service_bind_lane.py
Normal file
|
|
@ -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'}}]
|
||||
225
tools/service-binds/bind-lane.py
Executable file
225
tools/service-binds/bind-lane.py
Executable 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())
|
||||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue