feat: DID persistence + federation node names in sync
Part 1 — DID Persistence: - Deploy script creates /var/lib/archipelago/identity/ directory - First-boot script creates identity dir with proper ownership - Identity load now logs pubkey to confirm persistence across restarts Part 2 — Node Names: - NodeStateSnapshot includes node_name field - build_local_state() passes server name to sync responses - update_node_state() stores peer's announced name on the FederatedNode - Names propagate automatically during federation.sync-state Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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)?)
|
||||
|
||||
@@ -53,6 +53,8 @@ pub struct FederatedNode {
|
||||
pub struct NodeStateSnapshot {
|
||||
pub timestamp: String,
|
||||
#[serde(default)]
|
||||
pub node_name: Option<String>,
|
||||
#[serde(default)]
|
||||
pub apps: Vec<AppStatus>,
|
||||
#[serde(default)]
|
||||
pub cpu_usage_percent: Option<f64>,
|
||||
@@ -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<String>,
|
||||
) -> 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()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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.*
|
||||
|
||||
@@ -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"), {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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..."
|
||||
|
||||
Reference in New Issue
Block a user