private@homelab: ~/latest
local-first guides · privacy-aware · no noisy tracking
private@homelab:~$ cat guides/article.md
· ·
8–11 minutes
read

Zigbee2MQTT permit_join: The Security Risk After 2.0

Zigbee2MQTT 2.0 removed permanent permit_join. What the 254-second join window still exposes, and how a device passlist controls membership locally.

permit_join is the Zigbee2MQTT setting that decides whether new devices can join your network. It is also the setting most people flip once during initial setup and never think about again, which is how it earned a reputation as the quiet security risk sitting inside an otherwise local-first stack.

That reputation is out of date, and the correction matters more than the original warning. Zigbee2MQTT 2.0 removed the ability to hold the join window open indefinitely, capping it at 254 seconds. If you are running 2.x, the scenario most of the older advice describes cannot happen on your install. What remains is a narrower and more useful question: what that 254-second window still exposes, what a stale pre-2.0 config file might still be carrying, and which settings control network membership itself rather than just its timing. None of them need a cloud account or a vendor dependency.

What permit_join actually controls

permit_join is the state that decides whether new devices can join the Zigbee network Z2M manages. That is the whole of it. It does not authenticate devices that have already joined, and it does not touch message encryption. It is a join gate, not a security perimeter.

Opening that gate from the frontend button, or over MQTT via zigbee2mqtt/bridge/request/permit_join, starts a join window that runs for 254 seconds by default and then closes on its own. The same button closes it early once your device has appeared, which is worth doing out of habit.

Note · compatibility

Everything below describes Zigbee2MQTT 2.x behavior. On 1.x the join window could be held open permanently, so if you are still on an older release the risk picture is the one described in the next section, not the current one.

What changed in Zigbee2MQTT 2.0

Most of the writing that treats permit_join as a live security hole predates version 2.0. It describes a real setup: permit_join: true written directly into configuration.yaml rather than using the temporary toggle, holding the network open for new joins persistently and across restarts, because the config file is read fresh on every start. That was the concern behind a long-running feature request (issue #889) asking for an enforced timeout.

Version 2.0 closed it. The 2.0.0 breaking-changes list removes the option to permit joining forever and limits the window to a maximum of 254 seconds, and the Home Assistant permit-join switch disables itself automatically at the same limit. The current state of the argument is the inverse of the old one: there is now a standing feature request (issue #25626) asking for the “permit join forever” behavior to be restored, from users whose pairing workflows depended on it.

Note · before you start

Check your own configuration.yaml for a leftover permit_join: true from a pre-2.0 setup pass. On 2.x it will not hold your network open, but a stale line like that is a reasonable signal the rest of the file has not been reviewed since either.

What the 254-second window still exposes

The residual risk is real, and it is worth stating precisely rather than dramatically. While the window is open, any device within radio range can join without presenting a shared secret. There is no Wi-Fi password anywhere in that path, no account, and no pairing PIN by default. Zigbee’s join process was never built around a shared credential the way Wi-Fi’s was, so “open” does mean open for as long as the window lasts.

Two practical consequences follow. First, if you open the window and walk away, anything in range that is actively looking for a network can take the slot, not only the device in your hand. Second, an open window also attracts join attempts from devices belonging to a neighbouring, unrelated Zigbee network, which is a non-security failure mode with the same symptom. If an entry you do not recognise turns up in the device list, that is worth investigating rather than dismissing, and our ghost device troubleshooting guide covers how to tell an unexpected join from a coordinator fault.

Controlling membership instead of timing: the passlist

Everything above is still a timing control, resting on the network happening to be closed right now. Z2M’s securing guide points at a stronger option, and calls a passlist the most restrictive and therefore most secure choice. A passlist is an explicit allowlist of device IEEE addresses in configuration.yaml. Only listed devices are allowed on the network, so membership becomes a decision you make per device rather than a side effect of when a window happened to be open.

configuration.yaml
# Representative IEEE addresses. Only these devices may be
# on the network; everything else is removed.
passlist:
  - '0x00158d0001d8e1e2'
  - '0x00158d0002a4b3c1'
Warning

A passlist is not only a join filter. Z2M’s documentation states that every device not on the passlist will be removed from the network. Enumerate every device you already own and list all of them before you enable it, or your next restart will silently drop the ones you missed and take their automations with them.

I think this option deserves better signposting than it gets. The passlist lives in configuration.yaml with its own documentation page under configuration, while the securing guide that recommends it sits in the installation section several pages away. Someone worried enough about join security to go looking is exactly the reader a passlist suits, and is unlikely to arrive at it by accident. Our MQTT broker security checklist covers the broker, frontend auth and file-permission side of the same job, and deliberately stops short of the join layer, which is the gap this piece fills.

Two hardening steps worth doing at the same time

Many Zigbee networks still run on a well-known, fixed default network encryption key. Z2M’s securing guide recommends generating a random one instead by setting network_key: GENERATE under the advanced block; Z2M replaces the placeholder with a randomly generated key on the next startup.

configuration.yaml
advanced:
  network_key: GENERATE

Rotating the key is not free. Every device on the mesh holds a copy of the old one, so a rotation means re-pairing all of them, not just restarting the service. Plan it for a day you are not depending on the automations those devices drive.

Install codes are the other piece, and they carry two conditions. An install code is assigned to a device during manufacture and is usually printed on it as text or a QR code, so the device has to have one, which is vendor-dependent. Support also varies by radio: Z2M documents install-code and QR-code commissioning as implemented for zstack (Texas Instruments), ember (Silicon Labs EmberZNet) and deconz (Dresden Elektronik, requiring recent coordinator firmware) adapters. Check both your coordinator and the devices themselves before planning anything fleet-wide around it.

Quick reference: what controls what

Control What it governs Where it lives
Join window When joining is possible. 254 seconds maximum on 2.x, then closes itself Frontend button or MQTT request
permit_join: true in config Held the network open persistently on 1.x. No permanent effect on 2.x Legacy config line, worth clearing out
Passlist Which devices may be on the network at all, regardless of window state configuration.yaml
Install code How the network key reaches a joining device during pairing Per device, adapter-dependent
network_key: GENERATE Whether the mesh runs on a default or a random key configuration.yaml, advanced block

The working sequence is short. Open the join window only when you are actively adding something, and close it once the device appears. If the device is secondhand, factory reset it first, since a previous owner’s network identity causes confusing join behavior; our Touchlink reset guide covers that process. Once your roster is stable, move to a passlist so membership stops depending on window timing. Rotate the network key off its default separately, accepting the re-pairing cost as a one-time tax.

None of this depends on your Wi-Fi segmentation, but if the coordinator’s host already sits on its own IoT VLAN, that is a second and independent layer worth keeping. Our IoT VLAN guide covers that broader segmentation context.

FAQ

Is it safe to leave Zigbee2MQTT’s permit_join on all the time? On 2.x you cannot: joining is capped at 254 seconds and the window closes itself, including the Home Assistant switch. On 1.x, setting permit_join: true in configuration.yaml did hold the network open persistently, and that is the configuration the older security warnings are about.

How long does permit_join stay open by default in Zigbee2MQTT? 254 seconds, whether you open it from the frontend button or over MQTT. You can close it earlier from the same control, and on 2.x that ceiling cannot be raised.

What is a Zigbee device passlist and how do I set one up? It is an explicit allowlist of device IEEE addresses in configuration.yaml that controls network membership directly rather than through join-window timing. Z2M’s securing guide calls it the most restrictive option. Add every device you already own before enabling it, because devices missing from the list are removed from the network.

Can a stranger’s device join my Zigbee network without my Wi-Fi password? While the join window is open, yes. Zigbee pairing does not check a Wi-Fi credential or any shared secret by default. Since 2.0 that exposure is bounded to 254 seconds at a time, which is the difference between a narrow window and an open door.

Do I need to change my Zigbee network key from the default? It is recommended. Many networks still run on a well-known fixed key, and Z2M’s guide treats generating a random one via network_key: GENERATE as standard hardening, with the caveat that every device needs re-pairing afterward.

What this piece verified

This is a documentation and issue-tracker synthesis, built from Z2M’s own pairing, securing and passlist documentation plus the 2.0.0 breaking-changes discussion. No device pairing or network configuration was run against physical hardware for this piece, and nothing above should be read as hands-on verification.

Confirmed: the 254-second join window and its behavior on both the frontend button and the MQTT request; the removal of permit-joining-forever in 2.0 and the standing request to restore it; the passlist as the securing guide’s most restrictive recommendation, including that unlisted devices are removed; the network_key: GENERATE mechanism and its re-pairing cost; and the adapter list for install-code support. Two things to check on your own setup rather than take from here. First, whether your configuration.yaml still carries a permit_join line from an old setup pass. Second, whether your specific coordinator and your specific devices both support install codes, since the two conditions are independent.

The open question now runs the other way from how it used to. The maintainers closed the permanent join window deliberately, and the request to reopen it is coming from users, not from an oversight. Whether a bounded compromise appears, something like a long but finite window for bulk pairing, is the thing worth watching in the 2.x line.

local-firstHome Assistantno-cloud