Key rotation for OTA encryption: a dedicated CLI command and a Device Builder rotation tool
- Dominant language
- No language data
- Stars
- 0
- Forks
- 0
- PR merge metrics
- No merged PRs in 30d
Description
## Summary
OTA encryption authenticates every upload with the key the device is running and never downgrades to plaintext (esphome/esphome#18489, esphome/esphome#18979). That guarantee is the point of the feature, and it also means changing the key is a two key operation: the new firmware has to be built with the new key and uploaded with the old one. Today the CLI can only present the key in the config, so a key change on a device that requires encryption is done by serial flash or through the web server OTA platform; the docs describe exactly that (esphome/esphome.io#7327). This issue is for giving that operation a supported path in the CLI and in the Device Builder.
## Why now
Changing an OTA credential has never had a good path. The documented way to change an OTA password is an `on_boot` lambda that calls `set_auth_password()` with the new value, installed once and then removed, followed by a second edit to move the new password into `password:` ([Updating the Password](https://esphome.io/components/ota/esphome/#updating-the-password)). That is a workaround, not a workflow, and it has no dashboard equivalent. Encryption replaces the password for most devices, so this is the moment to give key changes a proper path rather than carry the same gap forward.
The plaintext fallback in esphome/esphome#18979 is a migration aid for firmware that predates the offer and is marked `Remove before 2027.3.0`. While it exists, a device without the `ota: encryption:` block still receives its update after a key change: the CLI tries the new key, the handshake fails, and the upload continues in plaintext with a warning. When the fallback is removed, encryption is enforced whenever the CLI has a key, which is the intended end state; this tooling is what makes a key change routine in that state instead of a serial flash. So this should land before 2027.3.0, and the fallback removal should be sequenced after it.
Devices that never change their key are unaffected either way. A device with a runtime provisioned api key and no OTA block is also unaffected on the device side, since its OTA offer follows the live api key and Home Assistant re-provisioning already rotates it.
## CLI
Three pieces, from smallest to largest:
An `--ota-key` style upload flag for advanced users and scripted use, so an upload can present a key other than the one in the config. On its own it leaves the config and `secrets.yaml` edit, the ordering, and the failure handling to the user, which is where mistakes turn into serial flashes, so it is a building block rather than the answer.
A dedicated command, working name `esphome rotate-key config.yaml` (naming to be worked out), that:
- Takes the new key from the user or generates one, and writes it to the config and `secrets.yaml` the way the wizard and Device Builder do.
- Builds with the new key and uploads presenting the old key for the handshake.
- Commits the config change only once the device reports the new firmware, so a failed upload leaves a config that still matches what the device runs.
- Covers both shapes: the bare `ota: encryption:` that inherits the api key, and an explicit OTA `key:` on a device without a static api key (MQTT only devices, runtime provisioned api keys).
- Refuses cleanly on firmware older than 2026.9.0, which does not offer OTA encryption.
A config side form of the same thing may be wanted too: an `old_key:` next to `key:` under `ota: encryption:` that CLI uploads fall back to when the device rejects the current key. The CLI tries `key:` first and `old_key:` second, so a rotation becomes a plain edit and install, and the field is removed by the user or the tooling once the device reports the new firmware. It is uploader side only: the device is built with `key:` alone and never holds two keys, and validation can warn when `old_key:` outlives the rotation.
This also gives the Home Assistant key handoff a safe move: when Home Assistant pushes a new api key, the Device Builder writes the new key to `key:` and keeps the previous one in `old_key:`, whether or not the previous one still works, so the next CLI upload tries the new key first and falls back to the old one instead of being refused.
Uploading with the old key does not weaken anything: whoever holds the current key already has full OTA access, so presenting it is the same authorization the device grants today. No protocol change is needed; espota2 negotiates with whatever PSK it is given, so the work is on the CLI side, letting the upload key differ from the config key for that one run and handling the config update safely.
## Device Builder
The dashboard knows the deployed config and the key it was built with, so it can offer a rotate action on the device page that drives the same sequence: generate, write the secret, compile with the new key, upload with the old, and only then mark the config as deployed.
Two related follow ups: warn before an ordinary install when the key in the draft differs from the key the device was last installed with, since that install will be refused (not part of esphome/device-builder-frontend#1714); and keep the HA key handoff from replacing an explicit OTA key the device still requires (the refusal path in esphome/device-builder#2665).
## Rejected alternatives
- Building the device with both an old and a new key reintroduces two keys per device, which the one key design deliberately avoids; `old_key:` above is different because it never reaches the firmware.
- Having the device accept its previous key for a grace period adds device side state and a window where a retired key still works, which weakens the guarantee for no gain over presenting the old key from the uploader.
## Related
- esphome/esphome#18979: a device with an api key offers OTA encryption and the CLI uses it; the fail closed behaviour with the block is unchanged.
- esphome/esphome.io#7327: documents the current serial flash or web server path in the fail closed callout, to be updated once this lands.
Contributor guide
No contributing guide indexed for this repository
Research direction
Start by locating the CLI upload flow, config and secrets handling, and the Device Builder device-page action described in the issue; also read the espota2 negotiation reference. Done means a rotation can build with the new key, upload with the old key, and update deployment state only after success across both CLI and Device Builder paths.
Written by the indexing model from the issue text.
Assessment
- Domain
- cli, frontend, tooling
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 32/100