diff --git a/kubernetes/apps/home-assistant/kustomization.yaml b/kubernetes/apps/home-assistant/kustomization.yaml index a9859bcf..4d884e06 100644 --- a/kubernetes/apps/home-assistant/kustomization.yaml +++ b/kubernetes/apps/home-assistant/kustomization.yaml @@ -16,9 +16,6 @@ resources: # probes use the cert account mosquitto-probe, REN-953); removed with its # rotator in the REN-802 cleanup step. - mosquitto-health-credentials-externalsecret.yaml - # open3e-int broker credential: open3e-esp32 gateway on the Vitocal internal - # CAN; OpenBao-backed + Class-A rotated. - - mosquitto-open3e-int-credentials-externalsecret.yaml - mosquitto-config.yaml - mosquitto-certificate.yaml - mosquitto-service.yaml diff --git a/kubernetes/apps/home-assistant/mosquitto-bootstrap.yaml b/kubernetes/apps/home-assistant/mosquitto-bootstrap.yaml index 96f1ea37..b874e543 100644 --- a/kubernetes/apps/home-assistant/mosquitto-bootstrap.yaml +++ b/kubernetes/apps/home-assistant/mosquitto-bootstrap.yaml @@ -177,77 +177,6 @@ spec: limits: memory: 64Mi --- -# Second broker user: `shellies`. Separate Secret (not a second key on -# mosquitto-credentials) so each credential's lifecycle is independent — -# rotating one never touches the other, and existing clusters whose -# mosquitto-credentials was already generated for `homeassistant` only -# don't end up with a half-populated Secret. -apiVersion: batch/v1 -kind: Job -metadata: - name: mosquitto-shellies-credentials-bootstrap - namespace: home-assistant - annotations: - # Job pod templates are immutable; force-recreate so image bumps - # apply on Flux re-reconcile. Safe: the Job is idempotent (see above). - kustomize.toolkit.fluxcd.io/force: enabled -spec: - backoffLimit: 4 - template: - metadata: - labels: - app.kubernetes.io/name: mosquitto-bootstrap - spec: - serviceAccountName: mosquitto-bootstrap - restartPolicy: OnFailure - containers: - - name: bootstrap - image: alpine:3.24@sha256:294b683cb724975bec92580e1e685676bd4b50bda910ddb8c51d4cabeaec77e6 - command: - - sh - - -c - - | - set -eu - apk add --no-cache curl jq >/dev/null - - NS=home-assistant - NAME=mosquitto-shellies-credentials - TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token) - CA=/var/run/secrets/kubernetes.io/serviceaccount/ca.crt - API=https://kubernetes.default.svc - - if curl -fsSL --cacert "$CA" -H "Authorization: Bearer $TOKEN" \ - "$API/api/v1/namespaces/$NS/secrets/$NAME" >/dev/null 2>&1; then - echo "$NAME already exists; nothing to do." - exit 0 - fi - - PW=$(tr -dc 'A-Za-z0-9' /dev/null - - echo "generated $NAME" - resources: - requests: - cpu: 10m - memory: 32Mi - limits: - memory: 64Mi ---- # Fourth broker user: `sml2mqtt` — the power-meter bridge. Publish-only # under `sml2mqtt/#` (ACL in mosquitto-deployment.yaml). Own credential so # its lifecycle is independent of the others. The sml2mqtt pod reads this @@ -392,7 +321,7 @@ spec: --- # Broker user `zigbee2mqtt` — the Zigbee2MQTT bridge (ZHA replacement). Scoped # to its own `zigbee2mqtt/#` subtree plus `write homeassistant/#` (so it can -# publish HA MQTT-discovery configs, exactly like the `shellies` user) and +# publish HA MQTT-discovery configs) and # `read homeassistant/status` (HA's birth/will topic; on `online` Z2M # republishes cached device states, NOT discovery configs — ACL and the full # reasoning in mosquitto-deployment.yaml). Own credential so its lifecycle is diff --git a/kubernetes/apps/home-assistant/mosquitto-deployment.yaml b/kubernetes/apps/home-assistant/mosquitto-deployment.yaml index b8193c8e..593c04d8 100644 --- a/kubernetes/apps/home-assistant/mosquitto-deployment.yaml +++ b/kubernetes/apps/home-assistant/mosquitto-deployment.yaml @@ -5,14 +5,6 @@ metadata: namespace: home-assistant labels: app.kubernetes.io/name: mosquitto - annotations: - # REQUIRED for credential rotation to take effect. The startup shim rebuilds - # the password file only on pod start, so without this a rotated Secret sits - # inert while the old password stays valid indefinitely. - # - # Listed per-Secret rather than with the blanket auto annotation, so that - # enrolling another broker user is a visible edit to this line. - secret.reloader.stakater.com/reload: mosquitto-open3e-int-credentials spec: replicas: 1 strategy: @@ -74,23 +66,14 @@ spec: set -eu rm -f /mosquitto/auth/passwd mosquitto_passwd -c -b /mosquitto/auth/passwd homeassistant "$HA_PASSWORD" - mosquitto_passwd -b /mosquitto/auth/passwd shellies "$SHELLY_PASSWORD" mosquitto_passwd -b /mosquitto/auth/passwd admin "$ADMIN_PASSWORD" mosquitto_passwd -b /mosquitto/auth/passwd sml2mqtt "$SML2MQTT_PASSWORD" mosquitto_passwd -b /mosquitto/auth/passwd open3e "$OPEN3E_PASSWORD" - mosquitto_passwd -b /mosquitto/auth/passwd open3e-int "$OPEN3E_INT_PASSWORD" mosquitto_passwd -b /mosquitto/auth/passwd evcc "$EVCC_PASSWORD" mosquitto_passwd -b /mosquitto/auth/passwd zigbee2mqtt "$ZIGBEE2MQTT_PASSWORD" - # ACL: HA keeps full access (subscribes to everything); the - # shellies user is `readwrite shellies/#` (devices publish state - # and subscribe to their command topics) + `write homeassistant/#` - # so Shelly Gen2 firmwares can publish HA MQTT-discovery configs. - # It is WRITE-only on homeassistant/# on purpose: a publish-only - # client never needs to read the whole discovery/state tree, and - # denying read removes device-inventory recon for a compromised - # Shelly. (Full per-device write scoping isn't expressible with - # the flat discovery topic layout; that would need a dynamic-auth - # plugin — out of scope.) + # ACL: HA keeps full access (subscribes to everything). Shellies + # are not legacy users any more: each has its own Dynamic Security + # account from mqtt-accounts.yaml (REN-802, REN-1017). # `admin` is a workstation/debugging credential — full # readwrite, kept on its own credential so its password can # be rotated independently of the HA session. @@ -111,10 +94,6 @@ spec: user homeassistant topic readwrite # - user shellies - topic readwrite shellies/# - topic write homeassistant/# - user admin topic readwrite # @@ -124,12 +103,6 @@ spec: user open3e topic readwrite open3e/# - # open3e-esp32 gateway on the Vitocal internal CAN: own subtree - # under open3e/ so edge-1's listener on open3e/cmnd never sees - # its commands. - user open3e-int - topic readwrite open3e/int/# - user evcc topic read open3e/# topic readwrite evcc/# @@ -137,9 +110,8 @@ spec: # `zigbee2mqtt` is the Zigbee2MQTT bridge (the ZHA replacement). # Owns its own `zigbee2mqtt/#` subtree; may WRITE `homeassistant/#` - # to publish HA MQTT-discovery configs (write-only there, like the - # shellies user, to deny discovery-tree recon from a compromised - # bridge); and READs `homeassistant/status`, HA's birth/will topic. + # to publish HA MQTT-discovery configs (write-only there, to deny + # discovery-tree recon from a compromised bridge); and READs `homeassistant/status`, HA's birth/will topic. # # That read grant does NOT buy a discovery re-publish: on # `online`, Z2M 2.12.1 republishes bridge/state and cached device @@ -176,11 +148,6 @@ spec: secretKeyRef: name: mosquitto-credentials key: homeassistant-password - - name: SHELLY_PASSWORD - valueFrom: - secretKeyRef: - name: mosquitto-shellies-credentials - key: password - name: ADMIN_PASSWORD valueFrom: secretKeyRef: @@ -196,13 +163,6 @@ spec: secretKeyRef: name: mosquitto-open3e-credentials key: password - # open3e-int: OpenBao-backed + Class-A rotated (see - # mosquitto-open3e-int-credentials-externalsecret.yaml). - - name: OPEN3E_INT_PASSWORD - valueFrom: - secretKeyRef: - name: mosquitto-open3e-int-credentials - key: password - name: EVCC_PASSWORD valueFrom: secretKeyRef: diff --git a/kubernetes/apps/home-assistant/mosquitto-open3e-int-credentials-externalsecret.yaml b/kubernetes/apps/home-assistant/mosquitto-open3e-int-credentials-externalsecret.yaml deleted file mode 100644 index 48b6ded7..00000000 --- a/kubernetes/apps/home-assistant/mosquitto-open3e-int-credentials-externalsecret.yaml +++ /dev/null @@ -1,21 +0,0 @@ -# Projects the mosquitto `open3e-int` broker credential (open3e-esp32 gateway -# on the Vitocal internal CAN) from OpenBao into the Secret the broker -# consumes. Rotator: kubernetes/infrastructure/openbao/rotation/mosquitto-open3e-int.yaml. -# -# creationPolicy is implicitly Owner: deleting this ExternalSecret deletes the -# live Secret. Set creationPolicy: Orphan first to detach without destroying. -apiVersion: external-secrets.io/v1 -kind: ExternalSecret -metadata: - name: mosquitto-open3e-int-credentials - namespace: home-assistant -spec: - refreshInterval: 10m - secretStoreRef: - kind: ClusterSecretStore - name: openbao - target: - name: mosquitto-open3e-int-credentials - dataFrom: - - extract: - key: apps/home-assistant/mosquitto-open3e-int-credentials diff --git a/kubernetes/apps/home-assistant/mqtt-accounts.yaml b/kubernetes/apps/home-assistant/mqtt-accounts.yaml index 16a9a33d..fcc4cddf 100644 --- a/kubernetes/apps/home-assistant/mqtt-accounts.yaml +++ b/kubernetes/apps/home-assistant/mqtt-accounts.yaml @@ -61,9 +61,10 @@ acl: ["read open3e/#", "read zigbee2mqtt/#", "read homeassistant/#"] # Shellies (REN-1017): one password account per device, pushed by the # controller through shelly-manager's per-device API. Name shelly- = -# shelly-manager device id. Same ACL as the legacy user `shellies`, which -# keeps working until every device has moved. Gen2+ devices reboot once -# when their password is pushed. +# shelly-manager device id. ACL as the former shared user `shellies` +# (removed in REN-1017 step 4). Gen2+ devices reboot once when their password +# is pushed. A new Shelly needs its entry here first: shelly-manager's MQTT +# template user no longer exists on the broker. - name: shelly-5 # Plus i4, the API test device of REN-929 auth: password deliver: {shelly-manager: 5} diff --git a/kubernetes/infrastructure/openbao/policies/rotator-mosquitto-open3e-int.hcl b/kubernetes/infrastructure/openbao/policies/rotator-mosquitto-open3e-int.hcl deleted file mode 100644 index c92fa50e..00000000 --- a/kubernetes/infrastructure/openbao/policies/rotator-mosquitto-open3e-int.hcl +++ /dev/null @@ -1,30 +0,0 @@ -# OpenBao policy: rotator-mosquitto-open3e-int -# -# Least-privilege policy for the mosquitto `open3e-int` broker-credential -# rotation CronJob (kubernetes auth role `rotator-mosquitto-open3e-int`). -# Per-secret isolation, same template as rotator-mosquitto-health. -# -# NOT applied by Flux. Apply ONCE with an admin token: -# -# bao policy write rotator-mosquitto-open3e-int \ -# kubernetes/infrastructure/openbao/policies/rotator-mosquitto-open3e-int.hcl -# -# bao write auth/kubernetes/role/rotator-mosquitto-open3e-int \ -# bound_service_account_names=mosquitto-open3e-int-secret-rotator \ -# bound_service_account_namespaces=openbao \ -# policies=rotator-mosquitto-open3e-int ttl=15m -# -# Do NOT set token_no_default_policy=true (kv-v2 patch preflight needs default). -# -# BOOTSTRAP: `random` mode rotates an existing field and never creates one. -# Seed the path once with a `password` field before the first rotation: -# -# bao kv put -mount=homelab apps/home-assistant/mosquitto-open3e-int-credentials \ -# password= -# -# The consumer is the open3e-esp32 gateway on the heat pump's internal CAN -# (NVS key mq_pass); a rotation takes effect on the device only after its -# MQTT settings are updated (web UI or NVS image). -path "homelab/data/apps/home-assistant/mosquitto-open3e-int-credentials" { - capabilities = ["read", "patch"] -} diff --git a/kubernetes/infrastructure/openbao/rotation/kustomization.yaml b/kubernetes/infrastructure/openbao/rotation/kustomization.yaml index 3de37f92..f1645b74 100644 --- a/kubernetes/infrastructure/openbao/rotation/kustomization.yaml +++ b/kubernetes/infrastructure/openbao/rotation/kustomization.yaml @@ -48,7 +48,6 @@ resources: - review-bot.yaml - claude-oauth.yaml - mosquitto-health.yaml - - mosquitto-open3e-int.yaml # NOT ROTATED, deliberately — recorded here because the whole point of this # directory is that unrotated credentials should be a visible choice: # diff --git a/kubernetes/infrastructure/openbao/rotation/mosquitto-open3e-int.yaml b/kubernetes/infrastructure/openbao/rotation/mosquitto-open3e-int.yaml deleted file mode 100644 index 84af284f..00000000 --- a/kubernetes/infrastructure/openbao/rotation/mosquitto-open3e-int.yaml +++ /dev/null @@ -1,83 +0,0 @@ ---- -# Class-A rotation of the mosquitto `open3e-int` broker credential: the -# open3e-esp32 gateway on the Vitocal internal CAN (ECU 0x6e0). Same -# rotator -> OpenBao -> ESO -> Reloader chain as mosquitto-health.yaml. -# -# The consumer is an embedded device: after a rotation its MQTT password must -# be updated on the device (web UI Einstellungen, or an NVS image over USB). -# -# ON-DEMAND: kubectl -n openbao create job --from=cronjob/mosquitto-open3e-int-secret-rotate mosquitto-open3e-int-secret-rotate-now -apiVersion: v1 -kind: ServiceAccount -metadata: - name: mosquitto-open3e-int-secret-rotator - namespace: openbao - labels: - app.kubernetes.io/name: mosquitto - app.kubernetes.io/component: secret-rotator ---- -apiVersion: batch/v1 -kind: CronJob -metadata: - name: mosquitto-open3e-int-secret-rotate - namespace: openbao - labels: - app.kubernetes.io/name: mosquitto - app.kubernetes.io/component: secret-rotator -spec: - # Quarterly, own month offset (paperless: 1,4,7,10; health: 2,5,8,11). - schedule: "0 4 1 3,6,9,12 *" - # Suspended (REN-802): the ESP32 only gets its password by hand, so a - # rotation would take the gateway offline. MQTT credentials move to the - # git-defined account register; this rotator is removed in that cleanup. - suspend: true - timeZone: Europe/Berlin - concurrencyPolicy: Forbid - successfulJobsHistoryLimit: 2 - failedJobsHistoryLimit: 5 - startingDeadlineSeconds: 3600 - jobTemplate: - spec: - backoffLimit: 2 - activeDeadlineSeconds: 300 - ttlSecondsAfterFinished: 259200 # 3 d - template: - metadata: - labels: - app.kubernetes.io/name: mosquitto - app.kubernetes.io/component: secret-rotator - spec: - serviceAccountName: mosquitto-open3e-int-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 - args: ["random"] - 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-mosquitto-open3e-int - - name: BAO_PATH - value: homelab/apps/home-assistant/mosquitto-open3e-int-credentials - - name: BAO_FIELDS - value: "password" - - name: RANDOM_BYTES - value: "32" - resources: - requests: - cpu: 25m - memory: 16Mi - limits: - memory: 48Mi