diff --git a/core/archipelago/src/api/rpc/federation.rs b/core/archipelago/src/api/rpc/federation.rs index 1e517493..4c360de3 100644 --- a/core/archipelago/src/api/rpc/federation.rs +++ b/core/archipelago/src/api/rpc/federation.rs @@ -328,8 +328,9 @@ impl RpcHandler { let tor_active = data.server_info.tor_address.is_some(); + let server_name = data.server_info.name.clone().filter(|n| !n.is_empty()); let state = federation::build_local_state( - apps, 0.0, 0, 0, 0, 0, 0, tor_active, + apps, 0.0, 0, 0, 0, 0, 0, tor_active, server_name, ); Ok(serde_json::to_value(&state)?) diff --git a/core/archipelago/src/federation.rs b/core/archipelago/src/federation.rs index 6e7ab038..b7b3cfe8 100644 --- a/core/archipelago/src/federation.rs +++ b/core/archipelago/src/federation.rs @@ -53,6 +53,8 @@ pub struct FederatedNode { pub struct NodeStateSnapshot { pub timestamp: String, #[serde(default)] + pub node_name: Option, + #[serde(default)] pub apps: Vec, #[serde(default)] pub cpu_usage_percent: Option, @@ -184,6 +186,12 @@ pub async fn update_node_state( let mut nodes = load_nodes(data_dir).await?; if let Some(node) = nodes.iter_mut().find(|n| n.did == did) { node.last_seen = Some(state.timestamp.clone()); + // Update node name from sync if provided (peer announced their name) + if let Some(ref name) = state.node_name { + if !name.is_empty() { + node.name = Some(name.clone()); + } + } node.last_state = Some(state); save_nodes(data_dir, &nodes).await?; } @@ -495,9 +503,11 @@ pub fn build_local_state( disk_total: u64, uptime: u64, tor_active: bool, + server_name: Option, ) -> NodeStateSnapshot { NodeStateSnapshot { timestamp: chrono::Utc::now().to_rfc3339(), + node_name: server_name, apps, cpu_usage_percent: Some(cpu), mem_used_bytes: Some(mem_used), @@ -813,9 +823,11 @@ mod tests { 500_000_000_000, 3600, true, + Some("Test Node".to_string()), ); assert_eq!(state.apps.len(), 1); assert_eq!(state.cpu_usage_percent, Some(25.5)); assert_eq!(state.tor_active, Some(true)); + assert_eq!(state.node_name, Some("Test Node".to_string())); } } diff --git a/core/archipelago/src/identity.rs b/core/archipelago/src/identity.rs index 05bf83a8..fe5277ea 100644 --- a/core/archipelago/src/identity.rs +++ b/core/archipelago/src/identity.rs @@ -35,7 +35,10 @@ impl NodeIdentity { let arr: [u8; 32] = bytes .try_into() .map_err(|_| anyhow::anyhow!("Invalid node key length"))?; - SigningKey::from_bytes(&arr) + let key = SigningKey::from_bytes(&arr); + let pubkey_hex = hex::encode(key.verifying_key().as_bytes()); + tracing::info!("Loaded existing node identity (pubkey: {}...)", &pubkey_hex[..16]); + key } else { let signing_key = SigningKey::generate(&mut OsRng); fs::write(&key_path, signing_key.to_bytes()) diff --git a/core/archipelago/src/transport/delta.rs b/core/archipelago/src/transport/delta.rs index 33807023..01bfca8c 100644 --- a/core/archipelago/src/transport/delta.rs +++ b/core/archipelago/src/transport/delta.rs @@ -195,6 +195,7 @@ mod tests { fn sample_snapshot_a() -> NodeStateSnapshot { NodeStateSnapshot { timestamp: "2026-03-16T12:00:00Z".to_string(), + node_name: Some("Test Node A".to_string()), apps: vec![ AppStatus { id: "bitcoin-knots".to_string(), @@ -225,6 +226,7 @@ mod tests { fn sample_snapshot_b() -> NodeStateSnapshot { NodeStateSnapshot { timestamp: "2026-03-16T12:05:00Z".to_string(), + node_name: Some("Test Node A".to_string()), apps: vec![ AppStatus { id: "bitcoin-knots".to_string(), diff --git a/docs/MASTER_PLAN.md b/docs/MASTER_PLAN.md index 49b2eca5..80124d97 100644 --- a/docs/MASTER_PLAN.md +++ b/docs/MASTER_PLAN.md @@ -17,6 +17,7 @@ | **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 | ### Phase 2: User Testing (controlled, real hardware) @@ -105,6 +106,44 @@ 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) +**Priority**: P1 — High +**Status**: PLANNED (2026-03-19) + +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. + +**Design**: +- LUKS2 partition for `/var/lib/archipelago/` created during ISO install +- Cipher: AES-256-XTS (hardware AES-NI on x86_64, ChaCha20 fallback on ARM without AES-NI) +- Key derived from setup password via Argon2id + hardware salt (`/sys/class/dmi/id/product_uuid`) +- Key file stored at `/root/.luks-archipelago.key` (root:600, on boot partition) +- Auto-unlock via `/etc/crypttab` on every boot — no passphrase prompt +- Password change in Settings re-derives key and rotates LUKS keyslot + +**Threat model**: +- Disk removed from machine = fully encrypted, unreadable +- Running machine with login = transparent (same as today) +- Forgot password = cannot decrypt (correct sovereign behavior) + +**Tasks**: +- [ ] 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 +- [ ] Settings password change: re-derive LUKS key, add new keyslot, remove old +- [ ] 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` + +**Key files**: +- `image-recipe/build-auto-installer-iso.sh` — partition creation +- `scripts/first-boot-containers.sh` — runs after LUKS mount +- `core/archipelago/src/api/rpc/system.rs` — password change handler +- `core/archipelago/src/server.rs` — startup checks + +--- + ## Post-Beta (FROZEN) *These tasks are deferred until after beta ships. Do not start.* diff --git a/neode-ui/dev-dist/sw.js b/neode-ui/dev-dist/sw.js index c76374d5..e492dc0f 100644 --- a/neode-ui/dev-dist/sw.js +++ b/neode-ui/dev-dist/sw.js @@ -82,7 +82,7 @@ define(['./workbox-21a80088'], (function (workbox) { 'use strict'; "revision": "3ca0b8505b4bec776b69afdba2768812" }, { "url": "index.html", - "revision": "0.8sq55gh6vdc" + "revision": "0.13rknjqo28" }], {}); workbox.cleanupOutdatedCaches(); workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), { diff --git a/scripts/deploy-to-target.sh b/scripts/deploy-to-target.sh index 5bfebb12..ec0ab4c5 100755 --- a/scripts/deploy-to-target.sh +++ b/scripts/deploy-to-target.sh @@ -678,9 +678,10 @@ PYEOF sudo mkdir -p /var/lib/archipelago/dwn/protocols sudo mkdir -p /var/lib/archipelago/content/files sudo mkdir -p /var/lib/archipelago/federation + sudo mkdir -p /var/lib/archipelago/identity sudo mkdir -p /var/lib/archipelago/identities sudo mkdir -p /var/lib/archipelago/tor-config - sudo chown -R archipelago:archipelago /var/lib/archipelago/dwn /var/lib/archipelago/content /var/lib/archipelago/federation /var/lib/archipelago/identities /var/lib/archipelago/tor-config 2>/dev/null || true + sudo chown -R archipelago:archipelago /var/lib/archipelago/dwn /var/lib/archipelago/content /var/lib/archipelago/federation /var/lib/archipelago/identity /var/lib/archipelago/identities /var/lib/archipelago/tor-config 2>/dev/null || true # Fix secrets directory ownership (must be readable by archipelago user, not root) sudo chown -R archipelago:archipelago /var/lib/archipelago/secrets 2>/dev/null || true sudo chmod 700 /var/lib/archipelago/secrets 2>/dev/null || true diff --git a/scripts/first-boot-containers.sh b/scripts/first-boot-containers.sh index 02339871..6d88070f 100644 --- a/scripts/first-boot-containers.sh +++ b/scripts/first-boot-containers.sh @@ -798,11 +798,13 @@ SJSON log "Created initial tor-config/services.json" fi -# identities: backend identity manager stores DIDs here +# identity: node Ed25519 keypair (DID) — MUST persist across deployments +mkdir -p /var/lib/archipelago/identity +# identities: backend identity manager stores user DIDs here mkdir -p /var/lib/archipelago/identities # Ensure archipelago user can write to these directories -chown -R 1000:1000 /var/lib/archipelago/tor-config /var/lib/archipelago/identities 2>/dev/null || true +chown -R 1000:1000 /var/lib/archipelago/tor-config /var/lib/archipelago/identity /var/lib/archipelago/identities 2>/dev/null || true # 11. Post-boot validation log "Validating container creation..."