cmc/dotfiles

Using GNU Stow to manage my dotfiles. config dotfiles macos stow

Commit 41618d30fc

41618d30fc1fdc9ef26aa5003ac48fadf0d1fe82

parent: 0c68f866ef

Verified · cmc

cmc <hello@cleberg.net> · 2026-08-03 16:43 UTC

revamp security headers and site confs

Layout: unified · split

linux/nginx/etc/nginx/conf.d/cleberg.dev.conf +1
@@ -5,6 +5,7 @@ server {
5 server_name cleberg.dev; 5 server_name cleberg.dev;
6 root /var/www/cleberg.dev/; 6 root /var/www/cleberg.dev/;
7 include custom.d/basic.conf; 7 include custom.d/basic.conf;
8 include custom.d/security/content-security-policy.conf;
8 location / { try_files $uri $uri/ =404; } 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 # No per-vhost error_log: it overrode the global "off" and wrote visitor 5 # No per-vhost error_log: it overrode the global "off" and wrote visitor
6 # IPs to disk. 6 # IPs to disk.
7 7
8 # basic.conf now carries HSTS + Permissions-Policy. CSP stays explicit -- 8 # basic.conf carries HSTS + Permissions-Policy. CSP stays explicit, and now
9 # the shared policy is written for this vhost (it allows img.cleberg.net). 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 include custom.d/basic.conf; 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 root /var/www/cleberg.net/; 17 root /var/www/cleberg.net/;
13 include custom.d/redirects/blog.conf; 18 include custom.d/redirects/blog.conf;
14 port_in_redirect off; 19 port_in_redirect off;
@@ -21,6 +26,10 @@ server {
21 default_type text/markdown; 26 default_type text/markdown;
22 add_header Content-Type "text/markdown; charset=utf-8" always; 27 add_header Content-Type "text/markdown; charset=utf-8" always;
23 add_header Vary "Accept" always; 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 location = / { 35 location = / {
@@ -29,6 +38,11 @@ server {
29 } 38 }
30 39
31 add_header Vary "Accept" always; 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 try_files /index.html =404; 46 try_files /index.html =404;
33 } 47 }
34 48
@@ -48,6 +62,11 @@ server {
48 rewrite ^/(.+)\.html$ /org/$1.org last; 62 rewrite ^/(.+)\.html$ /org/$1.org last;
49 } 63 }
50 add_header Vary "Accept" always; 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 try_files $uri =404; 70 try_files $uri =404;
52 } 71 }
53} 72}
linux/nginx/etc/nginx/conf.d/cv.conf +2
@@ -3,6 +3,7 @@ server {
3 server_name cv.cleberg.net; 3 server_name cv.cleberg.net;
4 add_header Onion-Location "http://xe43aewwiybmbo5qsstx7t5kmgkd2ei4bkkvqinwd7fxfmkekvnjbbad.onion$request_uri" always; 4 add_header Onion-Location "http://xe43aewwiybmbo5qsstx7t5kmgkd2ei4bkkvqinwd7fxfmkekvnjbbad.onion$request_uri" always;
5 include custom.d/basic.conf; 5 include custom.d/basic.conf;
6 include custom.d/security/content-security-policy.conf;
6 root /var/www/cv/; 7 root /var/www/cv/;
7 autoindex on; 8 autoindex on;
8 location / { try_files $uri $uri/ /index.html; } 9 location / { try_files $uri $uri/ /index.html; }
@@ -12,6 +13,7 @@ server {
12 listen 127.0.0.1:10015; 13 listen 127.0.0.1:10015;
13 server_name xe43aewwiybmbo5qsstx7t5kmgkd2ei4bkkvqinwd7fxfmkekvnjbbad.onion; 14 server_name xe43aewwiybmbo5qsstx7t5kmgkd2ei4bkkvqinwd7fxfmkekvnjbbad.onion;
14 include custom.d/basic.conf; 15 include custom.d/basic.conf;
16 include custom.d/security/content-security-policy.conf;
15 root /var/www/cv/; 17 root /var/www/cv/;
16 autoindex on; 18 autoindex on;
17 location / { try_files $uri $uri/ /index.html; } 19 location / { try_files $uri $uri/ /index.html; }
linux/nginx/etc/nginx/conf.d/files.conf +2
@@ -7,6 +7,7 @@ server {
7 server_name files.krz.sh; 7 server_name files.krz.sh;
8 add_header Onion-Location "http://yt2y635dc6zyxziy5ytkfz36bmkgcxuccq3meom6qp44gjdat54wt5id.onion$request_uri" always; 8 add_header Onion-Location "http://yt2y635dc6zyxziy5ytkfz36bmkgcxuccq3meom6qp44gjdat54wt5id.onion$request_uri" always;
9 include custom.d/basic.conf; 9 include custom.d/basic.conf;
10 include custom.d/security/content-security-policy.conf;
10 root /var/www/files/; 11 root /var/www/files/;
11 autoindex on; 12 autoindex on;
12 location / { try_files $uri $uri/ /index.html; } 13 location / { try_files $uri $uri/ /index.html; }
@@ -16,6 +17,7 @@ server {
16 listen 127.0.0.1:10018; 17 listen 127.0.0.1:10018;
17 server_name yt2y635dc6zyxziy5ytkfz36bmkgcxuccq3meom6qp44gjdat54wt5id.onion; 18 server_name yt2y635dc6zyxziy5ytkfz36bmkgcxuccq3meom6qp44gjdat54wt5id.onion;
18 include custom.d/basic.conf; 19 include custom.d/basic.conf;
20 include custom.d/security/content-security-policy.conf;
19 root /var/www/files/; 21 root /var/www/files/;
20 autoindex on; 22 autoindex on;
21 location / { try_files $uri $uri/ /index.html; } 23 location / { try_files $uri $uri/ /index.html; }
linux/nginx/etc/nginx/conf.d/hn.conf +1
@@ -7,6 +7,7 @@ server {
7 autoindex on; 7 autoindex on;
8 add_header Onion-Location "http://r3yfeffyj7ornpikojmw75u3sn2la7tqnmcmwgv2ov7if24sm5czqeid.onion$request_uri" always; 8 add_header Onion-Location "http://r3yfeffyj7ornpikojmw75u3sn2la7tqnmcmwgv2ov7if24sm5czqeid.onion$request_uri" always;
9 include custom.d/basic.conf; 9 include custom.d/basic.conf;
10 include custom.d/security/content-security-policy.conf;
10 location / { try_files $uri $uri/ /index.html; } 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 server_name img.cleberg.net; 3 server_name img.cleberg.net;
4 add_header Onion-Location "http://ltf2dfg7tj263ll24lxuq7igejcrhcazjp6whgg6u6vyjir2iahqggad.onion$request_uri" always; 4 add_header Onion-Location "http://ltf2dfg7tj263ll24lxuq7igejcrhcazjp6whgg6u6vyjir2iahqggad.onion$request_uri" always;
5 include custom.d/basic.conf; 5 include custom.d/basic.conf;
6 include custom.d/security/content-security-policy.conf;
6 root /var/www/img/; 7 root /var/www/img/;
7 autoindex on; 8 autoindex on;
8 location / { try_files $uri $uri/ =404; } 9 location / { try_files $uri $uri/ =404; }
@@ -12,6 +13,7 @@ server {
12 listen 127.0.0.1:10026; 13 listen 127.0.0.1:10026;
13 server_name ltf2dfg7tj263ll24lxuq7igejcrhcazjp6whgg6u6vyjir2iahqggad.onion; 14 server_name ltf2dfg7tj263ll24lxuq7igejcrhcazjp6whgg6u6vyjir2iahqggad.onion;
14 include custom.d/basic.conf; 15 include custom.d/basic.conf;
16 include custom.d/security/content-security-policy.conf;
15 root /var/www/img/; 17 root /var/www/img/;
16 autoindex on; 18 autoindex on;
17 location / { try_files $uri $uri/ =404; } 19 location / { try_files $uri $uri/ =404; }
linux/nginx/etc/nginx/conf.d/office.conf +2
@@ -5,12 +5,14 @@ server {
5 add_header Onion-Location "http://uwtjz6pof52bzdkj242mub77ls2ji3qiznbsuoir235rxynqxadpghid.onion$request_uri" always; 5 add_header Onion-Location "http://uwtjz6pof52bzdkj242mub77ls2ji3qiznbsuoir235rxynqxadpghid.onion$request_uri" always;
6 root /var/www/office/; 6 root /var/www/office/;
7 include custom.d/basic.conf; 7 include custom.d/basic.conf;
8 include custom.d/security/content-security-policy-inline.conf;
8 location / { try_files $uri $uri/ /index.html; } 9 location / { try_files $uri $uri/ /index.html; }
9} 10}
10server { 11server {
11 listen 127.0.0.1:10032; 12 listen 127.0.0.1:10032;
12 server_name uwtjz6pof52bzdkj242mub77ls2ji3qiznbsuoir235rxynqxadpghid.onion; 13 server_name uwtjz6pof52bzdkj242mub77ls2ji3qiznbsuoir235rxynqxadpghid.onion;
13 include custom.d/basic.conf; 14 include custom.d/basic.conf;
15 include custom.d/security/content-security-policy-inline.conf;
14 root /var/www/office/; 16 root /var/www/office/;
15 location / { try_files $uri $uri/ /index.html; } 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 add_header Onion-Location "http://7vsifofpucgi3wva52nbwhwglrmbyxgdapbkkmgniqlsskspux5zocyd.onion$request_uri" always; 5 add_header Onion-Location "http://7vsifofpucgi3wva52nbwhwglrmbyxgdapbkkmgniqlsskspux5zocyd.onion$request_uri" always;
6 root /var/www/org/; 6 root /var/www/org/;
7 include custom.d/basic.conf; 7 include custom.d/basic.conf;
8 include custom.d/security/content-security-policy.conf;
8 location / { try_files $uri $uri/ /index.html; } 9 location / { try_files $uri $uri/ /index.html; }
9} 10}
10server { 11server {
11 listen 127.0.0.1:10034; 12 listen 127.0.0.1:10034;
12 server_name 7vsifofpucgi3wva52nbwhwglrmbyxgdapbkkmgniqlsskspux5zocyd.onion; 13 server_name 7vsifofpucgi3wva52nbwhwglrmbyxgdapbkkmgniqlsskspux5zocyd.onion;
13 include custom.d/basic.conf; 14 include custom.d/basic.conf;
15 include custom.d/security/content-security-policy.conf;
14 root /var/www/org/; 16 root /var/www/org/;
15 location / { try_files $uri $uri/ /index.html; } 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 3# HISTORY: this file used to be a Host-based router for all three Piped
4# localhost:8077 -- 4# hostnames, because the Cloudflare tunnel sent pipedapi.krz.sh and
5# piped.krz.sh -> :8077 5# pipedproxy.krz.sh to :8077 as well, landing them on the frontend and breaking
6# pipedapi.krz.sh -> :8077 (should be the backend) 6# every API call. The dashboard was corrected 2026-08-03 to point pipedapi at
7# pipedproxy.krz.sh -> :8077 (should be the media proxy) 7# :8078 and pipedproxy at :8079 directly, so those two server blocks became
8# so the API and media-proxy hostnames landed on the frontend and Piped was 8# dead code and were removed.
9# broken. The frontend advertises BACKEND_HOSTNAME=pipedapi.krz.sh to browsers,
10# so every API call failed.
11# 9#
12# The tidier fix is two edits in the Cloudflare dashboard (point pipedapi at 10# Ports: frontend :8076, backend :8078, media proxy :8079.
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.
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# NOTE: custom.d/basic.conf is deliberately NOT included. Its Permissions-Policy 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# Frontend. default_server so the Tor onion for piped (which targets :8077 with 23# Frontend. default_server so the Tor onion for piped (which targets :8077 with
23# a .onion Host header) also lands here. 24# a .onion Host header) also lands here.
@@ -32,43 +33,13 @@ server {
32 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 33 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
33 proxy_set_header X-Forwarded-Proto $scheme; 34 proxy_set_header X-Forwarded-Proto $scheme;
34 proxy_http_version 1.1; 35 proxy_http_version 1.1;
35 }
36}
37
38# Backend API.
39server {
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.
57server {
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; 37 # This vhost previously served NO security headers at all, because
70 proxy_request_buffering off; 38 # basic.conf was excluded wholesale to avoid its `fullscreen=()`.
71 proxy_read_timeout 300; 39 # The media bundle keeps every other restriction and permits
72 proxy_send_timeout 300; 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 @@
1server {
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
12server {
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 server_name reminiscecleberg.com; 5 server_name reminiscecleberg.com;
6 root /var/www/reminiscecleberg.com/; 6 root /var/www/reminiscecleberg.com/;
7 include custom.d/basic.conf; 7 include custom.d/basic.conf;
8 include custom.d/security/content-security-policy.conf;
8 location / { try_files $uri $uri/ =404; } 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 listen 127.0.0.1:10001; 2 listen 127.0.0.1:10001;
3 server_name rogue.krz.sh; 3 server_name rogue.krz.sh;
4 4
5 root /var/www/rogue; 5 root /var/www/rogue/;
6 index index.html; 6 index index.html;
7 autoindex on;
7 8
8 server_tokens off; 9 server_tokens off;
9 etag off; 10 etag off;
@@ -13,9 +14,15 @@ server {
13 gzip_vary off; 14 gzip_vary off;
14 charset off; 15 charset off;
15 16
16 location = / { 17 # `/` used to 404: this block did `try_files /index.html =404` and there is
17 try_files /index.html =404; 18 # no index.html at the webroot, so it short-circuited before `autoindex on`
18 } 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 location = /index.html { 27 location = /index.html {
21 try_files /index.html =404; 28 try_files /index.html =404;
linux/nginx/etc/nginx/conf.d/rss.conf +8
@@ -6,7 +6,15 @@ server {
6 include custom.d/basic.conf; 6 include custom.d/basic.conf;
7 location / { 7 location / {
8 proxy_pass http://freshrss/; 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 add_header X-Frame-Options SAMEORIGIN; 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 proxy_redirect off; 18 proxy_redirect off;
11 proxy_buffering off; 19 proxy_buffering off;
12 proxy_set_header Host $host; 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# Add Content-Security-Policy for HTML documents. 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.
35map $sent_http_content_type $content_security_policy { 52map $sent_http_content_type $content_security_policy {
36 ~*text/(html|javascript)|application/pdf|xml " 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";
37 default-src 'self'; 54}
38 img-src 'self' https://img.cleberg.net; 55
39 base-uri 'none'; 56# cleberg.* only. Mirrors what the site actually loads, verified by grepping
40 form-action 'self'; 57# the served HTML: <img src> -> img.cleberg.net (151), <script src> ->
41 frame-ancestors 'none'; 58# bubbles.town (174), stylesheets -> https://cleberg.net (180, absolute).
42 object-src 'none'; 59#
43 upgrade-insecure-requests 60# `https://cleberg.net` must be allowed explicitly: the HTML uses ABSOLUTE
44 "; 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.
68map $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.
81map $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.
103map $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.
112map $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.
120map $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# Add Permissions-Policy for HTML documents. 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.
8add_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.
4add_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.
9add_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.
5add_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
23add_header Referrer-Policy $referrer_policy always;
24add_header X-Content-Type-Options nosniff always;
25add_header X-Frame-Options $x_frame_options always;
26add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
27add_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.
8add_header Referrer-Policy $referrer_policy always;
9add_header X-Content-Type-Options nosniff always;
10add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
11add_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).
9add_header Referrer-Policy $referrer_policy always;
10add_header X-Content-Type-Options nosniff always;
11add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
12add_header Permissions-Policy $permissions_policy always;