Files
bigbrother/operator_setup.sh
T
Cobra 2b00b28de6 Add reverse SSH tunnel (autossh) as VPN option in operator wizard
Generates a dedicated tunnel keypair during setup, prints public key
for operator to pre-stage on server, writes private key to SD card.
On first boot: installs autossh, writes bb-tunnel.service with
correct RemoteForward and reconnect settings. Zero network traffic
until operator initiates connection through the tunnel port.
2026-04-07 14:30:10 -04:00

760 lines
29 KiB
Bash
Executable File

#!/usr/bin/env bash
# BigBrother Operator Setup Wizard
#
# Single interactive script for prepping Orange Pi Zero 3 (or compatible Debian) SD cards.
# Operator runs this on their machine with an SD card inserted.
#
# What this does:
# 1. Detects block devices and has operator pick SD card target
# 2. Downloads/caches Armbian minimal image for Orange Pi Zero 3
# 3. Flashes image to card with verification
# 4. Mounts card and collects configuration (WiFi, SSH, VPN, alerter, etc.)
# 5. Applies all config to card in a single batch after operator confirms summary
# 6. Sets up first-boot systemd service for autonomous installation
# 7. Unmounts card — ready to insert and power on
#
# Style: bash only, ANSI colors, numbered menus, c2itall-style (no preamble, direct action)
# Root required for: mount, unmount, dd, partprobe, rsync to /mnt, chown, chmod on mounted fs
#
set -euo pipefail
# ══════════════════════════════════════════════════════════════════════════════
# COLORS (bash ANSI only)
# ══════════════════════════════════════════════════════════════════════════════
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
BLUE='\033[0;34m'
MAGENTA='\033[0;35m'
BOLD='\033[1m'
NC='\033[0m'
# ══════════════════════════════════════════════════════════════════════════════
# HELPERS
# ══════════════════════════════════════════════════════════════════════════════
info() { echo -e "${GREEN}[+]${NC} $*"; }
warn() { echo -e "${YELLOW}[!]${NC} $*"; }
error() { echo -e "${RED}[-]${NC} $*"; exit 1; }
step() { echo -e "${CYAN}[*]${NC} $*"; }
section() {
echo ""
echo -e "${BOLD}${BLUE}─────────────────────────────────────────────${NC}"
echo -e "${BOLD}$*${NC}"
echo -e "${BOLD}${BLUE}─────────────────────────────────────────────${NC}"
echo ""
}
banner() {
echo -e "${BOLD}${MAGENTA}╔════════════════════════════════════════════╗${NC}"
echo -e "${BOLD}${MAGENTA}║ BIGBROTHER // OPERATOR SETUP ║${NC}"
echo -e "${BOLD}${MAGENTA}╚════════════════════════════════════════════╝${NC}"
echo ""
}
prompt_default() {
local var="$1" prompt="$2" default="$3"
echo -n -e "${CYAN}${prompt}${NC}"
[[ -n "$default" ]] && echo -n -e " ${BOLD}[${default}]${NC}"
echo -n ": "
read -r input
if [[ -z "$input" ]]; then
printf -v "$var" '%s' "$default"
else
printf -v "$var" '%s' "$input"
fi
}
# ══════════════════════════════════════════════════════════════════════════════
# PREFLIGHT
# ══════════════════════════════════════════════════════════════════════════════
[[ $EUID -eq 0 ]] || error "Must run as root (use: sudo bash operator_setup.sh)"
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
# When run via sudo, use the invoking user's home — not /root
REAL_HOME=$(getent passwd "${SUDO_USER:-$(whoami)}" | cut -d: -f6)
CACHE_DIR="${REAL_HOME}/.bigbrother/images"
CREDS="${REAL_HOME}/.local/bin/creds"
MOUNT_POINT="/mnt/opi"
mkdir -p "$CACHE_DIR"
# ══════════════════════════════════════════════════════════════════════════════
# 1. BANNER
# ══════════════════════════════════════════════════════════════════════════════
clear
banner
# ══════════════════════════════════════════════════════════════════════════════
# 2. SD CARD DETECTION
# ══════════════════════════════════════════════════════════════════════════════
section "SD Card Detection"
info "Scanning block devices..."
echo ""
lsblk -ndo NAME,SIZE,LABEL,TYPE | grep -E "^(sd|mmc)" | nl -v 1 -w1 -s') ' || error "No block devices found. Insert SD card and try again."
echo ""
echo -n -e "${CYAN}Enter device number (1-9):${NC} "
read -r device_num
[[ "$device_num" =~ ^[1-9]$ ]] || error "Invalid selection"
device_row=$(lsblk -ndo NAME,SIZE,LABEL,TYPE | grep -E "^(sd|mmc)" | sed -n "${device_num}p" || true)
DEVICE=$(echo "$device_row" | awk '{print $1}')
DEVICE_SIZE=$(echo "$device_row" | awk '{print $2}')
[[ -n "$DEVICE" ]] || error "Invalid selection — no device at that number"
info "Selected: /dev/${DEVICE} (${DEVICE_SIZE})"
warn "This will OVERWRITE all data on /dev/${DEVICE}"
echo -n -e "${YELLOW}Type device name to confirm (${DEVICE}):${NC} "
read -r confirm_dev
[[ "$confirm_dev" == "$DEVICE" ]] || error "Device name mismatch. Aborting."
info "Device confirmed: /dev/${DEVICE}"
# ══════════════════════════════════════════════════════════════════════════════
# 3. IMAGE MANAGEMENT
# ══════════════════════════════════════════════════════════════════════════════
section "Image Management"
# Look for existing cached image
CACHED_IMAGE=$(find "$CACHE_DIR" -maxdepth 1 -name 'Armbian_*_Orangepizero3_bookworm_current_*_minimal.img.xz' 2>/dev/null | head -1 || true)
if [[ -n "$CACHED_IMAGE" ]]; then
info "Found cached image: $(basename $CACHED_IMAGE)"
echo -n -e "${CYAN}Use cached image? (y/n):${NC} "
read -r use_cached
if [[ "$use_cached" =~ ^[yY]$ ]]; then
IMAGE="$CACHED_IMAGE"
else
IMAGE=""
fi
else
IMAGE=""
fi
if [[ -z "$IMAGE" ]]; then
step "Downloading Armbian image..."
# Hardcoded known-good URL (operator can update)
IMAGE_URL="https://dl.armbian.com/orangepizero3/Armbian_25.5.1_Orangepizero3_bookworm_current_6.12.23_minimal.img.xz"
IMAGE_FILENAME=$(basename "$IMAGE_URL")
IMAGE="${CACHE_DIR}/${IMAGE_FILENAME}"
if [[ ! -f "$IMAGE" ]]; then
curl -L --progress-bar "$IMAGE_URL" -o "$IMAGE" || error "Failed to download image"
# Verify download: file exists and is > 50MB
if [[ ! -s "$IMAGE" ]] || [[ $(stat -c%s "$IMAGE" 2>/dev/null || echo 0) -lt 52428800 ]]; then
rm -f "$IMAGE"
error "Download failed or file too small (expected > 50MB)"
fi
info "Image downloaded: $IMAGE_FILENAME"
else
info "Image already cached: $IMAGE_FILENAME"
fi
fi
# SHA256 verification (optional, skip if no .sha file)
SHA_FILE="${IMAGE}.sha"
if [[ -f "$SHA_FILE" ]]; then
step "Verifying SHA256..."
cd "$CACHE_DIR"
sha256sum -c "$(basename $SHA_FILE)" >/dev/null 2>&1 && info "SHA256 verified" || warn "SHA256 mismatch (proceeding anyway)"
cd - >/dev/null
fi
# ══════════════════════════════════════════════════════════════════════════════
# 4. CONFIGURATION WIZARD
# ══════════════════════════════════════════════════════════════════════════════
section "Configuration Wizard"
# WiFi (optional)
step "WiFi Configuration"
prompt_default enable_wifi "Configure WiFi? (y/n)" "n"
if [[ "$enable_wifi" =~ ^[yY]$ ]]; then
prompt_default wifi_ssid "WiFi SSID" ""
while true; do
echo -n -e "${CYAN}WiFi Passphrase${NC}: "
read -s -r wifi_pass; echo
echo -n -e "${CYAN}Confirm Passphrase${NC}: "
read -s -r wifi_pass2; echo
if [[ "$wifi_pass" == "$wifi_pass2" ]]; then
break
else
warn "Passphrases do not match. Try again."
fi
done
WIFI_SSID="$wifi_ssid"
WIFI_PASS="$wifi_pass"
unset wifi_pass wifi_pass2
else
WIFI_SSID=""
WIFI_PASS=""
fi
# Root password (required)
step "Root Password"
DEFAULT_ROOT_PASS=$(openssl rand -base64 16 | tr -d '=+/')
ROOT_PASS_IS_AUTO=false
echo -e "${YELLOW}Auto-generated password: ${BOLD}${DEFAULT_ROOT_PASS}${NC}"
prompt_default root_pass "Enter custom password or press Enter to use auto-generated" ""
if [[ -z "$root_pass" ]]; then
ROOT_PASS="$DEFAULT_ROOT_PASS"
ROOT_PASS_IS_AUTO=true
else
ROOT_PASS="$root_pass"
fi
unset root_pass
# SSH Key
step "SSH Key Configuration"
echo " [1] Generate new keypair (stored in Infisical)"
echo " [2] Use existing public key file"
prompt_default ssh_opt "SSH option (1-2)" "1"
if [[ "$ssh_opt" == "1" ]]; then
SSH_MODE="generate"
elif [[ "$ssh_opt" == "2" ]]; then
SSH_MODE="existing"
prompt_default ssh_pubkey_path "Path to public key file" ""
[[ -f "$ssh_pubkey_path" ]] || error "File not found: $ssh_pubkey_path"
SSH_PUBKEY=$(cat "$ssh_pubkey_path")
else
error "Invalid SSH option"
fi
# Device ID
step "Device ID"
DEFAULT_DEVICE_ID="bb-$(cat /proc/sys/kernel/random/uuid | cut -c1-8)"
prompt_default device_id "Device label (e.g. bb-a3f2c1)" "$DEFAULT_DEVICE_ID"
DEVICE_ID="$device_id"
# Hostname — pick from MAC device profiles (matches MAC identity)
step "Hostname"
PROFILE_NAMES=(
"Fire TV Stick 4K|amazon-firetv"
"Fire TV Stick Lite|amazon-lite"
"Fire TV Cube|amazon-cube"
"Roku Ultra|roku-ultra"
"Roku Express|roku-express"
"Chromecast|chromecast"
"Chromecast with Google TV|chromecast-gtv"
"Apple TV 4K|apple-tv-4k"
"Apple TV HD|apple-tv-hd"
"Samsung Smart TV|samsung-tv"
"Samsung QLED TV|samsung-qled"
"LG webOS TV|lg-tv-webos"
"LG OLED TV|lg-tv-oled"
"Vizio SmartCast TV|vizio-tv"
"TCL Roku TV|tcl-tv"
"Sony Bravia Android TV|sony-tv"
"Sonos One|sonos-one"
"Sonos Beam|sonos-beam"
"Google Home Mini|google-home"
"Google Nest Hub|google-nest"
"Echo Dot|echo-dot"
"Echo Show|echo-show"
"Ring Doorbell|ring-doorbell"
"Nest Thermostat|nest-thermo"
"Nest Protect|nest-protect"
"Philips Hue Bridge|philips-hue"
"TP-Link Kasa Smart Plug|tplink-kasa"
"Wyze Cam v3|wyze-cam"
"Ecobee Thermostat|ecobee-thermo"
"Roomba i7|roomba-i7"
"Roomba j7|roomba-j7"
"Raspberry Pi IoT Sensor|raspberrypi-sensor"
"Raspberry Pi IoT Gateway|raspberrypi-gateway"
"Raspberry Pi IoT Controller|raspberrypi-ctrl"
"HP LaserJet Pro|hp-laserjet"
"HP OfficeJet Pro|hp-officejet"
"Brother HL-L2350DW|brother-hl"
"Brother MFC-L2750DW|brother-mfc"
"Brother DCP-L2550DW|brother-dcp"
"Epson EcoTank ET-4760|epson-ecotank"
"Canon PIXMA G6020|canon-pixma"
"Xbox Series X|xbox-series-x"
"PlayStation 5|ps5"
"PlayStation 4|ps4"
"Nintendo Switch|nintendo-switch"
"iPhone 15 Pro|iphone-15"
"iPad Air|ipad-air"
"Galaxy Tab S9|galaxy-tab"
"Galaxy S24|galaxy-s24"
"UniFi AP|unifi-ap"
"USG Gateway|usg-gateway"
"TP-Link Archer AX50|archer-ax50"
"TP-Link Deco M5|deco-m5"
"Netgear Orbi|orbi"
"Netgear Nighthawk|nighthawk"
)
PROFILE_IDX=$(( RANDOM % ${#PROFILE_NAMES[@]} ))
SELECTED_PROFILE="${PROFILE_NAMES[$PROFILE_IDX]}"
PROFILE_DISPLAY="${SELECTED_PROFILE%%|*}"
DEFAULT_HOSTNAME="${SELECTED_PROFILE##*|}"
echo -e " Profile: ${BOLD}${PROFILE_DISPLAY}${NC}"
prompt_default bb_hostname "Hostname" "$DEFAULT_HOSTNAME"
BB_HOSTNAME="$bb_hostname"
# Auto-start mode
step "Auto-start Mode on Boot"
echo " [1] Passive only (capture + analysis, no active offense)"
echo " [2] Full autonomous (all modules)"
echo " [3] Manual (operator SSHs in to start)"
prompt_default mode_opt "Select mode (1-3)" "1"
if [[ "$mode_opt" == "1" ]]; then
BB_MODE="passive"
elif [[ "$mode_opt" == "2" ]]; then
BB_MODE="autonomous"
elif [[ "$mode_opt" == "3" ]]; then
BB_MODE="manual"
else
error "Invalid mode"
fi
# VPN tunnel
step "VPN Tunnel"
echo " [1] None"
echo " [2] Tailscale"
echo " [3] WireGuard"
echo " [4] Reverse SSH Tunnel (autossh) — no traffic until operator connects"
prompt_default vpn_opt "Select VPN (1-4)" "1"
BB_VPN="none"
TAILSCALE_KEY=""
WG_CONFIG=""
AUTOSSH_SERVER=""
AUTOSSH_USER="root"
AUTOSSH_PORT="22"
AUTOSSH_REMOTE_PORT=""
AUTOSSH_PRIVKEY=""
AUTOSSH_PUBKEY=""
if [[ "$vpn_opt" == "1" ]]; then
BB_VPN="none"
elif [[ "$vpn_opt" == "2" ]]; then
BB_VPN="tailscale"
prompt_default tailscale_key "Tailscale auth key" ""
TAILSCALE_KEY="$tailscale_key"
elif [[ "$vpn_opt" == "3" ]]; then
BB_VPN="wireguard"
prompt_default wg_config_path "Path to WireGuard config (wg0.conf)" ""
[[ -f "$wg_config_path" ]] || error "File not found: $wg_config_path"
WG_CONFIG=$(cat "$wg_config_path")
elif [[ "$vpn_opt" == "4" ]]; then
BB_VPN="autossh"
prompt_default AUTOSSH_SERVER "Operator SSH server (host or IP)" ""
[[ -n "$AUTOSSH_SERVER" ]] || error "Server address required"
prompt_default AUTOSSH_USER "SSH user on server" "root"
prompt_default AUTOSSH_PORT "SSH port on server" "22"
DEFAULT_REMOTE_PORT=$(( 20000 + RANDOM % 10000 ))
prompt_default AUTOSSH_REMOTE_PORT "Remote tunnel port (device SSH appears here on server)" "$DEFAULT_REMOTE_PORT"
# Generate dedicated tunnel keypair — keeps it separate from device management key
_TMP_TKEY=$(mktemp)
ssh-keygen -t ed25519 -N "" -C "bb-tunnel-${DEVICE_ID}" -f "$_TMP_TKEY" -q
AUTOSSH_PRIVKEY=$(cat "$_TMP_TKEY")
AUTOSSH_PUBKEY=$(cat "${_TMP_TKEY}.pub")
rm -f "$_TMP_TKEY" "${_TMP_TKEY}.pub"
echo ""
echo -e " ${YELLOW}Add this public key to ${AUTOSSH_USER}@${AUTOSSH_SERVER}:~/.ssh/authorized_keys before powering on:${NC}"
echo ""
echo -e " ${BOLD}${AUTOSSH_PUBKEY}${NC}"
echo ""
echo -n -e "${CYAN} Press Enter once the key is on the server:${NC} "
read -r
else
error "Invalid VPN option"
fi
# Network alerter webhook
step "Network Alerter"
prompt_default enable_alerter "Enable alerter webhook? (y/n)" "n"
if [[ "$enable_alerter" =~ ^[yY]$ ]]; then
prompt_default webhook_url "Webhook URL (Slack/Discord/Matrix)" ""
BB_ALERTER_WEBHOOK="$webhook_url"
else
BB_ALERTER_WEBHOOK=""
fi
# ══════════════════════════════════════════════════════════════════════════════
# 5. CONFIG SUMMARY
# ══════════════════════════════════════════════════════════════════════════════
section "Configuration Summary"
echo "Device ID: ${BOLD}${DEVICE_ID}${NC}"
echo "Hostname: ${BOLD}${BB_HOSTNAME}${NC}"
echo "Target: ${BOLD}/dev/${DEVICE}${NC}"
echo "Image: ${BOLD}$(basename $IMAGE)${NC}"
echo ""
if [[ "$ROOT_PASS_IS_AUTO" == true ]]; then
echo "Root Pass: ${BOLD}${RED}AUTO-GENERATED — RECORD THIS: ${DEFAULT_ROOT_PASS}${NC}"
else
echo "Root Pass: ${BOLD}[custom]${NC}"
fi
echo "WiFi: ${BOLD}$([ -n "$WIFI_SSID" ] && echo "$WIFI_SSID" || echo "disabled")${NC}"
echo "SSH Mode: ${BOLD}${SSH_MODE}${NC}"
echo "Auto-start: ${BOLD}${BB_MODE}${NC}"
if [[ "$BB_VPN" == "autossh" ]]; then
echo "VPN: ${BOLD}autossh → ${AUTOSSH_USER}@${AUTOSSH_SERVER}:${AUTOSSH_PORT} (remote port ${AUTOSSH_REMOTE_PORT})${NC}"
else
echo "VPN: ${BOLD}${BB_VPN}${NC}"
fi
echo "Alerter: ${BOLD}$([ -n "$BB_ALERTER_WEBHOOK" ] && echo "enabled" || echo "disabled")${NC}"
echo ""
echo -n -e "${CYAN}Proceed with flashing and configuration? (y/n):${NC} "
read -r confirm_proceed
[[ "$confirm_proceed" =~ ^[yY]$ ]] || { info "Aborted"; exit 0; }
# ══════════════════════════════════════════════════════════════════════════════
# 6. FLASH AND MOUNT
# ══════════════════════════════════════════════════════════════════════════════
section "Flashing SD Card"
step "Unmounting any existing mounts on /dev/${DEVICE}..."
for part in /dev/${DEVICE}*; do
[[ -b "$part" ]] && umount "$part" 2>/dev/null || true
done
step "Flashing image to /dev/${DEVICE}..."
xzcat "$IMAGE" | dd of="/dev/${DEVICE}" bs=4M status=progress conv=fsync || error "Flash failed"
info "Flash complete"
step "Running partprobe..."
partprobe "/dev/${DEVICE}" || warn "partprobe failed (non-critical)"
sleep 3
step "Mounting to ${MOUNT_POINT}..."
mkdir -p "$MOUNT_POINT"
# Try /dev/${DEVICE}1 (most common for single-partition images)
mount "/dev/${DEVICE}1" "$MOUNT_POINT" 2>/dev/null || mount "/dev/${DEVICE}p1" "$MOUNT_POINT" || error "Mount failed. Check device partitioning."
info "Mounted at ${MOUNT_POINT}"
# ══════════════════════════════════════════════════════════════════════════════
# 7. APPLY CONFIGURATION TO CARD
# ══════════════════════════════════════════════════════════════════════════════
section "Applying Configuration"
# WiFi profile
if [[ -n "$WIFI_SSID" ]]; then
step "Writing WiFi profile..."
NM_DIR="${MOUNT_POINT}/etc/NetworkManager/system-connections"
NM_FILE="${NM_DIR}/wifi-bb.nmconnection"
mkdir -p "$NM_DIR"
WIFI_UUID=$(cat /proc/sys/kernel/random/uuid)
cat > "$NM_FILE" << EOF
[connection]
id=wifi-bb
uuid=${WIFI_UUID}
type=wifi
autoconnect=true
autoconnect-priority=10
[wifi]
mode=infrastructure
ssid=${WIFI_SSID}
[wifi-security]
auth-alg=open
key-mgmt=wpa-psk
psk=${WIFI_PASS}
[ipv4]
method=auto
[ipv6]
addr-gen-mode=stable-privacy
method=auto
EOF
chmod 600 "$NM_FILE"
chown root:root "$NM_FILE"
info "WiFi profile written (SSID: ${WIFI_SSID}, UUID: ${WIFI_UUID})"
fi
# Root password
step "Setting root password..."
ROOT_PASS_HASH=$(openssl passwd -6 "$ROOT_PASS")
sed -i "s|^root:[^:]*:|root:${ROOT_PASS_HASH}:|" "${MOUNT_POINT}/etc/shadow"
info "Root password set"
unset ROOT_PASS ROOT_PASS_HASH
# SSH keys
step "Configuring SSH..."
SSH_DIR="${MOUNT_POINT}/root/.ssh"
mkdir -p "$SSH_DIR"
if [[ "$SSH_MODE" == "generate" ]]; then
TMP_KEY=$(mktemp)
ssh-keygen -t ed25519 -N "" -C "" -f "$TMP_KEY" -q
PRIVKEY=$(cat "$TMP_KEY")
PUBKEY=$(cat "${TMP_KEY}.pub")
rm -f "$TMP_KEY" "${TMP_KEY}.pub"
echo "$PUBKEY" > "${SSH_DIR}/authorized_keys"
INFISICAL_KEY="SSH_PRIVKEY_$(echo "$DEVICE_ID" | tr '[:lower:]-' '[:upper:]_')"
if [[ -x "$CREDS" ]] && "$CREDS" set "$INFISICAL_KEY" "$PRIVKEY" bigbrother 2>/dev/null; then
info "Private key stored in Infisical: bigbrother/${INFISICAL_KEY}"
else
warn "Could not store in Infisical. SAVE THIS PRIVATE KEY:"
echo "---"
echo "$PRIVKEY"
echo "---"
fi
unset PRIVKEY
else
echo "$SSH_PUBKEY" > "${SSH_DIR}/authorized_keys"
fi
chmod 700 "$SSH_DIR"
chmod 600 "${SSH_DIR}/authorized_keys"
info "SSH authorized_keys configured"
# Hostname
step "Setting hostname..."
echo "$BB_HOSTNAME" > "${MOUNT_POINT}/etc/hostname"
sed -i "s/orangepi/$BB_HOSTNAME/g" "${MOUNT_POINT}/etc/hosts" 2>/dev/null || true
info "Hostname set to: $BB_HOSTNAME"
# Disable first-login wizard
rm -f "${MOUNT_POINT}/root/.not_logged_in_yet"
# Copy BigBrother source
step "Copying BigBrother source to card..."
SRC_DEST="${MOUNT_POINT}/root/bb-src"
rm -rf "$SRC_DEST"
rsync -a \
--exclude='.git' \
--exclude='.venv' \
--exclude='storage/' \
--exclude='__pycache__' \
--exclude='*.pyc' \
--exclude='*.db' \
"${SCRIPT_DIR}/" "$SRC_DEST/" || error "rsync failed"
info "Source copied to /root/bb-src/"
# Write bb-config.env
step "Writing configuration to card..."
cat > "${MOUNT_POINT}/root/bb-config.env" << EOF
BB_MODE="${BB_MODE}"
BB_VPN="${BB_VPN}"
BB_DEVICE_ID="${DEVICE_ID}"
EOF
if [[ -n "$BB_ALERTER_WEBHOOK" ]]; then
echo "BB_ALERTER_WEBHOOK=\"${BB_ALERTER_WEBHOOK}\"" >> "${MOUNT_POINT}/root/bb-config.env"
fi
# Store Tailscale auth key if needed
if [[ "$BB_VPN" == "tailscale" && -n "$TAILSCALE_KEY" ]]; then
echo "BB_TAILSCALE_KEY=\"${TAILSCALE_KEY}\"" >> "${MOUNT_POINT}/root/bb-config.env"
fi
# Store WireGuard config if needed
if [[ "$BB_VPN" == "wireguard" && -n "$WG_CONFIG" ]]; then
echo "$WG_CONFIG" > "${MOUNT_POINT}/etc/wireguard/wg0.conf"
chmod 600 "${MOUNT_POINT}/etc/wireguard/wg0.conf"
fi
# Store autossh config if needed
if [[ "$BB_VPN" == "autossh" ]]; then
echo "BB_AUTOSSH_SERVER=\"${AUTOSSH_SERVER}\"" >> "${MOUNT_POINT}/root/bb-config.env"
echo "BB_AUTOSSH_USER=\"${AUTOSSH_USER}\"" >> "${MOUNT_POINT}/root/bb-config.env"
echo "BB_AUTOSSH_PORT=\"${AUTOSSH_PORT}\"" >> "${MOUNT_POINT}/root/bb-config.env"
echo "BB_AUTOSSH_REMOTE_PORT=\"${AUTOSSH_REMOTE_PORT}\"" >> "${MOUNT_POINT}/root/bb-config.env"
# Write tunnel private key — separate from device management key
mkdir -p "${MOUNT_POINT}/root/.ssh"
echo "$AUTOSSH_PRIVKEY" > "${MOUNT_POINT}/root/.ssh/bb-tunnel-key"
chmod 600 "${MOUNT_POINT}/root/.ssh/bb-tunnel-key"
info "Tunnel key written to /root/.ssh/bb-tunnel-key"
unset AUTOSSH_PRIVKEY
fi
chmod 600 "${MOUNT_POINT}/root/bb-config.env"
info "Configuration written"
# First-boot script
step "Installing first-boot service..."
cat > "${MOUNT_POINT}/root/bb-firstboot-run.sh" << 'RUNEOF'
#!/usr/bin/env bash
# Runs once on first boot, then disables itself
set -euo pipefail
LOG=/root/bb-firstboot.log
exec >> "$LOG" 2>&1
echo "=== BigBrother first-boot setup: $(date) ==="
# Wait for internet connectivity
echo "[*] Waiting for internet..."
for i in $(seq 1 30); do
if curl -sf --max-time 5 http://deb.debian.org/debian > /dev/null 2>&1; then
echo "[+] Internet OK"
break
fi
if [[ $i -eq 30 ]]; then
echo "[-] No internet after 90s"
exit 1
fi
sleep 3
done
source /root/bb-config.env
# Install BigBrother
cd /root/bb-src
bash setup.sh --enable-service
# VPN setup
if [[ "${BB_VPN:-}" == "tailscale" && -n "${BB_TAILSCALE_KEY:-}" ]]; then
echo "[*] Installing Tailscale..."
curl -fsSL https://tailscale.com/install.sh | sh
echo "[*] Connecting to Tailscale..."
tailscale up --auth-key="${BB_TAILSCALE_KEY}" --accept-routes || echo "[-] Tailscale auth failed"
# Remove auth key from config after use
sed -i '/BB_TAILSCALE_KEY/d' /root/bb-config.env
elif [[ "${BB_VPN:-}" == "wireguard" ]]; then
echo "[*] Installing WireGuard..."
apt-get update >/dev/null 2>&1
apt-get install -y wireguard >/dev/null 2>&1
systemctl enable --now wg-quick@wg0 || echo "[-] WireGuard startup failed"
elif [[ "${BB_VPN:-}" == "autossh" ]]; then
echo "[*] Installing autossh..."
apt-get update >/dev/null 2>&1
apt-get install -y autossh >/dev/null 2>&1
chmod 600 /root/.ssh/bb-tunnel-key 2>/dev/null || true
cat > /etc/systemd/system/bb-tunnel.service << SVCEOF
[Unit]
Description=BigBrother reverse SSH tunnel
After=network-online.target
Wants=network-online.target
[Service]
Environment="AUTOSSH_GATETIME=0"
ExecStart=/usr/bin/autossh -M 0 -N \
-o ServerAliveInterval=30 \
-o ServerAliveCountMax=3 \
-o StrictHostKeyChecking=no \
-o ExitOnForwardFailure=yes \
-i /root/.ssh/bb-tunnel-key \
-p ${BB_AUTOSSH_PORT} \
-R ${BB_AUTOSSH_REMOTE_PORT}:localhost:22 \
${BB_AUTOSSH_USER}@${BB_AUTOSSH_SERVER}
Restart=always
RestartSec=30
User=root
[Install]
WantedBy=multi-user.target
SVCEOF
systemctl daemon-reload
systemctl enable --now bb-tunnel.service || echo "[-] Tunnel service startup failed"
echo "[+] Reverse SSH tunnel configured (remote port ${BB_AUTOSSH_REMOTE_PORT})"
fi
# Network alerter
if [[ -n "${BB_ALERTER_WEBHOOK:-}" ]]; then
echo "[*] Sending online alert..."
curl -sf -X POST "$BB_ALERTER_WEBHOOK" \
-H 'Content-Type: application/json' \
-d "{\"text\":\"BigBrother ${BB_DEVICE_ID} online\"}" || true
fi
echo "[+] First-boot complete: $(date)"
# Disable ourselves
rm -f /root/.bb-firstboot-needed
systemctl disable bb-firstboot.service
RUNEOF
chmod 755 "${MOUNT_POINT}/root/bb-firstboot-run.sh"
# First-boot systemd service
cat > "${MOUNT_POINT}/etc/systemd/system/bb-firstboot.service" << 'SVCEOF'
[Unit]
Description=BigBrother first-boot setup
After=network-online.target
Wants=network-online.target
ConditionPathExists=/root/.bb-firstboot-needed
[Service]
Type=oneshot
ExecStart=/bin/bash /root/bb-firstboot-run.sh
RemainAfterExit=yes
TimeoutStartSec=900
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
SVCEOF
# Enable service symlink
WANTS_DIR="${MOUNT_POINT}/etc/systemd/system/multi-user.target.wants"
mkdir -p "$WANTS_DIR"
ln -sf /etc/systemd/system/bb-firstboot.service "${WANTS_DIR}/bb-firstboot.service"
# Firstboot sentinel
touch "${MOUNT_POINT}/root/.bb-firstboot-needed"
info "First-boot service installed and enabled"
# ══════════════════════════════════════════════════════════════════════════════
# 8. UNMOUNT
# ══════════════════════════════════════════════════════════════════════════════
section "Finalizing"
step "Syncing and unmounting..."
sync
umount "$MOUNT_POINT" || warn "umount failed (non-critical)"
info "SD card ready to remove"
# ══════════════════════════════════════════════════════════════════════════════
# 9. DONE
# ══════════════════════════════════════════════════════════════════════════════
section "Setup Complete"
echo "${GREEN}Insert SD card into device and power on.${NC}"
echo ""
echo "The device will:"
echo " 1. Boot and connect to WiFi (if configured)"
echo " 2. Run /root/bb-firstboot-run.sh automatically"
echo " 3. Install BigBrother source and dependencies"
echo " 4. Set up systemd services"
echo " 5. Configure VPN (if selected)"
echo " 6. Send alerter webhook (if configured)"
echo ""
echo "No further operator interaction required. Device walks away autonomously."
echo ""
echo "${CYAN}To SSH after first-boot:${NC}"
if [[ "$SSH_MODE" == "generate" ]]; then
INFISICAL_KEY="SSH_PRIVKEY_$(echo "$DEVICE_ID" | tr '[:lower:]-' '[:upper:]_')"
if [[ -x "$CREDS" ]]; then
echo " ${CREDS} get ${INFISICAL_KEY} bigbrother > /tmp/${DEVICE_ID}.key"
echo " chmod 600 /tmp/${DEVICE_ID}.key"
echo " ssh -i /tmp/${DEVICE_ID}.key root@<DEVICE_IP>"
fi
else
echo " ssh -i $(dirname $ssh_pubkey_path)/$(basename $ssh_pubkey_path .pub) root@<DEVICE_IP>"
fi
echo ""
if [[ "$BB_VPN" == "autossh" ]]; then
echo ""
echo "${CYAN}Tunnel connection (after first boot):${NC}"
echo " From operator server: ssh -p ${AUTOSSH_REMOTE_PORT} root@127.0.0.1"
echo " One-liner from anywhere (requires GatewayPorts yes on server):"
echo " ssh -p ${AUTOSSH_REMOTE_PORT} root@${AUTOSSH_SERVER}"
fi
echo ""
echo "${CYAN}Monitor first-boot:${NC}"
echo " ssh root@<DEVICE_IP> 'tail -f /root/bb-firstboot.log'"
echo ""