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.
9.8 KiB
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.
# 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.
# 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:
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.
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:
- Connect to AdaCam WiFi
GET /pair→ returns{"serial": "ABC123"}- App derives token:
sha256("adacam-api-ABC123-token")[:32] - 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.
# 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:
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:
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:
- GPS tracks in /data — attacker gets your driving history. Accept this risk.
- Frames in /tmp/adacam/pics — tmpfs, lost on reboot. Not a concern.
- 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
- WiFi password: Replace static
adacam2026with per-device derived password from serial/MAC - SSH hardening: Add sshd_config changes, disable password auth, inject
$ADACAM_PUBKEY - USB updater:
systemctl mask usb-updater && rm -f /usr/bin/usb-updater - Firewall: Add firewall.sh to /data/persist/ and call it, add to install.sh
- Device serial: Write serial to
/data/adacam/device_serialfor token derivation - 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.