Problem/Motivation

The roadmap's OpenBao item planned a Key provider that fetches the Master KEK from the store into Drupal. A stronger design is to never bring the Master KEK into Drupal at all, using the Transit secrets engine (encryption as a service): the Master KEK is a Transit key created in the store and never exported. Drupal sends a freshly generated Subject KEK to transit/encrypt and stores the returned ciphertext; it sends that ciphertext to transit/decrypt to get the Subject KEK back. A full Drupal compromise then cannot exfiltrate the root key for offline or permanent decryption. The Transit API is identical on OpenBao and HashiCorp Vault, so this works against either.

Proposed resolution

Implemented as a pluggable Master KEK wrap mechanism in pdv core, plus a submodule that implements it against Transit.

pdv core gains a MasterKeyWrap plugin type (attribute, interface, base, manager). The default local plugin keeps today's local-AEAD wrapping. SubjectKeyManager selects the plugin by the Master KEK Key's type (not by a cipher suite), so existing local keys are unchanged and new mechanisms slot in:

  • create: the chosen plugin wraps the new Subject KEK; the returned blob is stored as wrapped_key.
  • unwrap: the plugin recorded on the row unwraps it.
  • rotate: when source and target share a mechanism the plugin's native rewrap is used (for Transit, transit/rewrap, so the Subject KEK plaintext never leaves the store); across mechanisms it unwraps with the source and re-wraps with the target.

The Master KEK selectors (site-wide and per-tenant) offer keys whose type a wrap plugin claims.

pdv_vault submodule adds a vault_transit wrap plugin and a pdv_vault_transit Key type whose value is the Transit key name. It calls the store directly over HTTP, so it needs no extra dependencies. The Transit key is created with derived=true and every call passes the owner identity as the per-call context, giving the same owner-binding as the local AEAD.

Auth. The token only needs encrypt/decrypt/rewrap on the one Transit key (never key export, never reading arbitrary secrets), supplied from settings.php or an environment variable, never DB config.

Trade-offs

  • The Subject KEK plaintext still transits to and from the store on create and unwrap (Drupal needs it to wrap document DEKs); only the Master KEK never leaves, and during rotation the Subject KEK does not either.
  • Every owner-key resolve becomes a store round-trip (latency and an availability dependency), versus fetch-the-master-once-then-local-crypto. The standard KMS/HSM trade-off.

Remaining tasks

  • Review and merge the merge request.

API changes

New MasterKeyWrap plugin type in pdv core. SubjectKeyManager (which is @internal) gains the wrap-plugin manager as a constructor argument. No behaviour change for existing local Master KEKs.

User interface changes

The Master KEK select on the settings and tenant forms now also lists keys whose type a wrap plugin handles. Setup is documented in a new "Master KEK in OpenBao or Vault" handbook page.

Issue fork pdv-3594042

Command icon Show commands

Start within a Git clone of the project using the version control instructions.

Or, if you do not have SSH keys set up on git.drupalcode.org:

Comments

mably created an issue. See original summary.

mably’s picture

Ecosystem survey + OpenBao verification, before writing any pdv code.

Existing modules
- vault (3.0.0, ^9.3 || ^10 || ^11): a generic Vault/OpenBao HTTP client (Guzzle) with pluggable auth and a write() API that reaches any path, including transit/encrypt, transit/decrypt and transit/rewrap. It ships the auth plugin type but no concrete auth plugin; Token and AppRole are separate modules to add.
- encrypt_vault_transit (2.0.0, ^9.3 || ^10 || ^11): an Encrypt-module encryption method calling transit/encrypt and transit/decrypt. encrypt and decrypt only, no rewrap (rewrap is not part of the Encrypt method interface).
- baokey: an OpenBao/Vault KV key provider (the fetch-the-key approach), Drupal 9/10 only.

Verification
- OpenBao compatibility: enabling transit, creating a key, and an encrypt then decrypt round-trip all succeed against a running OpenBao over the standard Vault API. The vault module speaks that same API via Guzzle, so it is protocol-compatible; a full call through it additionally needs one auth-plugin submodule.
- Rewrap: absent from encrypt_vault_transit. Either call the vault client's transit/rewrap directly (server-side; the subject KEK never leaves OpenBao during rotation), or contribute a rewrap operation upstream to encrypt_vault_transit.

Revised design
- Depend on drupal/vault (plus an auth submodule) and call its client directly for transit encrypt/decrypt/rewrap. More capable than the encrypt_vault_transit Encrypt-method wrapper (which lacks rewrap) and avoids pulling in the Encrypt module.
- A master-key-wrap PLUGIN type, so the basic/local path stays first-class alongside OpenBao:
- local (default): the current CryptoService plus the Key-module Master KEK.
- openbao_transit: delegate wrap, unwrap and rewrap to OpenBao via the vault client.
Each pdv_subject_key row's cipher_suite (or a dedicated wrap-method field) selects the plugin, so records stay self-describing and mixed or migrated deployments work.
- Token scope stays narrow: transit encrypt/decrypt/rewrap on the one key, never key export.

Upstream follow-up: push a rewrap operation to encrypt_vault_transit so its Transit method also supports key rotation (tracked separately).

mably’s picture

Issue summary: View changes
mably’s picture

Status: Active » Needs review

  • mably committed 3d6c2432 on 1.x
    feat: #3594042 OpenBao Transit-backed Master KEK: never export the root...
mably’s picture

Status: Needs review » Fixed

Now that this issue is closed, review the contribution record.

As a contributor, attribute any organization that helped you, or if you volunteered your own time.

Maintainers, credit people who helped resolve this issue.

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.