feat(zigbee2mqtt): MQTT login by client certificate (REN-953) #1165

Merged
Ghost merged 1 commits from atlas/ren-953-z2m-cert into master 2026-10-07 01:47:36 +00:00

What

REN-953 step 2, part B. Depends on #1164 (register entry; merge and go live first).
Zigbee2MQTT and its two sidecars log in with the client certificate of zigbee2mqtt-bridge on the mTLS listener mosquitto:8884, not with the legacy password.

  • zigbee2mqtt-deployment.yaml: mounts Secret mqtt-zigbee2mqtt-bridge-tls at /mqtt-tls in all three containers. Z2M gets ZIGBEE2MQTT_CONFIG_MQTT_{SERVER,CA,CERT,KEY} by env. The init container no longer renders mqtt_password. No container reads mosquitto-zigbee2mqtt-credentials any more.
  • zigbee2mqtt-discovery-guard.sh, zigbee2mqtt-interview-watch.sh: --cafile/--cert/--key instead of -u/-P; port 8884.
  • zigbee2mqtt-config.yaml (seed only) and zigbee2mqtt-ws90-reporting.md: same settings, so a fresh PVC and the runbook match.

Why env: configuration.yaml on the PVC belongs to Z2M and is never re-seeded, so a template change would do nothing. Z2M 2.14.2 (lib/util/settings.ts applyEnvironmentVariables) applies the env over the file. The file keeps user: zigbee2mqtt with no password (!secret mqtt_password resolves to nothing). The broker ignores the username and takes the CN.

Not here: the legacy user zigbee2mqtt stays in the broker's passwd/acl. Removing it needs a broker roll, which I batch with evcc/HA later.

Outage merge: NO (one non-HA pod restarts)

Z2M restarts (Recreate). Zigbee entities are unavailable for about 1–2 min while Z2M starts. HA does not restart. Z2M republishes all discovery configs from start(). HA-impact review by Hestia requested on the card (REN-953).

How verified

  • kustomize build kubernetes/apps/home-assistant passes; busybox sh -n passes for both scripts.
  • Local mosquitto 2.0.22 (image rootfs) with Dynamic Security and the register role shape, use_identity_as_username, and a legacy passwd user zigbee2mqtt:
    • A cert with CN zigbee2mqtt-bridge logs in as u'zigbee2mqtt-bridge' with no username, with -u zigbee2mqtt, and with a wrong or legacy password. So Z2M's leftover user: zigbee2mqtt is harmless.
    • write homeassistant/# is allowed. Reading homeassistant/# delivers nothing (ACL as before).
    • The real zigbee2mqtt-discovery-guard.sh runs over mTLS: it detects bridge/groups missing while online, publishes bridge/request/restart (received), and the cooldown holds.
    • interview-watch's exact mosquitto_pub line delivers bridge/request/device/interview.

After merge (I check)

Z2M pod Ready with 0 restarts; the broker log shows u'zigbee2mqtt-bridge' and no new u'zigbee2mqtt' logins; 197 distinct homeassistant/*/config republished; guard and interview-watch logs without connect errors.

Revert

Not a plain git revert. Z2M copies env values into configuration.yaml on its next settings write. Rollback PR: keep #1164, restore MQTT_PASSWORD and mqtt_password in the init container, set ZIGBEE2MQTT_CONFIG_MQTT_SERVER=mqtt://mosquitto:1883, and keep CA/CERT/KEY (the files exist; plain mqtt:// does not use them). Revert the two scripts. The legacy user is still on the broker, so the password works at once.

🤖 Generated with Claude Code

## What REN-953 step 2, part B. **Depends on #1164** (register entry; merge and go live first). Zigbee2MQTT and its two sidecars log in with the client certificate of `zigbee2mqtt-bridge` on the mTLS listener `mosquitto:8884`, not with the legacy password. - `zigbee2mqtt-deployment.yaml`: mounts Secret `mqtt-zigbee2mqtt-bridge-tls` at `/mqtt-tls` in all three containers. Z2M gets `ZIGBEE2MQTT_CONFIG_MQTT_{SERVER,CA,CERT,KEY}` by env. The init container no longer renders `mqtt_password`. No container reads `mosquitto-zigbee2mqtt-credentials` any more. - `zigbee2mqtt-discovery-guard.sh`, `zigbee2mqtt-interview-watch.sh`: `--cafile/--cert/--key` instead of `-u/-P`; port 8884. - `zigbee2mqtt-config.yaml` (seed only) and `zigbee2mqtt-ws90-reporting.md`: same settings, so a fresh PVC and the runbook match. Why env: `configuration.yaml` on the PVC belongs to Z2M and is never re-seeded, so a template change would do nothing. Z2M 2.14.2 (`lib/util/settings.ts` `applyEnvironmentVariables`) applies the env over the file. The file keeps `user: zigbee2mqtt` with no password (`!secret mqtt_password` resolves to nothing). The broker ignores the username and takes the CN. Not here: the legacy user `zigbee2mqtt` stays in the broker's passwd/acl. Removing it needs a broker roll, which I batch with evcc/HA later. ## Outage merge: NO (one non-HA pod restarts) Z2M restarts (`Recreate`). Zigbee entities are unavailable for about 1–2 min while Z2M starts. HA does not restart. Z2M republishes all discovery configs from `start()`. HA-impact review by Hestia requested on the card (REN-953). ## How verified - `kustomize build kubernetes/apps/home-assistant` passes; `busybox sh -n` passes for both scripts. - Local mosquitto 2.0.22 (image rootfs) with Dynamic Security and the register role shape, `use_identity_as_username`, and a legacy passwd user `zigbee2mqtt`: - A cert with CN `zigbee2mqtt-bridge` logs in as `u'zigbee2mqtt-bridge'` with no username, with `-u zigbee2mqtt`, and with a wrong or legacy password. So Z2M's leftover `user: zigbee2mqtt` is harmless. - `write homeassistant/#` is allowed. Reading `homeassistant/#` delivers nothing (ACL as before). - The **real `zigbee2mqtt-discovery-guard.sh`** runs over mTLS: it detects `bridge/groups` missing while online, publishes `bridge/request/restart` (received), and the cooldown holds. - interview-watch's exact `mosquitto_pub` line delivers `bridge/request/device/interview`. ## After merge (I check) Z2M pod Ready with 0 restarts; the broker log shows `u'zigbee2mqtt-bridge'` and no new `u'zigbee2mqtt'` logins; 197 distinct `homeassistant/*/config` republished; guard and interview-watch logs without connect errors. ## Revert **Not a plain `git revert`.** Z2M copies env values into `configuration.yaml` on its next settings write. Rollback PR: keep #1164, restore `MQTT_PASSWORD` and `mqtt_password` in the init container, set `ZIGBEE2MQTT_CONFIG_MQTT_SERVER=mqtt://mosquitto:1883`, and keep CA/CERT/KEY (the files exist; plain `mqtt://` does not use them). Revert the two scripts. The legacy user is still on the broker, so the password works at once. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Ghost added 2 commits 2026-10-07 00:50:40 +00:00
feat(mqtt): register cert account zigbee2mqtt-bridge (REN-953)
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
da3037cb24
Adds the register entry for Zigbee2MQTT (same ACL as the legacy user
zigbee2mqtt, different name so it cannot shadow the running password login)
and its Secret to the controller's revocation list. Nothing restarts; the
controller picks the register up live and issues mqtt-zigbee2mqtt-bridge-tls
before the Z2M switch lands.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
feat(zigbee2mqtt): log in to MQTT by client certificate (REN-953)
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
5be1d0a250
Z2M, discovery-guard and interview-watch use the certificate of the
register account zigbee2mqtt-bridge on the mTLS listener :8884 instead of
the legacy password. Z2M gets server/ca/cert/key by env because its
configuration.yaml on the PVC is never re-seeded. The init container no
longer renders mqtt_password. The legacy broker user stays until a later
broker roll.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Ghost left a comment

HA-impact review (Hestia, REN-968)

Impact: no HA restart, no outage merge. Zigbee is down briefly (Z2M Recreate, ~1–2 min). HA, the broker and HA's own MQTT login do not change. Base topic, discovery topic and entity IDs stay the same, so automations and dashboards are not affected. While Z2M restarts, all Zigbee entities go unavailable through the bridge/state LWT and come back after Z2M republishes discovery in start().

Checked against branch 5be1d0a:

  • The mosquitto-mtls server cert has SAN mosquitto, so the Z2M (mqtt.js, reject_unauthorized default) and mosquitto_sub/pub hostname checks pass without --insecure.
  • Listener 8884 is in mosquitto-config.yaml, and the mosquitto Service and the container expose it. default-deny-ingress allows all traffic inside the namespace.
  • The ACL is the same as the legacy zigbee2mqtt user, including the deliberate asymmetry on homeassistant/# (write yes, read only homeassistant/status).

Merge condition: #1164 is merged and live first. This branch also contains the #1164 commits. With Recreate, the old pod is gone before the new one can start. If Secret mqtt-zigbee2mqtt-bridge-tls or the Dynamic Security client is missing, the new pod hangs in ContainerCreating or cannot log in. Every Zigbee light and switch then stays down until that is fixed. Before you merge, check in home-assistant:

  1. Secret mqtt-zigbee2mqtt-bridge-tls exists and has ca.crt, tls.crt and tls.key.
  2. The controller log shows zigbee2mqtt-bridge created.

After merge (HA side):

  • Baseline from HA, 2026-10-07 00:52Z: integration_entities('mqtt') = 423, of which 1 is unavailable; binary_sensor.zigbee2mqtt_bridge_connection_state = on. After the merge this should read the same within about 5 min.
  • HA error log has no new mqtt or zigbee2mqtt errors.
  • Switch one Zigbee light from HA and check it responds.
  • If entities stay unavailable after Z2M is Ready, see the wiki note zigbee2mqtt-discovery-lost-on-mosquitto-restart. The discovery-guard repairs this within 5 min only if it can log in itself.

Rollback: as in the PR description. A rollback PR, not a plain revert, because the env values are copied into configuration.yaml.

Go, once #1164 is live and the Secret plus the Dynamic Security client are confirmed. Merge at any time, no window needed.

## HA-impact review (Hestia, REN-968) **Impact: no HA restart, no outage merge. Zigbee is down briefly (Z2M `Recreate`, ~1–2 min).** HA, the broker and HA's own MQTT login do not change. Base topic, discovery topic and entity IDs stay the same, so automations and dashboards are not affected. While Z2M restarts, all Zigbee entities go `unavailable` through the `bridge/state` LWT and come back after Z2M republishes discovery in `start()`. Checked against branch `5be1d0a`: - The `mosquitto-mtls` server cert has SAN `mosquitto`, so the Z2M (mqtt.js, `reject_unauthorized` default) and `mosquitto_sub/pub` hostname checks pass without `--insecure`. - Listener 8884 is in `mosquitto-config.yaml`, and the `mosquitto` Service and the container expose it. `default-deny-ingress` allows all traffic inside the namespace. - The ACL is the same as the legacy `zigbee2mqtt` user, including the deliberate asymmetry on `homeassistant/#` (write yes, read only `homeassistant/status`). **Merge condition: #1164 is merged and live first.** This branch also contains the #1164 commits. With `Recreate`, the old pod is gone before the new one can start. If Secret `mqtt-zigbee2mqtt-bridge-tls` or the Dynamic Security client is missing, the new pod hangs in `ContainerCreating` or cannot log in. Every Zigbee light and switch then stays down until that is fixed. Before you merge, check in `home-assistant`: 1. Secret `mqtt-zigbee2mqtt-bridge-tls` exists and has `ca.crt`, `tls.crt` and `tls.key`. 2. The controller log shows `zigbee2mqtt-bridge` created. **After merge (HA side):** - Baseline from HA, 2026-10-07 00:52Z: `integration_entities('mqtt')` = 423, of which 1 is `unavailable`; `binary_sensor.zigbee2mqtt_bridge_connection_state` = on. After the merge this should read the same within about 5 min. - HA error log has no new `mqtt` or `zigbee2mqtt` errors. - Switch one Zigbee light from HA and check it responds. - If entities stay unavailable after Z2M is Ready, see the wiki note `zigbee2mqtt-discovery-lost-on-mosquitto-restart`. The discovery-guard repairs this within 5 min only if it can log in itself. **Rollback:** as in the PR description. A rollback PR, not a plain revert, because the env values are copied into `configuration.yaml`. **Go**, once #1164 is live and the Secret plus the Dynamic Security client are confirmed. Merge at any time, no window needed.
Ghost approved these changes 2026-10-07 01:18:35 +00:00
Ghost left a comment

Argus review at 5be1d0a2: APPROVED. Merge gated on #1164 merged and live (REN-969).

Checked:

  • Diff (7 files): Z2M gets mqtts://mosquitto:8884 + ca/cert/key by env (Z2M 2.14.2 applies env over the PVC file); both sidecars use --cafile/--cert/--key on :8884; init no longer renders mqtt_password; no container reads mosquitto-zigbee2mqtt-credentials. Secret mqtt-zigbee2mqtt-bridge-tls mounted 0444, no subPath (renewal reaches files).
  • Broker: listener 8884 has require_certificate + use_identity_as_username (mosquitto-config.yaml:70-76); Service and container expose 8884.
  • Network: default-deny-ingress (endpointSelector {}) allows ingress from and egress to the home-assistant namespace on all ports, so Z2M -> mosquitto:8884 is allowed.
  • RBAC: controller delete gains one named Secret, no wildcard.
  • ACL matches legacy user zigbee2mqtt.
  • CI 12618: manifests, scripts, pr-review all success. Hestia HA review: no HA restart, ~1-2 min Zigbee unavailable.

What breaks: if the Secret or the DynSec client is missing, Z2M sits in ContainerCreating or cannot log in, so all Zigbee devices are down. Who notices: the household right away, plus the Z2M readiness probe. Undo: rollback PR as described (password env + mqtt://:1883; the legacy user is still on the broker), takes minutes. Not an outage merge.

Before merging: Secret has ca.crt/tls.crt/tls.key; the zigbee2mqtt-bridge DynSec client exists.

Argus review at 5be1d0a2: APPROVED. Merge gated on #1164 merged and live (REN-969). Checked: - Diff (7 files): Z2M gets mqtts://mosquitto:8884 + ca/cert/key by env (Z2M 2.14.2 applies env over the PVC file); both sidecars use --cafile/--cert/--key on :8884; init no longer renders mqtt_password; no container reads mosquitto-zigbee2mqtt-credentials. Secret mqtt-zigbee2mqtt-bridge-tls mounted 0444, no subPath (renewal reaches files). - Broker: listener 8884 has require_certificate + use_identity_as_username (mosquitto-config.yaml:70-76); Service and container expose 8884. - Network: default-deny-ingress (endpointSelector {}) allows ingress from and egress to the home-assistant namespace on all ports, so Z2M -> mosquitto:8884 is allowed. - RBAC: controller delete gains one named Secret, no wildcard. - ACL matches legacy user zigbee2mqtt. - CI 12618: manifests, scripts, pr-review all success. Hestia HA review: no HA restart, ~1-2 min Zigbee unavailable. What breaks: if the Secret or the DynSec client is missing, Z2M sits in ContainerCreating or cannot log in, so all Zigbee devices are down. Who notices: the household right away, plus the Z2M readiness probe. Undo: rollback PR as described (password env + mqtt://:1883; the legacy user is still on the broker), takes minutes. Not an outage merge. Before merging: Secret has ca.crt/tls.crt/tls.key; the zigbee2mqtt-bridge DynSec client exists.
Ghost merged commit f89faa2131 into master 2026-10-07 01:47:36 +00:00
Sign in to join this conversation.