MASON-IN-0005: restore ops-mason as the OpenBao builder (ops-mason-construct)
Founder decision 2026-10-18, "build, but never log in": a plan-scoped grant
that writes workload-kv-read-* policies, external-secrets-* and AppRole role
definitions, and create-only placeholder KV under platform/{workloads,suppliers},
and denies role-id, secret-id, token creation and all value reads, so the
MASON-IN-0004 chain stays broken. Adds bao-session.sh build, policy tests,
INTENT/AGENTS alignment and MASON-WP-0015. Not installed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 278674@bnt-lap001
Assistant-Session: 64827407-e196-4ce3-b70c-76e7b537493f
This commit is contained in:
parent
e2ac6448c2
commit
e7a368af0f
7 changed files with 406 additions and 21 deletions
49
AGENTS.md
49
AGENTS.md
|
|
@ -285,9 +285,9 @@ it as `BAO_TOKEN=$(cat ~/.claude-bao-token) bao <cmd>`.
|
||||||
### What the grant authorises
|
### What the grant authorises
|
||||||
|
|
||||||
Policy `ops-mason-survey` (`policies/ops-mason-survey.hcl`), 45 minutes, max
|
Policy `ops-mason-survey` (`policies/ops-mason-survey.hcl`), 45 minutes, max
|
||||||
one hour, named `claude-<task>`. Read-only, by founder decision 2026-10-07
|
one hour, named `claude-<task>`. Read-only (MASON-IN-0004, 2026-10-07); used
|
||||||
(MASON-IN-0004, option (c)+(a)); it replaces `ops-mason-build`, now in
|
for surveys. Building uses the separate `build` grant below (MASON-IN-0005).
|
||||||
`policies/retired/` and never to be installed.
|
`ops-mason-build` stays in `policies/retired/`, never to be installed.
|
||||||
|
|
||||||
| Allowed | Denied / not granted |
|
| Allowed | Denied / not granted |
|
||||||
|---|---|
|
|---|---|
|
||||||
|
|
@ -333,15 +333,40 @@ action to a piece of work rather than to a person. Use the workplan task where
|
||||||
there is one — `grant MASON-WP-0003-T02` — otherwise something a reader would
|
there is one — `grant MASON-WP-0003-T02` — otherwise something a reader would
|
||||||
recognise later.
|
recognise later.
|
||||||
|
|
||||||
|
### The build grant (MASON-IN-0005)
|
||||||
|
|
||||||
|
ops-mason is the builder. After a plan is approved, the founder mints the
|
||||||
|
build grant:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/bao-session.sh build <plan-task> # ops-mason-construct, 45m, claude-<task>
|
||||||
|
```
|
||||||
|
|
||||||
|
Policy `ops-mason-construct` (`policies/ops-mason-construct.hcl`). It
|
||||||
|
includes everything the survey grant can do, plus:
|
||||||
|
|
||||||
|
| Allowed | Still denied |
|
||||||
|
|---|---|
|
||||||
|
| create/update/delete `sys/policies/acl/workload-kv-read-*` | every other policy; `default`, `root`, `ops-mason-*` (explicit deny) |
|
||||||
|
| create/update/delete `auth/kubernetes/role/external-secrets-*` and AppRole role definitions, with `token_policies` limited to `workload-kv-read-*` | AppRole `role-id`, `secret-id*`, `custom-secret-id`; `auth/token/create*` |
|
||||||
|
| `create` (no read or update) on `platform/data/{workloads,suppliers}/*`, so placeholders only | reading any `*/data/*`; overwriting an existing value |
|
||||||
|
| full metadata on `platform/metadata/{workloads,suppliers}/*` (describe, tear down) | `sys/mounts/*`, `sys/auth/*`, `operators/` and `secret/` writes |
|
||||||
|
|
||||||
|
The grant can build a lane but can never log in as one, which keeps the
|
||||||
|
MASON-IN-0004 chain broken. AppRole secret-id delivery stays a founder act;
|
||||||
|
Kubernetes-auth lanes need no secret-id. After every build, compare the
|
||||||
|
written policy text with the plan's policy file (MASON-WP-0015-T04).
|
||||||
|
|
||||||
|
Install it the same way as the survey policy, with `ops-mason-construct` and
|
||||||
|
its file path.
|
||||||
|
|
||||||
### When a broader grant is genuinely needed
|
### When a broader grant is genuinely needed
|
||||||
|
|
||||||
Writing a policy or auth role, minting a secret-id or a token, enabling a
|
Minting a secret-id or a token, enabling a mount, reading a value, a policy
|
||||||
mount, reading a value, or touching another subsystem's paths is out of
|
outside `workload-kv-read-*`, or another subsystem's paths is outside both
|
||||||
`ops-mason-survey` on purpose. Do not widen the policy to get past a refusal.
|
grants on purpose. Do not widen a policy to get past a refusal. Those acts go
|
||||||
Builds that need writes are applied by the founder through the
|
through the founder and the railiance-platform delegated applier
|
||||||
railiance-platform delegated applier (`scripts/credential-change.py
|
(`scripts/credential-change.py applier-apply` under `warden access
|
||||||
applier-apply` under `warden access openbao-platform-admin-login --exec`), as
|
openbao-platform-admin-login --exec`), or need their own named policy and
|
||||||
for CCR-2026-0033/0034; ops-mason supplies the plan and verifies afterwards.
|
founder decision.
|
||||||
Anything else belongs to another repo or needs its own named policy and its
|
|
||||||
own founder decision.
|
|
||||||
|
|
||||||
|
|
|
||||||
17
INTENT.md
17
INTENT.md
|
|
@ -79,10 +79,12 @@ tooling_contacts:
|
||||||
intended_owner: secrets-engine
|
intended_owner: secrets-engine
|
||||||
blocked_on: >-
|
blocked_on: >-
|
||||||
No engine exposes a policy / auth-role construction surface. Since
|
No engine exposes a policy / auth-role construction surface. Since
|
||||||
MASON-IN-0004 (founder, 2026-10-07) ops-mason holds no build grant:
|
MASON-IN-0005 (founder, 2026-10-18) ops-mason builds these writes itself
|
||||||
these writes are applied by the founder through the railiance-platform
|
under the founder-minted, plan-scoped ops-mason-construct token:
|
||||||
delegated applier; ops-mason surveys and verifies under the read-only
|
workload-kv-read-* policies, external-secrets-* Kubernetes roles, AppRole
|
||||||
ops-mason-survey token.
|
role definitions, and placeholder KV structure. It can never read a
|
||||||
|
role-id, mint a secret-id or token, or read a value (MASON-IN-0004 stays
|
||||||
|
closed). Survey-only sessions use ops-mason-survey.
|
||||||
review: "2026-12-21"
|
review: "2026-12-21"
|
||||||
gap_state: accepted
|
gap_state: accepted
|
||||||
accepted_exception: *plan-approval-gate
|
accepted_exception: *plan-approval-gate
|
||||||
|
|
@ -97,7 +99,10 @@ tooling_contacts:
|
||||||
intended_owner: secrets-engine
|
intended_owner: secrets-engine
|
||||||
blocked_on: >-
|
blocked_on: >-
|
||||||
No engine exposes credential issuance for AppRole lanes; secrets-engine
|
No engine exposes credential issuance for AppRole lanes; secrets-engine
|
||||||
owns credential lifecycle but fronts no issuance API today.
|
owns credential lifecycle but fronts no issuance API today. Since
|
||||||
|
MASON-IN-0005 this step is a founder act: ops-mason-construct denies
|
||||||
|
role-id and secret-id, so ops-mason prepares the delivery and the founder
|
||||||
|
runs it. Kubernetes-auth lanes need no such step.
|
||||||
review: "2026-12-21"
|
review: "2026-12-21"
|
||||||
gap_state: accepted
|
gap_state: accepted
|
||||||
accepted_exception: *plan-approval-gate
|
accepted_exception: *plan-approval-gate
|
||||||
|
|
@ -167,7 +172,7 @@ tooling_contacts:
|
||||||
tooling: OpenBao
|
tooling: OpenBao
|
||||||
operation: "Operator OIDC login; mint a scoped, bounded token under the operator's own session"
|
operation: "Operator OIDC login; mint a scoped, bounded token under the operator's own session"
|
||||||
write: true
|
write: true
|
||||||
capability: "Hand an agent session a task-scoped, read-only ops-mason-survey token instead of the operator's session"
|
capability: "Hand an agent session a task-scoped token instead of the operator's session: read-only ops-mason-survey, or plan-scoped ops-mason-construct after approval (MASON-IN-0005)"
|
||||||
supplied_authority: none
|
supplied_authority: none
|
||||||
note: >-
|
note: >-
|
||||||
Runs under the operator's own authority and narrows it; ops-mason holds
|
Runs under the operator's own authority and narrows it; ops-mason holds
|
||||||
|
|
|
||||||
|
|
@ -189,3 +189,41 @@ installed: >-
|
||||||
A follow-up read shows ops-mason-build absent and ops-mason-survey present.
|
A follow-up read shows ops-mason-build absent and ops-mason-survey present.
|
||||||
state_hub_intake_id: "01a11118-1f35-7ad5-ad51-65dd785c932f"
|
state_hub_intake_id: "01a11118-1f35-7ad5-ad51-65dd785c932f"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## MASON-IN-0005 — Restore ops-mason as the OpenBao builder without reopening MASON-IN-0004
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
id: MASON-IN-0005
|
||||||
|
kind: intake
|
||||||
|
title: Restore ops-mason as the OpenBao builder without reopening MASON-IN-0004
|
||||||
|
status: open
|
||||||
|
origin: review
|
||||||
|
origin_ref: MASON-IN-0004; INTENT.md review of ops-mason and ops-warden, 2026-10-18
|
||||||
|
priority: high
|
||||||
|
owner: ops-mason
|
||||||
|
requested_by: founder
|
||||||
|
description: >-
|
||||||
|
Founder, 2026-10-18: ops-mason exists to set up and organize secrets
|
||||||
|
structure in OpenBao systematically; ops-warden finds and provides it. The
|
||||||
|
read-only resolution of MASON-IN-0004 returned every build to the founder,
|
||||||
|
which contradicts ops-mason INTENT.md (phase 4, success criterion 1: no
|
||||||
|
founder hand-running bao) and leaves ops-warden's INTENT (route and assist,
|
||||||
|
ops-mason the peer lane owner that builds) with nothing built to route to.
|
||||||
|
Both INTENT bodies already agree; the grant and the INTENT frontmatter
|
||||||
|
blocked_on text do not.
|
||||||
|
resolution: >-
|
||||||
|
Founder decision (Bernd Worsch, 2026-10-18, this session): "Build, but
|
||||||
|
never log in". New policy policies/ops-mason-construct.hcl: create, update and
|
||||||
|
delete workload-kv-read-* policies; external-secrets-* Kubernetes auth roles
|
||||||
|
and AppRole role definitions limited to workload-kv-read-* token policies;
|
||||||
|
create-only placeholder KV data and full metadata under
|
||||||
|
platform/{workloads,suppliers}/; explicit denies on role-id, secret-id,
|
||||||
|
token creation, every */data/* read, and the default, root and ops-mason-* policies.
|
||||||
|
ops-mason can build a lane but never log in as one, which breaks the
|
||||||
|
MASON-IN-0004 chain at step three. AppRole secret-id delivery stays a
|
||||||
|
founder act; Kubernetes auth (the current default) needs none. A post-build
|
||||||
|
check comparing policy text with the plan file is the detective control for
|
||||||
|
over-broad content. ops-mason-survey stays for survey-only sessions.
|
||||||
|
Implementation and install: MASON-WP-0015.
|
||||||
|
updated: '2026-10-18T00:00:00Z'
|
||||||
|
```
|
||||||
|
|
|
||||||
133
policies/ops-mason-construct.hcl
Normal file
133
policies/ops-mason-construct.hcl
Normal file
|
|
@ -0,0 +1,133 @@
|
||||||
|
# ops-mason-construct — build security structure; never become what it builds.
|
||||||
|
#
|
||||||
|
# Founder decision 2026-10-18 (MASON-IN-0005, supersedes the read-only
|
||||||
|
# resolution of MASON-IN-0004): ops-mason is the builder (INTENT.md, phase 4).
|
||||||
|
# It creates, changes and tears down policies, auth roles and KV structure
|
||||||
|
# for an approved construction plan. ops-warden routes to what it built.
|
||||||
|
#
|
||||||
|
# How the MASON-IN-0004 escalation stays closed: that chain was
|
||||||
|
# write policy -> bind role -> read role-id -> mint secret-id -> log in.
|
||||||
|
# This grant keeps the first two steps (the job) and holds none of the steps
|
||||||
|
# that turn a role into a login: no role-id, no secret-id, no token creation,
|
||||||
|
# no KV value reads. ops-mason can build a lane but cannot use one. The
|
||||||
|
# remaining risk, a built policy broader than its plan, is caught after the
|
||||||
|
# fact by a post-build check, policy text vs plan file (MASON-WP-0015-T04), and the
|
||||||
|
# audit log (display_name claude-*).
|
||||||
|
#
|
||||||
|
# Granted only by the founder's own OIDC session through
|
||||||
|
# GRANT_POLICY=ops-mason-construct ./scripts/bao-session.sh grant <plan-task>
|
||||||
|
# as a 45-minute named token, after the plan is approved. Survey-only sessions
|
||||||
|
# keep using ops-mason-survey.
|
||||||
|
#
|
||||||
|
# Path priority (same note as ops-mason-survey.hcl): within one policy the most
|
||||||
|
# specific path wins, and a `+` segment loses to a literal one. The denies below
|
||||||
|
# are literal or more specific than the grants they bound.
|
||||||
|
#
|
||||||
|
# allowed_parameters: OpenBao glob support on list-valued parameters must be
|
||||||
|
# confirmed by the negative tests in MASON-WP-0015-T03 before this is relied on.
|
||||||
|
|
||||||
|
# --- everything the survey grant can read -----------------------------------
|
||||||
|
path "sys/mounts" { capabilities = ["read", "list"] }
|
||||||
|
path "sys/auth" { capabilities = ["read", "list"] }
|
||||||
|
path "sys/policies/acl" { capabilities = ["list"] }
|
||||||
|
path "auth/approle/role" { capabilities = ["list"] }
|
||||||
|
path "auth/netkingdom/role/*" { capabilities = ["read", "list"] }
|
||||||
|
path "auth/jwt/role/*" { capabilities = ["read", "list"] }
|
||||||
|
path "auth/keycape-services/role/*" { capabilities = ["read", "list"] }
|
||||||
|
path "operators/metadata/*" { capabilities = ["read", "list"] }
|
||||||
|
path "sys/capabilities" { capabilities = ["update"] }
|
||||||
|
path "sys/capabilities-self" { capabilities = ["update"] }
|
||||||
|
path "auth/token/lookup-self" { capabilities = ["read"] }
|
||||||
|
path "auth/token/revoke-self" { capabilities = ["update"] }
|
||||||
|
|
||||||
|
# Every policy stays readable for the survey and for verification.
|
||||||
|
path "sys/policies/acl/*" {
|
||||||
|
capabilities = ["read"]
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- build: workload read policies ------------------------------------------
|
||||||
|
# The lane convention (railiance-platform/openbao/policies). Content is not
|
||||||
|
# constrainable in OpenBao; the post-build check compares it with the plan.
|
||||||
|
path "sys/policies/acl/workload-kv-read-*" {
|
||||||
|
capabilities = ["create", "read", "update", "delete"]
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- build: Kubernetes auth roles for External Secrets ----------------------
|
||||||
|
# Role may carry only workload-kv-read-* policies; legacy `policies` refused.
|
||||||
|
path "auth/kubernetes/role/external-secrets-*" {
|
||||||
|
capabilities = ["create", "read", "update", "delete", "list"]
|
||||||
|
allowed_parameters = {
|
||||||
|
"token_policies" = ["workload-kv-read-*"]
|
||||||
|
"*" = []
|
||||||
|
}
|
||||||
|
denied_parameters = {
|
||||||
|
"policies" = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
path "auth/kubernetes/role/*" {
|
||||||
|
capabilities = ["read", "list"]
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- build: AppRole role definitions (never credentials) --------------------
|
||||||
|
# Definition only. role-id and secret-id are denied below, so an AppRole
|
||||||
|
# lane's credential delivery stays a founder act.
|
||||||
|
path "auth/approle/role/+" {
|
||||||
|
capabilities = ["create", "read", "update", "delete"]
|
||||||
|
allowed_parameters = {
|
||||||
|
"token_policies" = ["workload-kv-read-*"]
|
||||||
|
"*" = []
|
||||||
|
}
|
||||||
|
denied_parameters = {
|
||||||
|
"policies" = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- build: KV structure on the platform mount ------------------------------
|
||||||
|
# create without read/update: a new path can get placeholder values (`xxx`),
|
||||||
|
# an existing value can be neither read nor overwritten.
|
||||||
|
path "platform/data/workloads/*" {
|
||||||
|
capabilities = ["create"]
|
||||||
|
}
|
||||||
|
|
||||||
|
path "platform/data/suppliers/*" {
|
||||||
|
capabilities = ["create"]
|
||||||
|
}
|
||||||
|
|
||||||
|
# Describe, inventory and tear down paths (custom_metadata, metadata delete).
|
||||||
|
path "platform/metadata/workloads/*" {
|
||||||
|
capabilities = ["create", "read", "update", "delete", "list"]
|
||||||
|
}
|
||||||
|
|
||||||
|
path "platform/metadata/suppliers/*" {
|
||||||
|
capabilities = ["create", "read", "update", "delete", "list"]
|
||||||
|
}
|
||||||
|
|
||||||
|
path "platform/metadata/*" {
|
||||||
|
capabilities = ["read", "list"]
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- the grant cannot widen itself or every token ---------------------------
|
||||||
|
path "sys/policies/acl/ops-mason-construct" { capabilities = ["deny"] }
|
||||||
|
path "sys/policies/acl/ops-mason-survey" { capabilities = ["deny"] }
|
||||||
|
path "sys/policies/acl/default" { capabilities = ["deny"] }
|
||||||
|
path "sys/policies/acl/root" { capabilities = ["deny"] }
|
||||||
|
|
||||||
|
# --- never become what it builds ---------------------------------------------
|
||||||
|
path "auth/approle/role/+/role-id" { capabilities = ["deny"] }
|
||||||
|
path "auth/approle/role/+/secret-id" { capabilities = ["deny"] }
|
||||||
|
path "auth/approle/role/+/secret-id/*" { capabilities = ["deny"] }
|
||||||
|
path "auth/approle/role/+/secret-id-accessor/*" { capabilities = ["deny"] }
|
||||||
|
path "auth/approle/role/+/custom-secret-id" { capabilities = ["deny"] }
|
||||||
|
path "auth/token/create" { capabilities = ["deny"] }
|
||||||
|
path "auth/token/create/*" { capabilities = ["deny"] }
|
||||||
|
path "auth/token/create-orphan" { capabilities = ["deny"] }
|
||||||
|
|
||||||
|
# --- never read a value --------------------------------------------------------
|
||||||
|
path "platform/data/*" { capabilities = ["deny"] }
|
||||||
|
path "operators/data/*" { capabilities = ["deny"] }
|
||||||
|
path "secret/data/*" { capabilities = ["deny"] }
|
||||||
|
|
||||||
|
# Not granted, on purpose: sys/mounts/* and sys/auth/* (no new mounts or auth
|
||||||
|
# methods), auth/token/revoke (other tokens), operators/ and secret/ writes,
|
||||||
|
# any policy outside workload-kv-read-*, any role outside the two shapes above.
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
# ./scripts/bao-session.sh status what session exists, if any
|
# ./scripts/bao-session.sh status what session exists, if any
|
||||||
# ./scripts/bao-session.sh login refresh YOUR operator session (OIDC)
|
# ./scripts/bao-session.sh login refresh YOUR operator session (OIDC)
|
||||||
# ./scripts/bao-session.sh grant <task> mint a scoped, time-boxed token for Claude
|
# ./scripts/bao-session.sh grant <task> mint a scoped, time-boxed token for Claude
|
||||||
|
# ./scripts/bao-session.sh build <task> same, with ops-mason-construct (approved plan only)
|
||||||
# ./scripts/bao-session.sh revoke end the granted token immediately
|
# ./scripts/bao-session.sh revoke end the granted token immediately
|
||||||
#
|
#
|
||||||
# Why `grant` exists
|
# Why `grant` exists
|
||||||
|
|
@ -16,8 +17,12 @@
|
||||||
# and read mounts, auth methods, policies, auth role definitions
|
# and read mounts, auth methods, policies, auth role definitions
|
||||||
# and KV *metadata*, and query capabilities. It cannot write a
|
# and KV *metadata*, and query capabilities. It cannot write a
|
||||||
# policy or role, mint a secret-id or a token, or read a secret
|
# policy or role, mint a secret-id or a token, or read a secret
|
||||||
# value (MASON-IN-0004, founder decision 2026-10-07). Builds go
|
# value (MASON-IN-0004, founder decision 2026-10-07).
|
||||||
# through the founder-attended railiance-platform applier.
|
# `build` carries ops-mason-construct instead (MASON-IN-0005,
|
||||||
|
# 2026-10-18): it writes workload-kv-read-* policies, ESO/AppRole
|
||||||
|
# role definitions and placeholder KV structure, and still cannot
|
||||||
|
# read a role-id, mint a secret-id or token, or read a value.
|
||||||
|
# Mint it only after the plan is approved.
|
||||||
# * bounded — 45 minutes, no renewal past one hour.
|
# * bounded — 45 minutes, no renewal past one hour.
|
||||||
# * named — display_name claude-<task>, so the audit log says which task an
|
# * named — display_name claude-<task>, so the audit log says which task an
|
||||||
# action belonged to, not just that platform-root did it.
|
# action belonged to, not just that platform-root did it.
|
||||||
|
|
@ -130,6 +135,7 @@ case "${1:-status}" in
|
||||||
status) cmd_status ;;
|
status) cmd_status ;;
|
||||||
login) cmd_login ;;
|
login) cmd_login ;;
|
||||||
grant) shift; cmd_grant "${1:-}" ;;
|
grant) shift; cmd_grant "${1:-}" ;;
|
||||||
|
build) shift; GRANT_POLICY=ops-mason-construct; cmd_grant "${1:-}" ;;
|
||||||
revoke) cmd_revoke ;;
|
revoke) cmd_revoke ;;
|
||||||
*) echo "usage: $0 status|login|grant <task>|revoke" >&2; exit 2 ;;
|
*) echo "usage: $0 status|login|grant <task>|build <task>|revoke" >&2; exit 2 ;;
|
||||||
esac
|
esac
|
||||||
|
|
|
||||||
68
tests/test_construct_policy.py
Normal file
68
tests/test_construct_policy.py
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
"""ops-mason-construct can build a lane but never log in as one (MASON-IN-0005)."""
|
||||||
|
|
||||||
|
import re
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
POLICY = Path(__file__).resolve().parents[1] / "policies" / "ops-mason-construct.hcl"
|
||||||
|
|
||||||
|
|
||||||
|
def _stanzas() -> dict[str, str]:
|
||||||
|
text = "\n".join(line.split("#", 1)[0] for line in POLICY.read_text().splitlines())
|
||||||
|
return {
|
||||||
|
m.group(1): m.group(2)
|
||||||
|
for m in re.finditer(r'path\s+"([^"]+)"\s*\{(.*?)\n?\s*\}\s*(?=path|\Z)', text, re.S)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _caps(body: str) -> set[str]:
|
||||||
|
m = re.search(r"capabilities\s*=\s*\[([^\]]*)\]", body)
|
||||||
|
return set(re.findall(r'"(\w+)"', m.group(1))) if m else set()
|
||||||
|
|
||||||
|
|
||||||
|
def test_login_and_value_paths_are_denied():
|
||||||
|
stanzas = _stanzas()
|
||||||
|
for path in [
|
||||||
|
"auth/approle/role/+/role-id",
|
||||||
|
"auth/approle/role/+/secret-id",
|
||||||
|
"auth/approle/role/+/secret-id/*",
|
||||||
|
"auth/approle/role/+/custom-secret-id",
|
||||||
|
"auth/token/create",
|
||||||
|
"auth/token/create/*",
|
||||||
|
"auth/token/create-orphan",
|
||||||
|
"platform/data/*",
|
||||||
|
"operators/data/*",
|
||||||
|
"secret/data/*",
|
||||||
|
"sys/policies/acl/ops-mason-construct",
|
||||||
|
"sys/policies/acl/ops-mason-survey",
|
||||||
|
"sys/policies/acl/default",
|
||||||
|
"sys/policies/acl/root",
|
||||||
|
]:
|
||||||
|
assert _caps(stanzas[path]) == {"deny"}, path
|
||||||
|
|
||||||
|
|
||||||
|
def test_kv_data_is_create_only():
|
||||||
|
stanzas = _stanzas()
|
||||||
|
for path in ["platform/data/workloads/*", "platform/data/suppliers/*"]:
|
||||||
|
assert _caps(stanzas[path]) == {"create"}, path
|
||||||
|
for path, body in stanzas.items():
|
||||||
|
if "/data/" in path:
|
||||||
|
assert not _caps(body) & {"read", "update", "list"}, path
|
||||||
|
|
||||||
|
|
||||||
|
def test_policy_writes_limited_to_workload_read_prefix():
|
||||||
|
for path, body in _stanzas().items():
|
||||||
|
if path.startswith("sys/policies/acl/") and _caps(body) & {"create", "update", "delete"}:
|
||||||
|
assert path == "sys/policies/acl/workload-kv-read-*", path
|
||||||
|
|
||||||
|
|
||||||
|
def test_role_writes_restrict_token_policies():
|
||||||
|
for path, body in _stanzas().items():
|
||||||
|
if path.startswith("auth/") and "/role/" in path and _caps(body) & {"create", "update"}:
|
||||||
|
assert '"token_policies" = ["workload-kv-read-*"]' in body, path
|
||||||
|
assert re.search(r'denied_parameters\s*=\s*\{\s*"policies"\s*=\s*\[\]', body), path
|
||||||
|
|
||||||
|
|
||||||
|
def test_no_mount_or_auth_method_writes():
|
||||||
|
for path, body in _stanzas().items():
|
||||||
|
if path.startswith(("sys/mounts/", "sys/auth/")):
|
||||||
|
assert _caps(body) <= {"read", "list", "deny"}, path
|
||||||
110
workplans/MASON-WP-0015-construct-grant.md
Normal file
110
workplans/MASON-WP-0015-construct-grant.md
Normal file
|
|
@ -0,0 +1,110 @@
|
||||||
|
---
|
||||||
|
id: MASON-WP-0015
|
||||||
|
type: workplan
|
||||||
|
title: "Restore ops-mason as the OpenBao builder: the ops-mason-construct grant"
|
||||||
|
domain: infotech
|
||||||
|
repo: ops-mason
|
||||||
|
status: active
|
||||||
|
owner: claude
|
||||||
|
topic_slug: netkingdom-security-architecture
|
||||||
|
created: "2026-10-18"
|
||||||
|
updated: "2026-10-18"
|
||||||
|
flavor: implementation
|
||||||
|
depends_on: []
|
||||||
|
---
|
||||||
|
|
||||||
|
# ops-mason-construct grant
|
||||||
|
|
||||||
|
Decision: `intakes/intakes.md` MASON-IN-0005 (founder, 2026-10-18, "Build, but
|
||||||
|
never log in"). ops-mason builds structure in OpenBao; ops-warden finds and
|
||||||
|
provides it. The grant breaks the MASON-IN-0004 chain at the login step.
|
||||||
|
|
||||||
|
## Review both INTENTs and draft the grant
|
||||||
|
|
||||||
|
```task
|
||||||
|
id: MASON-WP-0015-T01
|
||||||
|
status: done
|
||||||
|
priority: high
|
||||||
|
```
|
||||||
|
|
||||||
|
Reviewed ops-mason and ops-warden INTENT.md. The bodies agree (ops-mason
|
||||||
|
builds, ops-warden routes and assists); the read-only grant did not. Wrote
|
||||||
|
`policies/ops-mason-construct.hcl` and `tests/test_construct_policy.py`, added
|
||||||
|
`bao-session.sh build <task>`, and updated INTENT.md frontmatter and the
|
||||||
|
AGENTS.md OpenBao section.
|
||||||
|
|
||||||
|
## Install the policy (founder act)
|
||||||
|
|
||||||
|
```task
|
||||||
|
id: MASON-WP-0015-T02
|
||||||
|
status: todo
|
||||||
|
priority: high
|
||||||
|
```
|
||||||
|
|
||||||
|
Same silent wrapper and hash receipt as the survey policy (AGENTS.md), with
|
||||||
|
`ops-mason-construct` and
|
||||||
|
`/home/worsch/ops-mason/policies/ops-mason-construct.hcl`. Confirm your OIDC
|
||||||
|
session can mint a token carrying it (`bao-session.sh build MASON-WP-0015-T03`).
|
||||||
|
|
||||||
|
## Live negative and positive tests
|
||||||
|
|
||||||
|
```task
|
||||||
|
id: MASON-WP-0015-T03
|
||||||
|
status: wait
|
||||||
|
priority: high
|
||||||
|
```
|
||||||
|
|
||||||
|
Under the build grant, against throwaway names (`workload-kv-read-mason-probe`,
|
||||||
|
`external-secrets-mason-probe`, `platform/suppliers/mason/probe`): policy and
|
||||||
|
role create/delete succeed; a role with `token_policies=default`, or with
|
||||||
|
legacy `policies`, is refused (this confirms glob support in
|
||||||
|
`allowed_parameters`); role-id read, secret-id write, token create, data read,
|
||||||
|
and a write over the existing probe path are all refused; a policy write
|
||||||
|
outside `workload-kv-read-*` is refused. Record the results as a
|
||||||
|
names-only receipt. If the glob check fails, stop and redesign the role
|
||||||
|
stanzas before any build.
|
||||||
|
|
||||||
|
## Executor and post-build check
|
||||||
|
|
||||||
|
```task
|
||||||
|
id: MASON-WP-0015-T04
|
||||||
|
status: todo
|
||||||
|
priority: high
|
||||||
|
```
|
||||||
|
|
||||||
|
`src/ops_mason/executor.py` `build_approle_kv_lane` mints the role-id and
|
||||||
|
secret-id itself (lines ~420-426). Split it so that it stops after the role
|
||||||
|
definition and emits the founder delivery step. Add a post-build check that
|
||||||
|
reads each written policy and compares it with the plan's policy file
|
||||||
|
(normalising the trailing newline). Run the check in every build; a mismatch
|
||||||
|
fails the build record.
|
||||||
|
|
||||||
|
## Align ops-warden and railiance-platform references
|
||||||
|
|
||||||
|
```task
|
||||||
|
id: MASON-WP-0015-T05
|
||||||
|
status: todo
|
||||||
|
priority: medium
|
||||||
|
```
|
||||||
|
|
||||||
|
ops-warden `registry/routing/catalog.yaml` and
|
||||||
|
`wiki/playbooks/openbao-structure-survey.md` describe ops-mason as read-only.
|
||||||
|
The railiance-platform references are in
|
||||||
|
`docs/credential-lane-designs/application-backup-delivery-2026-10-08.md` and
|
||||||
|
RPF-WP-0053 and RPF-WP-0057. Message each owner with MASON-IN-0005, pointers
|
||||||
|
only. They edit their own files.
|
||||||
|
|
||||||
|
## Close the Kubernetes variant
|
||||||
|
|
||||||
|
```task
|
||||||
|
id: MASON-WP-0015-T06
|
||||||
|
status: todo
|
||||||
|
priority: medium
|
||||||
|
```
|
||||||
|
|
||||||
|
A Kubernetes role ops-mason builds can be logged in to by anyone holding a
|
||||||
|
token for the bound ServiceAccount. Confirm that the kube identity ops-mason
|
||||||
|
applies with cannot `create serviceaccounts/token` or Pods in namespaces it
|
||||||
|
binds roles to (`kubectl auth can-i`). `kubernetes_plane` already refuses
|
||||||
|
the Pod and Secret kinds. Record the result in INTENT.md's
|
||||||
|
`kubernetes-plane-apply` entry.
|
||||||
Loading…
Add table
Add a link
Reference in a new issue