feat(forge): resolve an optional forge read credential (STATE-WP-0084-T02/T03)
Nine repositories are invisible to derivation because central may not read them. This adds the consuming half of the credential lane MASON-WP-0003 built. The cluster has no agent injector and no secrets-store CSI driver, so the pod authenticates to OpenBao with a projected ServiceAccount token (audience `openbao`, not the API server) and reads the KV path itself. `forgeRead.*` carries coordinates only; no credential is a chart value, an image layer, or a Kubernetes Secret. The credential reaches git through GIT_CONFIG_* setting http.extraHeader, not through `-c` and not through userinfo in the clone URL — both of those put the token in the process listing. It is redacted from ForgeDeriveError, which is logged, stored in reset outcomes, and returned over the API. Absent stays a supported state: with no credential, or with OpenBao unreachable, resolution returns None and public derivation runs unchanged. Raising would turn "nine repositories are unreadable" into "the pass failed", which is what T01 exists to prevent. Chart default is disabled. 717 pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Assistant: claude-code Assistant-Model: opus Assistant-Process: 2583210@bnt-lap001 Assistant-Session: f2bff2d5-e9b2-4338-92ca-10282a927006
This commit is contained in:
parent
ab6438235e
commit
470ece82ed
8 changed files with 1334 additions and 7 deletions
|
|
@ -112,7 +112,7 @@ covered by tests that fail if the retirement path is reachable from either.
|
|||
|
||||
```task
|
||||
id: STATE-WP-0084-T02
|
||||
status: wait
|
||||
status: progress
|
||||
priority: medium
|
||||
state_hub_task_id: "d8d41a89-1ebc-5118-bb73-cf8d8c114e16"
|
||||
```
|
||||
|
|
@ -129,11 +129,37 @@ Rotation must not require a chart change or a redeploy.
|
|||
Acceptance: the pod can read the credential; nothing in the repository contains
|
||||
it; rotating the token does not require a redeploy.
|
||||
|
||||
**Chart landed 2026-08-27; awaits the minted token.** The cluster has no OpenBao
|
||||
agent injector and no secrets-store CSI driver — checked, not assumed — so
|
||||
there is nothing to inject with. The pod authenticates to OpenBao itself, which
|
||||
is what `MASON-WP-0003-T02`'s Kubernetes auth role was built for.
|
||||
|
||||
- `templates/serviceaccount.yaml` creates ServiceAccount `state-hub`, and the
|
||||
Deployment now sets `serviceAccountName`. The auth role binds to this name and
|
||||
deliberately not to `default`, so until this ships the pod cannot authenticate
|
||||
at all. **This changes the identity the running pod uses** — expect a pod
|
||||
restart on upgrade.
|
||||
- The OpenBao token is a *projected* ServiceAccount token with audience
|
||||
`openbao`, not the legacy auto-mounted one. The auto-mounted token's audience
|
||||
is the API server, so a copy of it is a credential for the cluster; this one
|
||||
is only accepted by OpenBao, and the kubelet rotates it in place.
|
||||
- `forgeRead.*` carries coordinates only — address, role, KV path, key. No
|
||||
credential is a chart value, an image layer, or a Kubernetes Secret in this
|
||||
release. Rotating the token in OpenBao needs no chart change and no redeploy:
|
||||
the value is re-read every 5 minutes.
|
||||
- Default `forgeRead.enabled: false`. A hub without the credential still derives
|
||||
every public repository, so this is added capability, not a prerequisite.
|
||||
|
||||
Remaining before this is `done`: the minted read-only Forgejo token in the KV
|
||||
path (`paste_once_provision`, outside both repositories), and a deploy with
|
||||
`forgeRead.enabled=true` plus the real address and path in
|
||||
`deploy/railiance/apps/helm/state-hub-values.yaml`.
|
||||
|
||||
## Teach the derivation to use it
|
||||
|
||||
```task
|
||||
id: STATE-WP-0084-T03
|
||||
status: wait
|
||||
status: progress
|
||||
priority: medium
|
||||
state_hub_task_id: "b22b24d9-7ed3-533c-bda7-3130693cf4d2"
|
||||
```
|
||||
|
|
@ -152,6 +178,43 @@ Acceptance: private repositories derive; a hub without the credential still
|
|||
derives public ones; no credential appears in logs, process listings, or
|
||||
recorded clone URLs.
|
||||
|
||||
**Code landed 2026-08-27**, ahead of T02 — writing the consumer does not need
|
||||
the secret to exist. `api/services/forge_projection.py`:
|
||||
|
||||
- `forge_read_token()` reads `FORGE_READ_TOKEN_FILE` in preference to
|
||||
`FORGE_READ_TOKEN`. A mounted file is what lets T02 satisfy "rotation must
|
||||
not require a redeploy"; the environment cannot be rotated in place. An
|
||||
unreadable token *file* returns `None` rather than falling back to the
|
||||
environment — falling back would let a broken mount look like success while
|
||||
quietly using a stale value.
|
||||
- Absent is a supported state, not a degraded one: with no credential the git
|
||||
environment is untouched and public derivation runs exactly as before.
|
||||
- The credential is passed as `GIT_CONFIG_COUNT`/`GIT_CONFIG_KEY_0`/
|
||||
`GIT_CONFIG_VALUE_0` setting `http.extraHeader`, not as `-c` on the command
|
||||
line and not as userinfo in the URL. Both alternatives put the token in the
|
||||
process listing, where anything that can run `ps` reads it.
|
||||
- `_run_git` redacts the token from `ForgeDeriveError` before raising, because
|
||||
that message is logged, stored in a reset outcome, and returned over the API.
|
||||
- `api/services/forge_credential.py` resolves the credential from a mounted
|
||||
file, an environment variable, or OpenBao via Kubernetes auth, in that order.
|
||||
Production uses the third; the first two make the code runnable and testable
|
||||
outside the cluster. Configuring a file means *that* file and nothing else —
|
||||
no fall-through, so a broken mount cannot silently resolve to a stale value.
|
||||
- Every failure resolves to `None`, never an exception: no configuration, no
|
||||
network, OpenBao down, permission denied. Raising would convert "nine
|
||||
repositories are unreadable" into "the whole pass failed", which is the
|
||||
outcome T01 exists to prevent.
|
||||
- The resolved value is cached for 5 minutes — a fleet reset of 121
|
||||
repositories must not authenticate 121 times, and a rotated token must still
|
||||
be picked up without a redeploy.
|
||||
|
||||
Nine tests in `TestForgeCredential` cover each of those, including the two that
|
||||
are silent when wrong: token-in-argv and token-in-exception. 43 pass in
|
||||
`tests/test_forge_projection.py`; 717 pass across the suite.
|
||||
|
||||
Remains `progress` because the acceptance clause "private repositories derive"
|
||||
cannot be observed until T02 supplies a credential; T04 confirms it.
|
||||
|
||||
## Confirm the nine
|
||||
|
||||
```task
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue