--- 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//` 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-` 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.