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.
125 lines
4.8 KiB
Markdown
125 lines
4.8 KiB
Markdown
# 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.
|