Policy workload-kv-read-state-hub-forge-derivation and Kubernetes auth role state-hub-forge-derivation, verified both directions through a 2-minute test token that was revoked after use: read on its own path; deny on forgejo-admin, on the shared llm-connect provider secrets, and on the parent metadata path. Kubernetes auth replaces the approved AppRole. The plan's §2 survey was written from disk with no OpenBao session and could not see that kubernetes/ auth is enabled on this cluster; the plan's own phase-4 instruction to re-verify against live state is what surfaced it. Ruled by the founder at phase 4. No static credential is created, so nothing has to be delivered into the cluster or rotated. The live survey also re-checked reuse: workload-kv-read-agent-harness- forgejo grants one repository deploy key, not organisation-wide read, so §2's rejection of reuse stands. ops-mason handled no secret value. The KV path does not exist until paste_once_provision writes the token. 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
272 lines
14 KiB
Markdown
272 lines
14 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` | **not yet created** — comes into existence when `paste_once_provision` writes the value; the policy grants read on it regardless |
|
|
| 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 | **not handled by ops-mason** — forge owner mints, `paste_once_provision` delivers |
|
|
| 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.
|