docs: security hardening plan v1.0
Addresses all 4 open CVEs with specific implementations: - CVE-2: Per-device WiFi password derived from serial/MAC + salt - CVE-11: SSH key-only auth, ADACAM_PUBKEY injection at liberation - CVE-14: adacam-api bearer token derived from device serial - CVE-7/16: Kill usb-updater, no OTA for single-owner device Also covers: firewall rules, tunnel security assessment, data-at-rest recommendations, priority order for implementation.
This commit is contained in:
parent
86188ef54f
commit
9c4b0e26b9
1 changed files with 272 additions and 0 deletions
272
security/HARDENING_PLAN.md
Normal file
272
security/HARDENING_PLAN.md
Normal file
|
|
@ -0,0 +1,272 @@
|
||||||
|
# AdaCam Security Hardening Plan
|
||||||
|
|
||||||
|
**Status:** v1.0 — 2026-03-14
|
||||||
|
**Based on:** 16 CVEs found in Hivemapper firmware
|
||||||
|
**12 already addressed by liberate.sh. 4 open items documented below.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Threat Model
|
||||||
|
|
||||||
|
### Tier 1: WiFi Proximity Attacker (PRIMARY THREAT)
|
||||||
|
Someone in range of the AP — parked next to you, neighbor, parking lot. Can see SSID, attempt to connect, sniff traffic. This is the realistic threat. WiFi password and AP-accessible services are the attack surface.
|
||||||
|
|
||||||
|
### Tier 2: Physical Attacker
|
||||||
|
Someone with hands on the device — theft, break-in, impound lot. Can access USB port, serial console, eMMC. Data at rest and the USB update path are the attack surface.
|
||||||
|
|
||||||
|
### Tier 3: Hivemapper Post-Liberation
|
||||||
|
mender-client killed. globalconfig blocked in /etc/hosts. They can't push updates or reach the device anymore. Attack surface: none if liberation was done correctly.
|
||||||
|
|
||||||
|
### Tier 4: Internet Attacker
|
||||||
|
Device has no inbound ports. Can't reach it directly. Would need to compromise Rackham first to abuse the reverse tunnel. Low likelihood.
|
||||||
|
|
||||||
|
**What we're NOT defending against:** Nation-state physical attacks (JTAG/chip-off), phone compromise while on AP, supply chain attacks on factory firmware.
|
||||||
|
|
||||||
|
**Bottom line:** WiFi proximity is the real threat. Everything else is handled or requires physical access you can't fully prevent on a dashcam.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## CVE Remediation
|
||||||
|
|
||||||
|
### CVE-2: WiFi Password — Per-Device Derivation
|
||||||
|
|
||||||
|
**Problem:** `adacam2026` is static across all liberated devices. One leak exposes everyone.
|
||||||
|
|
||||||
|
**Fix:** Derive per-device password from hardware serial at liberation time.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# In liberate.sh
|
||||||
|
SERIAL=$(cat /proc/device-tree/serial-number 2>/dev/null | tr -d '\0')
|
||||||
|
[ -z "$SERIAL" ] && SERIAL=$(ip link show wlp1s0f0 | grep link/ether | awk '{print $2}' | tr -d ':')
|
||||||
|
|
||||||
|
# 12-char derived password
|
||||||
|
WIFI_PASS=$(echo -n "adacam-${SERIAL}-2026" | sha256sum | cut -c1-12)
|
||||||
|
|
||||||
|
# Apply
|
||||||
|
sed -i "s/^wpa_passphrase=.*/wpa_passphrase=${WIFI_PASS}/" /var/hostapd-2g.conf
|
||||||
|
|
||||||
|
# Save for Varroa pairing
|
||||||
|
echo "$WIFI_PASS" > /data/adacam/wifi_password
|
||||||
|
chmod 600 /data/adacam/wifi_password
|
||||||
|
```
|
||||||
|
|
||||||
|
**Varroa gets the password:** liberate.sh prints it at the end. User notes it. Formula is public — deterministic from serial printed on device label.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### CVE-11: SSH — Key-Based Auth Only
|
||||||
|
|
||||||
|
**Problem:** root SSH with no password on an open AP is indefensible.
|
||||||
|
|
||||||
|
**Fix:** Disable password auth entirely. Key injection at liberation time.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# In liberate.sh
|
||||||
|
sed -i 's/^#*PermitRootLogin.*/PermitRootLogin prohibit-password/' /etc/ssh/sshd_config
|
||||||
|
sed -i 's/^#*PasswordAuthentication.*/PasswordAuthentication no/' /etc/ssh/sshd_config
|
||||||
|
sed -i 's/^#*PermitEmptyPasswords.*/PermitEmptyPasswords no/' /etc/ssh/sshd_config
|
||||||
|
|
||||||
|
mkdir -p /root/.ssh && chmod 700 /root/.ssh
|
||||||
|
|
||||||
|
# Inject user's key (pass as ADACAM_PUBKEY env var)
|
||||||
|
if [ -n "$ADACAM_PUBKEY" ]; then
|
||||||
|
echo "$ADACAM_PUBKEY" >> /root/.ssh/authorized_keys
|
||||||
|
chmod 600 /root/.ssh/authorized_keys
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Device generates its own tunnel key (recovery path via Rackham)
|
||||||
|
ssh-keygen -t ed25519 -f /data/adacam/ssh/tunnel_key -N "" -C "adacam-tunnel"
|
||||||
|
cat /data/adacam/ssh/tunnel_key.pub >> /root/.ssh/authorized_keys
|
||||||
|
|
||||||
|
systemctl restart sshd
|
||||||
|
```
|
||||||
|
|
||||||
|
**Usage:**
|
||||||
|
```bash
|
||||||
|
ADACAM_PUBKEY="ssh-ed25519 AAAA... cobb@phone" bash liberate.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
**Recovery:** If personal key is lost, SSH in via Rackham tunnel (which uses the device's own tunnel key).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### CVE-14: adacam-api Config Endpoint — Bearer Token Auth
|
||||||
|
|
||||||
|
**Problem:** Config endpoint unauthenticated — anyone on AP can reconfigure device.
|
||||||
|
|
||||||
|
**Fix:** Bearer token derived from device serial. Same formula on device and Varroa app.
|
||||||
|
|
||||||
|
```python
|
||||||
|
import hashlib
|
||||||
|
|
||||||
|
def get_api_token():
|
||||||
|
serial = open('/data/adacam/device_serial').read().strip()
|
||||||
|
return hashlib.sha256(f"adacam-api-{serial}-token".encode()).hexdigest()[:32]
|
||||||
|
|
||||||
|
def require_auth(f):
|
||||||
|
from functools import wraps
|
||||||
|
from flask import request, jsonify
|
||||||
|
@wraps(f)
|
||||||
|
def decorated(*args, **kwargs):
|
||||||
|
token = request.headers.get('Authorization', '').replace('Bearer ', '')
|
||||||
|
if token != get_api_token():
|
||||||
|
return jsonify({'error': 'unauthorized'}), 401
|
||||||
|
return f(*args, **kwargs)
|
||||||
|
return decorated
|
||||||
|
```
|
||||||
|
|
||||||
|
**Endpoints requiring auth:** `POST /config`, `POST /capture`, `GET /frames`
|
||||||
|
**No auth needed:** `GET /status`, `GET /deviceinfo`, `GET /pair`
|
||||||
|
|
||||||
|
**Varroa pairing flow:**
|
||||||
|
1. Connect to AdaCam WiFi
|
||||||
|
2. `GET /pair` → returns `{"serial": "ABC123"}`
|
||||||
|
3. App derives token: `sha256("adacam-api-ABC123-token")[:32]`
|
||||||
|
4. App uses token for all subsequent requests — no storage needed, deterministic
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### CVE-7/16: Firmware Updates — Kill It
|
||||||
|
|
||||||
|
**Problem:** Mender has no signing key. usb-updater installs anything without verification.
|
||||||
|
|
||||||
|
**Decision: Kill both. No OTA for adacam.**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# In liberate.sh
|
||||||
|
systemctl mask usb-updater 2>/dev/null || true
|
||||||
|
rm -f /usr/bin/usb-updater
|
||||||
|
|
||||||
|
# mender-client already masked
|
||||||
|
# Belt and suspenders — also block update endpoints
|
||||||
|
echo "0.0.0.0 s3.amazonaws.com" >> /etc/hosts # blocks firmware bucket
|
||||||
|
```
|
||||||
|
|
||||||
|
**Rationale:** Single owner, SSH access available, no need for OTA infrastructure. Updates = SSH in and redeploy services. If this becomes a multi-user product, revisit with proper signing.
|
||||||
|
|
||||||
|
**Update procedure:**
|
||||||
|
```bash
|
||||||
|
ssh root@10.77.0.1 "cd /opt/adacam && git pull && systemctl restart adacam-api adacam-capture"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Firewall Rules
|
||||||
|
|
||||||
|
Factory firmware has zero iptables rules. Add to liberate.sh:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cat > /data/persist/firewall.sh << 'FIREWALL'
|
||||||
|
#!/bin/bash
|
||||||
|
iptables -F && iptables -X
|
||||||
|
iptables -P INPUT DROP
|
||||||
|
iptables -P FORWARD DROP
|
||||||
|
iptables -P OUTPUT ACCEPT
|
||||||
|
|
||||||
|
# Loopback
|
||||||
|
iptables -A INPUT -i lo -j ACCEPT
|
||||||
|
|
||||||
|
# Established
|
||||||
|
iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
|
||||||
|
|
||||||
|
# AP interface only — SSH and API
|
||||||
|
iptables -A INPUT -i wlp1s0f0 -p tcp --dport 22 -j ACCEPT # SSH
|
||||||
|
iptables -A INPUT -i wlp1s0f0 -p tcp --dport 5000 -j ACCEPT # adacam-api
|
||||||
|
iptables -A INPUT -i wlp1s0f0 -p udp --dport 67 -j ACCEPT # DHCP
|
||||||
|
iptables -A INPUT -i wlp1s0f0 -p udp --dport 53 -j ACCEPT # DNS
|
||||||
|
|
||||||
|
# Block everything on LTE/WAN
|
||||||
|
iptables -A INPUT -i wwan0 -j DROP
|
||||||
|
iptables -A INPUT -i ppp+ -j DROP
|
||||||
|
|
||||||
|
# ICMP for diagnostics
|
||||||
|
iptables -A INPUT -p icmp --icmp-type echo-request -j ACCEPT
|
||||||
|
FIREWALL
|
||||||
|
|
||||||
|
chmod +x /data/persist/firewall.sh
|
||||||
|
/data/persist/firewall.sh # Apply immediately
|
||||||
|
```
|
||||||
|
|
||||||
|
Add `/data/persist/firewall.sh` call to `/data/persist/install.sh` so it runs on every boot.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## adacam-api Auth for Varroa
|
||||||
|
|
||||||
|
**Decision:** Bearer token derived from device serial. No complex pairing state.
|
||||||
|
|
||||||
|
| Option | Decision |
|
||||||
|
|--------|----------|
|
||||||
|
| No auth (trust AP) | ❌ WiFi password could leak |
|
||||||
|
| mTLS | ❌ Android cert management is pain |
|
||||||
|
| Bearer token from serial | ✅ Simple, stateless, deterministic |
|
||||||
|
| OAuth/JWT | ❌ Who's the auth server? |
|
||||||
|
| Pairing flow with state | ❌ Phone reset = broken pairing |
|
||||||
|
|
||||||
|
Token is never transmitted — both sides derive it from serial. Attacker needs serial + WiFi password to have any chance. Good enough for a private AP dashcam.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Tunnel Security
|
||||||
|
|
||||||
|
**Current:** Device generates ed25519 keypair at liberation. Reverse tunnel to `cobb@142.44.213.229:2222`.
|
||||||
|
|
||||||
|
**Assessment:**
|
||||||
|
- ✅ Per-device key (not shared across devices)
|
||||||
|
- ✅ Outbound only — device initiates, nothing inbound
|
||||||
|
- ✅ autossh keeps it alive
|
||||||
|
- ⚠️ If Rackham is compromised, attacker has tunnel access to device
|
||||||
|
- ⚠️ Tunnel key is stored on device at `/data/adacam/ssh/tunnel_key` — if device is stolen, key is exposed
|
||||||
|
|
||||||
|
**Mitigations:**
|
||||||
|
- Rackham firewall: restrict port 2222 to localhost only (already the case with reverse tunnels)
|
||||||
|
- Rotate tunnel key if device is stolen: `ssh root@device "ssh-keygen -f /data/adacam/ssh/tunnel_key -N ''"` and update Rackham authorized_keys
|
||||||
|
|
||||||
|
**For multi-user deployments:** Each user runs their own tunnel to their own server. Document this clearly.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Data at Rest
|
||||||
|
|
||||||
|
**What's in /data:**
|
||||||
|
- `/data/recording/` — GPS tracks, IMU data, frame metadata in SQLite
|
||||||
|
- `/data/adacam/` — config, device identity, API token material
|
||||||
|
- `/data/persist/` — liberation artifacts
|
||||||
|
|
||||||
|
**Privacy considerations:**
|
||||||
|
- GPS tracks = complete driving history. Sensitive.
|
||||||
|
- Frames from camera = faces, plates, private property
|
||||||
|
- No encryption at rest currently
|
||||||
|
|
||||||
|
**Recommendation for now:** Don't encrypt. Complexity vs. threat. If device is stolen:
|
||||||
|
1. GPS tracks in /data — attacker gets your driving history. Accept this risk.
|
||||||
|
2. Frames in /tmp/adacam/pics — tmpfs, lost on reboot. Not a concern.
|
||||||
|
3. Config/keys — device is compromised, rotate everything from Rackham.
|
||||||
|
|
||||||
|
**Future:** If this becomes a product, encrypt /data with a key derived from device-specific hardware (TPM would be ideal but Keem Bay TEE is available via tee-supplicant).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## liberate.sh Changes Required
|
||||||
|
|
||||||
|
1. **WiFi password**: Replace static `adacam2026` with per-device derived password from serial/MAC
|
||||||
|
2. **SSH hardening**: Add sshd_config changes, disable password auth, inject `$ADACAM_PUBKEY`
|
||||||
|
3. **USB updater**: `systemctl mask usb-updater && rm -f /usr/bin/usb-updater`
|
||||||
|
4. **Firewall**: Add firewall.sh to /data/persist/ and call it, add to install.sh
|
||||||
|
5. **Device serial**: Write serial to `/data/adacam/device_serial` for token derivation
|
||||||
|
6. **Print summary**: End of script prints WiFi password, device serial, SSH connection string
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Priority Order
|
||||||
|
|
||||||
|
| Priority | Item | Effort | Risk if Skipped |
|
||||||
|
|----------|------|--------|-----------------|
|
||||||
|
| 🔴 P0 | SSH key auth (CVE-11) | 30 min | Anyone on AP = root |
|
||||||
|
| 🔴 P0 | Firewall rules | 30 min | Services exposed unnecessarily |
|
||||||
|
| 🟡 P1 | Per-device WiFi password (CVE-2) | 1 hr | Shared password across devices |
|
||||||
|
| 🟡 P1 | adacam-api bearer token (CVE-14) | 2 hrs | Config writable from AP |
|
||||||
|
| 🟢 P2 | Kill usb-updater (CVE-7/16) | 10 min | Physical USB flash possible |
|
||||||
|
| 🟢 P2 | Data at rest encryption | weeks | Low threat, high effort |
|
||||||
|
|
||||||
|
**Do P0 items before the device leaves the house.**
|
||||||
Loading…
Add table
Add a link
Reference in a new issue