Aller au contenu principal
Retour au blog

Symfony 8.2 KeyManagement : chiffrer tes données avec un KMS

Symfony 8.2 ajoute KeyManagement : une API unique pour AWS KMS, Azure, Google et Vault, chiffrement enveloppe et colonnes Doctrine chiffrées et cherchables.

Flavien Métivier · 11 min de lecture
Sommaire · 7 parties

Un KMS (Key Management System) est un service managé qui garde la clé maître chez le fournisseur : elle n'en sort jamais et chaque usage est audité. Le composant KeyManagement de Symfony 8.2 expose une API unique pour chiffrer des données avec AWS KMS, Azure Key Vault, Google Cloud KMS ou HashiCorp Vault. Présenté le 15 septembre 2026 sur le blog officiel, contribution de Florent Morselli (PR #64052), il arrive avec Symfony 8.2, dont la sortie est annoncée pour fin novembre 2026. Il est expérimental.

Ta base de données contient des emails, des pièces jointes, des tokens d'API tiers. Le chiffrement du disque et celui de la base protègent contre le vol d'un volume ou d'un dump. Ils ne protègent pas contre une injection SQL, un export mal filtré ou un accès admin trop large. Ces cas-là demandent de chiffrer dans l'application, avant l'écriture. Beaucoup d'équipes le font déjà avec une clé dans une variable d'environnement et un openssl_encrypt maison ; les plus rigoureuses passent par un KMS, au prix d'un SDK et d'un schéma de chiffrement différents par fournisseur.

Précision utile : le chiffrement applicatif réduit une surface d'exposition, il ne rend pas une application « conforme RGPD » à lui seul. Il s'inscrit dans un durcissement plus large, comme celui de notre checklist OWASP pour sécuriser une application Symfony. Et le choix d'un fournisseur de KMS relève des mêmes arbitrages que ceux décrits dans notre article sur la souveraineté et la conformité RGPD.

Chiffrement direct ou enveloppe : quel mode choisir ?

Un client KMS sait chiffrer, déchiffrer et générer des clés de données. Tu l’utilises de deux façons :

  • Mode direct : le payload part au KMS, qui renvoie le chiffré. Le fournisseur voit donc la donnée en clair, et la taille est limitée (4 Ko sur AWS KMS). Adapté à un token ou à un secret court.
  • Mode enveloppe : le KMS fournit une clé de données fraîche, l’application chiffre localement en AES-256-GCM, puis range cette clé, enveloppée par la clé maître, dans l’objet Envelope. La donnée ne quitte jamais ton application, quelle que soit sa taille.

Le chiffrement enveloppe consiste donc à chiffrer la donnée localement avec une clé de données, puis à faire encapsuler cette clé par la clé maître du KMS. Ton code ne manipule jamais la clé maître, seulement son identifiant (alias, ARN, URL de clé). Voici les deux modes avec le backend local libsodium, à remplacer en production par un pont cloud :

php
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)));

Le découpage en paquets suit la même logique : symfony/key-management fournit les interfaces, l'enveloppe et des backends locaux (libsodium, OpenSSL) pensés pour le dev et les tests. Chaque fournisseur vit dans son propre pont, et tu n'installes que ce que tu utilises :

  • symfony/aws-key-management pour AWS KMS
  • symfony/azure-keyvault-key-management pour Azure Key Vault
  • symfony/google-cloud-key-management pour Google Cloud KMS
  • symfony/hashicorp-vault-key-management pour HashiCorp Vault
  • symfony/doctrine-dbal-key-management et symfony/doctrine-orm-key-management pour Doctrine

Configurer KeyManagement dans Symfony : DSN, clients et #[Target]

Dans une application Symfony, tu déclares tes clients par DSN sous la clé key_management. L'astuce utile au quotidien est le bloc when@dev : chaque client cloud est remplacé par un backend libsodium local sous le même nom. Le code et les tests ne changent pas, et personne n'a besoin d'un compte cloud pour développer.

yaml
# 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)%'

Revers de la médaille : un client « sodium » en dev ne reproduit ni la latence ni les permissions IAM de la production. Prévois un test d'intégration contre le vrai fournisseur en préproduction. Côté code, tu injectes EnvelopeEncrypterInterface (ou EncrypterInterface et DecrypterInterface pour le mode direct), avec #[Target] pour viser un client autre que celui par défaut :

php
use Symfony\Component\DependencyInjection\Attribute\Target;
use Symfony\Component\KeyManagement\EnvelopeEncrypterInterface;

class DocumentStorage
{
    public function __construct(
        private EnvelopeEncrypterInterface $encrypter,
        #[Target('vault')]
        private EnvelopeEncrypterInterface $vaultEncrypter,
    ) {
    }

    // ...
}

Les quatre commandes console key-management

bash
$ 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
  • encrypt et decrypt lisent STDIN et écrivent sur STDOUT : tu peux les chaîner pour rechiffrer une enveloppe sous une autre clé.
  • generate-data-key affiche, selon la PR, la clé en clair et enveloppée. À ne jamais lancer dans un job CI dont les logs sont conservés.
  • rewrap-data-keys réenveloppe les clés de données stockées en base (voir plus bas).
  • En dev, un panneau de la web debug toolbar liste les appels faits à chaque client KMS : tu repères vite un chiffrement par ligne là où tu attendais un appel par lot.

Chiffrer une colonne Doctrine et la rendre cherchable avec un blind index

C'est là que le composant devient concret pour une application métier. symfony/doctrine-dbal-key-management fournit un EncryptedType qui enveloppe n'importe quel type Doctrine avec un chiffrement par enveloppe au niveau de la colonne. symfony/doctrine-orm-key-management ajoute l'attribut #[BlindIndexed].

Votre dette technique s'accumule ?

Audit complet en 10 jours. Recommandations priorisées et actionnables.

Calculer mon audit →

Le problème que règle ce second pont : le chiffrement est randomisé. Deux lignes avec la même valeur stockent deux chiffrés différents, donc WHERE email = ? ne renvoie jamais rien. Un blind index est une colonne qui stocke un digest à clé de la valeur, ce qui permet de rechercher une donnée chiffrée sans la déchiffrer. Le pont ORM la remplit à chaque flush :

php
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)]);

Trois points d’intégration à anticiper avant de copier l’exemple :

  • Le nom de type encrypted_string est à toi. Tu le déclares dans un service EncryptedTypes, qui associe chaque nom au type Doctrine enveloppé et à sa clé, puis tu appelles register() depuis le boot() du kernel pour que les types existent avant la première requête.
  • Le listener du pont ORM ne couvre que l’écriture : pour une recherche, tu appelles of() toi-même, comme dans l’exemple.
  • Le service d’index Email a besoin d’un client KMS et d’une clé enveloppée créée avec key-management:generate-data-key.

La PR explique pourquoi un attribut #[Encrypted] initialement prévu a été retiré : il enregistrait les types dans un listener de métadonnées qui ne se déclenche pas quand les métadonnées viennent du cache, comme c'est le cas en production. D'où l'enregistrement explicite dans boot().

Rotation des clés : changer de clé maître ou de fournisseur sans réécrire les lignes

Le pont DBAL peut conserver les clés de données dans une table, via l'option key_management.store. Chaque ligne chiffrée porte alors une référence de 16 octets au lieu d'une clé enveloppée complète, et le KMS est appelé une fois par clé de données plutôt qu'une fois par ligne. Configuration minimale, donnée par la PR :

yaml
key_management:
  clients:
    aws: '%env(KMS_DSN)%'
  store:
    client: aws
    key_id: 'alias/app-key'

C'est ce qui rend key-management:rewrap-data-keys intéressant : migrer vers une autre clé maître, voire un autre fournisseur, réenveloppe uniquement les clés stockées. Aucune ligne chiffrée n'est lue ni réécrite. Selon la PR, les lignes sont commitées une par une (une exécution interrompue se reprend en relançant la commande), et --dry-run montre la portée. Sans store, chaque valeur porte sa propre clé enveloppée, et changer de clé maître revient à rechiffrer tout le jeu de données.

Le store a aussi un effet direct sur les coûts. Sans lui, chaque lecture ou écriture d'une valeur chiffrée coûte un aller-retour réseau vers le KMS, avec la latence et la facturation par appel qui vont avec. Pour une colonne à fort volume, pars directement sur le store.

Les limites à regarder avant de décider

  • Statut expérimental : l’API peut changer dans une version mineure, hors promesse de rétrocompatibilité. La PR place d’ailleurs les ponts Doctrine dans des paquets séparés pour ne pas exposer le bridge Doctrine stable à une API encore mouvante.
  • Prérequis : d’après Packagist, PHP 8.4.1 minimum et les composants config/DI en 8.2.
  • Documentation : la PR de documentation n’est pas encore mergée. Les loaders de clés et le détail de la rotation se lisent surtout dans le code.
  • Blind index : recherche à égalité exacte uniquement, et l’index révèle quelles lignes partagent une même valeur. N’indexe que des valeurs à forte entropie ; un statut ou un pays n’a rien à faire là.
  • Backends locaux : sodium:// est fait pour le dev et les tests, pas pour la production.

Un mot sur le positionnement : KeyManagement ne remplace pas secrets:*. Le vault Symfony sert à livrer des secrets de configuration (DSN, mots de passe) à l'application ; KeyManagement chiffre des données applicatives avec des clés dont la matière ne quitte pas le fournisseur. Les deux cohabitent. Pour l'essayer avant la sortie, Jérôme Tamarelle indique sous le billet : composer require symfony/key-management:^8.2@dev.

Faut-il l’adopter dès Symfony 8.2 ?

Le composant règle le bon problème : ton code dépend d'une interface et d'un identifiant de clé, pas d'un SDK cloud, et la rotation par réenveloppe supprime le chantier de rechiffrement massif qui fait reculer les équipes. Je ne le mettrais pourtant pas, dès la sortie, sur toutes les colonnes de ton modèle. L'ordre que je suivrais :

  • Commence par ce que tu ne cherches jamais : documents, tokens, pièces jointes, en mode enveloppe. Gain de sécurité net, sans blind index.
  • Isole l’expérimental derrière ta propre interface, par exemple un DocumentVault. Si l’API bouge en 8.3, tu corriges un adaptateur, pas cinquante services.
  • Réserve le blind index aux valeurs à forte entropie et décide champ par champ si la fuite de fréquence est acceptable.
  • Teste la rotation dès le départ : active le store, lance rewrap-data-keys --dry-run sur un jeu de recette, et mesure.

Le chiffrement ne remplace ni le contrôle d'accès ni une authentification solide : sur ce point, voir notre article sur la sécurité de l'authentification Symfony en 2026. Il ajoute une couche, et KeyManagement la rend enfin raisonnable à maintenir.

Avant de brancher un KMS, encore faut-il savoir ce qu'il doit protéger. Un Bear Scan examine en 10 jours ton code et tes dépendances, y compris l'exposition des données et la gestion des secrets, et te rend un rapport priorisé : tu sais quelles données chiffrer en premier. À toi ensuite de décider si ce chiffrement passe par ce composant, une fois stabilisé.

Cet article vous a été utile ? Partagez-le.

Portrait de Flavien Métivier, fondateur de The Bearded Bear : barbe longue, lunettes, sur fond sombre

Écrit par

Flavien Métivier

Expert PHP/Symfony indépendant depuis 2014, ancien CTO de Kartable et d'Extrabat. Plus de 80 projets menés, de l'audit à la migration.

Mon parcours

À lire ensuite

Votre dette technique s'accumule ?

Audit complet en 10 jours. Recommandations priorisées et actionnables.

Newsletter Bear Bytes

Veille Claude Code, Symfony et PHP, retours d'expérience terrain et conseils techniques, directement dans votre boîte mail.