Bind OpenBao builds to approved inputs and secure credential delivery

Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a0e75a-fc5c-7913-9dba-9846210c766d
This commit is contained in:
tegwick 2026-09-28 11:55:39 +02:00
parent ee1dc2b651
commit 2b83324c01
8 changed files with 622 additions and 84 deletions

View file

@ -95,3 +95,93 @@ section does not exist until phase 4 has run.
See `plans/rein-openweights-openrouter-approle.md` once T01 is
implemented against this format — the first real plan, not a synthetic
one, per `MASON-WP-0001`'s own scoping.
## Binding OpenBao approval to execution inputs
The AppRole and Kubernetes-auth builders additionally require two frontmatter
fields: `build_spec` and `approved_spec_sha256`. `build_spec` is the complete
value-free output of `ops_mason.executor.build_spec_document(spec)`. It includes
the engine, exact policy/role/path and identity bindings, token bounds, policy
reuse and content pin, delivery destination, audit destination, and executable.
`credential_type` must match its `engine`.
During drafting, render the specification and its digest for review:
```python
from pathlib import Path
from ops_mason.executor import AppRoleKVSpec, build_spec_document, build_spec_digest
spec = AppRoleKVSpec(
policy_name="workload-kv-read-example",
kv_path="platform/workloads/example/runtime",
approle_name="example",
token_num_uses=8,
delivery_dir=Path("/home/consumer/.local/example/approle"),
audit_log_path=Path("/home/builder/ops-mason/audit/build-log.jsonl"),
)
document = build_spec_document(spec) # put this mapping under build_spec
candidate_digest = build_spec_digest(document) # present alongside the review
```
The digest is SHA-256 of UTF-8 JSON with sorted keys, compact separators and
no NaN values. Paths render as absolute strings; tuples render as lists. All
specification fields, including explicit defaults and null values, participate.
After the founder approves that exact specification, record the digest as
`approved_spec_sha256` alongside `approved_by`, `approved_at` and
`status: approved`. Quote approval dates in YAML. A candidate digest alone
is not approval, and the digest is an integrity marker, not a signature.
Before any OpenBao command, the builder reloads the plan from disk, checks
approval and engine, validates the specification, and compares both the
frontmatter mapping and approved digest to the actual inputs. Duplicate YAML
keys, missing build specifications, changed scope or changed destinations are
refused. Editing the mapping without renewing its approval digest cannot reuse
the previous approval. The plan file remains the trust boundary: this does not
protect against someone authorized to rewrite the approval record itself.
Historical plans stay historical. They are not automatically given new
specifications or approvals. Before re-executing one, draft and review the exact
new inputs through the existing four-phase process. No live lane was changed
by this format migration.
### Input and policy constraints
The existing AppRole engine creates only exact KV-v2 read lanes: one literal
entry path, `read` on data and metadata, no wildcard, empty/parent path segment,
HCL injection, or write capability. OIDC matrix writers remain a separate,
blocked contract under MASON-WP-0005. Role and policy names must be single
literal names; Kubernetes service-account names/namespaces must be explicit,
nonempty, unique DNS labels. Token use counts must be explicit nonnegative
integers (zero is deliberately unlimited when approved); TTLs are positive
integer durations in `s`, `m` or `h`, with maximum TTL at least initial TTL.
`secret_id_ttl: "0"` remains an explicit reviewable choice.
For `AppRoleKVSpec(reuse_policy=True)`, also provide
`reuse_policy_sha256`. For `KubernetesKVSpec`, provide `policy_sha256`, since
that builder always binds an existing policy. During the scoped structure
survey, hash the exact UTF-8 output of `bao policy read <name>`, including its
trailing newline, and review that policy's full scope. The builder reads only
policy text, checks its hash before auth-role writes, and refuses drift. A
reused policy may deliberately cover more than the AppRole's nominal KV path;
its reviewed contents, not that path field alone, define the grant. Rechecking
a pin is not an atomic lock against later administrative policy changes.
### Credential delivery and failed builds
AppRole delivery requires an absolute directory. Every directory component is
opened without following symlinks; missing directories are created privately.
An existing final directory must belong to the executing account with mode
0700. Both credential files are reserved exclusively at mode 0600 before any
policy/auth write or credential issuance. Existing files, hardlinks and symlink
paths are refused rather than overwritten or permission-repaired. Delivery
uses the held file descriptors and flushes credentials to disk.
OpenBao error and timeout output is suppressed so a failed credential command
cannot echo sensitive material. Audit output contains object names, the
approved specification digest and approval attribution, never credentials.
A failed build can leave private empty or partially delivered files, and may
have changed policy/auth structure or issued a credential before failing.
Do not blindly retry or delete them: inspect structure and audit metadata,
resolve/revoke any partial issuance through the scoped custody procedure, and
review a fresh delivery destination before retrying. The executor does not
claim transactional rollback or automatically widen its revocation authority.