# 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: ```typescript 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: ```nginx 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: ```rust fn cors_origin(req: &Request) -> 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: ```nginx 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"`