Skip to content

Master KEK in OpenBao or HashiCorp Vault

pdv_vault lets the Master KEK live inside OpenBao or HashiCorp Vault instead of in a local Key. The root key never reaches Drupal: pdv sends the per-user Subject KEK to the store's Transit engine to be wrapped, and stores only the returned ciphertext.

How it works

pdv uses three-tier envelope encryption (see Concepts); the Master KEK only ever wraps the per-user Subject KEK. With pdv_vault that wrap step happens server-side in the secret store:

  • Wrap sends the Subject KEK to transit/encrypt/<key>; the store returns a vault:v<n>:... ciphertext, which pdv stores in the subject-key row. The Master KEK itself never leaves the store.
  • Unwrap sends that ciphertext to transit/decrypt/<key>.
  • Rotation uses transit/rewrap/<key>: the store re-encrypts the blob under the latest key version without ever exposing the Subject KEK plaintext.

Owner-binding. The Transit key is created with derived=true, and every call passes the owner's identity (pdv:subject:<uid>) as the per-call context. A blob wrapped for one owner cannot be unwrapped under another owner's context, mirroring the additional-authenticated-data binding of pdv's local AEAD.

Requirements

  • An OpenBao or HashiCorp Vault server reachable from the Drupal site.
  • The Transit secrets engine enabled.
  • A Transit key created with derived=true (required for owner-binding).
  • A token whose policy allows encrypt, decrypt and rewrap on that key.

1. Prepare the secret store

With the bao CLI (use vault for HashiCorp Vault, the commands are identical):

bao secrets enable transit
bao write -f transit/keys/pdv-master derived=true

derived=true is mandatory

Owner-binding relies on per-owner derivation. A key created without derived=true rejects the context parameter and pdv's wrap calls fail.

Grant a least-privilege policy for just this key. Save it as pdv-transit.hcl:

path "transit/encrypt/pdv-master" { capabilities = ["update"] }
path "transit/decrypt/pdv-master" { capabilities = ["update"] }
path "transit/rewrap/pdv-master"  { capabilities = ["update"] }

Register the policy and mint a token bound to it:

bao policy write pdv-transit pdv-transit.hcl
bao token create -policy=pdv-transit -no-default-policy -period=768h

The token value that command prints is what goes in settings.php below.

Token lifetime is yours to manage

pdv_vault calls Transit directly and does not renew the token for you. Use a periodic (-period) token and renew it out of band (bao token renew), or a lifetime that matches your operations. For unattended renewal, AppRole authentication is a natural next step.

(For local development, OpenBao dev/auto-init setups often expose a fixed root token; that is fine for a dev stack but never for production.)

2. Point Drupal at the store (settings.php)

The store's address and access token come from settings.php, never from configuration, so the token is not written to the database or exported with config:

$settings['pdv_vault']['address'] = 'https://vault.example.com:8200';
$settings['pdv_vault']['token'] = 'your-secret-store-token';

The environment variables PDV_VAULT_ADDR and PDV_VAULT_TOKEN are read as a fallback, which suits container deployments that inject secrets as env vars.

Reachability is on the crypto path

The store is contacted on every vault read and write. If it is unreachable the operation fails (after a short timeout) rather than exposing data, so treat the store's availability as part of the vault's availability, and use TLS to it in production.

3. Enable the module

drush en pdv_vault

4. Create the Master KEK Key

At Configuration -> System -> Keys (/admin/config/system/keys), add a key:

  • Key type: Vault/OpenBao Transit (PDV Master KEK).
  • Transit key name: the name from step 1, e.g. pdv-master.

This Key holds only the name of the Transit key, no cryptographic material.

5. Select it as the Master KEK

At Configuration -> Personal Data Vault (/admin/config/pdv/settings) the Master KEK select now lists the Transit Key alongside any local 256-bit encryption keys. Select it and save.

From then on, new Subject KEKs are wrapped in the store. Subjects created under the previous Master KEK keep resolving under it (each row records the key that wrapped it) until you re-wrap them: run Vault subjects -> Rotate to current Master KEK (/admin/config/pdv/subjects), exactly as for any Master KEK change in Installation. Cron drains the same backlog.

A single tenant can also point its own Master KEK at a Transit Key, the same way.

Rotating the Transit key

Rotation is a property of the store, and pdv keeps working across it:

  1. Rotate in the store: bao write -f transit/keys/pdv-master/rotate. This adds a new key version; existing ciphertexts keep decrypting against their original version.
  2. To move pdv's stored blobs onto the new version, run Vault subjects -> Rotate to current Master KEK. It calls transit/rewrap server-side, so no Subject KEK plaintext is ever exposed during rotation.

Step 2 is optional unless you raise the store's min_decryption_version to retire old versions, in which case re-wrap first so no blob is left on a version about to be disallowed.

HashiCorp Vault instead of OpenBao

The Transit API is identical on both, and pdv_vault works against either with no code or configuration difference: point address at the Vault server and use a Vault token. Substitute vault for bao in the commands above.