Auth & Security

Tenancy Isolation

How every request is pinned to a tenant, how the token–tenant relationship is resolved, and how the cross-tenant boundary is enforced.

Ductor is multi-tenant from the ground up: every pool, rule, connection, workflow run, and API key belongs to a tenant. Isolation is the guarantee that one tenant can never read or mutate another's data — and it is enforced at the request boundary, not left to application code to remember.

Isolation vs entitlements

Isolation is whose data is this?; entitlements are how much may this tenant do?. Isolation is what makes an entitlement limit meaningful. This page covers the boundary; see also Concepts → Tenancy.

How the tenant is determined

Every authenticated request must resolve to exactly one tenant. The tenant interceptor resolves it from a priority cascade:

  1. JWT / Principal claims. The authenticated Principal.TenantID (from the token's tenant_id claim, or an API key's owning tenant) is authoritative and checked first.
  2. X-Tenant-ID header. For direct Connect/HTTP requests, the X-Tenant-ID header supplies the tenant when the credential is not itself tenant-scoped (for example a service token or an anonymous/dev deployment).
  3. gRPC metadata. The x-tenant-id metadata equivalent, used on the gateway loopback path.

If none of these yields a tenant, the request is refused (tenant ID required: set X-Tenant-ID header or authenticate with a tenant-scoped token). The resolved value is then format-validated (it must be a well-formed tenant ID) and the tenant is state-checked — a tenant that does not resolve to an active row is rejected before the handler runs.

A tenant-scoped token wins over the header

Because claims are consulted first, a caller holding a token whose tenant_id claim is tenant A cannot escape into tenant B by adding an X-Tenant-ID: B header — the claim already fixed the tenant. The header only supplies a tenant when the credential does not carry one.

Exempt methods

A small, explicit set of methods runs without tenant enforcement — the health probes (HealthService/Health, HealthService/Ready) and tenant bootstrap (TenantService/CreateTenant, which by definition has no tenant yet). Everything else requires a resolved, valid, active tenant.

The cross-tenant boundary

Resolving a tenant is not enough; the authorizer also verifies that the caller may act on the target tenant of the request:

  • Self-match. For a normal request, the target tenant equals the principal's tenant, and the boundary check passes as a same-tenant self-match.
  • Platform-admin crossing. A principal holding the deployment-issued platform-admin role may cross tenant boundaries through the governed control plane. Tenant admin remains confined to its own tenant.
  • Violation. Any other case where the principal's tenant does not match the target is a tenant boundary violation, logged and denied.

Control-plane RPCs carry a target tenant in the body

Some administrative RPCs act on a tenant named in the request body (not the caller's own). For those, the interceptor resolves the resource tenant from the declared body field (or a registered resolver) and runs the boundary check against that real target — never silently against the caller's own tenant.

Declared-but-unresolvable fails closed

If a method declares that it takes a target tenant from the request but the field is missing or empty, the request is denied rather than falling back to the principal's tenant. A control-plane method that cannot determine its target tenant must not degrade into a same-tenant self-match — that would be a cross-tenant escalation. Streaming handlers, which cannot peek the first message, resolve to unresolved for body-tenant methods and therefore fail closed.

Isolation extends to secrets

Tenant isolation is not only a request-time check — it is baked into how connector credentials are stored. Each credential blob is AEAD-encrypted with tenant-bound associated data (tenantID | providerKey), so a ciphertext decrypted under the wrong tenant fails loudly. That blocks a cross-tenant credential swap even if a blob were somehow moved between tenants. See Hardening → Connector credential encryption.

Isolation guarantees, summarized

  • Every authenticated request is pinned to exactly one validated, active tenant, or it is refused.
  • A tenant-scoped token's claim is authoritative and cannot be overridden by a header.
  • Cross-tenant access requires the deployment-issued platform-admin role; tenant admin and all other mismatches are denied.
  • Control-plane RPCs check against the real target tenant and fail closed when it cannot be resolved.
  • Credential secrets are cryptographically bound to their tenant at rest.