From 41618d30fc1fdc9ef26aa5003ac48fadf0d1fe82 Mon Sep 17 00:00:00 2001 From: Christian Cleberg Date: Mon, 3 Aug 2026 11:43:15 -0500 Subject: revamp security headers and site confs --- linux/nginx/etc/nginx/conf.d/cleberg.dev.conf | 1 + linux/nginx/etc/nginx/conf.d/cleberg.net.conf | 25 +++++- linux/nginx/etc/nginx/conf.d/cv.conf | 2 + linux/nginx/etc/nginx/conf.d/files.conf | 2 + linux/nginx/etc/nginx/conf.d/hn.conf | 1 + linux/nginx/etc/nginx/conf.d/img.conf | 2 + linux/nginx/etc/nginx/conf.d/office.conf | 2 + linux/nginx/etc/nginx/conf.d/org.conf | 2 + linux/nginx/etc/nginx/conf.d/piped.conf | 75 ++++++----------- linux/nginx/etc/nginx/conf.d/projects.conf | 22 ----- .../etc/nginx/conf.d/reminiscecleberg.com.conf | 1 + linux/nginx/etc/nginx/conf.d/rogue.conf | 15 +++- linux/nginx/etc/nginx/conf.d/rss.conf | 8 ++ .../etc/nginx/custom.d/http/content_type_maps.conf | 95 ++++++++++++++++++++-- .../security/content-security-policy-cmc.conf | 8 ++ ...ntent-security-policy-freshrss-report-only.conf | 4 + .../security/content-security-policy-inline.conf | 9 ++ .../content-security-policy-piped-report-only.conf | 5 ++ .../custom.d/security/headers_in_location.conf | 27 ++++++ .../security/headers_in_location_media.conf | 11 +++ .../security/headers_in_location_no_xfo.conf | 12 +++ 21 files changed, 239 insertions(+), 90 deletions(-) delete mode 100644 linux/nginx/etc/nginx/conf.d/projects.conf create mode 100644 linux/nginx/etc/nginx/custom.d/security/content-security-policy-cmc.conf create mode 100644 linux/nginx/etc/nginx/custom.d/security/content-security-policy-freshrss-report-only.conf create mode 100644 linux/nginx/etc/nginx/custom.d/security/content-security-policy-inline.conf create mode 100644 linux/nginx/etc/nginx/custom.d/security/content-security-policy-piped-report-only.conf create mode 100644 linux/nginx/etc/nginx/custom.d/security/headers_in_location.conf create mode 100644 linux/nginx/etc/nginx/custom.d/security/headers_in_location_media.conf create mode 100644 linux/nginx/etc/nginx/custom.d/security/headers_in_location_no_xfo.conf diff --git a/linux/nginx/etc/nginx/conf.d/cleberg.dev.conf b/linux/nginx/etc/nginx/conf.d/cleberg.dev.conf index 69282e0..bd7dd07 100644 --- a/linux/nginx/etc/nginx/conf.d/cleberg.dev.conf +++ b/linux/nginx/etc/nginx/conf.d/cleberg.dev.conf @@ -5,6 +5,7 @@ server { server_name cleberg.dev; root /var/www/cleberg.dev/; include custom.d/basic.conf; + include custom.d/security/content-security-policy.conf; location / { try_files $uri $uri/ =404; } } diff --git a/linux/nginx/etc/nginx/conf.d/cleberg.net.conf b/linux/nginx/etc/nginx/conf.d/cleberg.net.conf index 6030fd0..fb24a37 100644 --- a/linux/nginx/etc/nginx/conf.d/cleberg.net.conf +++ b/linux/nginx/etc/nginx/conf.d/cleberg.net.conf @@ -5,10 +5,15 @@ server { # No per-vhost error_log: it overrode the global "off" and wrote visitor # IPs to disk. - # basic.conf now carries HSTS + Permissions-Policy. CSP stays explicit -- - # the shared policy is written for this vhost (it allows img.cleberg.net). + # basic.conf carries HSTS + Permissions-Policy. CSP stays explicit, and now + # uses the cmc-specific policy: the shared $content_security_policy was made + # identity-neutral so it is safe to reach for on a krz vhost by default. + # + # NOTE: every `location` below that sets its own add_header must ALSO + # include these two files, or it discards them (nginx add_header does not + # merge across levels). See custom.d/security/headers_in_location.conf. include custom.d/basic.conf; - include custom.d/security/content-security-policy.conf; + include custom.d/security/content-security-policy-cmc.conf; root /var/www/cleberg.net/; include custom.d/redirects/blog.conf; port_in_redirect off; @@ -21,6 +26,10 @@ server { default_type text/markdown; add_header Content-Type "text/markdown; charset=utf-8" always; add_header Vary "Accept" always; + # Restore what this block's own add_header discarded: + include custom.d/security/headers_in_location.conf; + include custom.d/security/content-security-policy-cmc.conf; + add_header Onion-Location "http://paske4urhs6nttrtlkuwa5cowum3fjkc6yv6kl4ncx3mjxcd77764nqd.onion$request_uri" always; } location = / { @@ -29,6 +38,11 @@ server { } add_header Vary "Accept" always; + # Restore what this block's own add_header discarded. Without these the + # HOMEPAGE served no security headers and no Onion-Location at all. + include custom.d/security/headers_in_location.conf; + include custom.d/security/content-security-policy-cmc.conf; + add_header Onion-Location "http://paske4urhs6nttrtlkuwa5cowum3fjkc6yv6kl4ncx3mjxcd77764nqd.onion$request_uri" always; try_files /index.html =404; } @@ -48,6 +62,11 @@ server { rewrite ^/(.+)\.html$ /org/$1.org last; } add_header Vary "Accept" always; + # Restore what this block's own add_header discarded. This location + # serves every article/blog page on the site. + include custom.d/security/headers_in_location.conf; + include custom.d/security/content-security-policy-cmc.conf; + add_header Onion-Location "http://paske4urhs6nttrtlkuwa5cowum3fjkc6yv6kl4ncx3mjxcd77764nqd.onion$request_uri" always; try_files $uri =404; } } diff --git a/linux/nginx/etc/nginx/conf.d/cv.conf b/linux/nginx/etc/nginx/conf.d/cv.conf index 3dc73a2..a8e6836 100644 --- a/linux/nginx/etc/nginx/conf.d/cv.conf +++ b/linux/nginx/etc/nginx/conf.d/cv.conf @@ -3,6 +3,7 @@ server { server_name cv.cleberg.net; add_header Onion-Location "http://xe43aewwiybmbo5qsstx7t5kmgkd2ei4bkkvqinwd7fxfmkekvnjbbad.onion$request_uri" always; include custom.d/basic.conf; + include custom.d/security/content-security-policy.conf; root /var/www/cv/; autoindex on; location / { try_files $uri $uri/ /index.html; } @@ -12,6 +13,7 @@ server { listen 127.0.0.1:10015; server_name xe43aewwiybmbo5qsstx7t5kmgkd2ei4bkkvqinwd7fxfmkekvnjbbad.onion; include custom.d/basic.conf; + include custom.d/security/content-security-policy.conf; root /var/www/cv/; autoindex on; location / { try_files $uri $uri/ /index.html; } diff --git a/linux/nginx/etc/nginx/conf.d/files.conf b/linux/nginx/etc/nginx/conf.d/files.conf index 684e895..3c36465 100644 --- a/linux/nginx/etc/nginx/conf.d/files.conf +++ b/linux/nginx/etc/nginx/conf.d/files.conf @@ -7,6 +7,7 @@ server { server_name files.krz.sh; add_header Onion-Location "http://yt2y635dc6zyxziy5ytkfz36bmkgcxuccq3meom6qp44gjdat54wt5id.onion$request_uri" always; include custom.d/basic.conf; + include custom.d/security/content-security-policy.conf; root /var/www/files/; autoindex on; location / { try_files $uri $uri/ /index.html; } @@ -16,6 +17,7 @@ server { listen 127.0.0.1:10018; server_name yt2y635dc6zyxziy5ytkfz36bmkgcxuccq3meom6qp44gjdat54wt5id.onion; include custom.d/basic.conf; + include custom.d/security/content-security-policy.conf; root /var/www/files/; autoindex on; location / { try_files $uri $uri/ /index.html; } diff --git a/linux/nginx/etc/nginx/conf.d/hn.conf b/linux/nginx/etc/nginx/conf.d/hn.conf index 4b2cab2..8f699da 100644 --- a/linux/nginx/etc/nginx/conf.d/hn.conf +++ b/linux/nginx/etc/nginx/conf.d/hn.conf @@ -7,6 +7,7 @@ server { autoindex on; add_header Onion-Location "http://r3yfeffyj7ornpikojmw75u3sn2la7tqnmcmwgv2ov7if24sm5czqeid.onion$request_uri" always; include custom.d/basic.conf; + include custom.d/security/content-security-policy.conf; location / { try_files $uri $uri/ /index.html; } } diff --git a/linux/nginx/etc/nginx/conf.d/img.conf b/linux/nginx/etc/nginx/conf.d/img.conf index 16c9ab0..bf5128b 100644 --- a/linux/nginx/etc/nginx/conf.d/img.conf +++ b/linux/nginx/etc/nginx/conf.d/img.conf @@ -3,6 +3,7 @@ server { server_name img.cleberg.net; add_header Onion-Location "http://ltf2dfg7tj263ll24lxuq7igejcrhcazjp6whgg6u6vyjir2iahqggad.onion$request_uri" always; include custom.d/basic.conf; + include custom.d/security/content-security-policy.conf; root /var/www/img/; autoindex on; location / { try_files $uri $uri/ =404; } @@ -12,6 +13,7 @@ server { listen 127.0.0.1:10026; server_name ltf2dfg7tj263ll24lxuq7igejcrhcazjp6whgg6u6vyjir2iahqggad.onion; include custom.d/basic.conf; + include custom.d/security/content-security-policy.conf; root /var/www/img/; autoindex on; location / { try_files $uri $uri/ =404; } diff --git a/linux/nginx/etc/nginx/conf.d/office.conf b/linux/nginx/etc/nginx/conf.d/office.conf index 74c7032..a9bd5bb 100644 --- a/linux/nginx/etc/nginx/conf.d/office.conf +++ b/linux/nginx/etc/nginx/conf.d/office.conf @@ -5,12 +5,14 @@ server { add_header Onion-Location "http://uwtjz6pof52bzdkj242mub77ls2ji3qiznbsuoir235rxynqxadpghid.onion$request_uri" always; root /var/www/office/; include custom.d/basic.conf; + include custom.d/security/content-security-policy-inline.conf; location / { try_files $uri $uri/ /index.html; } } server { listen 127.0.0.1:10032; server_name uwtjz6pof52bzdkj242mub77ls2ji3qiznbsuoir235rxynqxadpghid.onion; include custom.d/basic.conf; + include custom.d/security/content-security-policy-inline.conf; root /var/www/office/; location / { try_files $uri $uri/ /index.html; } } diff --git a/linux/nginx/etc/nginx/conf.d/org.conf b/linux/nginx/etc/nginx/conf.d/org.conf index fbf84a0..06b5f17 100644 --- a/linux/nginx/etc/nginx/conf.d/org.conf +++ b/linux/nginx/etc/nginx/conf.d/org.conf @@ -5,12 +5,14 @@ server { add_header Onion-Location "http://7vsifofpucgi3wva52nbwhwglrmbyxgdapbkkmgniqlsskspux5zocyd.onion$request_uri" always; root /var/www/org/; include custom.d/basic.conf; + include custom.d/security/content-security-policy.conf; location / { try_files $uri $uri/ /index.html; } } server { listen 127.0.0.1:10034; server_name 7vsifofpucgi3wva52nbwhwglrmbyxgdapbkkmgniqlsskspux5zocyd.onion; include custom.d/basic.conf; + include custom.d/security/content-security-policy.conf; root /var/www/org/; location / { try_files $uri $uri/ /index.html; } } diff --git a/linux/nginx/etc/nginx/conf.d/piped.conf b/linux/nginx/etc/nginx/conf.d/piped.conf index c1ac91e..ad5dcda 100644 --- a/linux/nginx/etc/nginx/conf.d/piped.conf +++ b/linux/nginx/etc/nginx/conf.d/piped.conf @@ -1,23 +1,24 @@ -# Piped -- Host-based router on 127.0.0.1:8077 +# Piped frontend -- 127.0.0.1:8077 -> container on :8076 # -# WHY THIS EXISTS: the Cloudflare tunnel routes all three Piped hostnames to -# localhost:8077 -- -# piped.krz.sh -> :8077 -# pipedapi.krz.sh -> :8077 (should be the backend) -# pipedproxy.krz.sh -> :8077 (should be the media proxy) -# so the API and media-proxy hostnames landed on the frontend and Piped was -# broken. The frontend advertises BACKEND_HOSTNAME=pipedapi.krz.sh to browsers, -# so every API call failed. +# HISTORY: this file used to be a Host-based router for all three Piped +# hostnames, because the Cloudflare tunnel sent pipedapi.krz.sh and +# pipedproxy.krz.sh to :8077 as well, landing them on the frontend and breaking +# every API call. The dashboard was corrected 2026-08-03 to point pipedapi at +# :8078 and pipedproxy at :8079 directly, so those two server blocks became +# dead code and were removed. # -# The tidier fix is two edits in the Cloudflare dashboard (point pipedapi at -# :8078 and pipedproxy at :8079). This file fixes it server-side instead, and -# is harmless if the dashboard is corrected later -- the tunnel would simply -# reach the containers directly and these blocks would go unused. +# Ports: frontend :8076, backend :8078, media proxy :8079. # -# Ports: frontend :8076 (moved from :8077), backend :8078, media proxy :8079. +# WHY THE FRONTEND BLOCK STAYS: the tunnel still routes piped.krz.sh here +# (confirmed -- the security headers added below appear on the public +# response), and the Tor onion for piped targets :8077 with a .onion Host +# header, which needs default_server to land somewhere. Routing the frontend +# straight to :8076 would also drop the header/CSP work below. # # NOTE: custom.d/basic.conf is deliberately NOT included. Its Permissions-Policy -# sets fullscreen=(), which would stop videos going fullscreen. +# sets fullscreen=(), which would stop videos going fullscreen. See +# headers_in_location_media.conf, included in the location, for the variant +# that keeps every other restriction. # Frontend. default_server so the Tor onion for piped (which targets :8077 with # a .onion Host header) also lands here. @@ -32,43 +33,13 @@ server { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_http_version 1.1; - } -} - -# Backend API. -server { - listen 127.0.0.1:8077; - server_name pipedapi.krz.sh; - - location / { - proxy_pass http://127.0.0.1:8078; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_http_version 1.1; - # The backend emits its own CORS headers; do not add or override any - # here or the browser will reject the API responses. - proxy_read_timeout 120; - } -} - -# Media proxy. Streams video, so no buffering and generous timeouts. -server { - listen 127.0.0.1:8077; - server_name pipedproxy.krz.sh; - - location / { - proxy_pass http://127.0.0.1:8079; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_http_version 1.1; - proxy_buffering off; - proxy_request_buffering off; - proxy_read_timeout 300; - proxy_send_timeout 300; + # This vhost previously served NO security headers at all, because + # basic.conf was excluded wholesale to avoid its `fullscreen=()`. + # The media bundle keeps every other restriction and permits + # fullscreen/autoplay/PiP on this origin, so the player still works. + include custom.d/security/headers_in_location_media.conf; + # CSP is REPORT-ONLY -- see content_type_maps.conf. Nothing is blocked. + include custom.d/security/content-security-policy-piped-report-only.conf; } } diff --git a/linux/nginx/etc/nginx/conf.d/projects.conf b/linux/nginx/etc/nginx/conf.d/projects.conf deleted file mode 100644 index bcdfae9..0000000 --- a/linux/nginx/etc/nginx/conf.d/projects.conf +++ /dev/null @@ -1,22 +0,0 @@ -server { - listen 127.0.0.1:10040; - server_name projects.zerolabs.sh; - - add_header Onion-Location "http://krednuajd2wpt67ozmkollddpbdphpeyh74hbenagzoxqjncrtrgxkid.onion$request_uri" always; - root /var/www/projects/; - autoindex on; - include custom.d/basic.conf; - location / { try_files $uri $uri/ /index.html; } -} - -server { - listen 127.0.0.1:10041; - server_name krednuajd2wpt67ozmkollddpbdphpeyh74hbenagzoxqjncrtrgxkid.onion; - include custom.d/basic.conf; - root /var/www/projects/; - autoindex on; - location / { try_files $uri $uri/ /index.html; } -} - - - diff --git a/linux/nginx/etc/nginx/conf.d/reminiscecleberg.com.conf b/linux/nginx/etc/nginx/conf.d/reminiscecleberg.com.conf index 02b1721..a84bf2c 100644 --- a/linux/nginx/etc/nginx/conf.d/reminiscecleberg.com.conf +++ b/linux/nginx/etc/nginx/conf.d/reminiscecleberg.com.conf @@ -5,6 +5,7 @@ server { server_name reminiscecleberg.com; root /var/www/reminiscecleberg.com/; include custom.d/basic.conf; + include custom.d/security/content-security-policy.conf; location / { try_files $uri $uri/ =404; } } diff --git a/linux/nginx/etc/nginx/conf.d/rogue.conf b/linux/nginx/etc/nginx/conf.d/rogue.conf index 1618be3..3c85dd8 100644 --- a/linux/nginx/etc/nginx/conf.d/rogue.conf +++ b/linux/nginx/etc/nginx/conf.d/rogue.conf @@ -2,8 +2,9 @@ server { listen 127.0.0.1:10001; server_name rogue.krz.sh; - root /var/www/rogue; + root /var/www/rogue/; index index.html; + autoindex on; server_tokens off; etag off; @@ -13,9 +14,15 @@ server { gzip_vary off; charset off; - location = / { - try_files /index.html =404; - } + # `/` used to 404: this block did `try_files /index.html =404` and there is + # no index.html at the webroot, so it short-circuited before `autoindex on` + # could ever produce a listing. Removed, so `/` now falls through to the + # server-level `index`/`autoindex` and lists 1kb/ and 1mb/. + # + # Deliberately NOT adding basic.conf here: this vhost strips headers on + # purpose (server_tokens/etag/expires/gzip/charset all off) because it is a + # byte-size experiment -- a 1 KB CSS-only game. Adding security headers + # would defeat the point of the vhost. location = /index.html { try_files /index.html =404; diff --git a/linux/nginx/etc/nginx/conf.d/rss.conf b/linux/nginx/etc/nginx/conf.d/rss.conf index 44bde88..963950f 100644 --- a/linux/nginx/etc/nginx/conf.d/rss.conf +++ b/linux/nginx/etc/nginx/conf.d/rss.conf @@ -6,7 +6,15 @@ server { include custom.d/basic.conf; location / { proxy_pass http://freshrss/; + # This add_header discarded EVERYTHING from basic.conf -- nginx does + # not merge add_header across levels. Verified: this vhost was serving + # X-Frame-Options and nothing else (no Referrer-Policy, no HSTS, no + # Permissions-Policy, no X-Content-Type-Options). Restored below. + # SAMEORIGIN (not the shared map's DENY) -- hence the _no_xfo bundle, + # which omits X-Frame-Options so the two do not conflict. add_header X-Frame-Options SAMEORIGIN; + include custom.d/security/headers_in_location_no_xfo.conf; + include custom.d/security/content-security-policy-freshrss-report-only.conf; proxy_redirect off; proxy_buffering off; proxy_set_header Host $host; diff --git a/linux/nginx/etc/nginx/custom.d/http/content_type_maps.conf b/linux/nginx/etc/nginx/custom.d/http/content_type_maps.conf index 8f00d12..ee53aed 100644 --- a/linux/nginx/etc/nginx/custom.d/http/content_type_maps.conf +++ b/linux/nginx/etc/nginx/custom.d/http/content_type_maps.conf @@ -32,16 +32,93 @@ map $sent_http_content_type $x_frame_options { } # Add Content-Security-Policy for HTML documents. +# +# TWO POLICIES, DELIBERATELY SEPARATE -- do not merge them. +# +# $content_security_policy -- identity-neutral. Safe on ANY vhost. +# $content_security_policy_cmc -- for `cleberg.*` vhosts ONLY. Names +# img.cleberg.net, so serving it on a +# `krz`/`zerolabs` vhost would put the +# real-name domain in a response header. +# +# The previous single map hardcoded `img-src 'self' https://img.cleberg.net` +# and was the only policy available, so any per-app CSP work would have leaked +# the real-name domain onto a pseudonymous vhost by default. Keeping the +# default neutral makes the safe choice the automatic one. +# +# Also fixed here: the old value was a multi-line quoted string containing 8 +# literal newlines, which nginx emitted verbatim -- a folded, malformed header +# that clients saw as empty. Policies must be ONE line. map $sent_http_content_type $content_security_policy { - ~*text/(html|javascript)|application/pdf|xml " - default-src 'self'; - img-src 'self' https://img.cleberg.net; - base-uri 'none'; - form-action 'self'; - frame-ancestors 'none'; - object-src 'none'; - upgrade-insecure-requests - "; + ~*text/(html|javascript)|application/pdf|xml "default-src 'self'; img-src 'self' data:; base-uri 'none'; form-action 'self'; frame-ancestors 'none'; object-src 'none'; upgrade-insecure-requests"; +} + +# cleberg.* only. Mirrors what the site actually loads, verified by grepping +# the served HTML: -> img.cleberg.net (151),