From b7e60af8233a8f2d580bde6d74ac9fb1fe559f6f Mon Sep 17 00:00:00 2001 From: Dorian Date: Thu, 26 Mar 2026 09:12:16 +0000 Subject: [PATCH] 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) --- .gitea/workflows/build-iso.yml | 47 ++++ core/archipelago/src/api/rpc/auth.rs | 29 ++ core/archipelago/src/api/rpc/dispatcher.rs | 2 + core/archipelago/src/api/rpc/middleware.rs | 9 + docs/MASTER_PLAN.md | 73 ++++- image-recipe/build-auto-installer-iso.sh | 300 +++++++++++++++++++-- neode-ui/src/api/filebrowser-client.ts | 4 +- neode-ui/src/views/RootRedirect.vue | 51 ++-- 8 files changed, 463 insertions(+), 52 deletions(-) create mode 100644 .gitea/workflows/build-iso.yml diff --git a/.gitea/workflows/build-iso.yml b/.gitea/workflows/build-iso.yml new file mode 100644 index 00000000..0a3f8cb7 --- /dev/null +++ b/.gitea/workflows/build-iso.yml @@ -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 diff --git a/core/archipelago/src/api/rpc/auth.rs b/core/archipelago/src/api/rpc/auth.rs index 3ce50a23..4f1a765c 100644 --- a/core/archipelago/src/api/rpc/auth.rs +++ b/core/archipelago/src/api/rpc/auth.rs @@ -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 { + 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, + ) -> Result { + // 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 { self.auth_manager.complete_onboarding().await?; Ok(serde_json::json!(true)) diff --git a/core/archipelago/src/api/rpc/dispatcher.rs b/core/archipelago/src/api/rpc/dispatcher.rs index b883988c..7e0c05f4 100644 --- a/core/archipelago/src/api/rpc/dispatcher.rs +++ b/core/archipelago/src/api/rpc/dispatcher.rs @@ -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, diff --git a/core/archipelago/src/api/rpc/middleware.rs b/core/archipelago/src/api/rpc/middleware.rs index 9b80fa6f..0470a855 100644 --- a/core/archipelago/src/api/rpc/middleware.rs +++ b/core/archipelago/src/api/rpc/middleware.rs @@ -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 diff --git a/docs/MASTER_PLAN.md b/docs/MASTER_PLAN.md index 1ec3104c..42d321d2 100644 --- a/docs/MASTER_PLAN.md +++ b/docs/MASTER_PLAN.md @@ -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) diff --git a/image-recipe/build-auto-installer-iso.sh b/image-recipe/build-auto-installer-iso.sh index 622995e7..1a519427 100755 --- a/image-recipe/build-auto-installer-iso.sh +++ b/image-recipe/build-auto-installer-iso.sh @@ -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 </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 "" diff --git a/neode-ui/src/api/filebrowser-client.ts b/neode-ui/src/api/filebrowser-client.ts index b5db64cb..3cc6f1c7 100644 --- a/neode-ui/src/api/filebrowser-client.ts +++ b/neode-ui/src/api/filebrowser-client.ts @@ -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 { diff --git a/neode-ui/src/views/RootRedirect.vue b/neode-ui/src/views/RootRedirect.vue index ac221e11..12cec31d 100644 --- a/neode-ui/src/views/RootRedirect.vue +++ b/neode-ui/src/views/RootRedirect.vue @@ -38,6 +38,19 @@ async function quickHealthCheck(): Promise { } } +async function checkOnboarded(): Promise { + try { + const result = await Promise.race([ + isOnboardingComplete(), + new Promise((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((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 })