feat: Phase 6 — nginx security headers, CSP hardening, rate limiting

- CSP: removed unsafe-eval, tightened frame-src to self + host ports,
  added frame-ancestors, base-uri, form-action directives
- X-Frame-Options: SAMEORIGIN added after proxy_hide_header on all app proxies
- HSTS: max-age=31536000; includeSubDomains on all server blocks
- Rate limiting: 20r/s on /rpc/ with burst=40, 3r/s auth zone
- Added X-DNS-Prefetch-Control, Permissions-Policy payment=() header

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-18 00:57:16 +00:00
parent 3418c273d4
commit 022e7e484a
4 changed files with 86 additions and 10 deletions

View File

@@ -484,27 +484,27 @@
> protect users. We add headers that prevent clickjacking, content type confusion, and XSS. We also
> add rate limiting so attackers can't overwhelm the server with requests.
- [ ] **Fix Content Security Policy**: In `image-recipe/configs/nginx-archipelago.conf`, find line ~14 with the existing CSP. Replace the CSP header with a strict version:
- [x] **Fix Content Security Policy**: In `image-recipe/configs/nginx-archipelago.conf`, find line ~14 with the existing CSP. Replace the CSP header with a strict version:
```nginx
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; 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'; base-uri 'self'; form-action 'self';" always;
```
Note: `'unsafe-inline'` for styles is needed because Vue scoped styles sometimes inject inline styles. `'unsafe-eval'` is removed — if the app breaks, it means some JS is using `eval()` which should be fixed in code instead.
Deploy the nginx config. Test the web UI thoroughly — if anything breaks, check browser console for CSP violations and adjust the policy minimally.
- [ ] **Replace X-Frame-Options stripping with SAMEORIGIN**: In `image-recipe/configs/snippets/archipelago-https-app-proxies.conf`, find all 38 occurrences of `proxy_hide_header X-Frame-Options;`. For each one, add after it:
- [x] **Replace X-Frame-Options stripping with SAMEORIGIN**: In `image-recipe/configs/snippets/archipelago-https-app-proxies.conf`, find all 38 occurrences of `proxy_hide_header X-Frame-Options;`. For each one, add after it:
```nginx
add_header X-Frame-Options "SAMEORIGIN" always;
```
This allows Archipelago's own UI to iframe apps but blocks external sites from framing them. Do the same in the HTTP config in `nginx-archipelago.conf`.
Deploy and test: open an app in the Archipelago iframe — should still load.
- [ ] **Add HSTS header**: In `image-recipe/configs/nginx-archipelago.conf`, add to the HTTPS server block (or main server block if using HTTPS):
- [x] **Add HSTS header**: In `image-recipe/configs/nginx-archipelago.conf`, add to the HTTPS server block (or main server block if using HTTPS):
```nginx
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
```
Note: Do NOT add `preload` — this is a local server, not a public domain.
- [ ] **Add rate limiting to RPC endpoint**: In `image-recipe/configs/nginx-archipelago.conf`, add at the top (before the `server` block):
- [x] **Add rate limiting to RPC endpoint**: In `image-recipe/configs/nginx-archipelago.conf`, add at the top (before the `server` block):
```nginx
# Rate limit zones
limit_req_zone $binary_remote_addr zone=rpc:10m rate=20r/s;
@@ -518,7 +518,7 @@
For auth-specific endpoints, apply stricter limits in the backend or add a separate location for auth RPCs.
Deploy and test: normal UI use should work fine. Rapid-fire requests should get 429 responses.
- [ ] **Add remaining security headers**: In `image-recipe/configs/nginx-archipelago.conf`, add to the server block:
- [x] **Add remaining security headers**: In `image-recipe/configs/nginx-archipelago.conf`, add to the server block:
```nginx
add_header X-Content-Type-Options "nosniff" always;
add_header X-DNS-Prefetch-Control "off" always;
@@ -527,7 +527,7 @@
```
Deploy and verify: `curl -sI http://192.168.1.198 | grep -i "x-content\|referrer\|permissions\|strict-transport"`.
- [ ] **Verify Phase 6 — Nginx hardened**: Run these checks from another machine:
- [x] **Verify Phase 6 — Nginx hardened**: Run these checks from another machine:
1. `curl -sI http://192.168.1.198 | grep -i "content-security-policy"` — CSP header present, no `unsafe-eval`.
2. `curl -sI http://192.168.1.198 | grep -i "x-content-type"` — `nosniff` present.
3. `curl -sI http://192.168.1.198 | grep -i "x-frame-options"` — present on app proxies.