Prepare the read-only attended session for four open custody questions
Four owner threads are each blocked on a fact only an authenticated read can establish: whether the legacy whynot-design npm path still exists, which field backs the authoritative lane, whether the two KeyCape approval policies match repo source after an activation that recorded policy_applied false, which netkingdom bound group claims already exist as input to CCR-2026-0019, and which fields the governed backup lane carries. Adds scripts/openbao_open_questions_session.py, which contains no mutating bao verb and emits only a mode-0600 metadata receipt, plus the run-book in docs/openbao-open-questions-session.md. Field-name resolution is a data read, so the runner returns sorted key names and no value reaches argv, disk or the receipt; that caveat is stated rather than glossed. Not run. The rapp-qonto rotation and the RPF-WP-0029 provider invalidation are explicitly out of scope. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WLUjpv3ssxNRAEPPgLFnEB Assistant: claude-code Assistant-Model: opus Assistant-Process: 1275505@bnt-lap001 Assistant-Session: 97265baa-f08f-4032-b290-a1e2965a69c5
This commit is contained in:
parent
251456acd2
commit
f0c2fd58cd
2 changed files with 270 additions and 0 deletions
103
docs/openbao-open-questions-session.md
Normal file
103
docs/openbao-open-questions-session.md
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
# Attended OpenBao session — four open custody questions
|
||||
|
||||
One founder-attended session that settles the questions currently blocking four
|
||||
owner threads. Everything in it is read-only against OpenBao. It provisions
|
||||
nothing, approves nothing, and rotates nothing.
|
||||
|
||||
Prepared 2026-09-09 under RPF-WP-0035. Not yet run.
|
||||
|
||||
## Why a session rather than a reply
|
||||
|
||||
Four owner threads are each blocked on a fact that only an authenticated read can
|
||||
establish, and each has been answered so far with "I will not guess":
|
||||
|
||||
| Question | Thread | What is undecidable from repo source |
|
||||
| --- | --- | --- |
|
||||
| Q1 | secrets-engine `546403e4` | Whether the legacy path `secret/coulomb/whynot-design/npm/publish` still exists and holds a real token |
|
||||
| Q2 | secrets-engine `546403e4` | Which field backs the authoritative npm lane. My CCR says `NPM_AUTH_TOKEN`; secrets-engine says `npm_token`. Field names are not visible in KV v2 metadata |
|
||||
| Q3 | internal | The 2026-09-09 activation receipt records `policy_applied: false` for both KeyCape approval lanes while `role_applied: true`. Reads verified, so the policies must exist — but existence and repo agreement were never confirmed |
|
||||
| Q4 | CCR-2026-0019 | Which netkingdom OIDC roles and bound group claims already exist, as input to the operator group claim that lane is missing |
|
||||
| Q5 | risk-nexus `ee702ac9` | Which fields the governed backup lane actually carries, as RPF-WP-0029-T02 context |
|
||||
|
||||
## Authority and containment
|
||||
|
||||
Authority is the governed `openbao-platform-admin-login` lane: `founder_required`,
|
||||
attended OIDC through `bao login -method=oidc -path=netkingdom role=platform-admin`.
|
||||
The owner command is never run directly.
|
||||
|
||||
```sh
|
||||
scripts/openbao-attended-exec.py -- \
|
||||
/usr/bin/python3 /home/worsch/railiance-platform/scripts/openbao_open_questions_session.py \
|
||||
--receipt /tmp/<new-unique-name>.json
|
||||
```
|
||||
|
||||
`scripts/openbao-attended-exec.py` supplies the WSL browser launcher and then
|
||||
`exec`s into `warden access openbao-platform-admin-login --exec`, so Warden keeps
|
||||
the envelope: it captures both streams, owns the temporary token helper and
|
||||
self-revokes when the command returns. Use a receipt path that does not exist —
|
||||
the writer opens it `O_EXCL` at mode 0600 and refuses to overwrite.
|
||||
|
||||
The runner prints nothing. Its entire output is the receipt.
|
||||
|
||||
## What the runner reads, and the one honest caveat
|
||||
|
||||
| Step | Call | Emitted |
|
||||
| --- | --- | --- |
|
||||
| Q1 | `secrets list`, then `kv metadata get` on the legacy path if the mount exists | mount present, path present, current version, created/updated time |
|
||||
| Q2 | `kv get` on `platform/workloads/coulomb/whynot-design/npm-publish` | **sorted field names only**, plus KV version |
|
||||
| Q3 | `policy read` for both KeyCape approval policies | present, matches-repo-source, sha256 of each normalized text |
|
||||
| Q4 | `list auth/netkingdom/role`, then `read` each | role names, bound claims, groups/user claim, token policies, TTL |
|
||||
| Q5 | `kv get` on `platform/workloads/railiance/backup/offsite-lane` | **sorted field names only**, plus KV version |
|
||||
|
||||
**The caveat, stated plainly:** Q2 and Q5 are data reads. Field names cannot be
|
||||
obtained from KV v2 metadata, so the value transits the runner's memory. It is
|
||||
never printed, never passed on argv, never written to disk, and never placed in
|
||||
the receipt — `field_names()` returns key names and the values go out of scope
|
||||
with the function. If that is not acceptable for the backup lane in particular,
|
||||
drop Q5: it is context for RISK-F-0010, not a dependency of it.
|
||||
|
||||
For Q5 the receipt records `predecessor_material_recorded: false`, and no value,
|
||||
fingerprint, length or shape of any predecessor credential is captured — which is
|
||||
the constraint risk-nexus set.
|
||||
|
||||
## What each answer unblocks
|
||||
|
||||
- **Q1 + Q2 → secrets-engine.** A pointer plus a field name closes their custody
|
||||
question. If Q2 returns `npm_token`, my CCR-2026-0001 is wrong about the field
|
||||
and I correct it; if it returns `NPM_AUTH_TOKEN`, their catalog entry is the one
|
||||
that moves, as a reviewed lane change carrying its own approval. If Q1 finds a
|
||||
populated legacy path, that is a stale duplicate holding a real token and
|
||||
becomes its own disposition item — do not delete it inside this session.
|
||||
- **Q3 → internal.** Either the policies match the repo, and the activation's
|
||||
`policy_applied: false` meant "no change needed", or they drift and that is a
|
||||
finding to fix under RPF-WP-0035 before anyone treats the verifier lanes as
|
||||
clean.
|
||||
- **Q4 → CCR-2026-0019.** Existing bound claims show what the estate already uses
|
||||
for operator lanes. This is **input, not confirmation** — the authorized group
|
||||
claim is NetKingdom's and KeyCape's to state, and the receipt says so in-line so
|
||||
a later reader cannot mistake the listing for an approval.
|
||||
- **Q5 → risk-nexus.** Confirms the governed lane is populated with the expected
|
||||
field names, supporting the fail-closed argument already made on that thread.
|
||||
|
||||
## Explicitly out of scope
|
||||
|
||||
- **The rapp-qonto KeyCape client rotation** (ops-warden `c1aafba4`). It is a
|
||||
two-custodian CAS write with a service restart and needs its own reviewed
|
||||
request first. Do not fold it into a read-only session because the operator
|
||||
happens to be authenticated.
|
||||
- **Predecessor share invalidation** for RPF-WP-0029-T02. That is a provider-side
|
||||
action in the Nextcloud UI, not an OpenBao operation, and it needs the provider
|
||||
owner. It can share the same sitting, but it is a separate step with a separate
|
||||
receipt.
|
||||
- **Any write, seed, rotation or policy apply.** The runner contains no mutating
|
||||
verb; if a question turns out to need one, it comes back as a request.
|
||||
- **Client-side reads for the approval clients.** CCR-2026-0019/0020 remain
|
||||
in_flight; this session does not advance them beyond Q4's input.
|
||||
|
||||
## After the session
|
||||
|
||||
1. Store the receipt under `docs/evidence/` with the run date, values absent.
|
||||
2. Reply to secrets-engine (Q1/Q2), risk-nexus (Q5) and, if Q3 drifts, record the
|
||||
finding against RPF-WP-0035.
|
||||
3. If Q1 finds a populated legacy path, open a disposition item for it rather
|
||||
than acting on it in the moment.
|
||||
167
scripts/openbao_open_questions_session.py
Executable file
167
scripts/openbao_open_questions_session.py
Executable file
|
|
@ -0,0 +1,167 @@
|
|||
"""Read-only, silent owner session for the four open custody questions.
|
||||
|
||||
Runs inside Warden's attended login envelope. Reads no secret value into the
|
||||
receipt: where a data read is unavoidable (field-name resolution), only sorted
|
||||
key names leave this process. Applies nothing, writes nothing to OpenBao, and
|
||||
records no approval. Warden owns the temporary token helper and self-revokes
|
||||
after this command returns.
|
||||
|
||||
Questions settled:
|
||||
Q1 Does legacy mount/path secret/coulomb/whynot-design/npm/publish exist?
|
||||
Q2 Which field name backs the authoritative npm lane, NPM_AUTH_TOKEN or
|
||||
npm_token? (secrets-engine message 546403e4)
|
||||
Q3 Do the two KeyCape approval policies live in OpenBao and match the repo
|
||||
source? (activation receipt recorded policy_applied false)
|
||||
Q4 Which netkingdom OIDC roles and bound group claims already exist, as
|
||||
non-binding input to CCR-2026-0019?
|
||||
Q5 Governed backup lane field presence, as RPF-WP-0029-T02 / RISK-F-0010
|
||||
context. Never the value, fingerprint, length or shape.
|
||||
"""
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path(__file__).resolve().parent.parent
|
||||
LEGACY_MOUNT = 'secret/'
|
||||
LEGACY_PATH = 'secret/coulomb/whynot-design/npm/publish'
|
||||
NPM_PATH = 'platform/workloads/coulomb/whynot-design/npm-publish'
|
||||
BACKUP_PATH = 'platform/workloads/railiance/backup/offsite-lane'
|
||||
POLICIES = {
|
||||
'workload-kv-read-keycape-secrets-engine-approval':
|
||||
'openbao/policies/workload-kv-read-keycape-secrets-engine-approval.hcl',
|
||||
'workload-kv-read-keycape-approval-engine-operator':
|
||||
'openbao/policies/workload-kv-read-keycape-approval-engine-operator.hcl',
|
||||
}
|
||||
FIELD_NAME_RE = re.compile(r'^[A-Za-z0-9_.-]{1,64}$')
|
||||
|
||||
|
||||
def bao(*args, timeout=20):
|
||||
"""Run one bao command. Returns (returncode, parsed-json-or-None)."""
|
||||
result = subprocess.run(['bao', *args], capture_output=True, text=True, timeout=timeout)
|
||||
if result.returncode:
|
||||
return result.returncode, None
|
||||
try:
|
||||
return 0, json.loads(result.stdout)
|
||||
except json.JSONDecodeError:
|
||||
return 0, None
|
||||
|
||||
|
||||
def field_names(payload):
|
||||
"""Sorted key names only. A value never leaves this function."""
|
||||
data = ((payload or {}).get('data') or {}).get('data') or {}
|
||||
names = sorted(str(k) for k in data.keys())
|
||||
if any(not FIELD_NAME_RE.match(n) for n in names):
|
||||
raise ValueError('unexpected field-name shape; refusing to record')
|
||||
return names
|
||||
|
||||
|
||||
def kv_version(payload):
|
||||
meta = ((payload or {}).get('data') or {}).get('metadata') or {}
|
||||
return meta.get('version')
|
||||
|
||||
|
||||
def normalized_policy(text):
|
||||
lines = [line.strip() for line in text.splitlines()]
|
||||
return '\n'.join(line for line in lines if line)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('--receipt', type=Path, required=True)
|
||||
args = parser.parse_args()
|
||||
|
||||
receipt = {
|
||||
'schema': 'platform.openbao-open-questions-session.v1',
|
||||
'observed_at': datetime.now(timezone.utc).isoformat(),
|
||||
'operation': 'read-only observation',
|
||||
'credential_values_emitted': False,
|
||||
'openbao_mutations': 0,
|
||||
'questions': ['Q1', 'Q2', 'Q3', 'Q4', 'Q5'],
|
||||
}
|
||||
|
||||
# Q1 - legacy mount and path existence.
|
||||
rc, mounts = bao('secrets', 'list', '-format=json')
|
||||
legacy_mount_present = bool(mounts and LEGACY_MOUNT in mounts)
|
||||
q1 = {'legacy_mount_present': legacy_mount_present, 'legacy_path': LEGACY_PATH}
|
||||
if legacy_mount_present:
|
||||
rc, meta = bao('kv', 'metadata', 'get', '-format=json', LEGACY_PATH)
|
||||
q1['legacy_path_present'] = rc == 0
|
||||
if rc == 0 and meta:
|
||||
data = meta.get('data') or {}
|
||||
q1['current_version'] = data.get('current_version')
|
||||
q1['created_time'] = data.get('created_time')
|
||||
q1['updated_time'] = data.get('updated_time')
|
||||
else:
|
||||
q1['legacy_path_present'] = False
|
||||
receipt['q1_legacy_npm_path'] = q1
|
||||
|
||||
# Q2 - authoritative npm lane field names.
|
||||
rc, payload = bao('kv', 'get', '-format=json', NPM_PATH)
|
||||
receipt['q2_npm_lane'] = {
|
||||
'path': NPM_PATH,
|
||||
'readable': rc == 0,
|
||||
'field_names': field_names(payload) if rc == 0 else None,
|
||||
'kv_version': kv_version(payload) if rc == 0 else None,
|
||||
'values_recorded': False,
|
||||
}
|
||||
|
||||
# Q3 - KeyCape approval policy presence and drift.
|
||||
q3 = {}
|
||||
for name, rel in POLICIES.items():
|
||||
rc, payload = bao('policy', 'read', '-format=json', name)
|
||||
entry = {'present': rc == 0}
|
||||
if rc == 0:
|
||||
live = normalized_policy(((payload or {}).get('data') or {}).get('policy', ''))
|
||||
source = normalized_policy((REPO / rel).read_text())
|
||||
entry['matches_repo_source'] = live == source
|
||||
entry['live_sha256'] = hashlib.sha256(live.encode()).hexdigest()
|
||||
entry['source_sha256'] = hashlib.sha256(source.encode()).hexdigest()
|
||||
q3[name] = entry
|
||||
receipt['q3_keycape_policies'] = q3
|
||||
|
||||
# Q4 - existing netkingdom OIDC roles and bound claims (non-secret config).
|
||||
rc, listing = bao('list', '-format=json', 'auth/netkingdom/role')
|
||||
roles = {}
|
||||
if rc == 0 and isinstance(listing, list):
|
||||
for role in listing:
|
||||
rc, payload = bao('read', '-format=json', f'auth/netkingdom/role/{role}')
|
||||
if rc:
|
||||
continue
|
||||
data = (payload or {}).get('data') or {}
|
||||
roles[str(role)] = {
|
||||
'bound_claims': data.get('bound_claims'),
|
||||
'groups_claim': data.get('groups_claim'),
|
||||
'user_claim': data.get('user_claim'),
|
||||
'token_policies': data.get('token_policies'),
|
||||
'token_ttl': data.get('token_ttl'),
|
||||
}
|
||||
receipt['q4_netkingdom_roles'] = {
|
||||
'listed': rc == 0,
|
||||
'roles': roles,
|
||||
'note': 'input only; the authorized operator group claim is confirmed by '
|
||||
'NetKingdom/KeyCape, not inferred from this listing',
|
||||
}
|
||||
|
||||
# Q5 - governed backup lane field presence.
|
||||
rc, payload = bao('kv', 'get', '-format=json', BACKUP_PATH)
|
||||
receipt['q5_backup_lane'] = {
|
||||
'path': BACKUP_PATH,
|
||||
'readable': rc == 0,
|
||||
'field_names': field_names(payload) if rc == 0 else None,
|
||||
'kv_version': kv_version(payload) if rc == 0 else None,
|
||||
'predecessor_material_recorded': False,
|
||||
}
|
||||
|
||||
fd = os.open(args.receipt, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
|
||||
with os.fdopen(fd, 'w') as stream:
|
||||
json.dump(receipt, stream, indent=2)
|
||||
stream.write('\n')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Loading…
Add table
Add a link
Reference in a new issue