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 SeeCLAUDE.mdfor 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 thecall()method'sfetch()options. Then incore/archipelago/src/api/rpc/mod.rs, find where the RPC POST handler processes requests and add a check: if theX-Requested-Withheader is missing or notXMLHttpRequest, return 403 Forbidden. This blocks cross-origin form submissions that bypass CORS preflight. Also add the same header toneode-ui/src/api/container-client.tsif it makes direct fetch calls. Runcargo clippyon 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 mainserverblock (before thelocationblocks), 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.confif 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 allproxy_hide_header X-Frame-Options;lines inside app proxy location blocks (e.g.,/app/mempool/,/app/btcpay/, etc.). Replace eachproxy_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 asanitizePathfunction 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
safePathhelper (or the raw path variable) withsanitizePath(path)in these methods:list(),getDownloadUrl(),upload(),createFolder(),rename(),delete(). Search forsafePathin the file and update each occurrence. Runcd neode-ui && npm run type-checkto 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:69exposes 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:- 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; } - In
filebrowser-client.ts, changegetDownloadUrl()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. - Alternatively, create a backend proxy endpoint in
core/archipelago/src/api/handler.rsthat 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.
- In
-
Replace wildcard CORS with specific origins: In
core/archipelago/src/api/handler.rs, findconst 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-Originto the requesting origin (only if it matches) instead of*. Apply to all endpoints that currently useCORS_ANY:/api/container/logs,/archipelago/node-message,/proxy/lnd/. Add properOPTIONSpreflight handling for these endpoints (return 204 with CORS headers). Runcargo clippyafter 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):
- In
core/archipelago/src/api/rpc/auth.rs, when a session is created (login), generate a random CSRF token usingrand::Rngandhex::encode. - Store the CSRF token alongside the session in the session map.
- Return the CSRF token in the login response JSON:
"csrf_token": csrf_token. - In
core/archipelago/src/api/rpc/mod.rs, for all state-changing RPC methods, validate that theX-CSRF-Tokenheader matches the session's stored token. Return 403 if missing/mismatched. - Exempt
auth.loginfrom CSRF validation.
Frontend (Vue):
- In the login handler, store the CSRF token from the response:
localStorage.setItem('csrf_token', res.csrf_token). - In
neode-ui/src/api/rpc-client.ts, add'X-CSRF-Token': localStorage.getItem('csrf_token') || ''to every request header. - 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. - In
-
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:
curl -sI http://192.168.1.228shows X-Content-Type-Options, Referrer-Policy headerscurl -sI http://192.168.1.228/app/mempool/shows X-Frame-Options: SAMEORIGIN- RPC calls include X-Requested-With header (check browser DevTools)
- FileBrowser download URLs don't contain auth tokens
- CORS responses use specific origin, not
*:curl -H "Origin: http://evil.com" http://192.168.1.228/api/container/logs— should NOT returnAccess-Control-Allow-Origin: * - CSRF token present in RPC requests
- 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"