Minted on a dedicated identity rather than a site admin. A PAT on tegwick would have read every repository on the instance — wider than the approved organisation-wide breadth and wider than the plan's stated blast radius. Built as user state-hub-svc in team coulomb/state-hub-read (read, all repositories, repo.code unit only), following the existing issue-core-svc precedent. The ceiling is enforced by org membership, not by a scope string, and clones are attributable to a named identity. Delivered by warden desk --act paste_once_provision; ops-mason neither minted nor pasted the value. Verified: rapp-openbao, rapp-core-hub, rapp-issue-core and rapp-policy-nexus all read — four of the nine that failed STATE-WP-0083-T04. Push 403, admin API 403, /api/v1/user 403 (the scope carries no read:user). Clone test used GIT_ASKPASS, so no credential entered argv or a clone URL. Records a deviation rather than glossing it: SCOPE.md puts secret values out of scope, and that held for minting and delivery but not for verification — the value was read back to fingerprint it and to run the clone test, under the build-stage disclosure posture agreed 2026-08-27. 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
323 lines
16 KiB
Markdown
323 lines
16 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: 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/<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.
|
|
|
|
## 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.
|