# Archipelago Security Assessment — Attack Surface Map

**Target:** 192.168.1.228 (Archipelago OS)
**Date:** 2026-03-06
**Phase:** Reconnaissance

---

## 1. Target Overview

### Technologies Detected

| Layer | Technology | Version |
|-------|-----------|---------|
| OS | Debian 12 (Bookworm) | — |
| Web Server | nginx | 1.22.1 |
| Reverse Proxy (containers) | OpenResty (Nginx Proxy Manager) | 2.14.0 |
| Backend | Rust (custom binary) | — |
| Frontend | Vue 3 + TypeScript + Vite 7 | — |
| Container Runtime | Podman (rootless) | — |
| SSH | OpenSSH | 9.2p1 |
| TLS | Self-signed cert (archipelago.local) | Valid 2026-02-17 to 2027-02-17 |

### Open Ports and Services

| Port | Service | Description | Auth Required |
|------|---------|-------------|---------------|
| 22/tcp | SSH | OpenSSH 9.2p1 (Debian) | Yes (password) |
| 80/tcp | HTTP | nginx 1.22.1 — Archipelago main UI | No |
| 81/tcp | HTTP | OpenResty — Nginx Proxy Manager | **No (setup:false)** |
| 443/tcp | HTTPS | nginx 1.22.1 — Self-signed TLS | No |
| 3000/tcp | HTTP | Grafana (proxied via /app/grafana/) | Per-app |
| 3001/tcp | HTTP | Uptime Kuma (proxied via /app/uptime-kuma/) | Per-app |
| 5678/tcp | HTTP | Archipelago Rust backend (JSON-RPC) | **None** |
| 8080/tcp | HTTPS | LND REST API (auto-generated cert) | Macaroon |
| 8081/tcp | HTTP | LND UI (proxied via /app/lnd/) | Per-app |
| 8082/tcp | HTTP | Vaultwarden (proxied via /app/vaultwarden/) | Per-app |

### Container Inventory (30 containers, confirmed via unauthenticated RPC)

bitcoin-knots, tailscale, filebrowser, bitcoin-ui, lnd, homeassistant, searxng, portainer, archy-mempool-db, grafana (exited), onlyoffice, archy-nbxplorer, archy-btcpay-db, mempool-electrs, nginx-proxy-manager, nextcloud, vaultwarden, uptime-kuma, immich_postgres, immich_redis, immich_server, jellyfin, photoprism, archy-lnd-ui, archy-electrs-ui, mempool-api, archy-mempool-web, btcpay-server, archy-tor, fedimint

---

## 2. Attack Surface Map

### 2.1 Backend RPC Endpoints (POST /rpc/v1)

All endpoints are exposed via a single JSON-RPC handler at `/rpc/v1`. **There is no authentication middleware** — every method is callable by any network client without a session token.

#### Authentication Methods
| Method | Purpose | Auth Check |
|--------|---------|------------|
| `auth.login` | Password login | Checks password (bcrypt) but **returns no session token** |
| `auth.logout` | Logout | No-op (returns null) |
| `auth.changePassword` | Change password + optional SSH password | Verifies current password internally |
| `auth.onboardingComplete` | Mark onboarding done | **None** |
| `auth.isOnboardingComplete` | Check onboarding status | **None** |
| `auth.resetOnboarding` | Reset onboarding state | **None** |

#### Container Management (all unauthenticated)
| Method | Purpose | Confirmed Callable |
|--------|---------|-------------------|
| `container-list` | List all containers with IDs, images, state | **Yes — full inventory returned** |
| `container-install` | Install container from manifest path | Yes (requires file path on server) |
| `container-start` | Start a container by app_id | Yes |
| `container-stop` | Stop a container by app_id | Yes |
| `container-remove` | Remove a container by app_id | Yes |
| `container-status` | Get container status | Yes (dev mode required) |
| `container-logs` | Get container logs | Yes (dev mode required) |
| `container-health` | Get container health | Yes (dev mode required) |

#### Package Management (all unauthenticated)
| Method | Purpose | Confirmed Callable |
|--------|---------|-------------------|
| `package.install` | Install Docker image as package | Yes |
| `package.start` | Start a package | **Yes — returned success for nonexistent ID** |
| `package.stop` | Stop a package | **Yes — returned success for nonexistent ID** |
| `package.restart` | Restart a package | Yes |
| `package.uninstall` | Uninstall a package | Yes |
| `bundled-app-start` | Start bundled app | Yes |
| `bundled-app-stop` | Stop bundled app | Yes |

#### Node Identity & Peers (all unauthenticated)
| Method | Purpose | Confirmed Callable |
|--------|---------|-------------------|
| `node.did` | **Get node DID and public key** | **Yes — returned full identity** |
| `node.signChallenge` | **Sign arbitrary challenge with node private key** | **Yes — returned valid signature** |
| `node.createBackup` | **Create encrypted backup of node identity** | **Yes — returned backup blob** |
| `node.tor-address` | Get Tor onion address | Yes |
| `node.nostr-publish` | Publish node identity to Nostr | Yes (requires config) |
| `node.nostr-pubkey` | Get Nostr public key | Yes |
| `node-nostr-verify-revoked` | Verify revocation status | Yes |
| `node-add-peer` | Add a peer node | Yes |
| `node-list-peers` | **List all peer nodes** | **Yes — returned peer list with onions** |
| `node-remove-peer` | Remove a peer | Yes |
| `node-send-message` | Send message to peer via Tor | Yes |
| `node-check-peer` | Check peer reachability | Yes |
| `node-messages-received` | Get received messages | Yes |
| `node-nostr-discover` | Discover peers via Nostr | Yes |

#### Bitcoin & Lightning (unauthenticated, errors reveal internal state)
| Method | Purpose | Confirmed Callable |
|--------|---------|-------------------|
| `bitcoin.getinfo` | Bitcoin node info | Yes (errors expose backend status) |
| `lnd.getinfo` | LND info | Yes (error reveals macaroon path) |

#### Utility
| Method | Purpose |
|--------|---------|
| `echo` / `server.echo` | Echo test (unauthenticated) |

### 2.2 HTTP Endpoints (non-RPC)

| Method | Path | Purpose | Auth |
|--------|------|---------|------|
| GET | `/health` | Health check → returns SPA HTML (nginx catch-all) | None |
| POST | `/archipelago/node-message` | **Receive P2P messages from other nodes** | **None** |
| GET | `/ws/db` | WebSocket for real-time state updates | **None** (proxied via nginx) |
| GET | `/aiui/api/claude/*` | Proxy to Claude API (port 3141) | **None at nginx level** |
| GET | `/aiui/api/openrouter/*` | **Open proxy to openrouter.ai** | **None** |
| GET | `/aiui/api/web-search` | Proxy to SearXNG (port 8888) | None |
| GET | `/app/{name}/*` | Proxy to 20+ containerized apps | Per-app (see below) |

### 2.3 App Proxies (nginx — all strip X-Frame-Options and CSP)

Every `/app/*` location block includes:
```
proxy_hide_header X-Frame-Options;
proxy_hide_header Content-Security-Policy;
```

This means every proxied app loses its own clickjacking and CSP protections when accessed through the Archipelago nginx reverse proxy.

| Path | Backend | Notable |
|------|---------|---------|
| `/app/nextcloud/` | :8085 | client_max_body_size not set (default 1MB) |
| `/app/vaultwarden/` | :8082 | **Password manager — CSP stripped** |
| `/app/immich/` | :2283 | Photo management |
| `/app/filebrowser/` | :8083 | **client_max_body_size 10G**, request_buffering off |
| `/app/portainer/` | :9000 | **Container management UI** |
| `/app/grafana/` | :3000 | Monitoring |
| `/app/jellyfin/` | :8096 | Media server |
| `/app/uptime-kuma/` | :3001 | Monitoring |
| `/app/searxng/` | :8888 | Search engine |
| `/app/onlyoffice/` | :9980 | Document editor |
| `/app/lnd/` | :8081 | Lightning UI |
| `/app/mempool/` | :4080 | Bitcoin explorer |
| `/app/btcpay/` | :23000 | Payment processing |
| `/app/homeassistant/` | :8123 | IoT (86400s timeout!) |
| `/app/photoprism/` | :2342 | Photo management |
| `/app/fedimint/` | :8175 | Federation mint |
| `/app/tailscale/` | :8240 | VPN |
| `/app/ollama/` | :11434 | **LLM API — could be used to run inference** |
| `/app/bitcoin-ui/` | :8334 | Bitcoin UI |
| `/app/electrs/` | :50002 | Electrs |
| `/app/nginx-proxy-manager/` | :81 | **Meta: proxy to proxy manager** |
| `/app/penpot/` | :9001 | Design tool |
| `/app/endurain/` | :8080 | Fitness tracker |

### 2.4 Input Vectors

| Vector | Location | Details |
|--------|----------|---------|
| JSON-RPC body | POST /rpc/v1 | All params parsed from JSON body, no size limit at app level |
| URL query params | GET /api/container/logs?app_id=X&lines=N | `app_id` passed to shell command (podman) |
| JSON body | POST /archipelago/node-message | `from_pubkey`, `message` fields stored directly |
| WebSocket | /ws/db | Receives state broadcasts, client messages not validated |
| File upload | /app/filebrowser/ | 10GB max upload via filebrowser proxy |
| Path | /proxy/lnd/* | Path suffix forwarded to internal LND REST API |

### 2.5 Authentication Mechanisms

**The system has a fundamental authentication design flaw:**

1. `auth.login` validates a password but **returns `null`** on success — no session token, no cookie, no JWT
2. There is no authentication middleware in the Rust backend — the `RpcHandler::handle()` function dispatches all methods without any auth check
3. The frontend likely manages auth state client-side only (localStorage/Pinia store)
4. The backend runs as **`User=root`** (per `archipelago.service`)
5. Dev mode is **permanently enabled** (`ARCHIPELAGO_DEV_MODE=true` in the systemd service)
6. Default dev password `password123` is hardcoded in source and referenced in CLAUDE.md

---

## 3. Interesting Findings

### 3.1 CRITICAL: No Server-Side Authentication on Any RPC Method

**Confirmed by testing:** Every single RPC method is callable without authentication. Container management, node identity operations, peer management, package installation — all accessible to any network client.

Evidence:
```
POST /rpc/v1 {"method":"container-list"} → Full container inventory
POST /rpc/v1 {"method":"node.did"} → Node DID + public key
POST /rpc/v1 {"method":"node.signChallenge","params":{"challenge":"test"}} → Valid signature
POST /rpc/v1 {"method":"node.createBackup","params":{"passphrase":"test"}} → Encrypted backup blob
POST /rpc/v1 {"method":"auth.resetOnboarding"} → Success (reset state)
POST /rpc/v1 {"method":"node-list-peers"} → Full peer list with .onion addresses
```

### 3.2 CRITICAL: Backend Runs as Root with Dev Mode Enabled

The systemd service file (`archipelago.service`) specifies:
```
User=root
Environment="ARCHIPELAGO_DEV_MODE=true"
```

Combined with unauthenticated RPC, this means an attacker can:
- Install arbitrary container images via `package.install`
- Start/stop/remove any container
- Execute `sudo podman` commands (the code calls `sudo podman` throughout)

### 3.3 HIGH: Arbitrary File Read via container-install

The `container-install` RPC method accepts a `manifest_path` parameter that is read directly from the filesystem:
```rust
let manifest_content = tokio::fs::read_to_string(manifest_path).await
```

Tested: sending `/etc/passwd` resulted in "Failed to parse manifest" (read succeeded, YAML parse failed). This is a confirmed arbitrary file read — the error message changes based on whether the file exists and is valid YAML.

### 3.4 HIGH: Node Private Key Signing Oracle

The `node.signChallenge` method signs arbitrary data with the node's Ed25519 private key — without authentication. An attacker can impersonate the node by signing any challenge.

### 3.5 HIGH: SSRF via LND Proxy

The handler at `/proxy/lnd/*` forwards requests to `http://127.0.0.1:8080` + the path suffix:
```rust
let url = format!("http://127.0.0.1:8080{}", suffix);
```

While the base URL is fixed, path manipulation could access unexpected LND REST endpoints. The proxy also adds `Access-Control-Allow-Origin: *` to all responses.

### 3.6 HIGH: Open Proxy to External API (OpenRouter)

The nginx config at `/aiui/api/openrouter/` proxies directly to `https://openrouter.ai/api/` without any authentication at the nginx layer. If the Claude proxy (port 3141) stores an API key, it could be abused for free inference.

### 3.7 HIGH: Nginx Proxy Manager Unconfigured

Port 81 returns `{"status":"OK","setup":false"}` — the Nginx Proxy Manager has never completed initial setup. An attacker could complete the setup process and gain control of the proxy configuration.

### 3.8 MEDIUM: Missing Security Headers

The main nginx server block has **zero** security headers:
- No `X-Frame-Options` (clickjacking)
- No `Content-Security-Policy`
- No `X-Content-Type-Options`
- No `Strict-Transport-Security`
- No `X-XSS-Protection`
- No `Referrer-Policy`
- Server header leaks version: `Server: nginx/1.22.1`

### 3.9 MEDIUM: CSP/X-Frame-Options Stripping on All App Proxies

Every `/app/*` proxy location explicitly strips `X-Frame-Options` and `Content-Security-Policy`. This removes clickjacking protection from security-sensitive apps like Vaultwarden (password manager) and Portainer (container management).

### 3.10 MEDIUM: CORS Wildcard on Multiple Endpoints

The Rust backend sets `Access-Control-Allow-Origin: *` on:
- `/api/container/logs`
- `/archipelago/node-message`
- `/electrs-status`
- `/proxy/lnd/*`

### 3.11 MEDIUM: Unauthenticated P2P Message Injection

`POST /archipelago/node-message` accepts arbitrary `from_pubkey` and `message` fields and stores them without any verification:
```rust
node_msg::store_received(&from, &msg).await;
```

An attacker can inject fake messages that appear to come from any peer.

### 3.12 MEDIUM: Information Disclosure

- Error messages leak internal paths and service state:
  - `"Failed to read LND admin macaroon — is LND installed?"` (reveals LND status)
  - `"Container orchestrator not available (dev mode required)"` (reveals mode)
- `container-list` returns full container IDs, image names with tags, ports
- `node.did` returns the node's cryptographic identity
- `node-list-peers` returns peer onion addresses and public keys
- NPM API reveals version `2.14.0`
- LND REST API on 8080 is directly accessible, reveals startup state
- Vaultwarden on 8082 is directly accessible

### 3.13 LOW: Self-Signed TLS Certificate

The HTTPS certificate is self-signed with `commonName=archipelago.local`. SAN includes both server IPs (192.168.1.228 and 192.168.1.198) and a Tailscale IP (10.0.0.1). This is expected for a local appliance but enables MitM if users accept the cert.

### 3.14 LOW: Session Secret Placeholder

`core/.env.production` contains `ARCHIPELAGO_SESSION_SECRET=CHANGE_ME_ON_FIRST_RUN`. If this value is ever used for session signing, all sessions would be forgeable.

### 3.15 INFO: Docker Images Using `latest` Tag

Several containers use `latest` tags (bitcoin-knots, tailscale, searxng, mempool-electrs, nginx-proxy-manager, uptime-kuma, photoprism, archy-tor), violating the project's own security policy of pinning versions.

---

## 4. Priority Targets

### P1: CRITICAL — Complete Authentication Bypass on All RPC Methods

- **What:** Every RPC method (container management, node identity, package install, peer management) is callable without authentication
- **Why it's interesting:** Full administrative control over the node from any device on the same network. An attacker can stop Bitcoin/LND, install malicious containers, exfiltrate the node identity, and manipulate peer relationships
- **Category:** Broken Authentication (OWASP A07:2021)
- **Confirmed:** Yes — tested every major method category unauthenticated
- **Impact:** Critical — full system compromise from LAN

### P2: CRITICAL — Arbitrary File Read via container-install manifest_path

- **What:** The `container-install` RPC method reads any file path on the server filesystem (as root)
- **Why it's interesting:** Can read `/etc/shadow`, private keys, LND macaroons, Bitcoin wallet files, or any secret on the system. The file content leaks through YAML parsing errors for non-YAML files, and returns full content for valid YAML files
- **Category:** Path Traversal / Arbitrary File Read (OWASP A01:2021)
- **Confirmed:** Yes — `/etc/passwd` was read successfully (parse error confirms read)
- **Impact:** Critical — read any file as root

### P3: HIGH — Node Private Key Signing Oracle

- **What:** `node.signChallenge` signs any attacker-supplied data with the node's Ed25519 private key, no auth required
- **Why it's interesting:** Enables complete node identity impersonation. An attacker can forge proofs-of-control, sign messages as the node, and potentially steal funds if the key is used for financial operations
- **Category:** Broken Authentication + Cryptographic Failures (OWASP A02:2021)
- **Confirmed:** Yes — received valid signature for arbitrary challenge
- **Impact:** High — node identity theft

### P4: HIGH — Unauthenticated Container/Package Management

- **What:** `package.install`, `package.stop`, `container-stop`, `container-remove` all work without authentication
- **Why it's interesting:** An attacker can install a malicious container image (e.g., cryptominer, reverse shell) or stop critical services (Bitcoin node, LND). The `package.install` method pulls and runs arbitrary Docker images as root
- **Category:** Broken Access Control (OWASP A01:2021)
- **Confirmed:** Yes — `package.stop` returned success for test input; `container-list` returned full inventory
- **Impact:** High — arbitrary code execution via container, denial of service

### P5: HIGH — Nginx Proxy Manager Setup Not Complete (Takeover)

- **What:** NPM on port 81 returns `"setup":false` — initial admin account was never created
- **Why it's interesting:** An attacker can complete the setup wizard, create an admin account, and gain full control over the reverse proxy configuration — redirecting traffic, adding new proxy hosts, or intercepting TLS
- **Category:** Security Misconfiguration (OWASP A05:2021)
- **Confirmed:** Yes — API returns setup:false; default credentials rejected (setup truly incomplete)
- **Impact:** High — proxy takeover, traffic interception

### P6: HIGH — Backend Running as Root with Dev Mode

- **What:** The `archipelago.service` runs the backend as `User=root` with `ARCHIPELAGO_DEV_MODE=true` permanently
- **Why it's interesting:** All `sudo podman` calls succeed trivially. Combined with unauthenticated RPC, this gives an attacker root-level container operations. Dev mode may enable additional attack surface
- **Category:** Security Misconfiguration (OWASP A05:2021)
- **Confirmed:** Yes — from systemd unit file in source
- **Impact:** High — amplifies all other vulnerabilities

### P7: MEDIUM — SSRF via /proxy/lnd/ and /aiui/api/openrouter/

- **What:** Two server-side proxy endpoints forward requests to internal/external services without authentication
- **Why it's interesting:** `/proxy/lnd/` provides access to the LND REST API (potentially allowing channel/wallet operations). `/aiui/api/openrouter/` is an open proxy to an external AI API
- **Category:** SSRF (OWASP A10:2021)
- **Confirmed:** Partial — endpoints respond, but LND returns "starting up" for the specific test
- **Impact:** Medium — access to internal services, potential financial operations

### P8: MEDIUM — CSP/X-Frame-Options Stripping Enables Clickjacking

- **What:** All 20+ app proxy locations strip `X-Frame-Options` and `Content-Security-Policy` headers
- **Why it's interesting:** Enables clickjacking attacks on Vaultwarden (password manager), Portainer (container admin), and other sensitive applications
- **Category:** Security Misconfiguration (OWASP A05:2021)
- **Confirmed:** Yes — from nginx config source code
- **Impact:** Medium — credential theft via clickjacking on password manager

### P9: MEDIUM — P2P Message Injection

- **What:** `POST /archipelago/node-message` accepts and stores messages with arbitrary `from_pubkey` without signature verification
- **Why it's interesting:** Enables spoofing messages from trusted peers, potentially manipulating node operator behavior or triggering automated responses
- **Category:** Injection / Insufficient Verification (OWASP A03:2021)
- **Confirmed:** Yes — received `{"ok":true}` for spoofed message
- **Impact:** Medium — social engineering, trust manipulation

### P10: LOW — Missing Security Headers (Entire Application)

- **What:** No CSP, HSTS, X-Frame-Options, X-Content-Type-Options on the main application
- **Why it's interesting:** Standard hardening gap that enables various client-side attacks
- **Category:** Security Misconfiguration (OWASP A05:2021)
- **Confirmed:** Yes — from HTTP response headers
- **Impact:** Low — enables other attacks (XSS, clickjacking, MIME sniffing)

---

## Summary

The most critical finding is the **complete absence of server-side authentication** on the RPC API. The `auth.login` method validates passwords but never issues session tokens, and no middleware checks authentication before dispatching RPC methods. Combined with the backend running as root, this gives any LAN attacker full administrative control over the node — including container management, node identity operations, and file system access.

**Immediate recommendations:**
1. Implement session-based authentication middleware that gates all RPC methods except `auth.login`, `echo`, and `auth.isOnboardingComplete`
2. Fix the `container-install` path traversal by validating `manifest_path` against an allowlist of directories
3. Require authentication for `node.signChallenge` and `node.createBackup`
4. Complete or disable the Nginx Proxy Manager setup on port 81
5. Stop running the backend as root; switch to a dedicated service account
6. Disable dev mode in production (`ARCHIPELAGO_DEV_MODE=false`)
