Let the session run the npm questions alone, and refuse an ambient token
--questions selects a subset, so the npm field can be settled with Q1,Q2 without the two data reads the other questions imply; Q5 is the only step touching the backup lane and is now excludable by name. The runner also refuses to start when OPENBAO_TOKEN, BAO_TOKEN or VAULT_TOKEN is set. A standing token would let every read succeed without the attended login and produce a receipt that looks attended and is not. --allow-ambient-token overrides and records attended_identity false rather than claiming provenance it does not have. 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
f3cf832a35
commit
07b6b63fe6
2 changed files with 120 additions and 69 deletions
|
|
@ -39,6 +39,25 @@ the writer opens it `O_EXCL` at mode 0600 and refuses to overwrite.
|
||||||
|
|
||||||
The runner prints nothing. Its entire output is the receipt.
|
The runner prints nothing. Its entire output is the receipt.
|
||||||
|
|
||||||
|
**To settle the npm field alone**, run only the two questions that bear on it:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
scripts/openbao-attended-exec.py -- \
|
||||||
|
/usr/bin/python3 /home/worsch/railiance-platform/scripts/openbao_open_questions_session.py \
|
||||||
|
--questions Q1,Q2 --receipt /tmp/<new-unique-name>.json
|
||||||
|
```
|
||||||
|
|
||||||
|
`--questions` defaults to all five. Q5 is the only step that reads the backup
|
||||||
|
lane, so naming a subset is also how to exclude it.
|
||||||
|
|
||||||
|
**Ambient-token guard.** The runner refuses to start if `OPENBAO_TOKEN`,
|
||||||
|
`BAO_TOKEN` or `VAULT_TOKEN` is set in the environment. A standing token would
|
||||||
|
let these reads succeed without the attended login, producing a receipt that
|
||||||
|
looks attended and is not. Unset it first; the envelope supplies the identity.
|
||||||
|
`--allow-ambient-token` overrides, and then the receipt records
|
||||||
|
`attended_identity: false` — which is the honest label for that read, not a
|
||||||
|
formality.
|
||||||
|
|
||||||
## What the runner reads, and the one honest caveat
|
## What the runner reads, and the one honest caveat
|
||||||
|
|
||||||
| Step | Call | Emitted |
|
| Step | Call | Emitted |
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,7 @@ POLICIES = {
|
||||||
'openbao/policies/workload-kv-read-keycape-approval-engine-operator.hcl',
|
'openbao/policies/workload-kv-read-keycape-approval-engine-operator.hcl',
|
||||||
}
|
}
|
||||||
FIELD_NAME_RE = re.compile(r'^[A-Za-z0-9_.-]{1,64}$')
|
FIELD_NAME_RE = re.compile(r'^[A-Za-z0-9_.-]{1,64}$')
|
||||||
|
AMBIENT_TOKEN_VARS = ('OPENBAO_TOKEN', 'BAO_TOKEN', 'VAULT_TOKEN')
|
||||||
|
|
||||||
|
|
||||||
def bao(*args, timeout=20):
|
def bao(*args, timeout=20):
|
||||||
|
|
@ -73,7 +74,31 @@ def normalized_policy(text):
|
||||||
def main():
|
def main():
|
||||||
parser = argparse.ArgumentParser(description=__doc__)
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
parser.add_argument('--receipt', type=Path, required=True)
|
parser.add_argument('--receipt', type=Path, required=True)
|
||||||
|
parser.add_argument(
|
||||||
|
'--allow-ambient-token', action='store_true',
|
||||||
|
help='Proceed despite a standing token in the environment. The receipt '
|
||||||
|
'then records attended_identity false.')
|
||||||
|
parser.add_argument(
|
||||||
|
'--questions', default='Q1,Q2,Q3,Q4,Q5',
|
||||||
|
help='Comma-separated subset to run. Q1,Q2 settle the npm field alone; '
|
||||||
|
'Q5 is the only step that reads the backup lane.')
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
ambient = sorted(v for v in AMBIENT_TOKEN_VARS if os.environ.get(v))
|
||||||
|
if ambient and not args.allow_ambient_token:
|
||||||
|
# A standing token in the environment would let these reads succeed
|
||||||
|
# without the attended login, producing a receipt that looks attended
|
||||||
|
# and is not. Refuse rather than silently record the wrong provenance.
|
||||||
|
raise SystemExit(
|
||||||
|
'ambient OpenBao token present in ' + ', '.join(ambient) +
|
||||||
|
'; unset it so the attended envelope supplies the identity, or pass '
|
||||||
|
'--allow-ambient-token to record the read as unattended')
|
||||||
|
|
||||||
|
selected = {q.strip().upper() for q in args.questions.split(',') if q.strip()}
|
||||||
|
unknown = selected - {'Q1', 'Q2', 'Q3', 'Q4', 'Q5'}
|
||||||
|
if unknown:
|
||||||
|
raise SystemExit('unknown question(s): ' + ', '.join(sorted(unknown)))
|
||||||
|
if not selected:
|
||||||
|
raise SystemExit('at least one question is required')
|
||||||
|
|
||||||
receipt = {
|
receipt = {
|
||||||
'schema': 'platform.openbao-open-questions-session.v1',
|
'schema': 'platform.openbao-open-questions-session.v1',
|
||||||
|
|
@ -81,9 +106,12 @@ def main():
|
||||||
'operation': 'read-only observation',
|
'operation': 'read-only observation',
|
||||||
'credential_values_emitted': False,
|
'credential_values_emitted': False,
|
||||||
'openbao_mutations': 0,
|
'openbao_mutations': 0,
|
||||||
'questions': ['Q1', 'Q2', 'Q3', 'Q4', 'Q5'],
|
'questions': sorted(selected),
|
||||||
|
'attended_identity': not ambient,
|
||||||
|
'ambient_token_vars_present': ambient,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if 'Q1' in selected:
|
||||||
# Q1 - legacy mount and path existence.
|
# Q1 - legacy mount and path existence.
|
||||||
rc, mounts = bao('secrets', 'list', '-format=json')
|
rc, mounts = bao('secrets', 'list', '-format=json')
|
||||||
legacy_mount_present = bool(mounts and LEGACY_MOUNT in mounts)
|
legacy_mount_present = bool(mounts and LEGACY_MOUNT in mounts)
|
||||||
|
|
@ -100,6 +128,7 @@ def main():
|
||||||
q1['legacy_path_present'] = False
|
q1['legacy_path_present'] = False
|
||||||
receipt['q1_legacy_npm_path'] = q1
|
receipt['q1_legacy_npm_path'] = q1
|
||||||
|
|
||||||
|
if 'Q2' in selected:
|
||||||
# Q2 - authoritative npm lane field names.
|
# Q2 - authoritative npm lane field names.
|
||||||
rc, payload = bao('kv', 'get', '-format=json', NPM_PATH)
|
rc, payload = bao('kv', 'get', '-format=json', NPM_PATH)
|
||||||
receipt['q2_npm_lane'] = {
|
receipt['q2_npm_lane'] = {
|
||||||
|
|
@ -110,6 +139,7 @@ def main():
|
||||||
'values_recorded': False,
|
'values_recorded': False,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if 'Q3' in selected:
|
||||||
# Q3 - KeyCape approval policy presence and drift.
|
# Q3 - KeyCape approval policy presence and drift.
|
||||||
q3 = {}
|
q3 = {}
|
||||||
for name, rel in POLICIES.items():
|
for name, rel in POLICIES.items():
|
||||||
|
|
@ -124,6 +154,7 @@ def main():
|
||||||
q3[name] = entry
|
q3[name] = entry
|
||||||
receipt['q3_keycape_policies'] = q3
|
receipt['q3_keycape_policies'] = q3
|
||||||
|
|
||||||
|
if 'Q4' in selected:
|
||||||
# Q4 - existing netkingdom OIDC roles and bound claims (non-secret config).
|
# Q4 - existing netkingdom OIDC roles and bound claims (non-secret config).
|
||||||
rc, listing = bao('list', '-format=json', 'auth/netkingdom/role')
|
rc, listing = bao('list', '-format=json', 'auth/netkingdom/role')
|
||||||
roles = {}
|
roles = {}
|
||||||
|
|
@ -147,6 +178,7 @@ def main():
|
||||||
'NetKingdom/KeyCape, not inferred from this listing',
|
'NetKingdom/KeyCape, not inferred from this listing',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if 'Q5' in selected:
|
||||||
# Q5 - governed backup lane field presence.
|
# Q5 - governed backup lane field presence.
|
||||||
rc, payload = bao('kv', 'get', '-format=json', BACKUP_PATH)
|
rc, payload = bao('kv', 'get', '-format=json', BACKUP_PATH)
|
||||||
receipt['q5_backup_lane'] = {
|
receipt['q5_backup_lane'] = {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue