diff --git a/systems/jeeves/web_services/haproxy.cfg b/systems/jeeves/web_services/haproxy.cfg index 3281cf4..14d4be2 100644 --- a/systems/jeeves/web_services/haproxy.cfg +++ b/systems/jeeves/web_services/haproxy.cfg @@ -37,6 +37,23 @@ frontend ContentSwitching acl host_gitea hdr(host) -i gitea.tmmworkshop.com acl host_norn_sight hdr(host) -i www.norn-sight.com + # --- Request logging --- + # Capture the Host header and User-Agent so the httplog shows who is + # requesting what. They appear in the log's {captured|headers} field, + # in this order: {host|user-agent}. Client IP is already logged by httplog. + # Kept above the rate limiting so denied (429) requests are captured too. + http-request capture req.hdr(Host) len 100 + http-request capture req.hdr(User-Agent) len 128 + + # --- robots.txt --- + # Serve a single global robots.txt for every vhost (asks crawlers to wait + # 10s between requests via Crawl-delay). Returned for both HTTP and HTTPS. + # File is deployed to /etc/haproxy/robots.txt by haproxy.nix. + # Kept above the rate limiting so crawlers can always read it: `return` is a + # terminating action, so robots.txt requests are never tracked or denied. + acl is_robots path /robots.txt + http-request return status 200 content-type "text/plain" file /etc/haproxy/robots.txt if is_robots + # --- Rate limiting (Gitea only, per source IP) --- # Trusted devices exempt from rate limiting (add one line per IP/CIDR). # Internal / reserved-for-private-use ranges: @@ -47,42 +64,25 @@ frontend ContentSwitching # Add specific public devices below as needed: # acl rate_limit_allowlist src 192.0.2.50 - # Logged-in Gitea sessions bypass the rate limits. Gitea sets the - # `i_like_gitea` session cookie on login, and it is only sent to the Gitea - # vhost, so this only affects Gitea traffic. Note: this matches cookie - # PRESENCE, not validity, so it filters anonymous crawlers (which carry no - # cookie) rather than acting as a hard security boundary. - acl gitea_logged_in req.cook(i_like_gitea) -m found - # Track HTTP request rate per client IP over a 10s sliding window. Only Gitea # is rate-limited; all other vhosts are left alone. # ipv6 table type also covers IPv4 (mapped), so it works for both binds. stick-table type ipv6 size 100k expire 30s store http_req_rate(10s) - http-request track-sc0 src if host_gitea !is_acme !rate_limit_allowlist !gitea_logged_in + http-request track-sc0 src if host_gitea !is_acme !rate_limit_allowlist # Threshold: deny (429) when a client exceeds this many requests per 10s. - acl over_rate_limit sc_http_req_rate(0) gt 10 - http-request deny deny_status 429 if over_rate_limit host_gitea !is_acme !rate_limit_allowlist !gitea_logged_in - - # --- Request logging --- - # Capture the Host header and User-Agent so the httplog shows who is - # requesting what. They appear in the log's {captured|headers} field, - # in this order: {host|user-agent}. Client IP is already logged by httplog. - http-request capture req.hdr(Host) len 100 - http-request capture req.hdr(User-Agent) len 128 - - # --- robots.txt --- - # Serve a single global robots.txt for every vhost (asks crawlers to wait - # 10s between requests via Crawl-delay). Returned for both HTTP and HTTPS. - # File is deployed to /etc/haproxy/robots.txt by haproxy.nix. - acl is_robots path /robots.txt - http-request return status 200 content-type "text/plain" file /etc/haproxy/robots.txt if is_robots + # Kept loose (50/10s) since logged-in users are rate-limited too; a page + # load can burst a few dozen asset requests. + acl over_rate_limit sc_http_req_rate(0) gt 50 + http-request deny deny_status 429 if over_rate_limit host_gitea !is_acme !rate_limit_allowlist # --- Per-endpoint limit: Gitea compare/diff is expensive; cap at 1 req / 5 min / IP --- # Tracked in a separate 5-minute table (st_compare) since a proxy has only one # inline stick-table. Allow-listed (internal) IPs are exempt. - acl is_gitea_compare path_beg /Richie/dotfiles/compare - http-request track-sc1 src table st_compare if host_gitea is_gitea_compare !rate_limit_allowlist !gitea_logged_in - http-request deny deny_status 429 if host_gitea is_gitea_compare !rate_limit_allowlist !gitea_logged_in { sc_http_req_rate(1,st_compare) gt 1 } + # Matches ///compare on every repo; -i because Gitea routes are + # case-insensitive. + acl is_gitea_compare path_reg -i ^/[^/]+/[^/]+/compare + http-request track-sc1 src table st_compare if host_gitea is_gitea_compare !rate_limit_allowlist + http-request deny deny_status 429 if host_gitea is_gitea_compare !rate_limit_allowlist { sc_http_req_rate(1,st_compare) gt 1 } # Hosts allowed to serve plain HTTP (add entries to skip the HTTPS redirect) acl allow_http hdr(host) -i __none__ @@ -117,11 +117,9 @@ backend cache_nodes server server 127.0.0.1:5000 backend jellyfin - option httpchk - option forwardfor - http-check send meth GET uri /health - http-check expect string Healthy - server jellyfin 127.0.0.1:8096 + mode http + option forwardfor + server jellyfin 127.0.0.1:8096 backend share_nodes mode http