Avec Vault tout s’.env-ole

Avec Vault tout s’.env-ole

Je m'abonne
Temps de lecture: 12 mins

Cela fait plusieurs articles où je parle de sécurité côté serveur. Mais en tant que développeur web, notre première préoccupation reste évidemment la communication entre services (APIs). Et pour cela, il faut pouvoir jongler proprement entre l'environnement de développement et l'environnement de production.

Vient alors la question essentielle :

Comment transmettre et stocker nos informations sensibles (clés API, tokens, secrets, etc.) en toute sécurité ?

  • Comment stocker en toute sécurité les API_KEY des services tiers ?
  • Comment partager ou transmettre ces secrets entre développeurs, serveurs et environnements ?

Si ce n'est pas ton premier rodéo avec Symfony, tu es probablement déjà familier avec l'usage des .env mais est-ce que tu connais son Vault ?

Bref rappel du processus des variables d'environnements.

Petit disclaimer, j'utilise ci-après un exemple de clé API Stripe, c'est évidement juste un random string. (Je préfère prévenir 😅).

1. $_ENV chez PHP

En PHP quand on veut accéder à une variable d'environnement de notre OS on utilise le classique $_ENV .

On accède alors à toutes les variables d'environnements que notre serveur (apache ou fpm) à accès. C'est brut, c’est sale.

Nous si on utilise Symfony c'est pour une raison : sa structure. Alors on veut pas de ça.

On laisse tomber le $_ENV de PHP et on passe chez son grand frère.

2. Les variables d’environnement chez Symfony

La fonction getenv()

Avec getenv() ça nous permet d'accéder aux variables d’environnement de notre système de la même manière qu'un $_ENV.

Si tu es sur Unix, rends toi dans ton CLI et fait : export STRIPE_API_KEY=0868ae5d0bf763d5319820b78fadf9e4ad6abddc .

Tout de suite après tu lance un echo $STRIPE_API_KEY.

echo

On voit bien que l'export rend la variable STRIPE_API_KEY disponible dans nos interactions avec le CLI.

Sans changer de console, tu peux te créer un projet symfony symfony local:new et crée toi un contrôleur comme ceci :

namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\Routing\Attribute\Route;

final class SecretController extends AbstractController
{
    #[Route('/secret', name: 'app_secret')]
    public function index(): JsonResponse
    {
        dd(getenv('STRIPE_API_KEY'));
    }
}

Toujours dans cette même console : 

  • Lance le serveur symfony server:start.
  • Rends to sur ton navigateur avec localhost:8000/secret.

On a alors accès à notre variable préalablement enregistrée.

Notre variable accessible
Notre variable STRIPE_API_KEY accessible

On est d'accord, c'est pas très pratique du tout de créer des variables sur notre OS pour ensuite les utiliser sur notre application. Mais c'est intéressant de savoir que ça existe.

D'une part car un jour ça peut être utile mais d'autre part ça permet de mieux comprendre le fonctionnement des variables d’environnement.

On récap : getenv() ça permet d’accéder aux variables d’environnements déclarées sur notre OS dont le user symfony a accès.

ParameterBagInterface (PBI pour les intimes)

Avec ParameterBagInterface c'est un peu différent, l'interface nous permet d'avoir accès aux variables uniquement si elles sont renseignées dans notre config/services.yaml.

On va changer notre contrôleur pour utiliser PBI :

#[Route('/secret', name: 'app_secret')]
public function index(ParameterBagInterface $parameters): JsonResponse
{
    dd($parameters->getParameter('app.stripe.key'));
}

Recharge ta page.

Paramètre inconnu

Quand on utilise PBI, les variables doivent être renseignées dans config/services.yaml :

parameters:
    app.stripe.key: '%env(STRIPE_API_KEY)%'

Ici on dit à PBI de mapper app.stripe.key avec notre variable d'environnement préalablement exportée STRIPE_API_KEY.

De cette manière, on récupère la même chose qu'avec le getenv().

C'est intéressant pour avoir accès aux variables de notre OS mais c'est pas non plus super pratique, car il faut penser à exporter nos variables sur l'ensemble des serveurs qui utiliseront l’application.

En bref, c'est relouuu !!!

C'est pour ça qu’on utilise les .env ! (punaise le mec fait 10 min d'explications juste pour en arriver aux .env. Les calculs sont pas bons Kévin !!!).

3. Les .env*

Pour continuer la suite du tuto, tu peux éteindre ton serveur Symfony et le relancer dans une nouvelle console, de cette manière on purge STRIPE_API_KEY des variables enregistrées pour la démo et on repart sur une base saine.

Un .env du coup c’est juste un fichier avec des variables (plus ou moins confidentielle).

le fichier de base .env c’est le reflet des variables d’environnement que ton application symfony aura besoin pour fonctionner :

APP_ENV=prod
APP_DEBUG=0
APP_SECRET=ARENSEIGNER

STRIPE_API_KEY=ARENSEIGNER

DEFAULT_URI=ARENSEIGNER

Il ne doit jamais contenir de secrets, de clés API etc … Ce fichier .env est inclus dans les commits de ton application. Donc on évite de faire fuiter des clés sur Github et Cie !

Vérifie toujours que dans ton .gitignore soit inscrit /.env.* pour exclure tout les autres des commits ! Sinon … tu auras des surprises dans tes logs serveur !

Breach .env
( Coucou Laconis 😅 ).

Mais dans ce cas, pourquoi on crée un .env et pas juste un .env.quelquechose ?

Mais oui Jamie ?

Et bien Fred parce que ce .env sert de template pour les autres.

En sachant ce qu'on doit y mettre dedans, c'est plus simple pour construire les autres.

Mon .env.local :

APP_ENV=dev
APP_DEBUG=1
APP_SECRET=61b0892aa8eab90ac0f2c605aa067601968bf7c5

DEFAULT_URI=http://localhost

J'ai juste repris mon .env et je l'ai remplis avec les informations dont Symfony aura besoin.

De cette façon je suis certain de ne rien commit par erreur.

PBI est plutôt intelligent car il va d’abord aller voir dans le .env si une variable existe, puis si un .env.prod ou un .env.dev existe et écraser la valeur précédente présente. Et il terminera toujours par chercher un .env.*.local pour écraser une dernière fois les variables précédentes.

Cela permet de gérer finement les accès.

On utilise un .env pour chaque environnement : .env.local, .env.prod, .env.test, .env.stage etc

.env

Pour illustrer tout ça, si tu as copié-collé les mêmes variables dans les mêmes .env.* que moi tu vas pouvoir tester ça.

Dans ton contrôleur, tu es censé avoir toujours le même code :

#[Route('/secret', name: 'app_secret')]
public function index(ParameterBagInterface $parameters): JsonResponse
{
    dd($parameters->get('app.stripe.key'));
}

Rafraîchis ta page :

La variable du .env

PBI est venu chercher dans le .env la variable renseignée plus tôt dans notre service.yaml.

Sauf que c’est pas vraiment "ARENSEIGNER" qu’on veut, ça c'est juste le .env du git.

C'est pour ça qu'on utilise du .env.quelquechose.

Renseigne maintenant dans ton .env.local le STRIPE_API_KEY=0868ae5d0bf763d5319820b78fadf9e4ad6abddc et recharge ta page.

Notre variable override

On voit bien que le .local a écrasé le .env pour les variables définies.

Si tu utilises Symfony tu connais, mais du coup, est-ce que tu connais le Vault ?

Vaultech

4. Le Vault de Symfony

C'est un coffre fort pour se passer d’inclure les variables sensibles comme des clés API ou des identifiants sensibles dans les .env.*.

De cette manière, on peut se transmettre hyper facilement les infos entre serveurs ou les copains !

Un .env.local, ou un .env.prod, il faut les créer à la main à chaque fois qu’on change de serveur. Et il y a 2 inconvénients majeurs :

    • C’est facile à lire (cat .env.local et sésame ouvre toi !)
    • C’est facile à leaker (si le .gitignore n'est pas bien configuré)

Symfony a donc prévu le coup pour éviter ce genre d’incident avec son Vault.

Ce coffre se base sur l'utilisation d’une paire de clé cryptographique à la manière de SSH :

    • Clé publique pour le chiffrement
    • Clé privée pour le déchiffrement

Pour le principe des clés, je te laisse lire mon article sur SSH.

Pour l'installation et la configuration, je me base sur sa doc.

Génération de la paire de clés cryptographique

// On a besoin de la dépendance à Sodium pour la paire de clés
composer require paragonie/sodium_compat

symfony console secrets:generate-keys 

Et là Symfony vient de nous créer une paire de clés disponibles dans notre config/secrets/ :

Par défaut la commande de génération va récupérer l'APP_ENV le plus bas dans la chaîne des .env et créer la paire de clés associées.

Tu peux aussi forcer la génération de clés pour un environnement spécifique (ici prod) avec cette commande :

APP_RUNTIME_ENV=prod php bin/console secrets:generate-keys

Génération des clés terminées

On se retrouve donc avec un dossier dev et un dossier prod :

Génération des clés

Ce qu'il faut impérativement faire à présent, c'est sécuriser tes commits dans ton .gitignore et retirer la clé privée de chaque environnement (sinon c'est complètement inutile tout ça !) avant de push.

Ajoute ceci : /config/secrets/*/*.decrypt.private.php .

De cette manière, seules les clés de chiffrement seront commit, pas celles du déchiffrement.

On est safe comme ça.

Ecriture / Lecture des secrets

Pour créer un secret, la commande est celle-ci : symfony console secrets:set mon-secret .

Si tu veux créer un secret pour un environnement spécifique, ajoute simplement en début de commande APP_RUNTIME_ENV=prod .

Pour continuer l’exemple on va garder notre STRIPE_API_KEY et on lance la commande : symfony console secrets:set STRIPE_API_KEY.

Génération du secret

Symfony est tellement confiant dans son système de chiffrement qu'il nous dit qu’on peut carrément commit notre fichier chiffré ! C'est ultra badass ! En même temps la librairie de chiffrement NaCL/LibSodium est une des plus robustes. Elle est utilisée chez Cloudflare, Signal, WhatsApp et même Tor se base dessus.

On a maintenant dans notre dossier config/secrets des nouveaux fichiers correspondants aux valeurs.

Nos nouveaux secrets

On peut aussi accéder à la liste de nos secrets avec cette commande : symfony console secrets:list.

La liste des secretsAvec le paramètre -- reveal on peut voir les valeurs. C'est grâce à la clé de déchiffrement ça. symfony console secrets:list --reveal :

Revelio !

reveal des secrets

Ok sauf que là c’est problématique, on a dans notre Vault la clé, mais elle est override par la valeur de notre .env.local, donc on va l’enlever de tout nos .env*, c’est plus sûr ! 

On l’enlève et on refait un coup de console :

reveal des secrets

Voila comme ça on est safe !

On oubliera pas d’ajouter une valeur pour la prod grâce à APP_RUNTIME_ENV=prod symfony console secrets:set STRIPE_API_KEY --random pour l'utiliser sur notre serveur.

On peut la lire également avec la commande APP_RUNTIME_ENV symfony console secrets:list --reveal

Les secrets de la prod

On voit bien qu'on a un secret STRIPE_API_KEY différent pour 2 environnements ! Un dev et un prod.

Tu peux aussi si tu le souhaites supprimer un secret avec :remove .

Et comment on utilise les secrets ?

Si la clé de déchiffrement est dispo sur l'environnement, alors tu l'utilises comme les .env ! Tu enregistres la valeur dans ton service.yaml et tu l'appelles avec PBI comme d'habitude.

Plus besoin de flipper sur un commit un peu douteux.

Tant que tu ne donnes pas ta clé de déchiffrement, tout va bien !

Pour le passage en prod, Symfony a une variable SYMFONY_DECRYPTION_SECRET qu'il utilise pour savoir où se situe la clé de déchiffrement.

Tu peux par exemple créer un dossier /etc/symfony/keys et y placer la clé :

// On crée le dossier symfony puis le dossier keys puis le dossier monapp
mkdir -p /etc/symfony/keys/monapp
// Place la clé de chiffrement et donne lui les bons droits (juste lecture)
chown www-data:www-data /etc/symfony/keys/monapp/prod.decrypt.private.php
chmod 600 /etc/symfony/keys/monapp/prod.decrypt.private.php

Comme ça, seul www-data aura la possibilité de lire la clé.

Et on termine en indiquant à Symfony où se trouve notre clé avec notre .env : SYMFONY_DECRYPTION_SECRET=/etc/symfony/keys/monapp/prod.decrypt.private.php

Si tu as un doute sur le fait de commit ton fichier contenant ton secret, tant que la personne n'a pas la clé de déchiffrement, rien ne t'arriveras.

Essaye de supprimer ton fichier de déchiffrement et de lire un secret :

Echec de la lecture

Impossible de lire le secret sans la clé et si en plus tu utilises un dépôt privé, tu ne crains plus grand chose. 

On peut aussi faire de la rotation de clés pour augmenter la sécurité et en cas de fuite de clés, pouvoir les changer rapidement. Pour éviter les cas de stress c'est bon à prendre.

A présent tu sais tout sur le Vault de Symfony.

C’est une belle solution pour la sécurité des identifiants sensibles et la gestion des environnements. 

Évidement ce système n’est pas infaillible dans la mesure où il repose complétement sur un seul fichier à voler, mais ça réduit bien la surface d’attaque au lieu d’un .env en clair sur le serveur (ou dans un commit !).