feat: LUKS2 encryption, boot sequence fixes, onboarding auth, CI/CD
Some checks failed
Build Archipelago ISO / build-iso (push) Has been cancelled

- LUKS2 full-partition encryption for /var/lib/archipelago/ (TASK-42)
  4-partition layout: BIOS + EFI + root (30GB) + encrypted data
  AES-256-XTS with AES-NI detection, ChaCha20 fallback for ARM
  Auto-unlock via crypttab + random key file

- Fix EFI boot errors: remove shim-signed, clean shim artifacts
- Fix first-boot sequence: always show boot animation before onboarding
- Fix stale localStorage causing login instead of onboarding (BUG-47)

- Add auth.setup + auth.isSetup RPC handlers for password on clean install
- Add onboarding methods to UNAUTHENTICATED_METHODS (DID sign 403 fix)

- FileBrowser bundled in unbundled ISO, fix auto-login Secure cookie (BUG-46)
- Kiosk mode: xorg/chromium in rootfs, toggle script, MOTD instructions

- Add Gitea Actions CI/CD workflow for automatic ISO builds

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-26 09:12:16 +00:00
parent 5c15c52113
commit 08bb2c80d4
8 changed files with 463 additions and 52 deletions

View File

@@ -214,7 +214,7 @@ RUN apt-get update && apt-get install -y \
${LINUX_IMAGE_PKG} \
${GRUB_EFI_PKG} \
${GRUB_EFI_SIGNED_PKG} \
${GRUB_PC_PKG} shim-signed \
${GRUB_PC_PKG} \
systemd \
systemd-sysv \
dbus \
@@ -235,11 +235,15 @@ RUN apt-get update && apt-get install -y \
locales \
console-setup \
keyboard-configuration \
cryptsetup \
firmware-realtek \
firmware-iwlwifi \
firmware-misc-nonfree \
intel-microcode \
amd64-microcode \
xorg \
chromium \
unclutter \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
@@ -611,10 +615,25 @@ fi
echo ""
if [ "$UNBUNDLED" = "1" ]; then
echo "📦 Step 3b: SKIPPING container image bundling (UNBUNDLED mode)"
echo " Apps will be downloaded on-demand from the Marketplace after install."
echo "📦 Step 3b: Bundling core containers only (UNBUNDLED mode)"
echo " Optional apps will be downloaded on-demand from the Marketplace after install."
IMAGES_DIR="$ARCH_DIR/container-images"
mkdir -p "$IMAGES_DIR"
# FileBrowser is a core dependency (powers the Cloud file manager) — always bundle it
CORE_IMAGE="${FILEBROWSER_IMAGE:-docker.io/filebrowser/filebrowser:v2}"
CORE_FILE="filebrowser.tar"
if [ -f "$IMAGES_DIR/$CORE_FILE" ]; then
echo " ✅ Using cached: $CORE_FILE"
else
echo " Pulling $CORE_IMAGE ($CONTAINER_PLATFORM)..."
if $CONTAINER_CMD pull --platform $CONTAINER_PLATFORM "$CORE_IMAGE"; then
$CONTAINER_CMD save "$CORE_IMAGE" -o "$IMAGES_DIR/$CORE_FILE" 2>/dev/null && \
echo " ✅ Saved core: $CORE_FILE ($(du -h "$IMAGES_DIR/$CORE_FILE" | cut -f1))" || \
echo " ⚠️ Failed to save $CORE_IMAGE"
else
echo " ⚠️ Failed to pull $CORE_IMAGE — Cloud will not work until installed"
fi
fi
else
echo "📦 Step 3b: Bundling container images for offline use..."
@@ -877,9 +896,58 @@ cp "$WORK_DIR/setup-tor.sh" "$ARCH_DIR/scripts/"
cp "$WORK_DIR/archipelago-setup-tor.service" "$ARCH_DIR/scripts/"
# First-boot: create core containers (bitcoin, mempool, btcpay, lnd, fedimint, homeassistant)
# Skip for unbundled builds — no images pre-loaded, users install from Marketplace
# Unbundled builds only create FileBrowser (core dependency for Cloud)
if [ "$UNBUNDLED" = "1" ]; then
echo " Skipping first-boot containers (UNBUNDLED: apps installed from Marketplace)"
echo " Creating minimal first-boot service (UNBUNDLED: FileBrowser only)..."
# Create a minimal first-boot script that only starts FileBrowser
cat > "$WORK_DIR/first-boot-containers-unbundled.sh" <<'FBUNBUNDLED'
#!/bin/bash
# Minimal first-boot: create FileBrowser container only (unbundled ISO)
set -e
DOCKER="podman"
LOG="/var/log/archipelago-first-boot.log"
echo "[$(date)] Starting minimal first-boot (unbundled)..." >> "$LOG"
# Create Cloud storage directories
mkdir -p /var/lib/archipelago/filebrowser
mkdir -p /var/lib/archipelago/data/cloud/{Documents,Photos,Music,Videos,Downloads}
chown -R 1000:1000 /var/lib/archipelago/filebrowser
chown -R 1000:1000 /var/lib/archipelago/data
if ! $DOCKER ps -a --format '{{.Names}}' 2>/dev/null | grep -q filebrowser; then
echo "[$(date)] Creating FileBrowser container..." >> "$LOG"
$DOCKER run -d --name filebrowser --restart unless-stopped \
--health-cmd="curl -sf http://localhost:80/ || exit 1" \
--health-interval=30s --health-timeout=5s --health-retries=3 \
--memory=256m \
-p 8083:80 \
-v /var/lib/archipelago/filebrowser:/srv \
docker.io/filebrowser/filebrowser:v2 2>>"$LOG" && \
echo "[$(date)] FileBrowser created successfully" >> "$LOG" || \
echo "[$(date)] WARNING: FileBrowser creation failed" >> "$LOG"
fi
echo "[$(date)] Minimal first-boot complete" >> "$LOG"
FBUNBUNDLED
chmod +x "$WORK_DIR/first-boot-containers-unbundled.sh"
cp "$WORK_DIR/first-boot-containers-unbundled.sh" "$ARCH_DIR/scripts/first-boot-containers.sh"
cat > "$WORK_DIR/archipelago-first-boot-containers.service" <<'FBCSERVICE'
[Unit]
Description=Create core Archipelago containers on first boot
After=archipelago-setup-tor.service network-online.target podman.service
ConditionPathExists=/opt/archipelago/scripts/first-boot-containers.sh
ConditionPathExists=!/var/lib/archipelago/.first-boot-containers-done
[Service]
Type=oneshot
ExecStart=/opt/archipelago/scripts/first-boot-containers.sh
ExecStartPost=/usr/bin/touch /var/lib/archipelago/.first-boot-containers-done
RemainAfterExit=yes
[Install]
WantedBy=multi-user.target
FBCSERVICE
cp "$WORK_DIR/archipelago-first-boot-containers.service" "$ARCH_DIR/scripts/"
else
echo " Creating first-boot container creation service..."
# Copy shared script library
@@ -1112,8 +1180,8 @@ echo ""
umount ${TARGET_DISK}* 2>/dev/null || true
umount ${TARGET_DISK}p* 2>/dev/null || true
# Create partition table — dual BIOS+UEFI boot support
echo " [1/6] Creating partitions..."
# Create partition table — dual BIOS+UEFI boot + LUKS2 encrypted data
echo " [1/7] Creating partitions..."
parted -s "$TARGET_DISK" mklabel gpt
# Partition 1: 1MB BIOS boot partition (for legacy BIOS GRUB on GPT disks)
parted -s "$TARGET_DISK" mkpart bios_boot 1MiB 2MiB
@@ -1121,8 +1189,10 @@ parted -s "$TARGET_DISK" set 1 bios_grub on
# Partition 2: 512MB EFI System Partition (for UEFI boot)
parted -s "$TARGET_DISK" mkpart efi fat32 2MiB 514MiB
parted -s "$TARGET_DISK" set 2 esp on
# Partition 3: Root filesystem (remaining space)
parted -s "$TARGET_DISK" mkpart root ext4 514MiB 100%
# Partition 3: Root filesystem (30GB — system, packages, container runtime)
parted -s "$TARGET_DISK" mkpart root ext4 514MiB 30GiB
# Partition 4: Encrypted data (LUKS2 — Bitcoin data, secrets, app volumes)
parted -s "$TARGET_DISK" mkpart data 30GiB 100%
sleep 2
@@ -1131,36 +1201,91 @@ if [[ "$TARGET_DISK" == *nvme* ]]; then
BIOS_PART="${TARGET_DISK}p1"
EFI_PART="${TARGET_DISK}p2"
ROOT_PART="${TARGET_DISK}p3"
DATA_PART="${TARGET_DISK}p4"
else
BIOS_PART="${TARGET_DISK}1"
EFI_PART="${TARGET_DISK}2"
ROOT_PART="${TARGET_DISK}3"
DATA_PART="${TARGET_DISK}4"
fi
# Format partitions
echo " [2/6] Formatting partitions..."
echo " [2/7] Formatting partitions..."
# Zero out the BIOS boot partition to prevent FAT-fs read errors during boot
dd if=/dev/zero of="$BIOS_PART" bs=1M count=1 2>/dev/null || true
mkfs.vfat -F32 -n EFI "$EFI_PART"
mkfs.ext4 -F -L archipelago "$ROOT_PART"
# Mount
echo " [3/6] Mounting filesystems..."
# Mount root + extract rootfs (need cryptsetup from rootfs for LUKS)
echo " [3/7] Mounting filesystems..."
mkdir -p /mnt/target
mount "$ROOT_PART" /mnt/target
mkdir -p /mnt/target/boot/efi
mount "$EFI_PART" /mnt/target/boot/efi
# Extract rootfs
echo " [4/6] Installing system (this may take a few minutes)..."
echo " [4/7] Installing system (this may take a few minutes)..."
tar -xf "$ROOTFS_TAR" -C /mnt/target
# LUKS2 encryption for data partition
echo " [5/7] Encrypting data partition (LUKS2)..."
# Generate random 4KB key file
dd if=/dev/urandom of=/mnt/target/root/.luks-archipelago.key bs=4096 count=1 2>/dev/null
chmod 600 /mnt/target/root/.luks-archipelago.key
# Bind-mount /dev so cryptsetup can access the data partition from chroot
mount --bind /dev /mnt/target/dev
# Detect AES-NI support for cipher selection
if grep -q aes /proc/cpuinfo 2>/dev/null; then
LUKS_CIPHER="aes-xts-plain64"
echo " AES-NI detected — using AES-256-XTS"
else
LUKS_CIPHER="xchacha20,aes-adiantum-plain64"
echo " No AES-NI — using ChaCha20-Adiantum"
fi
# Format LUKS2 partition with key file
chroot /mnt/target cryptsetup luksFormat --type luks2 \
--key-file /root/.luks-archipelago.key \
--cipher "$LUKS_CIPHER" --key-size 512 \
--pbkdf argon2id --batch-mode \
"$DATA_PART"
# Open the LUKS volume
chroot /mnt/target cryptsetup open --type luks2 \
--key-file /root/.luks-archipelago.key \
"$DATA_PART" archipelago-data
# Unmount /dev (will be re-mounted later for grub-install)
umount /mnt/target/dev
# Format the inner filesystem
mkfs.ext4 -F -L archipelago-data /dev/mapper/archipelago-data
# Mount encrypted partition
mkdir -p /mnt/target/var/lib/archipelago
mount /dev/mapper/archipelago-data /mnt/target/var/lib/archipelago
# Recreate directory structure on encrypted partition
mkdir -p /mnt/target/var/lib/archipelago/{data,config,containers,secrets,tor,identities,lnd}
mkdir -p /mnt/target/var/lib/archipelago/data/cloud/{Documents,Photos,Music,Videos,Downloads}
chown -R 1000:1000 /mnt/target/var/lib/archipelago
echo " ✅ Data partition encrypted with LUKS2 ($LUKS_CIPHER)"
# Configure auto-unlock via crypttab (key file on root partition)
echo " [6/7] Configuring system..."
DATA_UUID=$(blkid -s UUID -o value "$DATA_PART")
echo "# LUKS2 encrypted data — auto-unlock with key file" > /mnt/target/etc/crypttab
echo "archipelago-data UUID=$DATA_UUID /root/.luks-archipelago.key luks,discard" >> /mnt/target/etc/crypttab
# Create fstab
echo " [5/6] Configuring system..."
cat > /mnt/target/etc/fstab <<EOF
# Archipelago Bitcoin Node OS
UUID=$(blkid -s UUID -o value "$ROOT_PART") / ext4 errors=remount-ro 0 1
UUID=$(blkid -s UUID -o value "$EFI_PART") /boot/efi vfat umask=0077 0 1
UUID=$(blkid -s UUID -o value "$ROOT_PART") / ext4 errors=remount-ro 0 1
UUID=$(blkid -s UUID -o value "$EFI_PART") /boot/efi vfat umask=0077 0 1
/dev/mapper/archipelago-data /var/lib/archipelago ext4 defaults 0 2
EOF
# Configure hostname
@@ -1325,6 +1450,18 @@ if [ -t 0 ] && [ -z "$ARCHIPELAGO_WELCOMED" ]; then
echo " 🔑 Password: archipelago (SSH) / password123 (Web UI)"
echo ""
fi
if [ -b /dev/mapper/archipelago-data ]; then
echo " 🔒 Storage: LUKS2 encrypted"
fi
echo " 📺 Display Mode:"
if systemctl is-active archipelago-kiosk.service >/dev/null 2>&1; then
echo " Kiosk ACTIVE — fullscreen web UI on display"
else
echo " Console login (MOTD)"
fi
echo " Toggle: sudo archipelago-kiosk enable — kiosk on display"
echo " sudo archipelago-kiosk disable — back to console"
echo ""
fi
PROFILE
chmod +x /mnt/target/etc/profile.d/archipelago.sh
@@ -1422,8 +1559,115 @@ RestartSec=5
WantedBy=multi-user.target
CLAUDESVC
# Kiosk mode — X11 + Chromium fullscreen on attached display
# Not enabled by default; toggle via: sudo archipelago-kiosk enable/disable
cat > /mnt/target/usr/local/bin/archipelago-kiosk-launcher <<'KIOSKLAUNCHER'
#!/bin/bash
# Start X server in the background
/usr/bin/Xorg :0 -nocursor vt1 -nolisten tcp -keeptty &
XPID=$!
sleep 2
if ! kill -0 $XPID 2>/dev/null; then
echo 'ERROR: Xorg failed to start'
exit 1
fi
export DISPLAY=:0
export HOME=/home/archipelago
xhost +SI:localuser:archipelago 2>/dev/null
xset s off 2>/dev/null
xset -dpms 2>/dev/null
xset s noblank 2>/dev/null
unclutter -idle 3 -root &
while true; do
sudo -u archipelago env DISPLAY=:0 HOME=/home/archipelago chromium \
--kiosk \
--app=http://localhost/kiosk \
--noerrdialogs \
--disable-infobars \
--disable-translate \
--no-first-run \
--check-for-update-interval=31536000 \
--disable-features=TranslateUI \
--disable-session-crashed-bubble \
--disable-save-password-bubble \
--disable-suggestions-service \
--disable-component-update \
--disable-gpu \
--user-data-dir=/home/archipelago/.config/chromium-kiosk
sleep 3
done
kill $XPID 2>/dev/null
KIOSKLAUNCHER
chmod +x /mnt/target/usr/local/bin/archipelago-kiosk-launcher
cat > /mnt/target/etc/systemd/system/archipelago-kiosk.service <<'KIOSKSVC'
[Unit]
Description=Archipelago Kiosk (X11 + Chromium)
After=archipelago.service
Wants=archipelago.service
ConditionPathExists=/usr/local/bin/archipelago-kiosk-launcher
Conflicts=getty@tty1.service
[Service]
Type=simple
ExecStartPre=/bin/bash -c 'for i in $(seq 1 15); do curl -sf http://localhost/health >/dev/null 2>&1 && exit 0; sleep 2; done; exit 0'
ExecStart=/usr/local/bin/archipelago-kiosk-launcher
TimeoutStartSec=60
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
KIOSKSVC
# Toggle script: sudo archipelago-kiosk enable|disable|status
cat > /mnt/target/usr/local/bin/archipelago-kiosk <<'KIOSKTOGGLE'
#!/bin/bash
set -e
case "${1:-status}" in
enable)
echo "Enabling kiosk mode (X11 + Chromium on display)..."
systemctl enable archipelago-kiosk.service
systemctl start archipelago-kiosk.service 2>/dev/null || true
echo "Kiosk mode ENABLED. Console login (tty1) is now disabled."
echo "To access the server, use SSH or the web UI."
;;
disable)
echo "Disabling kiosk mode (restoring console login)..."
systemctl stop archipelago-kiosk.service 2>/dev/null || true
systemctl disable archipelago-kiosk.service
systemctl restart getty@tty1.service 2>/dev/null || true
echo "Kiosk mode DISABLED. Console login restored on tty1."
;;
status)
if systemctl is-active archipelago-kiosk.service >/dev/null 2>&1; then
echo "Kiosk mode: ACTIVE (display showing web UI)"
elif systemctl is-enabled archipelago-kiosk.service >/dev/null 2>&1; then
echo "Kiosk mode: ENABLED (will start on next boot)"
else
echo "Kiosk mode: DISABLED (console login on tty1)"
fi
;;
*)
echo "Usage: archipelago-kiosk [enable|disable|status]"
echo " enable — Start kiosk (fullscreen web UI on display)"
echo " disable — Stop kiosk, restore console login"
echo " status — Show current mode"
exit 1
;;
esac
KIOSKTOGGLE
chmod +x /mnt/target/usr/local/bin/archipelago-kiosk
# Install GRUB
echo " [6/6] Installing bootloader..."
echo " [7/7] Installing bootloader..."
mount --bind /dev /mnt/target/dev
mount --bind /dev/pts /mnt/target/dev/pts
mount --bind /proc /mnt/target/proc
@@ -1462,10 +1706,10 @@ else
fi
fi
# EFI boot: grub-install --removable already placed unsigned GRUB at /EFI/BOOT/BOOTX64.EFI
# This works on all machines without Secure Boot. For Secure Boot, users must disable it.
# The shim chain was causing "Failed to open \EFI\BOOT\" errors with garbled filenames
# on machines where Secure Boot is disabled — the shim tries to verify signatures and fails.
# EFI boot: grub-install --removable places unsigned GRUB at /EFI/BOOT/BOOTX64.EFI
# No shim chain Secure Boot must be disabled. shim-signed was removed from rootfs
# because it installs BOOTX64.CSV + shimx64.efi which cause "Failed to open \EFI\BOOT\"
# errors with garbled filenames on every boot.
echo " Verifying EFI boot files..."
EFI_BOOT_DIR="/mnt/target/boot/efi/EFI/BOOT"
if [ "$ARCH" = "x86_64" ]; then
@@ -1473,6 +1717,16 @@ if [ "$ARCH" = "x86_64" ]; then
else
EFI_BOOT_BINARY="BOOTAA64.EFI"
fi
# Remove any residual shim chain files (from grub-efi-*-signed package hooks)
# These cause firmware to try loading garbled vendor paths before falling back
for shim_file in shimx64.efi mmx64.efi fbx64.efi BOOTX64.CSV shimaa64.efi mmaa64.efi fbaa64.efi BOOTAA64.CSV; do
if [ -f "$EFI_BOOT_DIR/$shim_file" ] && [ "$shim_file" != "$EFI_BOOT_BINARY" ]; then
rm -f "$EFI_BOOT_DIR/$shim_file"
echo " Removed shim artifact: $shim_file"
fi
done
# Also remove vendor-specific EFI directory (shim creates /EFI/archipelago/)
rm -rf "/mnt/target/boot/efi/EFI/archipelago" 2>/dev/null || true
if [ -f "$EFI_BOOT_DIR/$EFI_BOOT_BINARY" ]; then
echo " ✅ UEFI boot binary present: $EFI_BOOT_DIR/$EFI_BOOT_BINARY"
ls -la "$EFI_BOOT_DIR/"
@@ -1532,6 +1786,8 @@ umount /mnt/target/proc 2>/dev/null || true
umount /mnt/target/dev/pts 2>/dev/null || true
umount /mnt/target/dev 2>/dev/null || true
umount /mnt/target/boot/efi 2>/dev/null || true
umount /mnt/target/var/lib/archipelago 2>/dev/null || true
cryptsetup close archipelago-data 2>/dev/null || true
umount /mnt/target 2>/dev/null || true
echo ""