Merge pull request 'feat(mqtt): remove legacy broker users shellies and open3e-int (REN-1017 step 4, window)' (#1191) from atlas/ren-1017-remove-legacy-shellies into master
ci/woodpecker/cron/issue-fix Pipeline was successful
ci/woodpecker/cron/alert-monitor Pipeline was successful

This commit was merged in pull request #1191.
This commit is contained in:
argus-bot (Paperclip service account)
2026-10-08 00:48:49 +00:00
8 changed files with 10 additions and 258 deletions
@@ -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
@@ -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/urandom | head -c 32)
PW_B64=$(printf '%s' "$PW" | base64 | tr -d '\n')
body=$(jq -nc \
--arg name "$NAME" \
--arg ns "$NS" \
--arg pw "$PW_B64" \
'{apiVersion: "v1",
kind: "Secret",
metadata: {name: $name, namespace: $ns},
type: "Opaque",
data: {password: $pw}}')
curl -fsSL --cacert "$CA" -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" -X POST \
--data "$body" \
"$API/api/v1/namespaces/$NS/secrets" >/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
@@ -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:
@@ -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
@@ -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-<id> =
# 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}
@@ -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=<generated>
#
# 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"]
}
@@ -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:
#
@@ -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