Zero Trust

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

Benjamin Georgeault

Benjamin Georgeault

Lead Dev
PHP Symfony

Avant de parler Zero Trust

Contexte

Meme Bob l'éponge terrifié sous sa couverture

Le fisc

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)

La tendance ne faiblit pas

Par mois, d'oct. 2025 à sept. 2026. Source : frenchbreaches.com

Quelques exemples récents

24 M

Free

IBAN, coordonnées

Oct. 2024

?

Fédération Nationale des Chasseurs

Identité, permis, assurance

Janv. 2026

243 k

Éducation Nationale

Identité, adresse, téléphone

Mars 2026

~35 M

Système de santé (ARS, hôpitaux)

N° de sécurité sociale, parcours de soins

Avr. 2026

Sources : frenchbreaches.com - bonjourlafuite.eu.org - cnil.fr - cyberveille.esante.gouv.fr

Meme This is fine : un chien boit son café dans une pièce en feu

Pas si, mais quand

Repousser ce jour

Les fondamentaux

Le modèle Zero Trust

L'objectif du modèle

Réduire la confiance implicite

Au plus juste

Uniquement les ressources utiles
Moindre privilège

Sans distinction

Quelle que soit l'origine de la demande

Selon le contexte

Comportement, horaire, lieu
Réévalué régulièrement

Toujours vérifier
Ne jamais faire confiance

Historique

1994

Stephen Paul Marsh

Définition du concept (thèse)

2001

OSSTMM

Open Source Security Testing Methodology Manual
« Trust is a Vulnerability »

2004

Jericho Forum

Débat sur le principe

2009

Google BeyondCorp

Implémentation du modèle

2018

NIST SP 800-207

Zero Trust Architecture

2021

ANSSI

Avis scientifique et technique

Sources : fr.wikipedia.org - nist.gov - cyber.gouv.fr

Les 5 piliers du Zero Trust

Identité Équipements Réseaux Applications Données

Les mécanismes par pilier

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

Plus de détails

ANSSI

Modèle Zero Trust : les fondamentaux

NIST

SP 800-207, Zero Trust Architecture

Et Symfony ?

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

La base de données

Aujourd'hui, on se concentre sur la donnée

Ce qu'on vérifie

Qui se connecte, d'où, avec quels droits

La confiance

Le SGBD voit tout en clair. DBA, dump, sauvegarde, hébergeur.

Meme Trust me bro

Zero Trust, appliqué à la donnée

Même votre SGBD n'a pas à tout voir

Chiffrer les données

Concrètement

PHP, Symfony, PostgreSQL

Chiffrer, oui, mais où ?

ApplicationSymfony + DoctrineChiffrementTLSINSERT INTO "user" (email)VALUES ('jane@example.com')VALUES ('AQf3…9Qk=')Serveur PostgreSQLSGBDjane@example.comAQf3…9Qk=DisqueChiffré

Côté application

Le SGBD ne voit que du chiffré.

Application compromise : la clé est en mémoire.

En transit

L'écoute du réseau.

À l'arrivée, la donnée est en clair dans le SGBD.

Au repos

Le disque volé, le serveur mis au rebut.

Le SGBD déchiffre pour tout accès SQL : DBA, injection, dump.

pgcrypto ?

Chiffrement Serveur PostgreSQL

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

Où intercepter la donnée ?

Application PHPSymfonyDoctrineDoctrineUn seul point à intercepterSQLServeur PostgreSQLSGBDDisque

Un Type Custom Doctrine par champ

Chiffrement et clés à gérer

Symfony 8.2 : KeyManagement

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
    

Un KMS, c'est quoi ?

Key Management System : gérer vos clés de chiffrement

Création Stockage Utilisation Rotation Destruction

Clé principale

Interne au KMS

Clés de données

Partageable par le KMS pour vos besoins

Sources : Wikipédia, Key management

Les KMS compatibles

AWS KMS


symfony/aws-key-management:^8.2@dev
            

Azure Key Vault


symfony/azure-keyvault-key-management:^8.2@dev
            

Google Cloud KMS


symfony/google-cloud-key-management:^8.2@dev
            

HashiCorp Vault


symfony/hashicorp-vault-key-management:^8.2@dev
            

Chiffrement par enveloppe

Application

KMS

PostgreSQL

Clé principale
Enveloppe
jane@example.com
Clé chiffrée
Clé de données

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

Déchiffrer

Application

KMS

PostgreSQL

Clé principale
Enveloppe
AQf3…9Qk=
Clé chiffrée
Clé de données

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

Configurer vos KMS


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

Déclarer le type chiffré


# 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();
}
    

Et dans l'entité


#[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.

Cherchons un utilisateur


$repository->findOneBy(['email' => $email]);
    

null

Le chiffrement est non déterministe

Le vrai problème commence ici

Le dilemme de la recherche

Pourquoi 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

Meme des deux boutons : un personnage en sueur hésite entre deux boutons rouges

Solutions ?

Déterministe

Même valeur, même chiffré

WHERE = possible

Blind index

Chiffré aléatoire + une empreinte à côté

WHERE = sur l'empreinte

Pourquoi le blind index ?

Rotation clé indépendante

Changement de clé, change seulement data

En déterministe : nouvelle clé, tout est à rechiffrer.

Selon le besoin métier

Équivalents de LIKE

1 besoin = 1 index

Le blind index

Application

PostgreSQL

email_index
a91c…04e
3be0…f17
Clé d'index
jane@example.com
jane@example.com
7f3a…c21
7f3a…c21

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.

Dans l'entité


#[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

Cherchons à nouveau


// dans UserRepository
$this->findOneBy(['emailIndex' => $this->emailBlindIndex->of($email)]);
    

Un User

La recherche reste explicite : on cherche l'empreinte, pas la valeur

Même valeur ?


" Jane@ExAmPle.COM "  →  "Jane@example.com"
    

Le BlindedIndex Email normalise la valeur

Chercher sur une partie ?


// tous les comptes d'un domaine
#[BlindIndexed('email', EmailDomain::class)]
private string $emailDomainIndex = '';
    

L'index EmailDomain

ValeurGarder le domaineen minusculesHashclé d'indexemail_domain_indexjane@example.comexample.com5c2e…a90John@EXAMPLE.comexample.com5c2e…a90Rechercheexample.comexample.com5c2e…a90Même empreinte

Une empreinte par domaine

Votre propre index : le téléphone

É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

Et par pays ?


// "+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')
        ) ?? '';
    }
}
    

Deux index, une colonne


#[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 = '';
    

Attention aux raccourcis

Le BlindedIndex repose sur l'ORM

Une écriture niveau DBAL : l'index n'est pas recalculé

Que chiffrer avec recherche ?

Même valeur, même empreinte : attaque par fréquence

Bon candidat

E-mail, numéro de compte

Beaucoup de valeurs différentes

Mauvais candidat

Statut, genre

Retrouvés par analyse de fréquence

Et les autres langages ?

État de l'art

Et ailleurs ?

Chiffrement déterministeNon déterministeBlind indexRecherche possibleNatif à l'outil
PHP
Symfony 8.2
✗✓✓✓ égalité✓
Java
Spring / Hibernate
À écrire avec jakartaee/persistence✗
TS/JS
Prisma
✗✓✓✓ égalité✗
Python
Django
✗✓✗✗✗

Les outils

Et en NoSQL : MongoDB

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

Ce qu'il faut en retenir

Ce que ça ne protège pas

Une application compromise.

Perdre la clé principale

Clé principale perdue ou désactivée : données illisibles.

Plusieurs KMS

Un client composite


key_management:
    clients:
        aws: '%env(AWS_KMS_DSN)%'
        vault: '%env(VAULT_KMS_DSN)%'
        both:
            members: { aws: ~, vault: ~ }
    default_client: both
    

Chiffrer une base existante

  • Nouvelle colonne binaire
  • Reprendre chaque ligne
  • Recalculer les index via l'ORM

Un composant expérimental

L'API peut changer

Symfony 8.2 sort fin novembre 2026

Quoi chiffrer ?

  • Les données sensibles
  • Les données personnelles
  • Ne pas tout chiffrer systématiquement

Conclusion

Des questions ?

Meme Questions : un public qui lève la main

Merci !

Les slides

Meme Merci : Dwight (The Office) en larmes

Feedbacks