ADR 027 — Cell isolation tiers
- Status: Accepted (2026-07-16) — the logical ladder (shared-std / shared-prem / silo-reg) is adopted and shipped in Phase 2.5 (cell abstraction hardening); the physical escalation options (vCluster / AWS-account-per-cell / gVisor RuntimeClass) are recorded here as a warranted-but-deferred, hardware- and account-vending-gated escalation path
- Authors: Platform team
- Related: ADR 005,
ADR 015,
ADR 022,
ADR 024,
ADR 026,
Unified Infrastructure Control Plane Plan (private
alphaswarm_internalplanning repo)
Context
ADR 015 settled the runtime shape as a
cell-based modular monolith with per-cell data planes rather than domain
microservices. A cell is the unit of tenant placement and blast-radius
containment: alphaswarm_core.topology.Cell carries a tier, a
tenancy_strategy, a region/AZ, and a k8s_namespace, and the registry in
configs/deployment/topology.yaml maps every tenant onto exactly one cell.
What ADR 015 did not pin down is how strongly a cell isolates its tenants from one another and from the rest of the estate. That question has a spectrum of answers with a steep cost/assurance gradient, and different tenants sit at different points on it:
- A high-density SaaS tenant wants the cheapest safe option (shared infrastructure, logical separation).
- A premium tenant wants a blast-radius boundary at the data layer without paying for dedicated compute.
- A regulated tenant (FINRA / MiFID II / ISO 27001 / SOC 2 customer-PII) needs demonstrable, auditable separation — dedicated data, customer-held key material, and a default-deny network posture — and may contractually require a physical boundary (separate cluster control plane, separate cloud account, or a kernel-level syscall sandbox).
Phase 2.5 (cell abstraction hardening) is the point where the cell manifests
and the cell Terraform module both grew a per-cell default-deny
NetworkPolicy + explicit allow set, and where authz_matrix.yaml grew an
optional cell dimension (silo-reg cells resolve to hard_mandatory). Those
mechanisms only make sense against a written definition of the tiers they
implement. Without one, "silo-reg" is a label; operators cannot tell whether a
new tenant belongs in a shared namespace or a dedicated account, and reviewers
cannot tell whether a cell is configured to the assurance level its contract
demands. This ADR is that definition.
Decision
We adopt a three-tier logical isolation ladder as the default cell taxonomy, with a physical escalation path available for regulated tenants whose contracts require a boundary stronger than namespace + policy.
Tier 1 — shared-std (namespace + RLS + NetworkPolicy)
- Tenancy strategy:
shared_schema_rls. All tenants share one namespace, one Postgres database, and one schema; rows are separated by PostgreSQL Row-Level Security keyed on the tenant id inRequestContext. - Isolation controls: per-cell namespace; PSS
restricted; the Phase 2.5 per-cell default-deny NetworkPolicy with a minimal allow set (DNS, intra-cell, control-plane / Postgres / OTel egress, ingress-controller → cell); Linkerd mTLS between pods. - When warranted: the default for standard SaaS tenants. Highest density
(thousands of tenants per cell), lowest cost, and the weakest isolation on
the ladder — a bug that defeats RLS is a cross-tenant exposure, so this tier
is for
proprietary-alpha/internal data classifications, never customer PII.
Tier 2 — shared-prem (namespace + schema-per-tenant)
- Tenancy strategy:
schema_per_tenant. Tenants still share a namespace and a database server, but each tenant gets its own Postgres schema (and, where relevant, its own cache keyspace / object-store prefix). - Isolation controls: everything in Tier 1, plus a data-layer boundary that does not depend on every query carrying a correct RLS predicate. A schema-scoped role cannot address another tenant's tables at all.
- When warranted: premium tenants who want a blast-radius boundary at the data layer and noisy-neighbour insulation, but do not need dedicated compute or a physical boundary. Medium density; moderate cost.
Tier 3 — silo-reg (dedicated cell, BYOK/Transit, default-deny)
- Tenancy strategy:
database_per_enterprise, pinned to a single tenant (alphaswarm.io/pinned-tenant). The cell is dedicated: its own namespace, its own per-cell data plane (Postgres / Redis / MinIO / MLflow), and its own Vault Transit key (vault_transit_keyper cell) so the tenant's data is encrypted under customer-held key material (BYOK). - Isolation controls: everything above, plus the default-deny NetworkPolicy
is load-bearing rather than defence-in-depth (no shared datastore to reach);
the cell resolves to the
regulated/hard_mandatorygovernance tier inauthz_matrix.yaml(OPA fail-closed, distinct approver, step-up MFA on every apply);data_classification: customer-pii; FINRA / ISO 27001 posture. - When warranted: regulated or enterprise tenants with contractual data residency, key custody, or auditability requirements. Lowest density (one tenant per cell), highest logical-tier cost.
Escalation path — physical isolation (regulated tenants only)
silo-reg is the strongest logical tier, but it still shares a Kubernetes
control plane, a kernel, and (usually) a cloud account with other cells. For
tenants whose threat model or contract requires a boundary that survives a
control-plane, node-kernel, or cloud-account compromise, cells may escalate to
one or more physical options. These are recorded here as the sanctioned
ladder and are deferred / gated, not switched on by default (consistent with
ADR 026's "multi-account cell isolation" deferral):
- gVisor
RuntimeClass— a user-space kernel that intercepts syscalls, so agent-generated / untrusted code cannot reach the host kernel directly. Cheap relative to the others and already anticipated: the Kyverno policy02-require-runtime-classenforcesruntimeClassName: gvisorfor the agent sandbox pool. Warranted when the isolation concern is code execution (agent sandboxes, BYO strategy code) rather than tenant-data separation. - vCluster (virtual control plane per cell) — each cell gets its own virtual Kubernetes API server and controller set on shared nodes, so a compromised or misbehaving tenant cannot see or mutate cluster-wide objects. Warranted when a tenant needs control-plane isolation (their own CRDs, RBAC, admission) without the cost of a dedicated cluster. Gated on operator capacity to run the vCluster fleet.
- AWS-account-per-cell — the hardest boundary: a dedicated member account
vended through Control Tower /
infrastructure/envs/org-accounts, giving IAM, network, KMS, and billing separation with no shared blast radius. Warranted for the highest-assurance regulated tenants and for hard data-residency or billing-separation requirements. Gated on the account-vending saga and the single-node → multi-account production posture (ADR 024), hence deferred.
Tier selection is recorded as data, enforced as policy: the cell's tier
and tenancy_strategy live in the topology registry and are stamped as
alphaswarm.io/cell-tier / alphaswarm.io/tenancy-strategy on the namespace and
every workload; the governance consequence is resolved from the
authz_matrix.yaml cell dimension (ADR 026 §4). Escalation to a physical tier is
a deliberate, reviewed change to the cell's provisioning stack — never an
implicit default.
Consequences
- The tiers form a monotonic ladder: each step up strictly adds isolation controls and strictly reduces density/raises cost. A tenant can be promoted (shared-std → shared-prem → silo-reg → physical) without redefining the abstraction; only the cell it is pinned to changes.
- The Phase 2.5 per-cell default-deny NetworkPolicy is now the baseline for
every tier — including shared-std — so "which tier" changes the data-plane
and key-custody story, not whether the network is default-deny. Both the
IaC-provisioned cells (cell Terraform module) and the manifest-provisioned
cells (the
deployments/kubernetes/cells/overlays) ship the same default-deny + allow set. silo-regcells carry a governance cost by construction: every Terraform apply against them ishard_mandatory(fail-closed OPA, distinct approver, step-up), because theauthz_matrix.yamlcell dimension resolves theregulatedtier. This is intentional friction proportional to the data classification.- The physical tiers are explicit, warranted, and deferred: operators have a named escalation path to point regulated prospects at, without the platform paying vCluster/multi-account operational cost before a tenant needs it.
- Misclassification is now a reviewable error, not silent: a
customer-piiworkload in ashared-stdcell, or asilo-regcell without BYOK/Transit or default-deny, is visible in the topology + policy data and can be gated in CI.
Alternatives considered
- A single "isolation = namespace" model for all tenants (only Tier 1): rejected — it cannot satisfy regulated data-residency, key-custody, or auditability requirements, and forces customer PII to rely on RLS being perfect on every query path.
- A single "isolation = dedicated cluster/account" model for all tenants (only the physical tier): rejected — the cost and operational surface of an account or vCluster per tenant is unjustifiable for high-density standard SaaS, and would make the common case as expensive as the rarest one.
- A continuous knob per isolation control (free-form mix of RLS / schema /
netpol / runtimeclass / account per cell): rejected — an unbounded matrix is
unreviewable and untestable. A small, named, monotonic ladder maps cleanly to
the
CellTierenum, theauthz_matrix.yamlcell dimension, and a reviewer's mental model of "how isolated is this tenant". - Encoding the tier only in documentation, not in data: rejected — a label with no enforcement rots. The decision is bound to the topology registry, the cell labels, the per-cell NetworkPolicy, and the authz matrix so the configuration is the assurance statement.