Security Model¶
This page documents C3PO's security architecture: encryption, authentication, web security, and production hardening.
End-to-End Encryption¶
All agent ↔ C3PO communication is encrypted with ChaCha20-Poly1305 AEAD.
Cipher Suite¶
| Component | Algorithm | Key Size |
|---|---|---|
| Encryption | ChaCha20 | 256-bit |
| Authentication | Poly1305 | 128-bit tag |
| Key Derivation | HKDF-SHA256 | 256-bit output |
| Nonce | Random | 96-bit (12 bytes) |
Properties¶
- Confidentiality: ChaCha20 stream cipher
- Integrity: Poly1305 MAC — any tampering invalidates the tag
- Authenticity: Only holders of the master key can encrypt/decrypt
- Replay resistance: Random 96-bit nonce per message
- Per-device isolation: HKDF derives unique keys from
master_key + device_id
Key Derivation¶
Master Key (32 bytes, per device)
│
└── HKDF-SHA256
├── salt: device_id (UTF-8)
├── info: "espilon-c2-v1"
└── output: 32-byte encryption key
Even if two devices share the same master key (unlikely), their derived encryption keys differ because the device ID serves as the HKDF salt.
Mutual Authentication¶
HELLO Handshake¶
The connection handshake provides mutual authentication:
- Agent proves identity: Sends
HELLO:<device_id> - Server proves key possession: Encrypts a random 32-byte challenge with the device's key and sends it back
- Agent proves key possession: Successfully decrypts the challenge
If the device ID is unknown or the key doesn't match, the connection is dropped.
Failed Authentication¶
- Unknown device ID → connection closed immediately
- Decryption failure (wrong key) → message silently dropped
- No error messages are returned (prevents oracle attacks)
Key Management¶
Master Key Storage¶
| Location | Format | Purpose |
|---|---|---|
| ESP32 NVS | Binary 32 bytes | Agent-side key (factory partition, namespace crypto, key master_key) |
keys.json |
Hex-encoded JSON | Server-side key store |
Key Lifecycle¶
deploy.py provision
│
├── Generate random 32-byte key (or use provided hex)
├── Write to ESP32 factory NVS partition (0x10000)
└── Store in keys.json (device_id → hex_key)
keys.json Format¶
Critical Asset
keys.json is the most sensitive file in the deployment. If lost:
- You cannot communicate with provisioned devices
- Devices must be re-provisioned (re-flashed)
Always back up keys.json after provisioning devices.
Hot-Reload¶
If a device connects with an ID not in memory, C3PO re-reads keys.json from disk. This allows provisioning new devices via deploy.py without restarting C3PO.
Web Security¶
Session Management¶
| Feature | Implementation |
|---|---|
| CSRF protection | Random secrets.token_hex(32) per login page |
| Session fixation | session.clear() before setting logged_in |
| Credential comparison | hmac.compare_digest() (constant-time) |
| Session cookie | Flask default (HttpOnly, server-side) |
Authentication Modes¶
Session-based (browser):
Bearer token (API):
Dual auth (API endpoints): Accept either session cookie OR Bearer token.
Input Validation¶
All API inputs are validated with strict patterns:
| Field | Validation | Limit |
|---|---|---|
device_id |
^[a-zA-Z0-9_-]{1,32}$ |
32 chars |
command |
^[a-zA-Z][a-zA-Z0-9_-]{0,63}$ |
64 chars |
argv |
String array | Max 10 items, 256 chars each |
device_ids |
Array or "all" |
Max 100 devices |
hostname |
^[a-zA-Z0-9_-]{1,63}$ |
63 chars |
modules |
Known set only | Boolean values only |
| Firmware upload | .bin extension |
Validated MIME type |
Rate Limiting¶
Per-IP rate limiting via Flask-Limiter (in-memory storage):
| Endpoint | Limit | Purpose |
|---|---|---|
| All API endpoints | 200/minute | General protection |
POST /login |
5/minute | Brute-force prevention |
POST /api/commands |
60/minute | Command flooding |
POST /api/ota/deploy |
10/minute | Deploy abuse |
POST /api/ota/upload |
5/minute | Upload abuse |
POST /api/ota/build/start |
3/minute | Build resource protection |
Rate limit exceeded → HTTP 429 with JSON error body.
CORS¶
Configurable origin whitelist via CORS_ALLOWED_ORIGINS:
Originheader validated against whitelist- OPTIONS preflight returns 204
- Headers:
Access-Control-Allow-Origin,-Headers,-Methods,-Credentials - Empty whitelist = allow all origins (development only)
TCP Security¶
Buffer Limits¶
| Limit | Value | Purpose |
|---|---|---|
| Max buffer size | 1 MB | Prevents memory exhaustion |
| Max concurrent connections | 50 | ThreadPoolExecutor limit |
| Idle timeout | 300 seconds | Closes stale connections |
Frame Validation¶
- Only alphanumeric +
-_for device IDs - Only valid Base64 characters for payload
- Line-delimited (
\n) for framing - Invalid frames are silently dropped
Silent Failure Policy¶
C3PO never reveals information about failures to unauthenticated clients:
- Wrong device ID → connection closed
- Wrong key → message dropped (no error sent)
- Invalid frame format → dropped
- Decryption failure → dropped
This prevents enumeration attacks and timing oracles.
Firmware Security¶
OTA Updates¶
| Feature | Implementation |
|---|---|
| Transport | HTTPS by default (ESP-IDF TLS) |
| HTTP fallback | Opt-in via CONFIG_ESPILON_OTA_ALLOW_HTTP |
| Certificate bundle | Full mbedTLS certificate bundle when HTTPS |
| Short URL auth | Token parameter: /api/ota/fw/<file>?token=<MULTILAT_AUTH_TOKEN> |
Factory NVS Partition¶
The master key is stored in a dedicated NVS partition:
This partition is separate from the application partitions and survives OTA updates (only the app partitions are overwritten).
Production Hardening Checklist¶
Required Changes¶
- Change all default secrets in
.env:FLASK_SECRET_KEY— random 64+ character stringWEB_USERNAME/WEB_PASSWORD— strong credentialsCAMERA_SECRET_TOKEN— random tokenMULTILAT_AUTH_TOKEN— random token
- Remove
ESPILON_ALLOW_DEFAULTS=1if set
Recommended¶
- HTTPS reverse proxy: Put C3PO behind nginx/Caddy with TLS
- Firewall: Restrict
:2626(C2) and:5000(camera) to agent IPs only - Network segmentation: C3PO on isolated management VLAN
- Restrict CORS origins: Set specific origins, not empty (allow-all)
- Backup
keys.json: Encrypted backup after each provisioning - Log monitoring: Watch for repeated auth failures
- Docker secrets: Use Docker secrets instead of
.envfor sensitive values
Network Diagram (Production)¶
┌─────────────┐ ┌──────────────┐ ┌──────────────┐
│ Internet / │ │ Reverse │ │ C3PO │
│ Agent Net ├────►│ Proxy ├────►│ Container │
│ │ │ (nginx+TLS) │ │ │
│ │ │ :443 │ │ :8000 (web) │
│ ├─────┤──────────────┤────►│ :2626 (c2) │
│ ESP32 │ │ Firewall │ │ :5000 (udp) │
│ Agents │ │ (iptables) │ │ │
└─────────────┘ └──────────────┘ └──────────────┘
Threat Model¶
| Threat | Mitigation |
|---|---|
| Man-in-the-middle | ChaCha20-Poly1305 AEAD on all agent traffic |
| Key compromise | Per-device keys — compromise of one device doesn't affect others |
| Replay attacks | Random 96-bit nonce per message |
| Brute-force login | 5/minute rate limit on /login |
| API abuse | Per-IP rate limiting (200/min default) |
| Buffer overflow | 1 MB TCP buffer limit |
| Command injection | Strict regex validation on all inputs |
| Session fixation | Session cleared before login |
| CSRF | Token-based protection on login form |
| Timing attacks | Constant-time credential comparison |
| Device enumeration | Silent failure on unknown devices |
| Firmware tampering | HTTPS OTA with certificate pinning option |
See also
- Architecture — Protocol and crypto details
- Installation —
.envconfiguration - Deploy — Key provisioning pipeline