archy/loops/plan.md
2026-03-17 00:03:08 +00:00

8.6 KiB

Overnight Plan — Security Audit Remediation

Fix every finding from the 2026-03-05 security audit (docs/security-audit-2026-03-05.md). No new features, no design changes. Pure security hardening. Deploy after every change: ./scripts/deploy-to-target.sh --live — test at http://192.168.1.228 See CLAUDE.md for all project rules and conventions.


Phase 1: Low-Effort / High-Impact Fixes

  • Add X-Requested-With header to RPC client + validate server-side: In neode-ui/src/api/rpc-client.ts, add 'X-Requested-With': 'XMLHttpRequest' to the headers object in the call() method's fetch() options. Then in core/archipelago/src/api/rpc/mod.rs, find where the RPC POST handler processes requests and add a check: if the X-Requested-With header is missing or not XMLHttpRequest, return 403 Forbidden. This blocks cross-origin form submissions that bypass CORS preflight. Also add the same header to neode-ui/src/api/container-client.ts if it makes direct fetch calls. Run cargo clippy on the server after the Rust change. Test by visiting http://192.168.1.228 and verifying login + container start/stop still work.

  • Add nginx security headers (nosniff, referrer-policy): Edit image-recipe/configs/nginx-archipelago.conf. In the main server block (before the location blocks), add these headers:

    add_header X-Content-Type-Options "nosniff" always;
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;
    add_header X-DNS-Prefetch-Control "off" always;
    add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
    

    Also add the same headers to the HTTPS server block in image-recipe/configs/snippets/archipelago-https-app-proxies.conf if it has a separate server block. After editing, deploy to .228 and verify headers appear: curl -sI http://192.168.1.228 | grep -i "x-content-type\|referrer-policy". Then rsync the nginx config to the live server: sudo cp ~/archy/image-recipe/configs/nginx-archipelago.conf /etc/nginx/sites-available/archipelago && sudo nginx -t && sudo systemctl reload nginx.

  • Replace X-Frame-Options stripping with SAMEORIGIN override: In image-recipe/configs/nginx-archipelago.conf, find all proxy_hide_header X-Frame-Options; lines inside app proxy location blocks (e.g., /app/mempool/, /app/btcpay/, etc.). Replace each proxy_hide_header X-Frame-Options; with:

    proxy_hide_header X-Frame-Options;
    add_header X-Frame-Options "SAMEORIGIN" always;
    

    This allows Archipelago to iframe the apps but blocks external sites from framing them. Do the same in the HTTPS config at image-recipe/configs/snippets/archipelago-https-app-proxies.conf. Test by opening an app in the Archipelago UI iframe — it should still load. Then try loading the app URL directly in an iframe from a different origin — it should be blocked. Deploy nginx config to .228 and reload.

  • Sanitize FileBrowser paths client-side: In neode-ui/src/api/filebrowser-client.ts, add a sanitizePath function near the top:

    function sanitizePath(path: string): string {
      const segments = path.split('/').filter(s => s !== '..' && s !== '.' && s !== '')
      return '/' + segments.join('/')
    }
    

    Then replace all uses of the safePath helper (or the raw path variable) with sanitizePath(path) in these methods: list(), getDownloadUrl(), upload(), createFolder(), rename(), delete(). Search for safePath in the file and update each occurrence. Run cd neode-ui && npm run type-check to verify. Test by browsing Cloud folders in the UI — navigation, downloads, uploads should all still work.


Phase 2: Medium-Effort Fixes

  • Move FileBrowser download auth from URL to header-based proxy: Currently filebrowser-client.ts:69 exposes the JWT token in download URLs (?auth=${this.token}). Fix by adding an nginx location block that proxies download requests and injects the auth header server-side:

    1. In image-recipe/configs/nginx-archipelago.conf, add a new location block:
      location /api/cloud/download {
          internal;
          proxy_pass http://127.0.0.1:8083/api/raw$arg_path;
          proxy_set_header X-Auth $arg_token;
          proxy_hide_header X-Frame-Options;
      }
      
    2. In filebrowser-client.ts, change getDownloadUrl() to return /api/cloud/download?path=${encodeURIComponent(path)}&token=${this.token} — the token goes to nginx only, not exposed in browser history or referer headers.
    3. Alternatively, create a backend proxy endpoint in core/archipelago/src/api/handler.rs that reads the path, fetches from FileBrowser with the token in headers, and streams back. Test by downloading a file from Cloud — it should work without the token appearing in the browser URL bar.
  • Replace wildcard CORS with specific origins: In core/archipelago/src/api/handler.rs, find const CORS_ANY: &str = "*" and all places it's used. Replace the wildcard with the actual requesting origin if it matches an allowlist. Add a helper function:

    fn cors_origin(req: &Request<hyper::Body>) -> String {
        req.headers()
            .get("Origin")
            .and_then(|v| v.to_str().ok())
            .filter(|o| o.starts_with("http://192.168.") || o.starts_with("https://192.168.") || o.starts_with("http://localhost") || o.starts_with("http://100."))
            .unwrap_or("")
            .to_string()
    }
    

    Use this to set Access-Control-Allow-Origin to the requesting origin (only if it matches) instead of *. Apply to all endpoints that currently use CORS_ANY: /api/container/logs, /archipelago/node-message, /proxy/lnd/. Add proper OPTIONS preflight handling for these endpoints (return 204 with CORS headers). Run cargo clippy after changes. Test by verifying container logs, node messages, and LND proxy still work in the UI.


Phase 3: High-Effort Fixes

  • Implement CSRF synchronizer token pattern: Two-part change:

    Backend (Rust):

    1. In core/archipelago/src/api/rpc/auth.rs, when a session is created (login), generate a random CSRF token using rand::Rng and hex::encode.
    2. Store the CSRF token alongside the session in the session map.
    3. Return the CSRF token in the login response JSON: "csrf_token": csrf_token.
    4. In core/archipelago/src/api/rpc/mod.rs, for all state-changing RPC methods, validate that the X-CSRF-Token header matches the session's stored token. Return 403 if missing/mismatched.
    5. Exempt auth.login from CSRF validation.

    Frontend (Vue):

    1. In the login handler, store the CSRF token from the response: localStorage.setItem('csrf_token', res.csrf_token).
    2. In neode-ui/src/api/rpc-client.ts, add 'X-CSRF-Token': localStorage.getItem('csrf_token') || '' to every request header.
    3. On 403 CSRF error responses, redirect to login.

    Test by logging in and performing actions (start/stop containers, change settings). Then try crafting a cross-origin POST to /rpc/v1 — it should fail with 403.

  • Add Content-Security-Policy header: In image-recipe/configs/nginx-archipelago.conf, add a CSP header to the main UI location block. Start with report-only:

    add_header Content-Security-Policy-Report-Only "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self' data:; connect-src 'self' ws: wss:; frame-src 'self'; frame-ancestors 'self';" always;
    

    Deploy and check browser console for CSP violations. Fix violations by adjusting the policy. Once clean, change to enforcing Content-Security-Policy. Do NOT apply CSP to app proxy locations — those are third-party apps with different needs.


Post-Fix Verification

  • Run full security re-audit: After all fixes, verify each finding:

    1. curl -sI http://192.168.1.228 shows X-Content-Type-Options, Referrer-Policy headers
    2. curl -sI http://192.168.1.228/app/mempool/ shows X-Frame-Options: SAMEORIGIN
    3. RPC calls include X-Requested-With header (check browser DevTools)
    4. FileBrowser download URLs don't contain auth tokens
    5. CORS responses use specific origin, not *: curl -H "Origin: http://evil.com" http://192.168.1.228/api/container/logs — should NOT return Access-Control-Allow-Origin: *
    6. CSRF token present in RPC requests
    7. All UI features still work (login, apps, cloud, web5, settings)
  • Deploy to all nodes: After verification on .228, deploy to Arch 1 (100.82.97.63), Arch 2 (100.122.84.60), Arch 3 (100.124.105.113), .198 (192.168.1.198). Verify health on each: curl -sI http://{ip}/ | grep -i "x-content-type\|referrer"