{ config, pkgs, lib, ... }: { services.caddy = { enable = true; package = pkgs.caddy-custom; globalConfig = '' order rate_limit before basicauth servers { # Trust Cloudflare's edge IPs so {client_ip} resolves to the real visitor trusted_proxies static 173.245.48.0/20 103.21.244.0/22 103.22.200.0/22 103.31.4.0/22 141.101.64.0/18 108.162.192.0/18 190.93.240.0/20 188.114.96.0/20 197.234.240.0/22 198.41.128.0/17 162.158.0.0/15 104.16.0.0/13 104.24.0.0/14 172.64.0.0/13 131.0.72.0/22 2400:cb00::/32 2606:4700::/32 2803:f800::/32 2405:b500::/32 2405:8100::/32 2a06:98c0::/29 2c0f:f248::/32 metrics } ''; virtualHosts = { "www.noisebridge.net" = { extraConfig = '' # Health check endpoint handle /health { respond "ok" 200 } # Bot blocking @bots header_regexp User-Agent "(?i)(ClaudeBot|GPTBot|CCBot|Bytespider|AhrefsBot|SemrushBot|MJ12bot|DotBot|PetalBot|Amazonbot|anthropic-ai|ChatGPT-User|cohere-ai|FacebookBot|Google-Extended|PerplexityBot)" respond @bots 403 # robots.txt handle /robots.txt { respond "User-agent: ClaudeBot Disallow: / User-agent: GPTBot Disallow: / User-agent: CCBot Disallow: / User-agent: Bytespider Disallow: / User-agent: anthropic-ai Disallow: / User-agent: ChatGPT-User Disallow: / User-agent: * Allow: / Sitemap: https://www.noisebridge.net/sitemap.xml " } # Rate limiting for anonymous users (no session cookie) # {client_ip} works with or without a reverse proxy in front @anon { not header_regexp Cookie "nb_wiki_session=" } rate_limit @anon { zone anon_zone { key {client_ip} events 60 window 1m } } # Cache headers: anon gets public caching, logged-in gets private @logged_in { header_regexp Cookie "nb_wiki_session=" } header @anon Cache-Control "public, max-age=7200" header @logged_in Cache-Control "private, no-cache" # Security headers header { Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" X-Content-Type-Options "nosniff" X-Frame-Options "SAMEORIGIN" Referrer-Policy "strict-origin-when-cross-origin" } # Proxy to PHP-FPM php_fastcgi unix//run/phpfpm/mediawiki.sock { root ${config.services.mediawiki.finalPackage}/share/mediawiki } file_server { root ${config.services.mediawiki.finalPackage}/share/mediawiki } ''; }; "grafana.noisebridge.net" = { extraConfig = '' reverse_proxy localhost:3000 ''; }; # Domain redirects "noisebridge.net" = { extraConfig = '' redir https://www.noisebridge.net{uri} permanent ''; }; "noisebridge.com" = { extraConfig = '' redir https://www.noisebridge.net{uri} permanent ''; }; "noisebridge.org" = { extraConfig = '' redir https://www.noisebridge.net{uri} permanent ''; }; "noisebridge.io" = { extraConfig = '' redir https://www.noisebridge.net{uri} permanent ''; }; # ── Tor .onion vhost ── # Tor daemon forwards port 80 → localhost:8080. Caddy listens here # with HTTP only (no TLS — .onion v3 is already end-to-end encrypted). # # Differences from the clearnet vhost: # - No IP-based rate limiting (all Tor traffic arrives from 127.0.0.1) # - No HSTS (no TLS to enforce) # - No Cache-Control: public (no CDN to cache at) # - Bot blocking by User-Agent still works ":8080" = { extraConfig = '' # Bot blocking (same list as clearnet) @bots header_regexp User-Agent "(?i)(ClaudeBot|GPTBot|CCBot|Bytespider|AhrefsBot|SemrushBot|MJ12bot|DotBot|PetalBot|Amazonbot|anthropic-ai|ChatGPT-User|cohere-ai|FacebookBot|Google-Extended|PerplexityBot)" respond @bots 403 # robots.txt — block everything on .onion (no reason for bots to index) handle /robots.txt { respond "User-agent: * Disallow: / " } # Security headers (no HSTS — no TLS over .onion) header { X-Content-Type-Options "nosniff" X-Frame-Options "SAMEORIGIN" Referrer-Policy "no-referrer" X-Wiki-Access "tor" } php_fastcgi unix//run/phpfpm/mediawiki.sock { root ${config.services.mediawiki.finalPackage}/share/mediawiki } file_server { root ${config.services.mediawiki.finalPackage}/share/mediawiki } ''; }; }; }; # Port 8080 is only for local Tor daemon — not public # (firewall already blocks it since it's not in allowedTCPPorts) }