Saltar al contenido principal

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_internal planning 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 in RequestContext.
  • 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_key per 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_mandatory governance tier in authz_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 policy 02-require-runtime-class enforces runtimeClassName: gvisor for 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-reg cells carry a governance cost by construction: every Terraform apply against them is hard_mandatory (fail-closed OPA, distinct approver, step-up), because the authz_matrix.yaml cell dimension resolves the regulated tier. 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-pii workload in a shared-std cell, or a silo-reg cell 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 CellTier enum, the authz_matrix.yaml cell 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.