diff --git a/Caddyfile.prod b/Caddyfile.prod index 2b90894..7dde5b4 100644 --- a/Caddyfile.prod +++ b/Caddyfile.prod @@ -78,3 +78,59 @@ api-direct.github-store.org { output stdout } } + +# Komi Store paid backend. Hosted on the same VPS, separate Docker service +# (paid-app), separate Postgres database (komistore_paid). Will eventually +# absorb the github-store.org hostnames above once the free-tier migration +# lands; for now the two run side-by-side under distinct vhosts so DNS, +# Stripe webhook URLs, and Cloudflare KV bindings stay decoupled. +# +# DNS prereq: api.komistore.app must resolve to this VPS for Caddy's +# HTTP-01 challenge to issue a Let's Encrypt cert. Either: +# (a) DNS-only (grey-cloud) A record -> VPS IP, OR +# (b) Cloudflare proxied + SSL mode "Full (strict)" + Cloudflare origin +# cert OR DNS-01 challenge configured in Caddy. +# Start with (a) until traffic justifies fronting. +api.komistore.app { + # Direct-to-VPS path (grey-cloud DNS). Overwrite any client-supplied + # X-Forwarded-For with the real TCP source — without this a client who + # forges X-Forwarded-For can rotate past paid-app's rate limiter + # (which keys on X-Forwarded-For's first IP). Same defence as + # api-direct.github-store.org above. + request_header X-Forwarded-For {remote_host} + + # Strip any client-supplied CF-Connecting-IP. No Cloudflare hop on this + # vhost yet (grey-cloud); accepting the header would let any client + # claim any source IP. Re-evaluate if/when this vhost goes orange-cloud. + request_header -CF-Connecting-IP + + # Body cap. Stripe webhooks can be ~10 KB; checkout payloads are tiny. + # 1 MB matches the free-tier ceiling -- bumps need a written reason. + request_body { + max_size 1MB + } + + reverse_proxy paid-app:8080 + + header { + -Server + X-Content-Type-Options nosniff + X-Frame-Options DENY + Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" + # Looser than the API-only CSP above because the paid backend + # serves /legal HTML pages (terms, privacy) alongside JSON. + # default-src 'none' would block same-origin assets the legal + # pages load (linked stylesheets, images). Note that 'self' does + # NOT permit inline