ops-mason/policies/ops-mason-build.hcl
tegwick d020413d7a feat(mason): describe stored credentials, and stop the grant rewriting itself
custody-inventory.py walks operators/ and platform/workloads/, prints
each path's description, owner, consumers and recovery path, and marks
any missing them. Metadata only, never a value, so it runs under
ops-mason-build and can be handed to anyone orienting themselves. First
run: 21 paths, 17 undescribed.

Described the four this session touched, including on_loss — the field
whose absence meant the LLDAP predecessor's recovery path had to be
worked out from first principles while locked out.

ops-mason-build gains create/update on */metadata/*, since a description
is documentation rather than a value. delete stays absent: deleting a
metadata entry destroys every version of the secret beneath it.

It also now denies itself sys/policies/acl/ops-mason-build. Without that
the policy was advisory — a token that can write policies can delete its
own denials, so the claim that OpenBao enforces "never read a value" was
not true as written. An exact path outranks the glob, so changing what
ops-mason may do is now an operator act, visible as one in the audit log.

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

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 3377672@bnt-lap001
Assistant-Session: 15463ccf-238f-4e13-b163-93aa25c6d166
2026-08-28 11:43:08 +02:00

112 lines
4 KiB
HCL

# ops-mason-build — the authority ops-mason actually needs, and no more.
#
# SCOPE.md says ops-mason "never touches secret values, even transiently". Until
# now that was a promise in a document, kept by whoever was driving. This policy
# makes OpenBao enforce it: the builder can create policies, auth roles and KV
# path structure, and is denied every read of secret data on any mount.
#
# Granted through scripts/bao-session.sh as a 45-minute, named token, so an
# action in the audit log is attributable to a task rather than to whoever's
# operator session happened to be open.
# --- survey ----------------------------------------------------------------
# Phase 2 of the construction process is an existing-structure survey, and the
# plan for the state-hub lane was written without one because no session was
# available. That omission is why an AppRole was proposed on a cluster that
# already had kubernetes auth enabled.
path "sys/mounts" {
capabilities = ["read", "list"]
}
path "sys/auth" {
capabilities = ["read", "list"]
}
path "sys/policies/acl" {
capabilities = ["list"]
}
path "sys/policies/acl/*" {
capabilities = ["create", "read", "update", "list"]
}
# --- auth roles ------------------------------------------------------------
path "auth/kubernetes/role/*" {
capabilities = ["create", "read", "update", "list"]
}
path "auth/approle/role/*" {
capabilities = ["create", "read", "update", "list"]
}
# --- KV structure, never KV values -----------------------------------------
# Metadata carries versions, timestamps and custom_metadata — enough to confirm
# a path exists and that a paste-once delivery landed. It does not carry the
# value.
# create/update so a path can be *described* — custom_metadata is documentation,
# not a value. delete is absent: deleting a metadata entry destroys every
# version of the secret under it, which is a destructive act, not a build one.
path "platform/metadata/*" {
capabilities = ["create", "read", "update", "list"]
}
path "operators/metadata/*" {
capabilities = ["create", "read", "update", "list"]
}
# --- verification ----------------------------------------------------------
# Minting a short-lived test token and asking what it can reach is how a lane is
# proven correctly scoped, positively and negatively.
path "auth/token/create" {
capabilities = ["create", "update"]
}
path "auth/token/revoke" {
capabilities = ["update"]
}
path "auth/token/lookup-self" {
capabilities = ["read"]
}
path "sys/capabilities" {
capabilities = ["create", "update"]
}
path "sys/capabilities-self" {
capabilities = ["create", "update"]
}
# --- the grant cannot rewrite its own scope --------------------------------
# Without this, everything below is advisory: a token that can write policies
# can delete its own denials and then read anything. An exact path outranks the
# sys/policies/acl/* glob above, so this stanza wins.
#
# The consequence is deliberate — changing what ops-mason may do is an operator
# act, performed with an operator session, and visible as such in the audit log.
# It cannot be done quietly from inside a build.
path "sys/policies/acl/ops-mason-build" {
capabilities = ["read", "deny"]
}
# --- the line, stated as a denial ------------------------------------------
# Explicit deny outranks any grant, including one added here later by mistake.
# If ops-mason needs to prove a credential works, the consumer proves it, or a
# human does — see plans/state-hub-forge-derivation-read.md §8 for the one time
# this line was crossed and why it was recorded rather than glossed.
path "platform/data/*" {
capabilities = ["deny"]
}
path "operators/data/*" {
capabilities = ["deny"]
}
path "secret/data/*" {
capabilities = ["deny"]
}
# Mount management is deliberately absent. Enabling or tuning a secrets engine
# is a railiance-platform act; ops-mason builds inside mounts that already
# exist. A grant that needed sys/mounts/* would be a different, broader thing
# and should be recognised as such rather than folded in here.