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.
223 lines
8.2 KiB
YAML
223 lines
8.2 KiB
YAML
---
|
|
# Class-B automated rotation of the Home Assistant long-lived access tokens.
|
|
#
|
|
# Three separate LLATs, three CronJobs sharing one binary, one policy, one SA:
|
|
# prometheus-token -> Prometheus ServiceMonitor bearer token; also env of
|
|
# zone-configurator + speech-to-phrase (Reloader-annotated)
|
|
# config-sync-token -> the config-sync sidecar's HA reload calls
|
|
# shelly-manager-llat -> shelly-manager's HA config-flow/device-registry API
|
|
#
|
|
# HA mints/revokes LLATs ONLY over its WebSocket API (no REST equivalent), so
|
|
# these run the secret-rotator binary (ha-token mode). It mints under a UNIQUE
|
|
# name (base + random tag): HA refuses a second token with an existing
|
|
# client_name, and make-before-break runs while the outgoing token is still
|
|
# present. The outgoing token is identified by its JWT `iss`, never by name, so
|
|
# the tag is free. Make-before-break: read -> mint -> store -> reap; a failed
|
|
# store leaves the old token valid and stored (a no-op, not an outage). The
|
|
# reap keeps the new AND the replaced token, because consumers read the k8s
|
|
# Secret that ESO syncs only every 1h; the replaced one goes next quarter. Reap
|
|
# errors only warn: a Job retry would revoke the token consumers still hold.
|
|
#
|
|
# Reachability: HA :8123 (HA itself, not the :8124 XFF-anchor proxy) via the
|
|
# home-assistant-bgp Service ClusterIP. Needs BOTH netpol halves —
|
|
# home-assistant-rotator-ingress (home-assistant ns) and
|
|
# rotator-home-assistant-egress (openbao ns) — because the openbao default-deny
|
|
# only permits TCP egress to openbao/kube-dns/apiserver/world. The public name
|
|
# is unusable from a pod (ingress-LB hairpin).
|
|
apiVersion: v1
|
|
kind: ServiceAccount
|
|
metadata:
|
|
name: home-assistant-secret-rotator
|
|
namespace: openbao
|
|
labels:
|
|
app.kubernetes.io/name: home-assistant
|
|
app.kubernetes.io/component: secret-rotator
|
|
# OpenBao: auth/kubernetes/role/rotator-home-assistant binds this SA to
|
|
# rotator-home-assistant (read+patch the three HA token paths; no bootstrap).
|
|
---
|
|
apiVersion: batch/v1
|
|
kind: CronJob
|
|
metadata:
|
|
name: prometheus-token-secret-rotate
|
|
namespace: openbao
|
|
labels:
|
|
app.kubernetes.io/name: home-assistant
|
|
app.kubernetes.io/component: secret-rotator
|
|
spec:
|
|
suspend: false # tested end-to-end
|
|
schedule: "0 4 1 1,4,7,10 *" # quarterly, Europe/Berlin (offsets avoid HA restart pile-up)
|
|
timeZone: Europe/Berlin
|
|
concurrencyPolicy: Forbid
|
|
successfulJobsHistoryLimit: 2
|
|
failedJobsHistoryLimit: 5
|
|
startingDeadlineSeconds: 3600
|
|
jobTemplate:
|
|
spec:
|
|
backoffLimit: 1
|
|
activeDeadlineSeconds: 300
|
|
ttlSecondsAfterFinished: 259200 # 3 d
|
|
template:
|
|
metadata:
|
|
labels:
|
|
app.kubernetes.io/name: home-assistant
|
|
app.kubernetes.io/component: secret-rotator
|
|
spec:
|
|
serviceAccountName: home-assistant-secret-rotator
|
|
restartPolicy: Never
|
|
securityContext:
|
|
runAsNonRoot: true
|
|
runAsUser: 65534
|
|
runAsGroup: 65534
|
|
seccompProfile:
|
|
type: RuntimeDefault
|
|
containers:
|
|
- name: rotate
|
|
image: gitea.renner.uno/gitea_admin/secret-rotator:2026-07-30 # {"$imagepolicy": "flux-system:secret-rotator"}
|
|
args: ["ha-token"]
|
|
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-home-assistant
|
|
- name: BAO_PATH
|
|
value: homelab/apps/home-assistant/prometheus-token
|
|
- name: HA_URL
|
|
value: ws://home-assistant-bgp.home-assistant.svc.cluster.local:8123/api/websocket
|
|
- name: HA_CLIENT_NAME
|
|
value: "ServiceMonitor"
|
|
resources:
|
|
requests:
|
|
cpu: 25m
|
|
memory: 16Mi
|
|
limits:
|
|
memory: 48Mi
|
|
---
|
|
apiVersion: batch/v1
|
|
kind: CronJob
|
|
metadata:
|
|
name: config-sync-token-secret-rotate
|
|
namespace: openbao
|
|
labels:
|
|
app.kubernetes.io/name: home-assistant
|
|
app.kubernetes.io/component: secret-rotator
|
|
spec:
|
|
suspend: false # tested end-to-end
|
|
schedule: "5 4 1 1,4,7,10 *" # quarterly, Europe/Berlin (offsets avoid HA restart pile-up)
|
|
timeZone: Europe/Berlin
|
|
concurrencyPolicy: Forbid
|
|
successfulJobsHistoryLimit: 2
|
|
failedJobsHistoryLimit: 5
|
|
startingDeadlineSeconds: 3600
|
|
jobTemplate:
|
|
spec:
|
|
backoffLimit: 1
|
|
activeDeadlineSeconds: 300
|
|
ttlSecondsAfterFinished: 259200 # 3 d
|
|
template:
|
|
metadata:
|
|
labels:
|
|
app.kubernetes.io/name: home-assistant
|
|
app.kubernetes.io/component: secret-rotator
|
|
spec:
|
|
serviceAccountName: home-assistant-secret-rotator
|
|
restartPolicy: Never
|
|
securityContext:
|
|
runAsNonRoot: true
|
|
runAsUser: 65534
|
|
runAsGroup: 65534
|
|
seccompProfile:
|
|
type: RuntimeDefault
|
|
containers:
|
|
- name: rotate
|
|
image: gitea.renner.uno/gitea_admin/secret-rotator:2026-07-30 # {"$imagepolicy": "flux-system:secret-rotator"}
|
|
args: ["ha-token"]
|
|
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-home-assistant
|
|
- name: BAO_PATH
|
|
value: homelab/apps/home-assistant/config-sync-token
|
|
- name: HA_URL
|
|
value: ws://home-assistant-bgp.home-assistant.svc.cluster.local:8123/api/websocket
|
|
- name: HA_CLIENT_NAME
|
|
value: "Config Sync"
|
|
resources:
|
|
requests:
|
|
cpu: 25m
|
|
memory: 16Mi
|
|
limits:
|
|
memory: 48Mi
|
|
---
|
|
apiVersion: batch/v1
|
|
kind: CronJob
|
|
metadata:
|
|
name: shelly-manager-llat-secret-rotate
|
|
namespace: openbao
|
|
labels:
|
|
app.kubernetes.io/name: home-assistant
|
|
app.kubernetes.io/component: secret-rotator
|
|
spec:
|
|
suspend: false # tested end-to-end
|
|
schedule: "10 4 1 1,4,7,10 *" # quarterly, Europe/Berlin (offsets avoid HA restart pile-up)
|
|
timeZone: Europe/Berlin
|
|
concurrencyPolicy: Forbid
|
|
successfulJobsHistoryLimit: 2
|
|
failedJobsHistoryLimit: 5
|
|
startingDeadlineSeconds: 3600
|
|
jobTemplate:
|
|
spec:
|
|
backoffLimit: 1
|
|
activeDeadlineSeconds: 300
|
|
ttlSecondsAfterFinished: 259200 # 3 d
|
|
template:
|
|
metadata:
|
|
labels:
|
|
app.kubernetes.io/name: home-assistant
|
|
app.kubernetes.io/component: secret-rotator
|
|
spec:
|
|
serviceAccountName: home-assistant-secret-rotator
|
|
restartPolicy: Never
|
|
securityContext:
|
|
runAsNonRoot: true
|
|
runAsUser: 65534
|
|
runAsGroup: 65534
|
|
seccompProfile:
|
|
type: RuntimeDefault
|
|
containers:
|
|
- name: rotate
|
|
image: gitea.renner.uno/gitea_admin/secret-rotator:2026-07-30 # {"$imagepolicy": "flux-system:secret-rotator"}
|
|
args: ["ha-token"]
|
|
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-home-assistant
|
|
- name: BAO_PATH
|
|
value: homelab/apps/home-assistant/shelly-manager-llat
|
|
- name: HA_URL
|
|
value: ws://home-assistant-bgp.home-assistant.svc.cluster.local:8123/api/websocket
|
|
- name: HA_CLIENT_NAME
|
|
value: "Shelly Manager"
|
|
resources:
|
|
requests:
|
|
cpu: 25m
|
|
memory: 16Mi
|
|
limits:
|
|
memory: 48Mi
|