Prepare the secrets-engine-openbao KeyCape client registration as source only
All checks were successful
CI Smoke / host-smoke (push) Successful in 1s
CI Smoke / container-smoke (push) Successful in 2s

Owner-specific exact-match merge and rollback for sso/keycape-config, the one
proposed env secretKeyRef, synthetic tests and NK-WP-0046 for KEY-WP-0037-T03.
Nothing is applied; the live rollout waits on decision caafe4be.

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

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 132360@bnt-lap001
Assistant-Session: 769b1afd-4330-4c86-96b2-e510e26db42b
This commit is contained in:
tegwick 2026-10-06 13:35:12 +02:00
parent 45a189a391
commit a1e7302930
5 changed files with 549 additions and 0 deletions

View file

@ -218,6 +218,29 @@ The local callback page exchanges the authorization code and displays only
non-secret claims. KeyCape presents a browser OTP challenge between Authelia
password login and the final OIDC redirect whenever privacyIDEA requires MFA.
### Prepared, not live: `secrets-engine-openbao` service client (NK-WP-0046)
Source only for KeyCape KEY-WP-0037-T03; nothing here is registered or deployed.
The live run needs the founder's go (decision `caafe4be-4f68-47b4-9567-316d47b94992`)
and the platform-owned Secret `keycape-secrets-engine-openbao-client`
(key `client-secret`), which does not exist yet.
- `secrets-engine-openbao-client-rollout.py` — exact-match merge into
`sso/keycape-config`: absent adds only this client, identical is a noop,
duplicates/drift/shared `secretRef` refuse. `--operation remove` is the
rollback and removes only this exact registration. Default mode is
`inspect`; `apply` refuses without `--live-go-decision caafe4be-…` and,
for `add`, until the Deployment already carries the env binding.
- `secrets-engine-openbao-client.proposed.patch.yaml` — the single env
`secretKeyRef` for `KEYCAPE_SECRETS_ENGINE_OPENBAO_CLIENT_SECRET`.
- `test_secrets_engine_openbao_client_rollout.py` — synthetic fixtures only.
Order under the go: Secret materialised → server dry-run of both → env patch
(pod restarts with an unused env) → config `apply` → restart → native issuance
and denial checks (KEY-WP-0037-T04). Rollback reverses it: `--operation remove`,
restart, then drop the env binding. The image pin for that rollout is not yet
chosen; see NK-WP-0046.
## Secrets managed
| Secret name | Keys | Purpose |

View file

@ -0,0 +1,222 @@
#!/usr/bin/env python3
"""Owner-specific KeyCape registration of the secrets-engine-openbao service client.
KEY-WP-0037-T03 / NK-WP-0046. Source preparation only: the live operation
needs the founder's separate go (State Hub decision
caafe4be-4f68-47b4-9567-316d47b94992) and the platform-materialised Secret
keycape-secrets-engine-openbao-client (key client-secret). This helper merges
only the client registration into sso/keycape-config; the single Deployment
env binding is the separate proposed patch
secrets-engine-openbao-client.proposed.patch.yaml.
Modes default to non-mutating. `inspect` reads and plans; `dry-run` sends a
server-side dry-run patch; `apply` writes and is reserved for the attended
operator under the live go. `--operation remove` is the rollback: it removes
only this exact registration and nothing else.
Exact-match merge: absent -> add only this client; identical -> noop;
duplicates, drift or a shared secretRef -> fail closed. The signing key,
unrelated clients, policy and every other config byte are preserved. No Secret
value, parser message or Kubernetes error text leaves the process.
"""
from __future__ import annotations
import argparse
import base64
import copy
import importlib.util
import json
from pathlib import Path
import yaml
ROOT = Path(__file__).resolve().parent
def module(name, filename):
spec = importlib.util.spec_from_file_location(name, ROOT / filename)
result = importlib.util.module_from_spec(spec)
spec.loader.exec_module(result)
return result
# Reuse the owner lane's contained kubectl, refusal type and cluster pin.
lane = module('portal_rollout', 'portal-client-rollout.py')
pin = lane.pin
Refused = lane.Refused
require = lane.require
LIVE_GO_DECISION = 'caafe4be-4f68-47b4-9567-316d47b94992'
CLIENT_ID = 'secrets-engine-openbao'
ENV_NAME = 'KEYCAPE_SECRETS_ENGINE_OPENBAO_CLIENT_SECRET'
SECRET_NAME = 'keycape-secrets-engine-openbao-client'
SECRET_KEY = 'client-secret'
# Verbatim from key-cape config/service-clients.example.yaml (reviewed at
# KeyCape e22029e). No `audience` key: the audience defaults to the clientId.
CLIENT = {
'clientId': CLIENT_ID,
'displayName': 'secrets-engine OpenBao login',
'allowedScopes': ['openbao:login'],
'grantTypes': ['client_credentials'],
'clientType': 'confidential',
'secretRef': 'env:' + ENV_NAME,
'serviceSubject': 'service:secrets-engine',
'tenant': 'tenant:platform',
'roles': ['secrets-engine'],
'tokenLifetime': '15m',
}
ENV_BINDING = {'name': ENV_NAME, 'valueFrom': {
'secretKeyRef': {'name': SECRET_NAME, 'key': SECRET_KEY}}}
def _client_node(raw):
root = yaml.compose(raw)
node = next(value for key, value in root.value if key.value == 'clients')
require(isinstance(node, yaml.nodes.SequenceNode) and not node.flow_style,
'block_client_sequence_required')
return node
def _line_start(raw, index):
start = raw.rfind('\n', 0, index) + 1
return start if not raw[start:index].strip() else index
def _inspect(secret):
raw, config, _ = pin.issuer_document(secret)
require(config['authelia'].get('issuer') == pin.VERIFIED_UPSTREAM_ISSUER,
'verified_issuer_pin_required')
clients = config.get('clients')
require(isinstance(clients, list) and clients, 'client_sequence_required')
require(all(isinstance(c, dict) and 'clientId' in c for c in clients), 'client_shape_unexpected')
ids = [c['clientId'] for c in clients]
require(len(ids) == len(set(ids)), 'duplicate_client_id')
others = [c for c in clients if c['clientId'] != CLIENT_ID]
require(all(c.get('secretRef') != CLIENT['secretRef'] for c in others), 'secret_ref_already_bound')
return raw, config, ids
def replacement(secret):
"""Return (base64 config, changed) with only this client added."""
raw, config, ids = _inspect(secret)
if CLIENT_ID in ids:
require(config['clients'][ids.index(CLIENT_ID)] == CLIENT,
'existing_registration_differs_requires_reconciliation')
return secret['data']['config.yaml'], False
node = _client_node(raw)
index = _line_start(raw, node.end_mark.index)
indent = ' ' * node.start_mark.column
fragment = yaml.safe_dump([CLIENT], sort_keys=False, width=120)
fragment = ''.join(indent + line if line.strip() else line for line in fragment.splitlines(True))
if index and raw[index - 1] != '\n':
fragment = '\n' + fragment
updated = raw[:index] + fragment + raw[index:]
expected = copy.deepcopy(config)
expected['clients'].append(CLIENT)
require(yaml.load(updated, Loader=pin.UniqueLoader) == expected, 'unrelated_configuration_changed')
return base64.b64encode(updated.encode()).decode(), True
def removal(secret):
"""Rollback: return (base64 config, changed) with only this exact client removed."""
raw, config, ids = _inspect(secret)
if CLIENT_ID not in ids:
return secret['data']['config.yaml'], False
position = ids.index(CLIENT_ID)
require(config['clients'][position] == CLIENT, 'existing_registration_differs_requires_reconciliation')
require(len(ids) > 1, 'removal_would_empty_client_sequence')
node = _client_node(raw)
item = node.value[position]
start = raw.rfind('\n', 0, item.start_mark.index) + 1
require(raw[start:item.start_mark.index].strip() == '-', 'unexpected_item_layout')
if position + 1 < len(node.value):
following = node.value[position + 1].start_mark.index
end = raw.rfind('\n', 0, following) + 1
else:
end = _line_start(raw, node.end_mark.index)
updated = raw[:start] + raw[end:]
expected = copy.deepcopy(config)
del expected['clients'][position]
require(yaml.load(updated, Loader=pin.UniqueLoader) == expected, 'unrelated_configuration_changed')
return base64.b64encode(updated.encode()).decode(), True
def deployment_binding(deployment):
"""Report whether the live Deployment already carries exactly the one binding."""
containers = deployment['spec']['template']['spec']['containers']
container = next(c for c in containers if c['name'] == 'keycape')
matches = [e for e in container.get('env', []) if e.get('name') == ENV_NAME]
require(len(matches) <= 1 and all(e == ENV_BINDING for e in matches), 'env_binding_drift')
return bool(matches)
def run(args):
kubectl = lane.kubectl
cluster = kubectl(['get', 'namespace', 'kube-system', '-o', 'json'])
require(cluster['metadata']['uid'] == lane.CLUSTER_UID, 'cluster_identity_changed')
before = kubectl(['-n', 'sso', 'get', 'secret', 'keycape-config', '-o', 'json'])
require(not before['metadata'].get('ownerReferences'), 'controller_owned_secret')
metadata = pin.safe_metadata(before)
encoded, changed = (replacement if args.operation == 'add' else removal)(before)
deployment = kubectl(['-n', 'sso', 'get', 'deployment', 'keycape', '-o', 'json'])
bound = deployment_binding(deployment)
receipt = {'schema': 'netkingdom.keycape-secrets-engine-openbao-client.v1',
'target': 'sso/keycape-config', 'before': metadata, 'operation': args.operation,
'client_id': CLIENT_ID, 'change_needed': changed, 'mode': args.mode,
'deployment_env_bound': bound, 'values_emitted': False, 'changed': False}
if args.mode == 'inspect':
return receipt
if args.mode == 'apply':
# Attended only: the founder's live go must be named and the plan revision pinned.
require(args.live_go_decision == LIVE_GO_DECISION, 'founder_live_go_required')
if args.operation == 'add':
# Startup resolves secretRef; a registration without its env binding
# would make the next pod start fail closed.
require(bound, 'deployment_env_binding_required_first')
require(metadata == {'uid': args.expected_uid, 'resource_version': args.expected_resource_version},
'observed_revision_changed')
if changed:
patch = [
{'op': 'test', 'path': '/metadata/uid', 'value': metadata['uid']},
{'op': 'test', 'path': '/metadata/resourceVersion', 'value': metadata['resource_version']},
{'op': 'replace', 'path': '/data/config.yaml', 'value': encoded},
]
command = ['-n', 'sso', 'patch', 'secret', 'keycape-config', '--type=json',
'--patch-file=/dev/stdin', '-o', 'json']
if args.mode == 'dry-run':
command += ['--dry-run=server']
result = kubectl(command, patch)
require(result['data'] == dict(before['data'], **{'config.yaml': encoded}), 'patch_response_mismatch')
if args.mode == 'apply':
after = kubectl(['-n', 'sso', 'get', 'secret', 'keycape-config', '-o', 'json'])
require(after['metadata']['uid'] == metadata['uid']
and after['data'] == dict(before['data'], **{'config.yaml': encoded}),
'readback_mismatch_stop_without_stale_replay')
receipt.update(changed=changed, after=pin.safe_metadata(after), restart_required=changed)
receipt.update(unrelated_config_bytes_preserved=True, other_secret_data_unchanged=True)
return receipt
def main(argv=None):
parser = argparse.ArgumentParser(description=__doc__.split('\n', 1)[0])
parser.add_argument('mode', nargs='?', default='inspect', choices=['inspect', 'dry-run', 'apply'])
parser.add_argument('--operation', default='add', choices=['add', 'remove'])
parser.add_argument('--expected-uid')
parser.add_argument('--expected-resource-version')
parser.add_argument('--live-go-decision', help='required for apply: ' + LIVE_GO_DECISION)
args = parser.parse_args(argv)
try:
print(json.dumps(run(args), sort_keys=True))
return 0
except Refused as error:
print(json.dumps({'status': 'refused', 'reason': str(error)}))
except pin.IssuerPinError as error:
print(json.dumps({'status': 'refused', 'reason': str(error)}))
except Exception:
# Parser and Kubernetes errors can contain full Secret material.
print(json.dumps({'status': 'failed', 'reason': 'contained_operation_failed'}))
return 1
if __name__ == '__main__':
raise SystemExit(main())

View file

@ -0,0 +1,17 @@
# PROPOSED, NOT APPLIED. KEY-WP-0037-T03 / NK-WP-0046.
# Adds exactly one env binding to sso/keycape; values never appear here.
# Prerequisites: platform has materialised Secret keycape-secrets-engine-openbao-client
# (key client-secret) and the founder's live go (decision caafe4be) is resolved.
# Apply as a strategic-merge patch (env merges by name), server dry-run and diff
# first; then fold into deployment.yaml in the same change that records the go.
spec:
template:
spec:
containers:
- name: keycape
env:
- name: KEYCAPE_SECRETS_ENGINE_OPENBAO_CLIENT_SECRET
valueFrom:
secretKeyRef:
name: keycape-secrets-engine-openbao-client
key: client-secret

View file

@ -0,0 +1,211 @@
import base64
import contextlib
import copy
import importlib.util
import io
import json
from pathlib import Path
import unittest
import yaml
spec = importlib.util.spec_from_file_location(
'se_openbao_rollout', Path(__file__).with_name('secrets-engine-openbao-client-rollout.py'))
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
UID = '00000000-0000-4000-8000-000000000001'
KEYCAPE_EXAMPLE = Path('/home/worsch/key-cape/config/service-clients.example.yaml')
EXISTING = [
{'clientId': 'openbao-admin', 'clientType': 'public',
'redirectUris': ['http://localhost:8250/oidc/callback']},
{'clientId': 'secrets-engine-approval', 'audience': 'approval-engine',
'grantTypes': ['client_credentials'], 'clientType': 'confidential',
'secretRef': 'env:KEYCAPE_SECRETS_ENGINE_APPROVAL_CLIENT_SECRET',
'serviceSubject': 'service:secrets-engine', 'tenant': 'tenant:platform',
'roles': ['secrets-engine'], 'tokenLifetime': '15m'},
]
def fixture(clients=None, tail='tokenLifetime: 15m\npolicyPath: /var/lib/keycape-policy/policy.yaml\n'):
# Synthetic marker values only; no live Secret is loaded by this suite.
raw = ('# preserve this comment\nissuer: https://kc.coulomb.social\n'
'authelia:\n issuer: https://auth.coulomb.social\n clientSecret: synthetic-marker\n'
'clients:\n' + yaml.safe_dump(json.loads(json.dumps(EXISTING if clients is None else clients)),
sort_keys=False) + tail)
secret = {'metadata': {'uid': UID, 'resourceVersion': '7'},
'data': {'config.yaml': base64.b64encode(raw.encode()).decode(),
'signing-key.pem': 'c3ludGhldGljLXNpZ25pbmcta2V5'}}
return secret, raw
def decode(encoded):
return base64.b64decode(encoded).decode()
class RegistrationTests(unittest.TestCase):
def test_add_appends_only_this_client_and_preserves_bytes(self):
secret, raw = fixture()
before = copy.deepcopy(secret)
encoded, changed = module.replacement(secret)
updated = decode(encoded)
self.assertTrue(changed)
head = raw[:raw.index('tokenLifetime:')]
self.assertTrue(updated.startswith(head))
self.assertTrue(updated.endswith(raw[len(head):]))
expected = yaml.safe_load(raw)
expected['clients'].append(module.CLIENT)
self.assertEqual(yaml.safe_load(updated), expected)
self.assertEqual(secret, before) # input, including the signing key, untouched
def test_add_at_end_of_document(self):
secret, raw = fixture(tail='')
updated = decode(module.replacement(secret)[0])
self.assertTrue(updated.startswith(raw))
self.assertEqual(yaml.safe_load(updated)['clients'][-1], module.CLIENT)
def test_identical_registration_is_noop(self):
secret, _ = fixture()
secret['data']['config.yaml'] = module.replacement(secret)[0]
self.assertEqual(module.replacement(secret), (secret['data']['config.yaml'], False))
def test_drift_fails_closed(self):
for change in [dict(allowedScopes=['openbao:login', 'openbao:admin']),
dict(audience='openbao'), dict(tokenLifetime='1h'),
dict(roles=['secrets-engine', 'platform-admin']),
dict(tenant='tenant:other'), dict(clientType='public'),
dict(serviceSubject='service:other')]:
with self.subTest(change=change), self.assertRaises(module.Refused):
module.replacement(fixture(EXISTING + [dict(module.CLIENT, **change)])[0])
def test_duplicates_fail_closed(self):
for clients in [EXISTING + [module.CLIENT, module.CLIENT],
EXISTING + [EXISTING[0]]]:
with self.assertRaises(module.Refused):
module.replacement(fixture(clients)[0])
with self.assertRaises(module.Refused):
module.removal(fixture(clients)[0])
def test_secret_ref_reused_by_another_client_fails_closed(self):
other = dict(EXISTING[1], clientId='other', secretRef=module.CLIENT['secretRef'])
with self.assertRaises(module.Refused):
module.replacement(fixture(EXISTING + [other])[0])
def test_unverified_upstream_issuer_refused(self):
secret, raw = fixture()
raw = raw.replace('https://auth.coulomb.social', 'https://auth.example.invalid')
secret['data']['config.yaml'] = base64.b64encode(raw.encode()).decode()
with self.assertRaises(module.Refused):
module.replacement(secret)
def test_rollback_removes_only_this_client_and_restores_exact_bytes(self):
for tail in ['tokenLifetime: 15m\n', '']:
secret, raw = fixture(tail=tail)
secret['data']['config.yaml'] = module.replacement(secret)[0]
encoded, changed = module.removal(secret)
self.assertTrue(changed)
self.assertEqual(decode(encoded), raw)
def test_rollback_of_middle_entry_preserves_neighbours(self):
secret, raw = fixture(EXISTING[:1] + [module.CLIENT] + EXISTING[1:])
updated = decode(module.removal(secret)[0])
self.assertEqual(updated, fixture()[1])
def test_rollback_absent_is_noop_and_drift_refused(self):
secret, _ = fixture()
self.assertEqual(module.removal(secret), (secret['data']['config.yaml'], False))
with self.assertRaises(module.Refused):
module.removal(fixture(EXISTING + [dict(module.CLIENT, tokenLifetime='1h')])[0])
def test_registration_matches_keycape_source(self):
if not KEYCAPE_EXAMPLE.is_file():
self.skipTest('key-cape checkout absent')
source = yaml.safe_load(KEYCAPE_EXAMPLE.read_text())['clients']
self.assertEqual([c for c in source if c['clientId'] == module.CLIENT_ID], [module.CLIENT])
class DeploymentBindingTests(unittest.TestCase):
def deployment(self, env):
return {'spec': {'template': {'spec': {'containers': [{'name': 'keycape', 'env': env}]}}}}
def test_proposed_patch_carries_exactly_the_one_binding(self):
patch = yaml.safe_load(Path(__file__).with_name(
'secrets-engine-openbao-client.proposed.patch.yaml').read_text())
container = patch['spec']['template']['spec']['containers'][0]
self.assertEqual(container, {'name': 'keycape', 'env': [module.ENV_BINDING]})
def test_live_deployment_yaml_does_not_yet_bind(self):
docs = yaml.safe_load_all(Path(__file__).with_name('deployment.yaml').read_text())
dep = next(d for d in docs if d and d.get('kind') == 'Deployment')
self.assertFalse(module.deployment_binding(dep))
def test_binding_detection_and_drift(self):
self.assertTrue(module.deployment_binding(self.deployment([module.ENV_BINDING])))
wrong = {'name': module.ENV_NAME, 'value': 'synthetic'}
with self.assertRaises(module.Refused):
module.deployment_binding(self.deployment([wrong]))
class RunModeTests(unittest.TestCase):
"""Drive run() against a fake kubectl; nothing reaches a cluster."""
def setUp(self):
self.secret, _ = fixture()
self.calls = []
self.original = module.lane.kubectl
self.env = []
def fake(arguments, payload=None):
self.calls.append((arguments, payload))
if arguments[:2] == ['get', 'namespace']:
return {'metadata': {'uid': module.lane.CLUSTER_UID}}
if 'deployment' in arguments:
return {'spec': {'template': {'spec': {'containers': [{'name': 'keycape', 'env': self.env}]}}}}
if 'patch' in arguments:
return {'data': dict(self.secret['data'], **{'config.yaml': payload[-1]['value']})}
return copy.deepcopy(self.secret)
module.lane.kubectl = fake
def tearDown(self):
module.lane.kubectl = self.original
def main(self, *argv):
out = io.StringIO()
with contextlib.redirect_stdout(out):
code = module.main(list(argv))
return code, json.loads(out.getvalue())
def test_default_is_inspect_and_never_patches(self):
code, receipt = self.main()
self.assertEqual(code, 0)
self.assertEqual(receipt['mode'], 'inspect')
self.assertTrue(receipt['change_needed'])
self.assertFalse(any('patch' in a for a, _ in self.calls))
self.assertNotIn('synthetic', json.dumps(receipt))
def test_dry_run_is_server_side(self):
code, _ = self.main('dry-run', '--expected-uid', UID, '--expected-resource-version', '7')
self.assertEqual(code, 0)
patches = [a for a, _ in self.calls if 'patch' in a]
self.assertEqual(len(patches), 1)
self.assertIn('--dry-run=server', patches[0])
def test_apply_requires_live_go_and_env_binding(self):
base = ['apply', '--expected-uid', UID, '--expected-resource-version', '7']
self.assertEqual(self.main(*base), (1, {'status': 'refused', 'reason': 'founder_live_go_required'}))
self.assertEqual(self.main(*base, '--live-go-decision', module.LIVE_GO_DECISION),
(1, {'status': 'refused', 'reason': 'deployment_env_binding_required_first'}))
self.assertFalse(any('patch' in a for a, _ in self.calls))
def test_stale_revision_refused_before_patch(self):
code, receipt = self.main('dry-run', '--expected-uid', UID, '--expected-resource-version', '6')
self.assertEqual(receipt, {'status': 'refused', 'reason': 'observed_revision_changed'})
self.assertFalse(any('patch' in a for a, _ in self.calls))
def test_unexpected_errors_are_contained(self):
self.secret['data']['config.yaml'] = 'not base64 !!'
self.assertEqual(self.main(), (1, {'status': 'failed', 'reason': 'contained_operation_failed'}))
if __name__ == '__main__':
unittest.main()

View file

@ -0,0 +1,76 @@
---
id: NK-WP-0046
type: workplan
title: "Deployment-owned registration of the secrets-engine-openbao KeyCape client"
domain: infotech
repo: net-kingdom
status: active
flavor: implementation
owner: claude-code
topic_slug: netkingdom
created: "2026-10-06"
updated: "2026-10-06"
related: [KEY-WP-0037, RPF-WP-0035]
---
KeyCape KEY-WP-0037-T03 needs NetKingdom, which owns `sso/keycape` and its
config, to register the confidential service client `secrets-engine-openbao`
(subject service:secrets-engine, tenant tenant:platform, role secrets-engine,
scope openbao:login, client_credentials, 15m, secretRef
env:KEYCAPE_SECRETS_ENGINE_OPENBAO_CLIENT_SECRET, no resource-audience
override). Custody boundary 632a8893 is approved; this plan prepares source
only. No live change is authorized until decision
caafe4be-4f68-47b4-9567-316d47b94992 is resolved.
## Prepare the owner-specific merge, env binding and tests
```task
id: NK-WP-0046-T01
status: done
priority: high
```
`sso-mfa/k8s/keycape/secrets-engine-openbao-client-rollout.py` (exact-match
add/noop/refuse, byte-preserving; `--operation remove` rollback; default
`inspect`, `apply` gated on the live-go decision id and the env binding),
`secrets-engine-openbao-client.proposed.patch.yaml` (one env secretKeyRef from
`keycape-secrets-engine-openbao-client`/`client-secret`) and
`test_secrets_engine_openbao_client_rollout.py` (synthetic fixtures). The
registration is asserted equal to KeyCape `config/service-clients.example.yaml`
at e22029e. No cluster, OpenBao or ESO was read or written.
## Choose the reviewed image revision and digest
```task
id: NK-WP-0046-T02
status: todo
priority: high
```
Deployed pin in Git (`deployment.yaml`, since 019e8f2, 2026-09-28):
`forgejo.coulomb.social/coulomb/key-cape@sha256:6f79a2af1c695d39480173fad013facd84d21e7732860366af519302a2c496b8`,
built from KeyCape source 1164f65 per `history/2026-09-28-telemetry-admin-role.md`.
KeyCape main HEAD is e22029e. Decide whether the registration rides the
deployed image (the client needs no new issuer code) or a newer reviewed
digest; record the exact source revision and digest before the go. A newer
binary must be checked for startup refusals against the live config.
## Live registration rollout under the founder's go
```task
id: NK-WP-0046-T03
status: wait
priority: high
needs_human: true
decision_id: caafe4be-4f68-47b4-9567-316d47b94992
depends_on:
- NK-WP-0046-T02
blocking_reason: "Requires the founder's separate live go (caafe4be), the platform-materialised Secret keycape-secrets-engine-openbao-client and the chosen image pin."
```
Attended operator only: server dry-run and diff of the env patch and the
config merge, check headroom, apply the env patch, `apply` the merge with the
observed uid/resourceVersion, restart, then hand to KEY-WP-0037-T04 for native
issuance, wrong-secret/excess-scope denial and existing human-login checks.
Rollback: `--operation remove`, restart, remove the env binding. Fold the
binding into `deployment.yaml` in the same change that records the go.