diff --git a/docs/deployment/hetzner-server-setup.md b/docs/deployment/hetzner-server-setup.md index 4d3c504f..20a67f5f 100644 --- a/docs/deployment/hetzner-server-setup.md +++ b/docs/deployment/hetzner-server-setup.md @@ -758,7 +758,7 @@ Then restart Gitea: cd ~/gitea && docker compose up -d gitea ``` -### Future: Multi-Tenant Store Routing +### Multi-Tenant Store Routing Stores on each platform use two routing modes: @@ -769,49 +769,204 @@ Both modes are handled by the `StoreContextMiddleware` which reads the `Host` he #### Wildcard Subdomains (for store subdomains) -When stores start using subdomains like `acme.omsflow.lu`, add wildcard blocks: +Each non-main platform uses a wildcard Caddy block so any store subdomain (e.g. `acme.omsflow.lu`, `parcelproxy.hostwizard.lu`) is automatically routed without per-store Caddy changes. + +The wildcard blocks use the same origin cert as the platform root domain. The origin cert must include `*.` — see [Step 21.4](#214-generate-origin-certificates) for how to generate it. ```caddy *.omsflow.lu { + tls /etc/caddy/certs/omsflow.lu/cert.pem /etc/caddy/certs/omsflow.lu/key.pem reverse_proxy localhost:8001 } *.rewardflow.lu { + tls /etc/caddy/certs/rewardflow.lu/cert.pem /etc/caddy/certs/rewardflow.lu/key.pem reverse_proxy localhost:8001 } -*.wizard.lu { +*.hostwizard.lu { + tls /etc/caddy/certs/hostwizard.lu/cert.pem /etc/caddy/certs/hostwizard.lu/key.pem reverse_proxy localhost:8001 } ``` -!!! warning "Wildcard SSL requires DNS challenge" - Let's Encrypt cannot issue wildcard certificates via HTTP challenge. Wildcard certs require a **DNS challenge**, which means installing a Caddy DNS provider plugin (e.g. `caddy-dns/cloudflare`) and configuring API credentials for your DNS provider. See [Caddy DNS challenge docs](https://caddyserver.com/docs/automatic-https#dns-challenge). +!!! warning "No wildcard for wizard.lu" + `wizard.lu` cannot use a wildcard block because `git.wizard.lu` is DNS-only (grey cloud in Cloudflare) and uses a Let's Encrypt cert. A wildcard origin cert would conflict. For wizard.lu subdomains, add each one explicitly (same pattern as `api.wizard.lu`, `flower.wizard.lu`). -#### Custom Store Domains (for premium stores) +**Cloudflare DNS**: Add a wildcard DNS record for each platform: -When premium stores bring their own domains (e.g. `acme.lu`), use Caddy's **on-demand TLS**: +- `*.omsflow.lu` → A `91.99.65.229` (proxied, orange cloud) +- `*.rewardflow.lu` → A `91.99.65.229` (proxied, orange cloud) +- `*.hostwizard.lu` → A `91.99.65.229` (proxied, orange cloud) + +With this in place, adding a new store subdomain only requires a database entry (via admin UI) — no DNS or Caddy changes needed. + +#### Runbook: Add a Store Subdomain + +When a merchant creates a store with subdomain `acme` on the OMS platform: + +1. **Database**: Create the store via admin UI — set `subdomain = "acme"` and link to the platform. The `StoreContextMiddleware` will resolve `acme.omsflow.lu` automatically. +2. **DNS**: Already covered by the wildcard `*.omsflow.lu` record in Cloudflare. +3. **Caddy**: Already covered by the `*.omsflow.lu` block. +4. **SSL**: Already covered by the wildcard origin cert. +5. **Verify**: `curl -I https://acme.omsflow.lu/` + +No infrastructure changes needed — it's fully self-service. + +#### Runbook: Add a Custom Store Domain + +When a premium store brings their own domain (e.g. `wizamart.com`), the domain must be added to **your** Cloudflare account. This ensures you control SSL, WAF, and DNS — critical since you are responsible for the infrastructure. + +**Step 1: Add domain to Cloudflare** + +1. In [Cloudflare Dashboard](https://dash.cloudflare.com), click **Add a site** > enter `wizamart.com` +2. Select the Free plan +3. Cloudflare assigns NS records — give these to the store owner to update at their registrar +4. Wait for NS propagation (can take up to 24h, usually minutes) + +**Step 2: Configure DNS in Cloudflare** + +Add A records (proxied, orange cloud): + +| Type | Name | Content | Proxy | +|------|------|---------|-------| +| A | `wizamart.com` | `91.99.65.229` | Proxied | +| A | `www` | `91.99.65.229` | Proxied | + +**Step 3: Generate origin certificate** + +1. In Cloudflare: **SSL/TLS** > **Origin Server** > **Create Certificate** +2. Hostnames: `wizamart.com, www.wizamart.com` +3. Validity: 15 years +4. Download cert and key (key shown only once!) + +**Step 4: Install cert on server** + +```bash +sudo mkdir -p /etc/caddy/certs/wizamart.com +sudo nano /etc/caddy/certs/wizamart.com/cert.pem # paste certificate +sudo nano /etc/caddy/certs/wizamart.com/key.pem # paste private key +sudo chown -R caddy:caddy /etc/caddy/certs/wizamart.com +sudo chmod 600 /etc/caddy/certs/wizamart.com/key.pem +``` + +**Step 5: Add to Caddyfile** + +```bash +sudo nano /etc/caddy/Caddyfile +``` + +Add: ```caddy -https:// { - tls { - on_demand - } +# ─── Custom store domain: wizamart.com ──────────────────────── +wizamart.com { + tls /etc/caddy/certs/wizamart.com/cert.pem /etc/caddy/certs/wizamart.com/key.pem + reverse_proxy localhost:8001 +} + +www.wizamart.com { + tls /etc/caddy/certs/wizamart.com/cert.pem /etc/caddy/certs/wizamart.com/key.pem + redir https://wizamart.com{uri} permanent +} +``` + +**Step 6: Reload Caddy** + +```bash +sudo systemctl reload caddy +sudo systemctl status caddy +``` + +**Step 7: Configure Cloudflare settings** + +In Cloudflare dashboard for `wizamart.com`: + +| Setting | Location | Value | +|---|---|---| +| SSL mode | SSL/TLS > Overview | Full (Strict) | +| Always Use HTTPS | SSL/TLS > Edge Certificates | On | +| Bot Fight Mode | Security > Settings | On | + +**Step 8: Register domain in database** + +Via admin UI: create a `StoreDomain` record linking `wizamart.com` to the store and platform. + +**Step 9: Verify** + +```bash +curl -I https://wizamart.com/ +``` + +The `StoreContextMiddleware` will detect `wizamart.com` as a custom domain, look it up in the `store_domains` table, and route to the correct store. + +#### Runbook: Add a New Platform Domain + +When adding an entirely new platform (e.g. `newplatform.lu`): + +**Step 1: Cloudflare** + +1. Add `newplatform.lu` as a site in Cloudflare +2. Configure NS at registrar +3. Add DNS records (all proxied): + - `newplatform.lu` → A `91.99.65.229` + - `www.newplatform.lu` → A `91.99.65.229` + - `*.newplatform.lu` → A `91.99.65.229` (for store subdomains) + +**Step 2: Generate origin certificate** + +In Cloudflare: **SSL/TLS** > **Origin Server** > **Create Certificate** + +Hostnames: `newplatform.lu, www.newplatform.lu, *.newplatform.lu` + +**Step 3: Install cert and update Caddyfile** + +```bash +sudo mkdir -p /etc/caddy/certs/newplatform.lu +sudo nano /etc/caddy/certs/newplatform.lu/cert.pem +sudo nano /etc/caddy/certs/newplatform.lu/key.pem +sudo chown -R caddy:caddy /etc/caddy/certs/newplatform.lu +sudo chmod 600 /etc/caddy/certs/newplatform.lu/key.pem +``` + +Add to Caddyfile: + +```caddy +# ─── Platform: NewPlatform (newplatform.lu) ─────────────────── +newplatform.lu { + tls /etc/caddy/certs/newplatform.lu/cert.pem /etc/caddy/certs/newplatform.lu/key.pem + reverse_proxy localhost:8001 +} + +www.newplatform.lu { + tls /etc/caddy/certs/newplatform.lu/cert.pem /etc/caddy/certs/newplatform.lu/key.pem + redir https://newplatform.lu{uri} permanent +} + +*.newplatform.lu { + tls /etc/caddy/certs/newplatform.lu/cert.pem /etc/caddy/certs/newplatform.lu/key.pem reverse_proxy localhost:8001 } ``` -On-demand TLS auto-provisions SSL certificates when a new domain connects. Add an `ask` endpoint to validate that the domain is registered in the `store_domains` table, preventing abuse: - -```caddy -tls { - on_demand - ask http://localhost:8001/api/v1/internal/verify-domain -} +```bash +sudo systemctl reload caddy ``` -!!! note "Not needed yet" - Wildcard subdomains and custom domains are future work. The current Caddyfile handles all platform root domains and service subdomains. +**Step 4: Cloudflare settings** + +Same as other platforms — see [Step 21.6](#216-cloudflare-settings-per-domain). + +**Step 5: Database** + +Create the platform record via `init_production.py` or admin UI with `domain = "newplatform.lu"`. + +**Step 6: Verify** + +```bash +curl -I https://newplatform.lu/ +curl -I https://teststore.newplatform.lu/ +``` ## Step 15: Gitea Actions Runner @@ -2007,15 +2162,15 @@ Disable DNSSEC at the registrar before switching NS — re-enable later via Clou Cloudflare Origin Certificates (free, 15-year validity) avoid ACME challenge issues when traffic is proxied: 1. In Cloudflare: **SSL/TLS** > **Origin Server** > **Create Certificate** -2. Generate for each domain with **specific subdomains** (not wildcards): - - `wizard.lu`: `wizard.lu, api.wizard.lu, www.wizard.lu, flower.wizard.lu, grafana.wizard.lu` - - `omsflow.lu`: `omsflow.lu, www.omsflow.lu` - - `rewardflow.lu`: `rewardflow.lu, www.rewardflow.lu` - - `hostwizard.lu`: `hostwizard.lu, www.hostwizard.lu` +2. Generate for each domain: + - `wizard.lu`: `wizard.lu, api.wizard.lu, www.wizard.lu, flower.wizard.lu, grafana.wizard.lu` (**specific subdomains, no wildcard**) + - `omsflow.lu`: `omsflow.lu, www.omsflow.lu, *.omsflow.lu` (wildcard for store subdomains) + - `rewardflow.lu`: `rewardflow.lu, www.rewardflow.lu, *.rewardflow.lu` (wildcard for store subdomains) + - `hostwizard.lu`: `hostwizard.lu, www.hostwizard.lu, *.hostwizard.lu` (wildcard for store subdomains) 3. Download the certificate and private key (private key is shown only once) !!! warning "Do NOT use wildcard origin certs for wizard.lu" - A `*.wizard.lu` wildcard cert will match `git.wizard.lu`, which needs a Let's Encrypt cert (DNS-only, not proxied through Cloudflare). Use specific subdomains instead. + A `*.wizard.lu` wildcard cert will match `git.wizard.lu`, which is DNS-only (grey cloud) and uses a Let's Encrypt cert. A wildcard origin cert would conflict. Use specific subdomains instead. For new wizard.lu subdomains, add them explicitly to this cert and to the Caddyfile. Install on the server: @@ -2090,6 +2245,22 @@ www.hostwizard.lu { redir https://hostwizard.lu{uri} permanent } +# ─── Store subdomains (wildcard — all except wizard.lu) ────── +*.omsflow.lu { + tls /etc/caddy/certs/omsflow.lu/cert.pem /etc/caddy/certs/omsflow.lu/key.pem + reverse_proxy localhost:8001 +} + +*.rewardflow.lu { + tls /etc/caddy/certs/rewardflow.lu/cert.pem /etc/caddy/certs/rewardflow.lu/key.pem + reverse_proxy localhost:8001 +} + +*.hostwizard.lu { + tls /etc/caddy/certs/hostwizard.lu/cert.pem /etc/caddy/certs/hostwizard.lu/key.pem + reverse_proxy localhost:8001 +} + # ─── Services (wizard.lu origin cert) ─────────────────────── api.wizard.lu { tls /etc/caddy/certs/wizard.lu/cert.pem /etc/caddy/certs/wizard.lu/key.pem