MATIH Platform is in active MVP development. Documentation reflects current implementation status.
3. Security & Multi-Tenancy
Tenant Isolation

Tenant Isolation

Tenant isolation is the foundational security guarantee of the MATIH Platform. In a multi-tenant system, each tenant's data, configuration, and workloads must be completely invisible and inaccessible to all other tenants. MATIH enforces this guarantee at every layer: Kubernetes namespaces, network policies, application-level context propagation, database row-level security, and cryptographic key separation.


Isolation Architecture

Tenant isolation in MATIH operates at four distinct layers:

LayerMechanismEnforcement
InfrastructureKubernetes namespace per tenantResource quotas, network policies, RBAC
NetworkNetworkPolicy rulesIngress/egress restrictions per namespace
ApplicationTenantContextHolder (thread-local)Every database query, API call, and event filtered by tenant
DataPer-tenant encryption keys, row-level filteringCryptographic isolation of data at rest

Kubernetes Namespace Isolation

Each tenant in MATIH receives a dedicated Kubernetes namespace. This namespace serves as the primary isolation boundary for all tenant resources.

Namespace Structure

matih-system/              # Control plane services
matih-shared/              # Shared infrastructure (Kafka, databases)
tenant-acme-corp/          # Tenant: ACME Corporation
  ├── ai-service
  ├── query-engine
  ├── bi-service
  ├── ml-service
  └── ... (all data plane services)
tenant-globex/             # Tenant: Globex Corporation
  ├── ai-service
  ├── query-engine
  ├── bi-service
  └── ...

Namespace-Level Controls

Each tenant namespace is configured with:

ControlPurposeExample
ResourceQuotaLimit CPU, memory, and storage per tenantcpu: 8 cores, memory: 32Gi
LimitRangeDefault and maximum resource limits for podsdefault cpu: 500m, max cpu: 2
NetworkPolicyRestrict network communicationOnly allow traffic within namespace + control plane
RBACKubernetes role bindingsTenant service accounts can only access own namespace
PodSecurityStandardsPod security constraintsEnforce non-root containers, read-only filesystems

Resource Quota Example

apiVersion: v1
kind: ResourceQuota
metadata:
  name: tenant-quota
  namespace: tenant-acme-corp
spec:
  hard:
    requests.cpu: "8"
    requests.memory: 32Gi
    limits.cpu: "16"
    limits.memory: 64Gi
    persistentvolumeclaims: "10"
    pods: "50"
    services: "20"

Network Policy

Network policies ensure that pods in one tenant namespace cannot communicate with pods in another tenant namespace:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: tenant-isolation
  namespace: tenant-acme-corp
spec:
  podSelector: {}
  policyTypes:
    - Ingress
    - Egress
  ingress:
    # Allow traffic from within the same namespace
    - from:
        - podSelector: {}
    # Allow traffic from the control plane
    - from:
        - namespaceSelector:
            matchLabels:
              matih.ai/role: control-plane
    # Allow traffic from the tenant's ingress controller
    - from:
        - namespaceSelector:
            matchLabels:
              matih.ai/role: ingress
          podSelector:
            matchLabels:
              matih.ai/tenant: acme-corp
  egress:
    # Allow DNS resolution
    - to:
        - namespaceSelector: {}
          podSelector:
            matchLabels:
              k8s-app: kube-dns
      ports:
        - protocol: UDP
          port: 53
    # Allow traffic within the namespace
    - to:
        - podSelector: {}
    # Allow traffic to shared infrastructure
    - to:
        - namespaceSelector:
            matchLabels:
              matih.ai/role: shared-infra

Application-Level Tenant Context

While Kubernetes namespaces provide infrastructure isolation, application-level isolation ensures that every database query, API response, and event is scoped to the correct tenant. This is implemented through the TenantContextHolder and TenantContext classes.

TenantContextHolder

The TenantContextHolder uses Java's ThreadLocal to maintain tenant context throughout the lifecycle of a request:

public final class TenantContextHolder {
    private static final ThreadLocal<TenantContext> CONTEXT = new ThreadLocal<>();
 
    // Set tenant context at the beginning of a request
    public static void setTenantId(String tenantId) {
        CONTEXT.set(new TenantContext(tenantId, null, Map.of()));
    }
 
    // Get tenant context during request processing
    public static Optional<String> getTenantId() {
        return getContext().map(TenantContext::tenantId);
    }
 
    // Get required tenant ID (throws if not set)
    public static String requireTenantId() {
        return requireContext().tenantId();
    }
 
    // Clear context at the end of a request
    public static void clear() {
        CONTEXT.remove();
    }
}

TenantContext Record

The TenantContext record carries the complete tenant context:

public record TenantContext(
    String tenantId,       // Required: tenant identifier
    String userId,         // Optional: current user ID
    Map<String, Object> attributes  // Optional: additional context
) {
    public TenantContext {
        if (tenantId == null || tenantId.isBlank()) {
            throw new IllegalArgumentException("Tenant ID cannot be null or blank");
        }
        attributes = attributes != null ? Map.copyOf(attributes) : Map.of();
    }
}

Request Lifecycle

The tenant context is established at the very beginning of each request and cleared at the end:

HTTP Request arrives with JWT token
        |
        v
[SecurityFilter] -- Extract JWT claims
        |
        v
[TenantContextHolder.setContext()] -- Set tenant from JWT "tenant_id" claim
        |
        v
[Controller] --> [Service] --> [Repository] --> [Database]
        |           |              |                |
        |       All layers access TenantContextHolder.getTenantId()
        |       to scope their operations to the current tenant
        |
        v
[TenantContextHolder.clear()] -- Clean up after request completes

Context Propagation

The TenantContext persistence utility provides convenient access methods for the data access layer:

// In a repository or service method
String tenantId = TenantContext.requireCurrentTenantId();
 
// Check if running as system tenant
if (TenantContext.isSystemTenant()) {
    // System-level operations (cross-tenant queries, admin tasks)
}
 
// Execute with a different tenant context
TenantContext.withTenant("other-tenant", () -> {
    // All operations in this block use "other-tenant" context
    // Original context is restored when the block completes
});
 
// Execute as system tenant
TenantContext.asSystem(() -> {
    // Operations here use the "system" tenant context
    // Useful for cross-tenant aggregation, admin operations
});

Async Context Propagation

When operations are dispatched to background threads, the tenant context must be explicitly propagated. The TenantContextHolder provides wrappers for this:

// Wrap a Runnable to preserve tenant context
Runnable contextAware = TenantContextHolder.wrapWithContext(() -> {
    // This code runs in a different thread but has the correct tenant context
    String tenantId = TenantContextHolder.requireTenantId();
    processDataForTenant(tenantId);
});
 
executorService.submit(contextAware);
 
// Wrap a Callable to preserve tenant context
Callable<Result> contextAwareCallable = TenantContextHolder.wrapWithContext(() -> {
    return expensiveComputation();
});
 
Future<Result> future = executorService.submit(contextAwareCallable);

Without these wrappers, background threads would have no tenant context, which could lead to unauthorized data access or application errors.


System Tenant

The SYSTEM_TENANT constant ("system") represents the platform itself, used for operations that are not scoped to any specific tenant:

Use CaseExample
Cross-tenant aggregationPlatform-wide usage metrics
Admin operationsTenant provisioning, billing calculations
System maintenanceDatabase migrations, cache warming
Scheduled tasksHealth checks, cleanup jobs
// Execute as system tenant
TenantContext.asSystem(() -> {
    List<TenantUsage> allUsage = usageRepository.findAll();
    generateBillingReport(allUsage);
});

Access to the system tenant context is restricted to services running in the matih-system namespace and requires the super_admin role or specific system-level scopes.


Database Tenant Isolation

At the database layer, tenant isolation is enforced through multiple mechanisms:

Row-Level Security

Every tenant-scoped table includes a tenant_id column. The persistence layer automatically filters queries by the current tenant context:

-- Every query is automatically scoped
SELECT * FROM dashboards
WHERE tenant_id = 'acme-corp'  -- Injected from TenantContext
  AND id = 42;

Hibernate Multi-Tenancy

The TenantIdentifierResolver integrates with Hibernate's multi-tenancy support to automatically resolve the current tenant for all database operations:

public class TenantIdentifierResolver implements CurrentTenantIdentifierResolver {
 
    @Override
    public String resolveCurrentTenantIdentifier() {
        return TenantContext.getCurrentTenantIdOrDefault("system");
    }
 
    @Override
    public boolean validateExistingCurrentSessions() {
        return true;
    }
}

Schema-Per-Tenant Option

For tenants with stricter isolation requirements, MATIH supports schema-per-tenant isolation where each tenant has its own database schema:

PostgreSQL Database
├── schema: tenant_acme_corp
│   ├── dashboards
│   ├── queries
│   └── ...
├── schema: tenant_globex
│   ├── dashboards
│   ├── queries
│   └── ...
└── schema: system
    ├── tenants
    ├── users
    └── ...

Event-Level Isolation

All events published to Kafka include the tenant ID, and consumers filter events by tenant:

Event Envelope

{
  "eventId": "evt_abc123",
  "tenantId": "acme-corp",
  "eventType": "QUERY_EXECUTED",
  "timestamp": "2026-02-10T14:30:00Z",
  "payload": {
    "queryId": "q_456",
    "executionTimeMs": 230
  }
}

Topic-Per-Tenant Strategy

For high-isolation requirements, Kafka topics can be partitioned by tenant:

StrategyTopic PatternIsolation Level
Shared topic with tenant filtermatih.events (filter by tenantId)Standard
Tenant-prefixed topicsacme-corp.events, globex.eventsEnhanced
Dedicated Kafka clustersSeparate Kafka for each tenantMaximum

Service Mesh Isolation

In service-to-service communication, tenant context is propagated through HTTP headers:

HeaderPurpose
X-Tenant-IdCurrent tenant identifier
X-Correlation-IdRequest correlation for tracing
AuthorizationService-to-service JWT with tenant claim

Services validate that the tenant ID in the header matches the tenant ID in the JWT token. Any mismatch results in an immediate rejection.


Zero-Trust Enforcement

The ZeroTrustSecurityService enforces zero-trust principles across all service interactions:

  1. Verify explicitly. Every request is authenticated and authorized, regardless of network location.
  2. Least privilege access. Service tokens carry only the scopes needed for the specific call.
  3. Assume breach. Network policies restrict lateral movement even within a tenant namespace.
Service A (tenant-acme-corp)
    |
    |-- Service Token (5-min lifetime, scoped to sql:execute)
    |-- X-Tenant-Id: acme-corp
    |-- X-Correlation-Id: req_789
    v
Service B (tenant-acme-corp)
    |-- Validates JWT signature
    |-- Verifies tenant_id matches header
    |-- Checks scopes include sql:execute
    |-- Proceeds with tenant-scoped operation

Cross-Tenant Protection Patterns

Defense Against Tenant ID Manipulation

The platform protects against attempts to access another tenant's data:

Attack VectorProtection
Modified JWT tenant_id claimJWT signature verification
Spoofed X-Tenant-Id headerHeader must match JWT claim
Direct database query injectionRow-level security, parameterized queries
API parameter manipulationServer-side tenant context from JWT, not from request
Event replay from another tenantEvent tenant_id verified against consumer's tenant

Key Principle: Tenant ID Comes from the Token

The tenant ID is never taken from user input (query parameters, request body, headers submitted by clients). It is always extracted from the validated JWT token by the server:

// CORRECT: Tenant from JWT token (set by SecurityFilter)
String tenantId = TenantContextHolder.requireTenantId();
 
// INCORRECT: Never trust client-provided tenant ID
// String tenantId = request.getParameter("tenantId"); // DO NOT DO THIS

Testing Tenant Isolation

The platform includes integration tests that verify tenant isolation:

@Test
void shouldNotAccessOtherTenantData() {
    // Create data as tenant A
    TenantContext.withTenant("tenant-a", () -> {
        dashboardService.create(new Dashboard("My Dashboard"));
    });
 
    // Attempt to access as tenant B
    TenantContext.withTenant("tenant-b", () -> {
        List<Dashboard> dashboards = dashboardService.findAll();
        assertThat(dashboards).isEmpty(); // Tenant B sees nothing from Tenant A
    });
}
 
@Test
void shouldIsolateConcurrentRequests() {
    // Simulate concurrent requests from different tenants
    CompletableFuture<String> futureA = CompletableFuture.supplyAsync(
        TenantContextHolder.wrapWithContext(() -> {
            TenantContextHolder.setTenantId("tenant-a");
            return TenantContextHolder.requireTenantId();
        })
    );
 
    CompletableFuture<String> futureB = CompletableFuture.supplyAsync(
        TenantContextHolder.wrapWithContext(() -> {
            TenantContextHolder.setTenantId("tenant-b");
            return TenantContextHolder.requireTenantId();
        })
    );
 
    assertThat(futureA.join()).isEqualTo("tenant-a");
    assertThat(futureB.join()).isEqualTo("tenant-b");
}

Monitoring Tenant Isolation

Isolation violations are detected through:

SignalDetection Method
Cross-tenant data access attemptsAudit log analysis for tenant mismatch errors
Network policy violationsKubernetes audit logs, Calico/Cilium flow logs
Unusual API patternsAnomaly detection on per-tenant API usage
Permission escalationAlert on role changes and impersonation events

Metrics are exposed per tenant for monitoring:

matih_tenant_requests_total{tenant="acme-corp", status="200"}
matih_tenant_requests_total{tenant="acme-corp", status="403"}
matih_tenant_isolation_violations_total{tenant="acme-corp"}

Next Steps

Continue to Secret Management to learn how the platform securely manages credentials, API keys, and encryption keys across environments.