archy/loop/pentest/recon-attack-surface.md
Dorian 6623dbc4ab chore: add security pentest reports and remediation plan
Overnight pentest run produced recon, analysis, exploitation reports,
and a full security assessment. Plan.md updated with 22 prioritized
fix items for auth, SSRF, injection, XSS, and hardening.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 03:08:14 +00:00

21 KiB

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:

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:

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:

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)