Multi-Tenancy
Tayra supports multi-tenant key isolation out of the box. When enabled, each tenant's encryption keys are stored with a tenant-specific prefix, ensuring complete cryptographic separation between tenants. Tenant A can never access or decrypt Tenant B's data, even though they share the same physical key store.
How It Works
Multi-tenancy in Tayra is implemented as a decorator pattern. The TenantAwareKeyStore wraps any existing IKeyStore registration and transparently prefixes all key operations with the current tenant ID:
Logical key ID: patient-abc123
Stored key ID: tenant-a:patient-abc123When no tenant context is set (the tenant provider returns null), key operations pass through unchanged. This makes multi-tenancy opt-in and backward-compatible with single-tenant deployments.
Setup
Register multi-tenancy after your key store registration. You must provide an ITenantProvider implementation that resolves the current tenant:
var services = new ServiceCollection();
services.AddTayra(opts => opts.LicenseKey = licenseKey);
// Add multi-tenancy with a custom tenant provider
services.AddTayraMultiTenancy<HttpHeaderTenantProvider>(options =>
{
options.TenantSeparator = ":";
});The AddTayraMultiTenancy<T>() method:
- Registers your
ITenantProviderimplementation as a singleton - Removes the existing
IKeyStoreregistration - Wraps it with a
TenantAwareKeyStoredecorator - Re-registers the decorated store as the
IKeyStoreservice
Register Key Store First
You must register Tayra via AddTayra() (which defaults to the built-in InMemoryKeyStore, or chain a key store like .UseVaultKeyStore()) before calling AddTayraMultiTenancy(). The extension method decorates the existing IKeyStore registration, so it throws InvalidOperationException if no IKeyStore is found.
Configuration Options
The TayraMultiTenancyOptions class controls the key prefixing behavior:
var mtOptions = new TayraMultiTenancyOptions
{
// Separator between tenant ID and key ID (default: ":")
// With tenant "tenant-a" and key "patient-abc123",
// the actual key stored is "tenant-a:patient-abc123"
TenantSeparator = ":",
};| Property | Default | Description |
|---|---|---|
TenantSeparator | ":" | The character(s) inserted between the tenant ID and the original key ID |
With the default separator, a key ID patient-abc123 for tenant acme becomes acme:patient-abc123 in the underlying key store.
Implementing ITenantProvider
The ITenantProvider interface has a single method that returns the current tenant ID:
/// <summary>
/// Example tenant provider that resolves the tenant ID from
/// an ambient context. In a real application, this would read
/// from HttpContext headers, JWT claims, or a similar source.
/// </summary>
public class HttpHeaderTenantProvider : ITenantProvider
{
// In production, inject IHttpContextAccessor and read from headers/claims
private static readonly AsyncLocal<string?> CurrentTenant = new();
public string? GetCurrentTenantId()
{
return CurrentTenant.Value;
}
/// <summary>
/// Sets the current tenant for the async flow. Call this in middleware
/// or at the start of a request pipeline.
/// </summary>
public static void SetTenant(string? tenantId)
{
CurrentTenant.Value = tenantId;
}
}In real applications, you would typically resolve the tenant from:
- HTTP request headers (e.g.,
X-Tenant-Id) - JWT claims (e.g., a
tenant_idclaim) - Subdomain (e.g.,
acme.yourapp.com) - Route parameters (e.g.,
/api/{tenantId}/patients)
ASP.NET Core Integration
For ASP.NET Core applications, inject IHttpContextAccessor into your tenant provider to read headers or claims from the current request. Register it with services.AddHttpContextAccessor().
Null Tenant Passthrough
When ITenantProvider.GetCurrentTenantId() returns null, the TenantAwareKeyStore passes all operations through to the inner key store without any prefixing. This is useful for:
- Background jobs that run outside a tenant context
- System-level keys that are shared across tenants
- Migration scenarios where you need to access unprefixed keys
Key Isolation Guarantees
The TenantAwareKeyStore provides the following guarantees:
| Operation | Behavior |
|---|---|
StoreAsync | Key is stored with {tenantId}:{keyId} |
GetAsync | Only retrieves keys prefixed with the current tenant |
DeleteAsync | Only deletes keys prefixed with the current tenant |
ExistsAsync | Only checks keys prefixed with the current tenant |
DeleteByPrefixAsync | Prefix is scoped to the current tenant |
ListKeyIdsAsync | Returns only the current tenant's keys, with the tenant prefix stripped |
Production Deployment
In production multi-tenant deployments, always ensure your ITenantProvider returns a non-null value for tenant-scoped requests. A null tenant in a multi-tenant context could lead to keys being stored without isolation, potentially accessible from other tenant contexts.
See Also
- Dependency Injection -- Core service registration
- Key Stores -- Key store implementations
- Health Checks -- Monitoring key store connectivity
