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
210 lines
11 KiB
Markdown
210 lines
11 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: approved
|
|
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/<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.
|