From f71c0c8ed601991b6bab466634085288799ec065 Mon Sep 17 00:00:00 2001 From: Richie Cahill Date: Tue, 23 Jun 2026 23:38:16 -0400 Subject: [PATCH] added rate limits --- systems/jeeves/web_services/haproxy.cfg | 59 ++++++++++++++++++++++--- 1 file changed, 52 insertions(+), 7 deletions(-) diff --git a/systems/jeeves/web_services/haproxy.cfg b/systems/jeeves/web_services/haproxy.cfg index 0ae1794..3281cf4 100644 --- a/systems/jeeves/web_services/haproxy.cfg +++ b/systems/jeeves/web_services/haproxy.cfg @@ -29,6 +29,47 @@ frontend ContentSwitching # ACME challenge routing (must be first) acl is_acme path_beg /.well-known/acme-challenge/ + # Host ACLs (defined early so rate-limiting can scope to a single vhost) + acl host_audiobookshelf hdr(host) -i audiobookshelf.tmmworkshop.com + acl host_cache hdr(host) -i cache.tmmworkshop.com + acl host_jellyfin hdr(host) -i jellyfin.tmmworkshop.com + acl host_share hdr(host) -i share.tmmworkshop.com + acl host_gitea hdr(host) -i gitea.tmmworkshop.com + acl host_norn_sight hdr(host) -i www.norn-sight.com + + # --- 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: + # IPv4: RFC 1918 private, loopback, link-local + acl rate_limit_allowlist src 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16 127.0.0.0/8 169.254.0.0/16 + # IPv6: loopback, unique local (ULA), link-local + acl rate_limit_allowlist src ::1/128 fc00::/7 fe80::/10 + # 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 + # 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. @@ -36,13 +77,12 @@ frontend ContentSwitching acl is_robots path /robots.txt http-request return status 200 content-type "text/plain" file /etc/haproxy/robots.txt if is_robots - # tmmworkshop.com - acl host_audiobookshelf hdr(host) -i audiobookshelf.tmmworkshop.com - acl host_cache hdr(host) -i cache.tmmworkshop.com - acl host_jellyfin hdr(host) -i jellyfin.tmmworkshop.com - acl host_share hdr(host) -i share.tmmworkshop.com - acl host_gitea hdr(host) -i gitea.tmmworkshop.com - acl host_norn_sight hdr(host) -i www.norn-sight.com + # --- 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 } # Hosts allowed to serve plain HTTP (add entries to skip the HTTPS redirect) acl allow_http hdr(host) -i __none__ @@ -59,6 +99,11 @@ frontend ContentSwitching use_backend gitea if host_gitea use_backend norn_sight if host_norn_sight +# Stick-table only (no servers): tracks per-IP request rate to Gitea's compare +# endpoint over a 5-minute window so the frontend can cap it at 1 per 5 min. +backend st_compare + stick-table type ipv6 size 100k expire 600s store http_req_rate(300s) + backend acme_challenge mode http server acme 127.0.0.1:8402