## Phase 1: Infrastructure — CRITICAL Production Credential Hardening
> **Layman version**: Every Archipelago installation currently uses the same passwords (like every house
> in a neighborhood using the same door key). We fix this by generating unique random passwords per
> installation and storing them encrypted. This is the single most important security fix.
- [ ]**Generate random Bitcoin RPC credentials at first boot**: In `scripts/first-boot-containers.sh`, find all occurrences of `-rpcuser=archipelago` and `-rpcpassword=archipelago123`. Replace the hardcoded values with dynamically generated credentials:
1. At the top of the script (after the shebang and initial variables), add:
```bash
# Generate per-installation credentials if not already saved
2. Replace every `-rpcpassword=archipelago123` with `-rpcpassword=$BITCOIN_RPC_PASS` throughout the script.
3. Replace every `archipelago:archipelago123@` in connection strings (ElectrumX DAEMON_URL, etc.) with `$BITCOIN_RPC_USER:$BITCOIN_RPC_PASS@`.
4. Do the same in `scripts/deploy-to-target.sh` — search for `archipelago123` and replace with `$BITCOIN_RPC_PASS` (read from the same secrets file on the target server).
5. SSH to 192.168.1.228, generate the initial password file, restart bitcoin-knots with the new password, then restart all dependent containers (electrs, mempool-api, lnd, btcpay).
6. Verify: `sudo podman exec bitcoin-knots bitcoin-cli -rpcuser=archipelago -rpcpassword=$(cat /var/lib/archipelago/secrets/bitcoin-rpc-password) getblockchaininfo` should succeed. The old hardcoded password should fail.
- [ ]**Generate random database passwords at first boot**: Same pattern for all database passwords. In `scripts/first-boot-containers.sh`:
1. Add credential generation for each database service:
```bash
for svc in mempool btcpay immich penpot; do
if [ ! -f "$SECRETS_DIR/${svc}-db-password" ]; then
2. Replace `mempoolpass` with `$MEMPOOL_DB_PASS`, `btcpaypass` with `$BTCPAY_DB_PASS`, `immichpass` with `$IMMICH_DB_PASS`, `penpot` (password) with `$PENPOT_DB_PASS` throughout the script.
3. Replace `rootpass` (MySQL root) with a generated password too.
4. On the live server, update existing containers: stop each DB container, update the password in the DB itself, restart with new env vars.
5. Verify each service still connects to its database by checking container logs for connection errors.
- [ ]**Generate unique Fedimint gateway password per deployment**: In `scripts/first-boot-containers.sh` and `scripts/deploy-to-target.sh`, find the hardcoded bcrypt hash `$2y$10$t9YjjxkiktrlYvjajB/zgOMDnSNVg4HqrbDqh47u7Jf42whNdxNqC`. Replace with:
1. Generate a random password and hash it:
```bash
if [ ! -f "$SECRETS_DIR/fedimint-gateway-password" ]; then
2. Use `$FEDI_HASH` in the `--bcrypt-password-hash` argument.
3. Display the password in the first-boot log so the operator can note it.
4. Verify: open Fedimint gateway web UI and log in with the generated password.
- [ ]**Remove hardcoded Bitcoin RPC credentials from Rust backend**: In `core/archipelago/src/mesh/mod.rs`, find line ~610 with `.basic_auth("archipelago", Some("archipelago123"))`. Replace with:
1. Add a function to read credentials from the secrets file:
2. Call this function where RPC credentials are needed instead of hardcoding.
3. Do the same for any other `.basic_auth("archipelago", Some("archipelago123"))` calls in the codebase. Search with `grep -rn "archipelago123" core/` to find all occurrences.
4. Build on dev server: `cd ~/archy/core && cargo clippy --all-targets --all-features`.
5. Deploy and verify mesh Bitcoin relay still works.
- [ ]**Verify Phase 1 — No hardcoded passwords remain**: Run these checks:
1.`grep -rn "archipelago123" scripts/ core/ --include="*.rs" --include="*.sh"` — should return zero results (except comments explaining the migration).
2.`grep -rn "mempoolpass\|btcpaypass\|immichpass\|rootpass" scripts/ --include="*.sh"` — should return zero results.
3.`ls -la /var/lib/archipelago/secrets/` on the server — should show password files with `600` permissions.
4. All services still running: `sudo podman ps --format '{{.Names}} {{.Status}}' | grep -v "Up"` — should show nothing (all containers Up).
> **Layman version**: The backend currently runs as the all-powerful "root" user with no restrictions.
> If any bug is exploited, the attacker gets complete control of everything. We lock it down so the
> backend can only do what it needs to do — like giving a bank teller access to the cash drawer but
> not the vault, the CEO's office, or the security cameras.
- [ ]**Create unprivileged archipelago user for backend**: SSH to 192.168.1.228:
1. Check if user exists: `id archipelago`. If it's the login user (UID 1000), create a separate service user: `sudo useradd -r -s /usr/sbin/nologin -d /var/lib/archipelago archipelago-svc` (UID will be in the system range).
2. Actually — the `archipelago` user already exists as UID 1000 (the login user). The backend should run as this user, NOT root. Change `/etc/systemd/system/archipelago.service` to use `User=archipelago` instead of `User=root`.
Deploy the service file to the server: `scp image-recipe/configs/archipelago.service archipelago@192.168.1.228:/tmp/ && ssh archipelago@192.168.1.228 'sudo cp /tmp/archipelago.service /etc/systemd/system/ && sudo systemctl daemon-reload && sudo systemctl restart archipelago'`.
Watch the journal for errors: `ssh archipelago@192.168.1.228 'sudo journalctl -u archipelago -n 50 --no-pager'`. If the service fails to start due to a denied syscall or path, adjust the sandboxing (e.g., add the path to `ReadWritePaths` or the syscall group to `SystemCallFilter`). Iterate until the service starts cleanly.
- [ ]**Bind Bitcoin RPC to localhost only**: SSH to 192.168.1.228. Edit the bitcoin-knots container's start command:
1. Find where bitcoin-knots is started (in `scripts/first-boot-containers.sh` or via `podman inspect bitcoin-knots`).
2. Change `-rpcbind=0.0.0.0:8332` to `-rpcbind=127.0.0.1:8332 -rpcbind=::1:8332`.
3. Change `-rpcallowip=0.0.0.0/0` to `-rpcallowip=127.0.0.1/32 -rpcallowip=10.88.0.0/16` (the 10.88.x.x is Podman's default network — containers need to reach Bitcoin RPC).
4. Stop and recreate bitcoin-knots with the new flags.
5. Verify containers on the Podman network can still reach it: `sudo podman exec lnd bitcoin-cli -rpcconnect=bitcoin-knots -rpcuser=... getblockchaininfo`.
6. Verify external access is blocked: from another machine on the LAN, `curl http://192.168.1.228:8332` should fail/timeout.
- [ ]**Reduce Tailscale container privileges**: In `scripts/first-boot-containers.sh`, find the Tailscale container creation (line ~460). Replace `--privileged` with:
> **Layman version**: Two bugs in the Rust backend could let an attacker either run any command on your
> server (command injection) or crash your entire node at will (unwrap panic). These are the most
> dangerous code-level bugs found.
- [ ]**Fix command injection in VPN key generation**: In `core/archipelago/src/vpn.rs`, find lines 132-137 where `sh -c` is used with `format!("echo '{}' | wg pubkey", private_key)`. This is a textbook command injection vulnerability. Replace the entire block with safe stdin piping:
```rust
let mut child = tokio::process::Command::new("wg")
.arg("pubkey")
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.spawn()
.context("Failed to spawn wg pubkey")?;
if let Some(mut stdin) = child.stdin.take() {
use tokio::io::AsyncWriteExt;
stdin.write_all(private_key.as_bytes()).await
.context("Failed to write private key to wg stdin")?;
Search the entire `core/` directory for other `sh -c` or `bash -c` patterns: `grep -rn 'Command::new("sh")\|Command::new("bash")' core/`. Fix any other occurrences with the same pattern.
Test: If VPN setup is available in the UI, test generating a WireGuard key.
- [ ]**Fix unwrap crash in secrets manager**: In `core/security/src/secrets_manager.rs`, find line 112 with `secret_path.parent().unwrap()`. Replace with:
```rust
let parent = secret_path.parent()
.ok_or_else(|| anyhow::anyhow!("Invalid secret path: no parent directory for {:?}", secret_path))?;
Search for ALL `.unwrap()` calls in the file: `grep -n "unwrap()" core/security/src/secrets_manager.rs`. For each one in a non-test function, evaluate whether it can actually fail and replace with `?` or `.ok_or_else()` if so. Common safe unwraps (e.g., after a `.is_some()` check) can stay but should get a comment explaining why they're safe.
- [ ]**Fix expect crash in Tor proxy fallback**: In `core/archipelago/src/api/rpc/tor.rs`, find line ~525 with `.expect("valid proxy")`. Replace the entire proxy chain with proper error handling:
.context("Failed to create SOCKS5 proxy for Tor")?;
```
Search for ALL `.expect(` calls in non-test code: `grep -rn "\.expect(" core/archipelago/src/ --include="*.rs" | grep -v "#\[cfg(test)\]" | grep -v "mod tests"`. List them and fix any that could realistically fail in production.
- [ ]**Fix image verifier accepting unsigned images**: In `core/security/src/image_verifier.rs`, find lines 18-22 where the verifier returns `Ok(false)` for unsigned images. Change to:
```rust
if signature.is_none() && self.cosign_public_key.is_none() {
return Err(anyhow::anyhow!(
"Image '{}' has no signature and no cosign key is configured. \
All container images must be signed for production use.",
image
));
}
```
Also fix line 25-32 where missing cosign binary returns `Ok(false)`:
```rust
if !cosign_available {
return Err(anyhow::anyhow!(
"Cosign binary not found. Install cosign to verify container image signatures."
));
}
```
Build and test. Note: this may cause existing unsigned images to fail verification. If the system doesn't use cosign yet, add a config flag `require_signatures: bool` that defaults to `false` for now but can be flipped to `true` when cosign is deployed.
- [ ]**Verify Phase 3 — No more crash vectors**: Run these checks:
1.`grep -rn 'Command::new("sh")' core/ --include="*.rs"` — should return zero results.
2.`grep -rn "\.unwrap()" core/security/src/secrets_manager.rs | grep -v test` — should be minimal/commented.
3.`grep -rn "\.expect(" core/archipelago/src/api/ --include="*.rs" | grep -v test | grep -v "// SAFE:"` — review each remaining expect.
4.`cargo clippy --all-targets --all-features` — zero warnings.
> **Layman version**: The mesh network currently accepts messages from anyone who claims to be someone.
> It's like accepting a phone call from someone who says "Hi, I'm your bank" without verifying. We add
> cryptographic proof of identity (digital signatures) so every message is provably from who it claims.
> We also add checks so fake Bitcoin data can't be relayed.
- [ ]**Implement signed identity announcements**: In `core/archipelago/src/mesh/listener.rs`, find the identity advertisement handling (around line 923+). Modify the peer identity broadcast to include an Ed25519 signature:
1. When broadcasting identity (DID + Ed25519 pubkey), sign the announcement with the node's private key:
```rust
// In the identity broadcast function
let identity_payload = format!("{}:{}", did, hex::encode(&pubkey));
let signature = signing_key.sign(identity_payload.as_bytes());
// Include signature in the broadcast envelope
```
2. When receiving an identity announcement, verify the signature before accepting the peer:
```rust
// In the identity receive handler
let identity_payload = format!("{}:{}", claimed_did, hex::encode(&claimed_pubkey));
let verifying_key = ed25519_dalek::VerifyingKey::from_bytes(&claimed_pubkey)?;
.map_err(|_| anyhow::anyhow!("Identity announcement signature verification failed for {}", claimed_did))?;
```
3. Reject any identity announcement without a valid signature. Log the rejection at `warn!` level.
4. Update the `TypedEnvelope` struct in `message_types.rs` to include an optional `identity_signature` field if not already present.
Build and test with two mesh-connected nodes if available. If only one node, verify the code compiles and the identity broadcast includes signatures.
- [ ]**Verify envelope signatures on received messages**: In `core/archipelago/src/mesh/listener.rs`, find where incoming `TypedEnvelope` messages are processed. Add signature verification:
1. Before processing any message, call `envelope.verify_signature()` (which should already exist in `message_types.rs`).
2. If verification fails, log a warning and drop the message:
```rust
if !envelope.verify_signature(&peer_pubkey)? {
tracing::warn!(peer = %contact_id, "Dropping message with invalid signature");
continue;
}
```
3. For alert messages specifically, verify the alert is signed by the claimed peer's key before displaying or relaying.
Build and deploy.
- [ ]**Add Bitcoin transaction/block validation before relay**: In `core/archipelago/src/mesh/bitcoin_relay.rs`, find lines 210-232 where block headers and transactions are relayed:
// Check version bytes (first 4 bytes, little-endian)
let version = u32::from_le_bytes(tx_bytes[0..4].try_into()?);
if version <1||version> 3 {
return Ok(false);
}
Ok(true)
}
```
3. Add rate limiting: max 10 block headers per minute, max 5 transactions per minute per peer.
4. Call these validation functions before relaying any data.
Build and deploy.
- [ ]**Add message sequence numbers**: In `core/archipelago/src/mesh/message_types.rs`, add a `sequence: u64` field to `TypedEnvelope`:
1. Add the field to the struct (with `#[serde(default)]` for backwards compatibility with old messages).
2. In the message creation code, increment a per-peer counter for each outgoing message.
3. On receive, track the last seen sequence per peer and log out-of-order messages at `debug!` level.
4. Do NOT reject out-of-order messages (mesh is unreliable), but allow upper layers to reorder if needed.
Build and deploy.
- [ ]**Verify Phase 4 — Mesh authentication active**: Run these checks:
1.`grep -rn "verify_signature\|verify_strict" core/archipelago/src/mesh/ --include="*.rs"` — should show verification calls in listener.rs and message_types.rs.
## Phase 5: Frontend — XSS, Auth, and Input Validation
> **Layman version**: The web interface has a few places where an attacker could inject malicious code
> into the page (XSS), steal login cookies, or redirect you to a fake site after login. We fix all
> of these and add proper input sanitization everywhere.
- [ ]**Fix v-html XSS in BootScreen and Settings**: In `neode-ui/src/components/BootScreen.vue` line 55, replace `v-html="icons[currentIcon]"` with a safe rendering approach:
1. Since the icons are hardcoded SVG strings, create a computed property that returns the current icon and use `v-html` with a DOMPurify sanitizer.
7. Build and deploy. Verify boot screen animation still works. Verify TOTP QR code still renders on Settings page.
- [ ]**Fix FileBrowser cookie security flags**: In `neode-ui/src/api/filebrowser-client.ts` line 62, find `document.cookie = \`auth=${this.token}; path=/app/filebrowser; SameSite=Strict\``. This cookie is missing security flags. Since we can't set `HttpOnly` from JavaScript (that's a server-side flag), the best we can do client-side is:
The `Secure` flag ensures the cookie is only sent over HTTPS. For the long term (Phase 13), the FileBrowser auth should be proxied through the backend so the cookie can be set server-side with `HttpOnly`.
Also add an expiration so the cookie doesn't persist indefinitely:
3. Remove the `select-all` class — users should deliberately copy, not accidentally select.
Build and deploy. Verify TOTP setup flow still works.
- [ ]**Validate redirect URL after login**: In `neode-ui/src/router/index.ts`, find line 231 with `const redirectTo = (to.query.redirect as string) || '/dashboard'`. Replace with:
```typescript
function isLocalRedirect(path: unknown): path is string {
if (typeof path !== 'string') return false
try {
// Must be a relative path, not an absolute URL
if (path.startsWith('//') || path.includes('://')) return false
Run `npm run type-check`. Build and deploy. Test: visit `http://192.168.1.228/login?redirect=https://evil.com` — after login should go to `/dashboard`, NOT `evil.com`. Visit `http://192.168.1.228/login?redirect=/mesh` — after login should go to `/mesh`.
- [ ]**Add input trimming to all auth fields**: In `neode-ui/src/views/Login.vue`, find all password and input submissions. Add `.trim()` before sending:
1. Search for `password.value` in the file. Wherever it's submitted via RPC (e.g., `params: { password: password.value }`), change to `params: { password: password.value.trim() }`.
2. Do the same for TOTP code inputs, setup passwords, confirm passwords.
3. Also check `neode-ui/src/views/Settings.vue` for password change forms — trim those too.
Run `npm run type-check`. Build and deploy. Test login with a password that has trailing spaces — should still work.
- [ ]**Validate route parameters**: In `neode-ui/src/views/AppDetails.vue` (line ~485) and `neode-ui/src/views/AppSession.vue` (line ~267), add app ID validation:
1. Create a utility function in `neode-ui/src/utils/` or inline:
```typescript
function isValidAppId(id: unknown): id is string {
> **Layman version**: The web server (nginx) is missing security headers that tell browsers how to
> protect users. We add headers that prevent clickjacking, content type confusion, and XSS. We also
> add rate limiting so attackers can't overwhelm the server with requests.
- [ ]**Fix Content Security Policy**: In `image-recipe/configs/nginx-archipelago.conf`, find line ~14 with the existing CSP. Replace the CSP header with a strict version:
Note: `'unsafe-inline'` for styles is needed because Vue scoped styles sometimes inject inline styles. `'unsafe-eval'` is removed — if the app breaks, it means some JS is using `eval()` which should be fixed in code instead.
Deploy the nginx config. Test the web UI thoroughly — if anything breaks, check browser console for CSP violations and adjust the policy minimally.
- [ ]**Replace X-Frame-Options stripping with SAMEORIGIN**: In `image-recipe/configs/snippets/archipelago-https-app-proxies.conf`, find all 38 occurrences of `proxy_hide_header X-Frame-Options;`. For each one, add after it:
```nginx
add_header X-Frame-Options "SAMEORIGIN" always;
```
This allows Archipelago's own UI to iframe apps but blocks external sites from framing them. Do the same in the HTTP config in `nginx-archipelago.conf`.
Deploy and test: open an app in the Archipelago iframe — should still load.
- [ ]**Add HSTS header**: In `image-recipe/configs/nginx-archipelago.conf`, add to the HTTPS server block (or main server block if using HTTPS):
5. Rate limit test: `for i in $(seq 1 100); do curl -s -o /dev/null -w "%{http_code}\n" http://192.168.1.228/rpc/v1; done | sort | uniq -c` — should show some 429s.
> **Layman version**: These fixes improve defense-in-depth. They're not immediately exploitable like
> the critical bugs, but they close gaps that a sophisticated attacker could chain together. Think of
> it as adding deadbolts after fixing the broken window.
- [ ]**Add zeroization to SecretsManager**: In `core/security/src/secrets_manager.rs`, the encryption key stays in memory for the lifetime of the struct. Add zeroization on drop:
1. Add `zeroize` dependency to `core/security/Cargo.toml` if not present: `zeroize = { version = "1", features = ["derive"] }`.
2. Wrap the key material in a zeroizing wrapper. Since `Aes256Gcm` doesn't implement `Zeroize`, store the raw key separately:
- [ ]**Replace thread_rng with OsRng in secrets manager**: In `core/security/src/secrets_manager.rs`, find lines 64 and 221 where `rand::thread_rng().fill_bytes()` is used. Replace with:
OsRng.fill_bytes(&mut new_secret_bytes); // Line 221
```
Build and test.
- [ ]**Encrypt the remember-me HMAC secret**: In `core/archipelago/src/session.rs`, find lines 395-403 where the remember-me secret is stored as plaintext. Encrypt it using the secrets manager:
1. Instead of `std::fs::write(REMEMBER_SECRET_FILE, &secret)`, use the SecretsManager to encrypt the secret before writing.
2. On read, decrypt using SecretsManager.
3. If SecretsManager is not available at that point in the boot sequence, derive the secret from a combination of machine-specific data (e.g., `/etc/machine-id` + salt) using Argon2, so it's different per installation but deterministic.
Build, deploy, and test: remember-me login should still work after restart.
- [ ]**Use checked arithmetic for Bitcoin amounts**: In `core/archipelago/src/wallet/ecash.rs` line 64, replace the `.sum()` with checked addition:
Use `AppId` in RPC handler signatures where app IDs are accepted. The deserializer will validate automatically.
Build — fix all compilation errors from the type change. Deploy and test app operations.
- [ ]**Validate Tor service names**: In `core/archipelago/src/api/rpc/tor.rs`, find lines 426-427 where `name` is used in path operations. Add validation:
anyhow::bail!("Service name must be 1-64 characters");
}
if !name.chars().all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_') {
anyhow::bail!("Service name must contain only alphanumeric characters, hyphens, and underscores");
}
Ok(())
}
```
Call `validate_service_name(&name)?;` before any filesystem operation with the name.
Build and deploy.
- [ ]**Add per-user rate limiting on CPU-intensive RPC endpoints**: In `core/archipelago/src/api/rpc/mod.rs`, add a rate limiter for expensive operations:
1. Add a simple token-bucket rate limiter using a `HashMap<String, (Instant, u32)>` behind a `Mutex`.
- [ ]**Add alert signature verification on receive**: In `core/archipelago/src/mesh/listener.rs`, find where emergency alerts are processed. Before displaying or relaying an alert:
```rust
// Verify the alert is actually signed by the claimed peer
let peer_pubkey = resolve_peer_pubkey(&envelope.sender)?;
if !envelope.verify_signature(&peer_pubkey)? {
tracing::warn!(
claimed_sender = %envelope.sender,
"Dropping emergency alert with invalid signature — possible spoofing attempt"
- [ ]**Implement atomic ratchet state persistence**: In `core/archipelago/src/mesh/session.rs`, find lines 156-159 where ratchet state is saved. Replace with atomic write (write to temp file, then rename):
This ensures that a crash during write leaves either the old state (intact) or the new state (complete), never a partial/corrupt file.
Build and test.
- [ ]**Encrypt GPS in dead man's switch alerts**: In `core/archipelago/src/mesh/alerts.rs`, find where GPS coordinates are included in alerts. Encrypt the GPS data for intended recipients only:
1. Make GPS optional in the alert struct: `gps: Option<EncryptedGps>`.
2. When creating an alert, encrypt GPS coordinates using each trusted peer's public key.
3. Only intended recipients can decrypt the GPS. Other mesh relayers see the alert but not the location.
Build and test.
- [ ]**Systematic unwrap audit in mesh code**: Run `grep -rn "\.unwrap()\|\.expect(" core/archipelago/src/mesh/ --include="*.rs" | grep -v "mod tests" | grep -v "#\[test\]"`. For each occurrence:
1. If it's in message parsing/deserialization — replace with `?` (incoming data is untrusted).
2. If it's after a guaranteed check (e.g., `if x.is_some() { x.unwrap() }`) — refactor to `if let Some(v) = x`.
3. If it's truly infallible (e.g., regex compilation of a literal) — add `// SAFETY: literal regex cannot fail` comment.
Target: reduce unwrap/expect in non-test mesh code to under 20, all documented.
Build and run full test suite.
- [ ]**Verify Phase 8 — Mesh hardened**: Run these checks:
1.`cargo test --all-features` — all tests pass.
2.`grep -c "unwrap()\|\.expect(" core/archipelago/src/mesh/*.rs | grep -v test` — count should be under 20.
3. Backend starts cleanly with mesh enabled.
4. No ratchet state `.tmp` files left behind: `ls /var/lib/archipelago/mesh/sessions/*.tmp` — should be empty.
> **Layman version**: Currently, Tor is optional. Competitors like Start9 and nix-bitcoin route all
> traffic through Tor by default for maximum privacy. We match this by making Tor the default for
> all Bitcoin and Lightning network connections.
- [ ]**Install and configure Tor on first boot**: In `scripts/first-boot-containers.sh`, add a Tor container (or system service) that starts before other services:
1. Add a Tor container or verify the system Tor service is installed and enabled.
2. Configure Tor with a SocksPort on `127.0.0.1:9050`.
3. Add hidden service configs for: web UI (port 80), LND (port 8081), Bitcoin P2P (port 8333).
4. Save the generated `.onion` addresses to `/var/lib/archipelago/tor-hostnames/`.
- [ ]**Route Bitcoin Core through Tor by default**: Add `-proxy=127.0.0.1:9050` and `-onlynet=onion` to bitcoin-knots container flags. This routes all P2P connections through Tor, hiding the node's IP address from the Bitcoin network.
Test: `sudo podman exec bitcoin-knots bitcoin-cli getnetworkinfo` should show only onion connections.
- [ ]**Route LND through Tor**: Configure LND to use Tor for all connections. Add `--tor.active --tor.socks=127.0.0.1:9050` to LND start flags. Verify LND peers are connected via Tor.
- [ ]**Add .onion URL display in web UI**: In `neode-ui/src/views/Settings.vue`, add a section showing the node's .onion address for remote access via Tor Browser.
- [ ]**Add Tor toggle in settings**: Allow users to disable Tor if they prefer clearnet (some use cases require it). Default should be Tor-on.
- [ ]**Verify Phase 9 — Tor active**: Bitcoin peers are onion-only, LND via Tor, .onion address displayed in UI.
---
## Phase 10: Encrypted Backup System
> **Layman version**: If your hardware dies, you lose everything — Bitcoin wallet, Lightning channels,
> all app data. We build an encrypted backup system so you can restore to new hardware. Start9 has this;
> we need it too.
- [ ]**Design backup manifest**: Create a backup manifest that lists what to back up per app: data directories, config files, secrets. Store in `apps/{app-id}/manifest.yml` under a `backup:` key.
4. Handles version migration if backup is from an older version.
- [ ]**Add scheduled backups**: Allow users to configure automatic backups (daily/weekly) to external storage.
- [ ]**Verify Phase 10 — Backup/restore works**: Create a backup, delete an app's data, restore from backup, verify app works.
---
## Phase 11: Automated Update System
> **Layman version**: Currently, updates require SSH access and running a script manually. Users need
> a "click to update" button like Umbrel has. We build this with atomic updates that can roll back
> if something breaks.
- [ ]**Design update architecture**: Plan the update mechanism:
1. Backend checks for updates by fetching a signed manifest from a known URL (or local file for air-gapped).
2. Updates are downloaded as delta tarballs (frontend + backend binary).
3. Applied atomically: new binary placed alongside old, symlink swapped.
4. Rollback: if health check fails after update, swap symlink back.
- [ ]**Implement update check RPC endpoint**: Add `system.check_updates` that fetches the update manifest and returns available version + changelog.
- [ ]**Implement update apply RPC endpoint**: Add `system.apply_update` that downloads, verifies signature, applies, and restarts.
- [ ]**Add rollback mechanism**: If the backend fails to start after update (health check via systemd), automatically roll back to previous binary.
- [ ]**Add update UI in Settings**: Show current version, available updates, changelog, and "Update Now" button with progress indicator.
- [ ]**Verify Phase 11 — Updates work**: Simulate an update (place a new binary version), apply it, verify the system comes back up. Simulate a bad update, verify rollback.
---
## Phase 12: App Ecosystem Expansion
> **Layman version**: We have ~15 apps. The Bitcoin essentials are covered, but users expect at least
> 30 apps to compete with Start9/RaspiBlitz. We add the most-requested apps with proper security hardening.
- [ ]**Add missing essential Bitcoin apps**: Ensure these are available and work out of the box:
1. Fulcrum (Electrum server alternative — faster than Electrs for large wallets)
2. Thunderhub (Lightning management — alternative to Ride the Lightning)
3. LNbits (Lightning toolkit with extensions)
4. Lightning Terminal (Loop, Pool, Faraday in one UI)
5. Specter Desktop (multisig wallet management)
- [ ]**Add privacy-enhancing apps**:
1. JoinMarket / JAM (CoinJoin — RaspiBlitz has this, we should too)
- [ ]**Harden all new app manifests**: Every new app must have:
-`readonly_root: true`
-`cap_drop: ALL` + only required caps added
- Non-root user (UID > 1000)
-`no-new-privileges: true`
- Pinned image by SHA256 digest
- Health check configured
- [ ]**Verify Phase 12 — All apps work**: Install each new app, verify it starts, verify the UI loads, verify it connects to Bitcoin/Lightning if needed.
> **Layman version**: We've fixed all the known bugs. Now we add proactive security measures — things
> that prevent entire classes of bugs from being exploitable, even if new bugs are introduced later.
- [ ]**Add Content Security Policy nonce support**: Replace `'unsafe-inline'` in CSP with nonce-based script loading. This requires the backend to generate a random nonce per page load and inject it into both the CSP header and the script tags.
- [ ]**Implement session timeout**: In `core/archipelago/src/session.rs`, add configurable session timeout (default 24 hours, configurable in settings). Auto-expire sessions that haven't been active.
- [ ]**Add "active sessions" management**: Show all active sessions in the Settings UI with last-active time and IP. Allow users to terminate individual sessions or "log out everywhere."
- [ ]**Require re-authentication for sensitive operations**: Password change, 2FA setup/disable, and recovery code regeneration should require entering the current password, even if already logged in.
- [ ]**Implement audit logging**: Log all security-relevant events (login, logout, failed login, password change, 2FA change, app install/uninstall) to a dedicated audit log file with timestamps and source IPs.
- [ ]**Verify Phase 13**: Session timeout works, active sessions visible, re-auth required for sensitive ops, audit log populated.
---
## Phase 14: ISO Build Hardening
> **Layman version**: The ISO installer creates the initial system. We harden it so that a freshly
> installed Archipelago is secure out of the box — no manual hardening needed.
- [ ]**Force password change on first boot**: The installer should require setting a unique admin password. No default passwords should work after first boot.
- [ ]**Enable automatic security updates for the OS**: Configure unattended-upgrades for Debian security patches only (not full upgrades).
- [ ]**Harden SSH configuration**: In the installed system's sshd_config:
1. Disable password authentication (key-only).
2. Disable root login.
3. Use ed25519 host keys only.
Note: This is for the PRODUCTION installed system, not the dev server.
- [ ]**Configure firewall (UFW)**: Enable UFW on first boot with:
- [ ]**Pin all container images in first-boot script by SHA256 digest**: Replace any remaining `:latest` or version-only tags with `image@sha256:...` digests. Document how to update digests when new versions are released.
- [ ]**Verify Phase 14**: Flash a test ISO, boot it, verify all hardening is active, verify apps work.
---
## Phase 15: Penetration Test Round 1
> **Layman version**: We've fixed everything we know about. Now we try to break in ourselves to find
> what we missed. This is a structured attempt to attack the system from different angles.
- [ ]**Network-level testing**: From another machine on the LAN:
1. Port scan: `nmap -sV 192.168.1.228` — only expected ports should be open.
> **Layman version**: Users should be able to verify that the binary they're running was built from
> the exact source code they can read. This prevents supply chain attacks — nobody can sneak in
> malicious code without it being visible in the source.
- [ ]**Containerized build environment**: Create a Dockerfile that builds the Rust backend and Vue frontend in a deterministic environment (pinned Rust version, pinned Node version, pinned system libraries).
- [ ]**Publish build checksums**: After each release build, publish SHA256 checksums of all artifacts (backend binary, frontend bundle, ISO image).
- [ ]**Document verification process**: Write instructions for users to verify their installed binary matches the published checksum.
- [ ]**Verify Phase 17**: Build the same commit twice in the containerized environment — checksums should match.
---
## Phase 18: Mobile Companion & Remote Access
> **Layman version**: Umbrel has a mobile app. Start9 uses Tor .onion addresses for remote access.
> We need at least one of these so users can check on their node from their phone.
- [ ]**Implement Tor hidden service for web UI**: The web UI should be accessible via a .onion address from Tor Browser on any device, anywhere in the world, without port forwarding.
- [ ]**Optimize web UI for mobile**: Make the Vue UI responsive for phone-sized screens. Test on iOS Safari and Android Chrome.
- [ ]**Add PWA support**: Make the web UI installable as a Progressive Web App on mobile devices.
- [ ]**Verify Phase 18**: Access the node via Tor Browser on a phone. Install as PWA. All core features work on mobile.
---
## Phase 19: CoinJoin Integration
> **Layman version**: RaspiBlitz has JoinMarket, RoninDojo had Whirlpool. CoinJoin is essential for
> Bitcoin privacy — it mixes your coins with others so transactions can't be traced back to you.
- [ ]**Integrate JoinMarket/JAM**: Add JoinMarket as a containerized app with the JAM web UI. Auto-connect to the local Bitcoin Core instance.
- [ ]**Add CoinJoin guide**: Document how to use JoinMarket for privacy, including maker/taker roles and fee settings.
- [ ]**Verify Phase 19**: JoinMarket starts, connects to Bitcoin Core, JAM UI accessible, can create a test CoinJoin (testnet or small amount).
---
## Phase 20: Advanced Mesh Features
> **Layman version**: The mesh networking is already unique. Now we polish it — make it more reliable,
> add peer reputation (trust peers who send valid data), and improve the steganography to resist
> more sophisticated analysis.
- [ ]**Implement peer reputation system**: Track which peers send valid vs invalid data. Peers that consistently send valid block headers get higher trust scores. Peers that send invalid data get deprioritized.
- [ ]**Improve steganography resistance**: Add timing jitter to mesh transmissions so traffic patterns don't reveal communication. Vary message sizes to resist traffic analysis.
- [ ]**Add mesh health dashboard**: Show mesh network status, connected peers, message latency, relay statistics in the web UI.
> **Layman version**: We did this in Phase 15 with the early fixes. Now we repeat it with the full
> production system including all new features. This is the final check before v1.0.
- [ ]**Repeat all Phase 15 tests**: Network, web, auth, container — every test from Phase 15.
- [ ]**Test new features**: Tor access, backup/restore, updates, CoinJoin, mesh.
- [ ]**Test adversarial mesh scenarios**:
1. Rogue peer sending fake identities — should be rejected (Phase 4 fix).
2. Rogue peer sending invalid Bitcoin data — should be filtered (Phase 4 fix).
3. Rogue peer sending fake emergency alerts — should be rejected (Phase 8 fix).
4. Replay attack on mesh messages — sequence numbers should detect.
- [ ]**Test disaster recovery**:
1. Kill the server during a backup — verify partial backups are handled safely.
2. Kill the server during an update — verify rollback works.
3. Corrupt the ratchet state file — verify atomic persistence prevented data loss (Phase 8 fix).
4. Lose the admin password — verify recovery codes work (Phase 7 fix).
- [ ]**Document all findings and fix any issues**.
---
## Phase 22: Dependency Audit & Supply Chain
> **Layman version**: Our code might be secure, but if a library we depend on has a vulnerability,
> we're still exposed. We audit every dependency.
- [ ]**Run cargo audit**: `cd core && cargo install cargo-audit && cargo audit`. Fix or document all advisories.
- [ ]**Run npm audit**: `cd neode-ui && npm audit`. Fix all critical and high severity issues.
- [ ]**Review transitive dependencies**: For each direct dependency, check its dependency tree for abandoned or suspicious packages.
- [ ]**Pin all Cargo.lock and package-lock.json**: Ensure these lock files are committed and used in all builds.
- [ ]**Set up automated dependency monitoring**: Configure Dependabot or similar for automated security alerts on dependency vulnerabilities.
- [ ]**Verify Phase 22**: Zero critical/high advisories in both `cargo audit` and `npm audit`.
---
## Phase 23: Performance & Reliability Under Load
> **Layman version**: Security under normal use is one thing. Security under stress (many users,
> large blockchain, limited resources) is another. We test that the system remains stable and secure
> when pushed to its limits.
- [ ]**Stress test RPC endpoints**: Send 1000 concurrent RPC requests — verify rate limiting works and the server doesn't crash.
- [ ]**Test with full blockchain**: Verify the system handles a 600GB+ blockchain without running out of disk space, memory, or CPU.
- [ ]**Test mesh under high message volume**: Send 100 messages per minute through the mesh — verify encryption/decryption keeps up and memory doesn't leak.
- [ ]**Test container resource limits**: Start all apps simultaneously — verify memory and CPU limits prevent any single app from starving others.
- [ ]**Monitor for memory leaks**: Run the backend for 7 days continuously. Monitor RSS memory — should be stable, not growing.
- [ ]**Verify Phase 23**: System stable after 7 days of continuous operation with all apps running.
---
## Phase 24: Final Review & v1.0 Release
> **Layman version**: Everything is fixed, tested, hardened, and tested again. This is the final
> review before declaring the system production-ready.
- [ ]**Full code review**: Review every module one more time:
- [ ]**Verify all Phase checks pass**: Go through every "Verify Phase N" checklist from Phases 1-23. Every check must pass.
- [ ]**Compare against competitors one final time**: Re-evaluate the competitive comparison table. Document where Archipelago stands on every dimension.
- [ ]**Create security advisory process**: Document how security vulnerabilities should be reported, triaged, and disclosed. Create a SECURITY.md in the repository.
- [ ]**Tag v1.0 release**: Create the release with full changelog, checksums, and documentation.
- [ ]**Build and publish v1.0 ISO**: Final ISO build with all hardening active.