mirror of
https://github.com/CyberMind-FR/secubox-deb.git
synced 2026-07-28 21:17:36 +00:00
Two failures surfaced by the strict apt-get pass that --kiosk runs in the rpi chroot (the lenient `dpkg -i --force-depends || true` had been swallowing the first one on every build): 1. secubox-mediaflow postinst had `#DEBHELPER#` inside a comment. dh_installsystemd substitutes that token textually wherever it appears, so it expanded the systemd block mid-comment and orphaned "; kick one refresh" onto its own line → `syntax error near ';'` → configure fails on EVERY install. Reword the comment (real token stays alone on line 22). Bump to ~bookworm3. 2. The kiosk apt-get relied on DEBIAN_FRONTEND=noninteractive, which governs debconf, not dpkg's conffile prompt — a pre-existing /etc/secubox/mesh.toml triggered an interactive prompt that EOFs on the closed chroot stdin and aborted the build. Pass --force-confdef/--force-confold (as the main dpkg --configure pass already does) to auto-keep existing conffiles. Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
1475 lines
61 KiB
Bash
Executable File
1475 lines
61 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
# ══════════════════════════════════════════════════════════════════
|
||
# SecuBox-DEB — build-rpi-usb.sh v1.0
|
||
# Build a bootable USB image for Raspberry Pi 400 (arm64) with:
|
||
# - Native Pi bootloader (no GRUB)
|
||
# - All SecuBox packages slipstreamed
|
||
# - Root autologin
|
||
# - Optional GUI kiosk mode
|
||
# ══════════════════════════════════════════════════════════════════
|
||
set -euo pipefail
|
||
|
||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||
REPO_DIR="$(dirname "$SCRIPT_DIR")"
|
||
|
||
# ── Version ───────────────────────────────────────────────────────
|
||
SECUBOX_VERSION="1.10.0"
|
||
|
||
# ── Defaults ──────────────────────────────────────────────────────
|
||
SUITE="bookworm"
|
||
IMG_SIZE="8G"
|
||
OUT_DIR="${REPO_DIR}/output"
|
||
APT_MIRROR="http://deb.debian.org/debian"
|
||
USE_LOCAL_CACHE=0
|
||
INCLUDE_KIOSK=0
|
||
NO_COMPRESS=0
|
||
SLIPSTREAM_DEBS=0
|
||
|
||
RED='\033[0;31m'; CYAN='\033[0;36m'; GOLD='\033[0;33m'
|
||
GREEN='\033[0;32m'; NC='\033[0m'; BOLD='\033[1m'
|
||
|
||
log() { echo -e "${CYAN}[rpi-usb]${NC} $*"; }
|
||
ok() { echo -e "${GREEN}[ OK ]${NC} $*"; }
|
||
err() { echo -e "${RED}[FAIL ]${NC} $*" >&2; exit 1; }
|
||
warn() { echo -e "${GOLD}[ WARN ]${NC} $*"; }
|
||
|
||
usage() {
|
||
cat <<EOF
|
||
Usage: sudo bash build-rpi-usb.sh [OPTIONS]
|
||
|
||
--suite SUITE Debian suite (default: bookworm)
|
||
--out DIR Output directory (default: ./output)
|
||
--size SIZE Total image size (default: 8G)
|
||
--local-cache Use local APT cache
|
||
--slipstream Install .deb packages from output/debs/
|
||
--kiosk Include GUI kiosk mode packages
|
||
--no-compress Skip gzip compression
|
||
--help Show this help
|
||
|
||
Target: Raspberry Pi 400 (arm64)
|
||
Output: secubox-rpi-arm64-bookworm.img
|
||
|
||
Flash to USB/SD:
|
||
zcat output/secubox-rpi-arm64-bookworm.img.gz | sudo dd of=/dev/sdX bs=4M status=progress
|
||
|
||
EOF
|
||
exit 0
|
||
}
|
||
|
||
while [[ $# -gt 0 ]]; do
|
||
case "$1" in
|
||
--suite) SUITE="$2"; shift 2 ;;
|
||
--out) OUT_DIR="$2"; shift 2 ;;
|
||
--size) IMG_SIZE="$2"; shift 2 ;;
|
||
--local-cache) USE_LOCAL_CACHE=1; shift ;;
|
||
--slipstream) SLIPSTREAM_DEBS=1; shift ;;
|
||
--kiosk) INCLUDE_KIOSK=1; shift ;;
|
||
--no-compress) NO_COMPRESS=1; shift ;;
|
||
--help|-h) usage ;;
|
||
*) err "Unknown argument: $1" ;;
|
||
esac
|
||
done
|
||
|
||
# ── Checks ────────────────────────────────────────────────────────
|
||
[[ $EUID -ne 0 ]] && err "This script must be run as root (sudo)"
|
||
|
||
log "Checking dependencies..."
|
||
apt-get install -y -qq debootstrap qemu-user-static binfmt-support \
|
||
dosfstools parted e2fsprogs 2>/dev/null || true
|
||
|
||
for cmd in debootstrap parted mkfs.fat mkfs.ext4; do
|
||
command -v "$cmd" >/dev/null || err "Missing: $cmd"
|
||
done
|
||
|
||
# Enable ARM64 emulation
|
||
if [[ ! -f /proc/sys/fs/binfmt_misc/qemu-aarch64 ]]; then
|
||
update-binfmts --enable qemu-aarch64 2>/dev/null || warn "qemu-aarch64 binfmt not available"
|
||
fi
|
||
|
||
# ── Variables ─────────────────────────────────────────────────────
|
||
WORK_DIR=$(mktemp -d)
|
||
ROOTFS="${WORK_DIR}/rootfs"
|
||
IMG_FILE="${OUT_DIR}/secubox-rpi-arm64-${SUITE}.img"
|
||
|
||
cleanup() {
|
||
log "Cleaning up..."
|
||
# Unmount in reverse order
|
||
umount "${MNT:-}/boot/firmware" 2>/dev/null || true
|
||
umount "${MNT:-}" 2>/dev/null || true
|
||
umount -R "${ROOTFS}" 2>/dev/null || true
|
||
# Release loop device
|
||
if [[ -n "${LOOP:-}" ]]; then
|
||
losetup -d "$LOOP" 2>/dev/null || true
|
||
fi
|
||
# Clean workdir
|
||
rm -rf "${WORK_DIR}" 2>/dev/null || true
|
||
}
|
||
trap "cleanup" EXIT
|
||
|
||
mkdir -p "${OUT_DIR}" "${ROOTFS}"
|
||
|
||
log "══════════════════════════════════════════════════════════"
|
||
log "SecuBox Raspberry Pi 400 Image Builder"
|
||
log "Suite: ${SUITE} | Size: ${IMG_SIZE}"
|
||
log "══════════════════════════════════════════════════════════"
|
||
|
||
# ══════════════════════════════════════════════════════════════════
|
||
# Step 1: Debootstrap ARM64
|
||
# ══════════════════════════════════════════════════════════════════
|
||
log "1/7 Debootstrap arm64..."
|
||
|
||
# IMPORTANT: Do NOT include kernel/initramfs in debootstrap!
|
||
# initramfs generation under QEMU takes 30+ minutes
|
||
# We install kernel AFTER debootstrap with initramfs disabled
|
||
# Core system packages
|
||
INCLUDE_PKGS="systemd,systemd-sysv,dbus,nftables,openssh-server"
|
||
INCLUDE_PKGS+=",python3,python3-pip,nginx,curl,wget,ca-certificates,gnupg"
|
||
INCLUDE_PKGS+=",iproute2,iputils-ping,net-tools,wireguard-tools,dnsmasq"
|
||
INCLUDE_PKGS+=",sudo,less,vim-tiny,cron,rsync,jq"
|
||
INCLUDE_PKGS+=",parted,dosfstools,e2fsprogs,pciutils,usbutils"
|
||
|
||
# Python dependencies for SecuBox modules (apt packages)
|
||
# NOTE: python3-cryptography excluded - fails to configure under QEMU emulation
|
||
# (installed via pip later instead, which works reliably cross-arch)
|
||
INCLUDE_PKGS+=",python3-fastapi,python3-uvicorn,python3-httpx,python3-psutil"
|
||
INCLUDE_PKGS+=",python3-aiosqlite,python3-jinja2,python3-jwt"
|
||
INCLUDE_PKGS+=",python3-aiofiles,python3-pil,python3-tomli,python3-pydantic"
|
||
INCLUDE_PKGS+=",python3-jose,python3-toml,python3-netifaces"
|
||
# Auth/users engine runtime deps — WITHOUT these secubox-auth crashes at import
|
||
# (import pyotp / import qrcode) → nginx 502 → no login. Pure-Python, apt-safe.
|
||
# argon2 (compiled cffi) goes via the pip step below, mirroring cryptography.
|
||
INCLUDE_PKGS+=",python3-pyotp,python3-qrcode"
|
||
|
||
# Network and security tools
|
||
INCLUDE_PKGS+=",bridge-utils,dnsutils,iputils-arping,avahi-daemon,avahi-utils"
|
||
INCLUDE_PKGS+=",ieee-data,procps,openssl,haproxy,qrencode"
|
||
INCLUDE_PKGS+=",fonts-noto-color-emoji,locales,console-setup"
|
||
# Note: linux-image-arm64, plymouth, initramfs-tools installed later
|
||
|
||
debootstrap --arch=arm64 --foreign --include="${INCLUDE_PKGS}" \
|
||
"${SUITE}" "${ROOTFS}" "${APT_MIRROR}"
|
||
|
||
# Complete second stage with QEMU
|
||
cp /usr/bin/qemu-aarch64-static "${ROOTFS}/usr/bin/"
|
||
log "Running debootstrap second stage (this may take a few minutes)..."
|
||
chroot "${ROOTFS}" /debootstrap/debootstrap --second-stage
|
||
|
||
ok "Debootstrap complete"
|
||
|
||
# ══════════════════════════════════════════════════════════════════
|
||
# Step 1.5: Chroot safety net for qemu-arm64 (closes #436)
|
||
# ══════════════════════════════════════════════════════════════════
|
||
# In a qemu-arm64 chroot, /run/systemd/system is bind-mounted from host
|
||
# (so systemctl thinks systemd is running), but dbus can't be reached.
|
||
# Result: every postinst that calls `systemctl enable/start` fails with
|
||
# "Failed to connect to bus: Host is down", leaving packages
|
||
# installed-but-not-configured (including secubox-core, cascading to
|
||
# every secubox-* that depends on it).
|
||
#
|
||
# Three-part safety net :
|
||
# (1) divert /bin/systemctl → /bin/systemctl.distrib, install a wrapper
|
||
# that exports SYSTEMD_OFFLINE=1 before exec'ing the real binary;
|
||
# (2) install policy-rc.d returning 101 (blocks invoke-rc.d service
|
||
# starts during apt operations);
|
||
# (3) tmpfs-bind /boot/firmware so raspi-firmware's postinst
|
||
# (mountpoint -q /boot/firmware) doesn't fail before Step 7
|
||
# loop-mounts the real BOOT partition.
|
||
#
|
||
# Torn down right before Step 7 so the image ships clean.
|
||
log "Installing chroot safety net (systemctl wrapper, policy-rc.d, /boot/firmware tmpfs)"
|
||
|
||
# (1) systemctl wrapper
|
||
chroot "${ROOTFS}" dpkg-divert --add --rename --quiet /bin/systemctl
|
||
cat > "${ROOTFS}/bin/systemctl" <<'SYSTEMCTL_WRAPPER'
|
||
#!/bin/sh
|
||
# Build-time wrapper (#436): force offline mode so postinsts in a qemu
|
||
# chroot can enable/disable units via filesystem only, no dbus needed.
|
||
export SYSTEMD_OFFLINE=1
|
||
exec /bin/systemctl.distrib "$@"
|
||
SYSTEMCTL_WRAPPER
|
||
chmod 0755 "${ROOTFS}/bin/systemctl"
|
||
|
||
# (2) policy-rc.d
|
||
cat > "${ROOTFS}/usr/sbin/policy-rc.d" <<'POLICY_RC_D'
|
||
#!/bin/sh
|
||
# Build-time policy (#436): never start daemons during chroot apt ops.
|
||
exit 101
|
||
POLICY_RC_D
|
||
chmod 0755 "${ROOTFS}/usr/sbin/policy-rc.d"
|
||
|
||
# (3) /boot/firmware as a self-bind-mount (raspi-firmware postinst needs
|
||
# a real mountpoint, otherwise it errors with "missing /boot/firmware").
|
||
# tmpfs is wrong — files raspi-firmware writes (config.txt, kernel, initrd)
|
||
# get THROWN AWAY when the tmpfs is umounted in Step 6.5, then Step 7
|
||
# fails with `grep: config.txt: No such file or directory` (v2.13.9 trap).
|
||
# bind onto self: the dir IS a mountpoint, AND writes go to the underlying
|
||
# directory on the ROOTFS — files persist after umount.
|
||
mkdir -p "${ROOTFS}/boot/firmware"
|
||
mount --bind "${ROOTFS}/boot/firmware" "${ROOTFS}/boot/firmware"
|
||
|
||
# (4) /proc + /sys inside the chroot — raspi-firmware's update-initramfs hook
|
||
# uses findmnt which reads /proc/mounts. Without these, postinst fails
|
||
# silently with "findmnt: can't read /proc/mounts" before chmod/chown errors
|
||
# show. Matches build-live-usb.sh's pattern (lines 277-278).
|
||
mountpoint -q "${ROOTFS}/proc" || mount -t proc proc "${ROOTFS}/proc"
|
||
mountpoint -q "${ROOTFS}/sys" || mount -t sysfs sysfs "${ROOTFS}/sys"
|
||
|
||
ok "Chroot safety net installed"
|
||
|
||
# ══════════════════════════════════════════════════════════════════
|
||
# Step 2: Base configuration
|
||
# ══════════════════════════════════════════════════════════════════
|
||
log "2/7 System configuration..."
|
||
|
||
# Hostname
|
||
echo "secubox-rpi" > "${ROOTFS}/etc/hostname"
|
||
cat > "${ROOTFS}/etc/hosts" <<EOF
|
||
127.0.0.1 localhost secubox.local
|
||
127.0.1.1 secubox-rpi
|
||
::1 localhost ip6-localhost ip6-loopback
|
||
EOF
|
||
|
||
# Root password
|
||
chroot "${ROOTFS}" bash -c 'echo "root:secubox" | chpasswd'
|
||
|
||
# Timezone
|
||
ln -sf /usr/share/zoneinfo/Europe/Paris "${ROOTFS}/etc/localtime"
|
||
|
||
# Locale
|
||
echo "en_US.UTF-8 UTF-8" >> "${ROOTFS}/etc/locale.gen"
|
||
echo "fr_FR.UTF-8 UTF-8" >> "${ROOTFS}/etc/locale.gen"
|
||
chroot "${ROOTFS}" locale-gen 2>/dev/null || true
|
||
|
||
# Console keyboard
|
||
mkdir -p "${ROOTFS}/etc/default"
|
||
cat > "${ROOTFS}/etc/default/keyboard" <<EOF
|
||
XKBMODEL="pc105"
|
||
XKBLAYOUT="fr"
|
||
XKBVARIANT="azerty"
|
||
BACKSPACE="guess"
|
||
EOF
|
||
|
||
# Fstab
|
||
cat > "${ROOTFS}/etc/fstab" <<EOF
|
||
# SecuBox RPi fstab
|
||
/dev/mmcblk0p1 /boot/firmware vfat defaults 0 2
|
||
/dev/mmcblk0p2 / ext4 defaults,noatime 0 1
|
||
EOF
|
||
|
||
# Serial console for Pi
|
||
mkdir -p "${ROOTFS}/etc/systemd/system/serial-getty@ttyAMA0.service.d"
|
||
cat > "${ROOTFS}/etc/systemd/system/serial-getty@ttyAMA0.service.d/autologin.conf" <<EOF
|
||
[Service]
|
||
ExecStart=
|
||
ExecStart=-/sbin/agetty --autologin root --noclear %I \$TERM
|
||
EOF
|
||
|
||
# Enable serial console
|
||
chroot "${ROOTFS}" systemctl enable serial-getty@ttyAMA0.service 2>/dev/null || true
|
||
|
||
# Enable getty@tty1 for HDMI console
|
||
mkdir -p "${ROOTFS}/etc/systemd/system/getty@tty1.service.d"
|
||
cat > "${ROOTFS}/etc/systemd/system/getty@tty1.service.d/override.conf" <<EOF
|
||
[Service]
|
||
ExecStart=
|
||
ExecStart=-/sbin/agetty --autologin root --noclear %I \$TERM
|
||
Type=idle
|
||
EOF
|
||
chroot "${ROOTFS}" systemctl enable getty@tty1.service 2>/dev/null || true
|
||
chroot "${ROOTFS}" systemctl set-default multi-user.target 2>/dev/null || true
|
||
|
||
# ── Cyber Boot Splash (VT100 DEC PDP-style) ─────────────────────────
|
||
cat > "${ROOTFS}/usr/local/bin/secubox-splash" <<'SPLASH'
|
||
#!/bin/bash
|
||
# SecuBox Cyber Boot Splash - VT100/DEC PDP Style
|
||
|
||
ESC="\033"
|
||
GREEN="${ESC}[32m"
|
||
BRIGHT="${ESC}[1m"
|
||
DIM="${ESC}[2m"
|
||
BLINK="${ESC}[5m"
|
||
RESET="${ESC}[0m"
|
||
CLEAR="${ESC}[2J${ESC}[H"
|
||
|
||
echo -ne "$CLEAR$GREEN"
|
||
|
||
type_slow() {
|
||
local text="$1"
|
||
for ((i=0; i<${#text}; i++)); do
|
||
echo -n "${text:$i:1}"
|
||
sleep 0.02
|
||
done
|
||
echo ""
|
||
}
|
||
|
||
cat << 'BANNER'
|
||
|
||
____ _____ ____ _ _ ____ _____ __
|
||
/ ___|| ____/ ___| | | | __ ) / _ \ \/ /
|
||
\___ \| _|| | | | | | _ \| | | \ /
|
||
___) | |__| |___| |_| | |_) | |_| / \
|
||
|____/|_____\____|\___/|____/ \___/_/\_\
|
||
|
||
BANNER
|
||
|
||
echo ""
|
||
echo -e "${BRIGHT}================================================================${RESET}${GREEN}"
|
||
echo " RASPBERRY PI 400 SECURITY TERMINAL"
|
||
echo " SECUBOX CYBER DEFENSE SYSTEM"
|
||
echo -e "${BRIGHT}================================================================${RESET}${GREEN}"
|
||
echo ""
|
||
|
||
type_slow "BOOT SEQUENCE INITIATED..."
|
||
echo ""
|
||
|
||
steps=(
|
||
"MEMORY TEST.................... OK"
|
||
"LOADING KERNEL................. DONE"
|
||
"CRYPTOGRAPHIC MODULES.......... LOADED"
|
||
"NETWORK STACK.................. INITIALIZED"
|
||
"FIREWALL RULES................. ACTIVE"
|
||
"SECURE SHELL................... READY"
|
||
)
|
||
|
||
for step in "${steps[@]}"; do
|
||
echo -n " > "
|
||
type_slow "$step"
|
||
sleep 0.1
|
||
done
|
||
|
||
echo ""
|
||
echo -e "${BRIGHT}================================================================${RESET}${GREEN}"
|
||
echo ""
|
||
echo -e " ${BLINK}*${RESET}${GREEN} SYSTEM READY"
|
||
echo ""
|
||
echo " .------------------------------------------------."
|
||
echo " | SECUBOX CYBER SECURITY PLATFORM |"
|
||
echo " | TYPE 'help' FOR AVAILABLE COMMANDS |"
|
||
echo " | RASPBERRY PI 400 ARM64 EDITION |"
|
||
echo " '------------------------------------------------'"
|
||
echo ""
|
||
echo -e "${DIM} Press ENTER to continue...${RESET}${GREEN}"
|
||
read -t 5 || true
|
||
echo -ne "$RESET"
|
||
SPLASH
|
||
chmod +x "${ROOTFS}/usr/local/bin/secubox-splash"
|
||
|
||
# ── Boot Menu System ──────<E29480><E29480><EFBFBD>──────────────────────────────────────────
|
||
log "Installing boot menu system..."
|
||
|
||
# Boot menu script - shows on early boot with timeout
|
||
cat > "${ROOTFS}/usr/local/bin/secubox-bootmenu" <<'BOOTMENU'
|
||
#!/bin/bash
|
||
# SecuBox Boot Menu - Raspberry Pi Edition
|
||
# Runs early in boot to select operating mode
|
||
|
||
ESC="\033"
|
||
CYAN="${ESC}[36m"
|
||
GREEN="${ESC}[32m"
|
||
GOLD="${ESC}[33m"
|
||
WHITE="${ESC}[97m"
|
||
DIM="${ESC}[2m"
|
||
BOLD="${ESC}[1m"
|
||
RESET="${ESC}[0m"
|
||
CLEAR="${ESC}[2J${ESC}[H"
|
||
|
||
MODE_FILE="/var/lib/secubox/boot-mode"
|
||
DEFAULT_MODE="normal"
|
||
TIMEOUT=5
|
||
|
||
# Load saved mode
|
||
[[ -f "$MODE_FILE" ]] && DEFAULT_MODE=$(cat "$MODE_FILE")
|
||
|
||
show_menu() {
|
||
echo -ne "$CLEAR"
|
||
echo -e "${CYAN}${BOLD}"
|
||
cat << 'LOGO'
|
||
_____ ______ _____ _ _ ____ ______ __
|
||
/ ____| ____/ ____| | | | _ \ / __ \ \ / /
|
||
| (___ | |__ | | | | | | |_) | | | \ V /
|
||
\___ \| __|| | | | | | _ <| | | |> <
|
||
____) | |___| |____| |__| | |_) | |__| / . \
|
||
|_____/|______\_____|\____/|____/ \____/_/ \_\
|
||
LOGO
|
||
echo -e "${RESET}"
|
||
echo -e "${WHITE}${BOLD} RASPBERRY PI 400 - BOOT MODE SELECTION${RESET}"
|
||
echo -e "${DIM} ───<E29480><E29480>─────────────────────────────────────${RESET}"
|
||
echo ""
|
||
echo -e " ${GREEN}1)${RESET} ${BOLD}Normal${RESET} - Standard SecuBox with SSH + Web UI"
|
||
echo -e " ${GREEN}2)${RESET} ${BOLD}Kiosk${RESET} - Fullscreen browser dashboard"
|
||
echo -e " ${GREEN}3)${RESET} ${BOLD}Console${RESET} - Text-based TUI dashboard"
|
||
echo -e " ${GREEN}4)${RESET} ${BOLD}Bridge${RESET} - Transparent inline bridge mode"
|
||
echo -e " ${GREEN}5)${RESET} ${BOLD}Minimal${RESET} - SSH only, no services"
|
||
echo ""
|
||
echo -e "${DIM} ──<E29480><E29480><EFBFBD>───────────────────────────<E29480><E29480>──────────${RESET}"
|
||
echo -e " ${GOLD}Current: ${WHITE}${DEFAULT_MODE}${RESET}"
|
||
echo ""
|
||
}
|
||
|
||
apply_mode() {
|
||
local mode="$1"
|
||
mkdir -p /var/lib/secubox
|
||
echo "$mode" > "$MODE_FILE"
|
||
|
||
case "$mode" in
|
||
normal)
|
||
systemctl set-default multi-user.target
|
||
systemctl disable secubox-kiosk.service 2>/dev/null || true
|
||
;;
|
||
kiosk)
|
||
systemctl set-default graphical.target
|
||
systemctl enable secubox-kiosk.service 2>/dev/null || true
|
||
;;
|
||
console)
|
||
systemctl set-default multi-user.target
|
||
systemctl enable secubox-console.service 2>/dev/null || true
|
||
;;
|
||
bridge)
|
||
systemctl set-default multi-user.target
|
||
systemctl enable secubox-bridge.service 2>/dev/null || true
|
||
;;
|
||
minimal)
|
||
systemctl set-default multi-user.target
|
||
systemctl disable nginx secubox-hub secubox-portal 2>/dev/null || true
|
||
;;
|
||
esac
|
||
}
|
||
|
||
# Check if we should show menu (hold SHIFT or first boot)
|
||
SHOW_MENU=0
|
||
if [[ ! -f "$MODE_FILE" ]]; then
|
||
SHOW_MENU=1 # First boot
|
||
elif [[ -f /tmp/secubox-show-menu ]]; then
|
||
SHOW_MENU=1
|
||
rm -f /tmp/secubox-show-menu
|
||
fi
|
||
|
||
# Check for key press (simplified - check if SHIFT held)
|
||
read -t 0.1 -n 1 key 2>/dev/null && SHOW_MENU=1
|
||
|
||
if [[ $SHOW_MENU -eq 1 ]]; then
|
||
show_menu
|
||
echo -ne " ${WHITE}Select mode [1-5] or ENTER for default (${TIMEOUT}s): ${RESET}"
|
||
|
||
read -t $TIMEOUT -n 1 choice
|
||
echo ""
|
||
|
||
case "$choice" in
|
||
1) apply_mode "normal" ;;
|
||
2) apply_mode "kiosk" ;;
|
||
3) apply_mode "console" ;;
|
||
4) apply_mode "bridge" ;;
|
||
5) apply_mode "minimal" ;;
|
||
*) apply_mode "$DEFAULT_MODE" ;;
|
||
esac
|
||
|
||
echo -e " ${GREEN}Mode set: $(cat $MODE_FILE)${RESET}"
|
||
sleep 1
|
||
else
|
||
# Silent apply current mode
|
||
apply_mode "$DEFAULT_MODE"
|
||
fi
|
||
BOOTMENU
|
||
chmod +x "${ROOTFS}/usr/local/bin/secubox-bootmenu"
|
||
|
||
# Systemd service to run boot menu early
|
||
cat > "${ROOTFS}/etc/systemd/system/secubox-bootmenu.service" <<'BOOTSVC'
|
||
[Unit]
|
||
Description=SecuBox Boot Mode Selector
|
||
DefaultDependencies=no
|
||
Before=sysinit.target
|
||
After=systemd-vconsole-setup.service
|
||
ConditionPathExists=!/run/secubox/bootmenu-done
|
||
|
||
[Service]
|
||
Type=oneshot
|
||
ExecStart=/usr/local/bin/secubox-bootmenu
|
||
ExecStartPost=/usr/bin/touch /run/secubox/bootmenu-done
|
||
StandardInput=tty
|
||
StandardOutput=tty
|
||
TTYPath=/dev/tty1
|
||
TTYReset=yes
|
||
TTYVHangup=yes
|
||
|
||
[Install]
|
||
WantedBy=sysinit.target
|
||
BOOTSVC
|
||
|
||
chroot "${ROOTFS}" systemctl enable secubox-bootmenu.service 2>/dev/null || true
|
||
|
||
# Script to trigger boot menu on next reboot
|
||
cat > "${ROOTFS}/usr/local/bin/secubox-bootmenu-show" <<'SHOWMENU'
|
||
#!/bin/bash
|
||
touch /tmp/secubox-show-menu
|
||
echo "Boot menu will be shown on next reboot."
|
||
echo "Use 'reboot' to restart now."
|
||
SHOWMENU
|
||
chmod +x "${ROOTFS}/usr/local/bin/secubox-bootmenu-show"
|
||
|
||
ok "Boot menu system installed"
|
||
|
||
# Add splash to root's bashrc
|
||
cat >> "${ROOTFS}/root/.bashrc" <<'BASHRC'
|
||
|
||
# SecuBox Cyber Splash on login
|
||
if [ -t 0 ] && [ -z "$SECUBOX_SPLASH_SHOWN" ]; then
|
||
export SECUBOX_SPLASH_SHOWN=1
|
||
/usr/local/bin/secubox-splash 2>/dev/null || true
|
||
fi
|
||
BASHRC
|
||
|
||
# ── Plymouth Boot Splash Theme (SecuBox Cube) ──────────────────────
|
||
log "Installing Plymouth SecuBox-Cube boot splash..."
|
||
|
||
PLYMOUTH_DIR="${ROOTFS}/usr/share/plymouth/themes/secubox-cube"
|
||
mkdir -p "${PLYMOUTH_DIR}"
|
||
|
||
# Copy theme assets from source
|
||
CUBE_SRC="${SCRIPT_DIR}/plymouth/secubox-cube"
|
||
if [[ -d "${CUBE_SRC}" ]]; then
|
||
cp "${CUBE_SRC}/secubox-cube.plymouth" "${PLYMOUTH_DIR}/"
|
||
cp "${CUBE_SRC}/secubox-cube.script" "${PLYMOUTH_DIR}/"
|
||
cp "${CUBE_SRC}/logo.png" "${PLYMOUTH_DIR}/"
|
||
cp "${CUBE_SRC}/scanlines.png" "${PLYMOUTH_DIR}/"
|
||
cp "${CUBE_SRC}/progress-bg.png" "${PLYMOUTH_DIR}/"
|
||
cp "${CUBE_SRC}/progress-fg.png" "${PLYMOUTH_DIR}/"
|
||
cp "${CUBE_SRC}"/icon-*.png "${PLYMOUTH_DIR}/"
|
||
log " Copied cube theme assets from ${CUBE_SRC}"
|
||
else
|
||
warn "Cube theme source not found, creating minimal theme..."
|
||
cat > "${PLYMOUTH_DIR}/secubox-cube.plymouth" <<'PLYTHEME'
|
||
[Plymouth Theme]
|
||
Name=SecuBox Cube
|
||
Description=SecuBox 3D Rotating Cube Boot Splash
|
||
ModuleName=script
|
||
|
||
[script]
|
||
ImageDir=/usr/share/plymouth/themes/secubox-cube
|
||
ScriptFile=/usr/share/plymouth/themes/secubox-cube/secubox-cube.script
|
||
PLYTHEME
|
||
|
||
cat > "${PLYMOUTH_DIR}/secubox-cube.script" <<'PLYSCRIPT'
|
||
Window.SetBackgroundTopColor(0.0, 0.0, 0.0);
|
||
Window.SetBackgroundBottomColor(0.02, 0.04, 0.02);
|
||
screen_width = Window.GetWidth();
|
||
screen_height = Window.GetHeight();
|
||
center_x = screen_width / 2;
|
||
center_y = screen_height / 2;
|
||
banner_image = Image.Text("SECUBOX", 0.0, 1.0, 0.61, "Sans Bold 48");
|
||
banner_sprite = Sprite(banner_image);
|
||
banner_sprite.SetPosition(center_x - banner_image.GetWidth() / 2, center_y - 50, 1);
|
||
fun boot_progress_callback(duration, progress) {
|
||
bar_text = "";
|
||
for (i = 0; i < Math.Int(progress * 30); i++) bar_text = bar_text + "█";
|
||
for (i = Math.Int(progress * 30); i < 30; i++) bar_text = bar_text + "░";
|
||
bar_image = Image.Text(bar_text, 0.0, 0.8, 0.4, "Mono 12");
|
||
bar_sprite = Sprite(bar_image);
|
||
bar_sprite.SetPosition(center_x - bar_image.GetWidth() / 2, center_y + 80, 1);
|
||
}
|
||
Plymouth.SetBootProgressFunction(boot_progress_callback);
|
||
PLYSCRIPT
|
||
fi
|
||
|
||
mkdir -p "${ROOTFS}/etc/plymouth"
|
||
cat > "${ROOTFS}/etc/plymouth/plymouthd.conf" <<EOF
|
||
[Daemon]
|
||
Theme=secubox-cube
|
||
ShowDelay=0
|
||
DeviceTimeout=8
|
||
EOF
|
||
|
||
chroot "${ROOTFS}" plymouth-set-default-theme secubox-cube 2>/dev/null || true
|
||
|
||
mkdir -p "${ROOTFS}/etc/initramfs-tools/conf.d"
|
||
echo "FRAMEBUFFER=y" > "${ROOTFS}/etc/initramfs-tools/conf.d/plymouth"
|
||
|
||
ok "Plymouth SecuBox-Cube theme installed"
|
||
|
||
ok "System configured"
|
||
|
||
# ══════════════════════════════════════════════════════════════════
|
||
# Step 3: Network configuration
|
||
# ══════════════════════════════════════════════════════════════════
|
||
log "3/7 Network configuration..."
|
||
|
||
# Use systemd-networkd for simplicity
|
||
mkdir -p "${ROOTFS}/etc/systemd/network"
|
||
|
||
# Ethernet DHCP
|
||
cat > "${ROOTFS}/etc/systemd/network/10-eth.network" <<EOF
|
||
[Match]
|
||
Name=eth* en*
|
||
|
||
[Network]
|
||
DHCP=yes
|
||
|
||
[DHCP]
|
||
UseDNS=yes
|
||
UseHostname=no
|
||
EOF
|
||
|
||
# WiFi support (optional)
|
||
cat > "${ROOTFS}/etc/systemd/network/20-wlan.network" <<EOF
|
||
[Match]
|
||
Name=wlan*
|
||
|
||
[Network]
|
||
DHCP=yes
|
||
EOF
|
||
|
||
chroot "${ROOTFS}" systemctl enable systemd-networkd.service 2>/dev/null || true
|
||
chroot "${ROOTFS}" systemctl enable systemd-resolved.service 2>/dev/null || true
|
||
|
||
ok "Network configured"
|
||
|
||
# ══════════════════════════════════════════════════════════════════
|
||
# Step 4: Kernel and firmware (BEFORE SecuBox packages to keep apt clean)
|
||
# ══════════════════════════════════════════════════════════════════
|
||
log "4/7 Installing kernel and firmware..."
|
||
|
||
# APT sources
|
||
cat > "${ROOTFS}/etc/apt/sources.list" <<EOF
|
||
deb ${APT_MIRROR} ${SUITE} main contrib non-free non-free-firmware
|
||
deb ${APT_MIRROR} ${SUITE}-updates main contrib non-free non-free-firmware
|
||
EOF
|
||
|
||
chroot "${ROOTFS}" apt-get update -q
|
||
|
||
# Disable initramfs generation during package install (QEMU is too slow)
|
||
# We'll generate it manually later
|
||
mkdir -p "${ROOTFS}/etc/initramfs-tools"
|
||
cat > "${ROOTFS}/etc/initramfs-tools/update-initramfs.conf" <<EOF
|
||
# Disabled during image build (QEMU too slow)
|
||
# Re-enable on first boot if needed
|
||
update_initramfs=no
|
||
EOF
|
||
|
||
# Create hook to skip initramfs during kernel install
|
||
mkdir -p "${ROOTFS}/etc/kernel/postinst.d"
|
||
cat > "${ROOTFS}/etc/kernel/postinst.d/zz-skip-initramfs" <<'HOOK'
|
||
#!/bin/sh
|
||
# Skip initramfs during image build
|
||
if [ -f /etc/initramfs-tools/update-initramfs.conf ]; then
|
||
grep -q "update_initramfs=no" /etc/initramfs-tools/update-initramfs.conf && exit 0
|
||
fi
|
||
HOOK
|
||
chmod +x "${ROOTFS}/etc/kernel/postinst.d/zz-skip-initramfs"
|
||
|
||
# Install kernel, initramfs-tools, firmware, and Python deps while apt is clean
|
||
log "Installing kernel and base packages..."
|
||
chroot "${ROOTFS}" apt-get install -y -q --no-install-recommends \
|
||
linux-image-arm64 initramfs-tools plymouth plymouth-themes \
|
||
raspi-firmware firmware-brcm80211 firmware-misc-nonfree \
|
||
python3-pip python3-venv python3-netifaces \
|
||
2>/dev/null || warn "Some packages may have failed (continuing)"
|
||
|
||
# Remove the skip hook now
|
||
rm -f "${ROOTFS}/etc/kernel/postinst.d/zz-skip-initramfs"
|
||
|
||
# Re-enable initramfs updates
|
||
cat > "${ROOTFS}/etc/initramfs-tools/update-initramfs.conf" <<EOF
|
||
update_initramfs=yes
|
||
EOF
|
||
|
||
# Check kernel was installed
|
||
if ls "${ROOTFS}/lib/modules/"* >/dev/null 2>&1; then
|
||
ok "Kernel and firmware installed"
|
||
else
|
||
warn "Kernel may not have installed properly"
|
||
fi
|
||
|
||
# Install Python dependencies via pip
|
||
log "Installing Python dependencies via pip..."
|
||
chroot "${ROOTFS}" pip3 install --break-system-packages -q \
|
||
fastapi uvicorn[standard] python-jose[cryptography] httpx \
|
||
jinja2 tomli toml pyroute2 psutil pydantic \
|
||
aiofiles aiosqlite authlib cryptography argon2-cffi \
|
||
python-multipart websockets netifaces email-validator \
|
||
2>&1 | tail -10 || true
|
||
ok "Python dependencies installed"
|
||
|
||
# ══════════════════════════════════════════════════════════════════
|
||
# Step 5: SecuBox packages
|
||
# ══════════════════════════════════════════════════════════════════
|
||
log "5/7 Installing SecuBox packages..."
|
||
|
||
# Pre-generate SSL certificates for nginx (BEFORE package install)
|
||
# Packages' postinst scripts check for nginx/certs, so create them first
|
||
log "Pre-generating SSL certificates..."
|
||
mkdir -p "${ROOTFS}/etc/secubox/tls"
|
||
mkdir -p "${ROOTFS}/run/secubox"
|
||
mkdir -p "${ROOTFS}/var/lib/secubox"
|
||
mkdir -p "${ROOTFS}/etc/nginx/secubox.d"
|
||
|
||
# Generate on host system (chroot may lack /dev/urandom)
|
||
openssl req -x509 -newkey rsa:2048 -days 365 \
|
||
-keyout "${ROOTFS}/etc/secubox/tls/key.pem" \
|
||
-out "${ROOTFS}/etc/secubox/tls/cert.pem" \
|
||
-nodes -subj "/CN=secubox-rpi/O=CyberMind SecuBox/C=FR" \
|
||
-addext "subjectAltName=DNS:localhost,DNS:secubox.local,IP:127.0.0.1,IP:192.168.10.1" \
|
||
2>/dev/null
|
||
|
||
if [[ -f "${ROOTFS}/etc/secubox/tls/cert.pem" ]]; then
|
||
chmod 640 "${ROOTFS}/etc/secubox/tls/key.pem"
|
||
chmod 644 "${ROOTFS}/etc/secubox/tls/cert.pem"
|
||
ok "SSL certificates pre-generated"
|
||
else
|
||
warn "SSL cert generation failed"
|
||
fi
|
||
|
||
# SecuBox packages installation
|
||
install -d "${ROOTFS}/tmp/secubox-debs"
|
||
DEBS_INSTALLED=0
|
||
|
||
# Method 1: Slipstream from output/debs/ (--slipstream flag)
|
||
if [[ $SLIPSTREAM_DEBS -eq 1 ]]; then
|
||
DEBS_DIR="${REPO_DIR}/output/debs"
|
||
if [[ -d "${DEBS_DIR}" ]] && ls "${DEBS_DIR}"/secubox-*.deb >/dev/null 2>&1; then
|
||
log "Slipstream: installing packages from output/debs/..."
|
||
cp "${DEBS_DIR}"/secubox-*_all.deb "${ROOTFS}/tmp/secubox-debs/" 2>/dev/null || true
|
||
cp "${DEBS_DIR}"/secubox-*_arm64.deb "${ROOTFS}/tmp/secubox-debs/" 2>/dev/null || true
|
||
DEBS_INSTALLED=1
|
||
else
|
||
warn "Slipstream: no .deb files found in ${DEBS_DIR}"
|
||
fi
|
||
fi
|
||
|
||
# Method 2: Fallback to local cache
|
||
if [[ $DEBS_INSTALLED -eq 0 ]]; then
|
||
CACHE_DEBS="${REPO_DIR}/cache/repo/pool"
|
||
if [[ -d "$CACHE_DEBS" ]]; then
|
||
log "Installing SecuBox packages from cache..."
|
||
find "$CACHE_DEBS" -name "secubox-*_all.deb" -exec cp {} "${ROOTFS}/tmp/secubox-debs/" \;
|
||
find "$CACHE_DEBS" -name "secubox-*_arm64.deb" -exec cp {} "${ROOTFS}/tmp/secubox-debs/" \; 2>/dev/null || true
|
||
DEBS_INSTALLED=1
|
||
fi
|
||
fi
|
||
|
||
# Install collected packages
|
||
DEB_COUNT=$(ls "${ROOTFS}/tmp/secubox-debs/"*.deb 2>/dev/null | wc -l)
|
||
if [[ $DEB_COUNT -gt 0 ]]; then
|
||
log "Installing ${DEB_COUNT} SecuBox packages..."
|
||
|
||
# Ensure systemd directories exist before dpkg
|
||
install -d -m 755 "${ROOTFS}/usr/lib/systemd/system"
|
||
install -d -m 755 "${ROOTFS}/etc/systemd/system"
|
||
|
||
# Install secubox-core first (dependency)
|
||
if ls "${ROOTFS}/tmp/secubox-debs/secubox-core_"*.deb >/dev/null 2>&1; then
|
||
chroot "${ROOTFS}" bash -c 'dpkg -i --force-depends /tmp/secubox-debs/secubox-core_*.deb' 2>/dev/null || true
|
||
fi
|
||
|
||
# Install all packages with force-depends (pip provides Python deps)
|
||
# Note: Don't use head with pipefail - causes SIGPIPE failure
|
||
chroot "${ROOTFS}" bash -c 'dpkg -i --force-depends --force-overwrite /tmp/secubox-debs/*.deb' 2>&1 | \
|
||
grep -v "^dpkg: warning" || true
|
||
|
||
# Configure packages (skip apt-get -f as pip provides Python deps)
|
||
chroot "${ROOTFS}" dpkg --configure -a --force-confold 2>/dev/null || true
|
||
|
||
# Count installed
|
||
INSTALLED=$(chroot "${ROOTFS}" dpkg -l 'secubox-*' 2>/dev/null | grep "^ii" | wc -l)
|
||
ok "Installed ${INSTALLED}/${DEB_COUNT} SecuBox packages"
|
||
else
|
||
warn "No SecuBox packages found to install"
|
||
fi
|
||
|
||
rm -rf "${ROOTFS}/tmp/secubox-debs"
|
||
|
||
# ── Nginx cleanup after package install ──────────────────────────────────
|
||
log "Cleaning bad nginx configs from conf.d..."
|
||
for conf in "${ROOTFS}/etc/nginx/conf.d/"*secubox*.conf "${ROOTFS}/etc/nginx/conf.d/secubox-"*; do
|
||
[[ -f "$conf" ]] || [[ -L "$conf" ]] || continue
|
||
real_file="$conf"
|
||
if [[ -L "$conf" ]]; then
|
||
target=$(readlink "$conf")
|
||
if [[ "$target" == /* ]]; then
|
||
real_file="${ROOTFS}${target}"
|
||
else
|
||
real_file="${ROOTFS}/etc/nginx/conf.d/${target}"
|
||
fi
|
||
fi
|
||
if [[ -f "$real_file" ]] && grep -q "^location" "$real_file" 2>/dev/null; then
|
||
base=$(basename "$conf" .conf | sed 's/^secubox-//')
|
||
mkdir -p "${ROOTFS}/etc/nginx/secubox.d"
|
||
if [[ ! -f "${ROOTFS}/etc/nginx/secubox.d/${base}.conf" ]]; then
|
||
cp "$real_file" "${ROOTFS}/etc/nginx/secubox.d/${base}.conf" 2>/dev/null || true
|
||
fi
|
||
rm -f "$conf"
|
||
fi
|
||
done
|
||
|
||
log "Cleaning bad nginx configs from sites-enabled..."
|
||
for site in "${ROOTFS}/etc/nginx/sites-enabled/"*; do
|
||
[[ -f "$site" ]] || [[ -L "$site" ]] || continue
|
||
[[ "$(basename "$site")" == "secubox" ]] && continue
|
||
real_file="$site"
|
||
[[ -L "$site" ]] && real_file=$(readlink -f "$site") && real_file="${ROOTFS}${real_file#${ROOTFS}}"
|
||
[[ -f "$real_file" ]] || { rm -f "$site"; continue; }
|
||
if grep -q "^location" "$real_file" 2>/dev/null && ! grep -q "^server" "$real_file" 2>/dev/null; then
|
||
base=$(basename "$site" | sed 's/^secubox-//')
|
||
mkdir -p "${ROOTFS}/etc/nginx/secubox.d"
|
||
cp "$real_file" "${ROOTFS}/etc/nginx/secubox.d/${base}.conf" 2>/dev/null || true
|
||
rm -f "$site"
|
||
fi
|
||
done
|
||
|
||
# Activate .dpkg-new configs (dpkg leaves these when conffiles exist)
|
||
log "Activating .dpkg-new nginx configs..."
|
||
DPKG_NEW_COUNT=0
|
||
for newconf in "${ROOTFS}/etc/nginx/secubox.d/"*.dpkg-new; do
|
||
[[ -f "$newconf" ]] || continue
|
||
target="${newconf%.dpkg-new}"
|
||
mv "$newconf" "$target"
|
||
((DPKG_NEW_COUNT++)) || true
|
||
done
|
||
[[ $DPKG_NEW_COUNT -gt 0 ]] && ok "Activated ${DPKG_NEW_COUNT} .dpkg-new configs"
|
||
|
||
# Test nginx configuration
|
||
if chroot "${ROOTFS}" nginx -t 2>&1; then
|
||
ok "Nginx configuration valid"
|
||
else
|
||
warn "Nginx configuration has errors (will be fixed at firstboot)"
|
||
fi
|
||
|
||
# Clean apt cache (keep lists for potential future use)
|
||
chroot "${ROOTFS}" apt-get clean
|
||
|
||
# Enable nginx for API proxying (certs already generated earlier)
|
||
ln -sf /usr/lib/systemd/system/nginx.service \
|
||
"${ROOTFS}/etc/systemd/system/multi-user.target.wants/nginx.service" 2>/dev/null || true
|
||
|
||
ok "SecuBox packages installed"
|
||
|
||
# ══════════════════════════════════════════════════════════════════
|
||
# Step 5.3: Mass-mask non-essential services (closes #442)
|
||
# ══════════════════════════════════════════════════════════════════
|
||
# Each secubox-* .deb's postinst enables its service in
|
||
# multi-user.target.wants/. With secubox-full installed, that's 140+
|
||
# services. Pi 400 (4 GB) can't bring them all up at boot — multi-user
|
||
# never fully activates → graphical.target never activates →
|
||
# secubox-kiosk never starts → boot stops at tty1.
|
||
#
|
||
# Trim multi-user.target.wants/ down to a kiosk-essential whitelist :
|
||
# the web stack that serves http://127.0.0.1/ to chromium, plus the
|
||
# Debian/system minimum. Everything else stays *installed* (so a
|
||
# future profile change can re-enable) but is removed from the boot
|
||
# wants list.
|
||
log "Trimming multi-user.target.wants/ to kiosk-essential whitelist (#442)"
|
||
|
||
KIOSK_WANTS_KEEP=(
|
||
# Debian/system core
|
||
avahi-daemon.service
|
||
console-setup.service
|
||
cron.service
|
||
e2scrub_reap.service
|
||
networking.service
|
||
nginx.service
|
||
remote-fs.target
|
||
ssh.service
|
||
systemd-networkd.service
|
||
wpa_supplicant.service
|
||
# SecuBox kiosk web stack — serves http://127.0.0.1/
|
||
secubox-auth.service
|
||
secubox-certs.service
|
||
secubox-cookies.service
|
||
secubox-core.service
|
||
secubox-defaults.service
|
||
secubox-hardening.service
|
||
secubox-hub.service
|
||
secubox-portal.service
|
||
secubox-runtime.service
|
||
secubox-system.service
|
||
secubox-users.service
|
||
)
|
||
declare -A _KEEP_SET
|
||
for s in "${KIOSK_WANTS_KEEP[@]}"; do _KEEP_SET["$s"]=1; done
|
||
|
||
_wants_dir="${ROOTFS}/etc/systemd/system/multi-user.target.wants"
|
||
_removed=0
|
||
for f in "${_wants_dir}"/*; do
|
||
[[ -e "$f" ]] || continue
|
||
bn=$(basename "$f")
|
||
if [[ -z "${_KEEP_SET[$bn]:-}" ]]; then
|
||
rm -f "$f"
|
||
_removed=$((_removed + 1))
|
||
fi
|
||
done
|
||
ok "Removed ${_removed} non-essential symlinks (kept ${#KIOSK_WANTS_KEEP[@]})"
|
||
|
||
# secubox-bootmenu.service fails with exit 1 in the early-boot tty
|
||
# context (read -t on tty1 returns non-zero before getty is ready).
|
||
# It's redundant on rpi400 because the image pre-seeds boot-mode=kiosk
|
||
# + default.target=graphical.target. Remove it from sysinit.target.wants
|
||
# so its failure doesn't pollute the boot.
|
||
_sysinit_wants="${ROOTFS}/etc/systemd/system/sysinit.target.wants"
|
||
if [[ -e "${_sysinit_wants}/secubox-bootmenu.service" ]]; then
|
||
rm -f "${_sysinit_wants}/secubox-bootmenu.service"
|
||
ok "Removed secubox-bootmenu from sysinit.target.wants (was failing exit 1)"
|
||
fi
|
||
|
||
# ══════════════════════════════════════════════════════════════════
|
||
# Step 5.4: Optional GUI kiosk mode (closes #423)
|
||
# ══════════════════════════════════════════════════════════════════
|
||
# The --kiosk flag was parsed but never acted on (#423 was filed for this
|
||
# stub). Implements: install chromium + minimal X stack, ship the kiosk
|
||
# launcher + xinitrc from image/kiosk/, create the secubox-kiosk.service
|
||
# (already referenced by apply_mode in the boot menu above), pre-seed the
|
||
# boot-mode file to "kiosk" so first boot lands directly in the kiosk.
|
||
if [[ "${INCLUDE_KIOSK}" -eq 1 ]]; then
|
||
log "Installing kiosk mode (chromium fullscreen → http://127.0.0.1/) ..."
|
||
|
||
# Best-effort fix-broken (postinsts may still fail individually in qemu
|
||
# chroot even with the #436 safety net, but we want the kiosk install
|
||
# to proceed regardless). The fail-loud is on the kiosk install below,
|
||
# not on fix-broken.
|
||
log " apt --fix-broken install (best-effort, pre-existing state cleanup)"
|
||
# DEBIAN_FRONTEND=noninteractive governs debconf, NOT dpkg's conffile
|
||
# prompt — a pre-existing /etc/secubox/*.toml still triggers an interactive
|
||
# "install maintainer's version?" that EOFs on the closed chroot stdin and
|
||
# aborts the run. --force-confdef/--force-confold auto-answer (keep current),
|
||
# matching the main dpkg --configure pass above.
|
||
_DPKG_CONF='-o Dpkg::Options::=--force-confdef -o Dpkg::Options::=--force-confold'
|
||
chroot "${ROOTFS}" /bin/bash -c \
|
||
"DEBIAN_FRONTEND=noninteractive apt-get --fix-broken install -y -q ${_DPKG_CONF}" \
|
||
|| warn "apt --fix-broken install reported errors (continuing — kiosk install will report its own)"
|
||
|
||
# Fail-loud on apt errors (was silently masked with || warn, #433 root cause).
|
||
# If chromium/X can't install, abort the whole image build — silently
|
||
# shipping a no-kiosk image is worse than no image. The `-f` flag tells
|
||
# apt to satisfy deps even with some pre-existing broken state.
|
||
log " apt-get install kiosk stack"
|
||
chroot "${ROOTFS}" /bin/bash -c \
|
||
"DEBIAN_FRONTEND=noninteractive apt-get install -y -q -f --no-install-recommends ${_DPKG_CONF} \
|
||
xserver-xorg xinit openbox chromium x11-xserver-utils \
|
||
ca-certificates dbus-x11" \
|
||
|| err "apt-get install failed inside chroot — kiosk packages required when --kiosk is passed"
|
||
|
||
KIOSK_SRC="${SCRIPT_DIR}/kiosk"
|
||
[[ -d "${KIOSK_SRC}" ]] || err "image/kiosk/ source missing — required for --kiosk"
|
||
install -d "${ROOTFS}/usr/share/secubox/kiosk"
|
||
install -m 0755 "${KIOSK_SRC}/secubox-kiosk.sh" \
|
||
"${ROOTFS}/usr/share/secubox/kiosk/secubox-kiosk.sh"
|
||
install -m 0755 "${KIOSK_SRC}/xinitrc" \
|
||
"${ROOTFS}/root/.xinitrc"
|
||
ok "Copied kiosk launcher + xinitrc from ${KIOSK_SRC}"
|
||
|
||
# secubox-kiosk.service: matches the unit name apply_mode already
|
||
# enables/disables (lines ~353/357 of this script). Runs startx on vt7
|
||
# as root; xinitrc above exec's /usr/share/secubox/kiosk/secubox-kiosk.sh.
|
||
cat > "${ROOTFS}/etc/systemd/system/secubox-kiosk.service" <<'KIOSKSVC'
|
||
[Unit]
|
||
Description=SecuBox Kiosk (chromium fullscreen → http://127.0.0.1/)
|
||
After=multi-user.target network-online.target
|
||
Wants=network-online.target
|
||
Conflicts=getty@tty7.service
|
||
|
||
[Service]
|
||
Type=simple
|
||
User=root
|
||
Environment=HOME=/root
|
||
ExecStart=/usr/bin/startx -- :0 vt7
|
||
Restart=on-failure
|
||
RestartSec=5
|
||
StandardInput=tty
|
||
StandardOutput=tty
|
||
TTYPath=/dev/tty7
|
||
TTYReset=yes
|
||
TTYVHangup=yes
|
||
|
||
[Install]
|
||
WantedBy=graphical.target
|
||
KIOSKSVC
|
||
|
||
# First boot lands directly in kiosk — apply_mode (from the boot menu)
|
||
# reads /var/lib/secubox/boot-mode; seed it so DEFAULT_MODE=kiosk.
|
||
install -d "${ROOTFS}/var/lib/secubox"
|
||
echo "kiosk" > "${ROOTFS}/var/lib/secubox/boot-mode"
|
||
|
||
# Fail-loud — graphical.target must exist now that xserver-xorg is installed.
|
||
chroot "${ROOTFS}" systemctl set-default graphical.target \
|
||
|| err "systemctl set-default graphical.target failed — X stack not installed?"
|
||
chroot "${ROOTFS}" systemctl enable secubox-kiosk.service \
|
||
|| err "systemctl enable secubox-kiosk.service failed"
|
||
|
||
# Belt-and-suspenders: `systemctl enable` under SYSTEMD_OFFLINE in a qemu
|
||
# chroot doesn't always materialise the WantedBy symlink (observed v2.13.7).
|
||
# Create it with a *relative* target (matches systemctl's own convention
|
||
# and resolves correctly both inside the chroot AND from the host's
|
||
# assertion check — v2.13.8 used an absolute "/etc/..." path which the
|
||
# host's `[[ -e symlink ]]` could not resolve because the absolute target
|
||
# doesn't exist on the host fs).
|
||
install -d "${ROOTFS}/etc/systemd/system/graphical.target.wants"
|
||
ln -sf "../secubox-kiosk.service" \
|
||
"${ROOTFS}/etc/systemd/system/graphical.target.wants/secubox-kiosk.service"
|
||
|
||
# Build-time assertion (#433): verify EVERY kiosk artefact made it into
|
||
# the rootfs before Step 7's rsync. If any of these is missing, the .img
|
||
# would ship without kiosk despite the CI logging "Kiosk mode installed".
|
||
log " Asserting kiosk artefacts in rootfs (closes #433 silent-fail bug)"
|
||
_kiosk_fail=0
|
||
for _f in \
|
||
"${ROOTFS}/usr/bin/chromium" \
|
||
"${ROOTFS}/usr/bin/startx" \
|
||
"${ROOTFS}/usr/bin/openbox" \
|
||
"${ROOTFS}/usr/share/secubox/kiosk/secubox-kiosk.sh" \
|
||
"${ROOTFS}/root/.xinitrc" \
|
||
"${ROOTFS}/etc/systemd/system/secubox-kiosk.service" \
|
||
"${ROOTFS}/var/lib/secubox/boot-mode" \
|
||
; do
|
||
[[ -e "${_f}" ]] || { warn "MISSING: ${_f}"; _kiosk_fail=1; }
|
||
done
|
||
[[ "$(cat "${ROOTFS}/var/lib/secubox/boot-mode" 2>/dev/null)" == "kiosk" ]] \
|
||
|| { warn "boot-mode != 'kiosk'"; _kiosk_fail=1; }
|
||
[[ "$(readlink "${ROOTFS}/etc/systemd/system/default.target" 2>/dev/null)" \
|
||
== *"graphical.target" ]] || { warn "default.target != graphical.target"; _kiosk_fail=1; }
|
||
[[ -e "${ROOTFS}/etc/systemd/system/graphical.target.wants/secubox-kiosk.service" ]] \
|
||
|| { warn "secubox-kiosk.service not in graphical.target.wants/"; _kiosk_fail=1; }
|
||
|
||
[[ "${_kiosk_fail}" -eq 0 ]] \
|
||
|| err "Kiosk artefacts incomplete — refusing to publish a broken image"
|
||
|
||
ok "Kiosk mode installed and enabled (default boot mode = kiosk, all artefacts verified)"
|
||
else
|
||
log "Kiosk mode skipped (pass --kiosk to enable; #423)"
|
||
fi
|
||
|
||
# ══════════════════════════════════════════════════════════════════
|
||
# Step 5.5: SecuBox CRT-style banners + CLI helpers
|
||
# ══════════════════════════════════════════════════════════════════
|
||
# Ported from build-live-usb.sh so rpi400 ships with the same SecuBox
|
||
# look-and-feel + CLI tools as the amd64 live USB. Until the broader
|
||
# build-scripts refactor lands (see docs/superpowers/plans/2026-05-26-
|
||
# build-scripts-refactor.md), this block is the targeted port.
|
||
log "Creating SecuBox boot banners + CLI helpers..."
|
||
|
||
BUILD_TIMESTAMP=$(date -Iseconds)
|
||
|
||
# /etc/issue — pre-login banner with getty \4 escape for IPv4
|
||
printf '%b' "\e[38;5;29m
|
||
██████ ███████ ██████ ██ ██ ██████ ██████ ██ ██
|
||
██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
|
||
███████ █████ ██ ██ ██ ██████ ██ ██ ███
|
||
██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
|
||
███████ ███████ ██████ ██████ ██████ ██████ ██ ██
|
||
\e[0m
|
||
\e[38;5;45m ⚡ CyberMind Security Platform\e[0m \e[38;5;82mv${SECUBOX_VERSION}\e[0m \e[38;5;242m\\l @ \\n\e[0m
|
||
\e[38;5;242m Build: ${BUILD_TIMESTAMP}\e[0m
|
||
|
||
\e[38;5;250m 🔐 Default: \e[38;5;214mroot\e[38;5;250m / \e[38;5;214msecubox\e[0m
|
||
\e[38;5;250m 🌐 Web UI: \e[38;5;45mhttps://\\4:9443\e[0m
|
||
\e[38;5;250m 📡 SSH: \e[38;5;45mport 22\e[0m
|
||
|
||
\e[38;5;242m─────────────────────────────────────────────────────────────\e[0m
|
||
|
||
" > "${ROOTFS}/etc/issue"
|
||
|
||
# Dynamic MOTD via update-motd.d — live IP at login time
|
||
: > "${ROOTFS}/etc/motd"
|
||
mkdir -p "${ROOTFS}/etc/update-motd.d"
|
||
cat > "${ROOTFS}/etc/update-motd.d/10-secubox" <<MOTD_DYN
|
||
#!/bin/sh
|
||
# Generated by build-rpi-usb.sh — dynamic MOTD with live IP.
|
||
ip=\$(hostname -I 2>/dev/null | awk '{print \$1}')
|
||
[ -z "\$ip" ] && ip="no-ip"
|
||
printf '%b' "\e[38;5;214m
|
||
╔═══════════════════════════════════════════════════════════════╗
|
||
║\e[38;5;45m ███████╗███████╗ ██████╗██╗ ██╗██████╗ ██████╗ ██╗ ██╗ \e[38;5;214m║
|
||
║\e[38;5;45m ██╔════╝██╔════╝██╔════╝██║ ██║██╔══██╗██╔═══██╗╚██╗██╔╝ \e[38;5;214m║
|
||
║\e[38;5;45m ███████╗█████╗ ██║ ██║ ██║██████╔╝██║ ██║ ╚███╔╝ \e[38;5;214m║
|
||
║\e[38;5;45m ╚════██║██╔══╝ ██║ ██║ ██║██╔══██╗██║ ██║ ██╔██╗ \e[38;5;214m║
|
||
║\e[38;5;45m ███████║███████╗╚██████╗╚██████╔╝██████╔╝╚██████╔╝██╔╝ ██╗ \e[38;5;214m║
|
||
║\e[38;5;45m ╚══════╝╚══════╝ ╚═════╝ ╚═════╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝ \e[38;5;214m║
|
||
║\e[38;5;82m ⚡ RPI400 LIVE ⚡ v${SECUBOX_VERSION} \e[38;5;214m║
|
||
╚═══════════════════════════════════════════════════════════════╝\e[0m
|
||
|
||
\e[38;5;242m Build: ${BUILD_TIMESTAMP}\e[0m
|
||
|
||
\e[38;5;250m 🌐 Web UI: \e[38;5;45mhttps://\${ip}:9443\e[0m
|
||
\e[38;5;250m 🔐 Credentials: \e[38;5;214mroot\e[38;5;250m / \e[38;5;214msecubox\e[0m
|
||
\e[38;5;250m 📖 Docs: \e[38;5;45mhttps://secubox.in/docs\e[0m
|
||
|
||
\e[38;5;242m Type \e[38;5;82msecubox-status\e[38;5;242m for system overview\e[0m
|
||
|
||
"
|
||
MOTD_DYN
|
||
chmod +x "${ROOTFS}/etc/update-motd.d/10-secubox"
|
||
|
||
# /usr/bin/secubox-status — runtime system overview
|
||
cat > "${ROOTFS}/usr/bin/secubox-status" <<'STATUS_SCRIPT'
|
||
#!/bin/bash
|
||
GOLD='\033[38;5;214m'; CYAN='\033[38;5;45m'; GREEN='\033[38;5;82m'
|
||
RED='\033[38;5;196m'; GRAY='\033[38;5;242m'; WHITE='\033[38;5;250m'; RESET='\033[0m'
|
||
ok="${GREEN}●${RESET}"; fail="${RED}●${RESET}"; warn="${GOLD}●${RESET}"
|
||
echo -e "${CYAN}"
|
||
echo ' ╭──────────────────────────────────────────────────────────╮'
|
||
echo ' │ ⚡ SecuBox System Status ⚡ │'
|
||
echo ' ╰──────────────────────────────────────────────────────────╯'
|
||
echo -e "${RESET}"
|
||
echo -e "${WHITE} 📊 System Info${RESET}"
|
||
echo -e " ${GRAY}Hostname:${RESET} $(hostname)"
|
||
echo -e " ${GRAY}Uptime:${RESET} $(uptime -p 2>/dev/null || echo 'N/A')"
|
||
echo -e " ${GRAY}Memory:${RESET} $(free -h | awk '/^Mem:/{printf "%s / %s", $3, $2}')"
|
||
echo -e " ${GRAY}Disk:${RESET} $(df -h / | awk 'NR==2{printf "%s / %s (%s)", $3, $2, $5}')"
|
||
echo ""
|
||
echo -e "${WHITE} 🌐 Network${RESET}"
|
||
for iface in $(ip -o link show | awk -F': ' '{print $2}' | grep -v '^lo$'); do
|
||
ip_addr=$(ip -4 addr show "$iface" 2>/dev/null | grep -oP '(?<=inet\s)\d+(\.\d+){3}' | head -1)
|
||
[[ -n "$ip_addr" ]] && echo -e " ${ok} ${GRAY}${iface}:${RESET} ${CYAN}${ip_addr}${RESET}"
|
||
done
|
||
echo ""
|
||
echo -e "${WHITE} 🔧 Core Services${RESET}"
|
||
for svc in nginx haproxy secubox-api secubox-hub crowdsec; do
|
||
if systemctl is-active --quiet "$svc" 2>/dev/null; then
|
||
echo -e " ${ok} ${GRAY}${svc}${RESET}"
|
||
elif systemctl list-unit-files "${svc}.service" 2>/dev/null | grep -q "$svc"; then
|
||
echo -e " ${fail} ${GRAY}${svc}${RESET} (stopped)"
|
||
fi
|
||
done
|
||
echo ""
|
||
IP=$(hostname -I | awk '{print $1}')
|
||
echo -e "${GOLD} ────────────────────────────────────────────────────────────${RESET}"
|
||
echo -e "${WHITE} 🔗 Quick Access${RESET}"
|
||
echo -e " ${GRAY}Dashboard:${RESET} ${CYAN}https://${IP:-localhost}:9443${RESET}"
|
||
echo ""
|
||
STATUS_SCRIPT
|
||
chmod +x "${ROOTFS}/usr/bin/secubox-status"
|
||
|
||
# /usr/bin/secubox-help — command index
|
||
cat > "${ROOTFS}/usr/bin/secubox-help" <<'HELP_CMD'
|
||
#!/bin/bash
|
||
GOLD='\033[38;5;214m'; CYAN='\033[38;5;45m'; WHITE='\033[38;5;250m'
|
||
GRAY='\033[38;5;242m'; RESET='\033[0m'
|
||
echo -e "${GOLD}"
|
||
echo ' ╭─────────────────────────────────────────────────────────╮'
|
||
echo ' │ ⚡ SecuBox Quick Commands │'
|
||
echo ' ╰─────────────────────────────────────────────────────────╯'
|
||
echo -e "${RESET}"
|
||
echo -e " ${WHITE}System${RESET}"
|
||
echo -e " ${CYAN}secubox-status${GRAY} System overview with services${RESET}"
|
||
echo ""
|
||
echo -e " ${WHITE}Web Access${RESET}"
|
||
echo -e " ${GRAY}Dashboard:${RESET} ${CYAN}https://$(hostname -I 2>/dev/null | awk '{print $1}' || echo 'localhost'):9443${RESET}"
|
||
echo ""
|
||
HELP_CMD
|
||
chmod +x "${ROOTFS}/usr/bin/secubox-help"
|
||
|
||
# /etc/secubox/build-info.json — version + build identification
|
||
mkdir -p "${ROOTFS}/etc/secubox"
|
||
cat > "${ROOTFS}/etc/secubox/build-info.json" <<BUILDINFO
|
||
{
|
||
"build_timestamp": "${BUILD_TIMESTAMP}",
|
||
"build_date": "$(date -I)",
|
||
"git_commit": "$(git -C "$(dirname "${BASH_SOURCE[0]}")/.." rev-parse --short HEAD 2>/dev/null || echo 'unknown')",
|
||
"git_branch": "$(git -C "$(dirname "${BASH_SOURCE[0]}")/.." rev-parse --abbrev-ref HEAD 2>/dev/null || echo 'unknown')",
|
||
"board": "rpi400-arm64-live",
|
||
"version": "${SECUBOX_VERSION}",
|
||
"builder": "$(whoami)@$(hostname)"
|
||
}
|
||
BUILDINFO
|
||
|
||
ok "SecuBox banners + CLI helpers installed"
|
||
|
||
# ══════════════════════════════════════════════════════════════════
|
||
# Step 6: Raspberry Pi boot configuration
|
||
# ══════════════════════════════════════════════════════════════════
|
||
log "6/7 Configuring Pi bootloader..."
|
||
|
||
# config.txt for Pi 400
|
||
mkdir -p "${ROOTFS}/boot/firmware"
|
||
cat > "${ROOTFS}/boot/firmware/config.txt" <<EOF
|
||
# SecuBox Raspberry Pi 400 Configuration
|
||
# Pi 4/400 ARM64 Boot Configuration
|
||
|
||
[all]
|
||
# Use 64-bit kernel
|
||
arm_64bit=1
|
||
|
||
# Kernel (Debian naming) - initramfs added later if available
|
||
kernel=vmlinuz
|
||
# initramfs line added dynamically if initrd.img exists
|
||
|
||
# Automatically load appropriate DTB
|
||
# For Pi 400, the firmware will load bcm2711-rpi-400.dtb
|
||
|
||
[pi4]
|
||
# Pi 4 specific settings
|
||
max_framebuffers=2
|
||
|
||
[pi400]
|
||
# Pi 400 specific settings (Pi 400 runs cooler, can handle more)
|
||
arm_boost=1
|
||
|
||
[all]
|
||
# Display
|
||
hdmi_force_hotplug=1
|
||
disable_overscan=1
|
||
|
||
# GPU memory (64MB for headless, 128-256 for desktop)
|
||
gpu_mem=64
|
||
|
||
# Serial console for debugging
|
||
enable_uart=1
|
||
dtoverlay=disable-bt
|
||
|
||
# USB boot mode (already enabled on Pi 400 EEPROM)
|
||
# program_usb_boot_mode=1
|
||
|
||
# Camera/Display (disabled by default)
|
||
# camera_auto_detect=1
|
||
# display_auto_detect=1
|
||
EOF
|
||
|
||
# cmdline.txt - Pi can boot WITHOUT initrd using root= directly
|
||
# This avoids slow initramfs generation under QEMU
|
||
cat > "${ROOTFS}/boot/firmware/cmdline.txt" <<EOF
|
||
console=serial0,115200 console=tty1 root=/dev/mmcblk0p2 rootfstype=ext4 rootwait fsck.repair=yes quiet
|
||
EOF
|
||
|
||
# Check if initrd already exists from kernel install
|
||
if ls "${ROOTFS}/boot/initrd.img-"* >/dev/null 2>&1; then
|
||
log "initrd already exists from kernel install"
|
||
# Update config.txt to use it
|
||
sed -i 's/^initramfs.*/initramfs initrd.img followkernel/' "${ROOTFS}/boot/firmware/config.txt"
|
||
else
|
||
# No initrd - configure for direct boot (faster, simpler)
|
||
log "No initrd - configuring direct boot (no initramfs needed for Pi)"
|
||
# Remove initramfs line from config.txt since we're booting directly
|
||
sed -i '/^initramfs/d' "${ROOTFS}/boot/firmware/config.txt"
|
||
|
||
# Generate initrd (required for module loading)
|
||
# QEMU arm64 emulation is slow - allow 5 minutes
|
||
log "Generating initramfs (this may take 3-5 minutes under QEMU)..."
|
||
KVER=$(ls "${ROOTFS}/lib/modules/" 2>/dev/null | head -1)
|
||
if [[ -n "$KVER" ]]; then
|
||
# Create wrapper script to ensure PATH is set for mkinitramfs
|
||
cat > "${ROOTFS}/tmp/gen-initrd.sh" <<INITRD
|
||
#!/bin/bash
|
||
export PATH=/usr/sbin:/usr/bin:/sbin:/bin:/usr/local/sbin:/usr/local/bin
|
||
update-initramfs -c -k ${KVER}
|
||
INITRD
|
||
chmod +x "${ROOTFS}/tmp/gen-initrd.sh"
|
||
|
||
# Use timeout to prevent infinite hanging
|
||
if timeout 300 chroot "${ROOTFS}" /tmp/gen-initrd.sh; then
|
||
# Verify initrd was actually created and has content
|
||
if [[ -s "${ROOTFS}/boot/initrd.img-${KVER}" ]]; then
|
||
INITRD_SIZE=$(du -h "${ROOTFS}/boot/initrd.img-${KVER}" | cut -f1)
|
||
ok "initrd generated successfully (${INITRD_SIZE})"
|
||
sed -i '/^\[all\]/a initramfs initrd.img followkernel' "${ROOTFS}/boot/firmware/config.txt"
|
||
else
|
||
warn "initrd file empty or missing - Pi may not boot!"
|
||
fi
|
||
else
|
||
warn "initrd generation failed or timed out after 5 minutes!"
|
||
warn "Pi will boot but kernel modules won't load dynamically."
|
||
warn "For full functionality, rebuild on native ARM64 hardware."
|
||
# Don't exit - Pi can still boot, just with reduced functionality
|
||
fi
|
||
else
|
||
err "No kernel modules found in ${ROOTFS}/lib/modules/"
|
||
exit 1
|
||
fi
|
||
fi
|
||
|
||
ok "Pi bootloader configured"
|
||
|
||
# ══════════════════════════════════════════════════════════════════
|
||
# Step 6.5: Tear down chroot safety net (closes #436)
|
||
# ══════════════════════════════════════════════════════════════════
|
||
# Undo Step 1.5's scaffolding so the .img doesn't ship with build-time
|
||
# wrappers. Order matters : umount tmpfs BEFORE the rsync to /boot/firmware
|
||
# in Step 7, restore systemctl AFTER the last `chroot systemctl` call
|
||
# (which was Step 5.4's enable secubox-kiosk.service, already done by here).
|
||
log "Removing chroot safety net (systemctl wrapper, policy-rc.d, /boot/firmware bind)"
|
||
|
||
# (4) umount /proc + /sys (chroot no longer needs them)
|
||
umount -lf "${ROOTFS}/proc" 2>/dev/null || true
|
||
umount -lf "${ROOTFS}/sys" 2>/dev/null || true
|
||
|
||
# (3) umount /boot/firmware bind (Step 7 mounts the real BOOT partition there).
|
||
# Files written by raspi-firmware (config.txt, kernel, initrd, dtbs) PERSIST
|
||
# in the underlying ${ROOTFS}/boot/firmware after this umount because the
|
||
# bind was onto-self — Step 7's rsync to the loop-mounted real BOOT
|
||
# partition then carries them along.
|
||
umount "${ROOTFS}/boot/firmware" 2>/dev/null || warn "/boot/firmware bind was not mounted"
|
||
|
||
# (2) remove policy-rc.d (first boot must be able to start daemons)
|
||
rm -f "${ROOTFS}/usr/sbin/policy-rc.d"
|
||
|
||
# (1) restore real systemctl
|
||
rm -f "${ROOTFS}/bin/systemctl"
|
||
chroot "${ROOTFS}" dpkg-divert --remove --rename --quiet /bin/systemctl
|
||
|
||
ok "Chroot safety net removed"
|
||
|
||
# ══════════════════════════════════════════════════════════════════
|
||
# Step 7: Create image
|
||
# ══════════════════════════════════════════════════════════════════
|
||
log "7/7 Creating bootable image..."
|
||
|
||
rm -f "${IMG_FILE}" "${IMG_FILE}.gz"
|
||
truncate -s "${IMG_SIZE}" "${IMG_FILE}"
|
||
|
||
# Partition: MBR with boot + root
|
||
parted -s "${IMG_FILE}" \
|
||
mklabel msdos \
|
||
mkpart primary fat32 1MiB 257MiB \
|
||
mkpart primary ext4 257MiB 100% \
|
||
set 1 boot on
|
||
|
||
# Setup loop device
|
||
LOOP=$(losetup -fP --show "${IMG_FILE}")
|
||
log "Loop device: ${LOOP}"
|
||
|
||
# Ensure kernel sees partitions (race condition workaround)
|
||
partprobe "${LOOP}" 2>/dev/null || true
|
||
sleep 1
|
||
|
||
# Wait for partition devices to appear
|
||
for i in {1..10}; do
|
||
[[ -b "${LOOP}p1" ]] && break
|
||
sleep 1
|
||
done
|
||
[[ -b "${LOOP}p1" ]] || err "Partition ${LOOP}p1 not found after waiting"
|
||
|
||
# Format partitions
|
||
mkfs.vfat -F 32 -n BOOT "${LOOP}p1"
|
||
mkfs.ext4 -L ROOT -q "${LOOP}p2"
|
||
|
||
# Mount
|
||
MNT="${WORK_DIR}/mnt"
|
||
mkdir -p "${MNT}"
|
||
mount "${LOOP}p2" "${MNT}"
|
||
mkdir -p "${MNT}/boot/firmware"
|
||
mount "${LOOP}p1" "${MNT}/boot/firmware"
|
||
|
||
# Copy rootfs
|
||
log "Copying rootfs..."
|
||
rsync -aHAX --info=progress2 "${ROOTFS}/" "${MNT}/"
|
||
|
||
# Copy Raspberry Pi firmware files to boot partition
|
||
log "Copying Pi firmware files..."
|
||
|
||
# The raspi-firmware package installs files to /usr/lib/raspi-firmware/ in Debian
|
||
FIRMWARE_SRC="${MNT}/usr/lib/raspi-firmware"
|
||
if [[ -d "$FIRMWARE_SRC" ]]; then
|
||
# Copy all firmware files (start*.elf, fixup*.dat, bootcode.bin, etc.)
|
||
cp -v "${FIRMWARE_SRC}"/*.elf "${MNT}/boot/firmware/" 2>/dev/null || warn "No .elf files found"
|
||
cp -v "${FIRMWARE_SRC}"/*.dat "${MNT}/boot/firmware/" 2>/dev/null || warn "No .dat files found"
|
||
cp -v "${FIRMWARE_SRC}"/*.bin "${MNT}/boot/firmware/" 2>/dev/null || true
|
||
cp -v "${FIRMWARE_SRC}"/*.dtb "${MNT}/boot/firmware/" 2>/dev/null || true
|
||
|
||
# Copy overlays directory
|
||
if [[ -d "${FIRMWARE_SRC}/overlays" ]]; then
|
||
cp -rv "${FIRMWARE_SRC}/overlays" "${MNT}/boot/firmware/"
|
||
ok "Firmware overlays copied"
|
||
fi
|
||
ok "Pi firmware copied from raspi-firmware package"
|
||
else
|
||
warn "raspi-firmware not found at ${FIRMWARE_SRC}, trying alternative locations..."
|
||
# Alternative: firmware might be in /boot/firmware already
|
||
if [[ -d "${MNT}/boot/firmware" ]] && ls "${MNT}/boot/firmware/"*.elf >/dev/null 2>&1; then
|
||
ok "Firmware already present in /boot/firmware"
|
||
else
|
||
err "No Raspberry Pi firmware found! Install raspi-firmware package."
|
||
fi
|
||
fi
|
||
|
||
# Copy Device Tree Blobs from kernel package
|
||
if ls "${MNT}/usr/lib/linux-image-"*/broadcom/*.dtb >/dev/null 2>&1; then
|
||
cp -v "${MNT}/usr/lib/linux-image-"*/broadcom/bcm*.dtb "${MNT}/boot/firmware/" 2>/dev/null || true
|
||
ok "Kernel DTBs copied"
|
||
fi
|
||
|
||
# Copy kernel and initrd to boot partition
|
||
# Use ls | sort -V | tail -1 to get the latest version if multiple exist
|
||
VMLINUZ=$(ls "${MNT}/boot/vmlinuz-"* 2>/dev/null | sort -V | tail -1)
|
||
if [[ -n "$VMLINUZ" ]] && [[ -f "$VMLINUZ" ]]; then
|
||
cp "$VMLINUZ" "${MNT}/boot/firmware/vmlinuz"
|
||
ok "Kernel copied: $(basename $VMLINUZ)"
|
||
else
|
||
err "No kernel found in rootfs"
|
||
fi
|
||
|
||
INITRD=$(ls "${MNT}/boot/initrd.img-"* 2>/dev/null | sort -V | tail -1)
|
||
if [[ -n "$INITRD" ]] && [[ -f "$INITRD" ]]; then
|
||
cp "$INITRD" "${MNT}/boot/firmware/initrd.img"
|
||
# Add initramfs line to config.txt since we have an initrd
|
||
if ! grep -q "^initramfs" "${MNT}/boot/firmware/config.txt"; then
|
||
sed -i '/^kernel=vmlinuz/a initramfs initrd.img followkernel' "${MNT}/boot/firmware/config.txt"
|
||
fi
|
||
ok "Initrd copied: $(basename $INITRD)"
|
||
else
|
||
log "No initrd - Pi will boot directly (this is OK)"
|
||
# Ensure no initramfs line in config.txt
|
||
sed -i '/^initramfs/d' "${MNT}/boot/firmware/config.txt"
|
||
fi
|
||
|
||
# Verify critical boot files
|
||
log "Verifying boot files..."
|
||
MISSING_FILES=0
|
||
for f in start4.elf fixup4.dat vmlinuz; do
|
||
if [[ ! -f "${MNT}/boot/firmware/${f}" ]]; then
|
||
warn "Missing: ${f}"
|
||
MISSING_FILES=1
|
||
fi
|
||
done
|
||
if [[ $MISSING_FILES -eq 0 ]]; then
|
||
ok "All critical boot files present"
|
||
else
|
||
warn "Some boot files missing - Pi may not boot!"
|
||
fi
|
||
|
||
# Update fstab with proper UUIDs
|
||
BOOT_UUID=$(blkid -s UUID -o value "${LOOP}p1")
|
||
ROOT_UUID=$(blkid -s UUID -o value "${LOOP}p2")
|
||
cat > "${MNT}/etc/fstab" <<EOF
|
||
# SecuBox RPi fstab
|
||
UUID=${BOOT_UUID} /boot/firmware vfat defaults 0 2
|
||
UUID=${ROOT_UUID} / ext4 defaults,noatime 0 1
|
||
EOF
|
||
|
||
# Update cmdline with UUID (splash for Plymouth boot graphics)
|
||
cat > "${MNT}/boot/firmware/cmdline.txt" <<EOF
|
||
console=serial0,115200 console=tty1 root=UUID=${ROOT_UUID} rootfstype=ext4 elevator=deadline fsck.repair=yes rootwait quiet splash
|
||
EOF
|
||
|
||
# Sync and unmount
|
||
sync
|
||
umount "${MNT}/boot/firmware"
|
||
umount "${MNT}"
|
||
losetup -d "${LOOP}"
|
||
unset LOOP
|
||
|
||
IMG_SIZE_ACTUAL=$(du -sh "${IMG_FILE}" | cut -f1)
|
||
ok "Image created: ${IMG_SIZE_ACTUAL}"
|
||
|
||
# Compress
|
||
if [[ $NO_COMPRESS -eq 0 ]]; then
|
||
log "Compressing..."
|
||
gzip -9 -f "${IMG_FILE}"
|
||
FINAL_SIZE=$(du -sh "${IMG_FILE}.gz" | cut -f1)
|
||
sha256sum "${IMG_FILE}.gz" > "${IMG_FILE}.gz.sha256"
|
||
ok "Compressed: ${FINAL_SIZE}"
|
||
fi
|
||
|
||
# ══════════════════════════════════════════════════════════════════
|
||
# Done
|
||
# ══════════════════════════════════════════════════════════════════
|
||
echo ""
|
||
echo -e "${GREEN}${BOLD}════════════════════════════════════════════════════════${NC}"
|
||
echo -e "${GREEN}${BOLD} SecuBox Raspberry Pi 400 Image Ready!${NC}"
|
||
echo ""
|
||
echo -e " Image: ${IMG_FILE}${NO_COMPRESS:+.gz}"
|
||
echo ""
|
||
echo -e " ${CYAN}Flash to USB/SD:${NC}"
|
||
if [[ $NO_COMPRESS -eq 0 ]]; then
|
||
echo -e " zcat ${IMG_FILE}.gz | sudo dd of=/dev/sdX bs=4M status=progress"
|
||
else
|
||
echo -e " sudo dd if=${IMG_FILE} of=/dev/sdX bs=4M status=progress"
|
||
fi
|
||
echo ""
|
||
echo -e " ${CYAN}Default login:${NC} root / secubox"
|
||
echo -e "${GREEN}${BOLD}════════════════════════════════════════════════════════${NC}"
|