Blog
Envelope Encryption in Symfony 8.2: Schließ den Key weg, nicht nur die Daten
Symfony 8.2 verschlüsselt eine Doctrine-Spalte mit einem einzigen Attribut. Der Trick dahinter heißt Envelope Encryption: Schließ die Daten mit einem Key ab, und diesen Key mit einem zweiten, der deinen Key-Dienst nie verlässt. Hier siehst du, wie das funktioniert, mit einer Spielwiese zum Ausprobieren.
symfony php security 4 Min. Lesezeit
Von David Gorges
Geschäftsführer
Ein Attribut, und die Spalte ist verschlüsselt
Hier ist das neue Feature in einem Snippet:
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\KeyManagement\Bridge\DoctrineOrm\Attribute\Encrypted;
#[ORM\Entity]
class Customer
{
// ...
#[ORM\Column(nullable: true)]
#[Encrypted(key: 'customer.contact')]
private ?string $phone = null;
}
Das war's. In PHP ist $phone weiter ein ganz normaler String. In der Datenbank stehen unlesbare Bytes. Symfony verschlüsselt den Wert beim Speichern und entschlüsselt ihn beim Laden.
Aber Verschlüsselung ist nur so gut wie der Ort, an dem der Key liegt. Steht der Key in der .env neben dem Datenbank-Passwort, hat jeder, der das eine klaut, ziemlich sicher auch das andere. Genau dieses Problem löst Envelope Encryption.
Die Idee: zwei Keys
Stell dir eine verschlossene Kiste vor:
- Der Data Key schließt die Kiste ab. Er verschlüsselt deine Telefonnummer.
- Der Master Key schließt den Data Key ab. Er lebt in einem KMS, einem Key-Dienst wie AWS KMS oder HashiCorp Vault, und verlässt ihn nie.
Die verschlossene Kiste und ihr verschlossener Key liegen also nebeneinander, und nur das KMS kann den Key öffnen. Einen Key mit einem anderen Key abschließen nennt man Wrappen. So sieht das aus, wenn du einen Kunden speicherst:
-
KMS nach App Data Key ×2
Das KMS erzeugt einen neuen Data Key
Und gibt ihn zweimal heraus: einmal unverschlüsselt, einmal mit dem Master Key verschlüsselt.
-
App verschlüsselte Nummer
Deine App verschlüsselt die Telefonnummer
Mit dem unverschlüsselten Data Key, direkt in PHP (AES-256-GCM). Die Telefonnummer geht nie zum KMS.
-
App nach Datenbank beides verschlüsselt
In der Datenbank landet nur Verschlüsseltes
Die verschlüsselte Nummer und der verschlüsselte Data Key. Den unverschlüsselten Data Key vergisst die App, er landet nie auf der Platte.
Lesen funktioniert andersherum: Die App schickt den verschlüsselten Data Key zum KMS, bekommt ihn unverschlüsselt zurück und entschlüsselt damit die Nummer.
Probier es selbst aus. Das hier ist echte Verschlüsselung, in deinem Browser:
Diese Spielwiese verschlüsselt wirklich, direkt in deinem Browser. Dafür braucht sie JavaScript und eine sichere Verbindung (https).
Deine App
noch leer
KMS
- Noch keine Anfragen
| id | phone | wrapped_key | Entschlüsseln |
|---|
Klick zweimal mit derselben Nummer auf „Verschlüsseln & speichern“: völlig andere Bytes. Merk dir das, es wird später wichtig.
Was ein Angreifer bekommt
Jetzt der Teil, für den wir das alles machen.
-
Datenbank nach Angreifer SQL-Dump
Ein Angreifer kopiert die Datenbank
Ein Backup am falschen Ort, eine SQL-Injection. Er hat jetzt jede Zeile.
-
Angreifer nach KMS
Entschlüsseln kann den Data Key nur das KMS
Im Dump ist alles verschlüsselt, auch die Data Keys.
-
KMS nach Angreifer abgelehnt
Das KMS lehnt ab
Kein Zugriff auf das KMS, keine Data Keys, keine Telefonnummern.
Gut zu wissen
Suchen wird schwieriger. Dieselbe Telefonnummer wird nie zu denselben Bytes (erinnerst du dich an die Spielwiese?), also findet WHERE phone = ? nichts mehr. Für exakte Suchen gibt es in Symfony einen Blind Index: eine zusätzliche Spalte mit einem Fingerabdruck des Werts.
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)]);
Weniger KMS-Aufrufe. Standardmäßig bringt jeder Wert seinen eigenen verschlüsselten Data Key mit, also fragt jedes Speichern und jedes Laden beim KMS an. Mit der Option key_management.store liegen die verschlüsselten Data Keys in einer eigenen Tabelle und werden geteilt. Dann wird das KMS einmal pro Key gefragt statt einmal pro Zeile.
Den Master Key zu wechseln ist billig. Mit dieser Key-Tabelle verschlüsselt ein einziger Befehl alle Data Keys neu, mit einem neuen Master Key. Deine Kundenzeilen bleiben unberührt:
php bin/console key-management:rewrap-data-keys
Was du jetzt tun kannst
-
Die Spalten auflisten, die wirklich personenbezogen sind.
Telefon, Geburtsdatum, Notizen: alles, was in einem Leak wehtut.
-
In Produktion ein echtes KMS nehmen.
Lokal reicht
sodium://. In Produktion bringt dir ein Master Key in der.envgar nichts. -
Jeder Spalte, nach der du suchst, einen Blind Index geben.
Bevor die erste Query plötzlich nichts mehr findet.
-
Erst mit einer Kopie der Datenbank ausprobieren.
Die Symfony-Posts sagen nicht, wie bestehende, noch unverschlüsselte Zeilen verschlüsselt werden.
In einem Satz: In der Datenbank liegen nur verschlüsselte Werte und verschlüsselte Data Keys, und nur das KMS entscheidet, wer sie entschlüsseln darf.
Quellen
6 Quellen
-
New in Symfony 8.2: Encrypted Doctrine Columns
symfony.com · 15.10.2026
-
New in Symfony 8.2: KeyManagement Component
symfony.com
-
[KeyManagement] Add the component (#64052)
github.com
-
#[Encrypted] attribute (#66806)
github.com
-
KeyManagement documentation (#22280)
github.com
-
Envelope encryption
cloud.google.com