ops-mason/plans/state-hub-forge-derivation-read.md
tegwick 1fdd8896f4 refactor(workplan): split MASON-WP-0003 to the lane ops-mason owns
Trim MASON-WP-0003 to T01-T03 — decide, build the AppRole/policy/KV path
structure, register the catalog entry — and route it through the
four-phase pipeline via plans/state-hub-forge-derivation-read.md, which
supplies the phase-3 executive summary the workplan was bypassing.

T02 no longer mints or holds the token value: SCOPE.md puts secret
values out of scope, so the forge owner mints and ops-warden's
paste_once_provision desk delivers.

Deployment plumbing, derive_from_forge(), and the fleet re-run move to
state-hub/STATE-WP-0084.

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-26 21:25:43 +02:00

174 lines
8.7 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: draft
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.