This is the first article in a series showcasing the most important new features introduced by Symfony 8.2, which will be released at the end of November 2026.
Many applications need to encrypt their own data: database columns with personal information, uploaded documents, API tokens, etc. It's common for organizations to keep the keys for that in a managed Key Management System (KMS) such as AWS KMS, Azure Key Vault, Google Cloud KMS or HashiCorp Vault. In those systems, the master key never leaves the provider and every use of it is audited. The downside is that each provider ships its own SDK and encryption process.
Symfony 8.2 introduces the new KeyManagement component to abstract all of them
behind a single API. The core package (symfony/key-management) provides the
interfaces, the envelope encryption and local backends based on libsodium and
OpenSSL for development and tests. Each remote provider lives in its own bridge
package: symfony/aws-key-management, symfony/azure-keyvault-key-management,
symfony/google-cloud-key-management and symfony/hashicorp-vault-key-management.
The component is experimental, so its API may still change in minor versions.
Basic Usage
A KMS client encrypts, decrypts and generates data keys, and you can use it in
two ways. Direct mode sends the payload to the KMS and gets the ciphertext
back, so it's limited to small values (4 KB on AWS KMS) such as tokens.
Envelope mode asks the KMS for a fresh data key, encrypts the payload locally
with AES-256-GCM and stores that data key, wrapped by the master key, inside the
resulting Envelope. The data never leaves your application and it can be of
any size:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
use Symfony\Component\KeyManagement\Envelope;
use Symfony\Component\KeyManagement\EnvelopeEncrypter;
use Symfony\Component\KeyManagement\KeyLoader\InMemoryKeyLoader;
use Symfony\Component\KeyManagement\Local\SodiumKms;
// a local libsodium backend; in production, use one of the cloud bridges
$kms = new SodiumKms(new InMemoryKeyLoader([
'app-key' => sodium_crypto_aead_xchacha20poly1305_ietf_keygen(),
]));
// direct mode: short payloads, the KMS sees the plaintext
$ciphertext = $kms->encrypt('app-key', $apiToken);
$apiToken = $kms->decrypt($ciphertext);
// envelope mode: any size, the KMS only sees the wrapped data key
$encrypter = new EnvelopeEncrypter($kms);
$envelope = $encrypter->encrypt('app-key', $fileContents);
file_put_contents($path, $envelope);
$fileContents = $encrypter->decrypt(Envelope::fromBytes(file_get_contents($path)));
Your code never handles the master key itself, only its identifier (an alias, an ARN, a key URL, etc.) and the same envelope decrypts identically no matter which backend produced it, as long as that master key is reachable.
Symfony Integration
In Symfony applications, define one or more clients with DSNs under the new
key_management configuration option:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
# config/packages/key_management.yaml
key_management:
clients:
# e.g. aws-kms://default?region=eu-west-1
aws: '%env(AWS_KMS_DSN)%'
# e.g. hashicorp-vault-transit://TOKEN@vault.example.com:8200
vault: '%env(VAULT_KMS_DSN)%'
default_client: aws
when@dev:
# keys are inlined, base64url-encoded, so no external service is needed
key_management:
clients:
aws: 'sodium://?keys[app-key]=%env(DEV_KMS_KEY)%'
vault: 'sodium://?keys[app-key]=%env(DEV_KMS_KEY)%'
Then, inject the EnvelopeEncrypterInterface (or EncrypterInterface and
DecrypterInterface for the direct mode) in your services. Use the #[Target]
attribute to pick a client other than the default one:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
use Symfony\Component\DependencyInjection\Attribute\Target;
use Symfony\Component\KeyManagement\EnvelopeEncrypterInterface;
class DocumentStorage
{
public function __construct(
private EnvelopeEncrypterInterface $encrypter,
#[Target('vault')]
private EnvelopeEncrypterInterface $vaultEncrypter,
) {
}
// ...
}
The component also adds four console commands:
1 2 3 4
$ php bin/console key-management:encrypt
$ php bin/console key-management:decrypt
$ php bin/console key-management:generate-data-key
$ php bin/console key-management:rewrap-data-keys
The first two read STDIN and write to STDOUT, so you can pipe them to re-encrypt
an envelope under another key. In the dev environment, a new panel in the
web debug toolbar shows the calls made to each KMS client.
Encrypting Doctrine Entity Contents
Two extra bridges integrate the component with Doctrine:
symfony/doctrine-dbal-key-management provides an EncryptedType that
decorates any Doctrine type with column-level envelope encryption, and
symfony/doctrine-orm-key-management adds a #[BlindIndexed] attribute to
make those columns searchable:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\KeyManagement\BlindIndex\Email;
use Symfony\Component\KeyManagement\Bridge\DoctrineOrm\Attribute\BlindIndexed;
#[ORM\Entity]
class User
{
#[ORM\Column(type: 'encrypted_string')]
private string $email = '';
#[ORM\Column(length: 64)]
#[BlindIndexed('email', Email::class)]
private string $emailIndex = '';
// ...
}
// in a repository, look the row up by the digest of the value
$user = $this->findOneBy(['emailIndex' => $this->emailBlindIndex->of($email)]);
The blind index is needed because encryption is randomized: two rows with the
same value store different ciphertexts, so WHERE email = ? never matches. The
index column stores a keyed digest of the value instead, and the ORM bridge fills
it automatically on every flush. The Email index is a service that needs a
KMS client and a wrapped key created with key-management:generate-data-key.
The encrypted_string type name is yours to declare: list the type names in
an EncryptedTypes service (each one mapped to the Doctrine type it wraps and
to the key it encrypts under) and call its register() method from the
kernel's boot() method, so the types exist before the first query.
The DBAL bridge can also keep the data keys in a database table (see the
key_management.store option) so each encrypted row carries a 16-byte reference
instead of a full wrapped key, and the KMS is called once per data key instead
of once per row. This is what makes the key-management:rewrap-data-keys
command possible: moving your data to another master key or even another
provider only rewraps the stored keys, without reading or rewriting a single
encrypted row.
Read the KeyManagement documentation to learn about the available DSNs, the key loaders, data key rotation and the other options of this new component.