adacam/security/HARDENING_PLAN.md
Kayos 9c4b0e26b9 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.
2026-03-14 11:27:23 -07:00

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:

  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.

# 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:

  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.