feat: LUKS2 encryption, boot sequence fixes, onboarding auth, CI/CD

- 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

@ -0,0 +1,47 @@
name: Build Archipelago ISO
on:
push:
branches: [main]
workflow_dispatch:
jobs:
build-iso:
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install Rust toolchain
run: |
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
echo "$HOME/.cargo/bin" >> $GITHUB_PATH
- name: Build backend (release)
run: cargo build --release --manifest-path core/Cargo.toml
- name: Install backend binary
run: |
sudo cp core/target/release/archipelago /usr/local/bin/archipelago
sudo chmod +x /usr/local/bin/archipelago
- name: Build frontend
run: |
cd neode-ui
npm ci
npm run build
- name: Build ISO
run: |
cd image-recipe
sudo DEV_SERVER=localhost BUILD_FROM_SOURCE=0 ./build-auto-installer-iso.sh
env:
DEBIAN_FRONTEND: noninteractive
- name: Upload ISO artifact
uses: actions/upload-artifact@v3
with:
name: archipelago-iso
path: image-recipe/results/*.iso
retention-days: 30

View File

@ -66,6 +66,35 @@ impl RpcHandler {
Ok(serde_json::json!({ "success": true, "session_rotated": true }))
}
pub(super) async fn handle_auth_is_setup(&self) -> Result<serde_json::Value> {
let is_setup = self.auth_manager.is_setup().await?;
Ok(serde_json::json!(is_setup))
}
pub(super) async fn handle_auth_setup(
&self,
params: Option<serde_json::Value>,
) -> Result<serde_json::Value> {
// Prevent re-setup if already set up
let is_setup = self.auth_manager.is_setup().await?;
if is_setup {
return Err(anyhow::anyhow!("Already set up. Use auth.changePassword to change."));
}
let params = params.ok_or_else(|| anyhow::anyhow!("Missing params"))?;
let password = params
.get("password")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("Missing password"))?;
if password.len() < 8 {
return Err(anyhow::anyhow!("Password must be at least 8 characters"));
}
self.auth_manager.setup_user(password).await?;
Ok(serde_json::json!(true))
}
pub(super) async fn handle_auth_onboarding_complete(&self) -> Result<serde_json::Value> {
self.auth_manager.complete_onboarding().await?;
Ok(serde_json::json!(true))

View File

@ -16,6 +16,8 @@ impl RpcHandler {
"auth.login" => self.handle_auth_login(params).await,
"auth.logout" => self.handle_auth_logout().await,
"auth.changePassword" => self.handle_auth_change_password(params, session_token).await,
"auth.isSetup" => self.handle_auth_is_setup().await,
"auth.setup" => self.handle_auth_setup(params).await,
"auth.onboardingComplete" => self.handle_auth_onboarding_complete().await,
"auth.isOnboardingComplete" => self.handle_auth_is_onboarding_complete().await,
"auth.resetOnboarding" => self.handle_auth_reset_onboarding(params).await,

View File

@ -8,7 +8,16 @@ pub(super) const UNAUTHENTICATED_METHODS: &[&str] = &[
"auth.login.backup",
"auth.isOnboardingComplete",
"auth.isSetup",
"auth.setup",
"auth.onboardingComplete",
"health",
// Onboarding flow (before user has a session — DID creation, signing, backup)
"node.did",
"node.signChallenge",
"node.nostr-pubkey",
"node.createBackup",
"identity.verify",
"identity.resolve-did",
// Onboarding restore (before user account exists)
"backup.restore-identity",
// Inter-node RPC: called by federated peers over Tor, no session cookies

View File

@ -17,9 +17,12 @@
| **TASK-10** | **ISO build verification + multi-hardware test** | **P1** | PLANNED | - |
| **TASK-12** | **Beta telemetry — reporter + toggle + collector POST** | **P1** | IN PROGRESS | - |
| **TASK-39** | **Finish .198 rootless container migration** | **P1** | PLANNED | TASK-11 |
| **TASK-42** | **LUKS2 full-partition encryption for /var/lib/archipelago/** | **P1** | PLANNED | TASK-10 |
| **TASK-42** | **LUKS2 full-partition encryption for /var/lib/archipelago/** | **P1** | IN PROGRESS | - |
| **BUG-44** | **App iframe shows blank/broken when container is starting or crashed** | **P2** | PLANNED | - |
| **TASK-45** | **Deploy script: auto-chown data dirs after rootful→rootless migration** | **P2** | PLANNED | - |
| **BUG-46** | **FileBrowser missing in unbundled ISO + Cloud auto-login broken** | **P1** | IN PROGRESS | - |
| **BUG-47** | **Onboarding: DID sign 403 + blob HTTPS + no password setup** | **P1** | IN PROGRESS | - |
| **FEATURE-48** | **Meshtastic support for mesh (plug and play)** | **P1** | PLANNED | - |
### Phase 2: User Testing (controlled, real hardware)
@ -44,6 +47,7 @@
| **FEATURE-6** | **Watch-only wallet architecture** | **P1** | DEFERRED | - |
| **TASK-7** | **Mesh Bitcoin security hardening** | **P1** | DEFERRED | FEATURE-6 |
| **FEATURE-43** | **P2P encrypted voice/video calling (WebRTC over federation)** | **P1** | DEFERRED | - |
| **FEATURE-48** | **Meshtastic support for mesh (plug and play)** | **P1** | PLANNED | - |
## Active Work
@ -109,9 +113,9 @@ Tag every significant alpha version with git tags for easy rollback. Each tag sh
---
### TASK-42: LUKS2 full-partition encryption for /var/lib/archipelago/ (PLANNED)
### TASK-42: LUKS2 full-partition encryption for /var/lib/archipelago/ (IN PROGRESS)
**Priority**: P1 — High
**Status**: PLANNED (2026-03-19)
**Status**: IN PROGRESS (2026-03-26)
Encrypt all Archipelago app data at rest using LUKS2 full-partition encryption. Protects Bitcoin wallet data, LND macaroons, FileBrowser files, Vaultwarden vault, secrets, and everything else from physical disk seizure. Seamless UX — user never interacts with encryption directly.
@ -129,15 +133,15 @@ Encrypt all Archipelago app data at rest using LUKS2 full-partition encryption.
- Forgot password = cannot decrypt (correct sovereign behavior)
**Tasks**:
- [ ] ISO installer: create LUKS2 partition, format + mount at `/var/lib/archipelago/`
- [x] ISO installer: create LUKS2 partition, format + mount at `/var/lib/archipelago/`
- [ ] First-boot: derive LUKS key from setup password via Argon2id + hardware salt
- [ ] Store key file at `/root/.luks-archipelago.key` with 600 perms
- [ ] Configure `/etc/crypttab` for auto-unlock at boot
- [x] Store key file at `/root/.luks-archipelago.key` with 600 perms
- [x] Configure `/etc/crypttab` for auto-unlock at boot
- [ ] Settings password change: re-derive LUKS key, add new keyslot, remove old
- [ ] Detect AES-NI availability, fall back to ChaCha20 on ARM without it
- [x] Detect AES-NI availability, fall back to ChaCha20 on ARM without it
- [ ] Test: fresh install, reboot survives, power-cycle survives, password change works
- [ ] Test: disk removed from machine is unreadable
- [ ] Update `BUILD-GUIDE.md` and `image-recipe/build-auto-installer-iso.sh`
- [x] Update `image-recipe/build-auto-installer-iso.sh`
**Key files**:
- `image-recipe/build-auto-installer-iso.sh` — partition creation
@ -176,6 +180,59 @@ Also fix:
- `scripts/deploy-tailscale.sh` — Step 14 (UID mapping) and Step 22 (container creation)
- `scripts/first-boot-containers.sh` — container creation reference
### BUG-46: FileBrowser missing in unbundled ISO + Cloud auto-login broken (IN PROGRESS)
**Priority**: P1 — High
**Status**: IN PROGRESS (2026-03-26)
Two issues with the Cloud feature on fresh installs:
1. **FileBrowser not prepackaged in unbundled ISO** — The unbundled ISO variant doesn't include the FileBrowser container image, so Cloud doesn't work out of the box. FileBrowser is a core dependency (not an optional app) since it powers the Cloud file manager. Must be bundled even in the unbundled variant.
2. **FileBrowser auto-login not working** — The auto-login flow (so users don't need to enter separate FileBrowser credentials) appears broken. Need to investigate whether the auth proxy/token injection is functioning correctly on fresh installs.
**Tasks**:
- [x] Add FileBrowser image to unbundled ISO build (core dependency, always bundled)
- [x] Create minimal first-boot script for unbundled mode (FileBrowser only)
- [x] Fix auto-login: `Secure` cookie flag silently fails on HTTP — made conditional
- [x] Changed `SameSite=Strict` to `SameSite=Lax` for better navigation compatibility
- [ ] Test Cloud feature end-to-end on a fresh install (both bundled and unbundled)
**Key files**:
- `image-recipe/build-auto-installer-iso.sh` — UNBUNDLED container image list
- `scripts/first-boot-containers.sh` — FileBrowser container creation
- `image-recipe/configs/nginx-archipelago.conf` — FileBrowser proxy config
- `neode-ui/src/views/Cloud.vue` — Cloud UI / auto-login logic
### BUG-47: Onboarding: DID sign 403 + blob HTTPS + no password setup (IN PROGRESS)
**Priority**: P1 — High
**Status**: IN PROGRESS (2026-03-26)
Three onboarding issues on clean install:
1. **Sign DID returns 403 Forbidden** — The DID verification/signing step during onboarding fails with a 403 response from the backend.
2. **Blob URL HTTPS warning** — Browser complains about blob URL loaded over insecure connection (`blob:http://...` should be served over HTTPS). Likely related to the backup download on HTTP connections.
3. **No password setup on clean install** — Users cannot set a password during onboarding. The setup password flow is missing or broken.
**Root causes found**:
- `node.did`, `node.signChallenge`, `node.nostr-pubkey`, `node.createBackup`, `identity.verify` were NOT in `UNAUTHENTICATED_METHODS` — onboarding has no session, so they all returned 403
- `auth.setup` and `auth.isSetup` RPC methods were missing from the dispatcher — the frontend called them but no handler existed
- Blob HTTPS warning is a browser security feature on HTTP connections (not a code bug)
**Tasks**:
- [x] Add onboarding methods to UNAUTHENTICATED_METHODS in middleware.rs
- [x] Add `auth.setup` RPC handler (creates user with password, prevents re-setup)
- [x] Add `auth.isSetup` RPC handler (checks if user.json exists)
- [x] Rust compiles clean
- [ ] Blob URL HTTPS warning — known browser limitation on HTTP, no code fix needed
- [ ] Test full onboarding flow end-to-end on fresh ISO
**Key files**:
- `neode-ui/src/views/OnboardingVerify.vue` — DID signing step
- `neode-ui/src/views/OnboardingBackup.vue` — Backup download (blob URL)
- `neode-ui/src/views/OnboardingIntro.vue` — Password setup entry point
- `core/archipelago/src/api/rpc/auth.rs` — Auth RPC endpoints
- `core/archipelago/src/api/rpc/middleware.rs` — Request auth middleware
---
## Post-Beta (FROZEN)

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 ""

View File

@ -65,7 +65,9 @@ class FileBrowserClient {
const token = text.replace(/^"|"$/g, '')
// Store token as cookie — the only auth mechanism we use
const expires = new Date(Date.now() + 24 * 60 * 60 * 1000).toUTCString()
document.cookie = `auth=${token}; path=/app/filebrowser; SameSite=Strict; Secure; expires=${expires}`
// Only set Secure flag on HTTPS — on HTTP it silently prevents the cookie from being stored
const secure = window.location.protocol === 'https:' ? '; Secure' : ''
document.cookie = `auth=${token}; path=/app/filebrowser; SameSite=Lax${secure}; expires=${expires}`
this._authenticated = true
return true
} catch {

View File

@ -38,6 +38,19 @@ async function quickHealthCheck(): Promise<boolean> {
}
}
async function checkOnboarded(): Promise<boolean> {
try {
const result = await Promise.race([
isOnboardingComplete(),
new Promise<boolean>((resolve) => setTimeout(() => resolve(false), 3000)),
])
return result
} catch {
// Backend unreachable fall back to localStorage only as last resort
return localStorage.getItem('neode_onboarding_complete') === '1'
}
}
async function proceedToApp() {
const devMode = import.meta.env.VITE_DEV_MODE
if (devMode === 'setup' || devMode === 'existing') {
@ -45,23 +58,10 @@ async function proceedToApp() {
return
}
const localComplete = localStorage.getItem('neode_onboarding_complete') === '1'
if (localComplete) {
router.replace('/login').catch(() => {})
return
}
let seenOnboarding = false
try {
const result = await Promise.race([
isOnboardingComplete(),
new Promise<boolean>((resolve) => setTimeout(() => resolve(false), 3000)),
])
seenOnboarding = result
} catch {
seenOnboarding = false
}
router.replace(seenOnboarding ? '/login' : '/onboarding/intro').catch(() => {})
// Always check backend for authoritative onboarding state
// (localStorage can be stale from a previous install on the same IP)
const onboarded = await checkOnboarded()
router.replace(onboarded ? '/login' : '/onboarding/intro').catch(() => {})
}
function onServerReady() {
@ -98,14 +98,23 @@ onMounted(async () => {
return
}
// Production: quick health check
// Production: check server health
const isUp = await quickHealthCheck()
if (isUp) {
proceedToApp()
return
// Server is up check if onboarding is complete
const onboarded = await checkOnboarded()
if (onboarded) {
// Returning user, server is up go straight to login
proceedToApp()
return
}
// First boot: server is up but onboarding not done yet.
// Show boot animation anyway it lets services fully warm up
// (containers, DID resolver, etc.) before onboarding starts.
}
// Server not ready show boot screen
// Server not ready OR first boot show boot screen
showBootScreen.value = true
})
</script>