Security & Dependencies

Adding a New Module

  1. Create lib/thesada-mod-newmodule/src/NewModule.h and NewModule.cpp
  2. Inherit from Module, implement begin(), loop(), name()
  3. Use EventBus::publish() to emit data, EventBus::subscribe() to react
  4. Add #define ENABLE_NEWMODULE to thesada_config.h
  5. Add config block to data/config.json if needed
  6. Add MODULE_REGISTER(NewModule, ModulePriority::SENSOR) at the bottom of the .cpp file
  7. Create a library.json in the module directory (see existing modules for template)

No other files touched. ModuleRegistry.cpp has zero module-specific code - self-registration happens via the MODULE_REGISTER macro in the module file.


Security

Control Implementation
Dashboard + /api/state + /api/info Public (read-only sensor data)
All admin endpoints Bearer token or HTTP Basic Auth (backwards compatible)
Token auth POST /api/login with Basic Auth returns a 1-hour Bearer token (max 4 concurrent tokens)
Rate limiting /api/login and /api/auth/check: 5 failed attempts per source IP triggers a 30 s lockout (returns 429). Table holds 16 IPs.
WebSocket terminal Requires prior GET /api/ws/token (auth-gated); server records caller IP as authorized for 30 s (one-time use)
Path traversal /api/file rejects any path containing ..
TLS MQTT and OTA load the CA from /ca.crt on LittleFS. If that file is missing or empty, both subsystems fall back to a PROGMEM bundle baked into the firmware (common public TLS roots). The rotation path stays flash-based; the bundle is a safety net for wiped data partitions. See TLS exceptions below.

Token auth flow:

1. POST /api/login  (Authorization: Basic base64(user:pass))
   -> {"ok":true, "token":"<32-char-hex>", "expires_in":3600}
2. All admin requests: Authorization: Bearer <token>
3. Token stored in sessionStorage (persists across page refresh, cleared on tab close)
4. On device reboot, stale tokens are detected and login is re-prompted
5. Basic Auth still accepted on all admin endpoints (for curl, scripts, backwards compat)

WebSocket auth flow:

1. JS calls GET /api/ws/token  (Authorization: Bearer <token>)
2. Server records remoteIP -> authorized for 30 s
3. JS opens ws://device/ws/serial  (no credentials in URL)
4. WS_EVT_CONNECT: server checks remoteIP against grant table -> allow or close

Unauthenticated WebSocket connections (e.g. direct curl or wscat) are accepted at TCP level (101 Switching Protocols) then immediately closed with a WS close frame. The rejection is logged as [WRN][WebServer] WS: rejected - not pre-authorized.

Note: The web interface uses HTTP, not HTTPS. Admin credentials transit in cleartext on the LAN. For internet-exposed deployments, put the device behind a reverse proxy with TLS termination.

TLS exceptions

Not all outbound connections use /ca.crt. These paths use setInsecure() (TLS without certificate validation):

Path Reason Risk
MQTT before NTP sync Cert validation requires a valid system clock. Pre-NTP, the device connects insecure and upgrades to cert-validated once NTP syncs. First-boot MITM on untrusted networks. Low risk on LAN.
MQTT on low-heap boards A board with less than ~40 KB max contiguous heap cannot allocate for the TLS cert context. The connection stays on setInsecure() permanently when the upgrade is unsafe. No cert validation on constrained boards.
Telegram Bot API setInsecure() to avoid heap fragmentation that kills Telegram after a few alerts. Bot tokens are bearer creds visible to a MITM. Low risk on trusted upstream.

MQTT CLI trust model

cli/lua.exec runs arbitrary Lua with the full standard library (including io and os). Anyone with MQTT broker credentials can read /config.json (contains WiFi, MQTT, Telegram credentials in plaintext), overwrite /ca.crt, or access the filesystem. MQTT broker credentials therefore gate everything on the device - treat them with the same trust level as local root access.

Captive-portal auth notes

  • In AP mode (fallback setup): auth is skipped entirely so the user can configure WiFi. Anyone in radio range of the AP can read/write config until the device joins a real network again.
  • Empty web credentials: if web.user and web.password are both empty in config.json, admin endpoints are unprotected. A warning is logged at boot.

Hardware Watchdog

The firmware enables the ESP32 Task Watchdog Timer (30s timeout) at boot. If loop() fails to feed the watchdog within 30 seconds (hang, infinite loop, memory corruption), the device automatically reboots.

esp_task_wdt_init(30, true);   // 30s timeout, panic on expire
esp_task_wdt_add(NULL);        // monitor the loopTask
esp_task_wdt_reset();          // fed every loop() cycle

CI/CD

GitHub Actions pipeline (.github/workflows/ci.yml):

  • Every push to dev or main: builds the production OWB binary plus the debug variants (esp32-owb, esp32-owb-debug, esp32-s3-debug) and uploads them as artifacts.
  • Push to main with a new version: auto-creates a GitHub release with the production binary, the rescue binary, and a manifest pointer.
  • Existing version: release step is skipped (no duplicates).
  • Rescue envs (esp32-owb-rescue, esp32-s3-debug-rescue) are built on demand only - manual recovery flow.

Git workflow:

  1. Develop on dev - CI catches compile errors on every push
  2. When ready to release: bump FIRMWARE_VERSION in thesada_config.h, merge dev to main
  3. CI builds and creates the GitHub release automatically
  4. Production nodes pick up the new version via OTA

Dependencies

Library Version Purpose
Arduino framework (ESP32) espressif32 @ 6.13.0 Base framework
ArduinoJson 7.4.3 JSON config + event payloads
LittleFS built-in Filesystem (config, CA cert, Lua scripts)
PubSubClient 2.8 WiFi MQTT client
ESPAsyncWebServer git (ESP32Async) Web server + WebSocket (pulls AsyncTCP transitively)
ESP-Arduino-Lua git Lua 5.3 runtime (GPL-3.0)
TinyGSM 0.12.0 AT command modem driver
XPowersLib git AXP2101 PMU control
DallasTemperature + OneWire 4.0.6 / 2.3.8 DS18B20 sensors
Adafruit ADS1X15 2.6.2 ADS1115 ADC
HTTPClient + WiFiClientSecure built-in OTA manifest fetch + TLS
mbedtls built-in SHA256 verification for OTA + config drift detection

espressif32 6.13.0 requires intelhex in the PlatformIO Python environment (used to build the bootloader). Install once:

~/.local/pipx/venvs/platformio/bin/python -m pip install intelhex

Thesada - GPL-3.0-only (firmware) / CC BY-NC-SA 4.0 (docs) - License

This site uses Just the Docs, a documentation theme for Jekyll.