Search by

symfony / hashicorp-vault-key-management

fabpot

Symfony HashiCorp Vault Key Management Bridge

Package info

github.com/symfony/hashicorp-vault-key-management

Homepage

Type:symfony-key-management-bridge

pkg:composer/symfony/hashicorp-vault-key-management

Statistics

Installs: 0

Dependents: 0

Suggesters: 0

Stars: 0

8.2.x-dev 2026-09-14 08:33 UTC

This package is auto-updated.

Last update: 2026-09-14 08:45:52 UTC


README

Provides an implementation of Symfony\Component\KeyManagement\EncrypterInterface and Symfony\Component\KeyManagement\DataKeyGeneratorInterface backed by the HashiCorp Vault Transit secret engine. Encryption, decryption and data-key generation never expose the master key: Vault performs them server-side.

This Bridge is experimental. Experimental features are not covered by Symfony's Backward Compatibility Promise.

use Symfony\Component\HttpClient\HttpClient;
use Symfony\Component\KeyManagement\Bridge\HashiCorpVault\TransitKms;

$kms = new TransitKms(
    HttpClient::createForBaseUri('https://vault.example.com:8200/v1/'),
    $_SERVER['VAULT_TOKEN'],
);

$ciphertext = $kms->encrypt('app-key', 'hello world');
// → Ciphertext blob looks like "vault:v1:..."

$plaintext = $kms->decrypt($ciphertext);

$dataKey = $kms->generateDataKey('app-key', 32);
$result = $dataKey->use(fn (string $dek): string => /* local AEAD encrypt */);

DSN scheme

hashicorp-vault-transit://<token>@<host>[:<port>][/<path>][?mount=<mount>&namespace=<ns>]

The HTTP base URI is built from <host>[:<port>]<path>; if <path> is empty it defaults to /v1/. The Vault token is taken from the user component of the DSN. Default mount point is transit. The namespace option maps to Vault's X-Vault-Namespace header (Vault Enterprise multi-tenancy).

Example:

hashicorp-vault-transit://s.token@vault.example.com:8200/v1/?mount=transit&namespace=tenant-acme

AAD support

The $aad argument is forwarded as Vault's context parameter (HKDF context, base64-encoded). This works for keys created with derived=true. Vault does not reject a context on non-derived keys, it silently ignores it, which would make an AAD mismatch decrypt fine. The bridge therefore reads the key configuration once per key (GET <mount>/keys/<name>) when a non-empty $aad is used and refuses AAD for non-derived keys with an UnsupportedOperationException.

That preflight is a read on the key itself, so a token whose policy only grants update on the encryption endpoints is no longer enough as soon as an AAD is passed: the request fails with a RuntimeException reporting HTTP 403. Grant read on <mount>/keys/* too:

path "transit/encrypt/*"           { capabilities = ["update"] }
path "transit/decrypt/*"           { capabilities = ["update"] }
path "transit/datakey/plaintext/*" { capabilities = ["update"] }
# only needed when AAD is used, to tell derived keys from non-derived ones
path "transit/keys/*"              { capabilities = ["read"] }

Note that Vault's context is a key-derivation input, not authenticated data in the AEAD sense. The practical effect is similar (different context → unable to decrypt) but the security model differs slightly from backends like AWS KMS, where EncryptionContext is integrity-protected. AAD is treated as opaque bytes; if you have structured data, serialize it (e.g. canonical JSON with sorted keys) yourself and pass the resulting string.

Resources