ops-mason/plans/state-hub-forge-derivation-read.md
tegwick 7018137c01 feat(mason): approve the forge read lane and draft its catalog entry
Plan approved 2026-08-26, breadth organisation-wide repository read. The
§4 contingency was satisfied first: STATE-WP-0084-T01 landed the same
day, so a missing grant now surfaces as a named unreadable condition
rather than as silence — which is what the breadth argument rested on.

T01 done. T03 drafted in ops-warden on a branch (entry + playbook +
regenerated high-risk artifact). T02 needs an operator: no valid OpenBao
session here, and the token is the forge owner's to mint.

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:58:02 +02:00

11 KiB

id demand_source consumer_repo credential_type status approved_by approved_at breadth created updated
state-hub-forge-derivation-read ops-mason/workplans/MASON-WP-0003 state-hub openbao-approle-kv approved Bernd Worsch 2026-08-26 organisation-wide-read 2026-08-26 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:

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.