For Years My ESPHome Updates Carried My Wi-Fi Password in the Clear. 2026.9 Closed That.

Close-up of a dark circuit board

I have been flashing ESPHome devices over the air since my first ESP8266 temperature sensor, and I never once thought about what was actually crossing the network while the progress bar filled. An OTA password was set. The dashboard said OTA successful. That was the end of my thinking on the subject.

Then I read the ESPHome 2026.9.0 release notes, published on 16 September, and found the sentence that ruined my evening: until this release the firmware image travelled over the network in plaintext, so a passive listener on the LAN could capture the Wi-Fi credentials and the API encryption key baked into that image. The OTA password authenticated the uploader. It never made the transfer confidential.

That is not a vulnerability in the dramatic sense. Nobody broke in. It is just a thing that was true about my house for years, that I had never bothered to work out for myself, and that I would have kept not working out if someone had not written it down in a changelog.

What was actually on the wire

An ESPHome binary is not an abstract blob. It contains the SSID and password of the Wi-Fi network it is supposed to join, and it contains the Noise pre-shared key that my Home Assistant box uses to talk to that device. Every time I ran esphome run against a sensor, both of those went across my LAN in the clear, addressed to the device but visible to anything sitting on the same segment.

My honest threat model here is small. It is a flat home network, in an apartment, in Switzerland. The realistic attacker is a compromised smart TV or a guest device that should not have been on the main VLAN in the first place. But “my threat model is small” is exactly the sentence people say right before they explain how the thing got in. The local-first argument I keep making on this blog is that keeping data inside the house is worth something. It is worth rather less if the house itself is a broadcast medium.

Ethernet patch cables on a dark surface
The LAN is not a private channel just because it is yours. Photo: Pexels

What 2026.9 actually changes

The fix, contributed by bdraco in PR #18489, adds Noise encryption — ChaCha20-Poly1305 — to the esphome OTA platform, using the same protocol the native API already speaks. It sits on a new shared noise component that lifts the plumbing out of the API component so both can use it. There is no configuration for noise itself; you never touch it.

Because it uses the Noise NNpsk0 pattern with a pre-shared key, the key does two jobs at once: it keeps the image confidential and it proves who is uploading. Which is why encryption and password: are mutually exclusive. The password had one job, and the key already does it better.

On a device that already has an API encryption key, the new firmware simply offers OTA encryption using that key, and the CLI takes it whenever it is offered. No config change required. The config change is what turns “offered” into “required”.

The config is one line. The migration is two installs.

The end state is almost insultingly simple — a bare encryption: block under the OTA platform, which inherits the API key so there is exactly one secret per device:

api:
  encryption:
    key: !secret device_name__encryption_key

ota:
  - platform: esphome
    encryption:

Here is the part that will bite people who paste that in and hit install. Once encryption: is in the config, the CLI refuses to send a plaintext image. Old firmware cannot offer encryption. So the install against your existing device stops with the device did not offer encryption; refusing to send the image in plaintext, and you sit there wondering what you broke.

That is deliberate, and it is the right call: a silent fallback to plaintext would mean an attacker could strip encryption and force an unauthenticated upload. Both ends fail closed. But it does mean the order matters:

  1. Install 2026.9.0 first, with your config otherwise unchanged — keep the password:, make sure api: has an encryption: key:. This upload is still plaintext; that is unavoidable and it is the last one. Then check the device log for Encryption: offered, plaintext accepted.
  2. Now add encryption: under the OTA platform and remove password:, and install again. The log should read Encryption: required.

If a device’s API key was provisioned by Home Assistant rather than written in your YAML, copy that provisioned key into api: encryption: key: before step 1. Do not generate a fresh one — that locks Home Assistant out of the device. The OTA platform docs also cover the MQTT-only case, where you temporarily add an api: block as a stepping stone and then move the key to the OTA block.

The plaintext fallback is planned to disappear no earlier than 2027.3.0. After that, step 1 happens over a serial cable. So there is no rush, but there is a deadline, and I would rather do it at a desk than with a USB cable and a torch behind the washing machine.

It costs less than it used to, not more

My first instinct with anything involving the word “encryption” on an ESP8266 is to assume it eats flash I do not have. This release went the other way. The same PR series rewrote the noise-c and libsodium forks that had only ever been lightly adapted for embedded targets, and cut them close to in half.

On a d1 mini, turning on API encryption cost 80.9 KB of flash and 1376 bytes of static RAM in 2026.8.0. In 2026.9.0 it costs 41.9 KB and 48 bytes. On an ESP32 AtomU it drops from 65.9 KB to 37.7 KB; on a Pico W, 65.5 KB to 34.7 KB. The handshake itself falls from roughly 580 ms to roughly 300 ms on the ESP8266, and 63 ms to 46 ms on the ESP32.

So the ESP8266 devices I had quietly written off as “too small for this” mostly are not, and they got faster reconnects thrown in. That is the kind of change I like: not a feature, just someone going through old code properly.

Two things to know before you commit

First: there is no key rotation yet. The device only accepts the key it is running, and the CLI only presents the key in the config. Changing the key means a serial flash, or the web_server OTA platform if you have it configured. Losing the key means the same. A rotation mechanism is on the roadmap; it is not here. Put the key in your password manager now, not later.

Second: if you keep the web_server OTA platform alongside encryption:, validation will warn you, because that endpoint accepts the same firmware image over plaintext HTTP. Which rather defeats the exercise. The captive portal’s update page is treated differently and does not warn, because it only exists while the fallback AP is up and it is the intended way to rescue a device that cannot join Wi-Fi.

Worth noting that 2026.9.0 also has real breaking changes in modbus_controller — custom_command, register_count, force_new_range and skip_updates have all been renamed or replaced. If you have a solar inverter or an energy meter on Modbus, read the upgrade checklist before you touch anything. Between this and Home Assistant 2026.9 pulling Modbus into the UI, September has been a rough month for anyone with hand-written Modbus YAML.

I am doing step 1 across my eleven ESPHome devices this weekend and step 2 the weekend after, because I want a week of normal operation between “this device can be updated encrypted” and “this device will accept nothing else”. I will write up whatever goes wrong. Something usually does.

Leave a Reply

Your email address will not be published. Required fields are marked *