Email code@ilia.ae (subject zignode security). Please do not open a public issue for a
vulnerability. Include the board, firmware version, and how to reproduce. You'll get an
acknowledgement; fixes for confirmed issues ship in a patch release with credit if you want it.
This is a DIY, LAN-local Zigbee/Wi-Fi pump controller — not an internet-exposed device. The safety-critical property is the pump cannot run away or run dry, and that is defended in firmware regardless of the security posture:
- Pump parked OFF before radios; every ON bounded by a hard max-runtime watchdog + fail-safe drive.
- Dry-run gate, min-off cooldown, and an OTA maintenance lock (no update while pumping).
- OTA images are health-gated with bootloader rollback (
CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE). - The Wi-Fi/web panel is a self-closing maintenance window by default; its auth is optional.
Three independent layers, none of which require burning an eFuse:
- Image validity —
esp_ota_end()verifies the ESP-IDF image structure and its appended SHA-256 before anything can be marked bootable, so a corrupt or truncated-mid-header upload is rejected outright. - End-to-end digest (Wi-Fi OTA) — the panel hashes the
.binin the browser and sends it as anX-SHA-256request header; the device hashes every byte it accepts and aborts beforeesp_ota_end()/esp_ota_set_boot_partition()on any mismatch, so a mismatched image never becomes bootable. This catches what layer 1 cannot: a valid image that simply isn't the one you meant to send. The digest is logged and echoed in the HTTP response either way. Caveat:crypto.subtleis only available in a secure context, so over plainhttp://zignode.local/the browser cannot compute the digest — the upload still proceeds and the device still reports the hash it stored; you compare it manually. Supply the header yourself for an enforced check:curl -sS --data-binary @zignode.bin -H "X-SHA-256: $(shasum -a 256 zignode.bin | cut -d' ' -f1)" \ http://zignode.local/api/ota - Runtime attestation —
GET /api/statusreportsapp_sha(the running image's ELF SHA-256) andidf_ver, shown in the panel as Running image. A version string can't distinguish two builds of "2.0.0"; the hash can. Compare it against your build output to confirm exactly what is executing.
Plus the health-gated rollback: a new image must prove itself after reboot or the bootloader reverts to the previous one.
Not provided: authenticity. Any image with a matching digest is accepted — the digest proves integrity, not provenance. For a deployment that needs signed images without the eFuse risk of full Secure Boot, enable ESP-IDF's signed-app verification:
CONFIG_SECURE_SIGNED_APPS_NO_SECURE_BOOT=y
CONFIG_SECURE_SIGNED_ON_UPDATE_NO_SECURE_BOOT=y
This makes the OTA path verify an ECDSA signature against a key you control, and — unlike the
options below — burns no eFuses and stays reversible. Note that the web panel's Basic auth is
off by default (ZIGNODE_WEB_PASSWORD is empty), so on an untrusted LAN set a password and
prefer the self-closing maintenance window.
Secure Boot v2 and Flash Encryption are not enabled by default, on purpose:
- They burn irreversible eFuses — a mistake bricks the board permanently.
- They break the project's easy USB reflash and complicate the OTA + rollback flow that is the actual safety mechanism here.
- For a DIY pump on a home LAN the threat they address (an attacker with physical flash access cloning/altering the image) is low, and the cost/risk is high.
If a deployment needs them (e.g. a commercial build), enable per the ESP-IDF guides and test OTA + rollback end-to-end first:
CONFIG_SECURE_BOOT=y
CONFIG_SECURE_BOOT_V2_ENABLED=y
CONFIG_SECURE_FLASH_ENC_ENABLED=y
Mind that signed OTA images and a matching signing key become mandatory, and rollback interacts with the anti-rollback secure-version counter.
Releases are GPG-signed git tags; LICENSE/COPYRIGHT/CITATION.cff carry OpenTimestamps proofs.