Skip to content

Multi-tenancy

One Drupal instance can host several isolated vault realms, called tenants. Each tenant is a separate vault: its data is partitioned per (tenant, owner) and it can use its own Master KEK, so compromising one tenant's key cannot read another's. A person keeps a single Drupal account; if they appear in two tenants they hold a distinct, separately-encrypted vault in each.

Every install has one tenant out of the box - the default tenant, created on install - and that is all a single-tenant site ever needs. The tenant controls below stay hidden until you create a second tenant, so multi-tenancy is invisible unless you opt into it.

When to use it

Use multiple tenants when one site serves vaults that must not share an encryption boundary - for example several agencies or municipalities behind one Drupal, each wanting its own Master KEK and its own consumers, while citizens keep one login. If you only need one vault, leave the default tenant as is.

How isolation works

The key hierarchy gains the tenant as a partition:

  • Each owner has a distinct Subject KEK per tenant, so the same person's data in tenant A is encrypted independently of their data in tenant B.
  • Each tenant resolves a Master KEK: its own key, or - when left empty - the site-wide Master KEK from Configuration -> Personal Data Vault (/admin/config/pdv/settings). The default tenant ships with no key of its own, so it inherits the site-wide one and behaves exactly as a single-tenant install always did.
  • Items and subject keys carry their tenant, so a query, a grant, or a purge in one tenant never reaches another's rows.

The same person in two tenants holds two independently-encrypted vaults, each under its tenant's Master KEK:

flowchart TD
  User["One Drupal account (Alice)"]
  subgraph A["Tenant A"]
    MA["Master KEK A"] --> SA["Alice's Subject KEK (A)"] --> DA["Alice's items in A"]
  end
  subgraph B["Tenant B"]
    MB["Master KEK B"] --> SB["Alice's Subject KEK (B)"] --> DB["Alice's items in B"]
  end
  User --> SA
  User --> SB

Managing tenants

With the administer pdv tenants permission (restricted; grant only to trusted roles), manage realms at Configuration -> Personal Data Vault -> Vault tenants (/admin/config/pdv/tenants).

Each tenant has:

  • a machine name and label;
  • a Master KEK - select a key from the Key module to give this tenant its own cryptographic boundary, or leave it as Site default to inherit the site-wide Master KEK;
  • a Stay unlocked for duration - how long a passphrase-protected vault in this tenant stays unlocked while idle. Activity extends the unlock (an active owner is never locked out mid-session) and an idle vault re-locks after it. Leave at 0 to inherit the site-wide default (Default vault unlock idle timeout on the PDV settings form). There is no "never lock", but a multi-day value is allowed;
  • a What keeps the vault unlocked choice - what counts as activity for that timeout: Any page the owner visits (the vault stays unlocked while the owner uses the site, like a Drupal session) or Reading or saving vault items only (it locks again once the owner stops working with the vault, even while they keep browsing). Leave it at Site default to use the choice on the PDV settings form. A change applies to unlocks made after it.

The Master KEK rules from Installation apply per tenant: it must resolve to 32 bytes, it cannot be deleted or have its value changed while it still protects a tenant's data, and pointing a tenant at a different key requires a re-wrap (see Rotating a tenant's Master KEK).

A tenant cannot be deleted while anything still belongs to it: vault items, subject keys, item kinds scoped to it, or consumers bound to it. This holds wherever the delete comes from: its delete page, which says what is left, drush, a configuration import or code. Otherwise the leftovers would name a tenant that no longer exists, and a tenant created later under the same machine name would inherit them. The delete page also refuses to delete the default tenant.

Binding a consumer to a tenant

A consumer (an OAuth client, or a same-site module) acts inside exactly one tenant. When more than one tenant exists, the consumer add form shows a Tenant selector; the choice is immutable once the consumer is created (the field is locked on edit, and a change is refused). A consumer can only read and write vaults in its bound tenant - this is what scopes the cross-site API to a realm. With a single tenant the selector is hidden and every consumer belongs to the default tenant.

Shared and tenant-scoped item kinds

The item-kind vocabulary is hybrid: a kind is either shared (offered in every tenant, the default) or scoped to one tenant. On the kind form, a Tenant selector appears once more than one tenant exists - leave it Shared (all tenants) or pick a single tenant. The Item kinds list shows a Tenant column so you can see each kind's scope at a glance. Shared kinds and the current tenant's scoped kinds are the ones offered when storing an item.

Rotating a tenant's Master KEK

Master KEK rotation is per tenant: changing one tenant's key re-wraps only that tenant's subject keys, and never scans the others. After pointing a tenant at a new key, drain its re-wrap backlog:

  • From the UI, at Configuration -> Personal Data Vault -> Vault subjects (/admin/config/pdv/subjects), use the per-tenant Rotate to current Master KEK action; it re-wraps that tenant's subject keys in bounded batches and reports progress.
  • From the command line, drush pdv:rotate-keys drains every active tenant, and drush pdv:rotate-keys <tenant> drains a single tenant.

Cron advances each active tenant's backlog automatically. Once no subject is left on a tenant's old key, that key can be retired.

Resolving the current tenant (developers)

Which tenant a request acts in is decided by tenant resolvers: services tagged pdv.tenant_resolver implementing ConsumerTenantResolverInterface. Each resolver returns a tenant id or NULL to defer; the first non-null answer wins, and if none answer the default tenant is used.

pdv ships one resolver, ConsumerTenantResolver, which resolves the tenant from the calling consumer's immutable binding - so a cross-site API call lands in the consumer's tenant automatically. To resolve tenants another way (for example by domain or path), implement ConsumerTenantResolverInterface and tag the service:

services:
  my_module.tenant_resolver:
    class: Drupal\my_module\MyTenantResolver
    tags:
      - { name: pdv.tenant_resolver }

A domain-based connector is not shipped yet; see the Roadmap.

Marking a type as belonging to a tenant (developers)

An entity type whose rows belong to a vault tenant declares a pdv_tenant entity key naming the field (or, on a config entity, the exported property) that holds the tenant, the way core's owner and published keys name theirs: entity_keys: [..., 'pdv_tenant' => 'tenant']. pdv's items, subject keys and item kinds do. A tenant is then not deleted while rows of that type still name it, without pdv having to know the type. The key is named after pdv because a tenant of another module is another realm: a module keeping tenants of its own names its key after itself.

A config entity type scoped to a tenant also declares a dependency on it, as item kinds do in PdvItemKind::calculateDependencies(). A configuration import that removes a tenant and its config then deletes the config first, since the tenant is refused while anything still names it.

Core creates the column of every entity key as NOT NULL, so a content entity type whose tenant may be left empty must override its storage schema to keep that column nullable.