feat(forge): resolve an optional forge read credential (STATE-WP-0084-T02/T03)
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Build and Publish Multi-Context Image / build-and-push (push) Successful in 27s

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:
tegwick 2026-08-27 23:12:57 +02:00
parent ab6438235e
commit 470ece82ed
8 changed files with 1334 additions and 7 deletions

View file

@ -18,11 +18,14 @@ spec:
checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}
labels: {{- include "statehub.labels" . | nindent 8 }}
spec:
{{- if .Values.serviceAccount.name }}
serviceAccountName: {{ .Values.serviceAccount.name | quote }}
{{- end }}
securityContext: {{- toYaml .Values.podSecurityContext | nindent 8 }}
{{- with .Values.imagePullSecrets }}
imagePullSecrets: {{- toYaml . | nindent 8 }}
{{- end }}
{{- if or .Values.sweep.enabled .Values.classificationAllowed.enabled }}
{{- if or .Values.sweep.enabled .Values.classificationAllowed.enabled .Values.forgeRead.enabled }}
volumes:
{{- if .Values.sweep.enabled }}
- name: sweep-repos
@ -39,6 +42,19 @@ spec:
configMap:
name: {{ .Values.classificationAllowed.name | quote }}
{{- end }}
{{- if .Values.forgeRead.enabled }}
# A projected token with an explicit audience, not the legacy
# auto-mounted one: the auto-mounted token has the API server as its
# audience, 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.
- name: openbao-token
projected:
sources:
- serviceAccountToken:
path: token
audience: {{ .Values.forgeRead.openbao.audience | quote }}
expirationSeconds: {{ .Values.forgeRead.openbao.expirationSeconds }}
{{- end }}
{{- end }}
containers:
- name: state-hub
@ -58,7 +74,7 @@ spec:
- -c
- git config --global --add safe.directory '*'
{{- end }}
{{- if or .Values.sweep.enabled .Values.classificationAllowed.enabled }}
{{- if or .Values.sweep.enabled .Values.classificationAllowed.enabled .Values.forgeRead.enabled }}
volumeMounts:
{{- if .Values.sweep.enabled }}
- name: sweep-repos
@ -72,6 +88,11 @@ spec:
mountPath: {{ .Values.classificationAllowed.mountPath | quote }}
readOnly: true
{{- end }}
{{- if .Values.forgeRead.enabled }}
- name: openbao-token
mountPath: /var/run/secrets/openbao
readOnly: true
{{- end }}
env:
{{- if .Values.sweep.enabled }}
- name: STATE_HUB_SWEEP_HOSTNAME
@ -83,6 +104,20 @@ spec:
- name: REPO_CLASSIFICATION_ALLOWED_PATH
value: {{ printf "%s/repo-classification.allowed.yaml" .Values.classificationAllowed.mountPath | quote }}
{{- end }}
{{- if .Values.forgeRead.enabled }}
# Coordinates only. The token itself is never a chart value, never
# in the image, and never in a Kubernetes Secret in this release.
- name: OPENBAO_ADDR
value: {{ .Values.forgeRead.openbao.addr | quote }}
- name: OPENBAO_K8S_ROLE
value: {{ .Values.forgeRead.openbao.role | quote }}
- name: OPENBAO_K8S_TOKEN_PATH
value: /var/run/secrets/openbao/token
- name: FORGE_READ_SECRET_PATH
value: {{ .Values.forgeRead.openbao.secretPath | quote }}
- name: FORGE_READ_SECRET_KEY
value: {{ .Values.forgeRead.openbao.secretKey | quote }}
{{- end }}
{{- end }}
envFrom:
{{- if .Values.config.enabled }}

View file

@ -0,0 +1,12 @@
{{- if .Values.serviceAccount.create }}
# STATE-WP-0084-T02. The OpenBao Kubernetes auth role built by MASON-WP-0003-T02
# binds to this ServiceAccount by name and deliberately does not bind to
# `default` — so the pod must stop running as `default` before the lane can
# carry anything. Creating it here keeps that binding in the release rather than
# in an operator's memory.
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ .Values.serviceAccount.name | quote }}
labels: {{- include "statehub.labels" . | nindent 4 }}
{{- end }}

View file

@ -30,9 +30,34 @@ config:
instanceRole: unknown
instanceLabel: ""
# STATE-WP-0084-T02. The OpenBao Kubernetes auth role binds to this name and
# not to `default`, so this is load-bearing, not cosmetic.
serviceAccount:
create: true
name: state-hub
secret:
name: state-hub-env
# Forge read credential for deriving private repositories (STATE-WP-0084,
# MASON-WP-0003). Disabled by default: a hub without it still derives every
# public repository, so this is added capability, not a prerequisite.
#
# There is no agent injector and no secrets-store CSI driver on this cluster, so
# the pod authenticates to OpenBao itself with its projected ServiceAccount
# token and reads the KV path. Nothing here is the credential; these are only
# coordinates. Rotating the token in OpenBao needs no chart change and no
# redeploy.
forgeRead:
enabled: false
openbao:
addr: ""
role: state-hub-forge-derivation
audience: openbao
expirationSeconds: 3600
secretPath: ""
secretKey: token
resources:
requests:
cpu: 250m