Pourquoi (et comment) chiffrer vos données avant de les envoyer au SGBD

Lead Dev
PHP Symfony

Avant de parler Zero Trust

Mai à août 2026
Le système d'information de la DGFiP
Près de 353 000 particuliers
Identifiant fiscal, état civil, adresse, téléphone, e-mail, revenu fiscal de référence…
Sources : impots.gouv.fr - cyber.gouv.fr (rapport ANSSI)
Par mois, d'oct. 2025 à sept. 2026. Source : frenchbreaches.com
24 M
IBAN, coordonnées
Oct. 2024
?
Identité, permis, assurance
Janv. 2026
243 k
Identité, adresse, téléphone
Mars 2026
~35 M
N° de sécurité sociale, parcours de soins
Avr. 2026
Sources : frenchbreaches.com - bonjourlafuite.eu.org - cnil.fr - cyberveille.esante.gouv.fr

Repousser ce jour
Les fondamentaux
Réduire la confiance implicite
Uniquement les ressources utiles
Moindre privilège
Quelle que soit l'origine de la demande
Comportement, horaire, lieu
Réévalué régulièrement
1994
Définition du concept (thèse)
2001
Open Source Security Testing Methodology Manual
« Trust is a Vulnerability »
2004
Débat sur le principe
2018
Zero Trust Architecture
2021
Avis scientifique et technique
Sources : fr.wikipedia.org - nist.gov - cyber.gouv.fr
Identité
MFA
HSM
Réauthentification
Équipements
Mises à jour
EDR
Réseaux
Port fermé
Cloisonnement
Chiffrement
Applications
Proxy
Reverse proxy
Firewall applicatif
Données
Contrôle d'accès
Chiffrement
Identité
Limitation des tentatives
Utilisateur rechargé/requête
Équipements
Hors scope
Réseaux
access_control : IP, hôte
HTTPS
Applications
Voters : utilisateur/ressource
Hiérarchie de rôles
Données
Coffre de secrets
KeyManagement
Sources : security - access_token - access_control - voters - secrets
Aujourd'hui, on se concentre sur la donnée
Qui se connecte, d'où, avec quels droits
Le SGBD voit tout en clair. DBA, dump, sauvegarde, hébergeur.

Même votre SGBD n'a pas à tout voir
Chiffrer les données
Concrètement
Le SGBD ne voit que du chiffré.
Application compromise : la clé est en mémoire.
L'écoute du réseau.
À l'arrivée, la donnée est en clair dans le SGBD.
Le disque volé, le serveur mis au rebut.
Le SGBD déchiffre pour tout accès SQL : DBA, injection, dump.
La donnée et la clé voyagent en clair jusqu'au serveur.
Trust both system and database administrator. […] If you cannot, then better do crypto inside client application.
Source : documentation PostgreSQL, pgcrypto, Security Limitations
Un Type Custom Doctrine par champ
Chiffrement et clés à gérer
Un nouveau composant, sortie fin novembre 2026.
Gérer/Communiquer avec vos KMS
Flag expérimental (Amené à évoluer)
composer require symfony/key-management:^8.2@dev
Key Management System : gérer vos clés de chiffrement
Interne au KMS
Partageable par le KMS pour vos besoins
Sources : Wikipédia, Key management
symfony/aws-key-management:^8.2@dev
symfony/azure-keyvault-key-management:^8.2@dev
symfony/google-cloud-key-management:^8.2@dev
symfony/hashicorp-vault-key-management:^8.2@dev
En local : Sodium ou OpenSSL.
Application
KMS
PostgreSQL
Une clé de données, et sa copie chiffrée par le KMS
L'application récupère les deux
L'application chiffre la donnée, en local
La clé en clair est effacée
Donnée chiffrée + clé chiffrée : l'enveloppe
L'enveloppe part en base
Application
KMS
PostgreSQL
L'application relit l'enveloppe en base
Elle envoie la clé chiffrée au KMS
Le KMS la déchiffre avec la clé principale
Et renvoie la clé en clair à l'application
L'application déchiffre la donnée, en local
La clé en clair est de nouveau effacée
L'enveloppe est retirée : il ne reste que la donnée en clair
Le KMS n'a vu que la clé, jamais la donnée
# 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: vault
when@dev:
key_management:
clients:
aws: 'sodium://?keys[app-key]=%env(DEV_KMS_KEY)%'
vault: 'sodium://?keys[app-key]=%env(DEV_KMS_KEY)%'
# config/services.yaml
services:
app.encrypted_types:
class: Symfony\Component\KeyManagement\Bridge\DoctrineDbal\EncryptedTypes
public: true
arguments:
$envelopes: '@key_management.envelope_encrypter.aws'
$types:
encrypted_string: { type: string, key: 'app-key' }
// src/Kernel.php
public function boot(): void
{
parent::boot();
$this->container->get('app.encrypted_types')->register();
}
#[ORM\Entity]
class User
{
#[ORM\Column(type: 'encrypted_string')]
private string $email = '';
}
Le reste de l'application ne change pas.
En base : une colonne binaire (bytea), plus longue que la valeur.
$repository->findOneBy(['email' => $email]);
null
Le chiffrement est non déterministe
Le vrai problème commence ici
WHERE email = ? ne trouve rien
$encrypter->encrypt('app-key', 'jane@example.com'); // AQf3…9Qk=
$encrypter->encrypt('app-key', 'jane@example.com'); // Zt8x…M2w=
Même valeur, deux chiffrés différents

Même valeur, même chiffré
WHERE = possible
Chiffré aléatoire + une empreinte à côté
WHERE = sur l'empreinte
Changement de clé, change seulement data
En déterministe : nouvelle clé, tout est à rechiffrer.
Équivalents de LIKE
1 besoin = 1 index
Application
PostgreSQL
On enregistre jane@example.com.
Chiffrée, elle part dans email : illisible.
À côté, son empreinte, calculée avec la clé d'index.
Plus tard, on cherche jane@example.com.
Même valeur, même clé : même empreinte.
PostgreSQL compare les empreintes : ligne trouvée.
La base n'a jamais vu l'email en clair.
#[ORM\Entity]
class User
{
#[ORM\Column(type: 'encrypted_string')]
private string $email = '';
#[ORM\Column(length: 64)]
#[BlindIndexed('email', Email::class)]
private string $emailIndex = '';
}
L'empreinte est calculée à chaque flush
// dans UserRepository
$this->findOneBy(['emailIndex' => $this->emailBlindIndex->of($email)]);
Un User
La recherche reste explicite : on cherche l'empreinte, pas la valeur
" Jane@ExAmPle.COM " → "Jane@example.com"
Le BlindedIndex Email normalise la valeur
// tous les comptes d'un domaine
#[BlindIndexed('email', EmailDomain::class)]
private string $emailDomainIndex = '';
EmailDomainUne empreinte par domaine
Étendre BlindIndex, redéfinir project()
// "06 12 34 56 78", "+33 6 12-34-56-78" → "+33612345678"
final class Phone extends BlindIndex
{
protected function project(#[\SensitiveParameter] string $value): string
{
$util = PhoneNumberUtil::getInstance();
return $util->format(
$util->parse($value, 'FR'),
PhoneNumberFormat::E164,
);
}
}
Usage : giggsey/libphonenumber-for-php
// "+33 6 12 34 56 78" → "FR" ; la recherche "fr" → "FR"
final class PhoneCountry extends BlindIndex
{
protected function project(#[\SensitiveParameter] string $value): string
{
$util = PhoneNumberUtil::getInstance();
return $util->getRegionCodeForNumber(
$util->parse($value, 'FR')
) ?? '';
}
}
#[ORM\Column(type: 'encrypted_string')]
private string $phone = '';
#[ORM\Column(length: 64)]
#[BlindIndexed('phone', Phone::class)]
private string $phoneIndex = '';
#[ORM\Column(length: 64)]
#[BlindIndexed('phone', PhoneCountry::class)]
private string $phoneCountryIndex = '';
Le BlindedIndex repose sur l'ORM
Une écriture niveau DBAL : l'index n'est pas recalculé
Même valeur, même empreinte : attaque par fréquence
E-mail, numéro de compte
Beaucoup de valeurs différentes
Statut, genre
Retrouvés par analyse de fréquence
Et les autres langages ?
| Chiffrement déterministe | Non déterministe | Blind index | Recherche possible | Natif à l'outil | |
|---|---|---|---|---|---|
| PHP Symfony 8.2 | ✗ | ✓ | ✓ | ✓ égalité | ✓ |
| Java Spring / Hibernate | À écrire avec jakartaee/persistence | ✗ | |||
| TS/JS Prisma | ✗ | ✓ | ✓ | ✓ égalité | ✗ |
| Python Django | ✗ | ✓ | ✗ | ✗ | ✗ |
Queryable Encryption
#[ODM\Field(type: 'string')]
#[ODM\Encrypt(queryType: EncryptQuery::Equality)]
public string $ssn;
Natif dans Doctrine ODM depuis la 2.12
MongoDB Enterprise 7.0+ & Atlas
Sources : documentation MongoDB - Doctrine ODM
L'essentiel
Une application compromise.
Clé principale perdue ou désactivée : données illisibles.
Un client composite
key_management:
clients:
aws: '%env(AWS_KMS_DSN)%'
vault: '%env(VAULT_KMS_DSN)%'
both:
members: { aws: ~, vault: ~ }
default_client: both
L'API peut changer
Symfony 8.2 sort fin novembre 2026
