Skip to content

fix(firmware): OTA PSK cannot be provisioned — documented provision.py --ota-psk does not exist #1753

Description

@proffesor-for-testing

Summary

OTA firmware upload cannot be enabled on any node built from the shipped source, because the
only documented way to provision the device-side PSK does not exist.

main/ota_update.c:29-31 reads the pre-shared key from NVS namespace security, key
ota_psk. When it is empty, ota_check_auth() rejects every upload (fail-closed, RuView#596).
The comment at main/ota_update.c:39 tells the operator how to set it:

The OTA server still starts so the operator can provision.py --ota-psk <hex> over USB-CDC
without a reflash, but the upload endpoint will reject every request until the PSK is set.

firmware/esp32-csi-node/provision.py has no --ota-psk argument, and contains no reference
to ota_psk or the security namespace at all — it generates only the csi_cfg namespace
(from provision.py:177). The only other ota_psk in the tree is in
v2/crates/wifi-densepose-desktop (src/domain/config.rs:26,
ui/src/pages/Settings.tsx:138), which is the client-side token the desktop app sends,
not something written to device NVS.

Net effect: OTA=ready in the boot log means only that the HTTP server started
(main/main.c:371, :504); authentication can never be satisfied through any shipped
workflow.

Why now

Observed while testing v0.8.4 on hardware. Nodes are otherwise healthy and streaming, but the
only non-USB update path is unusable, so every firmware change requires physical access.

Reproduction

On a node flashed from the published v0.8.4 assets and joined to WiFi:

$ curl -X POST --data-binary @esp32-csi-node-v0.8.4-8mb.bin \
       -H 'Content-Type: application/octet-stream' \
       http://<node-ip>:8032/ota
HTTP 403 after ~135 KB uploaded
Authentication required. Use: Authorization: Bearer <psk>
$ python3 provision.py --help | grep -c ota-psk
0
$ grep -c 'ota_psk\|security' provision.py
0

GET /ota/status on the same node responds normally, so the server is up:

{"version":"0.8.4","running_partition":"ota_0","next_partition":"ota_1","max_size":921600}

Done when

  • provision.py can write security/ota_psk to device NVS over USB-CDC without a reflash,
    matching the behaviour ota_update.c:39 already documents; or
  • the firmware comment is corrected to name whatever the supported path actually is.

Work items

  • Add --ota-psk <hex> to provision.py, emitting the security namespace alongside
    csi_cfg in the generated NVS CSV.
  • Validate the value (the firmware caps it at OTA_PSK_MAX_LEN 65, hex-encoded SHA-256).
  • Decide whether the PSK belongs in the per-port state file — if so it needs the same
    protection as the WiFi password (see the state-file permissions issue).
  • Consider making the boot log distinguish "OTA server started" from "OTA can accept
    uploads", since OTA=ready currently implies the latter.

Secondary, untested

/ota/status advertises max_size: 921600 while the shipped v0.8.4 image is 1,126,624 bytes
and ota_1 is 2,097,152 bytes. Whether that gate would reject a valid image could not be
tested, because the auth gate rejects first. Flagging only; it may be a stale constant.

Activity

  1. added
    bugSomething isn't working
    securitySecurity-related issues and hardening
    on Sep 1, 2026
  2. ruvnet commented on Sep 30, 2026

    @ruvnet
    Owner

    provision.py has no --ota-psk flag and no reference to the security/ota_psk NVS key, making the OTA security feature unreachable without direct NVS manipulation. This means every firmware change currently requires physical USB access.

    The fix is to add to provision.py:

    parser.add_argument('--ota-psk', help='Pre-shared key written to security/ota_psk NVS namespace')

    And in the write path, write to the security namespace:

    if args.ota_psk:
        nvs_write(port, namespace='security', key='ota_psk', value=args.ota_psk)

    On the boot log: OTA=ready should be split into two distinct states — OTA=listening (HTTP server started) vs OTA=armed (PSK loaded from NVS and endpoint is authenticated). A node with no PSK in NVS should log OTA=unarmed so operators know uploads will be rejected before attempting a flash.


    Generated by Claude Code

  3. aepod commented on Oct 2, 2026

    @aepod
    Contributor

    Confirmed on 0.8.12, on an ESP32-S3 and an ESP32-C6. With no security/ota_psk in NVS, POST /ota fails closed as intended (#623), and the firmware log tells the operator to run provision.py --ota-psk, which main's provision.py doesn't accept. MEASURED. The missing flag on main is CODE-DERIVED at 03a89793 (provision.py unchanged).

    Environment: S3 and C6 nodes, 0.8.12 from source; macOS host with esptool 5.4.0 and esp-idf-nvs-partition-gen 0.3.0.

    python firmware/esp32-csi-node/provision.py --help | grep -i psk   # nothing on main
    curl -s -o /dev/null -w '%{http_code}\n' -X POST "http://<node-ip>:8032/ota" \
      --data-binary @esp32-csi-node.bin     # rejected: no key in NVS
  4. ruvnet commented on Oct 10, 2026

    @ruvnet
    Owner

    The missing --ota-psk flag is listed as open work in #2165 (follow-up tracking for the aepod community batch). No fix has landed yet — watch #2165 for progress.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingfirmwareESP32 firmwaresecuritySecurity-related issues and hardening

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions