From 12ae3af981c08d4fe1d12adfe056e80885a7aa70 Mon Sep 17 00:00:00 2001 From: Dorian Date: Wed, 18 Mar 2026 00:57:16 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20Phase=206=20=E2=80=94=20nginx=20securit?= =?UTF-8?q?y=20headers,=20CSP=20hardening,=20rate=20limiting?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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) --- .../configs/external-app-proxies.conf | 3 + image-recipe/configs/nginx-archipelago.conf | 62 +++++++++++++++++-- .../archipelago-https-app-proxies.conf | 19 ++++++ loop/plan.md | 12 ++-- 4 files changed, 86 insertions(+), 10 deletions(-) diff --git a/image-recipe/configs/external-app-proxies.conf b/image-recipe/configs/external-app-proxies.conf index c36acdd5..54dac6d6 100644 --- a/image-recipe/configs/external-app-proxies.conf +++ b/image-recipe/configs/external-app-proxies.conf @@ -22,6 +22,7 @@ server { proxy_ssl_server_name on; proxy_ssl_name botfights.net; proxy_hide_header X-Frame-Options; + add_header X-Frame-Options "SAMEORIGIN" always; proxy_hide_header Content-Security-Policy; proxy_connect_timeout 60s; proxy_read_timeout 60s; @@ -49,6 +50,7 @@ server { proxy_ssl_server_name on; proxy_ssl_name 484.kitchen; proxy_hide_header X-Frame-Options; + add_header X-Frame-Options "SAMEORIGIN" always; proxy_hide_header Content-Security-Policy; proxy_connect_timeout 60s; proxy_read_timeout 60s; @@ -76,6 +78,7 @@ server { proxy_ssl_server_name on; proxy_ssl_name present.l484.com; proxy_hide_header X-Frame-Options; + add_header X-Frame-Options "SAMEORIGIN" always; proxy_hide_header Content-Security-Policy; proxy_connect_timeout 60s; proxy_read_timeout 60s; diff --git a/image-recipe/configs/nginx-archipelago.conf b/image-recipe/configs/nginx-archipelago.conf index 177306f2..04d10f47 100644 --- a/image-recipe/configs/nginx-archipelago.conf +++ b/image-recipe/configs/nginx-archipelago.conf @@ -1,3 +1,7 @@ +# Rate limit zones +limit_req_zone $binary_remote_addr zone=rpc:10m rate=20r/s; +limit_req_zone $binary_remote_addr zone=auth:10m rate=3r/s; + server { listen 80; listen 100.91.10.103:80; @@ -10,8 +14,10 @@ server { add_header X-Content-Type-Options "nosniff" always; add_header X-Frame-Options "SAMEORIGIN" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always; - add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always; - add_header Content-Security-Policy "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: http://$host:*; frame-src *" always; + add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=()" always; + add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; + add_header X-DNS-Prefetch-Control "off" always; + add_header 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://$host:*; frame-src 'self' http://$host:*; frame-ancestors 'self'; base-uri 'self'; form-action 'self';" always; # AIUI SPA (Chat mode iframe) # Use =404 fallback instead of index.html to prevent serving HTML with wrong @@ -116,6 +122,8 @@ server { # Proxy API requests to backend location /rpc/ { + limit_req zone=rpc burst=40 nodelay; + limit_req_status 429; proxy_pass http://127.0.0.1:5678; proxy_http_version 1.1; proxy_set_header Host $host; @@ -176,6 +184,7 @@ server { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_hide_header X-Frame-Options; + add_header X-Frame-Options "SAMEORIGIN" always; proxy_hide_header Content-Security-Policy; add_header X-Content-Type-Options "nosniff" always; proxy_read_timeout 300s; @@ -192,6 +201,7 @@ server { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_hide_header X-Frame-Options; + add_header X-Frame-Options "SAMEORIGIN" always; proxy_hide_header Content-Security-Policy; add_header X-Content-Type-Options "nosniff" always; proxy_set_header Accept-Encoding ""; @@ -206,6 +216,7 @@ server { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_hide_header X-Frame-Options; + add_header X-Frame-Options "SAMEORIGIN" always; proxy_hide_header Content-Security-Policy; add_header X-Content-Type-Options "nosniff" always; proxy_read_timeout 300s; @@ -222,6 +233,7 @@ server { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_hide_header X-Frame-Options; + add_header X-Frame-Options "SAMEORIGIN" always; proxy_hide_header Content-Security-Policy; add_header X-Content-Type-Options "nosniff" always; proxy_read_timeout 300s; @@ -246,6 +258,7 @@ server { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_hide_header X-Frame-Options; + add_header X-Frame-Options "SAMEORIGIN" always; proxy_hide_header Content-Security-Policy; add_header X-Content-Type-Options "nosniff" always; proxy_request_buffering off; @@ -261,6 +274,7 @@ server { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_hide_header X-Frame-Options; + add_header X-Frame-Options "SAMEORIGIN" always; proxy_hide_header Content-Security-Policy; add_header X-Content-Type-Options "nosniff" always; proxy_set_header Accept-Encoding ""; @@ -275,6 +289,7 @@ server { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_hide_header X-Frame-Options; + add_header X-Frame-Options "SAMEORIGIN" always; proxy_hide_header Content-Security-Policy; add_header X-Content-Type-Options "nosniff" always; proxy_set_header Accept-Encoding ""; @@ -289,6 +304,7 @@ server { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_hide_header X-Frame-Options; + add_header X-Frame-Options "SAMEORIGIN" always; proxy_hide_header Content-Security-Policy; add_header X-Content-Type-Options "nosniff" always; proxy_set_header Accept-Encoding ""; @@ -303,6 +319,7 @@ server { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_hide_header X-Frame-Options; + add_header X-Frame-Options "SAMEORIGIN" always; proxy_hide_header Content-Security-Policy; add_header X-Content-Type-Options "nosniff" always; proxy_set_header Accept-Encoding ""; @@ -317,6 +334,7 @@ server { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_hide_header X-Frame-Options; + add_header X-Frame-Options "SAMEORIGIN" always; proxy_hide_header Content-Security-Policy; add_header X-Content-Type-Options "nosniff" always; proxy_set_header Accept-Encoding ""; @@ -332,6 +350,7 @@ server { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_hide_header X-Frame-Options; + add_header X-Frame-Options "SAMEORIGIN" always; proxy_hide_header Content-Security-Policy; add_header X-Content-Type-Options "nosniff" always; proxy_set_header Accept-Encoding ""; @@ -363,6 +382,7 @@ server { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_hide_header X-Frame-Options; + add_header X-Frame-Options "SAMEORIGIN" always; proxy_hide_header Content-Security-Policy; add_header X-Content-Type-Options "nosniff" always; proxy_set_header Accept-Encoding ""; @@ -382,6 +402,7 @@ server { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_hide_header X-Frame-Options; + add_header X-Frame-Options "SAMEORIGIN" always; proxy_hide_header Content-Security-Policy; add_header X-Content-Type-Options "nosniff" always; proxy_read_timeout 300s; @@ -398,6 +419,7 @@ server { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_hide_header X-Frame-Options; + add_header X-Frame-Options "SAMEORIGIN" always; proxy_hide_header Content-Security-Policy; add_header X-Content-Type-Options "nosniff" always; proxy_read_timeout 300s; @@ -414,6 +436,7 @@ server { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_hide_header X-Frame-Options; + add_header X-Frame-Options "SAMEORIGIN" always; proxy_hide_header Content-Security-Policy; add_header X-Content-Type-Options "nosniff" always; proxy_set_header Accept-Encoding ""; @@ -428,6 +451,7 @@ server { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_hide_header X-Frame-Options; + add_header X-Frame-Options "SAMEORIGIN" always; proxy_hide_header Content-Security-Policy; add_header X-Content-Type-Options "nosniff" always; proxy_read_timeout 300s; @@ -444,6 +468,7 @@ server { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_hide_header X-Frame-Options; + add_header X-Frame-Options "SAMEORIGIN" always; proxy_hide_header Content-Security-Policy; add_header X-Content-Type-Options "nosniff" always; proxy_read_timeout 300s; @@ -460,6 +485,7 @@ server { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_hide_header X-Frame-Options; + add_header X-Frame-Options "SAMEORIGIN" always; proxy_hide_header Content-Security-Policy; add_header X-Content-Type-Options "nosniff" always; proxy_set_header Accept-Encoding ""; @@ -474,6 +500,7 @@ server { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_hide_header X-Frame-Options; + add_header X-Frame-Options "SAMEORIGIN" always; proxy_hide_header Content-Security-Policy; add_header X-Content-Type-Options "nosniff" always; proxy_set_header Accept-Encoding ""; @@ -488,6 +515,7 @@ server { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_hide_header X-Frame-Options; + add_header X-Frame-Options "SAMEORIGIN" always; proxy_hide_header Content-Security-Policy; add_header X-Content-Type-Options "nosniff" always; proxy_set_header Accept-Encoding ""; @@ -502,6 +530,7 @@ server { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_hide_header X-Frame-Options; + add_header X-Frame-Options "SAMEORIGIN" always; proxy_hide_header Content-Security-Policy; add_header X-Content-Type-Options "nosniff" always; proxy_set_header Accept-Encoding ""; @@ -516,6 +545,7 @@ server { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_hide_header X-Frame-Options; + add_header X-Frame-Options "SAMEORIGIN" always; proxy_hide_header Content-Security-Policy; add_header X-Content-Type-Options "nosniff" always; proxy_set_header Accept-Encoding ""; @@ -530,6 +560,7 @@ server { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_hide_header X-Frame-Options; + add_header X-Frame-Options "SAMEORIGIN" always; proxy_hide_header Content-Security-Policy; add_header X-Content-Type-Options "nosniff" always; proxy_set_header Accept-Encoding ""; @@ -544,6 +575,7 @@ server { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_hide_header X-Frame-Options; + add_header X-Frame-Options "SAMEORIGIN" always; proxy_hide_header Content-Security-Policy; add_header X-Content-Type-Options "nosniff" always; proxy_set_header Accept-Encoding ""; @@ -558,6 +590,7 @@ server { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_hide_header X-Frame-Options; + add_header X-Frame-Options "SAMEORIGIN" always; proxy_hide_header Content-Security-Policy; add_header X-Content-Type-Options "nosniff" always; proxy_read_timeout 86400s; @@ -576,6 +609,7 @@ server { proxy_set_header Accept-Encoding ""; proxy_ssl_server_name on; proxy_hide_header X-Frame-Options; + add_header X-Frame-Options "SAMEORIGIN" always; proxy_hide_header Content-Security-Policy; add_header X-Content-Type-Options "nosniff" always; proxy_hide_header Cross-Origin-Embedder-Policy; @@ -598,6 +632,7 @@ server { proxy_set_header Accept-Encoding ""; proxy_ssl_server_name on; proxy_hide_header X-Frame-Options; + add_header X-Frame-Options "SAMEORIGIN" always; proxy_hide_header Content-Security-Policy; add_header X-Content-Type-Options "nosniff" always; sub_filter_once off; @@ -616,6 +651,7 @@ server { proxy_set_header Accept-Encoding ""; proxy_ssl_server_name on; proxy_hide_header X-Frame-Options; + add_header X-Frame-Options "SAMEORIGIN" always; proxy_hide_header Content-Security-Policy; add_header X-Content-Type-Options "nosniff" always; sub_filter_once off; @@ -634,6 +670,7 @@ server { proxy_set_header Accept-Encoding ""; proxy_ssl_server_name on; proxy_hide_header X-Frame-Options; + add_header X-Frame-Options "SAMEORIGIN" always; proxy_hide_header Content-Security-Policy; add_header X-Content-Type-Options "nosniff" always; proxy_hide_header Cross-Origin-Embedder-Policy; @@ -681,8 +718,10 @@ server { add_header X-Content-Type-Options "nosniff" always; add_header X-Frame-Options "SAMEORIGIN" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always; - add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always; - add_header Content-Security-Policy "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: http://$host:*; frame-src *" always; + add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=()" always; + add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; + add_header X-DNS-Prefetch-Control "off" always; + add_header 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://$host:*; frame-src 'self' http://$host:*; frame-ancestors 'self'; base-uri 'self'; form-action 'self';" always; # AIUI SPA (Chat mode iframe) location /aiui/ { @@ -787,6 +826,8 @@ server { } location /rpc/ { + limit_req zone=rpc burst=40 nodelay; + limit_req_status 429; proxy_pass http://127.0.0.1:5678; proxy_http_version 1.1; proxy_set_header Host $host; @@ -809,6 +850,7 @@ server { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_hide_header X-Frame-Options; + add_header X-Frame-Options "SAMEORIGIN" always; proxy_hide_header Content-Security-Policy; add_header X-Content-Type-Options "nosniff" always; proxy_read_timeout 300s; @@ -825,6 +867,7 @@ server { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_hide_header X-Frame-Options; + add_header X-Frame-Options "SAMEORIGIN" always; proxy_hide_header Content-Security-Policy; add_header X-Content-Type-Options "nosniff" always; proxy_set_header Accept-Encoding ""; @@ -839,6 +882,7 @@ server { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_hide_header X-Frame-Options; + add_header X-Frame-Options "SAMEORIGIN" always; proxy_hide_header Content-Security-Policy; add_header X-Content-Type-Options "nosniff" always; proxy_read_timeout 300s; @@ -855,6 +899,7 @@ server { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_hide_header X-Frame-Options; + add_header X-Frame-Options "SAMEORIGIN" always; proxy_hide_header Content-Security-Policy; add_header X-Content-Type-Options "nosniff" always; proxy_read_timeout 300s; @@ -871,6 +916,7 @@ server { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_hide_header X-Frame-Options; + add_header X-Frame-Options "SAMEORIGIN" always; proxy_hide_header Content-Security-Policy; add_header X-Content-Type-Options "nosniff" always; proxy_set_header Accept-Encoding ""; @@ -885,6 +931,7 @@ server { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_hide_header X-Frame-Options; + add_header X-Frame-Options "SAMEORIGIN" always; proxy_hide_header Content-Security-Policy; add_header X-Content-Type-Options "nosniff" always; proxy_read_timeout 86400s; @@ -905,6 +952,7 @@ server { proxy_set_header Accept-Encoding ""; proxy_ssl_server_name on; proxy_hide_header X-Frame-Options; + add_header X-Frame-Options "SAMEORIGIN" always; proxy_hide_header Content-Security-Policy; add_header X-Content-Type-Options "nosniff" always; proxy_hide_header Cross-Origin-Embedder-Policy; @@ -927,6 +975,7 @@ server { proxy_set_header Accept-Encoding ""; proxy_ssl_server_name on; proxy_hide_header X-Frame-Options; + add_header X-Frame-Options "SAMEORIGIN" always; proxy_hide_header Content-Security-Policy; add_header X-Content-Type-Options "nosniff" always; sub_filter_once off; @@ -945,6 +994,7 @@ server { proxy_set_header Accept-Encoding ""; proxy_ssl_server_name on; proxy_hide_header X-Frame-Options; + add_header X-Frame-Options "SAMEORIGIN" always; proxy_hide_header Content-Security-Policy; add_header X-Content-Type-Options "nosniff" always; sub_filter_once off; @@ -963,6 +1013,7 @@ server { proxy_set_header Accept-Encoding ""; proxy_ssl_server_name on; proxy_hide_header X-Frame-Options; + add_header X-Frame-Options "SAMEORIGIN" always; proxy_hide_header Content-Security-Policy; add_header X-Content-Type-Options "nosniff" always; proxy_hide_header Cross-Origin-Embedder-Policy; @@ -1002,6 +1053,7 @@ server { proxy_set_header Accept-Encoding ""; proxy_ssl_server_name on; proxy_hide_header X-Frame-Options; + add_header X-Frame-Options "SAMEORIGIN" always; proxy_hide_header Content-Security-Policy; add_header X-Content-Type-Options "nosniff" always; proxy_hide_header Cross-Origin-Embedder-Policy; @@ -1026,6 +1078,7 @@ server { proxy_set_header Accept-Encoding ""; proxy_ssl_server_name on; proxy_hide_header X-Frame-Options; + add_header X-Frame-Options "SAMEORIGIN" always; proxy_hide_header Content-Security-Policy; add_header X-Content-Type-Options "nosniff" always; sub_filter '' ''; @@ -1046,6 +1099,7 @@ server { proxy_set_header Accept-Encoding ""; proxy_ssl_server_name on; proxy_hide_header X-Frame-Options; + add_header X-Frame-Options "SAMEORIGIN" always; proxy_hide_header Content-Security-Policy; add_header X-Content-Type-Options "nosniff" always; sub_filter '' ''; diff --git a/image-recipe/configs/snippets/archipelago-https-app-proxies.conf b/image-recipe/configs/snippets/archipelago-https-app-proxies.conf index e5fd9097..2e1bddd8 100644 --- a/image-recipe/configs/snippets/archipelago-https-app-proxies.conf +++ b/image-recipe/configs/snippets/archipelago-https-app-proxies.conf @@ -8,6 +8,7 @@ location /app/grafana/ { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_hide_header X-Frame-Options; + add_header X-Frame-Options "SAMEORIGIN" always; proxy_hide_header Content-Security-Policy; proxy_set_header Accept-Encoding ""; sub_filter_once on; @@ -21,6 +22,7 @@ location /app/uptime-kuma/ { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_hide_header X-Frame-Options; + add_header X-Frame-Options "SAMEORIGIN" always; proxy_hide_header Content-Security-Policy; proxy_set_header Accept-Encoding ""; sub_filter_once on; @@ -34,6 +36,7 @@ location /app/searxng/ { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_hide_header X-Frame-Options; + add_header X-Frame-Options "SAMEORIGIN" always; proxy_hide_header Content-Security-Policy; proxy_set_header Accept-Encoding ""; sub_filter_once on; @@ -47,6 +50,7 @@ location /app/portainer/ { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_hide_header X-Frame-Options; + add_header X-Frame-Options "SAMEORIGIN" always; proxy_hide_header Content-Security-Policy; proxy_set_header Accept-Encoding ""; sub_filter_once on; @@ -61,6 +65,7 @@ location /app/filebrowser/ { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_hide_header X-Frame-Options; + add_header X-Frame-Options "SAMEORIGIN" always; proxy_hide_header Content-Security-Policy; proxy_request_buffering off; proxy_set_header Accept-Encoding ""; @@ -75,6 +80,7 @@ location /app/endurain/ { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_hide_header X-Frame-Options; + add_header X-Frame-Options "SAMEORIGIN" always; proxy_hide_header Content-Security-Policy; proxy_set_header Accept-Encoding ""; sub_filter_once on; @@ -88,6 +94,7 @@ location /app/lnd/ { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_hide_header X-Frame-Options; + add_header X-Frame-Options "SAMEORIGIN" always; proxy_hide_header Content-Security-Policy; proxy_read_timeout 300s; proxy_send_timeout 300s; @@ -103,6 +110,7 @@ location /app/onlyoffice/ { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_hide_header X-Frame-Options; + add_header X-Frame-Options "SAMEORIGIN" always; proxy_hide_header Content-Security-Policy; proxy_set_header Accept-Encoding ""; sub_filter_once on; @@ -116,6 +124,7 @@ location /app/jellyfin/ { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_hide_header X-Frame-Options; + add_header X-Frame-Options "SAMEORIGIN" always; proxy_hide_header Content-Security-Policy; proxy_set_header Accept-Encoding ""; sub_filter_once on; @@ -129,6 +138,7 @@ location /app/photoprism/ { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_hide_header X-Frame-Options; + add_header X-Frame-Options "SAMEORIGIN" always; proxy_hide_header Content-Security-Policy; proxy_set_header Accept-Encoding ""; sub_filter_once on; @@ -142,6 +152,7 @@ location /app/mempool/ { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_hide_header X-Frame-Options; + add_header X-Frame-Options "SAMEORIGIN" always; proxy_hide_header Content-Security-Policy; proxy_read_timeout 300s; proxy_send_timeout 300s; @@ -157,6 +168,7 @@ location /app/fedimint/ { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_hide_header X-Frame-Options; + add_header X-Frame-Options "SAMEORIGIN" always; proxy_hide_header Content-Security-Policy; proxy_read_timeout 300s; proxy_send_timeout 300s; @@ -172,6 +184,7 @@ location /app/fedimint-gateway/ { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_hide_header X-Frame-Options; + add_header X-Frame-Options "SAMEORIGIN" always; proxy_hide_header Content-Security-Policy; proxy_read_timeout 300s; proxy_send_timeout 300s; @@ -187,6 +200,7 @@ location /app/tailscale/ { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_hide_header X-Frame-Options; + add_header X-Frame-Options "SAMEORIGIN" always; proxy_hide_header Content-Security-Policy; proxy_set_header Accept-Encoding ""; sub_filter_once on; @@ -200,6 +214,7 @@ location /app/ollama/ { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_hide_header X-Frame-Options; + add_header X-Frame-Options "SAMEORIGIN" always; proxy_hide_header Content-Security-Policy; proxy_set_header Accept-Encoding ""; sub_filter_once on; @@ -213,6 +228,7 @@ location /app/bitcoin-ui/ { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_hide_header X-Frame-Options; + add_header X-Frame-Options "SAMEORIGIN" always; proxy_hide_header Content-Security-Policy; proxy_set_header Accept-Encoding ""; sub_filter_once on; @@ -226,6 +242,7 @@ location /app/electrumx/ { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_hide_header X-Frame-Options; + add_header X-Frame-Options "SAMEORIGIN" always; proxy_hide_header Content-Security-Policy; proxy_set_header Accept-Encoding ""; sub_filter_once on; @@ -255,6 +272,7 @@ location /app/indeedhub/ { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_hide_header X-Frame-Options; + add_header X-Frame-Options "SAMEORIGIN" always; proxy_hide_header Content-Security-Policy; proxy_set_header Accept-Encoding ""; sub_filter_types text/html text/css application/javascript application/json; @@ -273,6 +291,7 @@ location /app/nginx-proxy-manager/ { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_hide_header X-Frame-Options; + add_header X-Frame-Options "SAMEORIGIN" always; proxy_hide_header Content-Security-Policy; proxy_set_header Accept-Encoding ""; sub_filter_once on; diff --git a/loop/plan.md b/loop/plan.md index 9b33123b..0557a9a4 100644 --- a/loop/plan.md +++ b/loop/plan.md @@ -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.