Décryptage des secrets Symfony
Depuis la version 4.4
, Symfony vous donne la possibilité de conserver les informations sensibles de votre application dans des fichiers encodés.
Mais comprenez-vous ce que ça change réellement ?
Chez PrestaConcept, on fait du Symfony depuis bien longtemps maintenant. Nous avons suivi les évolutions du framework, et on s'est tenus à jour autant que possible (je pense qu'on peut dire avoir eu l'occasion d'essayer toutes les versions).
Nous utilisions déjà beaucoup les fichiers parameters.yml
sur les versions 2.x
& 3.x
, avec nos propres spécificités par rapport au standard Symfony :
parameters.default.yml
, versionné, contenant les paramètres communs à la plupart des environnements et non secretsparameters.yml
, non versionné, contenant les paramètres spécifiques à un environnement d'exécution et souvent secretsLes paramètres sont faciles à utiliser, présents depuis toujours dans Symfony, leur fonctionnement a peu changé depuis lors. Leurs valeurs étant compilées dans le conteneur qui est mis en cache, pour changer la valeur d'un paramètre, il est nécessaire de vider le cache applicatif.
Lorsque le composant Dotenv a été ajouté en Symfony 3.3, nous ne nous sommes pas immédiatement jetés dessus : le fait que ça soit un fichier unique (.env.dist à copier en .env) ne nous convenait pas.
Mais dès qu'il a été possible de découper le contenu de ce fichier en plusieurs unités logiques, nous l'avons tout de suite adopté :
.env
: contient toutes les valeurs par défaut.env.local
: contient les spécificités d'un environnement d'exécution.env.{env}
: contient les spécificités d'un environnement Symfony.env.{env}.local
: contient des valeurs fabriquées lors du build de l'application (notamment la version)Les variables d'environnements s'utilisent presque comme des paramètres (vous pouvez même assigner la valeur d'une variable d'environnement à un paramètre). Leurs valeurs ne sont pas compilées avec le conteneur mis en cache, mais appelées "dynamiquement" par celui-ci, changer la valeur d'une variable d'environnement ne nécessite donc pas de vider le cache applicatif (si toutefois vous n'avez pas utilisé une de ces variables d'environnements pour altérer quelque chose dans votre container, auquel cas vider le cache sera nécessaire).
Nous sommes toujours soucieux de nous améliorer. Aussi quand l'annonce du "Gestionnaire de Secrets" a été faite, nous-nous sommes penchés sur ça pour voir comment nous pouvions l'inclure dans nos habitudes.
Mais nous n'en avons pas tout de suite vu l'intérêt...
Pour faire simple, vous allez générer des paires de clés asymétriques pour chaque environnement. Et vous allez utiliser ces clés pour encoder/décoder vos secrets.
Déjà, ce n'est pas un composant, mais une collection de classes intégrées au FrameworkBundle
.
Une collection de commandes :
Qui permettent de manipuler les secrets, les clés de cryptage, etc...
Nous ne nous étalerons pas sur comment utiliser ces commandes ici, reportez-vous à la (toujours très bonne) documentation dédiée
Deux coffres à secrets :
LocalVault
qui parse le fichier .env.{env}.local
SodiumVault
qui contient toute la logique d'encodage/décodage des secretsJusqu'ici, vous me direz que vous auriez pu lire tout ça sur la doc. Et vous auriez raison (mais bon, je vous l'ai bien synthétisé non ?).
Mais c'est ici que réside la raison d'être de cet article.
Car de prime abord, nous n'avons pas réellement vu la plus-value de sécurité que pouvait apporter cette complexité.
Après-tout, si le fichier contenant les secrets de l'environnement (.env.local
) n'a jamais quitté le serveur, et si depuis le serveur, Symfony est capable de décoder les secrets grâce à la clé privée de l'environnement, quelle différence cela fait-il ?
Il y a bien une différence, et nous voulons partager avec vous le résultat de nos réflexions sur le sujet.
Nous avons fait l'erreur de ne considérer que la possibilité d'une intrusion directe sur le serveur. Dans ce genre de cas, ne vous méprenez pas, rien ne vous sauvera : vos secrets peuvent effectivement avoir été volés (c'est juste plus compliqué car un cat
sur le fichier ne suffit pas : il faut connaitre Symfony pour les récupérer).
Mais dans ce genre de cas, vous avez probablement plus à faire que simplement chercher à protéger vos secrets.
Le problème principal avec les variables d'environnement, c'est qu'elle sont dans la mémoire de PHP.
Que ce soit avec de vraies variables d'environnements ou via les variables qui sont chargées avec le composant Dotenv, toutes ces valeurs se retrouvent dans les Superglobales PHP $_ENV
& $_SERVER
.
Ça veut dire que quelqu'un qui arriverait à injecter du code PHP qui s'exécuterait dans votre application pourrait accéder à toutes ces informations.
Je vous conseille la très intéressante conférence donnée par Antti RÖSSI au Forum PHP 2020.
Lorsque vous utilisez les secrets Symfony, vous pouvez les référencer ces variables comme n'importe quelle autre variable d'environnement (%env(DATABASE_URL)%
), mais elles ne sont plus enregistrées dans les Superglobales.
De plus, le fichier sur le serveur étant encodé, il sera plus difficile pour quelqu'un injectant du code dans l'application de récupérer son contenu : il devra connaitre la mécanique de décodage pour pouvoir le lire.
Tout se passe dans la classe EnvVarProcessor.
Lorsqu'une variable d'environnement est demandée quelque part dans vos services, le code passe dans cette classe. Si jamais cette variable n'est enregistrée nulle part, Symfony demande à tous les EnvVarLoader de charger les variables qu'ils ont à leur disposition.
Il se trouve que la classe SodiumVault
(que l'on a vue un peu plus haut) implémente cette interface (c'est même la seule dans une application "classique").
Lorsqu'elle est interrogée pour faire ça, elle va lire le contenu des secrets, les décode un à un et les retourne décodés.
EnvVarProcessor
mémorise toutes ces variables de telle sorte à ne les demander qu'une seule et unique fois pour un processus donné.
Si vous ne connaissez pas cette classe, c'est elle qui s'occupe de convertir vos variables d'environnements (à la sauce "pipe", mais à l'envers) depuis Symfony 4.3.
Oui, une fois par processus : à chaque appel HTTP & chaque commande, les secrets sont décodés. Ce décodage prend nécessairement du temps, quelques millisecondes sacrifiés à sécuriser votre application. Après tout, n'oubliez pas que "sécurité" ne veut pas dire "inviolabilité" mais "difficulté à pénétrer".
D'ailleurs, la documentation au sujet des secrets met en garde sur ce point là, et vous propose de "dumper" le contenu de vos secrets dans le fichier
.env
. Mais si vous avez bien suivi, vous aurez compris qu'en faisant ça, vos secrets n'auront plus rien de secret...
Si vous souhaitez éviter que vos informations sensibles ne soient écrites dans les Superglobales, mais ne souhaitez pas vous embarquer dans la "complexité" inhérente aux secrets, vos pouvez toujours écrire votre propre EnvVarLoader
:
<?php
namespace App\Env;
use Symfony\Component\DependencyInjection\EnvVarLoaderInterface;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Symfony\Component\Dotenv\Dotenv;
final class HiddenEnvVars implements EnvVarLoaderInterface
{
private const FILE = '.env.hidden';
private ParameterBagInterface $parameters;
public function __construct(ParameterBagInterface $parameters)
{
$this->parameters = $parameters;
}
public function loadEnvVars(): array
{
$file = $this->parameters->get('kernel.project_dir') . \DIRECTORY_SEPARATOR . self::FILE;
return (new Dotenv())->parse(\file_get_contents($file), self::FILE);
}
}
Attention, cet exemple a pour but d'illustrer le fonctionnement des mécanismes. Vous pouvez vous en inspirer pour faire votre propre système : en lisant un autre type de fichier, ou ce que vous voulez... Utiliser les secrets reste la solution la plus sécurisée à notre disposition, il est donc recommandé de les utiliser.