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:
parent
f90b407054
commit
b7e60af823
47
.gitea/workflows/build-iso.yml
Normal file
47
.gitea/workflows/build-iso.yml
Normal 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
|
||||
@ -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))
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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 ""
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user