Files
homelab/kubernetes/infrastructure/openbao/rotation/claude-oauth.yaml
T
hermes-bot 6f1a7b6d96
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
fix(secret-rotator): never fail the HA run after the store; stage the image rollout
Folds the good parts of #1412 into this PR and addresses both review-bot
reviews (#1412 review 1789, #1414 review 1791):

- Everything after the OpenBao store is best-effort. Decode, reconnect,
  listing and revokes run in reap_superseded(); reap_summary() turns any
  error into "reap skipped" plus a warning, so rotate_ha_token returns Ok.
  A failed run would start a second pod (backoffLimit: 1) that keeps the
  token just stored and its successor and reaps the token consumers still
  hold until ESO syncs, i.e. the #1081 outage. Two unit tests cover it.
- Warn when the replaced token is not named "<client_name> [<6 hex>]":
  no later run reaps it, so it needs a manual revoke (from #1412).
- Staged image rollout (from #1412): only the three HA CronJobs carry the
  flux-system:secret-rotator marker; the other 17 rotators stay on
  2026-07-30 until a manual HA run validates the new binary, then a
  follow-up adds the marker to them. rotation/kustomization.yaml records
  the exception.
- Merged current main (a6f37e2b, 1691a0ac): the Reloader annotations for
  zone-configurator and speech-to-phrase are already there via #1409.
2026-10-21 07:59:52 +00:00

113 lines
4.7 KiB
YAML

---
# Sync the Claude subscription OAuth token (claude_code_oauth_token) into
# Woodpecker's ORG-level secret store. SYNC, NOT MINT: the token comes from an
# interactive `claude setup-token` (no headless mode), so the owner seeds the
# value into OpenBao at infrastructure/claude#setup-token by hand and
# refreshes it whenever they re-run setup-token; this CronJob only propagates
# whatever is stored there into the konairius org secret (create if absent, else
# patch value+events). All three agent pipelines
# (security-scan/update-audit/pr-review) inherit it.
#
# Runs daily rather than quarterly: a hand-refreshed value should reach
# Woodpecker within a day without waiting a quarter. It re-patches the same value
# on quiet days (idempotent, value not logged). To propagate an OAuth refresh
# immediately:
# kubectl -n openbao create job --from=cronjob/claude-oauth-secret-sync claude-oauth-sync-now
#
# woodpecker is reached in-cluster at woodpecker.woodpecker.svc:8000 (it moved
# out of `apps` in the namespace migration — see
# kubernetes/apps/woodpecker/MIGRATION-namespace.md). Needs the netpol
# halves woodpecker-allow-rotators (woodpecker ns) + rotator-claude-oauth-egress
# (openbao-ns.yaml). OpenBao policy/role rotator-claude-oauth + the bootstraps
# (woodpecker-bot token, the seeded OAuth value) are provisioned by hand — see
# policies/rotator-claude-oauth.hcl.
apiVersion: v1
kind: ServiceAccount
metadata:
name: claude-oauth-secret-rotator
namespace: openbao
labels:
app.kubernetes.io/name: claude-oauth
app.kubernetes.io/component: secret-rotator
---
apiVersion: batch/v1
kind: CronJob
metadata:
name: claude-oauth-secret-sync
namespace: openbao
labels:
app.kubernetes.io/name: claude-oauth
app.kubernetes.io/component: secret-rotator
spec:
# Live: image pinned to the build that carries `woodpecker-secret` mode, the
# OpenBao policy/role + bootstraps are applied, and BAO_PATH points at the
# seeded value. Runs daily at 04:20 to sync the OAuth value into the org secret.
suspend: false
schedule: "20 4 * * *" # daily, Europe/Berlin
timeZone: Europe/Berlin
concurrencyPolicy: Forbid
successfulJobsHistoryLimit: 2
failedJobsHistoryLimit: 5
startingDeadlineSeconds: 3600
jobTemplate:
spec:
# 4 (not the sibling rotators' 1-2) because this is the only DAILY
# rotator — every other CronJob here is quarterly, so it takes ~90x as
# many runs and is correspondingly more exposed to per-run transients.
# backoffLimit: 1 gave two attempts ~11 s apart, and on 2026-08-19 a
# sub-minute resolver transient took both out (Job
# claude-oauth-secret-sync-29785100, BackoffLimitExceeded), costing a
# day's sync. 4 spreads the attempts over ~150 s (10/20/40/80 s backoff),
# still well inside activeDeadlineSeconds.
backoffLimit: 4
activeDeadlineSeconds: 300
ttlSecondsAfterFinished: 259200 # 3 d
template:
metadata:
labels:
app.kubernetes.io/name: claude-oauth
app.kubernetes.io/component: secret-rotator
spec:
serviceAccountName: claude-oauth-secret-rotator
restartPolicy: Never
securityContext:
runAsNonRoot: true
runAsUser: 65534
runAsGroup: 65534
seccompProfile:
type: RuntimeDefault
containers:
- name: sync-woodpecker
image: gitea.renner.uno/gitea_admin/secret-rotator:2026-07-30
args: ["woodpecker-secret"]
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
env:
- name: BAO_ADDR
value: http://openbao.openbao.svc.cluster.local:8200
- name: BAO_ROLE
value: rotator-claude-oauth
- name: BOOTSTRAP_PATH
value: homelab/infrastructure/secret-rotators-bootstrap/woodpecker-bot
- name: BAO_PATH
value: homelab/infrastructure/claude
- name: BAO_FIELD
value: setup-token
- name: WOODPECKER_URL
value: http://woodpecker.woodpecker.svc.cluster.local:8000
- name: WOODPECKER_ORG
value: konairius
- name: WOODPECKER_SECRET_NAME
value: claude_code_oauth_token
- name: WOODPECKER_EVENTS
value: cron,deployment,manual,push,release,tag,pull_request
resources:
requests:
cpu: 25m
memory: 16Mi
limits:
memory: 48Mi