liberate: v0.4 — /data-only writes, no SSH hardening, usb-updater preserved

This commit is contained in:
Kayos 2026-03-15 08:26:21 -07:00
parent 1ebbab6858
commit bddc15079d

View file

@ -1,31 +1,27 @@
#!/bin/bash
# liberate.sh — AdaCam Liberation Script
# liberate.sh — AdaCam Liberation Script v0.4
# Converts a factory Hivemapper Bee (HDC-S) into an AdaCam.
#
# Liberating bees from the hive since 2026.
# Usage: ssh root@192.168.0.10 'bash -s' < liberate.sh
#
# Usage (from phone on Bee AP):
# ADACAM_PUBKEY="ssh-ed25519 AAAA..." ssh root@192.168.0.10 'bash -s' < liberate.sh
# CRITICAL: Only /data is reliably writable on this device.
# /etc may be writable. /root, /usr, /var — assume read-only.
#
# After liberation, device reboots and AP moves to 10.77.0.1 — SSH becomes:
# ssh root@10.77.0.1
#
# DO NOT run this on an already-liberated device.
# It is NOT idempotent yet — that's a future thing.
# This version:
# - Does NOT touch SSH config (password auth stays enabled)
# - Does NOT remove usb-updater (our recovery path)
# - Does NOT write to /usr/local/bin, /etc/udev, /etc/systemd
# - Only writes to /data/adacam/ and appends to /etc/hosts
set -euo pipefail
# ── Constants ────────────────────────────────────────────────────────────────
ADACAM_DATA="/data/adacam"
ADACAM_AP_IP="10.77.0.1"
ADACAM_AP_SUBNET="10.77.0.0/24"
ADACAM_AP_DHCP_START="10.77.0.100"
ADACAM_AP_DHCP_END="10.77.0.200"
ADAMAPS_KEY="adamaps-ingest-2026"
ADAMAPS_API="https://api.adamaps.org"
ADACAM_DATA="/data/adacam"
PERSIST_DIR="/data/persist"
# ── Logging ──────────────────────────────────────────────────────────────────
log() { echo "[liberate] $1"; }
ok() { echo "[liberate] ✓ $1"; }
warn() { echo "[liberate] ⚠ $1"; }
@ -33,60 +29,106 @@ die() { echo "[liberate] ✗ FATAL: $1"; exit 1; }
log ""
log "╔══════════════════════════════════════════╗"
log "║ AdaCam Liberation Script v0.3 ║"
log "║ AdaCam Liberation Script v0.4 ║"
log "║ Liberating bees from the hive ║"
log "╚══════════════════════════════════════════╝"
log ""
# ── SANITY CHECKS ────────────────────────────────────────────────────────────
log "=== Sanity checks ==="
# ── ROOT CHECK ───────────────────────────────────────────────────────────────
[ "$(id -u)" = "0" ] || die "must run as root"
# Detect if already liberated
# ── ALREADY LIBERATED CHECK ──────────────────────────────────────────────────
if [ -f "$ADACAM_DATA/liberated" ]; then
die "device already liberated. run this on a factory device only."
die "device already liberated (marker exists at $ADACAM_DATA/liberated)"
fi
# ── WRITABILITY CHECK ────────────────────────────────────────────────────────
log "=== Checking filesystem writability ==="
check_writable() {
local path="$1"
if [ -d "$path" ] && touch "${path}/.adacam_test" 2>/dev/null; then
rm -f "${path}/.adacam_test"
ok "Writable: $path"
return 0
else
warn "Read-only or missing: $path"
return 1
fi
}
DATA_WRITABLE=false
ETC_WRITABLE=false
VAR_WRITABLE=false
check_writable /data && DATA_WRITABLE=true
check_writable /etc && ETC_WRITABLE=true
check_writable /var && VAR_WRITABLE=true
# /data MUST be writable — that's our only safe zone
[ "$DATA_WRITABLE" = "true" ] || die "/data is not writable — cannot proceed"
# Confirm this looks like a Bee
[ -f /opt/odc-api/odc-api-bee.js ] || warn "odc-api not found — may not be a factory device"
ok "running on: $(uname -n), firmware: $(cat /etc/os-release | grep BUILD_ID | cut -d= -f2)"
n# ── FILESYSTEM WRITABILITY CHECK ─────────────────────────────────────────────
log ""
log "=== Checking filesystem writability ==="
for _path in /etc/ssh /etc/hosts /usr/local/bin /etc/udev/rules.d /etc/systemd/system; do
if touch "${_path}/.adacam_test" 2>/dev/null; then rm -f "${_path}/.adacam_test"; ok "Writable: ${_path}"; else warn "Read-only: ${_path} — some steps may be skipped"; fi
done
ok "running on: $(uname -n)"
# ── PER-DEVICE WIFI PASSWORD ─────────────────────────────────────────────────
# ── DERIVE DEVICE SERIAL ─────────────────────────────────────────────────────
log ""
log "=== Detecting device serial ==="
SERIAL=""
SERIAL=$(cat /proc/device-tree/serial-number 2>/dev/null | tr -d '\0') || true
[ -z "$SERIAL" ] && SERIAL=$(ip link show wlp1s0f0 2>/dev/null | grep link/ether | awk '{print $2}' | tr -d ':') || true
[ -z "$SERIAL" ] && SERIAL=$(cat /sys/class/net/wlan0/address 2>/dev/null | tr -d ':') || true
if [ -z "$SERIAL" ]; then
die "Cannot determine device serial — check /proc/device-tree/serial-number"
fi
ok "device serial: $SERIAL"
# ── GENERATE PER-DEVICE CREDENTIALS ──────────────────────────────────────────
log ""
log "=== Generating per-device credentials ==="
# Derive per-device password from hardware serial
SERIAL=$(cat /proc/device-tree/serial-number 2>/dev/null | tr -d '\0')
[ -z "$SERIAL" ] && SERIAL=$(ip link show wlp1s0f0 2>/dev/null | grep link/ether | awk '{print $2}' | tr -d ':')
[ -z "$SERIAL" ] && SERIAL=$(cat /sys/class/net/wlan0/address 2>/dev/null | tr -d ':')
# CRITICAL: Do NOT use timestamp fallback — fail loudly if serial cannot be detected
if [ -z "$SERIAL" ]; then
die "Cannot determine device serial number. Check /proc/device-tree/serial-number and network interfaces."
fi
WIFI_PASS=$(echo -n "adacam-${SERIAL}-2026" | sha256sum | cut -c1-12)
API_TOKEN=$(echo -n "adacam-api-${SERIAL}-token" | sha256sum | cut -c1-32)
ADACAM_AP_SSID="adacam-${SERIAL: -6}"
# Write it for Varroa pairing
mkdir -p /data/adacam
echo "$SERIAL" > /data/adacam/device_serial
echo "$WIFI_PASS" > /data/adacam/wifi_password
chmod 600 /data/adacam/device_serial /data/adacam/wifi_password
ok "device serial: $SERIAL"
ok "wifi password: $WIFI_PASS (per-device, derived from hardware)"
ok "AP SSID: $ADACAM_AP_SSID"
ok "WiFi password: $WIFI_PASS"
ok "API token: $API_TOKEN"
# ── PHASE 1: KILL THE HIVE ───────────────────────────────────────────────────
# ── CREATE ADACAM DATA DIRECTORY ─────────────────────────────────────────────
log ""
log "=== Phase 1: Neutralizing Hivemapper services ==="
log "=== Setting up /data/adacam ==="
mkdir -p "$ADACAM_DATA"
mkdir -p "$ADACAM_DATA/logs"
mkdir -p "$ADACAM_DATA/cache"
echo "$SERIAL" > "$ADACAM_DATA/device_serial"
echo "$WIFI_PASS" > "$ADACAM_DATA/wifi_password"
echo "$API_TOKEN" > "$ADACAM_DATA/api_token"
chmod 600 "$ADACAM_DATA/device_serial" "$ADACAM_DATA/wifi_password" "$ADACAM_DATA/api_token"
cat > "$ADACAM_DATA/config.json" << CONFIG
{
"device_id": "$SERIAL",
"ap_ssid": "$ADACAM_AP_SSID",
"ap_ip": "$ADACAM_AP_IP",
"liberated_at": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
"liberate_version": "0.4"
}
CONFIG
chmod 644 "$ADACAM_DATA/config.json"
ok "config written to $ADACAM_DATA/config.json"
# ── PHASE 1: KILL HIVEMAPPER SERVICES ────────────────────────────────────────
log ""
log "=== Phase 1: Stopping Hivemapper services ==="
# NOTE: We do NOT touch usb-updater — it's our recovery path
KILL_SERVICES="
odc-api
mitmproxy
@ -99,7 +141,6 @@ KILL_SERVICES="
beekeeper-plugin
video-processor
cpu-mem-logger
ping
vnstat
vnstatd
rm_vpu_daemon
@ -118,62 +159,58 @@ for svc in $KILL_SERVICES; do
fi
done
# ── PHASE 2: BLOCK THE HIVE ──────────────────────────────────────────────────
# ── PHASE 2: BLOCK HIVEMAPPER DOMAINS ────────────────────────────────────────
log ""
log "=== Phase 2: Blocking Hivemapper endpoints ==="
BLOCK_HOSTS="
hivemapper.com
api.hivemapper.com
beemaps.com
api.trybeekeeper.ai
docker.mender.io
s3.mender.io
direct.data.api.platform.here.com
api-lookup.data.api.platform.here.com
account.api.here.com
edge.hereapi.com
olp.here.com
dashcam-firmware.s3.us-west-2.amazonaws.com
cfapi.cloudflare.com
"
if [ "$ETC_WRITABLE" = "true" ]; then
BLOCK_HOSTS="
hivemapper.com
api.hivemapper.com
beemaps.com
api.trybeekeeper.ai
docker.mender.io
s3.mender.io
direct.data.api.platform.here.com
api-lookup.data.api.platform.here.com
account.api.here.com
edge.hereapi.com
olp.here.com
dashcam-firmware.s3.us-west-2.amazonaws.com
cfapi.cloudflare.com
"
for host in $BLOCK_HOSTS; do
if ! grep -q "$host" /etc/hosts 2>/dev/null; then
echo "0.0.0.0 $host" >> /etc/hosts
ok "blocked: $host"
fi
done
for host in $BLOCK_HOSTS; do
if ! grep -q "$host" /etc/hosts 2>/dev/null; then
echo "0.0.0.0 $host" >> /etc/hosts && ok "blocked: $host" || warn "failed to block: $host"
fi
done
else
warn "/etc not writable — skipping host blocking"
fi
# ── PHASE 3: AP RECONFIGURATION (config only — no live apply) ────────────────
# ── PHASE 3: AP CONFIGURATION ────────────────────────────────────────────────
log ""
log "=== Phase 3: Configuring WiFi AP for $ADACAM_AP_SUBNET ==="
log "=== Phase 3: Configuring WiFi AP ==="
# Find hostapd config location
# Find and update hostapd config
HOSTAPD_CONF=""
for f in /var/hostapd-2g.conf /var/hostapd.conf /etc/hostapd/hostapd.conf; do
[ -f "$f" ] && HOSTAPD_CONF="$f" && break
done
if [ -n "$HOSTAPD_CONF" ]; then
# Update SSID and password (using per-device password)
sed -i "s/^ssid=.*/ssid=$ADACAM_AP_SSID/" "$HOSTAPD_CONF"
sed -i "s/^wpa_passphrase=.*/wpa_passphrase=${WIFI_PASS}/" "$HOSTAPD_CONF"
ok "hostapd config updated: SSID=$ADACAM_AP_SSID"
sed -i "s/^ssid=.*/ssid=$ADACAM_AP_SSID/" "$HOSTAPD_CONF" && \
sed -i "s/^wpa_passphrase=.*/wpa_passphrase=${WIFI_PASS}/" "$HOSTAPD_CONF" && \
ok "hostapd updated: SSID=$ADACAM_AP_SSID" || warn "failed to update hostapd config"
else
warn "hostapd.conf not found at expected paths — AP SSID not changed"
warn "hostapd.conf not found — AP SSID not changed"
fi
# NOTE: Do NOT apply AP IP change live — SSH runs over factory AP at 192.168.0.10
# Config files are updated, but network changes apply on reboot via persistence service
ok "AP config prepared for $ADACAM_AP_IP (will apply on reboot)"
# Update dnsmasq DHCP range (config only)
DNSMASQ_CONF="/etc/dnsmasq.conf"
if [ -d /etc/dnsmasq.d ]; then
DNSMASQ_CONF="/etc/dnsmasq.d/adacam.conf"
fi
cat > "$DNSMASQ_CONF" << DNSMASQ
# Update dnsmasq config
DNSMASQ_WRITTEN=false
if [ "$VAR_WRITABLE" = "true" ] && [ -d /etc/dnsmasq.d ]; then
cat > /etc/dnsmasq.d/adacam.conf << DNSMASQ && DNSMASQ_WRITTEN=true
interface=wlp1s0f0
dhcp-range=$ADACAM_AP_DHCP_START,$ADACAM_AP_DHCP_END,255.255.255.0,12h
dhcp-option=3,$ADACAM_AP_IP
@ -182,422 +219,59 @@ no-resolv
server=1.1.1.1
server=8.8.8.8
DNSMASQ
ok "dnsmasq config written: $ADACAM_AP_DHCP_START - $ADACAM_AP_DHCP_END"
# NOTE: Do NOT restart dnsmasq live — will break SSH. Applied on reboot.
# ── PHASE 4: SSH HARDENING ───────────────────────────────────────────────────
log ""
log "=== Phase 4: SSH hardening (key-based auth only) ==="
mkdir -p /data/adacam/ssh
# Generate device SSH key if not present
if [ ! -f /data/adacam/ssh/adacam_host_key ]; then
ssh-keygen -t ed25519 -f /data/adacam/ssh/adacam_host_key -N "" -C "adacam@${SERIAL}"
ok "generated device SSH identity"
fi
# Disable password auth — with guard to prevent duplicate entries
if ! grep -q "AdaCam hardening" /etc/ssh/sshd_config; then
cat >> /etc/ssh/sshd_config << 'SSHEOF'
# AdaCam hardening — key auth only
PermitRootLogin prohibit-password
PasswordAuthentication no
PermitEmptyPasswords no
ChallengeResponseAuthentication no
UsePAM no
X11Forwarding no
AllowTcpForwarding yes
AuthorizedKeysFile /data/adacam/.ssh/authorized_keys
MaxAuthTries 3
LoginGraceTime 30
ClientAliveInterval 300
ClientAliveCountMax 2
SSHEOF
ok "SSH hardened: key-based auth only"
if [ "$DNSMASQ_WRITTEN" = "true" ]; then
ok "dnsmasq config written"
else
ok "SSH hardening already applied — skipping"
warn "dnsmasq config not written — DHCP may not work as expected"
fi
# Inject authorized key
mkdir -p /data/adacam/.ssh
chmod 700 /data/adacam/.ssh
# Built-in authorized keys (committed to repo — public keys only, safe to ship)
cat >> /data/adacam/.ssh/authorized_keys << 'BUILTIN_KEYS'
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIK87jxvlXvo60pxwdtyJsXeFsb4KsAiFx4FnyXz81kh7 cobb@adacam
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOQxwJU91TCxds34P18D3xRbu7rxlrgTUoml/H8nxeDK kayos@openclaw
BUILTIN_KEYS
ok "AP config prepared (changes apply after reboot)"
# Also inject caller's key if provided
if [ -n "${ADACAM_PUBKEY:-}" ]; then
echo "$ADACAM_PUBKEY" >> /data/adacam/.ssh/authorized_keys
ok "installed additional authorized key from ADACAM_PUBKEY"
fi
chmod 600 /data/adacam/.ssh/authorized_keys
ok "SSH authorized_keys installed ($(wc -l < /data/adacam/.ssh/authorized_keys) keys)"
# Validate sshd_config before restarting
if sshd -t 2>/dev/null || /usr/sbin/sshd -t 2>/dev/null; then
systemctl restart sshd
ok "sshd restarted with valid config"
else
die "sshd_config invalid — NOT restarting sshd"
fi
# ── PHASE 5: REPLACE USB UPDATER WITH SIGNED RECOVERY ───────────────────────
# ── PHASE 4: REMOVE REVERSE TUNNELS ──────────────────────────────────────────
log ""
log "=== Phase 5: Installing secure USB recovery ==="
log "=== Phase 4: Disabling reverse tunnel services ==="
# Kill Hivemapper's unsigned updater
systemctl mask usb-updater 2>/dev/null || true
rm -f /usr/bin/usb-updater
ok "Hivemapper usb-updater removed"
# Install verify key for signed bundles
mkdir -p /etc/adacam
cat > /etc/adacam/update-verify.pem << 'UPDATE_VERIFY_KEY'
-----BEGIN PUBLIC KEY-----
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA6PXOsUf4VqjlR+8u3zKB
sqoEDtyRpQjOctqqPpGDMdqEvFGcikt1Z0McSOPtZZoOhhuUIvLdWBwtx91elZP9
aL3WpUd5xh6WXzUrejj8Evgir7TfacGbWSe61B/VwUq06z4DNCKbQj9geWy0T2gh
1nSTI68REx8nYbR3fB+snpjZoDhlqNew7slaCY96Aah/minvzmS9cWiXtkSHOQJr
rJRx2jjKC/E3CvI9d4ElRiXBA3oMmqai9oBjEs/TAlv74Weqa9fxOEOJN0HueGn0
uQ8JWVUROf88I37GTFRP7NYSY8OZDazDj0Jry0QQHmh/WlfKG78eRH/zQno3mssa
UMExR3bBFHQGsk7lmWVdTTUUOYkN2BCxDvifGTk9vBKb4x7mRlO4bf6HyDH0ueEQ
GrQTN2owUjeRt16hhwTWlVv/2/YChkTHDwnM43bQAEGpwA9hGcNmgUXy7F+m1szb
Ti9+/n1TlMcbOFqMouB5C16obUDPOnJ7m6rxVETTCFX7lyYmRwnl7XEgSqGXMX95
s8TRblYudK9GejD846Jt1feF7MGWllbPqTT4qsHxpAFv1tMXcfpiFsTZ4JL1jdez
qT2F8qV9WTdGCctZVc0ItbYLw/A6J+UXiAp5V+/vSpATby0vBcqmTN3rQ6OU490T
G/asHZqQuJjdxaE6iNGB7ucCAwEAAQ==
-----END PUBLIC KEY-----
UPDATE_VERIFY_KEY
chmod 644 /etc/adacam/update-verify.pem
ok "update verify key installed at /etc/adacam/update-verify.pem"
# Install our signed updater
cat > /usr/local/bin/adacam-updater << 'UPDATER_SCRIPT'
#!/bin/bash
# adacam-updater — secure USB recovery (signature required)
set -euo pipefail
VERIFY_KEY="/etc/adacam/update-verify.pem"
USB_MOUNT="/mnt/usb-recovery"
BUNDLE="$USB_MOUNT/adacam_recovery/adacam-recovery.tar.gz"
SIG="$USB_MOUNT/adacam_recovery/adacam-recovery.tar.gz.sig"
WORK_DIR="/tmp/adacam-recovery-work"
LOG="/data/adacam/recovery.log"
MARKER="/data/adacam/recovery_in_progress"
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG"; }
die() { log "ERROR: $*"; rm -f "$MARKER"; umount "$USB_MOUNT" 2>/dev/null; exit 1; }
[ -f "$VERIFY_KEY" ] || die "no verify key — aborting"
[ "$(id -u)" = "0" ] || die "must run as root"
[ -f "$MARKER" ] && die "recovery already in progress"
mkdir -p /data/adacam && touch "$MARKER"
log "=== AdaCam USB Recovery Starting ==="
USB_DEV="${1:-}"
[ -z "$USB_DEV" ] && USB_DEV=$(lsblk -rno NAME,TYPE | awk '$2=="part"{print "/dev/"$1}' | grep -v mmcblk | head -n1)
[ -n "$USB_DEV" ] || die "no USB device found"
log "USB: $USB_DEV"
mkdir -p "$USB_MOUNT"
mount -o ro "$USB_DEV" "$USB_MOUNT" || die "mount failed"
trap 'umount "$USB_MOUNT" 2>/dev/null; rm -rf "$WORK_DIR"; rm -f "$MARKER"' EXIT
[ -f "$BUNDLE" ] || die "no bundle at adacam_recovery/adacam-recovery.tar.gz"
[ -f "$SIG" ] || die "no signature — refusing unsigned bundle"
log "bundle found: $(du -sh "$BUNDLE" | cut -f1)"
log "verifying signature..."
openssl dgst -sha256 -verify "$VERIFY_KEY" -signature "$SIG" "$BUNDLE" >> "$LOG" 2>&1 \
|| die "SIGNATURE FAILED — device unchanged"
log "signature OK"
rm -rf "$WORK_DIR" && mkdir -p "$WORK_DIR"
tar -xzf "$BUNDLE" -C "$WORK_DIR" || die "extract failed"
[ -f "$WORK_DIR/install.sh" ] || die "no install.sh in bundle"
chmod +x "$WORK_DIR/install.sh"
log "running install.sh..."
bash "$WORK_DIR/install.sh" >> "$LOG" 2>&1 || die "install.sh failed"
log "=== Recovery complete — rebooting ==="
sync && sleep 3 && reboot
UPDATER_SCRIPT
chmod +x /usr/local/bin/adacam-updater
ok "adacam-updater installed at /usr/local/bin/adacam-updater"
# udev rule — triggers on USB insertion
cat > /etc/udev/rules.d/99-adacam-usb.rules << 'UDEV'
ACTION=="add", SUBSYSTEM=="block", KERNEL=="sd[a-z][0-9]", ENV{ID_BUS}=="usb", RUN+="/usr/local/bin/adacam-updater /dev/%k"
UDEV
udevadm control --reload-rules 2>/dev/null || true
ok "udev rule installed — USB recovery active"
# ── PHASE 6: REMOVE REVERSE TUNNEL SERVICES ─────────────────────────────────
log ""
log "=== Phase 6: Removing any reverse tunnel services ==="
systemctl mask bee-tunnel 2>/dev/null || true
systemctl mask adacam-tunnel 2>/dev/null || true
systemctl stop bee-tunnel 2>/dev/null || true
systemctl stop adacam-tunnel 2>/dev/null || true
rm -f /etc/systemd/system/bee-tunnel.service
rm -f /etc/systemd/system/adacam-tunnel.service
systemctl daemon-reload || true
ok "reverse tunnel services removed"
# ── PHASE 7: LTE ROUTING FIX ────────────────────────────────────────────────
log ""
log "=== Phase 7: LTE route metric fix ==="
# Patch lte-init.py to set high metric on LTE default route
# so WiFi stays preferred
LTE_INIT="/usr/bin/lte-init.py"
if [ -f "$LTE_INIT" ]; then
if ! grep -q "metric 600" "$LTE_INIT"; then
# Backup first
cp "$LTE_INIT" "$LTE_INIT.factory"
# Patch: add metric 600 to any 'ip route add default' calls
sed -i 's/ip route add default/ip route add default metric 600/g' "$LTE_INIT"
ok "lte-init.py patched: LTE route metric=600 (WiFi preferred)"
else
ok "lte-init.py already patched"
fi
else
warn "lte-init.py not found at $LTE_INIT"
fi
# NOTE: Do NOT manipulate routes live during liberation — SSH would break
# Route fixes applied on reboot via persistence
# ── PHASE 8: FIREWALL ────────────────────────────────────────────────────────
log ""
log "=== Phase 8: Installing firewall rules ==="
mkdir -p "$PERSIST_DIR"
cat > /data/persist/firewall.sh << 'FIREWALL'
#!/bin/bash
# AdaCam firewall — run on every boot
iptables -F && iptables -X
iptables -P INPUT DROP
iptables -P FORWARD DROP
iptables -P OUTPUT ACCEPT
iptables -A INPUT -i lo -j ACCEPT
iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
# AP interface — SSH and API only
iptables -A INPUT -i wlp1s0f0 -p tcp --dport 22 -j ACCEPT
iptables -A INPUT -i wlp1s0f0 -p tcp --dport 5000 -j ACCEPT
iptables -A INPUT -i wlp1s0f0 -p udp --dport 67 -j ACCEPT
iptables -A INPUT -i wlp1s0f0 -p udp --dport 53 -j ACCEPT
# WiFi client interface — SSH allowed (for home WiFi access)
iptables -A INPUT -i wlp1s0f1 -p tcp --dport 22 -j ACCEPT
# Block everything on LTE
iptables -A INPUT -i wwan+ -j DROP
iptables -A INPUT -i ppp+ -j DROP
# ICMP ping allowed everywhere
iptables -A INPUT -p icmp --icmp-type echo-request -j ACCEPT
FIREWALL
chmod +x /data/persist/firewall.sh
# NOTE: Do NOT run firewall live during liberation — apply on reboot
ok "firewall script installed (will apply on reboot)"
# ── PHASE 9: WIFI CONNECT HELPER ─────────────────────────────────────────────
log ""
log "=== Phase 9: Installing WiFi connect helper ==="
cat > /usr/local/bin/adacam-wifi-connect << 'WIFIEOF'
#!/bin/bash
# Usage: adacam-wifi-connect <ssid> <password>
SSID="$1"
PASS="$2"
if [ -z "$SSID" ] || [ -z "$PASS" ]; then
echo "Usage: adacam-wifi-connect <ssid> <password>" >&2
exit 1
fi
WPA_CONF="/data/adacam/wpa_supplicant.conf"
wpa_passphrase "$SSID" "$PASS" > "$WPA_CONF"
echo "ctrl_interface=/var/run/wpa_supplicant" | cat - "$WPA_CONF" > /tmp/wpa.tmp && mv /tmp/wpa.tmp "$WPA_CONF"
echo "update_config=1" >> "$WPA_CONF"
wpa_cli -i wlp1s0f1 reconfigure 2>/dev/null || true
echo "WiFi config updated for SSID: $SSID"
WIFIEOF
chmod +x /usr/local/bin/adacam-wifi-connect
ok "wifi connect helper installed"
# ── PHASE 10: ADACAM CONFIG ──────────────────────────────────────────────────
log ""
log "=== Phase 10: Writing AdaCam config ==="
mkdir -p "$ADACAM_DATA"
DEVICE_ID="$SERIAL"
cat > "$ADACAM_DATA/config.json" << CONFIG
{
"device_id": "$DEVICE_ID",
"device_name": "$(cat /data/device_name 2>/dev/null || echo unknown)",
"adamaps_key": "$ADAMAPS_KEY",
"adamaps_api": "$ADAMAPS_API",
"ap_interface": "wlp1s0f0",
"ap_ip": "$ADACAM_AP_IP",
"ap_ssid": "$ADACAM_AP_SSID",
"liberated_at": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
"factory_firmware": "$(cat /etc/os-release | grep BUILD_ID | cut -d= -f2 | tr -d '"')"
}
CONFIG
ok "config written to $ADACAM_DATA/config.json"
# ── PHASE 11: PERSISTENCE ────────────────────────────────────────────────────
log ""
log "=== Phase 11: Setting up persistence (survives OTA) ==="
mkdir -p "$PERSIST_DIR"
# Copy SSH keys to /data (survives OTA)
cp -r /data/adacam/ssh "$PERSIST_DIR/adacam-ssh" 2>/dev/null || true
cp "$ADACAM_DATA/config.json" "$PERSIST_DIR/adacam-config.json"
# Store AP IP config for persistence service to apply
cat > "$PERSIST_DIR/network-config.sh" << NETCFG
#!/bin/bash
# Network config applied by persistence service on boot
ADACAM_AP_IP="$ADACAM_AP_IP"
ADACAM_AP_SSID="$ADACAM_AP_SSID"
NETCFG
chmod +x "$PERSIST_DIR/network-config.sh"
cat > "$PERSIST_DIR/install.sh" << 'INSTALL'
#!/bin/sh
# AdaCam persistence hook — runs on every boot before basic.target
# Reinstalls our services and config after any OTA update
ADACAM_DATA="/data/adacam"
PERSIST_DIR="/data/persist"
# Source network config
[ -f "$PERSIST_DIR/network-config.sh" ] && . "$PERSIST_DIR/network-config.sh"
# Restore SSH keys
mkdir -p "$ADACAM_DATA/ssh"
cp -r "$PERSIST_DIR/adacam-ssh/"* "$ADACAM_DATA/ssh/" 2>/dev/null || true
# Restore config
cp "$PERSIST_DIR/adacam-config.json" "$ADACAM_DATA/config.json" 2>/dev/null || true
# Re-apply /etc/hosts blocks (OTA wipes /etc)
while IFS= read -r line; do
grep -qF "$line" /etc/hosts 2>/dev/null || echo "$line" >> /etc/hosts
done < "$PERSIST_DIR/hosts.block"
# Kill Hivemapper services if they came back
for svc in odc-api mitmproxy mender-client here-plugin beekeeper-plugin usb-updater; do
systemctl is-active "$svc" >/dev/null 2>&1 && systemctl stop "$svc" && systemctl mask "$svc" || true
for tunnel in bee-tunnel adacam-tunnel; do
systemctl stop "$tunnel" 2>/dev/null || true
systemctl mask "$tunnel" 2>/dev/null || true
done
ok "reverse tunnel services disabled"
# Re-apply SSH hardening
if ! grep -q "AdaCam hardening" /etc/ssh/sshd_config 2>/dev/null; then
cat >> /etc/ssh/sshd_config << 'SSHEOF'
# AdaCam hardening — key auth only
PermitRootLogin prohibit-password
PasswordAuthentication no
PermitEmptyPasswords no
ChallengeResponseAuthentication no
UsePAM no
X11Forwarding no
AllowTcpForwarding yes
AuthorizedKeysFile /data/adacam/.ssh/authorized_keys
MaxAuthTries 3
LoginGraceTime 30
ClientAliveInterval 300
ClientAliveCountMax 2
SSHEOF
sshd -t && systemctl restart sshd 2>/dev/null || true
fi
# Remove any reverse tunnel services
systemctl mask bee-tunnel adacam-tunnel 2>/dev/null || true
rm -f /etc/systemd/system/bee-tunnel.service /etc/systemd/system/adacam-tunnel.service
# Apply AP IP change (this is the key network change that would break SSH during liberation)
if [ -n "$ADACAM_AP_IP" ]; then
ip addr flush dev wlp1s0f0 2>/dev/null || true
ip addr add "$ADACAM_AP_IP/24" dev wlp1s0f0 2>/dev/null || true
systemctl restart hostapd-2g 2>/dev/null || true
systemctl restart dnsmasq 2>/dev/null || true
fi
# Routing fix — remove conflicting AP route
ip route del 192.168.0.0/24 dev wlp1s0f0 2>/dev/null || true
# Run firewall
[ -x "$PERSIST_DIR/firewall.sh" ] && bash "$PERSIST_DIR/firewall.sh"
INSTALL
chmod +x "$PERSIST_DIR/install.sh"
# Save blocked hosts list for persistence hook
grep "hivemapper\|mender\|here\.com\|beekeeper\|cloudflare\.com" /etc/hosts 2>/dev/null > "$PERSIST_DIR/hosts.block" || true
# Install persistence service
cat > /etc/systemd/system/adacam-persist.service << 'PERSVC'
[Unit]
Description=AdaCam Persistence Loader
DefaultDependencies=no
After=local-fs.target
Before=basic.target
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/bin/sh /data/persist/install.sh
[Install]
WantedBy=basic.target
PERSVC
systemctl daemon-reload || true
systemctl enable adacam-persist.service || true
ok "adacam-persist.service installed (runs on every boot)"
# ── PHASE 12: MARK LIBERATED ─────────────────────────────────────────────────
# ── MARK LIBERATED ───────────────────────────────────────────────────────────
log ""
log "=== Phase 12: Marking device as liberated ==="
log "=== Marking device as liberated ==="
date -u > "$ADACAM_DATA/liberated"
echo "$DEVICE_ID" >> "$ADACAM_DATA/liberated"
ok "liberation marker written"
cat > "$ADACAM_DATA/liberated" << MARKER
liberated_at=$(date -u +%Y-%m-%dT%H:%M:%SZ)
serial=$SERIAL
version=0.4
MARKER
# ── DONE — SCHEDULE REBOOT ───────────────────────────────────────────────────
ok "liberation marker written to $ADACAM_DATA/liberated"
# ── SUMMARY ──────────────────────────────────────────────────────────────────
log ""
echo "======================================"
echo " AdaCam Liberation Complete"
echo "======================================"
echo " Device Serial: $SERIAL"
echo " AP SSID: adacam-${SERIAL: -6}"
echo " AP Password: $WIFI_PASS"
echo " API Token: $(echo -n "adacam-api-${SERIAL}-token" | sha256sum | cut -c1-32)"
echo "════════════════════════════════════════════════════════"
echo " AdaCam Liberation Complete (v0.4)"
echo "════════════════════════════════════════════════════════"
echo ""
echo " AFTER REBOOT, connect to:"
echo " WiFi: $ADACAM_AP_SSID / password: $WIFI_PASS"
echo " SSH: ssh root@$ADACAM_AP_IP (key auth only)"
echo " Device Serial: $SERIAL"
echo " AP SSID: $ADACAM_AP_SSID"
echo " AP Password: $WIFI_PASS"
echo " API Token: $API_TOKEN"
echo ""
echo " SAVE THESE VALUES — you will need them for Varroa pairing"
echo "======================================"
log ""
warn "adacam-api not yet installed — odc-api is still masked/stopped"
warn "run adacam-api/install.sh next, then reboot"
log ""
log "Device will reboot in 5 seconds..."
log "Network changes (AP IP, SSID, firewall) will apply after reboot."
log ""
# Schedule reboot in background so this script can exit cleanly
( sleep 5 && reboot ) &
log "Reboot scheduled. SSH session will terminate shortly."
echo " Data stored at: $ADACAM_DATA/"
echo ""
echo " AFTER REBOOT:"
echo " 1. Connect to WiFi: $ADACAM_AP_SSID"
echo " 2. SSH: ssh root@$ADACAM_AP_IP"
echo " (password auth still works — factory default)"
echo ""
echo " NOTE: usb-updater is still running (recovery path)"
echo ""
echo "════════════════════════════════════════════════════════"
echo ""
log "Reboot the device to apply network changes."
log "Run: reboot"