diff --git a/plans/state-hub-forge-derivation-read.md b/plans/state-hub-forge-derivation-read.md new file mode 100644 index 0000000..e428315 --- /dev/null +++ b/plans/state-hub-forge-derivation-read.md @@ -0,0 +1,174 @@ +--- +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//` 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. diff --git a/workplans/MASON-WP-0003-state-hub-forge-read-lane.md b/workplans/MASON-WP-0003-state-hub-forge-read-lane.md index f026993..56bb81a 100644 --- a/workplans/MASON-WP-0003-state-hub-forge-read-lane.md +++ b/workplans/MASON-WP-0003-state-hub-forge-read-lane.md @@ -12,6 +12,7 @@ updated: "2026-08-26" related: - CUST-ADR-012 - STATE-WP-0083 + - STATE-WP-0084 state_hub_workstream_id: "4015b46d-02f1-56ac-853e-87e8542cf0dd" --- @@ -19,9 +20,11 @@ state_hub_workstream_id: "4015b46d-02f1-56ac-853e-87e8542cf0dd" ## Goal -Give the State Hub read-only access to Forgejo so it can derive projections for -private repositories, and register the lane so `warden route find` answers this -question the next time someone asks it. +Build the credential lane that lets the State Hub read private repositories +from Forgejo, and register it so `warden route find` answers this question the +next time someone asks it. + +This workplan builds the lane. It does not consume it — see the handoff below. ## Why this exists @@ -42,22 +45,36 @@ others are invisible to the hub's derivation entirely. This is a limit on the ADR's own premise. *The forge is the projection source* holds only for repositories central can read, and nothing in the current design -says so. Worse, the failure is indistinguishable from a broken repository: the -pass records an error, not a policy, so "cannot read" and "does not exist" look -the same from the outside. +says so. ## Why ops-mason owns it `warden route find` returns no lane for this need. The nearest entry is -`forgejo-admin-api-token` — an operator PAT owned by `railiance-platform` — which -is far more authority than derivation requires and the wrong thing to reuse. +`forgejo-admin-api-token` — an operator PAT owned by `railiance-platform` — +which is far more authority than derivation requires and the wrong thing to +reuse. This repository builds "OpenBao AppRoles, policies, and KV secret paths so ops-warden always has something real to route to", which is exactly what is missing. ops-warden issues SSH certificates only and routes everything else; a Forgejo token is not its to vend. -## Decide the token's scope and breadth +## What ops-mason does not own + +Three parts of the end-to-end fix belong to other repositories and are tracked +in `state-hub/STATE-WP-0084`: + +- mounting the credential into the `state-hub` deployment; +- teaching `derive_from_forge()` to use it, and to distinguish "cannot read" + from "does not exist"; +- re-running the fleet reset and reviewing the nine first diffs. + +`STATE-WP-0084-T01` (the unreadable-vs-missing distinction) is not merely +downstream: the recommendation in this lane's construction plan depends on it, +because it is what makes a narrow grant fail visibly. It is worth landing with +or without this credential. + +## Run the construction plan through the decision gate ```task id: MASON-WP-0003-T01 @@ -66,129 +83,76 @@ priority: high state_hub_task_id: "36d26000-ae71-5a58-a563-a07ff5be664f" ``` -Two choices to settle before anything is created, because they determine what -the credential can do if it leaks. +`plans/state-hub-forge-derivation-read.md` holds phases 1–3: the demand, the +existing-structure survey (including why `forgejo-admin-api-token` is rejected +rather than reused), the proposed AppRole/policy/KV path, the review pass, and +the executive summary. -**Permission.** Derivation performs `git clone --depth 1` and nothing else, so -the token needs repository *read* and no other scope. It must not be able to -push, administer, or read other Forgejo resources. Reusing the existing admin PAT -is explicitly rejected: a projection that only reads should not hold an -identity that can write. +Three things need deciding, and they determine what the credential can do if it +leaks: -**Breadth.** Organisation-wide read means new private repositories derive without -further work and no one has to remember this lane exists. A per-repository grant -is tighter but needs maintenance every time a repository is added, and a -forgotten grant reappears as the same silent "cannot read" this workplan exists -to remove. Recommend organisation-wide read, on the grounds that the failure mode -of the narrow option is invisible. +1. **Approve, reject, or send back to phase 1.** +2. **Breadth** — organisation-wide repository read, or per-repository. The plan + recommends organisation-wide *contingent on `STATE-WP-0084-T01`*, and argues + the counter-case: an organisation-wide token that leaks reads every private + repository in the organisation. +3. **Rotation** — accept build-phase posture (no scheduled `secret_id` expiry), + or require rotation now. -Acceptance: scope and breadth recorded as a decision with the reasoning, not -just a value. +Permission is not in question: derivation performs `git clone --depth 1` and +nothing else, so the token gets repository *read* and no other scope. -## Create the AppRole, policy, and KV path +Acceptance: the plan reaches `status: reviewed`, the founder decision is +recorded with its reasoning rather than as a bare value, and the plan carries +`approved_by`/`approved_at`. + +## Build the AppRole, policy, and KV path structure ```task id: MASON-WP-0003-T02 -status: todo +status: wait priority: high state_hub_task_id: "68a1c8e4-c12b-550e-962c-06a90ab8c1c2" ``` -Create the read-only Forgejo token, an OpenBao policy granting read on exactly -one KV path, and an AppRole for the State Hub workload to authenticate with. +Phase 4, after T01 approval only. Create the OpenBao policy granting read on +exactly one KV path, the KV path *structure*, and the AppRole the `state-hub` +workload authenticates with. -The token value never appears in Git, State Hub, a workplan, a log, or a chat -message. This task records *where* the credential lives and *who* may read it — -never what it is. +**ops-mason does not touch the token value, even transiently.** The forge owner +mints the read-only Forgejo token; it reaches the KV path through ops-warden's +existing `paste_once_provision` desk. This task records *where* the credential +lives and *who* may read it — never what it is. The value never appears in Git, +State Hub, a workplan, a log, or a chat message. -Operator-executed. This workplan describes the lane; it does not perform the -creation. - -Acceptance: the KV path exists and holds a working read-only token; the policy -grants read on that path alone; the AppRole authenticates and cannot read -anything else. +Acceptance: the policy grants read on that path alone; the AppRole +authenticates and can read nothing else; the KV path exists and the desk has +delivered a working token into it, confirmed by the consumer rather than by +reading the value here. Recorded in `audit/build-log.jsonl` against the +approved plan. ## Register the lane in the routing catalog ```task id: MASON-WP-0003-T03 -status: todo +status: wait priority: high state_hub_task_id: "e14f0eb3-a920-5623-b4c1-c20858dbb45d" ``` -Add an entry so `warden route find "state hub read private repository"` resolves. -Its absence is why this had to be reasoned out from first principles rather than -looked up. +Propose a pointer-only entry (`warden_executes: false`, no authored `steps`, +`status: draft`) so `warden route find "state hub read private repository"` +resolves. Its absence is why this had to be reasoned out from first principles +rather than looked up. -The entry should name the owner, the subsystem, the KV path, the rotation -method, and state plainly that ops-warden routes but does not execute it. +The entry names the owner, the subsystem, the KV path, the rotation method, and +states plainly that ops-warden routes but does not execute it. Normal git +contribution to `ops-warden`, not a live API call. Acceptance: `warden route find` returns the lane; `warden route show` gives an operator enough to act without reading this workplan. -## Deliver the credential to the pod +## Handoff -```task -id: MASON-WP-0003-T04 -status: todo -priority: medium -state_hub_task_id: "1b802564-84aa-5934-8923-51b2ad9dd607" -``` - -Make the token available to the `state-hub` deployment as a Kubernetes Secret, -mounted or injected, and referenced by the chart the same way `state-hub-env` -already is. - -It must not be baked into the image, and it must not be committed to the chart. -Rotation must not require a chart change. - -Acceptance: the pod can read the credential; nothing in the repository contains -it; rotating the token does not require a redeploy. - -## Teach the derivation to use it - -```task -id: MASON-WP-0003-T05 -status: todo -priority: medium -state_hub_task_id: "4eeb9744-0d2f-57d8-a063-0e4ed371ac63" -``` - -`derive_from_forge()` in `state-hub` clones anonymously. It should use the -credential when present and continue working without it — a hub with no token -must still derive public repositories rather than failing wholesale. - -Credentials must never reach the clone URL in a way that lands in logs or process -listings; use a credential helper or header, not an embedded userinfo URL. - -**Distinguish "cannot read" from "does not exist".** A repository the hub is not -permitted to read must be reported as unreadable, not as a repository whose -records no longer derive. Today the reset treats a failed clone as an error, -which is correct only by accident: if a clone ever returned empty instead of -failing, every record in that repository would be proposed for retirement. That -is the failure that nearly retired `vergabe-teilnahme`, and it must be impossible -here by construction rather than by luck. - -Acceptance: private repositories derive; a hub without the credential still -derives public ones; an unreadable repository is reported as unreadable and never -as retirement work. - -## Confirm the nine - -```task -id: MASON-WP-0003-T06 -status: wait -priority: medium -state_hub_task_id: "cbc9c02c-e49a-54c3-91e5-51df238139a6" -``` - -Re-run the fleet reset and confirm the nine repositories derive rather than -error. Expect them to arrive with a real diff — quite possibly a large one, since -none has ever been reconciled against its files. - -Treat the first diff for each as review material, not as work to apply. Every -repository examined this way for the first time has produced findings. - -Acceptance: fleet errors fall from nine to zero; each newly readable repository -has its first diff reviewed before anything is applied. +Once T02 and T03 are done, notify `state-hub` so `STATE-WP-0084-T02` can +unblock. `STATE-WP-0084-T01` does not wait on this workplan.