Commit 41618d30fc
Verified · cmc
Layout: unified · split
linux/nginx/etc/nginx/conf.d/cleberg.dev.conf +1
| @@ -5,6 +5,7 @@ server { | ||
| 5 | 5 | server_name cleberg.dev; |
| 6 | 6 | root /var/www/cleberg.dev/; |
| 7 | 7 | include custom.d/basic.conf; |
| 8 | include custom.d/security/content-security-policy.conf; | |
| 8 | 9 | location / { try_files $uri $uri/ =404; } |
| 9 | 10 | } |
| 10 | 11 | |
linux/nginx/etc/nginx/conf.d/cleberg.net.conf +22 −3
| @@ -5,10 +5,15 @@ server { | ||
| 5 | 5 | # No per-vhost error_log: it overrode the global "off" and wrote visitor |
| 6 | 6 | # IPs to disk. |
| 7 | 7 | |
| 8 | # basic.conf now carries HSTS + Permissions-Policy. CSP stays explicit -- | |
| 9 | # the shared policy is written for this vhost (it allows img.cleberg.net). | |
| 8 | # basic.conf carries HSTS + Permissions-Policy. CSP stays explicit, and now | |
| 9 | # uses the cmc-specific policy: the shared $content_security_policy was made | |
| 10 | # identity-neutral so it is safe to reach for on a krz vhost by default. | |
| 11 | # | |
| 12 | # NOTE: every `location` below that sets its own add_header must ALSO | |
| 13 | # include these two files, or it discards them (nginx add_header does not | |
| 14 | # merge across levels). See custom.d/security/headers_in_location.conf. | |
| 10 | 15 | include custom.d/basic.conf; |
| 11 | include custom.d/security/content-security-policy.conf; | |
| 16 | include custom.d/security/content-security-policy-cmc.conf; | |
| 12 | 17 | root /var/www/cleberg.net/; |
| 13 | 18 | include custom.d/redirects/blog.conf; |
| 14 | 19 | port_in_redirect off; |
| @@ -21,6 +26,10 @@ server { | ||
| 21 | 26 | default_type text/markdown; |
| 22 | 27 | add_header Content-Type "text/markdown; charset=utf-8" always; |
| 23 | 28 | add_header Vary "Accept" always; |
| 29 | # Restore what this block's own add_header discarded: | |
| 30 | include custom.d/security/headers_in_location.conf; | |
| 31 | include custom.d/security/content-security-policy-cmc.conf; | |
| 32 | add_header Onion-Location "http://paske4urhs6nttrtlkuwa5cowum3fjkc6yv6kl4ncx3mjxcd77764nqd.onion$request_uri" always; | |
| 24 | 33 | } |
| 25 | 34 | |
| 26 | 35 | location = / { |
| @@ -29,6 +38,11 @@ server { | ||
| 29 | 38 | } |
| 30 | 39 | |
| 31 | 40 | add_header Vary "Accept" always; |
| 41 | # Restore what this block's own add_header discarded. Without these the | |
| 42 | # HOMEPAGE served no security headers and no Onion-Location at all. | |
| 43 | include custom.d/security/headers_in_location.conf; | |
| 44 | include custom.d/security/content-security-policy-cmc.conf; | |
| 45 | add_header Onion-Location "http://paske4urhs6nttrtlkuwa5cowum3fjkc6yv6kl4ncx3mjxcd77764nqd.onion$request_uri" always; | |
| 32 | 46 | try_files /index.html =404; |
| 33 | 47 | } |
| 34 | 48 | |
| @@ -48,6 +62,11 @@ server { | ||
| 48 | 62 | rewrite ^/(.+)\.html$ /org/$1.org last; |
| 49 | 63 | } |
| 50 | 64 | add_header Vary "Accept" always; |
| 65 | # Restore what this block's own add_header discarded. This location | |
| 66 | # serves every article/blog page on the site. | |
| 67 | include custom.d/security/headers_in_location.conf; | |
| 68 | include custom.d/security/content-security-policy-cmc.conf; | |
| 69 | add_header Onion-Location "http://paske4urhs6nttrtlkuwa5cowum3fjkc6yv6kl4ncx3mjxcd77764nqd.onion$request_uri" always; | |
| 51 | 70 | try_files $uri =404; |
| 52 | 71 | } |
| 53 | 72 | } |
linux/nginx/etc/nginx/conf.d/cv.conf +2
| @@ -3,6 +3,7 @@ server { | ||
| 3 | 3 | server_name cv.cleberg.net; |
| 4 | 4 | add_header Onion-Location "http://xe43aewwiybmbo5qsstx7t5kmgkd2ei4bkkvqinwd7fxfmkekvnjbbad.onion$request_uri" always; |
| 5 | 5 | include custom.d/basic.conf; |
| 6 | include custom.d/security/content-security-policy.conf; | |
| 6 | 7 | root /var/www/cv/; |
| 7 | 8 | autoindex on; |
| 8 | 9 | location / { try_files $uri $uri/ /index.html; } |
| @@ -12,6 +13,7 @@ server { | ||
| 12 | 13 | listen 127.0.0.1:10015; |
| 13 | 14 | server_name xe43aewwiybmbo5qsstx7t5kmgkd2ei4bkkvqinwd7fxfmkekvnjbbad.onion; |
| 14 | 15 | include custom.d/basic.conf; |
| 16 | include custom.d/security/content-security-policy.conf; | |
| 15 | 17 | root /var/www/cv/; |
| 16 | 18 | autoindex on; |
| 17 | 19 | location / { try_files $uri $uri/ /index.html; } |
linux/nginx/etc/nginx/conf.d/files.conf +2
| @@ -7,6 +7,7 @@ server { | ||
| 7 | 7 | server_name files.krz.sh; |
| 8 | 8 | add_header Onion-Location "http://yt2y635dc6zyxziy5ytkfz36bmkgcxuccq3meom6qp44gjdat54wt5id.onion$request_uri" always; |
| 9 | 9 | include custom.d/basic.conf; |
| 10 | include custom.d/security/content-security-policy.conf; | |
| 10 | 11 | root /var/www/files/; |
| 11 | 12 | autoindex on; |
| 12 | 13 | location / { try_files $uri $uri/ /index.html; } |
| @@ -16,6 +17,7 @@ server { | ||
| 16 | 17 | listen 127.0.0.1:10018; |
| 17 | 18 | server_name yt2y635dc6zyxziy5ytkfz36bmkgcxuccq3meom6qp44gjdat54wt5id.onion; |
| 18 | 19 | include custom.d/basic.conf; |
| 20 | include custom.d/security/content-security-policy.conf; | |
| 19 | 21 | root /var/www/files/; |
| 20 | 22 | autoindex on; |
| 21 | 23 | location / { try_files $uri $uri/ /index.html; } |
linux/nginx/etc/nginx/conf.d/hn.conf +1
| @@ -7,6 +7,7 @@ server { | ||
| 7 | 7 | autoindex on; |
| 8 | 8 | add_header Onion-Location "http://r3yfeffyj7ornpikojmw75u3sn2la7tqnmcmwgv2ov7if24sm5czqeid.onion$request_uri" always; |
| 9 | 9 | include custom.d/basic.conf; |
| 10 | include custom.d/security/content-security-policy.conf; | |
| 10 | 11 | location / { try_files $uri $uri/ /index.html; } |
| 11 | 12 | } |
| 12 | 13 | |
linux/nginx/etc/nginx/conf.d/img.conf +2
| @@ -3,6 +3,7 @@ server { | ||
| 3 | 3 | server_name img.cleberg.net; |
| 4 | 4 | add_header Onion-Location "http://ltf2dfg7tj263ll24lxuq7igejcrhcazjp6whgg6u6vyjir2iahqggad.onion$request_uri" always; |
| 5 | 5 | include custom.d/basic.conf; |
| 6 | include custom.d/security/content-security-policy.conf; | |
| 6 | 7 | root /var/www/img/; |
| 7 | 8 | autoindex on; |
| 8 | 9 | location / { try_files $uri $uri/ =404; } |
| @@ -12,6 +13,7 @@ server { | ||
| 12 | 13 | listen 127.0.0.1:10026; |
| 13 | 14 | server_name ltf2dfg7tj263ll24lxuq7igejcrhcazjp6whgg6u6vyjir2iahqggad.onion; |
| 14 | 15 | include custom.d/basic.conf; |
| 16 | include custom.d/security/content-security-policy.conf; | |
| 15 | 17 | root /var/www/img/; |
| 16 | 18 | autoindex on; |
| 17 | 19 | location / { try_files $uri $uri/ =404; } |
linux/nginx/etc/nginx/conf.d/office.conf +2
| @@ -5,12 +5,14 @@ server { | ||
| 5 | 5 | add_header Onion-Location "http://uwtjz6pof52bzdkj242mub77ls2ji3qiznbsuoir235rxynqxadpghid.onion$request_uri" always; |
| 6 | 6 | root /var/www/office/; |
| 7 | 7 | include custom.d/basic.conf; |
| 8 | include custom.d/security/content-security-policy-inline.conf; | |
| 8 | 9 | location / { try_files $uri $uri/ /index.html; } |
| 9 | 10 | } |
| 10 | 11 | server { |
| 11 | 12 | listen 127.0.0.1:10032; |
| 12 | 13 | server_name uwtjz6pof52bzdkj242mub77ls2ji3qiznbsuoir235rxynqxadpghid.onion; |
| 13 | 14 | include custom.d/basic.conf; |
| 15 | include custom.d/security/content-security-policy-inline.conf; | |
| 14 | 16 | root /var/www/office/; |
| 15 | 17 | location / { try_files $uri $uri/ /index.html; } |
| 16 | 18 | } |
linux/nginx/etc/nginx/conf.d/org.conf +2
| @@ -5,12 +5,14 @@ server { | ||
| 5 | 5 | add_header Onion-Location "http://7vsifofpucgi3wva52nbwhwglrmbyxgdapbkkmgniqlsskspux5zocyd.onion$request_uri" always; |
| 6 | 6 | root /var/www/org/; |
| 7 | 7 | include custom.d/basic.conf; |
| 8 | include custom.d/security/content-security-policy.conf; | |
| 8 | 9 | location / { try_files $uri $uri/ /index.html; } |
| 9 | 10 | } |
| 10 | 11 | server { |
| 11 | 12 | listen 127.0.0.1:10034; |
| 12 | 13 | server_name 7vsifofpucgi3wva52nbwhwglrmbyxgdapbkkmgniqlsskspux5zocyd.onion; |
| 13 | 14 | include custom.d/basic.conf; |
| 15 | include custom.d/security/content-security-policy.conf; | |
| 14 | 16 | root /var/www/org/; |
| 15 | 17 | location / { try_files $uri $uri/ /index.html; } |
| 16 | 18 | } |
linux/nginx/etc/nginx/conf.d/piped.conf +23 −52
| @@ -1,23 +1,24 @@ | ||
| 1 | # Piped -- Host-based router on 127.0.0.1:8077 | |
| 1 | # Piped frontend -- 127.0.0.1:8077 -> container on :8076 | |
| 2 | 2 | # |
| 3 | # WHY THIS EXISTS: the Cloudflare tunnel routes all three Piped hostnames to | |
| 4 | # localhost:8077 -- | |
| 5 | # piped.krz.sh -> :8077 | |
| 6 | # pipedapi.krz.sh -> :8077 (should be the backend) | |
| 7 | # pipedproxy.krz.sh -> :8077 (should be the media proxy) | |
| 8 | # so the API and media-proxy hostnames landed on the frontend and Piped was | |
| 9 | # broken. The frontend advertises BACKEND_HOSTNAME=pipedapi.krz.sh to browsers, | |
| 10 | # so every API call failed. | |
| 3 | # HISTORY: this file used to be a Host-based router for all three Piped | |
| 4 | # hostnames, because the Cloudflare tunnel sent pipedapi.krz.sh and | |
| 5 | # pipedproxy.krz.sh to :8077 as well, landing them on the frontend and breaking | |
| 6 | # every API call. The dashboard was corrected 2026-08-03 to point pipedapi at | |
| 7 | # :8078 and pipedproxy at :8079 directly, so those two server blocks became | |
| 8 | # dead code and were removed. | |
| 11 | 9 | # |
| 12 | # The tidier fix is two edits in the Cloudflare dashboard (point pipedapi at | |
| 13 | # :8078 and pipedproxy at :8079). This file fixes it server-side instead, and | |
| 14 | # is harmless if the dashboard is corrected later -- the tunnel would simply | |
| 15 | # reach the containers directly and these blocks would go unused. | |
| 10 | # Ports: frontend :8076, backend :8078, media proxy :8079. | |
| 16 | 11 | # |
| 17 | # Ports: frontend :8076 (moved from :8077), backend :8078, media proxy :8079. | |
| 12 | # WHY THE FRONTEND BLOCK STAYS: the tunnel still routes piped.krz.sh here | |
| 13 | # (confirmed -- the security headers added below appear on the public | |
| 14 | # response), and the Tor onion for piped targets :8077 with a .onion Host | |
| 15 | # header, which needs default_server to land somewhere. Routing the frontend | |
| 16 | # straight to :8076 would also drop the header/CSP work below. | |
| 18 | 17 | # |
| 19 | 18 | # NOTE: custom.d/basic.conf is deliberately NOT included. Its Permissions-Policy |
| 20 | # sets fullscreen=(), which would stop videos going fullscreen. | |
| 19 | # sets fullscreen=(), which would stop videos going fullscreen. See | |
| 20 | # headers_in_location_media.conf, included in the location, for the variant | |
| 21 | # that keeps every other restriction. | |
| 21 | 22 | |
| 22 | 23 | # Frontend. default_server so the Tor onion for piped (which targets :8077 with |
| 23 | 24 | # a .onion Host header) also lands here. |
| @@ -32,43 +33,13 @@ server { | ||
| 32 | 33 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; |
| 33 | 34 | proxy_set_header X-Forwarded-Proto $scheme; |
| 34 | 35 | proxy_http_version 1.1; |
| 35 | } | |
| 36 | } | |
| 37 | ||
| 38 | # Backend API. | |
| 39 | server { | |
| 40 | listen 127.0.0.1:8077; | |
| 41 | server_name pipedapi.krz.sh; | |
| 42 | ||
| 43 | location / { | |
| 44 | proxy_pass http://127.0.0.1:8078; | |
| 45 | proxy_set_header Host $host; | |
| 46 | proxy_set_header X-Real-IP $remote_addr; | |
| 47 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; | |
| 48 | proxy_set_header X-Forwarded-Proto $scheme; | |
| 49 | proxy_http_version 1.1; | |
| 50 | # The backend emits its own CORS headers; do not add or override any | |
| 51 | # here or the browser will reject the API responses. | |
| 52 | proxy_read_timeout 120; | |
| 53 | } | |
| 54 | } | |
| 55 | ||
| 56 | # Media proxy. Streams video, so no buffering and generous timeouts. | |
| 57 | server { | |
| 58 | listen 127.0.0.1:8077; | |
| 59 | server_name pipedproxy.krz.sh; | |
| 60 | ||
| 61 | location / { | |
| 62 | proxy_pass http://127.0.0.1:8079; | |
| 63 | proxy_set_header Host $host; | |
| 64 | proxy_set_header X-Real-IP $remote_addr; | |
| 65 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; | |
| 66 | proxy_set_header X-Forwarded-Proto $scheme; | |
| 67 | proxy_http_version 1.1; | |
| 68 | 36 | |
| 69 | proxy_buffering off; | |
| 70 | proxy_request_buffering off; | |
| 71 | proxy_read_timeout 300; | |
| 72 | proxy_send_timeout 300; | |
| 37 | # This vhost previously served NO security headers at all, because | |
| 38 | # basic.conf was excluded wholesale to avoid its `fullscreen=()`. | |
| 39 | # The media bundle keeps every other restriction and permits | |
| 40 | # fullscreen/autoplay/PiP on this origin, so the player still works. | |
| 41 | include custom.d/security/headers_in_location_media.conf; | |
| 42 | # CSP is REPORT-ONLY -- see content_type_maps.conf. Nothing is blocked. | |
| 43 | include custom.d/security/content-security-policy-piped-report-only.conf; | |
| 73 | 44 | } |
| 74 | 45 | } |
linux/nginx/etc/nginx/conf.d/projects.conf deleted −22
| @@ -1,22 +0,0 @@ | ||
| 1 | server { | |
| 2 | listen 127.0.0.1:10040; | |
| 3 | server_name projects.zerolabs.sh; | |
| 4 | ||
| 5 | add_header Onion-Location "http://krednuajd2wpt67ozmkollddpbdphpeyh74hbenagzoxqjncrtrgxkid.onion$request_uri" always; | |
| 6 | root /var/www/projects/; | |
| 7 | autoindex on; | |
| 8 | include custom.d/basic.conf; | |
| 9 | location / { try_files $uri $uri/ /index.html; } | |
| 10 | } | |
| 11 | ||
| 12 | server { | |
| 13 | listen 127.0.0.1:10041; | |
| 14 | server_name krednuajd2wpt67ozmkollddpbdphpeyh74hbenagzoxqjncrtrgxkid.onion; | |
| 15 | include custom.d/basic.conf; | |
| 16 | root /var/www/projects/; | |
| 17 | autoindex on; | |
| 18 | location / { try_files $uri $uri/ /index.html; } | |
| 19 | } | |
| 20 | ||
| 21 | ||
| 22 | ||
linux/nginx/etc/nginx/conf.d/reminiscecleberg.com.conf +1
| @@ -5,6 +5,7 @@ server { | ||
| 5 | 5 | server_name reminiscecleberg.com; |
| 6 | 6 | root /var/www/reminiscecleberg.com/; |
| 7 | 7 | include custom.d/basic.conf; |
| 8 | include custom.d/security/content-security-policy.conf; | |
| 8 | 9 | location / { try_files $uri $uri/ =404; } |
| 9 | 10 | } |
| 10 | 11 | |
linux/nginx/etc/nginx/conf.d/rogue.conf +11 −4
| @@ -2,8 +2,9 @@ server { | ||
| 2 | 2 | listen 127.0.0.1:10001; |
| 3 | 3 | server_name rogue.krz.sh; |
| 4 | 4 | |
| 5 | root /var/www/rogue; | |
| 5 | root /var/www/rogue/; | |
| 6 | 6 | index index.html; |
| 7 | autoindex on; | |
| 7 | 8 | |
| 8 | 9 | server_tokens off; |
| 9 | 10 | etag off; |
| @@ -13,9 +14,15 @@ server { | ||
| 13 | 14 | gzip_vary off; |
| 14 | 15 | charset off; |
| 15 | 16 | |
| 16 | location = / { | |
| 17 | try_files /index.html =404; | |
| 18 | } | |
| 17 | # `/` used to 404: this block did `try_files /index.html =404` and there is | |
| 18 | # no index.html at the webroot, so it short-circuited before `autoindex on` | |
| 19 | # could ever produce a listing. Removed, so `/` now falls through to the | |
| 20 | # server-level `index`/`autoindex` and lists 1kb/ and 1mb/. | |
| 21 | # | |
| 22 | # Deliberately NOT adding basic.conf here: this vhost strips headers on | |
| 23 | # purpose (server_tokens/etag/expires/gzip/charset all off) because it is a | |
| 24 | # byte-size experiment -- a 1 KB CSS-only game. Adding security headers | |
| 25 | # would defeat the point of the vhost. | |
| 19 | 26 | |
| 20 | 27 | location = /index.html { |
| 21 | 28 | try_files /index.html =404; |
linux/nginx/etc/nginx/conf.d/rss.conf +8
| @@ -6,7 +6,15 @@ server { | ||
| 6 | 6 | include custom.d/basic.conf; |
| 7 | 7 | location / { |
| 8 | 8 | proxy_pass http://freshrss/; |
| 9 | # This add_header discarded EVERYTHING from basic.conf -- nginx does | |
| 10 | # not merge add_header across levels. Verified: this vhost was serving | |
| 11 | # X-Frame-Options and nothing else (no Referrer-Policy, no HSTS, no | |
| 12 | # Permissions-Policy, no X-Content-Type-Options). Restored below. | |
| 13 | # SAMEORIGIN (not the shared map's DENY) -- hence the _no_xfo bundle, | |
| 14 | # which omits X-Frame-Options so the two do not conflict. | |
| 9 | 15 | add_header X-Frame-Options SAMEORIGIN; |
| 16 | include custom.d/security/headers_in_location_no_xfo.conf; | |
| 17 | include custom.d/security/content-security-policy-freshrss-report-only.conf; | |
| 10 | 18 | proxy_redirect off; |
| 11 | 19 | proxy_buffering off; |
| 12 | 20 | proxy_set_header Host $host; |
linux/nginx/etc/nginx/custom.d/http/content_type_maps.conf +86 −9
| @@ -32,16 +32,93 @@ map $sent_http_content_type $x_frame_options { | ||
| 32 | 32 | } |
| 33 | 33 | |
| 34 | 34 | # Add Content-Security-Policy for HTML documents. |
| 35 | # | |
| 36 | # TWO POLICIES, DELIBERATELY SEPARATE -- do not merge them. | |
| 37 | # | |
| 38 | # $content_security_policy -- identity-neutral. Safe on ANY vhost. | |
| 39 | # $content_security_policy_cmc -- for `cleberg.*` vhosts ONLY. Names | |
| 40 | # img.cleberg.net, so serving it on a | |
| 41 | # `krz`/`zerolabs` vhost would put the | |
| 42 | # real-name domain in a response header. | |
| 43 | # | |
| 44 | # The previous single map hardcoded `img-src 'self' https://img.cleberg.net` | |
| 45 | # and was the only policy available, so any per-app CSP work would have leaked | |
| 46 | # the real-name domain onto a pseudonymous vhost by default. Keeping the | |
| 47 | # default neutral makes the safe choice the automatic one. | |
| 48 | # | |
| 49 | # Also fixed here: the old value was a multi-line quoted string containing 8 | |
| 50 | # literal newlines, which nginx emitted verbatim -- a folded, malformed header | |
| 51 | # that clients saw as empty. Policies must be ONE line. | |
| 35 | 52 | map $sent_http_content_type $content_security_policy { |
| 36 | ~*text/(html|javascript)|application/pdf|xml " | |
| 37 | default-src 'self'; | |
| 38 | img-src 'self' https://img.cleberg.net; | |
| 39 | base-uri 'none'; | |
| 40 | form-action 'self'; | |
| 41 | frame-ancestors 'none'; | |
| 42 | object-src 'none'; | |
| 43 | upgrade-insecure-requests | |
| 44 | "; | |
| 53 | ~*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"; | |
| 54 | } | |
| 55 | ||
| 56 | # cleberg.* only. Mirrors what the site actually loads, verified by grepping | |
| 57 | # the served HTML: <img src> -> img.cleberg.net (151), <script src> -> | |
| 58 | # bubbles.town (174), stylesheets -> https://cleberg.net (180, absolute). | |
| 59 | # | |
| 60 | # `https://cleberg.net` must be allowed explicitly: the HTML uses ABSOLUTE | |
| 61 | # stylesheet URLs, so on the .onion (a different origin) they are cross-origin | |
| 62 | # and 'self' would block them. | |
| 63 | # | |
| 64 | # `upgrade-insecure-requests` is deliberately OMITTED: this vhost also serves | |
| 65 | # the .onion over plain http, where UIR would upgrade same-origin subresource | |
| 66 | # URLs to https and break them. Cloudflare already sets UIR on the clearnet | |
| 67 | # path. See the onion self-containment note in the project record. | |
| 68 | map $sent_http_content_type $content_security_policy_cmc { | |
| 69 | ~*text/(html|javascript)|application/pdf|xml "default-src 'self'; img-src 'self' https://img.cleberg.net data:; script-src 'self' https://bubbles.town; connect-src 'self' https://bubbles.town; style-src 'self' https://cleberg.net; font-src 'self' https://cleberg.net; base-uri 'none'; form-action 'self'; frame-ancestors 'none'; object-src 'none'"; | |
| 70 | } | |
| 71 | ||
| 72 | # For apps that ship inline <script>/<style>/style="" and cannot use the strict | |
| 73 | # policy. Currently only office.zerolabs.sh (5 files with inline <script>, | |
| 74 | # 3 with <style>, 2 with style attributes). Identity-neutral -- no host is | |
| 75 | # named, so it is safe on any vhost. | |
| 76 | # | |
| 77 | # 'unsafe-inline' is a real weakening: it is what strict CSP exists to prevent. | |
| 78 | # It is used here only because the alternative is no CSP at all, which is | |
| 79 | # strictly worse. The upgrade path is per-file hashes or nonces; that needs | |
| 80 | # changes to the app, not to nginx. | |
| 81 | map $sent_http_content_type $content_security_policy_inline { | |
| 82 | ~*text/(html|javascript)|application/pdf|xml "default-src 'self'; img-src 'self' data:; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; base-uri 'none'; form-action 'self'; frame-ancestors 'none'; object-src 'none'; upgrade-insecure-requests"; | |
| 83 | } | |
| 84 | ||
| 85 | # --- Proxied third-party apps ------------------------------------------- | |
| 86 | # These two are shipped REPORT-ONLY (see the *-report-only.conf includes). | |
| 87 | # Both are third-party SPAs whose runtime behaviour cannot be exercised from | |
| 88 | # the shell -- no browser here -- and a wrong directive fails SILENTLY: video | |
| 89 | # stops playing, or every article image disappears, with only a console | |
| 90 | # message. Report-Only gives the visibility with zero outage risk. Promote to | |
| 91 | # enforcing after checking a real browser console. | |
| 92 | ||
| 93 | # piped.krz.sh. Derived from evidence, not guesswork: | |
| 94 | # - connect-src: BACKEND_HOSTNAME=pipedapi.krz.sh (container env). | |
| 95 | # - img/media-src: the live API returns PROXY_PART=https://pipedproxy.krz.sh | |
| 96 | # (57 refs in a real /streams response). | |
| 97 | # - script-src needs 'unsafe-inline' AND data: -- index.html carries 4 inline | |
| 98 | # Vite shims, and the first one does `import 'data:text/javascript,...'`. | |
| 99 | # - blob: for media/worker -- HLS/DASH playback builds blob URLs. | |
| 100 | # NOTE pipedproxy.kavin.rocks is baked into the JS bundle as a fallback and is | |
| 101 | # deliberately NOT allowed: failing closed to the self-hosted proxy is the | |
| 102 | # privacy-correct outcome. | |
| 103 | map $sent_http_content_type $csp_piped { | |
| 104 | ~*text/(html|javascript)|application/pdf|xml "default-src 'self'; connect-src 'self' https://pipedapi.krz.sh; img-src 'self' https://pipedproxy.krz.sh data: blob:; media-src 'self' https://pipedproxy.krz.sh blob:; script-src 'self' 'unsafe-inline' data:; style-src 'self' 'unsafe-inline'; worker-src 'self' blob:; font-src 'self' data:; base-uri 'none'; form-action 'self'; frame-ancestors 'none'; object-src 'none'"; | |
| 105 | } | |
| 106 | ||
| 107 | # rss.zerolabs.sh (FreshRSS). Operator-only behind Cloudflare Access, so the | |
| 108 | # T5 stake is low; the risk is breakage. An RSS reader renders arbitrary feed | |
| 109 | # HTML, so img/media must allow remote hosts or every article image dies. | |
| 110 | # That remote fetching is inherent to the app and is controlled by FreshRSS's | |
| 111 | # own "load remote images" setting, not by CSP. | |
| 112 | map $sent_http_content_type $csp_freshrss { | |
| 113 | ~*text/(html|javascript)|application/pdf|xml "default-src 'self'; img-src 'self' https: data:; media-src 'self' https:; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; frame-src https:; base-uri 'none'; form-action 'self'; frame-ancestors 'self'; object-src 'none'"; | |
| 114 | } | |
| 115 | ||
| 116 | # Permissions-Policy variant for media apps. The shared policy sets | |
| 117 | # `fullscreen=()`, which is exactly why piped.conf excludes basic.conf -- it | |
| 118 | # would stop videos going fullscreen. This keeps every other restriction and | |
| 119 | # permits fullscreen on the app's own origin. | |
| 120 | map $sent_http_content_type $permissions_policy_media { | |
| 121 | ~*text/(html|javascript)|application/pdf|xml "accelerometer=(),autoplay=(self),browsing-topics=(),camera=(),display-capture=(),document-domain=(),encrypted-media=(self),fullscreen=(self),geolocation=(),gyroscope=(),magnetometer=(),microphone=(),midi=(),payment=(),picture-in-picture=(self),publickey-credentials-get=(),screen-wake-lock=(self),sync-xhr=(self),usb=(),web-share=(),xr-spatial-tracking=()"; | |
| 45 | 122 | } |
| 46 | 123 | |
| 47 | 124 | # Add Permissions-Policy for HTML documents. |
linux/nginx/etc/nginx/custom.d/security/content-security-policy-cmc.conf added +8
| @@ -0,0 +1,8 @@ | ||
| 1 | # Content-Security-Policy for `cleberg.*` vhosts ONLY. | |
| 2 | # | |
| 3 | # Uses $content_security_policy_cmc, which names img.cleberg.net. | |
| 4 | # DO NOT include this file in a `krz`/`zerolabs` vhost -- it would put the | |
| 5 | # real-name domain into a response header on a pseudonymous surface. | |
| 6 | # Those vhosts want custom.d/security/content-security-policy.conf, whose | |
| 7 | # policy is identity-neutral. | |
| 8 | add_header Content-Security-Policy $content_security_policy_cmc always; | |
linux/nginx/etc/nginx/custom.d/security/content-security-policy-freshrss-report-only.conf added +4
| @@ -0,0 +1,4 @@ | ||
| 1 | # rss.zerolabs.sh -- REPORT-ONLY. See the note in content_type_maps.conf: | |
| 2 | # the authenticated reading view cannot be exercised from the shell, and a | |
| 3 | # wrong img-src silently removes every article image. | |
| 4 | add_header Content-Security-Policy-Report-Only $csp_freshrss always; | |
linux/nginx/etc/nginx/custom.d/security/content-security-policy-inline.conf added +9
| @@ -0,0 +1,9 @@ | ||
| 1 | # Content-Security-Policy for apps that ship inline <script>/<style>. | |
| 2 | # | |
| 3 | # Identity-neutral (names no host), so it is safe on any vhost -- but it allows | |
| 4 | # 'unsafe-inline' for scripts and styles, which is exactly what a strict CSP is | |
| 5 | # meant to prevent. Prefer custom.d/security/content-security-policy.conf | |
| 6 | # wherever the app does not need this. | |
| 7 | # | |
| 8 | # Used by: office.zerolabs.sh. | |
| 9 | add_header Content-Security-Policy $content_security_policy_inline always; | |
linux/nginx/etc/nginx/custom.d/security/content-security-policy-piped-report-only.conf added +5
| @@ -0,0 +1,5 @@ | ||
| 1 | # piped.krz.sh -- REPORT-ONLY. Violations are reported to the browser console; | |
| 2 | # nothing is blocked. Promote to enforcing (rename the header to | |
| 3 | # Content-Security-Policy) only after loading a video, seeking, going | |
| 4 | # fullscreen and opening a channel page with a real browser console open. | |
| 5 | add_header Content-Security-Policy-Report-Only $csp_piped always; | |
linux/nginx/etc/nginx/custom.d/security/headers_in_location.conf added +27
| @@ -0,0 +1,27 @@ | ||
| 1 | # Re-includable copy of the server-level security headers. | |
| 2 | # | |
| 3 | # WHY THIS EXISTS -- nginx's add_header does NOT merge across levels. | |
| 4 | # A location block containing ANY add_header discards EVERY add_header | |
| 5 | # inherited from the server block. So a location that only wants to add | |
| 6 | # `Vary: Accept` silently loses the entire security header set. | |
| 7 | # | |
| 8 | # This bit cleberg.net hard: `location = /` (the homepage), `location /org/` | |
| 9 | # and `location ~ ^/(.+)\.html$` each set `add_header Vary`, and therefore | |
| 10 | # served NO Referrer-Policy, X-Content-Type-Options, X-Frame-Options, HSTS, | |
| 11 | # Permissions-Policy or CSP at all. Confirmed at the origin and end-to-end | |
| 12 | # over a real Tor circuit: the onion homepage returned zero security headers. | |
| 13 | # | |
| 14 | # It was invisible on the clearnet path because Cloudflare adds its own header | |
| 15 | # set at the edge, masking the gap -- but Tor visitors reach nginx directly, | |
| 16 | # so they got nothing. That is precisely the audience the onion exists for. | |
| 17 | # | |
| 18 | # Include this in ANY location that sets its own add_header. | |
| 19 | # Mirrors custom.d/basic.conf. CSP is NOT here -- it is per-vhost, so include | |
| 20 | # the right one alongside this file: | |
| 21 | # cleberg.* -> custom.d/security/content-security-policy-cmc.conf | |
| 22 | # krz/zerolabs/etc -> custom.d/security/content-security-policy.conf | |
| 23 | add_header Referrer-Policy $referrer_policy always; | |
| 24 | add_header X-Content-Type-Options nosniff always; | |
| 25 | add_header X-Frame-Options $x_frame_options always; | |
| 26 | add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always; | |
| 27 | add_header Permissions-Policy $permissions_policy always; | |
linux/nginx/etc/nginx/custom.d/security/headers_in_location_media.conf added +11
| @@ -0,0 +1,11 @@ | ||
| 1 | # Security headers for media apps, for use INSIDE a location block. | |
| 2 | # | |
| 3 | # Same purpose as headers_in_location.conf, but uses | |
| 4 | # $permissions_policy_media, which permits fullscreen/autoplay/PiP on the | |
| 5 | # app's own origin. The standard policy sets fullscreen=(), which breaks | |
| 6 | # video players -- that is why piped.conf historically included no headers | |
| 7 | # at all rather than the wrong ones. | |
| 8 | add_header Referrer-Policy $referrer_policy always; | |
| 9 | add_header X-Content-Type-Options nosniff always; | |
| 10 | add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always; | |
| 11 | add_header Permissions-Policy $permissions_policy_media always; | |
linux/nginx/etc/nginx/custom.d/security/headers_in_location_no_xfo.conf added +12
| @@ -0,0 +1,12 @@ | ||
| 1 | # Same as headers_in_location.conf but WITHOUT X-Frame-Options. | |
| 2 | # | |
| 3 | # For locations that set their own X-Frame-Options. Including the standard | |
| 4 | # bundle there would emit TWO conflicting X-Frame-Options headers (the shared | |
| 5 | # map's DENY plus the vhost's own SAMEORIGIN); browsers treat a conflicting | |
| 6 | # pair as invalid or apply the most restrictive, either way not what was meant. | |
| 7 | # | |
| 8 | # Used by: rss.zerolabs.sh, which needs SAMEORIGIN (FreshRSS frames itself). | |
| 9 | add_header Referrer-Policy $referrer_policy always; | |
| 10 | add_header X-Content-Type-Options nosniff always; | |
| 11 | add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always; | |
| 12 | add_header Permissions-Policy $permissions_policy always; | |