Security & Dependencies
Adding a New Module
- Create
lib/thesada-mod-newmodule/src/NewModule.handNewModule.cpp - Inherit from
Module, implementbegin(),loop(),name() - Use
EventBus::publish()to emit data,EventBus::subscribe()to react - Add
#define ENABLE_NEWMODULEtothesada_config.h - Add config block to
data/config.jsonif needed - Add
MODULE_REGISTER(NewModule, ModulePriority::SENSOR)at the bottom of the.cppfile - Create a
library.jsonin 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.userandweb.passwordare both empty inconfig.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
devormain: builds the production OWB binary plus the debug variants (esp32-owb,esp32-owb-debug,esp32-s3-debug) and uploads them as artifacts. - Push to
mainwith 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:
- Develop on
dev- CI catches compile errors on every push - When ready to release: bump
FIRMWARE_VERSIONinthesada_config.h, merge dev to main - CI builds and creates the GitHub release automatically
- 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 |
espressif326.13.0 requiresintelhexin the PlatformIO Python environment (used to build the bootloader). Install once:~/.local/pipx/venvs/platformio/bin/python -m pip install intelhex