cmc/dotfiles

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

Commit 7628832bfe

7628832bfe9aaf3deafc2787e596e0ca65b0cad0

parent: 263f281b30

Verified · cmc

cmc <hello@cleberg.net> · 2026-08-03 17:55 UTC

nginx: enforce piped CSP, drop dead freshrss CSP and rss vhost

Promote piped.krz.sh from Content-Security-Policy-Report-Only to enforcing.
Validated in a real browser first: homepage, VOD playback and seek on two
videos, channel page and a 39-result search were all clean, and every host
contacted was already allowed (self, pipedapi, pipedproxy, blob:). No fallback
to pipedproxy.kavin.rocks, so viewer IPs stay on the self-hosted proxy. The
check was proven meaningful before being trusted by injecting a disallowed
image and confirming a securitypolicyviolation with disposition "report".

Remove the FreshRSS CSP (include file plus the $csp_freshrss map). The header
never reached a browser: rss.krz.sh is routed tunnel-direct to the container
on :8099 and never transits nginx. Promoting it would also have regressed
behaviour rather than hardened it, since browsers enforce the intersection of
stacked policies and this one was stricter than FreshRSS's own exactly where
feed content lives (no blob: in img-src, no * in media-src/frame-src).
FreshRSS keeps its own, correctly RSS-aware, policy.

Delete conf.d/rss.conf entirely. It was unreachable: no onion targets :10045,
no other vhost uses the port, and rss.zerolabs.sh is redirected to rss.krz.sh
at the Cloudflare edge before the origin is reached.

Layout: unified · split

linux/nginx/etc/nginx/conf.d/rss.conf deleted −31
@@ -1,31 +0,0 @@
1upstream freshrss { server 127.0.0.1:8099; keepalive 64; }
2server {
3 listen 127.0.0.1:10045;
4 server_name rss.zerolabs.sh;
5
6 include custom.d/basic.conf;
7 location / {
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.
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;
18 proxy_redirect off;
19 proxy_buffering off;
20 proxy_set_header Host $host;
21 proxy_set_header X-Real-IP $remote_addr;
22 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
23 proxy_set_header X-Forwarded-Proto $scheme;
24 proxy_set_header X-Forwarded-Port $server_port;
25 proxy_read_timeout 90;
26 proxy_set_header Authorization $http_authorization;
27 proxy_pass_header Authorization;
28 }
29 include custom.d/security/robots_index_only.conf;
30}
31
linux/nginx/etc/nginx/custom.d/http/content_type_maps.conf +13 −16
@@ -99,13 +99,19 @@ map $sent_http_content_type $content_security_policy_inline {
99 ~*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'"; 99 ~*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'";
100} 100}
101 101
102# --- Proxied third-party apps ------------------------------------------- 102# --- Proxied third-party app ---------------------------------------------
103# These two are shipped REPORT-ONLY (see the *-report-only.conf includes). 103# Shipped Report-Only first, then PROMOTED TO ENFORCING 2026-08-03 after a real
104# Both are third-party SPAs whose runtime behaviour cannot be exercised from 104# browser validated it: homepage, VOD playback + seek on two videos, channel
105# the shell -- no browser here -- and a wrong directive fails SILENTLY: video 105# page, and a 39-result search were all clean, and every host contacted was
106# stops playing, or every article image disappears, with only a console 106# already in the policy (self, pipedapi, pipedproxy, blob:).
107# message. Report-Only gives the visibility with zero outage risk. Promote to 107#
108# enforcing after checking a real browser console. 108# The check was proven meaningful before being believed: a deliberately
109# disallowed image was injected and fired a securitypolicyviolation with
110# disposition "report", confirming the header was live rather than absent.
111#
112# (A FreshRSS policy lived here too. It was removed -- rss.krz.sh is routed
113# tunnel-direct to the container and never transits nginx, so the header never
114# reached a browser. FreshRSS enforces its own, correctly RSS-aware, CSP.)
109 115
110# piped.krz.sh. Derived from evidence, not guesswork: 116# piped.krz.sh. Derived from evidence, not guesswork:
111# - connect-src: BACKEND_HOSTNAME=pipedapi.krz.sh (container env). 117# - connect-src: BACKEND_HOSTNAME=pipedapi.krz.sh (container env).
@@ -121,15 +127,6 @@ map $sent_http_content_type $csp_piped {
121 ~*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'"; 127 ~*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'";
122} 128}
123 129
124# rss.zerolabs.sh (FreshRSS). Operator-only behind Cloudflare Access, so the
125# T5 stake is low; the risk is breakage. An RSS reader renders arbitrary feed
126# HTML, so img/media must allow remote hosts or every article image dies.
127# That remote fetching is inherent to the app and is controlled by FreshRSS's
128# own "load remote images" setting, not by CSP.
129map $sent_http_content_type $csp_freshrss {
130 ~*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'";
131}
132
133# Permissions-Policy variant for media apps. The shared policy sets 130# Permissions-Policy variant for media apps. The shared policy sets
134# `fullscreen=()`, which is exactly why piped.conf excludes basic.conf -- it 131# `fullscreen=()`, which is exactly why piped.conf excludes basic.conf -- it
135# would stop videos going fullscreen. This keeps every other restriction and 132# would stop videos going fullscreen. This keeps every other restriction and
linux/nginx/etc/nginx/custom.d/security/content-security-policy-freshrss-report-only.conf deleted −4
@@ -1,4 +0,0 @@
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-piped-report-only.conf +1 −1
@@ -2,4 +2,4 @@
2# nothing is blocked. Promote to enforcing (rename the header to 2# nothing is blocked. Promote to enforcing (rename the header to
3# Content-Security-Policy) only after loading a video, seeking, going 3# Content-Security-Policy) only after loading a video, seeking, going
4# fullscreen and opening a channel page with a real browser console open. 4# fullscreen and opening a channel page with a real browser console open.
5add_header Content-Security-Policy-Report-Only $csp_piped always; 5add_header Content-Security-Policy $csp_piped always;