fix(deploy): provision tenant API domains
Some checks failed
Architecture Governance / architecture (push) Failing after 6m16s
Some checks failed
Architecture Governance / architecture (push) Failing after 6m16s
Reconcile TLS, exact CORS, and backend proxying before release activation so every storefront uses its derived API host.
This commit is contained in:
124
docs/backend/TENANT-API-DOMAIN-HANDOFF.md
Normal file
124
docs/backend/TENANT-API-DOMAIN-HANDOFF.md
Normal file
@@ -0,0 +1,124 @@
|
||||
# Final handoff: tenant API domains
|
||||
|
||||
This is the required production contract between the shared frontend, nginx,
|
||||
and the backend. It supersedes any fixed `api.dexarmarket.ru` or same-origin
|
||||
`/backend` routing proposal.
|
||||
|
||||
## 1. Deterministic hostname rule
|
||||
|
||||
The frontend prefixes the **complete** storefront hostname with `api.`:
|
||||
|
||||
| Storefront | API origin | Bootstrap |
|
||||
|---|---|---|
|
||||
| `example.com` | `https://api.example.com` | `https://api.example.com/bootstrap` |
|
||||
| `store1.example.com` | `https://api.store1.example.com` | `https://api.store1.example.com/bootstrap` |
|
||||
| `www.example.com` | `https://api.www.example.com` | `https://api.www.example.com/bootstrap` |
|
||||
|
||||
Bootstrap, auth, legacy routes, and `/api/...` routes all use this origin.
|
||||
Localhost is the only exception and continues through the local `/api` proxy.
|
||||
|
||||
## 2. Backend changes required
|
||||
|
||||
For every request received publicly on `api.<storefront-host>`:
|
||||
|
||||
1. Behind the trusted project nginx, use `X-Storefront-Host`. nginx deliberately
|
||||
sends the same storefront value as upstream `Host` for compatibility with the
|
||||
currently live backend and preserves the public API hostname in
|
||||
`X-Forwarded-Host`.
|
||||
2. Without that trusted proxy, normalize the request `Host`: lowercase, remove
|
||||
the port, remove exactly one leading `api.` label when present, and retain
|
||||
every remaining label.
|
||||
3. Resolve that normalized storefront hostname through the tenant-domain
|
||||
registry. Do not infer a tenant from only the first label.
|
||||
4. Reject unknown, disabled, or unverified domains with `403` before reading
|
||||
tenant data. Never fall back to the default/Dexar tenant.
|
||||
5. Bind the authenticated session to the resolved tenant and reject a mismatch.
|
||||
6. Trust `X-Storefront-Host` / `X-Forwarded-*` only from the known nginx proxy;
|
||||
direct clients can forge them.
|
||||
7. Return JSON for `/bootstrap`, including a tenant identity that matches the
|
||||
normalized storefront domain. HTML or a default tenant response is a fault.
|
||||
|
||||
Pseudo-code:
|
||||
|
||||
```text
|
||||
if request.remoteAddress is trustedProxy:
|
||||
storefrontHost = lower(stripPort(request.header["X-Storefront-Host"]))
|
||||
else:
|
||||
requestHost = lower(stripPort(request.host))
|
||||
storefrontHost = removeAtMostOnePrefix(requestHost, "api.")
|
||||
tenant = registry.findVerifiedDomain(storefrontHost) ?? forbidden()
|
||||
request.tenant = tenant
|
||||
```
|
||||
|
||||
## 3. CORS contract
|
||||
|
||||
For API host `api.<storefront-host>`, allow exactly:
|
||||
|
||||
```http
|
||||
Access-Control-Allow-Origin: https://<storefront-host>
|
||||
Access-Control-Allow-Credentials: true
|
||||
Vary: Origin
|
||||
Access-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE, OPTIONS
|
||||
Access-Control-Allow-Headers: Authorization, Content-Type, AdminWebSessionID, X-Requested-With
|
||||
```
|
||||
|
||||
Answer valid preflight requests with `204`. Do not use `*` together with
|
||||
credentials. nginx applies this policy now; the backend should enforce the same
|
||||
allowlist when it is reached without that proxy.
|
||||
|
||||
## 4. nginx and TLS
|
||||
|
||||
Run the idempotent project script as root:
|
||||
|
||||
```bash
|
||||
scripts/deploy/configure-api-domain.sh \
|
||||
--domain gorbushka.market \
|
||||
--email ops@example.com \
|
||||
--upstream https://127.0.0.1:445
|
||||
```
|
||||
|
||||
It creates `api.gorbushka.market`, issues/renews its certificate, configures
|
||||
CORS, and proxies all paths to the backend. Upstream receives
|
||||
`Host: gorbushka.market`, `X-Forwarded-Host: api.gorbushka.market`, and
|
||||
`X-Storefront-Host: gorbushka.market`; the script then reloads nginx and verifies
|
||||
that `/bootstrap` returns a JSON object.
|
||||
|
||||
For `store1.example.com`, both DNS and TLS must exist for
|
||||
`api.store1.example.com`. A certificate for `*.example.com` does **not** cover
|
||||
that two-label-deep hostname.
|
||||
|
||||
## 5. CI/CD contract
|
||||
|
||||
`deploy.yml` runs the same root-owned configurator before activating a frontend
|
||||
release. Required production secrets:
|
||||
|
||||
| Secret | Example |
|
||||
|---|---|
|
||||
| `DEPLOY_HOST` | server hostname/IP |
|
||||
| `DEPLOY_USER` | `deploy` |
|
||||
| `DEPLOY_SSH_KEY` | private deploy key |
|
||||
| `DEPLOY_KNOWN_HOSTS` | pinned SSH host-key line |
|
||||
| `STOREFRONT_DOMAINS` | `gorbushka.market store1.example.com` |
|
||||
| `CERTBOT_EMAIL` | operations email |
|
||||
| `BACKEND_UPSTREAM` | `https://127.0.0.1:445` (optional default) |
|
||||
|
||||
One-time provisioning must first run `server-setup.sh`; it installs the helper
|
||||
as root-owned `/usr/local/sbin/marketplaces-configure-api-domain` and grants the
|
||||
deploy user permission to run only that validated command plus nginx reload.
|
||||
|
||||
## 6. Acceptance checks
|
||||
|
||||
For every storefront domain, all of these must pass:
|
||||
|
||||
```bash
|
||||
curl -fsS https://api.example.com/bootstrap | jq -e 'type == "object"'
|
||||
curl -i -X OPTIONS https://api.example.com/bootstrap \
|
||||
-H 'Origin: https://example.com' \
|
||||
-H 'Access-Control-Request-Method: GET'
|
||||
```
|
||||
|
||||
- Frontend bundle contains no fixed marketplace API hostname.
|
||||
- Root and nested storefronts call their matching `api.<full-hostname>`.
|
||||
- Unknown API hosts return `403`, not the default tenant.
|
||||
- `/bootstrap` returns JSON and the correct tenant.
|
||||
- API responses never return the Angular `index.html` fallback.
|
||||
Reference in New Issue
Block a user