Make docker-compose.yml enforce the api/enterprise-api binary swap
Closes the local/dev half of a gap named repeatedly across this phase's docs: Helm already made api/enterprise-api mutually exclusive (same enterprise.enabled flag that turns on RBAC/audit/SSO, both rendering to the same Service name/port); docker-compose.yml let both run side by side, with nothing actually pointing at enterprise-api by default. Mechanism: both services now carry a `profiles` entry (single-tenant / enterprise), selected via COMPOSE_PROFILES -- a new checked-in .env sets single-tenant as the zero-config default (unchanged behavior for anyone who doesn't touch it), and `COMPOSE_PROFILES=enterprise docker compose up` swaps to enterprise-api instead. Docker Compose profiles are purely additive (no "profile X excludes service Y" primitive), so true exclusivity comes from both being profile-gated with no shared default profile, not from one excluding the other directly. Mirrors Helm's same-Service-name trick so alerting's API_QUERY_URL and web's VITE_API_BASE_URL need zero conditional logic either way: enterprise-api now maps host port 8080 (was 8083, its own binary default -- overridden via HTTP_LISTEN_ADDR) and carries a `networks.default.aliases: [api]` entry, so whichever binary is actually running answers on the same compose-network hostname and host port. alerting's and web's depends_on for api/enterprise-api are now `required: false` (Compose's supported "optional dependency" shape) -- without it, compose errors on the inactive one rather than just skipping it, since depends_on doesn't otherwise know about profiles. Verified for real in this environment via `docker compose config` (renders and validates the merged YAML without needing a daemon): confirmed api/enterprise-api never both appear in --services output for either profile selection, confirmed enterprise-api's rendered block has port 8080/alias "api"/HTTP_LISTEN_ADDR ":8080" when the enterprise profile is active, and confirmed `docker compose run enterprise-api ...`/`docker compose build enterprise-api` (used by enterprise/README.md's and phase-4-runbook.md's provisioning steps) still work regardless of the active profile -- explicit service references bypass profile filtering, confirmed by the commands reaching a daemon-connection permission error rather than a profile-resolution error. Not verified: an actual `docker compose up` against a real daemon, still unavailable in this environment. Docs updated in lockstep -- CLAUDE.md, threat-model.md (including its summary table), phase-4-runbook.md (new §10a, §8's provisioning commands updated for the new port/profile), enterprise/README.md.
This commit is contained in:
+60
-14
@@ -329,17 +329,25 @@ close the ClickHouse half of the headline gap §"Known gaps" below used
|
||||
to describe as completely unbuilt. It's still a second binary you have
|
||||
to choose to run, though — see `/docs/security/threat-model.md`'s "Read
|
||||
this first" section. With OIDC login now built (§3a), a real
|
||||
`curl -X POST http://localhost:8083/query` walkthrough as a logged-in
|
||||
`curl -X POST http://localhost:8080/query` walkthrough as a logged-in
|
||||
tenant is *possible* now, but still needs the manual `tenant_memberships`
|
||||
bootstrap from §3a — a full end-to-end curl walkthrough isn't included
|
||||
here yet.
|
||||
|
||||
`api`/`enterprise-api` are now mutually exclusive via `COMPOSE_PROFILES`
|
||||
(checked in as `single-tenant` in `.env`, i.e. plain `api` runs by
|
||||
default) — see §10a below for why, and confirmation that it actually
|
||||
holds. `-provision-tenant` itself doesn't bind a port, so it runs fine
|
||||
regardless of the active profile; actually serving traffic on
|
||||
`enterprise-api` needs the `enterprise` profile active, since it now
|
||||
binds the same host port (8080) plain `api` does:
|
||||
|
||||
```sh
|
||||
docker compose build enterprise-api
|
||||
docker compose run --rm enterprise-api -provision-tenant=acme -display-name="Acme Corp"
|
||||
docker compose run --rm enterprise-api -provision-tenant=globex -display-name="Globex Corporation"
|
||||
docker compose up -d enterprise-api
|
||||
curl -s http://localhost:8083/healthz
|
||||
COMPOSE_PROFILES=enterprise docker compose build enterprise-api
|
||||
COMPOSE_PROFILES=enterprise docker compose run --rm enterprise-api -provision-tenant=acme -display-name="Acme Corp"
|
||||
COMPOSE_PROFILES=enterprise docker compose run --rm enterprise-api -provision-tenant=globex -display-name="Globex Corporation"
|
||||
COMPOSE_PROFILES=enterprise docker compose up -d enterprise-api
|
||||
curl -s http://localhost:8080/healthz
|
||||
```
|
||||
|
||||
Confirm isolation end to end against the live stack (this is the same
|
||||
@@ -436,18 +444,56 @@ each set of values and confirm `kubectl get deploy sentry-api -o
|
||||
jsonpath='{.spec.template.spec.containers[0].image}'` matches, and that
|
||||
`kubectl get svc sentry-api` routes to whichever one is actually running.
|
||||
|
||||
## 10a. Confirm `docker-compose.yml` now enforces the same binary swap
|
||||
|
||||
Local/dev parity with §10 above was a named gap ("`docker-compose.yml`
|
||||
still runs plain `api` unconditionally") -- closed via `COMPOSE_PROFILES`
|
||||
(`api`/`enterprise-api` are each gated behind a profile, `.env` checks in
|
||||
`single-tenant` as the zero-config default) plus the same "same host
|
||||
port, `enterprise-api` gets a `default.aliases: [api]` network alias"
|
||||
trick §10's Helm chart uses at the Service-name level. Verified in this
|
||||
environment via `docker compose config` (no daemon needed -- it renders
|
||||
and validates the merged YAML without starting anything):
|
||||
|
||||
```sh
|
||||
docker compose config --quiet && echo "config is valid"
|
||||
|
||||
# exactly one of api/enterprise-api per profile, never both or neither:
|
||||
docker compose config --services
|
||||
# expect: ... api ... (no enterprise-api)
|
||||
COMPOSE_PROFILES=enterprise docker compose config --services
|
||||
# expect: ... enterprise-api ... (no api)
|
||||
|
||||
# enterprise-api really does take over api's name/port when active:
|
||||
COMPOSE_PROFILES=enterprise docker compose config \
|
||||
| python3 -c "import yaml,sys,json; d=yaml.safe_load(sys.stdin)['services']['enterprise-api']; print(json.dumps({'ports': d['ports'], 'aliases': d['networks']['default']['aliases'], 'HTTP_LISTEN_ADDR': d['environment']['HTTP_LISTEN_ADDR']}, indent=2))"
|
||||
# expect port 8080 (not enterprise-api's own default 8083), alias
|
||||
# ["api"], and HTTP_LISTEN_ADDR ":8080"
|
||||
```
|
||||
|
||||
`docker compose run`/`build enterprise-api` (§8's provisioning steps)
|
||||
work regardless of the active profile -- explicit service references on
|
||||
the command line bypass profile filtering, confirmed in this
|
||||
environment (the commands got past client-side profile resolution and
|
||||
failed only on `permission denied ... docker.sock`, this environment's
|
||||
already-disclosed no-Docker-daemon-access limitation, not a
|
||||
profile-related error). **Not verified**: an actual `docker compose up`
|
||||
against a real daemon in this environment — the `config` rendering above
|
||||
proves the compose file's *shape* is correct, not that containers
|
||||
actually start and route traffic correctly end to end.
|
||||
|
||||
## Known gaps (do not treat this phase as done without reading these)
|
||||
|
||||
Full accounting: `/docs/security/threat-model.md`. Headline items:
|
||||
|
||||
- **Both storage engines' isolation exists, and the Helm chart now
|
||||
enforces which binary runs.** `deploy/helm/sentry/templates/api.yaml`/
|
||||
`enterprise-api.yaml` are mutually exclusive on `enterprise.enabled`
|
||||
(§10) -- a Helm-deployed cluster can't accidentally run the
|
||||
non-isolated binary once that flag is set. `docker-compose.yml` still
|
||||
runs plain `api` unconditionally alongside a separately-started
|
||||
`enterprise-api` (§8), so this enforcement doesn't extend to local/dev
|
||||
yet.
|
||||
- **Both storage engines' isolation exists, and both Helm and
|
||||
docker-compose now enforce which binary runs.**
|
||||
`deploy/helm/sentry/templates/api.yaml`/`enterprise-api.yaml` are
|
||||
mutually exclusive on `enterprise.enabled` (§10) -- a Helm-deployed
|
||||
cluster can't accidentally run the non-isolated binary once that flag
|
||||
is set. `docker-compose.yml`'s `api`/`enterprise-api` services are now
|
||||
the same mutually-exclusive choice via `COMPOSE_PROFILES` (§8, §10a),
|
||||
closing the local/dev parity gap this bullet used to name.
|
||||
- The `Tenant` CRD (`deploy/operator`) and `enterprise-api
|
||||
-provision-tenant` are still two independent provisioning mechanisms
|
||||
-- running both for the same tenant ID today takes two separate
|
||||
|
||||
@@ -44,20 +44,26 @@ searchclient`'s tests run a real in-process gRPC server and confirm the
|
||||
wire-level `SearchRequest` carries the right `tenant_id`. All pass, for
|
||||
real, no disclaimer needed for this specific claim.
|
||||
|
||||
**The Helm chart now closes this for K8s deployments; `docker-compose.yml`
|
||||
still doesn't.** `deploy/helm/sentry/templates/api.yaml` and
|
||||
`enterprise-api.yaml` are mutually exclusive, gated on opposite sides of
|
||||
the same `enterprise.enabled` flag, rendering to the same Service
|
||||
name/port — so a Helm-deployed cluster runs exactly one of the two
|
||||
binaries, chosen by the same flag that turns on RBAC/audit/SSO, not a
|
||||
second independently-forgettable decision. Verified by parsing (not
|
||||
**Both Helm and docker-compose now close this.**
|
||||
`deploy/helm/sentry/templates/api.yaml` and `enterprise-api.yaml` are
|
||||
mutually exclusive, gated on opposite sides of the same
|
||||
`enterprise.enabled` flag, rendering to the same Service name/port — so
|
||||
a Helm-deployed cluster runs exactly one of the two binaries, chosen by
|
||||
the same flag that turns on RBAC/audit/SSO, not a second
|
||||
independently-forgettable decision. Verified by parsing (not
|
||||
eyeballing) the rendered YAML under both values: exactly one `sentry-api`
|
||||
Deployment either way, with the right image. **`docker-compose.yml`
|
||||
still runs plain `api` unconditionally** and includes `enterprise-api`
|
||||
as an extra, separately-started service — local/dev parity with the Helm
|
||||
chart's enforcement is real remaining work. And this only constrains
|
||||
*deployment*, not *operation*: nothing stops an operator from manually
|
||||
running plain `api`'s image against a cluster that has tenants
|
||||
Deployment either way, with the right image. `docker-compose.yml`'s
|
||||
`api`/`enterprise-api` services are now the analogous mutually-exclusive
|
||||
choice, gated behind `COMPOSE_PROFILES` (`.env` checks in
|
||||
`single-tenant`, i.e. plain `api`, as the zero-config default) and
|
||||
sharing the same host port/network-alias trick to stay transparent to
|
||||
`alerting`/`web` either way — verified via `docker compose config`
|
||||
(renders and validates the merged YAML without a daemon; confirms
|
||||
`api`/`enterprise-api` never both appear in `--services` output for the
|
||||
same profile selection) — see `/docs/phase-4-runbook.md` §10a. This
|
||||
still only constrains *deployment*, not *operation*: nothing stops an
|
||||
operator from manually running plain `api`'s image against a cluster
|
||||
(or compose project) that has tenants
|
||||
provisioned, pointing at the same ClickHouse/Postgres. The Helm chart
|
||||
makes the *default*, chart-managed path correct; it isn't a runtime
|
||||
guard against misconfiguration.
|
||||
@@ -404,7 +410,7 @@ terms:
|
||||
| Tantivy tenant_id resolution (`enterprise/internal/searchclient`) | **Enforced, verified live** — real gRPC wire-level test |
|
||||
| Ingest tenant-awareness (ClickHouse and Tantivy both) | **Not implemented, undesigned** — every ingested record lands in the single shared database/index regardless of tenant |
|
||||
| Deployment actually routing traffic to `enterprise-api` (Helm) | **Enforced** — `api`/`enterprise-api` are mutually exclusive, same flag as RBAC/audit/SSO |
|
||||
| Deployment actually routing traffic to `enterprise-api` (docker-compose) | **Not implemented** — `docker-compose.yml` runs plain `api` unconditionally |
|
||||
| Deployment actually routing traffic to `enterprise-api` (docker-compose) | **Enforced** — `api`/`enterprise-api` are mutually exclusive via `COMPOSE_PROFILES`, same flag choice as Helm's `enterprise.enabled`; verified via `docker compose config`, not an actual `docker compose up` in this environment |
|
||||
| Human SSO login — OIDC | **Built, verified with a real fake IdP** (not yet tried against a real external IdP) |
|
||||
| Human SSO login — SAML | **Built, verified with a real fake IdP** (not yet tried against a real external IdP) |
|
||||
| Multi-tenant-membership login (tenant picker) | **Not implemented** — refused with a clear error, not guessed |
|
||||
|
||||
Reference in New Issue
Block a user