# Edge reverse proxy for the Scrabble contour. A single Basic-Auth gate covers # every operator surface under /_gm (the backend-rendered admin console and the # Grafana subpath); the game SPA (/app/, /telegram/, /vk/) and the Connect edge go to # the gateway; the catch-all — notably the public landing at / — goes to the # static landing container, so stray traffic never reaches the Go edge. # Mirrors ../galaxy-game's /_gm model. # # CADDY_SITE_ADDRESS is ":80" in the test contour (the host caddy terminates TLS # and forwards); set it to a domain in prod so this caddy does its own # ACME and the contour is self-contained. { admin off # Trust X-Forwarded-For from private-range upstreams so the real client IP survives # (chat moderation + per-IP rate limiting in the gateway). Test contour: the host caddy # (a private IP) is trusted, so its forwarded client IP is preserved. Prod (no host caddy): # clients connect from public IPs, which are NOT trusted, so Caddy uses the real peer — # the same config is correct (and spoof-safe) in both contours. servers { trusted_proxies static private_ranges } } {$CADDY_SITE_ADDRESS::80} { # HTTP/3 is advertised by default whenever this caddy terminates TLS (prod: # CADDY_SITE_ADDRESS is the domain). But UDP/443 is never reachable — the prod # compose maps only "443:443" (TCP) and ufw opens 443/tcp — so a client that cached # the `Alt-Svc: h3` advert (sticky for ma=2592000s) stalls on the dead QUIC path # before falling back to h2, which surfaced as the Telegram Mini App intermittently # hanging on load. `Alt-Svc: clear` actively drops any cached alternative and pins # clients to h2/h1; it is applied site-wide so every route is covered. In the test # contour this caddy serves plain :80 (no h3 to advertise) and the host caddy # re-stamps its own Alt-Svc, so the live test fix lives in the host caddy — here it # is the prod fix. Background + alternatives (incl. serving h3 for real): docs/EDGE_HTTP3.md. header Alt-Svc clear # Operator surfaces under /_gm: a single shared Basic-Auth, then route. @gm path /_gm /_gm/* handle @gm { basic_auth { {$GM_BASICAUTH_USER:gm} {$GM_BASICAUTH_HASH} } # Grafana serves from this sub-path (GF_SERVER_SERVE_FROM_SUB_PATH=true), so # the prefix is forwarded intact, not stripped. handle /_gm/grafana* { reverse_proxy grafana:3000 } # Everything else under /_gm is the backend-rendered admin console. handle { reverse_proxy backend:8080 } } # The game SPA and the Connect edge are served by the gateway. Strip any # client-supplied X-Scrabble-Honeypot here so the gateway only ever honours the # tag the honeypot block sets below (a client cannot self-tag a real request). @gateway path /app /app/* /telegram /telegram/* /vk /vk/* /dict/* /scrabble.edge.v1.Gateway/* handle @gateway { reverse_proxy gateway:8081 { header_up -X-Scrabble-Honeypot } } # Honeypot decoy paths: classic vulnerability-scanner bait no real client ever # requests. Route them to the gateway tagged with X-Scrabble-Honeypot — the set # replaces any client-supplied value — so it logs the scanner hit and (in prod) # bans the source IP. (A delete + set in one block would not work: Caddy applies # header_up deletions after sets, which would strip the tag we just set; the real # endpoints instead strip the header in the @gateway block above.) Keep this list # disjoint from every legitimate landing/app path. @honeypot path /.env /.git /.git/* /.aws/* /wp-login.php /wp-admin /wp-admin/* /phpmyadmin /phpmyadmin/* handle @honeypot { reverse_proxy gateway:8081 { header_up X-Scrabble-Honeypot 1 } } # Everything else — the public landing at / and any stray path — is static. handle { reverse_proxy landing:80 } }