0DATA Lab · Paper 014 · July 2026

Tenant Isolation

A 9-Layer Architecture for Shared Infrastructure

Hadda TIKIJJA
0DATA Lab, France

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.

Biomimetic transposition
Cell membrane → Each tenant has its own SPINA, NOVA, ALFA instances
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

LayerNameDomainMechanism
1PostgreSQL RLSDatabaseRow-Level Security policies
2PostgreSQL schemaDatabaseLogical schema per tenant
3Application policiesDatabaseCHECK constraints, triggers
4SPINA v2SigningLocal HMAC-SHA256 + eIDAS RFC 3161
5NOVAObservationInstance per tenant, bind 127.0.0.1
6ALFADecision-makingShared engine, isolated memory
7CockpitInterfaceDedicated domain, per-tenant JWT
8DNS / NATSCommunicationWildcard DNS, namespaced subjects
9Port mapNetworkNo 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.

ALTER TABLE organisms ENABLE ROW LEVEL SECURITY; CREATE POLICY tenant_isolation ON organisms USING (tenant_id = current_setting('app.current_tenant_id')::uuid);
Figure 1: PostgreSQL RLS policy — the tenant_id column is compared against the session variable.

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:

ALTER TABLE organisms ADD CONSTRAINT chk_tenant_owns CHECK (tenant_id IS NOT NULL AND tenant_id = current_setting('app.current_tenant_id')::uuid);
Figure 2: CHECK constraint preventing cross-tenant writes.

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.

Authorization: Bearer eyJ...tenant_id:"a1b2c3d4-..."...
Figure 3: Cockpit authentication header — JWT containing the tenant_id.

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:

tenant.a1b2c3d4.nova.scan.start tenant.a1b2c3d4.spina.sign tenant.e5f6g7h8.nova.scan.start
Figure 4: NATS subjects with tenant prefix. ACLs restrict access by prefix.

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)

PortProtocolServiceJustification
22TCPSSHAdministrative access
80TCPHTTP (nginx)HTTPS redirect + ACME
443TCP/UDPHTTPS (nginx)Reverse proxy, HTTP/3 QUIC
5060UDPSIP (FreeSWITCH)VoIP telephony
5080UDPSIP (FreeSWITCH)VoIP telephony

5.2 Internal ports (bind 127.0.0.1)

PortServiceTenantRole
5432PostgreSQLShared (RLS)Main database
6379RedisShared (prefixed)Cache, sessions
4222NATSShared (namespaced)Message bus
5090–5099NOVA instancesPer tenantNetwork scan
5100–5109NOVA APIPer tenantNOVA REST API
5110–5119SPINA workersPer tenantSigning/timestamping
8300Main APIShared (RLS)FastAPI API
8400–8409Cockpit workersPer tenantCockpit web interface
9090Cockpit systemInfrastructureSystem administration

5.3 UFW rules

ufw default deny incoming ufw default allow outgoing ufw allow 22/tcp ufw allow 80/tcp ufw allow 443 ufw allow 5060/udp ufw allow 5080/udp
Figure 5: UFW rules — only 5 ports are publicly exposed.

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:

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

#VectorMethodResult
1Direct SQL readSELECT * FROM organisms (without tenant_id filter)Failed — RLS returns 0 rows
2tenant_id modificationUPDATE organisms SET tenant_id = 'e5f6...'Failed — CHECK constraint rejects
3Cross-tenant API accessGET /api/organisms?tenant_id=e5f6...Failed — API ignores the parameter, uses the JWT
4Forged JWTJWT with tenant_id: "e5f6..." signed with key AFailed — Invalid signature
5Domain B Cockpit accessConnect to tenant-b.odata.fr with JWT AFailed — Domain + JWT mismatch, 403
6NATS subscriptionnats.sub("tenant.e5f6.*") with creds AFailed — ACL rejects the subscription
7NOVA processConnect to tenant B's NOVA portFailed — Bind 127.0.0.1, firewall blocks
8SPINA keyVerification attempt with tenant B's keyFailed — HMAC mismatch
9DNS enumerationSubdomain discovery attemptFailed — Opaque UUIDs, no zone transfer

6.3 Result

Cross-tenant pentest result
Zero successes across nine vectors. Each layer blocked the attempts that concerned it. Layers 1 (RLS) and 2 (constraints) blocked the SQL attempts. Layer 4 (SPINA) blocked cryptographic forgery. Layer 8 (NATS) blocked message interception. Layer 9 (firewall + bind) blocked direct network access.

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:

  1. Single point of failure. A forgotten WHERE clause in a join, a poorly tested schema migration, or an ORM bug exposes all tenants' data.
  2. 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.
  3. 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

DimensionClassic multi-tenant0DATA isolation (9 layers)
DatabaseWHERE tenant_id = ?RLS + constraints + triggers
SigningSharedPer-tenant HMAC + eIDAS
ObservationSharedNOVA instance per tenant
AnalysisSharedALFA with isolated memory
InterfaceSame domainDomain per tenant, dedicated JWT
MessagingShared subjectsNamespaced NATS with ACLs
NetworkServices on 0.0.0.0Bind 127.0.0.1, firewall
Depth1 layer9 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:

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.

Acknowledgements. To the PostgreSQL team for Row-Level Security, to the NATS maintainers for ACL-based namespacing, and to the 0DATA infrastructure that withstood the cross-tenant pentest without flinching. Isolation is not a feature — it is the very structure of the living.