Tenant Isolation
A 9-Layer Architecture for Shared Infrastructure
Abstract
Tenant isolation in a shared infrastructure cannot rest on a single layer. We document the nine-layer isolation architecture deployed on the 0DATA infrastructure: PostgreSQL Row-Level Security, SPINA per tenant, NOVA per tenant, Cockpit per domain, ALFA per level, offline PWA, port map, namespaced NATS subjects, and wildcard DNS. Each layer operates independently; the failure of one does not compromise the others. This architecture is inspired by the tight junctions of the biological epithelium, where each cell maintains its own membrane while participating in the common tissue. We describe each layer in detail, present the documented port map (no service listens on 0.0.0.0), and report the results of the cross-tenant penetration test — every attempt at cross-tenant read failed.
In one sentence
Nine independent isolation layers guarantee that a tenant can neither see, nor reach, nor alter another tenant's data — at any level of the stack.
1. Why Nine Layers
1.1 Defense in depth is not a metaphor
In classical cybersecurity, defense in depth is a theoretical principle. In a shared infrastructure hosting MSP clients with conflicting interests, it becomes a vital necessity. A managed service provider may simultaneously host a law firm, a clinic, and a direct competitor of the former — on the same physical server.
The traditional multi-tenant approach relies on application-level separation: the application filters queries according to a tenant_id. This approach fails spectacularly when a bug, an injection flaw, or a privilege escalation bypasses that single layer.
1.2 The biological model: cell membrane and tight junctions
The human intestinal epithelium illustrates our paradigm. Each cell has its own phospholipid membrane — that is the first barrier. Between cells, tight junctions form a second barrier, impermeable even to small molecules. A third barrier — the glycocalyx — filters macromolecules. None of these barriers is sufficient on its own; their superposition creates a seal close to the absolute.
Tight junctions → PostgreSQL RLS prevents any leak at the database level
Glycocalyx → The port map, the wildcard DNS, and namespaced NATS form a network filtration layer
1.3 The nine layers
| Layer | Name | Domain | Mechanism |
|---|---|---|---|
| 1 | PostgreSQL RLS | Database | Row-Level Security policies |
| 2 | PostgreSQL schema | Database | Logical schema per tenant |
| 3 | Application policies | Database | CHECK constraints, triggers |
| 4 | SPINA v2 | Signing | Local HMAC-SHA256 + eIDAS RFC 3161 |
| 5 | NOVA | Observation | Instance per tenant, bind 127.0.0.1 |
| 6 | ALFA | Decision-making | Shared engine, isolated memory |
| 7 | Cockpit | Interface | Dedicated domain, per-tenant JWT |
| 8 | DNS / NATS | Communication | Wildcard DNS, namespaced subjects |
| 9 | Port map | Network | No service on 0.0.0.0, documented ports |
2. Layers 1–3: PostgreSQL, the Foundation
2.1 Row-Level Security (Layer 1)
PostgreSQL Row-Level Security is the deepest mechanism of isolation. Every row of every table carries a tenant_id. An RLS policy applied to each table guarantees that the PostgreSQL session connected for tenant A physically cannot read tenant B's rows.
The current_setting function reads a PostgreSQL session variable. This variable is set by the connection pool at tenant authentication time. No application query can modify it — it is read-only from the application side. This layer is inviolable without PostgreSQL superuser access.
2.2 Schema per tenant (Layer 2)
Beyond RLS, each tenant has a dedicated logical schema — not a PostgreSQL schema in the CREATE SCHEMA sense, but a restricted data projection. Materialized views, partial indexes, and sequences are filtered by tenant_id. A tenant cannot enumerate the other tenants, because the tenants table itself is protected by RLS.
This layer prevents inference attacks: even if a tenant managed to measure the execution time of a query (timing attack), the partial indexes guarantee that other tenants' data is never scanned.
2.3 Constraints and triggers (Layer 3)
The third PostgreSQL layer uses CHECK constraints and BEFORE INSERT/UPDATE triggers to prevent writing data that would violate isolation:
An additional trigger locks any attempt to modify the tenant_id column after insertion. This redundancy is deliberate: if the RLS policy is disabled by mistake (for example during maintenance), the constraints and triggers maintain isolation.
3. Layers 4–6: SPINA, NOVA, ALFA
3.1 SPINA v2 — Per-tenant signing (Layer 4)
SPINA (Signature Protocol for Integrity and Non-repudiation of Assets) is 0DATA's signing protocol. In version 2, each tenant has:
- A local HMAC-SHA256 key, generated and stored in the tenant's space
- An eIDAS RFC 3161 certificate for qualified timestamping
- An isolated keystore, encrypted with a passphrase derived from the tenant's secret
Two tenants never share cryptographic material. If tenant A compromises its HMAC key, tenant B's signatures remain valid. The eIDAS timestamping is federated at the infrastructure level, but the timestamp token is stored in the tenant's schema.
Signature verification is executed in the tenant's PostgreSQL context. The spina_verify() function uses SECURITY DEFINER with an explicit SET app.current_tenant_id — it cannot leak outside the tenant.
3.2 NOVA — Instance per tenant (Layer 5)
NOVA is the network observation engine. Each MSP tenant has its own NOVA instance, run as a dedicated process:
- Exclusive bind on
127.0.0.1, distinct port for each tenant - Local scan database (SQLite or isolated PostgreSQL schema)
- No memory sharing between instances
- Distinct configuration file (
/opt/nova/tenants/<tenant_id>/config.yaml)
A tenant who controls its local network (and therefore the targets scanned by NOVA) cannot influence another tenant's NOVA instance. The processes are isolated at the operating-system level — kill -9 on tenant A's PID does not affect tenant B.
3.3 ALFA — Shared engine, isolated memory (Layer 6)
ALFA (Analytics Layer for Forensic Assessment) is the shared decision-making engine. Unlike NOVA, ALFA uses a shared engine for resource efficiency, but with strict memory isolation:
- Each analysis level (tenant, site, equipment) is compartmentalized
- Data is loaded into sealed memory segments
- Analysis results are written to the tenant's PostgreSQL schema
- No cross-tenant correlation is performed without explicit consent
ALFA applies the need-to-know principle at the algorithmic level: an analysis model run for tenant A receives only tenant A's data. Multi-tenant aggregate statistics (useful for benchmarking) use anonymized data with differential privacy (ε = 1.0).
4. Layers 7–9: Cockpit, DNS, NATS
4.1 Cockpit — Dedicated domain and per-tenant JWT (Layer 7)
Each tenant accesses the Cockpit interface through a dedicated domain: client.odata.fr. The SSL certificate (Let's Encrypt) is issued for this domain. The authentication JWT is signed with a tenant-specific secret key and contains the tenant_id in its claims.
The nginx middleware validates the JWT before routing the request. An attempt to access client-b.odata.fr with tenant A's JWT is rejected at the reverse proxy level — before the request even reaches the API. The domain itself constitutes an isolation boundary: a tenant cannot guess another tenant's subdomain (the names are UUIDs or opaque identifiers).
4.2 Wildcard DNS and namespaced NATS (Layer 8)
Layer 8 operates at the inter-service communication level:
Wildcard DNS. The *.odata.fr record points to the infrastructure. Each subdomain resolves, but the application server filters unregistered domains. A tenant cannot create an arbitrary subdomain — the DNS record is controlled by the infrastructure.
Namespaced NATS. The NATS message bus uses subjects prefixed by the tenant:
The NATS ACLs are configured so that tenant A's credentials can only publish or subscribe to subjects tenant.a1b2c3d4.*. The NATS server enforces these ACLs at the protocol level — a client cannot bypass the namespacing.
4.3 Port map — No service on 0.0.0.0 (Layer 9)
Layer 9 is the most physical: the documented port map. No internal service listens on 0.0.0.0. Each service is bound to 127.0.0.1 with an explicit port. The firewall (UFW, default deny policy) blocks all incoming traffic that is not explicitly allowed. This layer is documented in detail in Section 5.
5. Documented Port Map
The following table documents all listening ports on the infrastructure. The Bind column indicates the listening interface. No service listens on 0.0.0.0, with the exception of deliberately exposed public ports (HTTP/S, SSH, SIP).
5.1 Public ports (exposed)
| Port | Protocol | Service | Justification |
|---|---|---|---|
| 22 | TCP | SSH | Administrative access |
| 80 | TCP | HTTP (nginx) | HTTPS redirect + ACME |
| 443 | TCP/UDP | HTTPS (nginx) | Reverse proxy, HTTP/3 QUIC |
| 5060 | UDP | SIP (FreeSWITCH) | VoIP telephony |
| 5080 | UDP | SIP (FreeSWITCH) | VoIP telephony |
5.2 Internal ports (bind 127.0.0.1)
| Port | Service | Tenant | Role |
|---|---|---|---|
| 5432 | PostgreSQL | Shared (RLS) | Main database |
| 6379 | Redis | Shared (prefixed) | Cache, sessions |
| 4222 | NATS | Shared (namespaced) | Message bus |
| 5090–5099 | NOVA instances | Per tenant | Network scan |
| 5100–5109 | NOVA API | Per tenant | NOVA REST API |
| 5110–5119 | SPINA workers | Per tenant | Signing/timestamping |
| 8300 | Main API | Shared (RLS) | FastAPI API |
| 8400–8409 | Cockpit workers | Per tenant | Cockpit web interface |
| 9090 | Cockpit system | Infrastructure | System administration |
5.3 UFW rules
All other ports are blocked at the firewall level. The 127.0.0.1 bind adds a second barrier: even if the firewall were disabled, the internal services would not be reachable from outside.
6. Verification — Cross-Tenant Penetration Test
6.1 Test protocol
The cross-tenant penetration test was conducted on 23 July 2026. Two test tenants were created:
- Tenant A (
a1b2c3d4-...): "veterinary clinic" test data - Tenant B (
e5f6g7h8-...): "accounting firm" test data
The tests were run from tenant A's context, attempting to access tenant B's data. All tests used valid tenant A credentials.
6.2 Vectors tested
| # | Vector | Method | Result |
|---|---|---|---|
| 1 | Direct SQL read | SELECT * FROM organisms (without tenant_id filter) | Failed — RLS returns 0 rows |
| 2 | tenant_id modification | UPDATE organisms SET tenant_id = 'e5f6...' | Failed — CHECK constraint rejects |
| 3 | Cross-tenant API access | GET /api/organisms?tenant_id=e5f6... | Failed — API ignores the parameter, uses the JWT |
| 4 | Forged JWT | JWT with tenant_id: "e5f6..." signed with key A | Failed — Invalid signature |
| 5 | Domain B Cockpit access | Connect to tenant-b.odata.fr with JWT A | Failed — Domain + JWT mismatch, 403 |
| 6 | NATS subscription | nats.sub("tenant.e5f6.*") with creds A | Failed — ACL rejects the subscription |
| 7 | NOVA process | Connect to tenant B's NOVA port | Failed — Bind 127.0.0.1, firewall blocks |
| 8 | SPINA key | Verification attempt with tenant B's key | Failed — HMAC mismatch |
| 9 | DNS enumeration | Subdomain discovery attempt | Failed — Opaque UUIDs, no zone transfer |
6.3 Result
No layer was bypassed. No combination of vectors allowed cross-tenant data to be inferred.
7. Why Classic Multi-Tenant Is Insufficient
7.1 The single-layer model
The classic multi-tenant architecture — that of most SaaS — relies on a single application-level separation. The application appends WHERE tenant_id = ? to every query. This approach has three structural failures:
- Single point of failure. A forgotten WHERE clause in a join, a poorly tested schema migration, or an ORM bug exposes all tenants' data.
- No defense against privilege escalation. An attacker who obtains application administrator access (via SQL injection, credential stuffing, or a dependency vulnerability) bypasses the entirety of the isolation.
- No isolation at the infrastructure level. Database, cache, message bus — all these components are shared without an internal barrier. A memory leak in Redis exposes every tenant's sessions.
7.2 Comparison with the 0DATA approach
| Dimension | Classic multi-tenant | 0DATA isolation (9 layers) |
|---|---|---|
| Database | WHERE tenant_id = ? | RLS + constraints + triggers |
| Signing | Shared | Per-tenant HMAC + eIDAS |
| Observation | Shared | NOVA instance per tenant |
| Analysis | Shared | ALFA with isolated memory |
| Interface | Same domain | Domain per tenant, dedicated JWT |
| Messaging | Shared subjects | Namespaced NATS with ACLs |
| Network | Services on 0.0.0.0 | Bind 127.0.0.1, firewall |
| Depth | 1 layer | 9 independent layers |
7.3 The cost of isolation
Nine-layer isolation has a cost: deployment complexity, additional memory consumption (one NOVA instance per tenant), and maintenance of the NATS ACLs. This cost is accepted as a structural investment. In the MSP context, where a cross-tenant data leak can lead to lawsuits, GDPR sanctions, and the loss of clients, the cost of isolation is lower than the cost of a breach.
The architecture is documented, automated (Ansible deployment), and tested (cross-tenant pentest at each deployment). Complexity is kept in check through reproducibility.
8. Related Work and Outlook
Tenant isolation in shared infrastructures is an active topic in the literature. "Cage"-type architectures (Google Borg, AWS Nitro Enclaves) use hardware virtualization to isolate tenants — a robust but resource-expensive approach. "Shared-nothing" architectures (each tenant on its own cluster) offer perfect isolation at the price of economic inefficiency.
The 0DATA approach occupies an equilibrium point: shared infrastructure, isolated data. The biological model of tight junctions guides this architecture — each layer is independent, redundant, and verifiable.
Future work includes:
- Automatic per-tenant SPINA key rotation (configurable periodicity)
- Multi-tenant differential privacy for statistical aggregations
- Hardware isolation (SGX/TDX) for tenants with high regulatory requirements
- Continuous cross-tenant audit — automatic testing of the nine layers at each deployment
References
TIKIJJA, Hadda. "The Discipline". 0DATA Lab, Paper 001, July 2026.
TIKIJJA, Hadda. "The Nervous System". 0DATA Lab, Paper 003, July 2026.
TIKIJJA, Hadda. "The Digital Graft". 0DATA Lab, Paper 004, July 2026.
TIKIJJA, Hadda. "The Immune System". 0DATA Lab, Paper 005, July 2026.
TIKIJJA, Hadda. "Surface Audit". 0DATA Lab, Paper 014, July 2026.
PostgreSQL Documentation. "Row Security Policies". PostgreSQL 16, 2024.
NATS Documentation. "Subject-Based Messaging and ACLs". Synadia Communications, 2024.
eIDAS Regulation (EU) No 910/2014. "Electronic Identification and Trust Services".
RFC 3161. "Internet X.509 Public Key Infrastructure Time-Stamp Protocol (TSP)".
Dwork, C., Roth, A. "The Algorithmic Foundations of Differential Privacy". Foundations and Trends in Theoretical Computer Science, 2014.