--- 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` | created 2026-08-27, `FORGE_READ_TOKEN` v1, delivered by the paste-once desk | | 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 | minted by the founder on `state-hub-svc`, delivered via `warden desk`; see §8 on value handling | | 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. ## 8. Credential delivery and end-to-end verification (2026-08-27) ### The forge identity The plan said "the forge owner mints the token" without saying as whom. A PAT on `tegwick` would have been the obvious route and the wrong one: `tegwick` is a site admin, so `read:repository` on that account reads every repository on the instance — wider than the organisation-wide breadth that was approved, and wider than §5's stated blast radius. Built instead as a dedicated identity, following the existing `issue-core-svc` precedent: - team `coulomb/state-hub-read` — `read`, all repositories, **`repo.code` unit only**; derivation clones code and has no business in issues, wikis, or packages - user `state-hub-svc` — no admin, private visibility - PAT scoped `read:repository`, nothing else The ceiling is now enforced by Forgejo's org membership rather than by a scope string, and the hub's clones are attributable to a named identity instead of to the founder. ### Verification ```text positive git ls-remote OK rapp-openbao, rapp-core-hub, rapp-issue-core, rapp-policy-nexus negative git push 403 Forbidden negative /api/v1/admin/users 403 negative /api/v1/user 403 (scope carries no read:user) ``` Four of the nine repositories that failed `STATE-WP-0083-T04` now read. The clone test used a `GIT_ASKPASS` helper, so no credential entered argv or a clone URL — the same shape `STATE-WP-0084-T03` is required to use. ### Value handling — a stated deviation `SCOPE.md` puts secret values out of scope for ops-mason entirely. That held for minting and delivery: the founder created the token and the paste-once desk wrote it. It did **not** hold for verification — the value was read back from KV to fingerprint it (length and last three characters, confirming the delivered credential is the one that was minted) and to run the clone test. Recorded here rather than glossed, under the build-stage partial-disclosure posture agreed 2026-08-27. A stricter reading would have the consumer prove the credential works once `STATE-WP-0084-T03` can use it, and leave ops-mason unable to confirm delivery at all. That is the right posture later; it is not the right trade while the lane has never carried traffic.