secubox-deb/image/build-live-usb.sh
CyberMind-FR 6476897873 refactor: decommission secubox-authelia (SSO IdP) — remove package, keep gate authelia-free
Authelia (secubox-authelia, #239) was a failed/half-baked SSO PoC. Its partial
teardown also masked then unmasked the login-loop bug (hub-proxied /auth/login
never recorded sessions). Removing it for good:

- delete packages/secubox-authelia (package, service, nginx confs, LXC installer)
- move the durable pieces into secubox-hub (always installed):
  - nginx/secubox-lan-geo.conf  → /etc/nginx/conf.d/        (defines $lan_client)
  - nginx/zz-sbx-authgate.conf  → secubox.d/ + secubox-routes.d/
    keeps /__sbx_auth_verify + @sbx_auth_login defined WITHOUT Authelia:
    LAN clients pass (204), everything else denied (403 / default-deny).
    SSO-gated vhosts (lyrion, yacy, grafana, rustdesk, fmrelay) thus become
    LAN-only instead of losing their gate.
- drop secubox-authelia from image/build-live-usb.sh module list

No hard Depends referenced authelia (only description prose in
users/nextcloud/identity), so nothing else breaks. Live: purged on gk2 + c3box,
gate stubbed, lan-geo restored; verified lyrion/yacy/metablogizer still serve.
2026-07-02 08:14:08 +02:00

3818 lines
146 KiB
Bash
Executable File

#!/usr/bin/env bash
# ══════════════════════════════════════════════════════════════════
# SecuBox-DEB — build-live-usb.sh v2.0
# Build a bootable live USB image for amd64 with:
# - UEFI + Legacy BIOS hybrid boot
# - All SecuBox packages slipstreamed
# - Root autologin
# - Optional GUI kiosk mode
# - Network auto-detection
# ══════════════════════════════════════════════════════════════════
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
REPO_DIR="$(dirname "$SCRIPT_DIR")"
# ── Version & Build Info ──────────────────────────────────────────
SECUBOX_VERSION="1.10.0"
BUILD_TIMESTAMP=$(date '+%Y-%m-%d %H:%M')
BUILD_DATE=$(date '+%Y%m%d')
# ── Defaults ──────────────────────────────────────────────────────
SUITE="bookworm"
IMG_SIZE="8G"
OUT_DIR="${REPO_DIR}/output"
APT_MIRROR="http://deb.debian.org/debian"
APT_SECUBOX="https://apt.secubox.in"
USE_LOCAL_CACHE=0
SLIPSTREAM_DEBS=1
INCLUDE_PERSISTENCE=1
INCLUDE_KIOSK=1
PRESEED_FILE=""
NO_COMPRESS=0
EMBED_IMAGE=""
OVERLAY_MODE=0
# Static IP overrides (empty = DHCP bootstrap + secubox-net-detect, as before)
STATIC_IP=""
STATIC_GATEWAY=""
STATIC_DNS=""
# Default to the modern predictable-name wildcard (enp*, eno*, ens*, enx*).
# Literal "eth0" is the exception on Debian + systemd-udev — almost no
# modern hardware enumerates the primary NIC as eth0. See #128.
STATIC_IFACE="en*"
STATIC_HOSTNAME=""
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}[live-usb]${NC} $*"; }
ok() { echo -e "${GREEN}[ OK ]${NC} $*"; }
err() { echo -e "${RED}[FAIL ]${NC} $*" >&2; exit 1; }
warn() { echo -e "${GOLD}[ WARN ]${NC} $*"; }
# ── Parse args ────────────────────────────────────────────────────
usage() {
cat <<EOF
Usage: sudo bash build-live-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
--no-kiosk Disable GUI kiosk mode (enabled by default)
--no-persistence Don't include persistent storage partition
--no-compress Skip gzip compression (faster, for local testing)
--slipstream Include .deb packages from output/debs/ (default: enabled)
--no-slipstream Don't include local .deb packages
--preseed FILE Include preseed config archive
--embed-image IMG Embed image for disk flashing (creates installer USB)
--overlay Use advanced overlay partition layout (v1.6.7.2+)
--static-ip CIDR Fixed IP in CIDR form (e.g. 192.168.1.50/24). Disables
secubox-net-detect; writes a static netplan instead.
--gateway IP Default gateway (required when --static-ip is set)
--dns LIST DNS servers, comma-separated (default: gateway)
--iface NAME Netplan match name for the static iface. Supports
shell-style globs (default: en* — matches enp*,
eno*, ens*, enx*). Use a literal name only when
you know systemd-udev hasn't renamed the NIC.
--hostname NAME Hostname baked into the image (default: secubox-live)
--help Show this help
Features:
- UEFI + Legacy BIOS boot
- All SecuBox packages pre-installed
- Root autologin on console
- Network auto-detection at first boot
- GUI kiosk mode included by default (--no-kiosk to disable)
- Disk flasher tool for installing to internal storage
Output:
secubox-live-amd64-bookworm.img - Raw bootable image
secubox-live-amd64-bookworm.img.gz - Compressed image
Flash to USB:
zcat output/secubox-live-amd64-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 ;;
--kiosk) INCLUDE_KIOSK=1; shift ;;
--no-kiosk) INCLUDE_KIOSK=0; shift ;;
--no-persistence) INCLUDE_PERSISTENCE=0; shift ;;
--no-compress) NO_COMPRESS=1; shift ;;
--slipstream) SLIPSTREAM_DEBS=1; shift ;;
--no-slipstream) SLIPSTREAM_DEBS=0; shift ;;
--preseed) PRESEED_FILE="$2"; shift 2 ;;
--embed-image) EMBED_IMAGE="$2"; shift 2 ;;
--overlay) OVERLAY_MODE=1; shift ;;
--static-ip) STATIC_IP="$2"; shift 2 ;;
--gateway) STATIC_GATEWAY="$2"; shift 2 ;;
--dns) STATIC_DNS="$2"; shift 2 ;;
--iface) STATIC_IFACE="$2"; shift 2 ;;
--hostname) STATIC_HOSTNAME="$2"; shift 2 ;;
--help|-h) usage ;;
*) err "Unknown argument: $1" ;;
esac
done
# Validate static-IP block
if [[ -n "$STATIC_IP" ]]; then
[[ "$STATIC_IP" =~ ^[0-9.]+/[0-9]+$ ]] || err "--static-ip must be in CIDR form (e.g. 192.168.1.50/24)"
[[ -n "$STATIC_GATEWAY" ]] || err "--gateway is required when --static-ip is set"
[[ -n "$STATIC_DNS" ]] || STATIC_DNS="$STATIC_GATEWAY"
fi
# ── Checks ────────────────────────────────────────────────────────
[[ $EUID -ne 0 ]] && err "This script must be run as root (sudo)"
# Sanity: host /dev/null must be a character device. If a prior build was
# killed mid-flight, the chroot's bind-mounted /dev could have left a
# regular file in its place — and the rest of this script can't survive.
if [[ ! -c /dev/null ]]; then
err "host /dev/null is not a character device — recover with: sudo rm -f /dev/null && sudo mknod -m 666 /dev/null c 1 3"
fi
# Required tools
log "Checking dependencies..."
apt-get install -y -qq debootstrap squashfs-tools grub-efi-amd64-bin grub-pc-bin \
xorriso mtools dosfstools parted e2fsprogs live-boot 2>/dev/null || true
for cmd in debootstrap parted mkfs.fat mkfs.ext4 mksquashfs grub-mkimage; do
command -v "$cmd" >/dev/null || err "Missing: $cmd"
done
# ── Local cache detection ─────────────────────────────────────
if [[ $USE_LOCAL_CACHE -eq 1 ]]; then
if curl -sf "http://127.0.0.1:3142" >/dev/null 2>&1; then
APT_MIRROR="http://127.0.0.1:3142/deb.debian.org/debian"
log "Using apt-cacher-ng"
fi
if curl -sf "http://127.0.0.1:8080/dists/${SUITE}/Release" >/dev/null 2>&1; then
APT_SECUBOX="http://127.0.0.1:8080"
log "Using local SecuBox repo"
fi
fi
# ── Setup ─────────────────────────────────────────────────────────
mkdir -p "$OUT_DIR"
WORK_DIR=$(mktemp -d /tmp/secubox-live-XXXXXX)
ROOTFS="${WORK_DIR}/rootfs"
LIVE_DIR="${WORK_DIR}/live"
IMG_FILE="${OUT_DIR}/secubox-live-amd64-${SUITE}.img"
log "══════════════════════════════════════════════════════════"
log "Building SecuBox Live USB (amd64)"
log "Suite : ${SUITE}"
log "Image : ${IMG_FILE}"
log "Size : ${IMG_SIZE}"
log "Work dir : ${WORK_DIR}"
log "Kiosk : $([[ $INCLUDE_KIOSK -eq 1 ]] && echo "yes" || echo "no")"
log "══════════════════════════════════════════════════════════"
cleanup() {
log "Cleaning up..."
# Unmount in reverse order of mounting
umount -lf "${ROOTFS}/dev/pts" 2>/dev/null || true
umount -lf "${ROOTFS}/proc" 2>/dev/null || true
umount -lf "${ROOTFS}/sys" 2>/dev/null || true
# /dev is now a tmpfs (not a bind of the host's /dev) so its teardown
# is harmless. Still wait for in-flight writers before unmounting.
sync
sleep 1
if mountpoint -q "${ROOTFS}/dev" 2>/dev/null; then
umount -f "${ROOTFS}/dev" 2>/dev/null || umount -lf "${ROOTFS}/dev" 2>/dev/null || true
fi
umount -lf "${WORK_DIR}/mnt/"* 2>/dev/null || true
[[ -n "${LOOP:-}" ]] && losetup -d "${LOOP}" 2>/dev/null || true
# Final sanity: if the host's /dev/null got clobbered despite isolation,
# recreate the device node before exit so the caller's shell still works.
if [[ ! -c /dev/null ]]; then
log "WARNING: host /dev/null is not a char device — restoring"
rm -f /dev/null 2>/dev/null || true
mknod -m 666 /dev/null c 1 3 2>/dev/null || true
chown root:root /dev/null 2>/dev/null || true
fi
# Only remove if no mounts are active
if ! mount | grep -q "${WORK_DIR}"; then
rm -rf "${WORK_DIR}" 2>/dev/null || true
else
log "WARNING: ${WORK_DIR} still has active mounts, not removing"
fi
}
trap cleanup EXIT INT TERM
# Mount an isolated tmpfs /dev on the chroot and populate minimal device
# nodes the chroot needs. ROOTFS/dev never bind-mounts the host's /dev so
# that interrupted apt/debootstrap postinst scripts can't damage host
# device nodes (notably /dev/null). Idempotent.
mount_chroot_dev() {
local root="$1"
if mountpoint -q "${root}/dev"; then
return 0
fi
mkdir -p "${root}/dev"
mount -t tmpfs -o mode=0755,nosuid tmpfs "${root}/dev"
mknod -m 666 "${root}/dev/null" c 1 3
mknod -m 666 "${root}/dev/zero" c 1 5
mknod -m 666 "${root}/dev/full" c 1 7
mknod -m 666 "${root}/dev/random" c 1 8
mknod -m 666 "${root}/dev/urandom" c 1 9
mknod -m 666 "${root}/dev/tty" c 5 0
mknod -m 600 "${root}/dev/console" c 5 1
mknod -m 666 "${root}/dev/ptmx" c 5 2
ln -sf /proc/self/fd "${root}/dev/fd"
ln -sf /proc/self/fd/0 "${root}/dev/stdin"
ln -sf /proc/self/fd/1 "${root}/dev/stdout"
ln -sf /proc/self/fd/2 "${root}/dev/stderr"
mkdir -p "${root}/dev/pts" "${root}/dev/shm"
mount -t devpts -o newinstance,ptmxmode=0666,mode=0620 devpts "${root}/dev/pts"
}
# ══════════════════════════════════════════════════════════════════
# Step 1: Debootstrap
# ══════════════════════════════════════════════════════════════════
log "1/8 Debootstrap ${SUITE} amd64..."
mkdir -p "${ROOTFS}"
# Core system packages
INCLUDE_PKGS="systemd,systemd-sysv,dbus,netplan.io,nftables,openssh-server,locales"
INCLUDE_PKGS+=",python3,python3-pip,nginx,curl,wget,ca-certificates,gnupg,console-setup"
INCLUDE_PKGS+=",iproute2,iputils-ping,iputils-arping,ethtool,net-tools,wireguard-tools,isc-dhcp-client"
INCLUDE_PKGS+=",sudo,less,vim-tiny,logrotate,cron,rsync,jq,dnsmasq"
INCLUDE_PKGS+=",linux-image-amd64,live-boot,live-boot-initramfs-tools,live-config,live-config-systemd"
INCLUDE_PKGS+=",grub-efi-amd64,grub-pc-bin,efibootmgr,pciutils,usbutils,parted,dosfstools,lsb-release"
INCLUDE_PKGS+=",plymouth,plymouth-themes"
INCLUDE_PKGS+=",fonts-terminus,kbd"
# Python dependencies for SecuBox modules (apt packages)
# Note: Complex Python packages (cryptography, zmq, jose) moved to post-debootstrap
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-toml,python3-netifaces"
# Network and security tools (note: iputils-arping already included above)
INCLUDE_PKGS+=",bridge-utils,traceroute,dnsutils,whois,mtr-tiny,nmap"
INCLUDE_PKGS+=",avahi-daemon,avahi-utils,ieee-data,procps,openssl"
INCLUDE_PKGS+=",fonts-noto-color-emoji,haproxy,qrencode"
debootstrap --arch=amd64 --include="${INCLUDE_PKGS}" \
"${SUITE}" "${ROOTFS}" "${APT_MIRROR}"
ok "Debootstrap complete"
# ══════════════════════════════════════════════════════════════════
# Step 2: Base configuration
# ══════════════════════════════════════════════════════════════════
log "2/8 System configuration..."
# Only mount if not already mounted
mountpoint -q "${ROOTFS}/proc" || mount -t proc proc "${ROOTFS}/proc"
mountpoint -q "${ROOTFS}/sys" || mount -t sysfs sysfs "${ROOTFS}/sys"
mount_chroot_dev "${ROOTFS}"
# Hostname (override via --hostname)
EFFECTIVE_HOSTNAME="${STATIC_HOSTNAME:-secubox-live}"
echo "${EFFECTIVE_HOSTNAME}" > "${ROOTFS}/etc/hostname"
cat > "${ROOTFS}/etc/hosts" <<EOF
127.0.0.1 localhost ${EFFECTIVE_HOSTNAME} secubox secubox.local
::1 localhost ip6-localhost ip6-loopback
EOF
# Root password: secubox
chroot "${ROOTFS}" bash -c 'echo "root:secubox" | chpasswd'
# Timezone
echo "Europe/Paris" > "${ROOTFS}/etc/timezone"
chroot "${ROOTFS}" dpkg-reconfigure -f noninteractive tzdata 2>/dev/null || true
# Locale
chroot "${ROOTFS}" bash -c "locale-gen en_US.UTF-8 fr_FR.UTF-8 || true"
echo 'LANG=fr_FR.UTF-8' > "${ROOTFS}/etc/default/locale"
# French keyboard
cat > "${ROOTFS}/etc/default/keyboard" <<EOF
XKBMODEL="pc105"
XKBLAYOUT="fr"
XKBVARIANT="latin9"
EOF
echo 'KEYMAP=fr' > "${ROOTFS}/etc/vconsole.conf"
# Console font with UTF-8 box-drawing character support (Terminus)
mkdir -p "${ROOTFS}/etc/console-setup"
cat > "${ROOTFS}/etc/default/console-setup" <<EOF
ACTIVE_CONSOLES="/dev/tty[1-6]"
CHARMAP="UTF-8"
CODESET="Uni2"
FONTFACE="Terminus"
FONTSIZE="16"
EOF
# Also set vconsole for systemd
cat > "${ROOTFS}/etc/vconsole.conf" <<EOF
KEYMAP=fr
FONT=ter-v16n
FONT_MAP=
EOF
# Enable SSH root login with password
sed -i 's/#PermitRootLogin.*/PermitRootLogin yes/' "${ROOTFS}/etc/ssh/sshd_config"
sed -i 's/#PasswordAuthentication.*/PasswordAuthentication yes/' "${ROOTFS}/etc/ssh/sshd_config"
sed -i 's/PasswordAuthentication no/PasswordAuthentication yes/' "${ROOTFS}/etc/ssh/sshd_config"
# ── Autologin root on tty1 ────────────────────────────────────────
# Simple autologin - DO NOT use Type=idle (blocks until all jobs done = no input)
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 linux
StandardInput=tty
StandardOutput=tty
TTYVTDisallocate=no
EOF
# Disable login timeout (default 60s breaks autologin sessions)
sed -i 's/^LOGIN_TIMEOUT.*/LOGIN_TIMEOUT 0/' "${ROOTFS}/etc/login.defs"
# Also ensure no TMOUT in profile
echo 'unset TMOUT' >> "${ROOTFS}/etc/profile.d/secubox.sh"
# Enable gettys - tty1 (autologin), tty2-3 (backup login prompts)
chroot "${ROOTFS}" systemctl enable getty@tty1.service 2>/dev/null || true
chroot "${ROOTFS}" systemctl enable getty@tty2.service 2>/dev/null || true
chroot "${ROOTFS}" systemctl enable getty@tty3.service 2>/dev/null || true
chroot "${ROOTFS}" systemctl set-default multi-user.target 2>/dev/null || true
# Disable live-config autologin
mkdir -p "${ROOTFS}/etc/live/config.conf.d"
echo 'LIVE_CONFIG_NOAUTOLOGIN=true' > "${ROOTFS}/etc/live/config.conf.d/no-autologin.conf"
# Boot status (show for debugging)
mkdir -p "${ROOTFS}/etc/systemd/system.conf.d"
cat > "${ROOTFS}/etc/systemd/system.conf.d/boot.conf" <<EOF
[Manager]
ShowStatus=yes
DefaultTimeoutStartSec=30s
DefaultTimeoutStopSec=30s
EOF
# Disable console spam + enable SysRq for emergency recovery
cat > "${ROOTFS}/etc/sysctl.d/99-secubox.conf" <<EOF
kernel.consoleblank=0
net.ipv4.conf.all.log_martians=0
kernel.printk=1 1 1 1
kernel.sysrq=1
EOF
# ── Hardware Check Service ──────────────────────────────────────────
# Auto-check hardware and report at boot when secubox.hwcheck=1
cat > "${ROOTFS}/usr/local/bin/secubox-hwcheck" <<'HWCHECK'
#!/bin/bash
# SecuBox Hardware Check - Evaluates system hardware and reports status
REPORT_FILE="/var/log/secubox-hwcheck.log"
REPORT_CONSOLE="/dev/tty1"
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$REPORT_FILE"
[ -c "$REPORT_CONSOLE" ] && echo "$*" > "$REPORT_CONSOLE"
}
cyber_banner() {
# VT100 green phosphor style
echo -e "\033[32m"
cat << 'BANNER'
____ _____ ____ _ _ ____ _____ __
/ ___|| ____/ ___| | | | __ ) / _ \ \/ /
\___ \| _|| | | | | | _ \| | | \ /
___) | |__| |___| |_| | |_) | |_| / \
|____/|_____\____|\___/|____/ \___/_/\_\
================================================================
DEC PDP-11/70 COMPATIBLE - HARDWARE EVALUATION MODE
================================================================
BANNER
}
check_hw() {
clear
cyber_banner
sleep 1
log ">>>> SYSTEM HARDWARE SCAN INITIATED <<<<"
log "================================================================"
echo ""
# CPU
CPU_MODEL=$(grep -m1 "model name" /proc/cpuinfo | cut -d: -f2 | xargs)
CPU_CORES=$(grep -c processor /proc/cpuinfo)
log "CPU.......... $CPU_MODEL"
log " ($CPU_CORES PROCESSOR UNITS)"
# Memory
MEM_TOTAL=$(free -h | awk '/Mem:/ {print $2}')
MEM_AVAIL=$(free -h | awk '/Mem:/ {print $7}')
log "MEMORY....... $MEM_TOTAL TOTAL / $MEM_AVAIL AVAILABLE"
# Storage
log "STORAGE DEVICES:"
lsblk -d -o NAME,SIZE,TYPE,MODEL 2>/dev/null | grep -v "^NAME" | while read line; do
log " * $line"
done
# Network
log "NETWORK INTERFACES:"
for iface in /sys/class/net/*; do
name=$(basename "$iface")
[ "$name" = "lo" ] && continue
state=$(cat "$iface/operstate" 2>/dev/null || echo "unknown")
mac=$(cat "$iface/address" 2>/dev/null || echo "n/a")
if [ "$state" = "up" ]; then
log " [+] $name: $state ($mac)"
else
log " [-] $name: $state ($mac)"
fi
done
# Graphics
log "GRAPHICS ADAPTER:"
lspci 2>/dev/null | grep -iE "vga|3d|display" | while read line; do
log " * $line"
done
# Boot mode
if [ -d /sys/firmware/efi ]; then
log "BOOT MODE.... UEFI"
else
log "BOOT MODE.... BIOS/LEGACY"
fi
# Virtualization detection
VIRT=$(systemd-detect-virt 2>/dev/null || echo "none")
if [ "$VIRT" = "none" ]; then
log "PLATFORM..... BARE METAL"
else
log "PLATFORM..... VIRTUAL ($VIRT)"
fi
echo ""
log "================================================================"
log ">>>> HARDWARE CHECK COMPLETE - ALL SYSTEMS NOMINAL <<<<"
log ">>>> SECUBOX CYBER DEFENSE PLATFORM READY <<<<"
log "================================================================"
echo ""
}
# Cyber boot splash with status - VT100 style
cyber_splash() {
clear
echo -e "\033[32m"
cyber_banner
local steps=(
"INITIALIZING SECURE ENVIRONMENT.......... OK"
"LOADING CRYPTOGRAPHIC MODULES............ OK"
"CONFIGURING NETWORK STACK................ OK"
"ACTIVATING FIREWALL RULES................ OK"
"SCANNING FOR THREATS..................... CLEAR"
"HARDENING SYSTEM......................... OK"
"OPTIMIZING PERFORMANCE................... OK"
)
for step in "${steps[@]}"; do
echo -e " > $step"
sleep 0.3
done
echo ""
echo " >>>> SECURE BOOT SEQUENCE COMPLETE <<<<"
echo ""
sleep 1
}
# Check if hwcheck requested via kernel cmdline
if grep -q "secubox.hwcheck=1" /proc/cmdline; then
cyber_splash
check_hw
fi
HWCHECK
chmod +x "${ROOTFS}/usr/local/bin/secubox-hwcheck"
# Create retro CRT VT100 DEC PDP-style boot splash
cat > "${ROOTFS}/usr/local/bin/secubox-splash" <<'SPLASH'
#!/bin/bash
# SecuBox Cyber Boot Splash - VT100/DEC PDP Style
# VT100 escape codes
ESC="\033"
GREEN="${ESC}[32m"
BRIGHT="${ESC}[1m"
DIM="${ESC}[2m"
BLINK="${ESC}[5m"
RESET="${ESC}[0m"
CLEAR="${ESC}[2J${ESC}[H"
# Clear screen and set green phosphor look
echo -ne "$CLEAR$GREEN"
# Simulated boot delay
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 " DEC PDP-11/70 COMPATIBLE SECURITY TERMINAL"
echo " SECUBOX CYBER DEFENSE SYSTEM v1.3"
echo -e "${BRIGHT}================================================================${RESET}${GREEN}"
echo ""
type_slow "BOOT SEQUENCE INITIATED..."
echo ""
# Boot status messages
steps=(
"MEMORY TEST.................... 4096K OK"
"LOADING KERNEL................. DONE"
"CRYPTOGRAPHIC MODULES.......... LOADED"
"NETWORK STACK.................. INITIALIZED"
"FIREWALL RULES................. ACTIVE"
"INTRUSION DETECTION............ ARMED"
"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 " | UNAUTHORIZED ACCESS WILL BE PROSECUTED |"
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"
# Add splash to root's bashrc for login
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
# ── Profile.d script for login status display ──────────────────────────────────
mkdir -p "${ROOTFS}/etc/profile.d"
cat > "${ROOTFS}/etc/profile.d/secubox-login.sh" <<'PROFILE'
#!/bin/bash
# SecuBox Login Status Display
# Shows quick system status on interactive shell login
# Only run on interactive terminals
[[ $- != *i* ]] && return
[[ -z "$PS1" ]] && return
# Avoid double display with splash
[[ -n "$SECUBOX_STATUS_SHOWN" ]] && return
export SECUBOX_STATUS_SHOWN=1
# Colors
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'
# Quick status line after MOTD
show_quick_status() {
local ip=$(hostname -I 2>/dev/null | awk '{print $1}')
local services_ok=0
local services_fail=0
for svc in nginx secubox-api nftables; do
if systemctl is-active --quiet "$svc" 2>/dev/null; then
((services_ok++))
else
((services_fail++))
fi
done
# Mode indicator
local mode_icon="🖥️"
local mode_text="Console"
if [[ -f /var/lib/secubox/.kiosk-enabled ]]; then
mode_icon="🖼️"
mode_text="Kiosk"
elif [[ -f /var/lib/secubox/.tui-enabled ]]; then
mode_icon="📟"
mode_text="TUI"
fi
echo ""
echo -e "${GRAY}────────────────────────────────────────────────────────────${RESET}"
echo -e " ${mode_icon} ${CYAN}${mode_text}${RESET} │ ${GREEN}●${RESET} ${services_ok} services │ 🌐 ${CYAN}${ip:-no-ip}${RESET}"
echo -e "${GRAY}────────────────────────────────────────────────────────────${RESET}"
echo -e " ${GRAY}Type ${WHITE}secubox-status${GRAY} for details • ${WHITE}secubox-help${GRAY} for commands${RESET}"
echo ""
}
# Only show on tty login, not in SSH or within screen/tmux
if [[ -z "$SSH_TTY" ]] && [[ -z "$TMUX" ]] && [[ -z "$STY" ]]; then
show_quick_status
fi
PROFILE
chmod +x "${ROOTFS}/etc/profile.d/secubox-login.sh"
# ── SecuBox help command ─────────────────────────────────────────────────────
cat > "${ROOTFS}/usr/bin/secubox-help" <<'HELP_CMD'
#!/bin/bash
# SecuBox Quick Help
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 -e " ${CYAN}secubox-logs${GRAY} Live security logs${RESET}"
echo -e " ${CYAN}secubox-services${GRAY} Manage services${RESET}"
echo ""
echo -e " ${WHITE}Network${RESET}"
echo -e " ${CYAN}secubox-network${GRAY} Network configuration${RESET}"
echo -e " ${CYAN}secubox-firewall${GRAY} Firewall rules status${RESET}"
echo ""
echo -e " ${WHITE}Security${RESET}"
echo -e " ${CYAN}secubox-threats${GRAY} View blocked threats${RESET}"
echo -e " ${CYAN}secubox-waf-status${GRAY} WAF inspection status${RESET}"
echo ""
echo -e " ${WHITE}Modes${RESET}"
echo -e " ${CYAN}secubox-mode kiosk${GRAY} Switch to Kiosk GUI${RESET}"
echo -e " ${CYAN}secubox-mode tui${GRAY} Switch to TUI dashboard${RESET}"
echo -e " ${CYAN}secubox-mode console${GRAY} Switch to shell console${RESET}"
echo ""
echo -e " ${GRAY}Web UI: https://$(hostname -I 2>/dev/null | awk '{print $1}' || echo 'localhost'):9443${RESET}"
echo ""
HELP_CMD
chmod +x "${ROOTFS}/usr/bin/secubox-help"
# ── SecuBox logs command ─────────────────────────────────────────────────────
cat > "${ROOTFS}/usr/bin/secubox-logs" <<'LOGS_CMD'
#!/bin/bash
# SecuBox Live Security Logs
echo "📋 SecuBox Security Logs (Ctrl+C to exit)"
echo "─────────────────────────────────────────"
journalctl -f -u 'secubox-*' -u crowdsec -u suricata -u nginx --no-pager 2>/dev/null || \
journalctl -f --no-pager
LOGS_CMD
chmod +x "${ROOTFS}/usr/bin/secubox-logs"
# Systemd service for hardware check
cat > "${ROOTFS}/etc/systemd/system/secubox-hwcheck.service" <<'HWSVC'
[Unit]
Description=SecuBox Hardware Check
After=local-fs.target
Before=getty@tty1.service
ConditionKernelCommandLine=secubox.hwcheck=1
[Service]
Type=oneshot
ExecStart=/usr/local/bin/secubox-hwcheck
RemainAfterExit=yes
StandardOutput=journal+console
[Install]
WantedBy=multi-user.target
HWSVC
chroot "${ROOTFS}" systemctl enable secubox-hwcheck.service 2>/dev/null || true
# ── Network config ─────────────────────────────────────────────────
mkdir -p "${ROOTFS}/etc/netplan"
# Static IP branch — emit a fixed config and short-circuit secubox-net-detect.
if [[ -n "$STATIC_IP" ]]; then
log "Writing static netplan: ${STATIC_IP} gw=${STATIC_GATEWAY} dns=${STATIC_DNS} iface=${STATIC_IFACE}"
# Build nameservers YAML list
STATIC_DNS_YAML=""
IFS=',' read -ra _dns_arr <<< "$STATIC_DNS"
for d in "${_dns_arr[@]}"; do
STATIC_DNS_YAML+=" - $d"$'\n'
done
cat > "${ROOTFS}/etc/netplan/00-secubox.yaml" <<NETPLAN_STATIC
# /etc/netplan/00-secubox.yaml — SecuBox Live USB (Bare Metal) — Static IP
# Generated by build-live-usb.sh --static-ip ${STATIC_IP} --gateway ${STATIC_GATEWAY}
#
# secubox-net-detect is short-circuited via /var/lib/secubox/.net-configured
# at image-build time, so this file is not rewritten at first boot.
network:
version: 2
renderer: networkd
ethernets:
static-iface:
match:
name: "${STATIC_IFACE}"
dhcp4: false
dhcp6: false
addresses:
- ${STATIC_IP}
routes:
- to: default
via: ${STATIC_GATEWAY}
nameservers:
addresses:
${STATIC_DNS_YAML%$'\n'}
optional: false
NETPLAN_STATIC
chmod 600 "${ROOTFS}/etc/netplan/00-secubox.yaml"
# Mark the network as already configured so firstboot.sh skips secubox-net-detect.
mkdir -p "${ROOTFS}/var/lib/secubox"
touch "${ROOTFS}/var/lib/secubox/.net-configured"
ok "Static netplan written; secubox-net-detect disabled for this image"
else
# Main netplan config - Bootstrap for real AMD64 hardware
# Uses two separate match patterns to avoid conflicts when multiple interfaces match
# secubox-net-detect.service will generate proper config at first boot
cat > "${ROOTFS}/etc/netplan/00-secubox.yaml" <<'NETPLAN'
# /etc/netplan/00-secubox.yaml — SecuBox Live USB bootstrap.
#
# DHCP on every ethernet interface. That's it. Operators wire WiFi
# and router-mode br-lan later via secubox-net-* tools.
#
# Earlier versions baked an empty `br-lan` bridge with a static
# 192.168.10.1/24 address into the bootstrap. On bare-metal real
# hardware the physical NIC went silent and only the phantom br-lan
# showed an IP — networkd was honouring the static bridge but
# something (predictable rename? secubox-net-detect leftover?)
# stopped the DHCP request reaching the real cable. Stripping the
# bridge + the wifi-with-empty-SSID block restores classic DHCP.
# secubox-net-detect.service (disabled by default, no .wants/ link)
# can still be run by hand once the operator has decided whether
# this box is a router vs an endpoint.
network:
version: 2
renderer: networkd
ethernets:
# Match everything that *looks* like ethernet — covers enpXsY /
# enoX / ensX / enxAABBCC (USB ethernet) / ethX. All get DHCP.
eth-all:
match:
name: "e*"
dhcp4: true
dhcp6: false
dhcp4-overrides:
use-dns: true
use-routes: true
optional: true
NETPLAN
chmod 600 "${ROOTFS}/etc/netplan/00-secubox.yaml"
fi # end static-IP branch
# Fallback network script - runs after boot, retries DHCP and auto-discovers LAN
cat > "${ROOTFS}/usr/sbin/secubox-net-fallback" <<'FALLBACK'
#!/bin/bash
# SecuBox Network Fallback - retries DHCP, auto-discovers LAN subnet as fallback
logger -t secubox-net "Network fallback starting..."
# Common gateway IPs to probe (most common first, including .254 variants)
GATEWAYS="192.168.1.1 192.168.1.254 192.168.0.1 192.168.0.254 192.168.2.1 10.0.0.1 10.0.0.254"
GATEWAYS+=" 10.0.1.1 10.1.1.1 172.16.0.1 172.16.1.1 192.168.10.1 192.168.100.1"
# Fallback IP suffix to use (high number to avoid conflicts)
FALLBACK_SUFFIX="250"
# Try to discover gateway using ARP scan (if arping available)
arp_discover() {
local iface="$1"
command -v arping &>/dev/null || return 1
# Scan common subnets for any responding device
for subnet in "192.168.1" "192.168.0" "10.0.0" "172.16.0"; do
# Quick arping to .1 and .254
for gw in "${subnet}.1" "${subnet}.254"; do
if arping -c 1 -w 1 -I "$iface" "$gw" &>/dev/null; then
echo "$gw"
return 0
fi
done
done
return 1
}
# Discover LAN subnet by probing common gateways
discover_lan() {
local iface="$1"
# First try ARP discovery (faster and more reliable)
local arp_gw
arp_gw=$(arp_discover "$iface" 2>/dev/null)
# Verify ARP result is not a local IP (avoid self-discovery)
if [[ -n "$arp_gw" ]] && ! ip addr show 2>/dev/null | grep -q "inet ${arp_gw}/"; then
local subnet_base="${arp_gw%.*}"
local test_ip="${subnet_base}.${FALLBACK_SUFFIX}"
ip addr add "${test_ip}/24" dev "$iface" 2>/dev/null
ip route add default via "$arp_gw" dev "$iface" 2>/dev/null || true
echo "nameserver $arp_gw" > /etc/resolv.conf
echo "nameserver 8.8.8.8" >> /etc/resolv.conf
logger -t secubox-net "ARP discovered gateway $arp_gw, configured ${test_ip}/24"
return 0
fi
# Fall back to gateway probing
for gw in $GATEWAYS; do
# Skip if this IP is already assigned to any local interface (avoid self-ping)
if ip addr show 2>/dev/null | grep -q "inet ${gw}/"; then
logger -t secubox-net "Skipping $gw - it's a local IP"
continue
fi
# Extract subnet base (e.g., 192.168.1)
local subnet_base="${gw%.*}"
local test_ip="${subnet_base}.${FALLBACK_SUFFIX}"
# Temporarily add IP to probe the network
ip addr add "${test_ip}/24" dev "$iface" 2>/dev/null
# Try to ping gateway (1 packet, 1 second timeout)
if ping -c 1 -W 1 -I "$iface" "$gw" &>/dev/null; then
logger -t secubox-net "Discovered gateway $gw on $iface"
# Add default route
ip route add default via "$gw" dev "$iface" 2>/dev/null || true
# Add DNS (use gateway as DNS, fallback to public)
echo "nameserver $gw" > /etc/resolv.conf
echo "nameserver 8.8.8.8" >> /etc/resolv.conf
echo "nameserver 1.1.1.1" >> /etc/resolv.conf
logger -t secubox-net "Auto-configured $iface: ${test_ip}/24 via $gw"
return 0
fi
# Remove test IP if gateway not found
ip addr del "${test_ip}/24" dev "$iface" 2>/dev/null
done
return 1
}
# Wait for networkd to fully initialize
sleep 10
# Track if any interface got a real IP
GOT_IP=0
# Find all physical network interfaces (exclude lo, docker, veth, dummy, etc)
for iface in /sys/class/net/*; do
[ -e "$iface" ] || continue
IFACE=$(basename "$iface")
# Skip non-physical interfaces
case "$IFACE" in
lo|dummy*|docker*|veth*|br-*|virbr*) continue ;;
esac
# Only process interfaces starting with e (ethernet) or w (wifi)
case "$IFACE" in
e*|w*) ;;
*) continue ;;
esac
# Bring interface up
ip link set "$IFACE" up 2>/dev/null
# Check if interface has a valid IP (not link-local 169.254)
CURRENT_IP=$(ip -4 addr show "$IFACE" 2>/dev/null | grep -oP 'inet \K[0-9.]+' | grep -v '^169\.254\.' | head -1)
if [[ -n "$CURRENT_IP" ]]; then
logger -t secubox-net "Interface $IFACE has IP $CURRENT_IP"
GOT_IP=1
continue
fi
logger -t secubox-net "No valid IP on $IFACE, requesting DHCP..."
# Try dhclient for more reliable DHCP (if available)
if command -v dhclient &>/dev/null; then
timeout 30 dhclient -1 -v "$IFACE" 2>&1 | logger -t secubox-net || true
else
# Fallback to networkctl
networkctl reconfigure "$IFACE" 2>/dev/null || true
sleep 15
fi
# Check if DHCP succeeded
CURRENT_IP=$(ip -4 addr show "$IFACE" 2>/dev/null | grep -oP 'inet \K[0-9.]+' | grep -v '^169\.254\.' | head -1)
if [[ -n "$CURRENT_IP" ]]; then
logger -t secubox-net "DHCP succeeded on $IFACE: $CURRENT_IP"
GOT_IP=1
continue
fi
# DHCP failed - try auto-discovery on wired interfaces only
if [[ "$IFACE" == e* ]]; then
logger -t secubox-net "DHCP failed on $IFACE, trying LAN auto-discovery..."
if discover_lan "$IFACE"; then
logger -t secubox-net "LAN auto-discovery succeeded on $IFACE"
GOT_IP=1
fi
fi
done
# If no interface got DHCP, try gateway discovery (smart auto-IP)
if [[ "$GOT_IP" -eq 0 ]]; then
FIRST_IFACE=$(ls -1 /sys/class/net/ | grep -E '^e' | head -1)
if [[ -n "$FIRST_IFACE" ]]; then
logger -t secubox-net "No DHCP anywhere, trying gateway discovery on $FIRST_IFACE..."
# Try to discover LAN by probing common gateways
if discover_lan "$FIRST_IFACE"; then
logger -t secubox-net "Gateway discovery successful on $FIRST_IFACE"
GOT_IP=1
else
logger -t secubox-net "Gateway discovery failed, assigning link-local to $FIRST_IFACE"
ip addr add 169.254.1.1/16 dev "$FIRST_IFACE" 2>/dev/null || true
fi
fi
fi
logger -t secubox-net "Network fallback complete"
FALLBACK
chmod +x "${ROOTFS}/usr/sbin/secubox-net-fallback"
# Systemd service for fallback - runs late to give DHCP time
cat > "${ROOTFS}/etc/systemd/system/secubox-net-fallback.service" <<EOF
[Unit]
Description=SecuBox Network Fallback
After=network-online.target systemd-networkd.service
Wants=network-online.target
# Give networkd 30 seconds to complete DHCP before running fallback
ConditionPathExists=!/var/lib/secubox/.network-ok
[Service]
Type=oneshot
# Initial delay to let DHCP complete
ExecStartPre=/bin/sleep 15
ExecStart=/usr/sbin/secubox-net-fallback
RemainAfterExit=yes
TimeoutStartSec=120
[Install]
WantedBy=multi-user.target
EOF
chroot "${ROOTFS}" systemctl enable systemd-networkd.service 2>/dev/null || true
chroot "${ROOTFS}" systemctl enable secubox-net-fallback.service 2>/dev/null || true
chroot "${ROOTFS}" systemctl disable systemd-networkd-wait-online.service 2>/dev/null || true
chroot "${ROOTFS}" systemctl mask systemd-networkd-wait-online.service 2>/dev/null || true
# ── Plymouth Boot Splash Themes ──────────────────────
log "Installing Plymouth boot splash themes..."
# Install secubox-simple theme (DEFAULT - most compatible)
SIMPLE_DIR="${ROOTFS}/usr/share/plymouth/themes/secubox-simple"
mkdir -p "${SIMPLE_DIR}"
SIMPLE_SRC="${SCRIPT_DIR}/plymouth/secubox-simple"
if [[ -d "${SIMPLE_SRC}" ]]; then
cp "${SIMPLE_SRC}/secubox-simple.plymouth" "${SIMPLE_DIR}/"
cp "${SIMPLE_SRC}/secubox-simple.script" "${SIMPLE_DIR}/"
# Inject version dynamically
sed -i "s/v[0-9]\+\.[0-9]\+\.[0-9]\+\(\.[0-9]\+\)\?/v${SECUBOX_VERSION}/g" "${SIMPLE_DIR}/secubox-simple.script"
log " Copied simple theme from ${SIMPLE_SRC} (version: v${SECUBOX_VERSION})"
fi
# Install secubox-3d theme (advanced, optional)
PLYMOUTH_DIR="${ROOTFS}/usr/share/plymouth/themes/secubox-3d"
mkdir -p "${PLYMOUTH_DIR}"
DS_SRC="${SCRIPT_DIR}/plymouth/secubox-3d"
if [[ -d "${DS_SRC}" ]]; then
cp "${DS_SRC}/secubox-3d.plymouth" "${PLYMOUTH_DIR}/"
cp "${DS_SRC}/secubox-3d.script" "${PLYMOUTH_DIR}/"
cp "${DS_SRC}"/*.png "${PLYMOUTH_DIR}/" 2>/dev/null || true
log " Copied 3D theme from ${DS_SRC}"
fi
# Also install legacy cube theme as fallback
CUBE_DIR="${ROOTFS}/usr/share/plymouth/themes/secubox-cube"
mkdir -p "${CUBE_DIR}"
CUBE_SRC="${SCRIPT_DIR}/plymouth/secubox-cube"
if [[ -d "${CUBE_SRC}" ]]; then
cp "${CUBE_SRC}/secubox-cube.plymouth" "${CUBE_DIR}/"
cp "${CUBE_SRC}/secubox-cube.script" "${CUBE_DIR}/"
cp "${CUBE_SRC}/logo.png" "${CUBE_DIR}/"
cp "${CUBE_SRC}/scanlines.png" "${CUBE_DIR}/"
cp "${CUBE_SRC}/progress-bg.png" "${CUBE_DIR}/"
cp "${CUBE_SRC}/progress-fg.png" "${CUBE_DIR}/"
cp "${CUBE_SRC}"/icon-*.png "${CUBE_DIR}/"
log " Copied cube theme assets from ${CUBE_SRC}"
else
warn "Cube theme source not found at ${CUBE_SRC}, creating minimal theme..."
# Fallback: create minimal theme descriptor
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
# Minimal fallback script
cat > "${PLYMOUTH_DIR}/secubox-cube.script" <<'PLYSCRIPT'
/* SecuBox Cube - Minimal Fallback */
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_text = "SECUBOX";
banner_image = Image.Text(banner_text, 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);
subtitle_text = "CyberMind Security Platform";
subtitle_image = Image.Text(subtitle_text, 0.4, 0.6, 0.4, "Sans 16");
subtitle_sprite = Sprite(subtitle_image);
subtitle_sprite.SetPosition(center_x - subtitle_image.GetWidth() / 2, center_y + 20, 1);
fun boot_progress_callback(duration, progress) {
bar_width = 300;
fill = Math.Int(progress * bar_width);
bar_text = "";
for (i = 0; i < fill / 10; i++) bar_text = bar_text + "█";
for (i = fill / 10; i < bar_width / 10; 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);
message_sprite = Sprite();
fun message_callback(text) {
msg_image = Image.Text(text, 0.0, 0.7, 0.3, "Mono 12");
message_sprite.SetImage(msg_image);
message_sprite.SetPosition(center_x - msg_image.GetWidth() / 2, center_y + 120, 1);
}
Plymouth.SetMessageFunction(message_callback);
PLYSCRIPT
fi
# Set secubox-simple as default theme (most compatible)
mkdir -p "${ROOTFS}/etc/plymouth"
cat > "${ROOTFS}/etc/plymouth/plymouthd.conf" <<EOF
[Daemon]
Theme=secubox-simple
ShowDelay=0
DeviceTimeout=8
EOF
# Update alternatives to use our theme
chroot "${ROOTFS}" plymouth-set-default-theme secubox-simple 2>/dev/null || true
ok "Plymouth themes installed (default: secubox-simple)"
ok "Base configuration complete"
# ══════════════════════════════════════════════════════════════════
# Step 3: Firmware
# ══════════════════════════════════════════════════════════════════
log "3/8 Installing firmware..."
# Mount special filesystems for chroot (required for apt). Uses the
# isolated tmpfs /dev helper so the host's /dev is never bind-mounted.
# Cleanup is handled by the cleanup() function at script exit.
mount_chroot_fs() {
log "Mounting special filesystems in chroot..."
mount_chroot_dev "${ROOTFS}"
mountpoint -q "${ROOTFS}/proc" || mount -t proc proc "${ROOTFS}/proc"
mountpoint -q "${ROOTFS}/sys" || mount -t sysfs sysfs "${ROOTFS}/sys"
}
mount_chroot_fs
# Make EVERY dpkg op in the chroot keep existing conffiles and never prompt.
# secubox-mesh's mesh.toml is an auto-detected conffile; in the headless chroot
# its prompt aborts with "end of file on stdin at conffile prompt", failing the
# whole build. dpkg.cfg.d covers apt installs AND bare `dpkg --configure -a`.
install -d "${ROOTFS}/etc/dpkg/dpkg.cfg.d"
printf 'force-confold\nforce-confdef\n' > "${ROOTFS}/etc/dpkg/dpkg.cfg.d/90-secubox-confold"
# Deny service start/stop/reload during install — the chroot has no running
# init/dbus, so packages like dbus / the kiosk X11+chromium stack abort their
# postinst ("Failed to connect to system message bus", invoke-rc.d errors),
# which fails the whole build. Removed before squashfs so the real system
# boots services normally (systemd starts enabled units regardless).
cat > "${ROOTFS}/usr/sbin/policy-rc.d" <<'POLICY'
#!/bin/sh
exit 101
POLICY
chmod +x "${ROOTFS}/usr/sbin/policy-rc.d"
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
chroot "${ROOTFS}" apt-get install -y -q --no-install-recommends \
firmware-linux-free firmware-linux-nonfree firmware-misc-nonfree \
firmware-realtek firmware-iwlwifi firmware-atheros \
amd64-microcode intel-microcode 2>/dev/null || warn "Some firmware unavailable"
ok "Firmware installed"
# ── Install critical disk tools (for secubox-install) ─────────────
log "Installing disk tools..."
chroot "${ROOTFS}" apt-get install -y -q parted fdisk e2fsprogs dosfstools || warn "Disk tools install failed"
ok "Disk tools installed (parted, fdisk, e2fsprogs, dosfstools)"
# ── Install fake systemctl for chroot builds ───────────────────────
# Package postinst scripts call systemctl which fails in chroot.
# This wrapper silently succeeds for those calls during package install.
log "Installing chroot systemctl wrapper..."
cat > "${ROOTFS}/usr/local/sbin/systemctl-chroot" <<'FAKESYSTEMCTL'
#!/bin/bash
# Fake systemctl for chroot builds - always succeeds
# Real systemctl is at /bin/systemctl or /usr/bin/systemctl
exit 0
FAKESYSTEMCTL
chmod +x "${ROOTFS}/usr/local/sbin/systemctl-chroot"
# Divert real systemctl temporarily
if [[ -x "${ROOTFS}/bin/systemctl" ]]; then
mv "${ROOTFS}/bin/systemctl" "${ROOTFS}/bin/systemctl.real"
ln -sf /usr/local/sbin/systemctl-chroot "${ROOTFS}/bin/systemctl"
SYSTEMCTL_DIVERTED=1
else
SYSTEMCTL_DIVERTED=0
fi
# ══════════════════════════════════════════════════════════════════
# Step 4: SecuBox packages (slipstream ALL from cache/repo)
# ══════════════════════════════════════════════════════════════════
log "4/8 Installing ALL SecuBox packages..."
# Install Python dependencies FIRST (required by SecuBox packages)
log "Installing Python dependencies..."
chroot "${ROOTFS}" apt-get install -y -q python3-pip python3-venv 2>/dev/null || true
chroot "${ROOTFS}" pip3 install --break-system-packages -q \
'fastapi>=0.100' 'uvicorn[standard]>=0.25' 'pydantic[email]>=2.0' \
python-jose[cryptography] httpx jinja2 tomli toml pyroute2 psutil \
aiofiles aiosqlite authlib cryptography pillow zmq pyjwt \
python-multipart websockets netifaces email-validator textual \
2>&1 | tail -10 || true
ok "Python dependencies installed"
# Install heavy security services (not in debootstrap to keep initial download small)
log "Installing security services..."
# Add CrowdSec repository
chroot "${ROOTFS}" bash -c '
curl -s https://install.crowdsec.net | bash 2>/dev/null || true
' 2>/dev/null || warn "CrowdSec repo setup failed"
chroot "${ROOTFS}" apt-get update -q 2>/dev/null
# Install Python packages that fail during debootstrap
chroot "${ROOTFS}" bash -c "DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
python3-cryptography python3-jose python3-zmq 2>/dev/null" || warn "Some Python packages not installed"
chroot "${ROOTFS}" bash -c "DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
crowdsec glances netdata mosquitto coturn lxc debootstrap 2>/dev/null" || warn "Some services not installed"
ok "Security services installed"
# Create symlinks for uvicorn to ensure all service files can find it
# pip installs to /usr/local/bin/, but some services expect /usr/bin/
log "Creating uvicorn symlinks for service compatibility..."
chroot "${ROOTFS}" ln -sf /usr/local/bin/uvicorn /usr/bin/uvicorn 2>/dev/null || true
ok "uvicorn symlinks created"
# ── Pre-generate SSL certificates for nginx BEFORE installing packages ──
# (Package postinst scripts test nginx, which needs certs to exist)
# Generate certs on HOST (chroot may lack /dev/urandom) and copy them in
log "Pre-generating SSL certificates (needed for nginx test during package install)..."
mkdir -p "${ROOTFS}/etc/secubox/tls"
mkdir -p "${ROOTFS}/run/secubox"
mkdir -p "${ROOTFS}/var/lib/secubox"
# Generate on host system
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-live/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 - nginx may not start"
fi
# ══════════════════════════════════════════════════════════════════
# CRITICAL: Create secubox user BEFORE installing packages
# ══════════════════════════════════════════════════════════════════
# Package postinst scripts assume this user exists. Creating it here
# ensures it's available during package install and at boot time.
log "Creating secubox system user..."
if ! chroot "${ROOTFS}" id -u secubox >/dev/null 2>&1; then
chroot "${ROOTFS}" adduser --system --group --no-create-home \
--home /var/lib/secubox --shell /usr/sbin/nologin secubox
ok "Created secubox user"
else
log "secubox user already exists"
fi
# Create directories with correct ownership
chroot "${ROOTFS}" install -d -o secubox -g secubox -m 750 /etc/secubox
chroot "${ROOTFS}" install -d -o secubox -g secubox -m 775 /run/secubox
chroot "${ROOTFS}" install -d -o secubox -g secubox -m 750 /var/lib/secubox
chroot "${ROOTFS}" install -d -m 755 /usr/share/secubox/www
# Make sure TLS key is readable by secubox group (for hub service)
chroot "${ROOTFS}" chgrp secubox /etc/secubox/tls/key.pem 2>/dev/null || true
# Create tmpfiles.d entry that runs at boot to recreate /run/secubox
# (since /run is tmpfs and wiped on each boot)
mkdir -p "${ROOTFS}/etc/tmpfiles.d"
cat > "${ROOTFS}/etc/tmpfiles.d/secubox.conf" << 'TMPFILES'
# SecuBox runtime directory for Unix sockets
# Created by tmpfiles.d at boot (before services start)
d /run/secubox 0775 secubox secubox -
TMPFILES
log "tmpfiles.d configured for /run/secubox"
# Create systemd mount unit for /var/lib/secubox tmpfs (required for live boot)
# This overlays the read-only squashfs with a writable tmpfs
log "Creating tmpfs mount for /var/lib/secubox (live boot writable layer)..."
mkdir -p "${ROOTFS}/etc/systemd/system"
cat > "${ROOTFS}/etc/systemd/system/var-lib-secubox.mount" << 'MOUNTUNIT'
[Unit]
Description=SecuBox writable data directory (tmpfs overlay for live boot)
DefaultDependencies=no
After=local-fs.target
Before=sysinit.target
ConditionPathExists=!/var/lib/secubox/.persistent
[Mount]
What=tmpfs
Where=/var/lib/secubox
Type=tmpfs
Options=mode=0755,uid=secubox,gid=secubox,size=100M
[Install]
WantedBy=local-fs.target
MOUNTUNIT
# Enable the mount unit
chroot "${ROOTFS}" systemctl enable var-lib-secubox.mount 2>/dev/null || true
log "tmpfs mount unit created for /var/lib/secubox"
# Find all .deb files in cache/repo or output/
# Note: Packages may be in output/ directly OR output/debs/ subdirectory
CACHE_DEBS="${REPO_DIR}/cache/repo/pool"
# Check both output/ and output/debs/ for packages
if [[ -d "${REPO_DIR}/output" ]] && ls "${REPO_DIR}/output"/secubox-*.deb >/dev/null 2>&1; then
OUTPUT_DEBS="${REPO_DIR}/output"
else
OUTPUT_DEBS="${REPO_DIR}/output/debs"
fi
# Check both locations for packages (handle non-existent dirs gracefully)
if [[ -d "$CACHE_DEBS" ]]; then
CACHE_COUNT=$(find "$CACHE_DEBS" -name "secubox-*.deb" 2>/dev/null | wc -l)
else
CACHE_COUNT=0
fi
if [[ -d "$OUTPUT_DEBS" ]]; then
OUTPUT_COUNT=$(find "$OUTPUT_DEBS" -maxdepth 1 -name "secubox-*.deb" 2>/dev/null | wc -l)
else
OUTPUT_COUNT=0
fi
log "Found ${CACHE_COUNT} packages in cache, ${OUTPUT_COUNT} in output/debs"
if [[ $CACHE_COUNT -gt 0 ]] || [[ $OUTPUT_COUNT -gt 0 ]]; then
log "Slipstream: Installing from local packages"
install -d "${ROOTFS}/tmp/secubox-debs"
# Copy from output/debs FIRST (prefer newer local builds over cache)
# Filter by architecture: only _all.deb and _amd64.deb for x64 Live USB
COPIED_COUNT=0
SKIPPED_COUNT=0
if [[ -d "$OUTPUT_DEBS" ]]; then
for deb in "${OUTPUT_DEBS}"/secubox-*.deb; do
[[ -f "$deb" ]] || continue
deb_name=$(basename "$deb")
case "$deb_name" in
*_all.deb|*_amd64.deb)
cp "$deb" "${ROOTFS}/tmp/secubox-debs/"
((COPIED_COUNT++)) || true
;;
*)
((SKIPPED_COUNT++)) || true
;;
esac
done
log "Copied ${COPIED_COUNT} packages from output/debs"
[[ $SKIPPED_COUNT -gt 0 ]] && log "Skipped ${SKIPPED_COUNT} packages (wrong architecture)"
fi
# Then add from cache for packages not in output/debs
if [[ $CACHE_COUNT -gt 0 ]]; then
for deb in $(find "$CACHE_DEBS" -name "secubox-*_all.deb" -o -name "secubox-*_amd64.deb"); do
pkg_name=$(basename "$deb" | sed 's/_.*$//')
# Only copy if not already present (prefer output/debs version)
if ! ls "${ROOTFS}/tmp/secubox-debs/${pkg_name}_"*.deb >/dev/null 2>&1; then
cp "$deb" "${ROOTFS}/tmp/secubox-debs/"
log "Added ${pkg_name} from cache"
fi
done
fi
DEB_COUNT=$(ls "${ROOTFS}/tmp/secubox-debs/"*.deb 2>/dev/null | wc -l)
# List all packages to be installed
log "Slipstream: ${DEB_COUNT} packages to install:"
echo ""
ls -1 "${ROOTFS}/tmp/secubox-debs/"*.deb | xargs -I{} basename {} | sed 's/_.*$//' | sort -u | while read pkg; do
echo " - ${pkg}"
done
echo ""
# Pre-install textual via pip for secubox-console (Debian's version is too old)
if ls "${ROOTFS}/tmp/secubox-debs/secubox-console_"*.deb >/dev/null 2>&1; then
log "Pre-installing textual for console TUI (pip, Debian version too old)..."
chroot "${ROOTFS}" pip3 install --break-system-packages textual rich 2>/dev/null && \
ok "textual installed via pip" || warn "textual pip install failed"
fi
# ── Ensure systemd directories exist ─────────────────────────────────────
log "Ensuring systemd directories exist..."
install -d -m 755 "${ROOTFS}/usr/lib/systemd/system"
install -d -m 755 "${ROOTFS}/etc/systemd/system"
ok "systemd directories ready"
# ── Pre-install Python dependencies via pip (not in Debian repos) ───────
log "Pre-installing Python dependencies for SecuBox modules..."
chroot "${ROOTFS}" pip3 install --break-system-packages \
'fastapi>=0.100' 'uvicorn[standard]>=0.25' 'pydantic>=2.0' \
httpx python-jose aiofiles aiosqlite toml jinja2 psutil netifaces 2>/dev/null && \
ok "Python dependencies installed via pip" || warn "Some pip packages failed (may be installed by apt later)"
# Install core first (dependency for all)
if ls "${ROOTFS}/tmp/secubox-debs/secubox-core_"*.deb >/dev/null 2>&1; then
log "Installing secubox-core (dependency)..."
chroot "${ROOTFS}" bash -c 'dpkg -i --force-depends /tmp/secubox-debs/secubox-core_*.deb' || warn "secubox-core install failed"
fi
# Install all packages (force overwrite for duplicate files)
log "Installing all packages..."
# Use bash -c to ensure glob expansion happens inside chroot
chroot "${ROOTFS}" bash -c 'dpkg -i --force-depends --force-overwrite /tmp/secubox-debs/*.deb 2>&1' | \
grep -v "^dpkg: warning" | grep -v "^Selecting\|^Preparing\|^Unpacking\|^Setting up" | head -50 || true
# Configure packages (skip apt-get -f as pip provides Python deps)
# Second pass: reconfigure any packages that failed
log "Reconfiguring packages..."
chroot "${ROOTFS}" dpkg --configure -a --force-confold 2>/dev/null || true
# Install textual for console TUI (dependency of secubox-console)
if ls "${ROOTFS}/tmp/secubox-debs/secubox-console_"*.deb >/dev/null 2>&1 || \
chroot "${ROOTFS}" dpkg -l secubox-console 2>/dev/null | grep -q "^ii"; then
log "Installing textual for console TUI..."
# Try apt first, fall back to pip
if ! chroot "${ROOTFS}" apt-get install -y -q python3-textual python3-rich 2>/dev/null; then
chroot "${ROOTFS}" pip3 install --break-system-packages textual 2>/dev/null || \
warn "textual installation failed - console TUI may not work"
fi
ok "Console TUI dependencies installed"
fi
# Verify installations
log "Verifying installations..."
INSTALLED_COUNT=$(chroot "${ROOTFS}" dpkg -l 'secubox-*' 2>/dev/null | grep "^ii" | wc -l)
# List installed packages
echo ""
log "Installed SecuBox packages (${INSTALLED_COUNT}):"
chroot "${ROOTFS}" dpkg -l 'secubox-*' 2>/dev/null | grep "^ii" | awk '{print " ✓ " $2 " (" $3 ")"}' || true
echo ""
# Save list to image
chroot "${ROOTFS}" dpkg -l 'secubox-*' 2>/dev/null | grep "^ii" > "${ROOTFS}/var/lib/secubox/installed-packages.txt" || true
rm -rf "${ROOTFS}/tmp/secubox-debs"
ok "Slipstream: ${INSTALLED_COUNT}/${DEB_COUNT} packages installed successfully"
else
warn "No packages in cache/repo, trying APT..."
if curl -sf "${APT_SECUBOX}/dists/${SUITE}/Release" >/dev/null 2>&1; then
cat > "${ROOTFS}/etc/apt/sources.list.d/secubox.list" <<EOF
deb [trusted=yes] ${APT_SECUBOX} ${SUITE} main
EOF
chroot "${ROOTFS}" apt-get update -q
# Non-interactive conffile handling: secubox-mesh ships mesh.toml as a
# conffile and triggers a dpkg prompt (*** mesh.toml [Y/I/N/O/D/Z]) during
# configure, which fails the whole install in the headless chroot. Keep the
# packaged conffile and never prompt.
chroot "${ROOTFS}" bash -c 'DEBIAN_FRONTEND=noninteractive apt-get install -y -q \
-o Dpkg::Options::=--force-confold -o Dpkg::Options::=--force-confdef secubox-full' 2>/dev/null || true
chroot "${ROOTFS}" dpkg --configure -a --force-confold 2>/dev/null || true
# Verify secubox-core installed (dependency of secubox-full)
if ! chroot "${ROOTFS}" dpkg -l secubox-core 2>/dev/null | grep -q "^ii"; then
warn "secubox-full unavailable"
fi
fi
fi
# Fix misplaced nginx configs (some packages install to conf.d instead of secubox.d)
# Location blocks must be inside server blocks, so move them to secubox.d
log "Fixing nginx module configs..."
# Disable strict mode temporarily for glob handling
set +e
log "DEBUG: Checking conf.d..."
for conf in "${ROOTFS}/etc/nginx/conf.d/secubox-"*.conf "${ROOTFS}/etc/nginx/conf.d/"*secubox*.conf; do
[[ -e "$conf" ]] || continue
if [[ -L "$conf" ]]; then
rm -f "$conf"
log "Removed symlink $(basename "$conf") from conf.d/"
elif [[ -f "$conf" ]] && grep -q "^location" "$conf" 2>/dev/null; then
mv "$conf" "${ROOTFS}/etc/nginx/secubox.d/" 2>/dev/null || true
log "Moved $(basename "$conf") to secubox.d/"
fi
done
log "DEBUG: conf.d done"
log "DEBUG: Checking sites-enabled..."
for site in "${ROOTFS}/etc/nginx/sites-enabled/secubox-"*; do
[[ -e "$site" ]] || [[ -L "$site" ]] || continue
real_file="$site"
[[ -L "$site" ]] && real_file=$(readlink -f "$site" 2>/dev/null | sed "s|^|${ROOTFS}|; s|${ROOTFS}${ROOTFS}|${ROOTFS}|") || true
if [[ -f "$real_file" ]] && grep -q "^location" "$real_file" 2>/dev/null; then
base_name=$(basename "$site" | sed 's/^secubox-//')
cp "$real_file" "${ROOTFS}/etc/nginx/secubox.d/${base_name}.conf" 2>/dev/null || true
rm -f "$site"
log "Moved $(basename "$site") content to secubox.d/${base_name}.conf"
fi
done
log "DEBUG: sites-enabled done"
log "DEBUG: Cleaning broken symlinks in secubox.d..."
for conf in "${ROOTFS}/etc/nginx/secubox.d/"*.conf; do
if [[ -L "$conf" ]] && [[ ! -e "$conf" ]]; then
rm -f "$conf" && log "Removed broken symlink $(basename "$conf")" || true
fi
done
log "DEBUG: symlink cleanup 1 done"
log "DEBUG: Cleaning symlinks to non-existent targets..."
for conf in "${ROOTFS}/etc/nginx/secubox.d/"*.conf; do
if [[ -L "$conf" ]]; then
target=$(readlink "$conf" 2>/dev/null) || target=""
if [[ -n "$target" ]] && [[ "$target" == /* ]] && [[ ! -e "${ROOTFS}${target}" ]]; then
rm -f "$conf"
log "Removed broken symlink $(basename "$conf") -> $target"
fi
fi
done
log "DEBUG: symlink cleanup 2 done"
# Re-enable strict mode
set -e
ok "SecuBox packages installed"
# ── Fallback: Install core services if packages missing ────────────
# If secubox-core wasn't installed via deb, install the critical components manually
log "Installing core services fallback..."
# Create secubox user/group if missing
if ! chroot "${ROOTFS}" id -u secubox >/dev/null 2>&1; then
chroot "${ROOTFS}" adduser --system --group --no-create-home \
--home /var/lib/secubox --shell /usr/sbin/nologin secubox 2>/dev/null || true
ok "Created secubox user"
fi
# Install secubox_core Python module
CORE_PY_SRC="${SCRIPT_DIR}/../common/secubox_core"
if [[ -d "${CORE_PY_SRC}" ]]; then
CORE_PY_DST="${ROOTFS}/usr/lib/python3/dist-packages/secubox_core"
mkdir -p "${CORE_PY_DST}"
cp -r "${CORE_PY_SRC}/"*.py "${CORE_PY_DST}/" 2>/dev/null || true
ok "Installed secubox_core Python module"
fi
# Install secubox-runtime.service (creates /run/secubox)
if [[ ! -f "${ROOTFS}/usr/lib/systemd/system/secubox-runtime.service" ]]; then
cat > "${ROOTFS}/usr/lib/systemd/system/secubox-runtime.service" <<'RTMSVC'
[Unit]
Description=SecuBox Runtime Directory Setup
DefaultDependencies=no
Before=secubox-hub.service secubox-portal.service
After=local-fs.target
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/bin/mkdir -p /run/secubox
ExecStart=/bin/chown secubox:secubox /run/secubox
ExecStart=/bin/chmod 775 /run/secubox
[Install]
WantedBy=multi-user.target
RTMSVC
ln -sf /usr/lib/systemd/system/secubox-runtime.service \
"${ROOTFS}/etc/systemd/system/multi-user.target.wants/secubox-runtime.service"
ok "Installed secubox-runtime.service"
fi
# Install secubox-core.service
if [[ ! -f "${ROOTFS}/usr/lib/systemd/system/secubox-core.service" ]]; then
cat > "${ROOTFS}/usr/lib/systemd/system/secubox-core.service" <<'CORESVC'
[Unit]
Description=SecuBox Core Setup
After=network.target secubox-runtime.service
Requires=secubox-runtime.service
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/bin/mkdir -p /run/secubox
ExecStart=/bin/chown secubox:secubox /run/secubox
ExecStart=/bin/chmod 775 /run/secubox
ExecStart=/bin/sh -c "id -nG www-data | grep -q secubox || usermod -aG secubox www-data"
[Install]
WantedBy=multi-user.target
CORESVC
ln -sf /usr/lib/systemd/system/secubox-core.service \
"${ROOTFS}/etc/systemd/system/multi-user.target.wants/secubox-core.service"
ok "Installed secubox-core.service"
fi
# Install secubox-hub.service if package didn't install it
if [[ ! -f "${ROOTFS}/usr/lib/systemd/system/secubox-hub.service" ]]; then
cat > "${ROOTFS}/usr/lib/systemd/system/secubox-hub.service" <<'HUBSVC'
[Unit]
Description=SecuBox Hub — Dashboard Central API
After=network.target secubox-core.service secubox-runtime.service
Wants=secubox-core.service secubox-runtime.service
[Service]
UMask=0002
Type=simple
User=secubox
Group=secubox
WorkingDirectory=/usr/lib/secubox/hub
ExecStartPre=+/bin/mkdir -p /run/secubox
ExecStartPre=+/bin/chown secubox:secubox /run/secubox
ExecStartPre=+/bin/chmod 775 /run/secubox
ExecStart=/usr/bin/python3 -m uvicorn api.main:app \
--uds /run/secubox/hub.sock \
--log-level warning
Restart=on-failure
RestartSec=5
PrivateTmp=true
NoNewPrivileges=true
RuntimeDirectory=secubox
RuntimeDirectoryMode=0775
ProtectSystem=full
ReadWritePaths=/run/secubox /var/lib/secubox /etc/secubox
[Install]
WantedBy=multi-user.target
HUBSVC
ok "Installed secubox-hub.service"
fi
# Install hub API if missing
if [[ ! -d "${ROOTFS}/usr/lib/secubox/hub/api" ]]; then
HUB_API_SRC="${SCRIPT_DIR}/../packages/secubox-hub/api"
if [[ -d "${HUB_API_SRC}" ]]; then
mkdir -p "${ROOTFS}/usr/lib/secubox/hub/api"
cp -r "${HUB_API_SRC}/"* "${ROOTFS}/usr/lib/secubox/hub/api/"
ok "Installed hub API from packages/secubox-hub/api"
fi
fi
# Enable secubox-hub.service
ln -sf /usr/lib/systemd/system/secubox-hub.service \
"${ROOTFS}/etc/systemd/system/multi-user.target.wants/secubox-hub.service" 2>/dev/null || true
ok "Core services fallback complete"
# ── Fix systemd service namespaces for /run/secubox ────────────────
# Services with ProtectSystem=strict create mount namespaces that prevent
# socket creation in /run/secubox. Add RuntimeDirectory and tmpfiles.d.
log "Configuring systemd services for /run/secubox..."
# Create tmpfiles.d entry for /run/secubox
mkdir -p "${ROOTFS}/etc/tmpfiles.d"
echo "d /run/secubox 0775 secubox secubox -" > "${ROOTFS}/etc/tmpfiles.d/secubox.conf"
# Create systemd overrides for services that use ProtectSystem with /run/secubox
for unit in "${ROOTFS}"/usr/lib/systemd/system/secubox-*.service; do
[[ -f "$unit" ]] || continue
svc=$(basename "$unit" .service)
# Check if service uses ProtectSystem with ReadWritePaths containing /run/secubox
if grep -q "ProtectSystem=" "$unit" && grep -q "ReadWritePaths=.*/run/secubox" "$unit"; then
override_dir="${ROOTFS}/etc/systemd/system/${svc}.service.d"
mkdir -p "$override_dir"
cat > "$override_dir/runtime.conf" << 'EOF'
[Service]
# Fix for namespace issues with /run/secubox
RuntimeDirectory=secubox
RuntimeDirectoryMode=0775
RuntimeDirectoryPreserve=yes
EOF
log "Created override for $svc"
fi
done
ok "Systemd service overrides created"
# ── Create build metadata ──────────────────────────────────────────
log "Creating build metadata..."
BUILD_TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
BUILD_DATE=$(date +"%Y-%m-%d")
GIT_COMMIT=$(git rev-parse --short HEAD 2>/dev/null || echo "unknown")
GIT_BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "unknown")
mkdir -p "${ROOTFS}/etc/secubox"
cat > "${ROOTFS}/etc/secubox/build-info.json" <<EOF
{
"build_timestamp": "${BUILD_TIMESTAMP}",
"build_date": "${BUILD_DATE}",
"git_commit": "${GIT_COMMIT}",
"git_branch": "${GIT_BRANCH}",
"board": "amd64-live",
"version": "${SECUBOX_VERSION}",
"builder": "$(whoami)@$(hostname)"
}
EOF
log "Build metadata: ${BUILD_DATE} (${GIT_COMMIT})"
# ── Default config for demo ────────────────────────────────────
# Config file: /etc/secubox/secubox.conf (TOML)
# Credentials: admin / secubox (for live USB demo purposes)
cat > "${ROOTFS}/etc/secubox/secubox.conf" <<'SECUBOXCONF'
# SecuBox Configuration — Live USB Demo
# Generated by build-live-usb.sh
[global]
hostname = "secubox-live"
timezone = "Europe/Paris"
board = "amd64-live"
[api]
socket_dir = "/run/secubox"
jwt_secret = "live-usb-demo-secret-change-in-production"
[auth]
# Demo credentials — change in production!
[auth.users.admin]
password = "secubox"
role = "admin"
[auth.users.root]
password = "secubox"
role = "admin"
[crowdsec]
lapi_url = "http://127.0.0.1:8080"
lapi_key = ""
[wireguard]
interface = "wg0"
listen_port = 51820
SECUBOXCONF
chmod 644 "${ROOTFS}/etc/secubox/secubox.conf"
chroot "${ROOTFS}" chown root:secubox /etc/secubox/secubox.conf 2>/dev/null || true
log "Created default secubox.conf (admin/secubox)"
# Create users.json for portal authentication
# Password: secubox -> SHA256 hash
ADMIN_PASS_HASH="b8780673264e05b9dc557d371bd7cee0dd32a478205f63f5c55c1c037d9e6c22"
cat > "${ROOTFS}/etc/secubox/users.json" <<EOF
{
"admin": {
"password_hash": "${ADMIN_PASS_HASH}",
"email": "admin@secubox.local",
"role": "admin",
"created": "$(date -Iseconds)"
},
"root": {
"password_hash": "${ADMIN_PASS_HASH}",
"email": "root@secubox.local",
"role": "admin",
"created": "$(date -Iseconds)"
}
}
EOF
chmod 644 "${ROOTFS}/etc/secubox/users.json"
chroot "${ROOTFS}" chown root:secubox /etc/secubox/users.json 2>/dev/null || true
log "Created users.json (admin/secubox, root/secubox)"
# ── Restore real systemctl ─────────────────────────────────────────
if [[ ${SYSTEMCTL_DIVERTED:-0} -eq 1 ]] && [[ -x "${ROOTFS}/bin/systemctl.real" ]]; then
rm -f "${ROOTFS}/bin/systemctl"
mv "${ROOTFS}/bin/systemctl.real" "${ROOTFS}/bin/systemctl"
log "Restored real systemctl"
fi
rm -f "${ROOTFS}/usr/local/sbin/systemctl-chroot"
# ── Enable all SecuBox services ────────────────────────────────────
# CRITICAL: Package postinst scripts may fail in chroot, so we
# explicitly enable all services here by creating symlinks directly
# (systemctl enable doesn't work reliably without systemd running)
log "Enabling SecuBox services..."
# Create wants directory if missing
mkdir -p "${ROOTFS}/etc/systemd/system/multi-user.target.wants"
# First, enable critical setup services (creates /run/secubox socket directory)
# These must run before any other SecuBox service
for setup_svc in secubox-runtime.service secubox-core.service; do
if [[ -f "${ROOTFS}/usr/lib/systemd/system/${setup_svc}" ]]; then
ln -sf "/usr/lib/systemd/system/${setup_svc}" \
"${ROOTFS}/etc/systemd/system/multi-user.target.wants/${setup_svc}"
ok "${setup_svc} enabled (setup service)"
fi
done
# Enable all SecuBox API services by creating symlinks
ENABLED_COUNT=0
for svc in "${ROOTFS}/usr/lib/systemd/system/secubox-"*.service; do
[[ -f "$svc" ]] || continue
svc_name=$(basename "$svc")
# Skip services that should not auto-start
case "$svc_name" in
secubox-kiosk*.service|secubox-console.service|secubox-runtime.service)
# Kiosk and console are optional - enabled separately
# Runtime already handled above
continue
;;
esac
# Create symlink to enable service
ln -sf "/usr/lib/systemd/system/${svc_name}" \
"${ROOTFS}/etc/systemd/system/multi-user.target.wants/${svc_name}"
ENABLED_COUNT=$((ENABLED_COUNT + 1))
done
ok "Enabled ${ENABLED_COUNT} SecuBox services"
# Enable nginx for API proxying
ln -sf /usr/lib/systemd/system/nginx.service \
"${ROOTFS}/etc/systemd/system/multi-user.target.wants/nginx.service" 2>/dev/null || true
# ── Fix nginx configuration to ensure it starts ─────────────────────
log "Fixing nginx configuration..."
# Ensure snippets directory exists
mkdir -p "${ROOTFS}/etc/nginx/snippets"
mkdir -p "${ROOTFS}/etc/nginx/secubox.d"
# Install secubox-proxy.conf snippet if missing
if [[ ! -f "${ROOTFS}/etc/nginx/snippets/secubox-proxy.conf" ]]; then
if [[ -f "${ROOTFS}/usr/share/secubox-core/nginx/secubox-proxy.conf" ]]; then
cp "${ROOTFS}/usr/share/secubox-core/nginx/secubox-proxy.conf" \
"${ROOTFS}/etc/nginx/snippets/secubox-proxy.conf"
log "Installed secubox-proxy.conf snippet"
else
# Create minimal proxy config
cat > "${ROOTFS}/etc/nginx/snippets/secubox-proxy.conf" << 'PROXYEOF'
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 30s;
proxy_connect_timeout 5s;
proxy_buffering off;
PROXYEOF
log "Created minimal secubox-proxy.conf snippet"
fi
fi
# Install main secubox nginx config if missing
if [[ ! -f "${ROOTFS}/etc/nginx/sites-available/secubox" ]]; then
if [[ -f "${ROOTFS}/usr/share/secubox-core/nginx/secubox.conf" ]]; then
cp "${ROOTFS}/usr/share/secubox-core/nginx/secubox.conf" \
"${ROOTFS}/etc/nginx/sites-available/secubox"
ln -sf /etc/nginx/sites-available/secubox \
"${ROOTFS}/etc/nginx/sites-enabled/secubox"
log "Installed secubox nginx site config"
fi
fi
# ALWAYS remove default nginx site to avoid duplicate default_server conflict
rm -f "${ROOTFS}/etc/nginx/sites-enabled/default"
log "Removed default nginx site (conflicts with secubox)"
# Remove bad configs from conf.d (location-only files belong in secubox.d, not conf.d)
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
# Resolve symlink if needed
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 file contains location directive (not inside server block), it's misplaced
if [[ -f "$real_file" ]] && grep -q "^location" "$real_file" 2>/dev/null; then
base=$(basename "$conf" .conf | sed 's/^secubox-//')
# Move content to secubox.d if not already there
if [[ ! -f "${ROOTFS}/etc/nginx/secubox.d/${base}.conf" ]]; then
cp "$real_file" "${ROOTFS}/etc/nginx/secubox.d/${base}.conf" 2>/dev/null || true
log "Moved ${base} from conf.d to secubox.d"
fi
rm -f "$conf"
fi
done
# Remove bad configs from sites-enabled (location-only files belong in secubox.d)
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 # Keep main secubox config
# Check if file exists (resolve symlink)
real_file="$site"
[[ -L "$site" ]] && real_file=$(readlink -f "$site") && real_file="${ROOTFS}${real_file#${ROOTFS}}"
[[ -f "$real_file" ]] || { rm -f "$site"; continue; }
# If it starts with location (not server), move to secubox.d
if grep -q "^location" "$real_file" 2>/dev/null && ! grep -q "^server" "$real_file" 2>/dev/null; then
base=$(basename "$site" | sed 's/^secubox-//')
cp "$real_file" "${ROOTFS}/etc/nginx/secubox.d/${base}.conf" 2>/dev/null || true
rm -f "$site"
log "Moved $(basename "$site") to secubox.d/ (was location-only)"
fi
done
# ── Ensure hub.conf exists for the Hub API ────────────────────────────
# This is critical for authentication to work
if [[ ! -f "${ROOTFS}/etc/nginx/secubox.d/hub.conf" ]]; then
log "Creating hub.conf for Hub API..."
cat > "${ROOTFS}/etc/nginx/secubox.d/hub.conf" << 'HUBCONF'
# /etc/nginx/secubox.d/hub.conf
# SecuBox Hub API - Authentication and Dashboard
location /api/v1/hub/ {
proxy_pass http://unix:/run/secubox/hub.sock:/;
include /etc/nginx/snippets/secubox-proxy.conf;
}
HUBCONF
ok "Created hub.conf"
fi
# 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 in chroot
log "Testing nginx configuration..."
if chroot "${ROOTFS}" nginx -t 2>&1; then
ok "nginx configuration valid"
else
warn "nginx config test failed - attempting auto-fix..."
# Show specific error
error_output=$(chroot "${ROOTFS}" nginx -t 2>&1 | head -5)
echo "$error_output"
# Extract problematic file from error message
bad_file=$(echo "$error_output" | grep -oP '/etc/nginx/[^:]+' | head -1)
if [[ -n "$bad_file" ]] && [[ -f "${ROOTFS}${bad_file}" ]]; then
log "Disabling problematic config: $bad_file"
mv "${ROOTFS}${bad_file}" "${ROOTFS}${bad_file}.disabled" 2>/dev/null || rm -f "${ROOTFS}${bad_file}"
# Test again
if chroot "${ROOTFS}" nginx -t 2>&1; then
ok "nginx configuration fixed by disabling $bad_file"
else
warn "nginx still failing, may need manual fix at runtime"
fi
fi
# If SSL certs missing, regenerate
if [[ ! -f "${ROOTFS}/etc/secubox/tls/cert.pem" ]]; then
warn "SSL certificates missing, regenerating..."
mkdir -p "${ROOTFS}/etc/secubox/tls"
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-live/O=CyberMind SecuBox/C=FR" 2>/dev/null
fi
fi
# Note: SSL certs were already generated before package installation
# ══════════════════════════════════════════════════════════════════
# Step 5: Network detection & kiosk scripts
# ══════════════════════════════════════════════════════════════════
log "5/8 Installing SecuBox scripts..."
mkdir -p "${ROOTFS}/usr/sbin"
mkdir -p "${ROOTFS}/usr/lib/secubox"
# Copy scripts (including kiosk-launcher for robust startup, TUI, mode switcher, disk/eMMC flashers)
for script in secubox-net-detect secubox-net-reset secubox-kiosk-setup secubox-cmdline-handler secubox-kiosk-launcher secubox-x11-splash secubox-console-tui secubox-mode secubox-flash-disk secubox-flash-emmc; do
if [[ -f "${SCRIPT_DIR}/sbin/${script}" ]]; then
cp "${SCRIPT_DIR}/sbin/${script}" "${ROOTFS}/usr/sbin/"
chmod +x "${ROOTFS}/usr/sbin/${script}"
fi
done
# secubox-kiosk-tui: discoverable alias for the kiosk-mode fallback TUI
# (matches the `kiosk` mode in secubox-mode) — see issue #228.
if [[ -f "${ROOTFS}/usr/sbin/secubox-console-tui" ]]; then
ln -sf secubox-console-tui "${ROOTFS}/usr/sbin/secubox-kiosk-tui"
fi
# Copy overlay tools (v1.6.7.2+ - always included for factory reset capability)
for script in secubox-overlay-init secubox-snapshot secubox-ramcache secubox-factory-reset; do
if [[ -f "${SCRIPT_DIR}/sbin/${script}" ]]; then
cp "${SCRIPT_DIR}/sbin/${script}" "${ROOTFS}/usr/sbin/"
chmod +x "${ROOTFS}/usr/sbin/${script}"
log " + ${script}"
fi
done
# Embed target image for disk flashing (if specified)
if [[ -n "${EMBED_IMAGE}" ]]; then
if [[ -f "${EMBED_IMAGE}" ]]; then
log "Embedding target image for disk flashing..."
mkdir -p "${ROOTFS}/secubox"
cp "${EMBED_IMAGE}" "${ROOTFS}/secubox/secubox-x64.img.gz"
# Create checksum if not present
if [[ -f "${EMBED_IMAGE}.sha256" ]]; then
cp "${EMBED_IMAGE}.sha256" "${ROOTFS}/secubox/secubox-x64.img.gz.sha256"
else
sha256sum "${EMBED_IMAGE}" | awk '{print $1}' > "${ROOTFS}/secubox/secubox-x64.img.gz.sha256"
fi
ok "Embedded image: $(basename ${EMBED_IMAGE}) ($(du -sh ${EMBED_IMAGE} | cut -f1))"
else
err "Embed image not found: ${EMBED_IMAGE}"
fi
fi
# Copy firstboot
if [[ -f "${SCRIPT_DIR}/firstboot.sh" ]]; then
cp "${SCRIPT_DIR}/firstboot.sh" "${ROOTFS}/usr/lib/secubox/"
chmod +x "${ROOTFS}/usr/lib/secubox/firstboot.sh"
fi
# Systemd services (including wayland variant for kiosk and TUI)
mkdir -p "${ROOTFS}/etc/systemd/system"
for svc in secubox-net-detect secubox-cmdline secubox-kiosk secubox-kiosk-wayland secubox-console-tui; do
if [[ -f "${SCRIPT_DIR}/systemd/${svc}.service" ]]; then
cp "${SCRIPT_DIR}/systemd/${svc}.service" "${ROOTFS}/etc/systemd/system/"
fi
done
# Enable net-detect and cmdline services (using symlinks for chroot)
if [[ -f "${ROOTFS}/etc/systemd/system/secubox-cmdline.service" ]]; then
ln -sf /etc/systemd/system/secubox-cmdline.service \
"${ROOTFS}/etc/systemd/system/sysinit.target.wants/secubox-cmdline.service" 2>/dev/null || true
fi
# Firstboot service
cat > "${ROOTFS}/etc/systemd/system/secubox-firstboot.service" <<EOF
[Unit]
Description=SecuBox First Boot
After=network-online.target
ConditionPathExists=!/var/lib/secubox/.firstboot-done
[Service]
Type=oneshot
ExecStart=/usr/lib/secubox/firstboot.sh
ExecStartPost=/bin/touch /var/lib/secubox/.firstboot-done
RemainAfterExit=yes
[Install]
WantedBy=multi-user.target
EOF
# Enable firstboot using symlink
ln -sf /etc/systemd/system/secubox-firstboot.service \
"${ROOTFS}/etc/systemd/system/multi-user.target.wants/secubox-firstboot.service"
# ── Kiosk mode packages (X11 mode - better VM compatibility) ─────────
if [[ $INCLUDE_KIOSK -eq 1 ]]; then
log "Installing kiosk mode packages (X11)..."
# X11 packages for better VM/hardware compatibility
# Install in stages to handle dependencies properly:
# 1. Core X11 and x11-utils first (avoids luit conflict with chromium)
log "Installing X11 core packages..."
# First, fix any broken dpkg state from secubox packages (their postinst may fail in chroot)
chroot "${ROOTFS}" dpkg --configure -a --force-confold 2>/dev/null || true
# Install X11 packages non-interactively (avoid keyboard-configuration prompt)
# nodm = minimal display manager designed for kiosk/embedded systems
# Note: VirtualBox with VMSVGA controller (default) needs xserver-xorg-video-vmware
chroot "${ROOTFS}" env DEBIAN_FRONTEND=noninteractive apt-get install -y -q \
xorg xinit x11-xserver-utils x11-utils \
nodm \
xserver-xorg-video-fbdev \
xserver-xorg-video-vmware \
xserver-xorg-video-vesa \
fonts-dejavu-core fonts-noto-color-emoji fonts-symbola unclutter kbd \
libinput10 xdg-utils \
feh zenity 2>/dev/null || warn "Some X11 packages failed"
# Install VirtualBox guest additions for VM-friendly kiosk operation.
# Packages live in Debian `contrib` which sources.list already enables
# (set at step 3). Mouse integration, dynamic resolution, time sync.
log "Installing VirtualBox guest additions..."
chroot "${ROOTFS}" env DEBIAN_FRONTEND=noninteractive apt-get update -q
if chroot "${ROOTFS}" env DEBIAN_FRONTEND=noninteractive apt-get install -y -q \
virtualbox-guest-utils virtualbox-guest-x11; then
ok "VirtualBox guest additions installed"
else
warn "VirtualBox guest additions install failed (apt error above); falling back to vmware driver for VMSVGA"
fi
# Explicitly install emoji/symbol fonts (critical for sidebar icons)
log "Installing emoji and symbol font packages..."
chroot "${ROOTFS}" env DEBIAN_FRONTEND=noninteractive apt-get install -y -q \
fonts-noto-color-emoji fonts-symbola fonts-noto-core 2>/dev/null || warn "Some font packages failed"
# Also try to install fonts-noto-extra for better Unicode coverage
chroot "${ROOTFS}" env DEBIAN_FRONTEND=noninteractive apt-get install -y -q fonts-noto 2>/dev/null || true
# Configure fontconfig to use Noto Color Emoji for emojis (sidebar icons)
log "Configuring fontconfig for emoji support..."
mkdir -p "${ROOTFS}/etc/fonts/conf.d"
cat > "${ROOTFS}/etc/fonts/conf.d/99-noto-emoji.conf" <<'FONTCONF'
<?xml version="1.0"?>
<!DOCTYPE fontconfig SYSTEM "fonts.dtd">
<fontconfig>
<!-- Use Noto Color Emoji for emoji characters -->
<match target="pattern">
<test name="family"><string>emoji</string></test>
<edit name="family" mode="prepend" binding="strong">
<string>Noto Color Emoji</string>
</edit>
</match>
<!-- Add Noto Color Emoji and Symbola as fallback for all fonts -->
<match target="pattern">
<edit name="family" mode="append">
<string>Noto Color Emoji</string>
<string>Symbola</string>
</edit>
</match>
<!-- Accept both color and monochrome emoji fonts -->
<selectfont>
<acceptfont>
<pattern>
<patelt name="family"><string>Noto Color Emoji</string></patelt>
</pattern>
<pattern>
<patelt name="family"><string>Symbola</string></patelt>
</pattern>
</acceptfont>
</selectfont>
</fontconfig>
FONTCONF
# Rebuild font cache
chroot "${ROOTFS}" fc-cache -f 2>/dev/null || warn "fc-cache failed"
ok "Emoji fontconfig configured"
# Ensure critical kiosk packages are installed
chroot "${ROOTFS}" env DEBIAN_FRONTEND=noninteractive apt-get install -y -q xinit kbd || warn "xinit/kbd install failed"
# Fix any broken dependencies before installing chromium
chroot "${ROOTFS}" dpkg --configure -a --force-confold 2>/dev/null || true
chroot "${ROOTFS}" apt-get install -f -y -q 2>/dev/null || true
# 2. Install chromium separately (after x11-utils to avoid dependency conflicts)
log "Installing Chromium..."
chroot "${ROOTFS}" env DEBIAN_FRONTEND=noninteractive apt-get install -y -q chromium || warn "Chromium install failed"
# Verify key kiosk packages installed - warn if missing but continue
if ! chroot "${ROOTFS}" dpkg -l xinit 2>/dev/null | grep -q "^ii"; then
warn "xinit not installed - kiosk may not work"
# Try one more time with forced install
chroot "${ROOTFS}" env DEBIAN_FRONTEND=noninteractive apt-get install -y -q --fix-broken xinit 2>/dev/null || true
fi
if ! chroot "${ROOTFS}" dpkg -l chromium 2>/dev/null | grep -q "^ii"; then
warn "Chromium not installed - kiosk may not work properly"
fi
# Allow non-console users to start X server (required for systemd service)
mkdir -p "${ROOTFS}/etc/X11"
cat > "${ROOTFS}/etc/X11/Xwrapper.config" <<'XWRAP'
allowed_users=anybody
needs_root_rights=yes
XWRAP
# X11 config - auto-detect VM type at boot and use appropriate driver
# VirtualBox VMSVGA (default) needs vmware driver, VBoxVGA needs modesetting/vboxvideo
mkdir -p "${ROOTFS}/etc/X11/xorg.conf.d"
# Create a boot-time X11 configuration script
cat > "${ROOTFS}/usr/local/bin/secubox-x11-setup" <<'X11SETUP'
#!/bin/bash
# SecuBox X11 Setup - Auto-configure X11 driver based on hardware/VM type
# Run at boot before X11 starts
set -u
LOG_TAG="secubox-x11-setup"
log() { echo "[x11-setup] $*"; logger -t "$LOG_TAG" "$*" 2>/dev/null || true; }
# Detect virtualization type
VM_TYPE=$(systemd-detect-virt 2>/dev/null || echo "none")
log "VM type detected: $VM_TYPE"
# Detect GPU
GPU_INFO=$(lspci 2>/dev/null | grep -iE "vga|display|3d" | head -1 || echo "unknown")
log "GPU detected: $GPU_INFO"
# Determine driver
DRIVER="modesetting" # Safe default
case "$VM_TYPE" in
oracle)
# VirtualBox: use modesetting unconditionally. The previous
# "vmware" branch for VMSVGA controllers triggered vmwgfx kernel
# ERRORs ("Failed to send host log message") because vmwgfx
# expects a real VMware Workstation host — on VBox it half-loads
# then fails on the Xorg side, kiosk launcher gives up after 3
# retries and self-disables. modesetting talks DRM/KMS through
# the same vmwgfx kmod but ignores the broken host channel and
# actually paints pixels.
DRIVER="modesetting"
log "VirtualBox detected → modesetting driver (works for both VMSVGA + VBoxVGA)"
# Load VirtualBox kernel modules so Guest Additions integration works
modprobe vboxguest 2>/dev/null || true
modprobe vboxvideo 2>/dev/null || true
;;
vmware)
DRIVER="vmware"
log "VMware detected → vmware driver"
modprobe vmwgfx 2>/dev/null || true
;;
kvm|qemu)
DRIVER="modesetting"
log "KVM/QEMU detected → modesetting driver"
modprobe virtio-gpu 2>/dev/null || true
;;
none)
# Bare metal - NO config needed! Intel/AMD/NVIDIA auto-detect perfectly.
# Creating custom configs often causes MORE problems than it solves.
log "Bare metal detected - skipping X11 config (auto-detection is best)"
# Load common GPU drivers
modprobe i915 2>/dev/null || true
modprobe amdgpu 2>/dev/null || true
modprobe nouveau 2>/dev/null || true
# Remove any existing custom configs that might cause issues
rm -f /etc/X11/xorg.conf.d/10-*.conf 2>/dev/null || true
log "X11 will auto-detect Intel/AMD/NVIDIA hardware"
exit 0
;;
*)
DRIVER="modesetting"
log "Unknown VM type '$VM_TYPE' → modesetting driver"
;;
esac
# Write X11 configuration (VMs only - bare metal exits above)
mkdir -p /etc/X11/xorg.conf.d
# Sanitize GPU_INFO - keep only alphanumeric, spaces, colons, hyphens
# This prevents ANY special character from breaking the X11 config heredoc
GPU_INFO_SAFE=$(echo "$GPU_INFO" | tr -cd '[:alnum:] :-' | head -c 80)
cat > /etc/X11/xorg.conf.d/10-secubox-driver.conf <<XCONF
# Auto-generated by secubox-x11-setup (VM only)
# VM Type: $VM_TYPE
# GPU: $GPU_INFO_SAFE
# Driver: $DRIVER
Section "Device"
Identifier "SecuBox Graphics"
Driver "$DRIVER"
EndSection
Section "Screen"
Identifier "SecuBox Screen"
Device "SecuBox Graphics"
DefaultDepth 24
SubSection "Display"
Depth 24
Modes "1920x1080" "1280x720" "1024x768" "800x600"
EndSubSection
EndSection
Section "ServerFlags"
Option "DontZap" "false"
Option "BlankTime" "0"
Option "StandbyTime" "0"
Option "SuspendTime" "0"
Option "OffTime" "0"
Option "AutoAddDevices" "true"
Option "AllowEmptyInput" "false"
EndSection
Section "InputClass"
Identifier "libinput keyboard"
MatchIsKeyboard "on"
Driver "libinput"
EndSection
Section "InputClass"
Identifier "libinput pointer"
MatchIsPointer "on"
Driver "libinput"
EndSection
XCONF
log "X11 config written: /etc/X11/xorg.conf.d/10-secubox-driver.conf (driver=$DRIVER)"
X11SETUP
chmod +x "${ROOTFS}/usr/local/bin/secubox-x11-setup"
# Create systemd service to run X11 setup before kiosk
cat > "${ROOTFS}/etc/systemd/system/secubox-x11-setup.service" <<'SVCFILE'
[Unit]
Description=SecuBox X11 Driver Setup
DefaultDependencies=no
Before=secubox-kiosk.service display-manager.service
After=systemd-modules-load.service
[Service]
Type=oneshot
ExecStart=/usr/local/bin/secubox-x11-setup
RemainAfterExit=yes
[Install]
WantedBy=multi-user.target
SVCFILE
# Enable the service
ln -sf /etc/systemd/system/secubox-x11-setup.service \
"${ROOTFS}/etc/systemd/system/multi-user.target.wants/secubox-x11-setup.service"
# NO static fallback config - bare metal auto-detection works better without configs
# Only VMs need explicit driver configs, which secubox-x11-setup.service creates
# Ensure xorg.conf.d exists but is empty for bare metal
mkdir -p "${ROOTFS}/etc/X11/xorg.conf.d"
ok "X11 auto-detection setup created (VMs get explicit driver, bare metal auto-detects)"
# Create kiosk user with UID 1000 (use /bin/bash for X11 su commands)
if ! chroot "${ROOTFS}" id secubox-kiosk &>/dev/null; then
chroot "${ROOTFS}" useradd -r -u 1000 -m -d /home/secubox-kiosk -s /bin/bash \
-G video,audio,input,render secubox-kiosk 2>/dev/null || \
chroot "${ROOTFS}" useradd -r -m -d /home/secubox-kiosk -s /bin/bash \
-G video,audio,input,render secubox-kiosk
ok "Created kiosk user"
fi
# Create X11 .xsession for kiosk (used by nodm and xinit)
# Note: secubox-kiosk-launcher overwrites this with a more complete version at runtime
mkdir -p "${ROOTFS}/home/secubox-kiosk/.config"
cat > "${ROOTFS}/home/secubox-kiosk/.xsession" <<'XSESSION'
#!/bin/bash
# SecuBox Kiosk X Session v1.6.7.2 (fallback)
export DISPLAY=${DISPLAY:-:0}
logger -t secubox-kiosk "Starting kiosk session..."
# Basic X settings (ignore errors)
xset s off 2>/dev/null || true
xset -dpms 2>/dev/null || true
xset s noblank 2>/dev/null || true
# URL to display - use localhost for universal compatibility
URL="https://localhost/"
# Chromium with VT-switch-friendly flags (--start-fullscreen, not --kiosk)
# exec replaces shell with chromium - on exit, X session ends
exec chromium \
--start-fullscreen \
--no-first-run \
--no-sandbox \
--disable-gpu \
--disable-pinch \
--noerrdialogs \
--disable-translate \
--ignore-certificate-errors \
--disable-features=TranslateUI \
--window-position=0,0 \
"$URL"
XSESSION
chmod +x "${ROOTFS}/home/secubox-kiosk/.xsession"
# Create symlink for xinit compatibility
ln -sf .xsession "${ROOTFS}/home/secubox-kiosk/.xinitrc"
chroot "${ROOTFS}" chown -R secubox-kiosk:secubox-kiosk /home/secubox-kiosk
# Mark as installed AND enabled (so kiosk starts on first boot)
mkdir -p "${ROOTFS}/var/lib/secubox"
echo "installed=$(date -Iseconds)" > "${ROOTFS}/var/lib/secubox/.kiosk-installed"
touch "${ROOTFS}/var/lib/secubox/.kiosk-enabled"
echo "x11" > "${ROOTFS}/var/lib/secubox/.kiosk-mode"
# CRITICAL: Enable secubox-kiosk.service for systemd startup
# This ensures kiosk starts even if .bash_profile times out
# Note: Service is in /etc/systemd/system/ (not /usr/lib/)
mkdir -p "${ROOTFS}/etc/systemd/system/multi-user.target.wants"
if [[ -f "${ROOTFS}/etc/systemd/system/secubox-kiosk.service" ]]; then
ln -sf /etc/systemd/system/secubox-kiosk.service \
"${ROOTFS}/etc/systemd/system/multi-user.target.wants/secubox-kiosk.service"
log "Enabled secubox-kiosk.service for systemd startup"
else
err "secubox-kiosk.service not found in /etc/systemd/system/ - kiosk will NOT auto-start!"
fi
# Configure nodm display manager for reliable kiosk startup
cat > "${ROOTFS}/etc/default/nodm" <<'NODM'
# nodm configuration for SecuBox Kiosk
NODM_ENABLED=true
NODM_USER=secubox-kiosk
NODM_FIRST_VT=7
NODM_XSESSION=/home/secubox-kiosk/.xsession
NODM_X_OPTIONS="-nolisten tcp"
NODM_MIN_SESSION_TIME=60
NODM
# Disable nodm systemd service (we use secubox-kiosk.service instead)
chroot "${ROOTFS}" systemctl disable nodm 2>/dev/null || true
ok "nodm configured for kiosk"
# NEW APPROACH: Start kiosk from root's .bash_profile after autologin
# This is more reliable because:
# 1. Console always works first
# 2. User can see errors
# 3. Ctrl+C can stop X11 if it fails
log "Setting up kiosk to start from .bash_profile..."
# Create kiosk startup script (non-blocking)
cat > "${ROOTFS}/usr/local/bin/start-kiosk" <<'KIOSKSTART'
#!/bin/bash
# Start SecuBox Kiosk Mode - NON-BLOCKING
# Fallback for when secubox-kiosk.service doesn't start
# Runs in background, user always gets prompt
# Only run once (check lock file)
[[ -f /tmp/.kiosk-starting ]] && exit 0
# Don't run if systemd kiosk service is already active/activating
if systemctl is-active --quiet secubox-kiosk.service 2>/dev/null; then
echo "[Kiosk] Managed by systemd service"
exit 0
fi
if systemctl is-activating --quiet secubox-kiosk.service 2>/dev/null; then
echo "[Kiosk] Systemd service starting..."
exit 0
fi
touch /tmp/.kiosk-starting
# Check if kiosk is enabled
[[ ! -f /var/lib/secubox/.kiosk-enabled ]] && exit 0
echo "[Kiosk] Starting X11 in background (fallback mode)..."
echo "[Kiosk] Use 'pkill xinit' to stop, Alt+F1 for console"
# Configure X11
mkdir -p /etc/X11/xorg.conf.d
cat > /etc/X11/xorg.conf.d/10-kiosk.conf <<'XCONF'
Section "Device"
Identifier "Kiosk Graphics"
Driver "modesetting"
EndSection
XCONF
# Start X11 in background, switch to VT7 after 3 seconds
(
sleep 2
URL="https://localhost/"
export DISPLAY=:0
xinit /bin/bash -c "
xset s off 2>/dev/null
xset -dpms 2>/dev/null
exec chromium --start-fullscreen --no-first-run --no-sandbox --disable-gpu \
--disable-pinch --noerrdialogs --disable-translate \
--ignore-certificate-errors --window-position=0,0 '$URL'
" -- :0 vt7 -nolisten tcp -keeptty 2>/dev/null &
sleep 3
chvt 7 2>/dev/null
) &
# Return immediately - user gets prompt
KIOSKSTART
chmod +x "${ROOTFS}/usr/local/bin/start-kiosk"
# Add to root's .bash_profile (runs kiosk in background)
cat >> "${ROOTFS}/root/.bash_profile" <<'PROFILE'
# Auto-start kiosk mode if enabled (non-blocking)
if [[ -f /var/lib/secubox/.kiosk-enabled ]] && [[ "$(tty)" == "/dev/tty1" ]]; then
/usr/local/bin/start-kiosk &
fi
PROFILE
# Enable getty on tty2-6 for VT switching (essential for recovery)
# Note: tty1 autologin already configured in override.conf earlier
mkdir -p "${ROOTFS}/etc/systemd/system/getty.target.wants"
for tty in 2 3 4 5 6; do
ln -sf /usr/lib/systemd/system/getty@.service \
"${ROOTFS}/etc/systemd/system/getty.target.wants/getty@tty${tty}.service"
done
log "Getty enabled on tty2-6 for recovery"
ok "Kiosk configured to start from .bash_profile (Ctrl+C to skip)"
else
# ── No kiosk: Enable console TUI mode instead ─────────────────────
log "Kiosk disabled - enabling console TUI mode..."
# Enable secubox-console service if available
if [[ -f "${ROOTFS}/usr/lib/systemd/system/secubox-console.service" ]]; then
ln -sf /usr/lib/systemd/system/secubox-console.service \
"${ROOTFS}/etc/systemd/system/multi-user.target.wants/secubox-console.service"
mkdir -p "${ROOTFS}/var/lib/secubox"
echo "enabled" > "${ROOTFS}/var/lib/secubox/.console-enabled"
ok "Console TUI enabled on tty1"
else
log "secubox-console not installed - standard shell login"
fi
# Note: tty1 autologin already configured in override.conf earlier
# Enable getty on tty2-6 for VT switching
mkdir -p "${ROOTFS}/etc/systemd/system/getty.target.wants"
for tty in 2 3 4 5 6; do
ln -sf /usr/lib/systemd/system/getty@.service \
"${ROOTFS}/etc/systemd/system/getty.target.wants/getty@tty${tty}.service"
done
fi
ok "Scripts installed"
# ══════════════════════════════════════════════════════════════════
# Step 5b: Install to Disk script
# ══════════════════════════════════════════════════════════════════
log "Installing disk installer..."
cat > "${ROOTFS}/usr/bin/secubox-install" <<'INSTALLER'
#!/bin/bash
# ══════════════════════════════════════════════════════════════════
# SecuBox Install to Disk
# Installs the running live system to a target disk
# ══════════════════════════════════════════════════════════════════
set -e
CYAN='\033[0;36m'; GREEN='\033[0;32m'; RED='\033[0;31m'
GOLD='\033[0;33m'; NC='\033[0m'; BOLD='\033[1m'
log() { echo -e "${CYAN}[INSTALL]${NC} $*"; }
ok() { echo -e "${GREEN}[ ✓ ]${NC} $*"; }
err() { echo -e "${RED}[ ✗ ]${NC} $*" >&2; exit 1; }
warn() { echo -e "${GOLD}[ ! ]${NC} $*"; }
# Banner
echo ""
echo -e "${CYAN}${BOLD}╔═══════════════════════════════════════════════════════════════╗${NC}"
echo -e "${CYAN}${BOLD}║ ║${NC}"
echo -e "${CYAN}${BOLD}║ 🖥️ SecuBox — Install to Disk ║${NC}"
echo -e "${CYAN}${BOLD}║ ║${NC}"
echo -e "${CYAN}${BOLD}╚═══════════════════════════════════════════════════════════════╝${NC}"
echo ""
[[ $EUID -ne 0 ]] && err "Must run as root: sudo secubox-install"
# Find available disks
log "Scanning disks..."
DISKS=()
while IFS= read -r line; do
disk=$(echo "$line" | awk '{print $1}')
size=$(echo "$line" | awk '{print $2}')
model=$(echo "$line" | awk '{$1=$2=""; print $0}' | xargs)
# Skip USB boot disk (where we're running from)
mountpoint -q / && ROOT_DEV=$(findmnt -n -o SOURCE /) && \
[[ "$ROOT_DEV" == *"$disk"* ]] && continue
DISKS+=("$disk|$size|$model")
done < <(lsblk -d -n -o NAME,SIZE,MODEL | grep -E '^(sd|nvme|vd)')
[[ ${#DISKS[@]} -eq 0 ]] && err "No available disks found"
echo ""
echo -e "${BOLD}Available disks:${NC}"
echo "─────────────────────────────────────────────────────────────────"
idx=1
for d in "${DISKS[@]}"; do
IFS='|' read -r name size model <<< "$d"
printf " %d) %-12s %-10s %s\n" "$idx" "/dev/$name" "$size" "$model"
((idx++))
done
echo "─────────────────────────────────────────────────────────────────"
echo ""
read -p "Select disk [1-${#DISKS[@]}]: " choice
[[ -z "$choice" || ! "$choice" =~ ^[0-9]+$ ]] && err "Invalid selection"
((choice--))
[[ $choice -lt 0 || $choice -ge ${#DISKS[@]} ]] && err "Invalid selection"
IFS='|' read -r DISK_NAME DISK_SIZE DISK_MODEL <<< "${DISKS[$choice]}"
TARGET="/dev/$DISK_NAME"
echo ""
warn "⚠️ ALL DATA ON $TARGET ($DISK_SIZE - $DISK_MODEL) WILL BE DESTROYED!"
echo ""
read -p "Type 'YES' to confirm: " confirm
[[ "$confirm" != "YES" ]] && err "Installation cancelled"
# Partition the disk
log "Partitioning $TARGET..."
# Unmount any existing partitions
umount ${TARGET}* 2>/dev/null || true
# Create GPT partition table
parted -s "$TARGET" mklabel gpt
# Create partitions: ESP (512MB) + Root (rest - 4GB) + Data (4GB)
# Note: Use -- to stop option parsing (otherwise -4GiB is parsed as options)
parted -s "$TARGET" -- mkpart ESP fat32 1MiB 513MiB
parted -s "$TARGET" set 1 esp on
parted -s "$TARGET" -- mkpart root ext4 513MiB -4GiB
parted -s "$TARGET" -- mkpart data ext4 -4GiB 100%
partprobe "$TARGET"
sleep 2
# Determine partition names
if [[ "$TARGET" == *nvme* ]]; then
PART_ESP="${TARGET}p1"
PART_ROOT="${TARGET}p2"
PART_DATA="${TARGET}p3"
else
PART_ESP="${TARGET}1"
PART_ROOT="${TARGET}2"
PART_DATA="${TARGET}3"
fi
# Format partitions
log "Formatting partitions..."
mkfs.fat -F32 -n "ESP" "$PART_ESP"
mkfs.ext4 -L "secubox" -q "$PART_ROOT"
mkfs.ext4 -L "data" -q "$PART_DATA"
ok "Partitions created"
# Mount and copy system
log "Copying system files (this may take a while)..."
MNT="/mnt/secubox-install"
mkdir -p "$MNT"
mount "$PART_ROOT" "$MNT"
mkdir -p "$MNT/boot/efi" "$MNT/data"
mount "$PART_ESP" "$MNT/boot/efi"
# Copy the live filesystem
rsync -ax --info=progress2 \
--exclude="/proc/*" \
--exclude="/sys/*" \
--exclude="/dev/*" \
--exclude="/run/*" \
--exclude="/tmp/*" \
--exclude="/mnt/*" \
--exclude="/media/*" \
--exclude="/live/*" \
--exclude="/cdrom/*" \
/ "$MNT/"
ok "System files copied"
# Create necessary directories
mkdir -p "$MNT/proc" "$MNT/sys" "$MNT/dev" "$MNT/run" "$MNT/tmp" "$MNT/mnt"
# Generate fstab
log "Configuring fstab..."
ROOT_UUID=$(blkid -s UUID -o value "$PART_ROOT")
ESP_UUID=$(blkid -s UUID -o value "$PART_ESP")
DATA_UUID=$(blkid -s UUID -o value "$PART_DATA")
cat > "$MNT/etc/fstab" <<EOF
# SecuBox fstab - generated by secubox-install
UUID=$ROOT_UUID / ext4 defaults,errors=remount-ro 0 1
UUID=$ESP_UUID /boot/efi vfat umask=0077 0 1
UUID=$DATA_UUID /data ext4 defaults 0 2
EOF
ok "fstab configured"
# Install GRUB bootloader
log "Installing bootloader..."
mount --bind /dev "$MNT/dev"
mount --bind /proc "$MNT/proc"
mount --bind /sys "$MNT/sys"
chroot "$MNT" grub-install --target=x86_64-efi --efi-directory=/boot/efi --bootloader-id=SecuBox --recheck 2>/dev/null || \
warn "EFI GRUB install failed (may be BIOS system)"
# Create fallback bootloader for Lenovo/HP/Dell compatibility (Error 1962 fix)
# These systems only look at /EFI/BOOT/BOOTX64.EFI
if [[ -f "$MNT/boot/efi/EFI/SecuBox/grubx64.efi" ]]; then
mkdir -p "$MNT/boot/efi/EFI/BOOT"
cp "$MNT/boot/efi/EFI/SecuBox/grubx64.efi" "$MNT/boot/efi/EFI/BOOT/BOOTX64.EFI"
ok "Fallback EFI bootloader created (Lenovo/HP/Dell compatible)"
fi
chroot "$MNT" grub-install --target=i386-pc "$TARGET" 2>/dev/null || \
warn "BIOS GRUB install failed (may be EFI-only)"
chroot "$MNT" update-grub
ok "Bootloader installed"
# Cleanup
umount "$MNT/sys" "$MNT/proc" "$MNT/dev"
umount "$MNT/boot/efi"
umount "$MNT"
# Success banner
echo ""
echo -e "${GREEN}${BOLD}╔═══════════════════════════════════════════════════════════════╗${NC}"
echo -e "${GREEN}${BOLD}║ ║${NC}"
echo -e "${GREEN}${BOLD}║ ✅ SecuBox installed successfully! ║${NC}"
echo -e "${GREEN}${BOLD}║ ║${NC}"
echo -e "${GREEN}${BOLD}╚═══════════════════════════════════════════════════════════════╝${NC}"
echo ""
echo " Installed to: $TARGET ($DISK_SIZE)"
echo " Partitions:"
echo " ESP: $PART_ESP (EFI boot)"
echo " Root: $PART_ROOT (system)"
echo " Data: $PART_DATA (persistent data)"
echo ""
echo " Remove the USB drive and reboot to start SecuBox."
echo ""
read -p "Reboot now? [y/N]: " reboot_now
[[ "$reboot_now" =~ ^[Yy]$ ]] && reboot
INSTALLER
chmod +x "${ROOTFS}/usr/bin/secubox-install"
ok "Disk installer installed"
# ══════════════════════════════════════════════════════════════════
# Step 6: Cleanup rootfs
# ══════════════════════════════════════════════════════════════════
log "6/8 Cleaning up rootfs..."
# Preseed
if [[ -n "$PRESEED_FILE" ]] && [[ -f "$PRESEED_FILE" ]]; then
mkdir -p "${ROOTFS}/usr/share/secubox"
cp "$PRESEED_FILE" "${ROOTFS}/usr/share/secubox/preseed.tar.gz"
ok "Preseed included"
fi
# Mask problematic services
for svc in lxc-net lxc; do
chroot "${ROOTFS}" systemctl disable ${svc}.service 2>/dev/null || true
chroot "${ROOTFS}" systemctl mask ${svc}.service 2>/dev/null || true
done
# Mask incomplete/optional SecuBox modules (apps not installed in live image)
# These services are enabled by packages but their backend apps are not installed
log "Masking incomplete SecuBox modules..."
INCOMPLETE_MODULES=(
secubox-jabber
secubox-torrent
secubox-osip
secubox-openclaw
secubox-jellyfin
secubox-localai
secubox-cipher
secubox-lyrion
secubox-peertube
secubox-ollama
secubox-gotosocial
secubox-hexo
secubox-mealie
secubox-webradio
secubox-piobeer
secubox-picobrew
secubox-voip
secubox-zigbee
secubox-newsbin
secubox-ui-manager
secubox-ui-health
# Hardware-gated modules — restart-storm on live USB without the
# physical RTL-SDR + GSM modem present. Operators install + enable
# them by hand once they wire the SDR / EP06.
secubox-sentinelle-gsm
secubox-fmrelay
# LXC-backed modules — their host control plane immediately tries to
# probe the container at 10.100.0.X over the br-lxc bridge. On live
# USB the LXC stack isn't initialised + the container doesn't exist,
# so the service fails the health probe and goes into restart-fail
# loop. Filled the boot console with [FAILED] lines and held up the
# multi-user target. Operators run install-lxc.sh once on a real
# install to provision the container, then `secubox-lxc-modules
# enable <name>` to lift the mask.
secubox-grafana
secubox-yacy
secubox-rustdesk
secubox-lyrion
secubox-mail
secubox-gitea
secubox-matrix
secubox-horde
secubox-mitmproxy
secubox-nextcloud
secubox-rbs-sensor
)
for svc in "${INCOMPLETE_MODULES[@]}"; do
chroot "${ROOTFS}" systemctl disable ${svc}.service 2>/dev/null || true
chroot "${ROOTFS}" systemctl mask ${svc}.service 2>/dev/null || true
done
ok "Incomplete modules masked"
# Ensure squashfs module loads at boot (runtime)
echo "squashfs" >> "${ROOTFS}/etc/modules-load.d/live.conf"
echo "loop" >> "${ROOTFS}/etc/modules-load.d/live.conf"
echo "overlay" >> "${ROOTFS}/etc/modules-load.d/live.conf"
# CRITICAL: Add modules to initramfs for live-boot (must be in initrd, not just modules-load.d)
mkdir -p "${ROOTFS}/etc/initramfs-tools"
cat >> "${ROOTFS}/etc/initramfs-tools/modules" <<'EOFMOD'
# Live-boot required modules
squashfs
loop
overlay
# Filesystem support
ext4
vfat
iso9660
# USB/storage support
usb_storage
uas
sd_mod
# Block layer
dm_mod
# Virtual drivers for VMs
virtio_blk
virtio_scsi
virtio_pci
EOFMOD
# Configure initramfs for live-boot
cat > "${ROOTFS}/etc/initramfs-tools/conf.d/live-boot.conf" <<'EOFLIVE'
# Include most modules for hardware compatibility
MODULES=most
# Compress with gzip for faster boot
COMPRESS=gzip
# Resume disabled for live
RESUME=none
EOFLIVE
# Ensure Plymouth is in initramfs
mkdir -p "${ROOTFS}/etc/initramfs-tools/conf.d"
echo "FRAMEBUFFER=y" > "${ROOTFS}/etc/initramfs-tools/conf.d/plymouth"
# CRITICAL FIX: Force load squashfs module early via init-top script
# Use insmod directly since modprobe requires modules.dep which may not work
mkdir -p "${ROOTFS}/etc/initramfs-tools/scripts/init-top"
cat > "${ROOTFS}/etc/initramfs-tools/scripts/init-top/load-squashfs" <<'EOFSQ'
#!/bin/sh
PREREQ=""
prereqs() { echo "$PREREQ"; }
case "$1" in
prereqs) prereqs; exit 0;;
esac
# Find and load squashfs module using insmod (more reliable than modprobe)
KVER=$(uname -r)
for path in \
/usr/lib/modules/${KVER}/kernel/fs/squashfs/squashfs.ko \
/lib/modules/${KVER}/kernel/fs/squashfs/squashfs.ko \
$(find /usr/lib/modules /lib/modules -name "squashfs.ko*" 2>/dev/null | head -1)
do
if [ -f "$path" ]; then
insmod "$path" 2>/dev/null && break
fi
# Try with .xz or .zst extension
for ext in .xz .zst .gz; do
if [ -f "${path}${ext}" ]; then
# Decompress and load
case "$ext" in
.xz) xz -d -c "${path}${ext}" > /tmp/squashfs.ko && insmod /tmp/squashfs.ko 2>/dev/null && break 2 ;;
.zst) zstd -d -c "${path}${ext}" > /tmp/squashfs.ko && insmod /tmp/squashfs.ko 2>/dev/null && break 2 ;;
.gz) gzip -d -c "${path}${ext}" > /tmp/squashfs.ko && insmod /tmp/squashfs.ko 2>/dev/null && break 2 ;;
esac
fi
done
done
# Fallback to modprobe
modprobe -q squashfs 2>/dev/null || true
modprobe -q loop 2>/dev/null || true
modprobe -q overlay 2>/dev/null || true
EOFSQ
chmod +x "${ROOTFS}/etc/initramfs-tools/scripts/init-top/load-squashfs"
# Also add to hooks to ensure module is copied
mkdir -p "${ROOTFS}/etc/initramfs-tools/hooks"
cat > "${ROOTFS}/etc/initramfs-tools/hooks/live-squashfs" <<'EOFHOOK'
#!/bin/sh
PREREQ=""
prereqs() { echo "$PREREQ"; }
case "$1" in
prereqs) prereqs; exit 0;;
esac
. /usr/share/initramfs-tools/hook-functions
# Ensure squashfs module is included - CRITICAL for live-boot
manual_add_modules squashfs loop overlay
# Also copy the module directly to be safe
copy_modules_dir kernel/fs/squashfs
EOFHOOK
chmod +x "${ROOTFS}/etc/initramfs-tools/hooks/live-squashfs"
# Copy overlay initramfs hooks (v1.6.7.2+ - for advanced partition layout)
if [[ -d "${SCRIPT_DIR}/initramfs" ]]; then
log "Installing overlay initramfs hooks..."
# Copy hooks
if [[ -f "${SCRIPT_DIR}/initramfs/overlay-hooks" ]]; then
cp "${SCRIPT_DIR}/initramfs/overlay-hooks" "${ROOTFS}/etc/initramfs-tools/hooks/"
chmod +x "${ROOTFS}/etc/initramfs-tools/hooks/overlay-hooks"
fi
# Copy init-premount script
mkdir -p "${ROOTFS}/etc/initramfs-tools/scripts/init-premount"
if [[ -f "${SCRIPT_DIR}/initramfs/overlay-init-premount" ]]; then
cp "${SCRIPT_DIR}/initramfs/overlay-init-premount" \
"${ROOTFS}/etc/initramfs-tools/scripts/init-premount/secubox-overlay"
chmod +x "${ROOTFS}/etc/initramfs-tools/scripts/init-premount/secubox-overlay"
fi
# Copy local-bottom script
mkdir -p "${ROOTFS}/etc/initramfs-tools/scripts/local-bottom"
if [[ -f "${SCRIPT_DIR}/initramfs/overlay-local-bottom" ]]; then
cp "${SCRIPT_DIR}/initramfs/overlay-local-bottom" \
"${ROOTFS}/etc/initramfs-tools/scripts/local-bottom/secubox-persist"
chmod +x "${ROOTFS}/etc/initramfs-tools/scripts/local-bottom/secubox-persist"
fi
ok "Overlay initramfs hooks installed"
fi
# Force MODULES=most to include filesystem modules
sed -i 's/^MODULES=.*/MODULES=most/' "${ROOTFS}/etc/initramfs-tools/initramfs.conf" 2>/dev/null || \
echo "MODULES=most" >> "${ROOTFS}/etc/initramfs-tools/initramfs.conf"
# CRITICAL: Run depmod to update modules.dep BEFORE update-initramfs
# This ensures squashfs is properly indexed and can be loaded
log "Running depmod to index kernel modules..."
KVER=$(ls "${ROOTFS}/lib/modules/" | head -1)
chroot "${ROOTFS}" depmod -a "${KVER}" || warn "depmod failed"
# Verify squashfs is in modules.dep
if grep -q squashfs "${ROOTFS}/lib/modules/${KVER}/modules.dep"; then
ok "squashfs in modules.dep"
else
warn "squashfs NOT in modules.dep - adding manually"
echo "kernel/fs/squashfs/squashfs.ko:" >> "${ROOTFS}/lib/modules/${KVER}/modules.dep"
fi
# Regenerate initramfs with live-boot and Plymouth hooks
log "Regenerating initramfs with live-boot hooks..."
chroot "${ROOTFS}" update-initramfs -u -k all || warn "initramfs update failed"
# VERIFY squashfs is in initramfs
log "Verifying squashfs module in initramfs..."
INITRD=$(ls "${ROOTFS}"/boot/initrd.img-* 2>/dev/null | head -1)
if [[ -f "$INITRD" ]]; then
if lsinitramfs "$INITRD" 2>/dev/null | grep -q "squashfs.ko"; then
ok "squashfs module confirmed in initramfs"
else
warn "squashfs NOT in initramfs - forcing rebuild with verbose"
chroot "${ROOTFS}" update-initramfs -u -k all -v 2>&1 | grep -i squash || true
fi
fi
# Clean APT
chroot "${ROOTFS}" apt-get clean
rm -rf "${ROOTFS}/var/lib/apt/lists"/*
rm -rf "${ROOTFS}/var/cache/apt"/*.bin
rm -rf "${ROOTFS}/tmp"/*
# ── CRT-Style Boot Banner with Colors and Emojis ──────────────────────────────
log "Creating colorful boot banners..."
# Pre-login banner (/etc/issue) - ROOT green per Charte §05.
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"
# Post-login MOTD via update-motd.d so the IP can be substituted live
# at login time (pam_motd runs /etc/update-motd.d/* on every interactive
# session). Static /etc/motd kept blank — without it, pam_motd printed
# the literal `<IP>:9443` because nothing was substituting the placeholder.
# Operators see the box's actual IPv4 (or `no-ip` when DHCP hasn't fired).
: > "${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-live-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 ⚡ LIVE USB MODE ⚡ 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"
# Dynamic status script for interactive use
cat > "${ROOTFS}/usr/bin/secubox-status" <<'STATUS_SCRIPT'
#!/bin/bash
# SecuBox Status - CRT-style system overview
# CyberMind — https://cybermind.fr
# Colors
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'
# Status indicators
ok="${GREEN}●${RESET}"
fail="${RED}●${RESET}"
warn="${GOLD}●${RESET}"
# Header
echo -e "${CYAN}"
echo ' ╭──────────────────────────────────────────────────────────╮'
echo ' │ ⚡ SecuBox System Status ⚡ │'
echo ' ╰──────────────────────────────────────────────────────────╯'
echo -e "${RESET}"
# System info
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 (%.1f%%)", $3, $2, $3/$2*100}')"
echo -e " ${GRAY}Disk:${RESET} $(df -h / | awk 'NR==2{printf "%s / %s (%s)", $3, $2, $5}')"
echo ""
# Network info
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)
if [[ -n "$ip_addr" ]]; then
echo -e " ${GREEN}●${RESET} ${GRAY}${iface}:${RESET} ${CYAN}${ip_addr}${RESET}"
fi
done
echo ""
# Core services
echo -e "${WHITE} 🔧 Core Services${RESET}"
services=(nginx haproxy secubox-api secubox-hub crowdsec suricata)
for svc in "${services[@]}"; 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 ""
# Mode detection
echo -e "${WHITE} 🎮 Display Mode${RESET}"
if [[ -f /var/lib/secubox/.kiosk-enabled ]]; then
echo -e " ${ok} ${CYAN}Kiosk GUI${RESET} (Web browser on tty7)"
elif [[ -f /var/lib/secubox/.tui-enabled ]]; then
echo -e " ${ok} ${CYAN}Console TUI${RESET} (Text dashboard on tty1)"
else
echo -e " ${warn} ${CYAN}Console Shell${RESET} (Standard login)"
fi
echo ""
# Quick links
echo -e "${GOLD} ────────────────────────────────────────────────────────────${RESET}"
echo -e "${WHITE} 🔗 Quick Access${RESET}"
IP=$(hostname -I | awk '{print $1}')
echo -e " ${GRAY}Dashboard:${RESET} ${CYAN}https://${IP:-localhost}:9443${RESET}"
echo -e " ${GRAY}Admin API:${RESET} ${CYAN}https://${IP:-localhost}:9443/api/v1/${RESET}"
echo ""
STATUS_SCRIPT
chmod +x "${ROOTFS}/usr/bin/secubox-status"
# Password reset script for TTY recovery
cat > "${ROOTFS}/usr/bin/secubox-passwd" <<'PASSWD_SCRIPT'
#!/bin/bash
# SecuBox Password Reset
set -e
USERS_FILE="/etc/secubox/users.json"
CONF_FILE="/etc/secubox/secubox.conf"
echo "SecuBox Password Reset"
[[ $EUID -ne 0 ]] && echo "Error: Run as root" && exit 1
read -sp "New password for admin: " NEW_PASS && echo
read -sp "Confirm password: " CONFIRM && echo
[[ "$NEW_PASS" != "$CONFIRM" ]] && echo "Passwords don't match" && exit 1
[[ ${#NEW_PASS} -lt 4 ]] && echo "Password too short" && exit 1
NEW_HASH=$(echo -n "$NEW_PASS" | sha256sum | cut -d' ' -f1)
if [[ -f "$USERS_FILE" ]]; then
sed -i "s/\"password_hash\": \"[^\"]*\"/\"password_hash\": \"${NEW_HASH}\"/g" "$USERS_FILE"
echo "Updated users.json"
fi
if [[ -f "$CONF_FILE" ]]; then
sed -i "s/^password = .*/password = \"${NEW_PASS}\"/" "$CONF_FILE"
echo "Updated secubox.conf"
fi
systemctl restart secubox-portal secubox-hub 2>/dev/null || true
echo "Password reset complete!"
PASSWD_SCRIPT
chmod +x "${ROOTFS}/usr/bin/secubox-passwd"
ok "Password reset script installed (secubox-passwd)"
# Also add a boot-time banner display script
cat > "${ROOTFS}/usr/sbin/secubox-boot-banner" <<'BOOT_BANNER'
#!/bin/bash
# SecuBox Boot Banner - Displayed during boot
# CyberMind — https://cybermind.fr
GOLD='\033[38;5;214m'
CYAN='\033[38;5;45m'
GREEN='\033[38;5;82m'
GRAY='\033[38;5;242m'
RESET='\033[0m'
clear
echo -e "${GOLD}"
cat << 'LOGO'
██████ ███████ ██████ ██ ██ ██████ ██████ ██ ██
██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
███████ █████ ██ ██ ██ ██████ ██ ██ ███
██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
███████ ███████ ██████ ██████ ██████ ██████ ██ ██
LOGO
echo -e "${RESET}"
echo -e "${CYAN} ⚡ CyberMind Security Platform${RESET}"
echo -e "${GRAY} Booting...${RESET}"
echo ""
# Show boot progress
show_status() {
local name="$1"
local check="$2"
if eval "$check" 2>/dev/null; then
echo -e " ${GREEN}✓${RESET} ${name}"
else
echo -e " ${GRAY}○${RESET} ${name} (waiting...)"
fi
}
show_status "Network" "ip route | grep -q default"
show_status "Nginx" "systemctl is-active --quiet nginx"
show_status "SecuBox API" "systemctl is-active --quiet secubox-api"
echo ""
echo -e "${GRAY} Dashboard: https://$(hostname -I | awk '{print $1}' || echo 'localhost'):9443${RESET}"
echo ""
BOOT_BANNER
chmod +x "${ROOTFS}/usr/sbin/secubox-boot-banner"
ok "Boot banners created with CRT colors and emojis"
# Unmount
umount -lf "${ROOTFS}/proc" 2>/dev/null || true
umount -lf "${ROOTFS}/sys" 2>/dev/null || true
umount -lf "${ROOTFS}/dev" 2>/dev/null || true
ok "Rootfs cleaned"
# ── Final nginx fix (MUST run after ALL package installs) ──────────────────
log "Final nginx configuration cleanup..."
# Remove only MISPLACED location-only configs from conf.d (a `location` block is
# invalid at http context — it belongs in secubox.d/server). KEEP http-level
# configs: geo / map / limit_req_zone / upstream / log_format legitimately live
# in conf.d and define things other configs depend on. Blindly deleting them
# removed secubox-lan-geo.conf (geo $lan_client), so authelia.conf's
# `if ($lan_client)` hit "unknown lan_client variable" -> nginx -t fails ->
# no web server -> blank kiosk with a connection error.
for conf in "${ROOTFS}/etc/nginx/conf.d/"*secubox*.conf "${ROOTFS}/etc/nginx/conf.d/"*repo*.conf; do
[[ -e "$conf" ]] || [[ -L "$conf" ]] || continue
if grep -qE '^[[:space:]]*location[[:space:]]' "$conf" 2>/dev/null; then
log "Removing misplaced location config from conf.d: $(basename "$conf")"
rm -f "$conf"
fi
done
# Remove ALL broken symlinks in secubox.d (more aggressive cleanup)
log "Removing broken symlinks from secubox.d..."
find "${ROOTFS}/etc/nginx/secubox.d/" -maxdepth 1 -type l ! -exec test -e {} \; -delete 2>/dev/null || true
# Also check for any file that the glob matches but can't be read
for f in "${ROOTFS}/etc/nginx/secubox.d/"*.conf; do
[[ -e "$f" ]] && continue # File exists, skip
[[ -L "$f" ]] && { rm -f "$f"; log "Removed broken symlink: $(basename "$f")"; }
done
# Create empty placeholder if secubox-repo.conf is missing (some packages reference it)
if [[ ! -f "${ROOTFS}/etc/nginx/secubox.d/secubox-repo.conf" ]] && \
[[ ! -f "${ROOTFS}/etc/nginx/secubox.d/repo.conf" ]]; then
# Create minimal repo config to satisfy nginx
cat > "${ROOTFS}/etc/nginx/secubox.d/repo.conf" << 'REPOCONF'
# Placeholder - repo module not installed
REPOCONF
log "Created placeholder repo.conf"
fi
# Verify nginx config is valid
if [[ -x "${ROOTFS}/usr/sbin/nginx" ]]; then
if ! chroot "${ROOTFS}" nginx -t 2>&1 | grep -q "syntax is ok"; then
warn "nginx config still invalid after final cleanup (regenerated at first boot)"
# Show the error and try to fix. `|| true`: nginx -t returns non-zero here
# (config IS invalid — that's why we're in this branch), so without it the
# command substitution trips set -e/pipefail and aborts the whole build
# right after this warn. The image's nginx config is rebuilt at first boot
# by secubox-net-detect, so a build-time-invalid config is non-fatal.
nginx_error=$(chroot "${ROOTFS}" nginx -t 2>&1 | head -5 || true)
echo "$nginx_error"
# Extract missing file from error message and create empty config
missing_file=$(echo "$nginx_error" | grep -oP '"/etc/nginx/secubox\.d/\K[^"]+' || true)
if [[ -n "$missing_file" ]]; then
log "Creating missing config: $missing_file"
touch "${ROOTFS}/etc/nginx/secubox.d/${missing_file}"
fi
else
ok "Final nginx configuration valid"
fi
fi
# ── Final permission fixes (MUST be done after all setup, before squashfs) ──
log "Final permission fixes..."
# Ensure www directory exists with fallback index.html
mkdir -p "${ROOTFS}/usr/share/secubox/www"
if [[ ! -f "${ROOTFS}/usr/share/secubox/www/index.html" ]]; then
log "Creating fallback index.html..."
cat > "${ROOTFS}/usr/share/secubox/www/index.html" <<'FALLBACK_HTML'
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SecuBox Live</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Segoe UI', system-ui, sans-serif;
background: linear-gradient(135deg, #0a0a0f 0%, #1a1a2e 50%, #0a0a0f 100%);
color: #e8e6d9;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
.container {
text-align: center;
padding: 2rem;
max-width: 600px;
}
h1 {
font-size: 3rem;
color: #c9a84c;
margin-bottom: 1rem;
text-shadow: 0 0 20px rgba(201, 168, 76, 0.3);
}
.subtitle {
font-size: 1.2rem;
color: #00d4ff;
margin-bottom: 2rem;
}
.status {
background: rgba(0, 212, 255, 0.1);
border: 1px solid #00d4ff;
border-radius: 8px;
padding: 1.5rem;
margin-bottom: 2rem;
}
.status h2 {
color: #00ff41;
margin-bottom: 0.5rem;
}
.info {
color: #6b6b7a;
font-size: 0.9rem;
line-height: 1.6;
}
.info a {
color: #c9a84c;
text-decoration: none;
}
.info a:hover {
text-decoration: underline;
}
</style>
</head>
<body>
<div class="container">
<h1>⚡ SecuBox</h1>
<p class="subtitle">CyberMind Security Platform</p>
<div class="status">
<h2>✓ System Running</h2>
<p>SecuBox Live USB is operational</p>
</div>
<div class="info">
<p>Access the full dashboard at <a href="https://localhost/">https://localhost/</a></p>
<p>Default credentials: admin / admin</p>
<p><br>Console: Press Ctrl+Alt+F2 for terminal</p>
</div>
</div>
</body>
</html>
FALLBACK_HTML
ok "Fallback index.html created"
fi
# Fix www directory ownership (nginx runs as www-data)
if [[ -d "${ROOTFS}/usr/share/secubox/www" ]]; then
chown -R root:root "${ROOTFS}/usr/share/secubox/www"
chmod -R 755 "${ROOTFS}/usr/share/secubox/www"
find "${ROOTFS}/usr/share/secubox/www" -type f -exec chmod 644 {} \;
log "Fixed www directory: $(ls -ld ${ROOTFS}/usr/share/secubox/www)"
fi
# Fix /etc/secubox ownership
if [[ -d "${ROOTFS}/etc/secubox" ]]; then
chown -R root:root "${ROOTFS}/etc/secubox"
chmod 755 "${ROOTFS}/etc/secubox"
chmod 755 "${ROOTFS}/etc/secubox/tls" 2>/dev/null || true
chmod 644 "${ROOTFS}/etc/secubox/tls/cert.pem" 2>/dev/null || true
chmod 640 "${ROOTFS}/etc/secubox/tls/key.pem" 2>/dev/null || true
# Make key readable by www-data
chgrp www-data "${ROOTFS}/etc/secubox/tls/key.pem" 2>/dev/null || true
fi
# Fix nginx secubox.d
if [[ -d "${ROOTFS}/etc/nginx/secubox.d" ]]; then
chmod 755 "${ROOTFS}/etc/nginx/secubox.d"
chmod 644 "${ROOTFS}/etc/nginx/secubox.d/"*.conf 2>/dev/null || true
fi
ok "Permissions fixed"
# ══════════════════════════════════════════════════════════════════
# Step 7: Create SquashFS
# ══════════════════════════════════════════════════════════════════
# Unmount special filesystems before creating squashfs
log "Unmounting chroot filesystems..."
umount -lf "${ROOTFS}/dev/pts" 2>/dev/null || true
sync
sleep 1
umount -f "${ROOTFS}/dev" 2>/dev/null || umount -lf "${ROOTFS}/dev" 2>/dev/null || true
umount -lf "${ROOTFS}/proc" 2>/dev/null || true
umount -lf "${ROOTFS}/sys" 2>/dev/null || true
log "7/8 Creating SquashFS filesystem..."
mkdir -p "${LIVE_DIR}/live"
# Remove the build-time service-deny shim so the booted system starts services.
rm -f "${ROOTFS}/usr/sbin/policy-rc.d"
mksquashfs "${ROOTFS}" "${LIVE_DIR}/live/filesystem.squashfs" \
-comp xz -b 1M -Xdict-size 100% -e boot/grub -e boot/efi
cp "${ROOTFS}/boot/vmlinuz-"* "${LIVE_DIR}/live/vmlinuz"
cp "${ROOTFS}/boot/initrd.img-"* "${LIVE_DIR}/live/initrd.img"
# Create filesystem.size (required by some live-boot versions)
du -s "${ROOTFS}" | cut -f1 > "${LIVE_DIR}/live/filesystem.size"
# Create filesystem.packages list (read from dpkg status file since chroot not available)
awk '/^Package:/{pkg=$2} /^Version:/{print pkg,$2}' "${ROOTFS}/var/lib/dpkg/status" > "${LIVE_DIR}/live/filesystem.packages" 2>/dev/null || true
SQUASHFS_SIZE=$(du -sh "${LIVE_DIR}/live/filesystem.squashfs" | cut -f1)
ok "SquashFS: ${SQUASHFS_SIZE}"
# ══════════════════════════════════════════════════════════════════
# Step 8: Create bootable image
# ══════════════════════════════════════════════════════════════════
log "8/8 Creating bootable image (${IMG_SIZE})..."
# Delete old image
rm -f "${IMG_FILE}" "${IMG_FILE}.gz"
# Create image
truncate -s "${IMG_SIZE}" "${IMG_FILE}"
# Calculate partition sizes dynamically
# Convert IMG_SIZE to MiB (e.g., "4G" -> 4096, "8G" -> 8192)
IMG_SIZE_NUM="${IMG_SIZE%[GgMm]}"
IMG_SIZE_UNIT="${IMG_SIZE: -1}"
if [[ "$IMG_SIZE_UNIT" == "G" ]] || [[ "$IMG_SIZE_UNIT" == "g" ]]; then
IMG_SIZE_MIB=$((IMG_SIZE_NUM * 1024))
else
IMG_SIZE_MIB=$IMG_SIZE_NUM
fi
# Partition layout:
# 1: BIOS boot (2MB) - legacy grub
# 2: ESP (512MB) - UEFI
# 3: LIVE - squashfs + grub + kernels (dynamic, leaves room for persistence)
# 4: persistence (optional, ~1/3 of total for 8G+, or rest)
#
# OVERLAY MODE (v1.6.7.2+):
# 1: BIOS boot (2MB)
# 2: ESP (512MB)
# 3: SYSTEM (2GB) - squashfs read-only base
# 4: CONFIG (512MB) - persistent config
# 5: DATA (2GB) - persistent data/logs
# 6: SNAPSHOTS (1GB) - versioned backups
# 7: SWAP (512MB)
ESP_END=515 # 3 + 512 = 515 MiB
if [[ $OVERLAY_MODE -eq 1 ]]; then
# Advanced overlay partition layout (v1.6.7.2+)
# Requires minimum 8GB image
if [[ $IMG_SIZE_MIB -lt 7168 ]]; then
warn "Overlay mode requires at least 7GB image, got ${IMG_SIZE}"
warn "Falling back to standard layout"
OVERLAY_MODE=0
else
# Calculate partition boundaries
SYSTEM_END=$((ESP_END + 2048)) # 2GB for squashfs
CONFIG_END=$((SYSTEM_END + 512)) # 512MB for config
DATA_END=$((CONFIG_END + 2048)) # 2GB for data
SNAP_END=$((DATA_END + 1024)) # 1GB for snapshots
# SWAP uses remaining space (at least 512MB)
log "Overlay partition layout (v1.6.7.2):"
log " ESP: 3-${ESP_END}MiB (512MB)"
log " SYSTEM: ${ESP_END}-${SYSTEM_END}MiB (2GB)"
log " CONFIG: ${SYSTEM_END}-${CONFIG_END}MiB (512MB)"
log " DATA: ${CONFIG_END}-${DATA_END}MiB (2GB)"
log " SNAPSHOTS: ${DATA_END}-${SNAP_END}MiB (1GB)"
log " SWAP: ${SNAP_END}MiB-100%"
parted -s "${IMG_FILE}" \
mklabel gpt \
mkpart bios 1MiB 3MiB \
mkpart ESP fat32 3MiB ${ESP_END}MiB \
mkpart SYSTEM ext4 ${ESP_END}MiB ${SYSTEM_END}MiB \
mkpart CONFIG ext4 ${SYSTEM_END}MiB ${CONFIG_END}MiB \
mkpart DATA ext4 ${CONFIG_END}MiB ${DATA_END}MiB \
mkpart SNAPSHOTS ext4 ${DATA_END}MiB ${SNAP_END}MiB \
mkpart SWAP linux-swap ${SNAP_END}MiB 100% \
set 1 bios_grub on \
set 2 esp on \
set 2 boot on
fi
fi
if [[ $OVERLAY_MODE -ne 1 ]] && [[ $INCLUDE_PERSISTENCE -eq 1 ]]; then
# For persistence, leave 25% of image for persistence (min 512MB)
PERSIST_SIZE=$(( IMG_SIZE_MIB / 4 ))
[[ $PERSIST_SIZE -lt 512 ]] && PERSIST_SIZE=512
LIVE_END=$(( IMG_SIZE_MIB - PERSIST_SIZE - 2 )) # -2 for GPT backup
# Sanity check: LIVE must be at least 1GB for squashfs
[[ $LIVE_END -lt 1539 ]] && LIVE_END=1539
log "Partitions: ESP ${ESP_END}MiB, LIVE ${ESP_END}-${LIVE_END}MiB, Persist ${LIVE_END}MiB-100%"
parted -s "${IMG_FILE}" \
mklabel gpt \
mkpart bios 1MiB 3MiB \
mkpart ESP fat32 3MiB ${ESP_END}MiB \
mkpart LIVE ext4 ${ESP_END}MiB ${LIVE_END}MiB \
mkpart persistence ext4 ${LIVE_END}MiB 100% \
set 1 bios_grub on \
set 2 esp on \
set 2 boot on
else
log "Partitions: ESP ${ESP_END}MiB, LIVE ${ESP_END}MiB-100%"
parted -s "${IMG_FILE}" \
mklabel gpt \
mkpart bios 1MiB 3MiB \
mkpart ESP fat32 3MiB ${ESP_END}MiB \
mkpart LIVE ext4 ${ESP_END}MiB 100% \
set 1 bios_grub on \
set 2 esp on \
set 2 boot on
fi
# Setup loop
LOOP=$(losetup -f --show -P "${IMG_FILE}")
log "Loop: ${LOOP}"
# Wait for partitions
sleep 1
partprobe "${LOOP}" 2>/dev/null || true
sleep 1
# Verify partitions exist
[[ -b "${LOOP}p2" ]] || err "Partition ${LOOP}p2 not found"
# Format partitions based on mode
if [[ $OVERLAY_MODE -eq 1 ]]; then
log "Formatting overlay partitions..."
mkfs.fat -F32 -n ESP "${LOOP}p2"
mkfs.ext4 -L SYSTEM -q "${LOOP}p3"
mkfs.ext4 -L CONFIG -q "${LOOP}p4"
mkfs.ext4 -L DATA -q "${LOOP}p5"
mkfs.ext4 -L SNAPSHOTS -q "${LOOP}p6"
mkswap -L SWAP "${LOOP}p7"
# Create initial directory structure on CONFIG
MNT_CONFIG="${WORK_DIR}/mnt-config"
mkdir -p "${MNT_CONFIG}"
mount "${LOOP}p4" "${MNT_CONFIG}"
mkdir -p "${MNT_CONFIG}/etc/secubox"
mkdir -p "${MNT_CONFIG}/etc/nginx/secubox.d"
mkdir -p "${MNT_CONFIG}/etc/systemd/system"
mkdir -p "${MNT_CONFIG}/home"
umount "${MNT_CONFIG}"
# Create initial directory structure on DATA
MNT_DATA="${WORK_DIR}/mnt-data"
mkdir -p "${MNT_DATA}"
mount "${LOOP}p5" "${MNT_DATA}"
mkdir -p "${MNT_DATA}/var/lib/secubox"
mkdir -p "${MNT_DATA}/var/log/secubox"
mkdir -p "${MNT_DATA}/var/cache/secubox"
mkdir -p "${MNT_DATA}/srv/secubox"
touch "${MNT_DATA}/var/lib/secubox/.firstboot"
umount "${MNT_DATA}"
# Create factory snapshot directory
MNT_SNAP="${WORK_DIR}/mnt-snap"
mkdir -p "${MNT_SNAP}"
mount "${LOOP}p6" "${MNT_SNAP}"
mkdir -p "${MNT_SNAP}/factory"
echo '{"name":"factory","timestamp":"'$(date -Iseconds)'","type":"factory","version":"'${SECUBOX_VERSION}'"}' \
> "${MNT_SNAP}/factory/metadata.json"
umount "${MNT_SNAP}"
ok "Overlay partitions formatted"
else
# Standard partition format
mkfs.fat -F32 -n ESP "${LOOP}p2"
mkfs.ext4 -L LIVE -q "${LOOP}p3"
if [[ $INCLUDE_PERSISTENCE -eq 1 ]] && [[ -b "${LOOP}p4" ]]; then
mkfs.ext4 -L persistence -q "${LOOP}p4"
fi
fi
# Mount for file copy
MNT="${WORK_DIR}/mnt"
mkdir -p "${MNT}/esp" "${MNT}/live"
mount "${LOOP}p2" "${MNT}/esp"
if [[ $OVERLAY_MODE -eq 1 ]]; then
mount "${LOOP}p3" "${MNT}/live" # SYSTEM partition
else
mount "${LOOP}p3" "${MNT}/live"
fi
# Copy live files to root of LIVE partition
# GRUB looks for ($live)/live/* and live-boot looks for /live/filesystem.squashfs
mkdir -p "${MNT}/live/live"
cp "${LIVE_DIR}/live/filesystem.squashfs" "${MNT}/live/live/"
cp "${LIVE_DIR}/live/vmlinuz" "${MNT}/live/live/"
cp "${LIVE_DIR}/live/initrd.img" "${MNT}/live/live/"
cp "${LIVE_DIR}/live/filesystem.size" "${MNT}/live/live/" 2>/dev/null || true
cp "${LIVE_DIR}/live/filesystem.packages" "${MNT}/live/live/" 2>/dev/null || true
# Setup ESP
mkdir -p "${MNT}/esp/EFI/BOOT"
mkdir -p "${MNT}/esp/boot/grub/x86_64-efi"
mkdir -p "${MNT}/esp/boot/grub/i386-pc"
# Also copy to ESP for some UEFI
mkdir -p "${MNT}/esp/live"
cp "${LIVE_DIR}/live/vmlinuz" "${MNT}/esp/live/"
cp "${LIVE_DIR}/live/initrd.img" "${MNT}/esp/live/"
# GRUB config - Dynamic based on build options
# When kiosk is enabled, default to Kiosk GUI (entry 1), otherwise standard boot (entry 0)
if [[ "${INCLUDE_KIOSK:-1}" == "1" ]]; then
GRUB_DEFAULT=1 # Kiosk GUI is second entry
log "GRUB default: Kiosk GUI mode"
else
GRUB_DEFAULT=0 # Standard boot
log "GRUB default: Standard boot"
fi
cat > "${MNT}/esp/boot/grub/grub.cfg" <<GRUBCFG
set default=${GRUB_DEFAULT}
set timeout=5
insmod part_gpt
insmod fat
insmod ext2
insmod all_video
insmod echo
insmod gfxterm
search --no-floppy --label LIVE --set=live
# CRT-style menu colors (cyan on black, gold highlights)
set menu_color_normal=cyan/black
set menu_color_highlight=yellow/blue
# Version header
set pager=1
echo "SecuBox v${SECUBOX_VERSION} - Build ${BUILD_TIMESTAMP}"
echo ""
menuentry "⚡ SecuBox Live v${SECUBOX_VERSION}" --class secubox {
echo "SecuBox Live: Console mode with persistence"
echo "Access via: SSH or Web UI at https://<ip>:443"
linux (\$live)/live/vmlinuz boot=live live-media-path=/live rootdelay=10 components persistence quiet splash
initrd (\$live)/live/initrd.img
}
menuentry "🖼️ SecuBox Live v${SECUBOX_VERSION} (Kiosk GUI) [DEFAULT]" --class secubox {
echo "SecuBox Kiosk: Fullscreen GUI on HDMI/display"
echo "Touch/mouse friendly dashboard interface"
linux (\$live)/live/vmlinuz boot=live live-media-path=/live rootdelay=10 components persistence quiet splash secubox.kiosk=1
initrd (\$live)/live/initrd.img
}
GRUBCFG
# Append the rest of the menu entries with emoji indicators
cat >> "${MNT}/esp/boot/grub/grub.cfg" <<'GRUBCFG'
menuentry "📟 SecuBox Live (Console TUI)" --class secubox {
echo "Text User Interface mode - keyboard navigation"
echo "Low resource usage, works on any display"
linux ($live)/live/vmlinuz boot=live live-media-path=/live rootdelay=10 components persistence quiet secubox.mode=tui
initrd ($live)/live/initrd.img
}
menuentry "🌉 SecuBox Live (Bridge Mode)" --class secubox {
echo "Bridge Mode: Transparent inline sniffer"
echo "Traffic passes through for monitoring/analysis"
linux ($live)/live/vmlinuz boot=live live-media-path=/live rootdelay=10 components persistence quiet secubox.netmode=bridge
initrd ($live)/live/initrd.img
}
menuentry "🛡️ SecuBox Live (Safe Mode)" --class secubox {
echo "Safe Mode: Basic video driver, no persistence"
echo "Use if graphics issues prevent normal boot"
linux ($live)/live/vmlinuz boot=live live-media-path=/live rootdelay=10 components nomodeset console=tty0
initrd ($live)/live/initrd.img
}
menuentry "💾 Install SecuBox to Disk" --class secubox {
echo "Install SecuBox to internal drive"
echo "WARNING: This will erase the target disk!"
linux ($live)/live/vmlinuz boot=live live-media-path=/live rootdelay=10 components nomodeset console=tty0 secubox.install=1
initrd ($live)/live/initrd.img
}
menuentry "🚀 SecuBox Live (To RAM)" --class secubox {
echo "Load entire system into RAM"
echo "Faster operation, USB can be removed after boot"
linux ($live)/live/vmlinuz boot=live live-media-path=/live rootdelay=10 components toram quiet
initrd ($live)/live/initrd.img
}
menuentry "🔧 SecuBox Live (HW Check)" {
linux ($live)/live/vmlinuz boot=live live-media-path=/live rootdelay=10 components persistence quiet secubox.hwcheck=1
initrd ($live)/live/initrd.img
}
menuentry "🔧 SecuBox Live (HW Check - Text)" {
linux ($live)/live/vmlinuz boot=live live-media-path=/live rootdelay=10 components persistence nomodeset console=tty0 secubox.hwcheck=1
initrd ($live)/live/initrd.img
}
menuentry "🚨 Emergency Shell" {
linux ($live)/live/vmlinuz boot=live live-media-path=/live rootdelay=10 components nomodeset console=tty0 systemd.unit=emergency.target
initrd ($live)/live/initrd.img
}
menuentry "🐛 Debug (Verbose Boot)" {
linux ($live)/live/vmlinuz boot=live live-media-path=/live rootdelay=10 components nomodeset console=tty0 debug=1 break=init
initrd ($live)/live/initrd.img
}
menuentry "🐛 Debug (Break at Premount)" {
linux ($live)/live/vmlinuz boot=live live-media-path=/live rootdelay=10 components nomodeset console=tty0 debug=1 break=premount
initrd ($live)/live/initrd.img
}
GRUBCFG
# Add overlay-specific boot entries if overlay mode is enabled
if [[ $OVERLAY_MODE -eq 1 ]]; then
cat >> "${MNT}/esp/boot/grub/grub.cfg" <<'GRUBCFG_OVERLAY'
submenu "🔧 Overlay Advanced Options" {
menuentry "💾 SecuBox (RAM Only - No Persistence)" {
linux ($live)/live/vmlinuz boot=live live-media-path=/live rootdelay=10 components quiet splash secubox.persist=no
initrd ($live)/live/initrd.img
}
menuentry "🔄 SecuBox (Factory Reset)" {
linux ($live)/live/vmlinuz boot=live live-media-path=/live rootdelay=10 components quiet splash secubox.factory-reset=1
initrd ($live)/live/initrd.img
}
menuentry "📸 SecuBox (Recovery from Snapshot)" {
linux ($live)/live/vmlinuz boot=live live-media-path=/live rootdelay=10 components nomodeset console=tty0 secubox.recovery=1
initrd ($live)/live/initrd.img
}
}
GRUBCFG_OVERLAY
log "Added overlay boot menu entries"
fi
cp "${MNT}/esp/boot/grub/grub.cfg" "${MNT}/esp/EFI/BOOT/grub.cfg"
# Build GRUB EFI — verbatim from v2.10.3 (last known-good real UEFI
# boot). Do NOT optimise this block: every "improvement" attempted in
# the v2.12.x series (Secure Boot shim, $cmdpath, search --fs-uuid,
# extra modules, multi-stage fallbacks) broke at least one piece of
# hardware. Stay simple, stay shipped.
GRUB_MODS="part_gpt part_msdos fat ext2 normal linux boot configfile loopback chain efi_gop efi_uga ls search search_label gfxterm all_video"
cat > "${WORK_DIR}/grub-embed.cfg" <<'EMBEDCFG'
search --no-floppy --label ESP --set=root
set prefix=($root)/boot/grub
configfile $prefix/grub.cfg
EMBEDCFG
grub-mkimage -o "${MNT}/esp/EFI/BOOT/BOOTX64.EFI" \
-O x86_64-efi \
-c "${WORK_DIR}/grub-embed.cfg" \
-p /boot/grub \
${GRUB_MODS}
cp "${MNT}/esp/EFI/BOOT/BOOTX64.EFI" "${MNT}/esp/EFI/BOOT/grubx64.efi"
# Add startup.nsh for EFI shell auto-boot (VirtualBox/OVMF compatibility)
cat > "${MNT}/esp/startup.nsh" <<'STARTUPNSH'
@echo -off
\EFI\BOOT\BOOTX64.EFI
STARTUPNSH
# Copy GRUB modules
cp /usr/lib/grub/x86_64-efi/*.mod "${MNT}/esp/boot/grub/x86_64-efi/" 2>/dev/null || true
# Install BIOS GRUB
grub-install --target=i386-pc --boot-directory="${MNT}/esp/boot" --recheck "${LOOP}" 2>/dev/null || warn "BIOS GRUB failed"
cp /usr/lib/grub/i386-pc/*.mod "${MNT}/esp/boot/grub/i386-pc/" 2>/dev/null || true
ok "GRUB installed (UEFI + BIOS)"
# Persistence
if [[ $INCLUDE_PERSISTENCE -eq 1 ]] && [[ -b "${LOOP}p4" ]]; then
mkdir -p "${MNT}/pers"
mount "${LOOP}p4" "${MNT}/pers"
echo "/ union" > "${MNT}/pers/persistence.conf"
umount "${MNT}/pers"
ok "Persistence configured"
fi
# Sync and unmount
sync
umount "${MNT}/esp"
umount "${MNT}/live"
losetup -d "${LOOP}"
LOOP=""
ok "Bootable image created"
# ══════════════════════════════════════════════════════════════════
# Compress (optional)
# ══════════════════════════════════════════════════════════════════
if [[ $NO_COMPRESS -eq 0 ]]; then
log "Compressing..."
gzip -9 -f "${IMG_FILE}"
sha256sum "${IMG_FILE}.gz" > "${IMG_FILE}.gz.sha256"
FINAL_SIZE=$(du -sh "${IMG_FILE}.gz" | cut -f1)
FINAL_IMG="${IMG_FILE}.gz"
FLASH_CMD="zcat ${IMG_FILE}.gz | sudo dd of=/dev/sdX bs=4M status=progress"
else
log "Skipping compression (--no-compress)"
sha256sum "${IMG_FILE}" > "${IMG_FILE}.sha256"
FINAL_SIZE=$(du -sh "${IMG_FILE}" | cut -f1)
FINAL_IMG="${IMG_FILE}"
FLASH_CMD="sudo dd if=${IMG_FILE} of=/dev/sdX bs=4M status=progress"
fi
echo ""
echo -e "${GREEN}${BOLD}════════════════════════════════════════════════════════════${NC}"
echo -e "${GREEN}${BOLD} SecuBox Live USB Ready!${NC}"
echo ""
echo -e " Image: ${FINAL_IMG}"
echo -e " Size: ${FINAL_SIZE}"
echo ""
echo -e " ${BOLD}Flash:${NC}"
echo -e " ${FLASH_CMD}"
echo ""
echo -e " ${BOLD}Credentials:${NC}"
echo -e " SSH/Console: root / secubox"
echo -e " Web UI: https://localhost (or real LAN IP)"
echo ""
echo -e "${GREEN}${BOLD}════════════════════════════════════════════════════════════${NC}"