ops-mason/plans/state-hub-forge-derivation-read.md
tegwick 84c36a6c26 feat(mason): build the state-hub forge read lane (MASON-WP-0003-T02)
Policy workload-kv-read-state-hub-forge-derivation and Kubernetes auth
role state-hub-forge-derivation, verified both directions through a
2-minute test token that was revoked after use: read on its own path;
deny on forgejo-admin, on the shared llm-connect provider secrets, and
on the parent metadata path.

Kubernetes auth replaces the approved AppRole. The plan's §2 survey was
written from disk with no OpenBao session and could not see that
kubernetes/ auth is enabled on this cluster; the plan's own phase-4
instruction to re-verify against live state is what surfaced it. Ruled
by the founder at phase 4. No static credential is created, so nothing
has to be delivered into the cluster or rotated.

The live survey also re-checked reuse: workload-kv-read-agent-harness-
forgejo grants one repository deploy key, not organisation-wide read, so
§2's rejection of reuse stands.

ops-mason handled no secret value. The KV path does not exist until
paste_once_provision writes the token.

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-27 22:49:13 +02:00

272 lines
14 KiB
Markdown

---
id: state-hub-forge-derivation-read
demand_source: ops-mason/workplans/MASON-WP-0003
consumer_repo: state-hub
credential_type: openbao-approle-kv
status: built
approved_by: "Bernd Worsch"
approved_at: "2026-08-26"
breadth: organisation-wide-read
created: "2026-08-26"
updated: "2026-08-26"
---
# Construction plan: State Hub forge-derivation read lane
## 1. Demand
`CUST-ADR-012` decision 1 makes the forge the projection source: central
clones a repository's default branch and derives its work records from what
Forgejo holds. The `state-hub` pod does this anonymously today, which works
for public repositories and fails outright for private ones.
The first fleet-wide reset (`state-hub/STATE-WP-0083-T04`, 2026-08-26)
covered 121 repositories. Nine failed identically:
```text
fatal: could not read Username for 'https://forgejo.coulomb.social'
```
`rapp-core-hub`, `rapp-issue-core`, `rapp-openbao`, `rapp-policy-nexus` and
five others are invisible to derivation entirely.
The demand is therefore: a **read-only** credential the `state-hub` workload
can authenticate for non-interactively (no operator present — derivation runs
unattended in-cluster), sufficient for `git clone --depth 1` against private
repositories and nothing more.
## 2. Existing-structure survey
**Checked:** `ops-warden/registry/routing/catalog.yaml` (on disk).
**Not checked:** live `bao policy list` / `bao auth list` — no valid OpenBao
token in this session. Whoever executes phase 4 must re-verify against live
state before applying.
`warden route find "state hub read private repository"` returns **no lane**.
**Nearest existing entry: `forgejo-admin-api-token`**
(`ops-warden/registry/routing/catalog.yaml:577`) — a site-admin PAT owned by
`railiance-platform`, stored at `platform/workloads/forgejo/forgejo-admin`
(field `API_TOKEN`), gated by policy `workload-kv-read-forgejo-admin`, marked
`risk: high`, attended-minted, currently under interim delegation to
`secrets-engine` (`SECRETS-WP-0006`).
**Reuse considered and declined.** Binding a `state-hub` AppRole to
`workload-kv-read-forgejo-admin` is mechanically possible and is the wrong
build: derivation performs a shallow clone and nothing else, and a projection
that only reads must not hold an identity that can administer the forge. It
would also make an unattended in-cluster workload a standing consumer of a
lane that is explicitly attended-minted and mid-delegation. No compaction
opportunity exists in the other direction either — the admin lane keeps its
own consumers unchanged.
No second existing AppRole or policy is scoped to a Forgejo *read* credential.
This is genuinely new structure.
## 3. Proposed changes
| # | Action | Object | Reuse-vs-new rationale |
|---|---|---|---|
| 1 | create | KV v2 secret path `platform/workloads/state-hub/forge-derivation` (field `FORGE_READ_TOKEN`) | New, holds one credential and nothing else — mirrors the `platform/workloads/<workload>/<purpose>` shape already used by `platform/workloads/forgejo/forgejo-admin` |
| 2 | create | Policy `workload-kv-read-state-hub-forge-derivation` — read-only, scoped to exactly path #1 | New — same `workload-kv-read-<path-slug>` convention as `workload-kv-read-forgejo-admin`, scoped to one path |
| 3 | create | AppRole `state-hub-forge-derivation`, bound to policy #2 | New — unattended in-cluster login; TTLs per §4 |
| 4 | deliver | The Forgejo token value into path #1 via ops-warden's `paste_once_provision` desk | ops-mason never touches the value (`SCOPE.md`); the forge owner mints, the desk delivers |
| 5 | propose | ops-warden catalog entry `state-hub-forge-derivation-read` — pointer-only, `warden_executes: false`, `status: draft` | New — no existing entry covers this need; `forgejo-admin-api-token` stays as-is, untouched |
No tear-down. Nothing existing becomes redundant.
**Not in this plan** (other repos' work, tracked as `state-hub/STATE-WP-0084`):
mounting the credential into the `state-hub` deployment, teaching
`derive_from_forge()` to use it, and re-running the fleet reset.
## 4. Review notes (phase 2)
- **Naming check:** path, policy, and AppRole names all follow the
conventions visible in the existing catalog. No divergence proposed.
- **TTL/scoping check:** short-lived tokens (`token_ttl=15m`,
`token_max_ttl=30m`) matching the workload AppRole shape already in use.
Derivation fetches once per pass, so a bounded token costs nothing.
`secret_id_ttl` per current build-phase posture
(`ops-warden/wiki/WorkloadSecurityPosture.md`) — no rotation schedule
beyond what posture requires. **Explicit assumption, flagged in §5.**
- **Redundancy check:** no existing policy or AppRole grants Forgejo read.
- **Compaction opportunity:** none identified.
- **Ease of use for the consumer:** the pod already consumes `state-hub-env`;
the credential arrives the same way. Rotation must not require a chart
change or a redeploy — that constraint belongs to `STATE-WP-0084`, but it
is a consequence of this plan's delivery shape and is stated here so the
two do not drift.
- **Credential handling constraint (carried to the consumer):** the token
must never reach the clone URL as embedded userinfo, where it lands in logs
and process listings. Use a credential helper or an HTTP header.
### The breadth question, argued both ways
**Organisation-wide read.** New private repositories derive without further
work, and no one has to remember this lane exists.
**Per-repository read.** Tighter, but needs maintenance each time a
repository is added.
The original argument for organisation-wide read was that the narrow option
fails *invisibly* — a forgotten grant looks exactly like a broken repository.
That argument does not survive `STATE-WP-0084-T01`, which makes "cannot read"
report as unreadable rather than as an error or, worse, as an empty
derivation. Once a missing grant surfaces as a named condition, the narrow
option's failure mode is visible and recoverable.
What remains is a straight blast-radius trade: an organisation-wide read
token, if it leaks, reads **every private repository in the organisation**
source, history, and whatever those repositories contain. A per-repository
grant leaks nine repositories' worth.
**Recommendation: organisation-wide read, contingent on `STATE-WP-0084-T01`
landing first.** The maintenance burden of the narrow option is real and
recurring, and the visibility objection to it is genuine until T01 exists.
But this is the founder's call, not a mechanical one — §5 puts it as an
explicit decision, not a default.
## 5. Executive summary (phase 3)
**One-line ask:** create a read-only Forgejo credential lane so the State Hub
can derive projections for private repositories, which it cannot read today.
**Who/what gets access:** a new OpenBao AppRole `state-hub-forge-derivation`,
used by the `state-hub` workload in-cluster. Nothing else — no existing role,
human, or workload gains anything new.
**To what:** read-only access to exactly one KV path,
`platform/workloads/state-hub/forge-derivation`, which does not exist today
and will hold one Forgejo token. The AppRole cannot read the existing
`forgejo-admin` path or any other path.
The Forgejo token itself grants **repository read only** — no push, no
administration, no other Forgejo resource.
**For how long:** each login is a 15-minute token, renewable to a 30-minute
cap. The AppRole credential itself does not expire on a schedule at current
build-phase posture — the same choice already made for existing workload
AppRoles. Override this if you want rotation now.
**Blast radius if the credential leaks:** whoever holds the
`role_id`/`secret_id` pair can mint a short-lived token that reads one
Forgejo read token — nothing else in OpenBao. What that Forgejo token then
reads is the open decision:
- **organisation-wide read (recommended):** every private repository in the
organisation is readable. No write, no admin, no persistence.
- **per-repository read:** the nine listed repositories are readable, and
every future private repository needs a grant added by hand.
**Cost to reverse:** revoke the Forgejo token, delete one AppRole, one
policy, one KV path — four commands, no other lane or consumer affected. The
State Hub returns to deriving public repositories only, which is what it does
today.
**Sequencing note:** the recommendation above assumes `STATE-WP-0084-T01`
(report unreadable repositories as unreadable, never as retirement work)
lands first. That task is independently worth doing — today a clone that
returned empty instead of failing would propose every record in that
repository for retirement, the near-miss that `vergabe-teilnahme` produced.
**Decision needed:**
1. Approve the lane as proposed, reject, or send back to phase 1.
2. Breadth: organisation-wide read, or per-repository read.
3. Rotation: accept build-phase posture (no scheduled `secret_id` expiry), or
require rotation now.
## 6. Decision (phase 3 outcome)
**Approved 2026-08-26 by Bernd Worsch. Breadth: organisation-wide repository
read.**
The reasoning, recorded because a bare value would not survive the next person
asking why:
- **Approved as proposed.** No change to the path, policy, or AppRole shape in
§3, and no change to the rejection of `forgejo-admin-api-token` reuse in §2.
- **Organisation-wide read over per-repository.** The recurring maintenance
cost of the narrow option is real and lands on whoever adds a private
repository months from now, who has no reason to know this lane exists. The
accepted price is the wider leak radius: a leaked token reads every private
repository in the organisation — read only, no write, no administration, no
persistence.
- **The contingency in §4 is satisfied.** The recommendation was conditional on
`STATE-WP-0084-T01` landing first, so a missing grant would surface as a
named condition rather than as silence. It landed on 2026-08-26 (state-hub
`85181cd`): an unreadable repository now reports as unreadable, and a source
that produced no records cannot retire anything even when retirement is
acknowledged.
- **Rotation: not separately decided; build-phase posture stands** as proposed
— no scheduled `secret_id` expiry, matching the existing workload AppRoles.
This is the §5 default rather than an explicit ruling, and it is the thing to
revisit first when posture changes.
**What approval authorises.** Phase 4 for `MASON-WP-0003-T02` and `T03` only:
the OpenBao policy, the AppRole, the KV path structure, and the pointer-only
ops-warden catalog entry. It does not authorise ops-mason to mint or handle the
token value — the forge owner mints it with organisation-wide repository read
and no other scope, and `paste_once_provision` delivers it.
## 7. Build result (phase 4)
Built 2026-08-27 against a live `platform-root` session. Recorded in
`audit/build-log.jsonl`.
| # | Object | State |
|---|---|---|
| 1 | KV path `platform/workloads/state-hub/forge-derivation` | **not yet created** — comes into existence when `paste_once_provision` writes the value; the policy grants read on it regardless |
| 2 | Policy `workload-kv-read-state-hub-forge-derivation` | created, read on the data and metadata paths only |
| 3 | Kubernetes auth role `state-hub-forge-derivation` | created, bound to ServiceAccount `state-hub/state-hub`, `token_ttl=15m`, `token_max_ttl=30m` |
| 4 | Token value | **not handled by ops-mason** — forge owner mints, `paste_once_provision` delivers |
| 5 | ops-warden catalog entry | drafted on branch `mason/state-hub-forge-derivation-read`, `status: draft` |
### Deviation from the approved plan: Kubernetes auth, not AppRole
§2's survey was written from disk with no OpenBao session, and did not know that
`kubernetes/` auth is enabled on this same cluster (`https://10.43.0.1:443`)
with four `external-secrets-*` roles already using it. The plan's own phase-4
instruction — re-verify against live state before applying — is what surfaced it.
Ruled by the founder at phase 4 on 2026-08-27: **Kubernetes auth**. The pod
authenticates with its own projected ServiceAccount token, so no `role_id`
/`secret_id` pair exists to deliver into the cluster, store, or rotate. That
directly satisfies `STATE-WP-0084-T02`'s constraints — nothing baked into the
image, rotation without a redeploy — and removes a static credential of exactly
the kind whose custody gap cost a full session on the LLDAP side the previous
day.
### Verification
Positive and negative checked with `bao token capabilities` through a 2-minute
test token, revoked immediately after:
```text
platform/data/workloads/state-hub/forge-derivation read
platform/data/workloads/forgejo/forgejo-admin deny
platform/data/workloads/activity-core/llm-connect/...provider-secrets deny
platform/metadata/workloads/state-hub deny
```
The negative on `forgejo-admin` is the one that matters: not being able to do
what the admin lane can is the entire argument for building this lane rather
than reusing it.
### Reuse re-checked against live state
`bao policy list` shows one Forgejo-adjacent workload policy that §2 could not
see: `workload-kv-read-agent-harness-forgejo`. Read, and confirmed not
reusable — it grants a single agent-harness **deploy key** for one repository,
not organisation-wide read for a different consumer. §2's conclusion stands.
### Prerequisites before the lane carries traffic
1. ServiceAccount `state-hub` in namespace `state-hub` — the deployment
currently runs as `default`, and the role deliberately does not bind to
`default`, which would grant the lane to every pod in the namespace.
`STATE-WP-0084-T02`.
2. The Forgejo token, minted at organisation-wide repository read and delivered
to the KV path via `paste_once_provision`.
Until both are done the lane is correctly scoped and inert.