| name | iot-security |
| description | IoT security checks for the ESP32 pool-controller - Secure Boot, Flash Encryption, TLS, WPS, secrets management, network security, and 24/7 operational security. Use when asked to audit security, fix vulnerabilities, implement secure communication, or harden the device firmware. 🇩🇪 Deutsche Trigger: IoT Sicherheit, Secure Boot, Flash Verschlüsselung, TLS, WPS, Credentials, Passwörter, Netzwerksicherheit, Härtung, Verwundbarkeiten. |
| keywords | ["iot sicherheit","iot security","secure boot","flash verschlüsselung","flash encryption","tls","wps","credentials","passwörter","passwords","netzwerksicherheit","network security","härten","hardening","verwundbarkeit","vulnerability","eFuse","jtag"] |
IoT Security — Pool Controller
Security audit and hardening for the ESP32 pool-controller. This device controls
230V pool pumps - security failures can cause physical damage or create network
entry points.
🔍 Code Search: Use semble search "setInsecure" or semble search "password"
to find security-sensitive code. semble find-related helps trace credential
flow across components. See Agents.md §7 for full semble usage.
Threat Model
| Threat | Impact | Likelihood | Mitigation |
|---|
| Unauthorized MQTT control | Pump manipulation | Medium | Admin password, network segmentation |
| Firmware reverse engineering | IP theft, credential extraction | Low | Flash Encryption |
| Unauthorized OTA update | Malicious firmware | Low | Secure Boot + signed OTA |
| WiFi credential theft | Network access | Medium | Encrypted storage, no plaintext logs |
| Physical device access | Full compromise | Low | JTAG disable, debug UART off |
| Replay attack on MQTT | State manipulation | Low | TLS + unique client ID |
| Boot-loop attack | Denial of service | Very Low | Safe mode after 3 boots |
1. Flash Encryption & Secure Boot
Referenced in Agents.md §11 — not yet enabled in platformio.ini.
Secure Boot ensures only signed firmware runs:
board_build.secure = secure
board_build.secure_sign_key = secure_boot_signing_key.pem
Flash Encryption protects stored data (WiFi creds, MQTT passwords):
board_build.flash_encrypt = true
⚠ One-time operation: burning eFuses is irreversible. Generate keys offline:
espsecure.py generate_signing_key secure_boot_signing_key.pem
espefuse.py --port /dev/ttyUSB0 burn_key BLOCK_KEY0 flash_encryption_key.bin
Current status: Both are future work. The device currently stores WiFi passwords
in plaintext on LittleFS (/config.json, gitignored in .gitignore:114).
2. TLS for MQTT
Location: NetworkManager.cpp:126-130
Currently uses setInsecure() which skips certificate validation:
secureClient_->setInsecure();
Security gap: No certificate pinning or CA verification. Mitigations:
- For production: Use a real CA or self-signed cert with fingerprint:
secureClient_->setCACert(rootCACertificate);
secureClient_->setCertificate(clientCert);
secureClient_->setPrivateKey(clientKey);
- Low-risk mitigation: Keep on isolated VLAN with firewall rules
- Minimal mitigation: Ensure MQTT broker requires authentication even on LAN
3. Credential Storage
Current practice (ConfigManager.cpp:80-91):
- WiFi SSID/password stored in plaintext on LittleFS
- MQTT username/password stored in plaintext on LittleFS
- Admin password stored as SHA256 hash (no salt — see below)
- WPS credentials persisted to SPIFFS (
WpsProvisioner.cpp:126-127)
Improvements:
- Use ESP32 NVS for credential storage with read-protection
- At minimum:
chmod-equivalent filesystem permissions if supported
- Add salt to password hashing (
Agents.md §11 recommends against secrets in repo)
Password hashing issue (ConfigManager.cpp:17):
static constexpr const char* kDefaultPasswordHash = "8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918";
The default password hash is for "admin" (SHA256). On first login, user should change it. Consider:
- Forcing password change on first login
- Using
mbedtls_md_hmac with a per-device salt
4. Web Portal Security
Location: WebPortal.hpp
- Session timeout: 15 minutes (
kSessionTimeoutMs = 15 * 60 * 1000)
- Session token: Generated server-side
- Password verification: SHA256 hash comparison (
ConfigManager.cpp:182-183)
- AP mode: Open WiFi named "Pool-Controller-Setup" (
NetworkManager.cpp:101)
Risks:
- AP mode has no encryption — anyone within range can connect
- Session timeout is reasonable, but no rate-limiting on login attempts
- No HTTPS on the web portal (ESP32 can support it with server cert)
Audit check: Verify WebPortal.cpp doesn't expose credentials in HTML/JS responses.
5. WPS Provisioning Security
Location: WpsProvisioner.cpp
- Triggered by holding GPIO0 for 2 seconds
- WPS PIN mode: disabled (only PBC — Push Button Config)
- Session timeout: 120 seconds
- Credentials persisted to SPIFFS after successful WPS
Risks:
- WPS PBC is vulnerable to physical attack (someone with 2s GPIO0 access can extract WiFi credentials)
- Credentials stored in SPIFFS in plaintext at
/homie/config.json
- After WPS, the device reconnects with new credentials
6. OTA Update Security
Location: platformio.ini:53-61
OTA is configured but commented out by default:
When enabling OTA:
- Always set a strong
--auth password
- Consider using HTTPS OTA (
esp_https_ota) instead of plain ESP-OTA
- Verify firmware signature before applying (Secure Boot)
- Use dual-partition OTA (factory + OTA) for rollback
7. Debug Interface Security
From Agents.md §11:
Debug Interfaces (JTAG/UART) im Produktions-Build deaktivieren.
For ESP32:
build_flags =
-D CONFIG_SECURE_DISABLE_OCD=1
-D CONFIG_CONSOLE_UART_NONE=1
Current state: Serial console is enabled at 115200 baud, which provides full
system access. For production, consider disabling or restricting serial output.
8. Secure Coding Patterns for This Project
Memory Safety
- No
new/delete in hot-paths (existing rule in Agents.md)
unique_ptr for rule ownership (OperationModeNode.hpp:84) ✓
- RAII for all resources (
PoolController.hpp:11-21) ✓
Input Validation
- WiFi SSID/MQTT config validated at entry points? Check
WebPortal.cpp handlers
- MQTT message payload size: PubSubClient has internal buffer limits
- JSON deserialization uses
size_t limits (ConfigManager.cpp:69)
Rate Limiting
- No login rate-limiting on web portal
- MQTT reconnect backoff: 5s fixed interval — add jitter for production
- Sensor polling: already rate-limited by
TEMP_READ_INTERVAL
9. Security Audit Checklist
10. GitHub CodeQL Code Scanning
The project runs CodeQL static analysis on every push/PR
via .github/workflows/codeql-analysis.yml. Alerts appear at:
https://github.com/smart-swimmingpool/pool-controller/security/code-scanning
Rule Categories Most Relevant to This Project
| CodeQL Rule | Severity | What It Catches | Where It Hit |
|---|
cpp/potentially-dangerous-function | Critical | localtime(), asctime() -- not thread-safe | Timer.cpp, NorviOledDisplay.cpp, Rule.hpp (fixed) |
cpp/comparison-with-wider-type | High | Narrow vs wide type in loop condition | Homie library (path-excluded) |
cpp/commented-out-code | Note | Dead code in source | src/ -- remove before merge |
cpp/short-global-name | Note | Globals with < 5 char names | TimeClientHelper.cpp |
cpp/useless-expression | Warning | Expression without side effects | Library code (path-excluded) |
cpp/equality-on-floats | Note | == on float values | Library code (path-excluded) |
cpp/bitwise-sign-check | Warning | Sign check of bitwise op | OneWire library (path-excluded) |
cpp/constant-comparison | Warning | Comparison always same result | Homie library (path-excluded) |
Handling CodeQL Alerts
- Fix the issue in
src/ -- our own code is always fixable.
- Dismiss with "won't fix" for third-party library code in
.pio/libdeps/.
- Path-exclude library directories via
.github/codeql-config.yml (paths-ignore).
- Re-scan by pushing to the branch that triggered the alert. Alerts auto-close on
the next successful scan of
main when the alert no longer reproduces.
Pre-Merge CodeQL Gate
Before merging a PR:
Configuration
- Config:
.github/codeql-config.yml -- excludes .pio/libdeps/**/*
- Workflow:
.github/workflows/codeql-analysis.yml
- Runs on: push to
main, PRs to main, and weekly schedule
11. Network Segmentation Recommendation
[Pool Controller] ──WiFi──┐
▼
┌──────────────┐
│ IoT VLAN │
│ 10.0.x.0/24 │
├──────────────┤
│ MQTT Broker │
│ (auth + TLS) │
└──────┬───────┘
│ firewall rule: only MQTT:8883
▼
┌──────────────┐
│ Main LAN │
│ Home Ass. │
└──────────────┘
The pool controller should be on an isolated IoT VLAN with:
- Outbound: MQTT broker only (port 8883 TLS)
- Inbound: Nothing (no SSH, no HTTP from main LAN)
- DHCP: Static lease for monitoring
📚 References & Best Practices
Security Standards & Guidelines
ESP32 Specific Security
Network Security
Practical Implementation Guides
Tools & Scanners
- Gitleaks -
Secret detection in git repositories
- CodeQL -
Static analysis for security vulnerabilities
- MegaLinter -
Multi-language linting with security checks (OX Security)
🔍 Analysis Note: This skill was enhanced during the comprehensive IoT security
analysis performed by Vibe Code on 2025-01-15. See
PR #112
for implementation details.