From 5e5f88d6cb03d48f20b3c452aaf9b905e7952e07 Mon Sep 17 00:00:00 2001 From: Cobra Date: Tue, 7 Apr 2026 13:45:04 -0400 Subject: [PATCH] Full autonomous first-boot: fix WiFi UUID, self-installing setup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit WiFi keyfiles require UUID or NM silently ignores them — that was why WiFi never connected. Rewrote preconfig_sd.sh to: - Generate UUID for NM keyfile - Copy BB source to /root/bb-src/ on the card - Install bb-firstboot.service that runs setup.sh --enable-service after network-online.target, then disables itself - Per-device keygen, private key to Infisical Single command now produces a card that boots and self-installs. --- scripts/preconfig_sd.sh | 186 ++++++++++++++++++++++++++++++---------- 1 file changed, 142 insertions(+), 44 deletions(-) diff --git a/scripts/preconfig_sd.sh b/scripts/preconfig_sd.sh index b931216..ce3f678 100755 --- a/scripts/preconfig_sd.sh +++ b/scripts/preconfig_sd.sh @@ -1,20 +1,25 @@ #!/usr/bin/env bash -# Pre-configure an Armbian SD card before first boot. -# Handles: WiFi, root password, per-device SSH keypair, hostname, wizard bypass. +# Pre-configure an Armbian SD card for fully autonomous first-boot setup. # -# Generates a fresh ed25519 keypair unique to this device. -# Private key is stored in Infisical under /bigbrother/DEVICE_ID. -# Public key is written to the SD card — no operator keys touch the device. +# What this does: +# 1. Writes WiFi profile (correct NM keyfile with UUID) +# 2. Sets root password +# 3. Generates per-device SSH keypair, stores private key in Infisical +# 4. Copies BigBrother source onto the card +# 5. Installs a systemd firstboot service that runs setup.sh on first boot +# 6. Disables the Armbian first-login wizard +# +# After this: unmount, insert, power on. Device connects to WiFi and +# self-installs BigBrother with no further operator interaction required. # # Usage: sudo bash scripts/preconfig_sd.sh SSID PASSPHRASE DEVICE_ID [MOUNT_POINT] -# DEVICE_ID short identifier for this unit (e.g. bb-01, drop-lab) -# MOUNT_POINT defaults to /mnt/opi set -euo pipefail -RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; NC='\033[0m' +RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; CYAN='\033[0;36m'; NC='\033[0m' info() { echo -e "${GREEN}[+]${NC} $*"; } warn() { echo -e "${YELLOW}[!]${NC} $*"; } error() { echo -e "${RED}[-]${NC} $*"; exit 1; } +step() { echo -e "${CYAN}[*]${NC} $*"; } [[ $EUID -eq 0 ]] || error "Must run as root (sudo)" @@ -25,24 +30,31 @@ MOUNT="${4:-/mnt/opi}" [[ -z "$SSID" || -z "$PASSPHRASE" || -z "$DEVICE_ID" ]] && { echo "Usage: sudo $0 SSID PASSPHRASE DEVICE_ID [MOUNT_POINT]" - echo " DEVICE_ID short label for this unit stored in Infisical (e.g. bb-01)" + echo " DEVICE_ID short label for this unit (e.g. bb-01, drop-lab)" exit 1 } [[ -d "$MOUNT/etc" ]] || error "Mount point not valid: $MOUNT — is the SD card mounted?" +SCRIPT_DIR="$(cd "$(dirname "$0")/.." && pwd)" CREDS="${HOME}/.local/bin/creds" -[[ -x "$CREDS" ]] || error "creds CLI not found at $CREDS — cannot store key in Infisical" -# ── WiFi ────────────────────────────────────────────────────────────────────── +# ── 1. WiFi ─────────────────────────────────────────────────────────────────── +step "Writing WiFi profile..." NM_DIR="${MOUNT}/etc/NetworkManager/system-connections" NM_FILE="${NM_DIR}/wifi-bb.nmconnection" mkdir -p "$NM_DIR" + +# UUID is required — NM silently ignores keyfiles without it +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 @@ -57,12 +69,16 @@ psk=${PASSPHRASE} method=auto [ipv6] -method=disabled +addr-gen-mode=stable-privacy +method=auto EOF -chmod 600 "$NM_FILE" -info "WiFi profile written (${SSID})" -# ── Root password ───────────────────────────────────────────────────────────── +chmod 600 "$NM_FILE" +chown root:root "$NM_FILE" +info "WiFi profile written (SSID: ${SSID}, UUID: ${WIFI_UUID})" + +# ── 2. Root password ────────────────────────────────────────────────────────── +step "Setting root password..." read -s -r -p "New root password: " ROOT_PASS; echo read -s -r -p "Confirm: " ROOT_PASS2; echo [[ "$ROOT_PASS" == "$ROOT_PASS2" ]] || error "Passwords do not match" @@ -71,48 +87,130 @@ sed -i "s|^root:[^:]*:|root:${HASH}:|" "${MOUNT}/etc/shadow" info "Root password set" unset ROOT_PASS ROOT_PASS2 -# ── Per-device SSH keypair ──────────────────────────────────────────────────── -# Generate fresh keypair — no operator keys, no identifying comments +# ── 3. Per-device SSH keypair ───────────────────────────────────────────────── +step "Generating per-device SSH keypair..." 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" -# Store private key in Infisical under bigbrother/DEVICE_ID -# Key name: SSH_PRIVKEY_ (uppercase, hyphens to underscores) -INFISICAL_KEY="SSH_PRIVKEY_$(echo "$DEVICE_ID" | tr '[:lower:]-' '[:upper:]_')" -if "$CREDS" set "$INFISICAL_KEY" "$PRIVKEY" bigbrother 2>/dev/null; then - info "Private key stored in Infisical: bigbrother/${INFISICAL_KEY}" -else - # Fallback: print key and warn — operator must store it manually - warn "Could not store key in Infisical. SAVE THIS PRIVATE KEY NOW:" - echo "------" - echo "$PRIVKEY" - echo "------" -fi -unset PRIVKEY - -# Write only the public key to the device SSH_DIR="${MOUNT}/root/.ssh" mkdir -p "$SSH_DIR" echo "$PUBKEY" > "${SSH_DIR}/authorized_keys" chmod 700 "$SSH_DIR" chmod 600 "${SSH_DIR}/authorized_keys" -info "Per-device public key written to SD card (no operator keys present)" -# ── Hostname ────────────────────────────────────────────────────────────────── +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 NOW:" + echo "---" + echo "$PRIVKEY" + echo "---" +fi +unset PRIVKEY +info "Public key written to device (no operator keys present)" + +# ── 4. Copy BigBrother source onto card ─────────────────────────────────────── +step "Copying BigBrother source to SD card..." +SRC_DEST="${MOUNT}/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/" +info "Source copied to /root/bb-src/" + +# ── 5. First-boot systemd service ───────────────────────────────────────────── +step "Installing first-boot service..." + +# Sentinel file — service checks for this before running +touch "${MOUNT}/root/.bb-firstboot-needed" + +# The service wrapper — logs to /root/bb-firstboot.log +cat > "${MOUNT}/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 actual internet connectivity (apt needs it) +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 — aborting" + exit 1 + fi + sleep 3 +done + +cd /root/bb-src +bash setup.sh --enable-service + +echo "[+] Setup complete: $(date)" + +# Disable ourselves +rm -f /root/.bb-firstboot-needed +systemctl disable bb-firstboot.service +RUNEOF +chmod 755 "${MOUNT}/root/bb-firstboot-run.sh" + +cat > "${MOUNT}/etc/systemd/system/bb-firstboot.service" << 'SVCEOF' +[Unit] +Description=BigBrother first-boot installation +After=network-online.target +Wants=network-online.target +ConditionPathExists=/root/.bb-firstboot-needed + +[Service] +Type=oneshot +ExecStart=/root/bb-firstboot-run.sh +RemainAfterExit=yes +TimeoutStartSec=600 +StandardOutput=journal +StandardError=journal + +[Install] +WantedBy=multi-user.target +SVCEOF + +# Enable the service via symlink (same as systemctl enable without running systemd) +WANTS_DIR="${MOUNT}/etc/systemd/system/multi-user.target.wants" +mkdir -p "$WANTS_DIR" +ln -sf /etc/systemd/system/bb-firstboot.service "${WANTS_DIR}/bb-firstboot.service" +info "First-boot service installed and enabled" + +# ── 6. Hostname + wizard bypass ─────────────────────────────────────────────── echo "orangepi" > "${MOUNT}/etc/hostname" -info "Hostname set to: orangepi" - -# ── Bypass first-login wizard ───────────────────────────────────────────────── rm -f "${MOUNT}/root/.not_logged_in_yet" -info "First-login wizard disabled" +info "Hostname set, first-login wizard disabled" echo "" -info "SD card ready. Unmount, insert, and power on." -info "To connect after boot:" -info " ${CREDS} get ${INFISICAL_KEY} bigbrother > /tmp/bb-${DEVICE_ID}.key" -info " chmod 600 /tmp/bb-${DEVICE_ID}.key" -info " ssh -i /tmp/bb-${DEVICE_ID}.key root@10.0.0.0" -info "To deploy: bash scripts/deploy.sh root@10.0.0.0 --enable-service" +info "SD card ready. Unmount and insert." +echo "" +echo " On boot the device will:" +echo " 1. Connect to ${SSID}" +echo " 2. Run setup.sh automatically (~5-10 min)" +echo " 3. Start bigbrother-core.service" +echo "" +echo " To connect after setup:" +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@" +fi +echo "" +echo " Monitor first-boot: ssh ... 'tail -f /root/bb-firstboot.log'"