Les coulisses du passage en Open Source d'un dépôt Git vieux de 7 ans - Partie 1

9 min de lecture.

Supprimer les données sensibles d'un dépôt Git vieux de 7 ans

Après 7 années en tant que logiciel propriétaire Cap Collectif a décidé de mettre à disposition l'intégralité du code en open source. Une très bonne nouvelle pour nos clients mais un sacré challenge pour notre équipe technique, puisque cela n'avait pas été anticipé !

Pour bien comprendre, les développeurs travaillent depuis le début sur un dépôt Git privé sur GitHub qui s'appelle cap-collectif/platform. Nous avons choisi de ne pas rendre open source celui-ci car il contient tout notre historique de travail (Issue, Pull requests, …) ce qui représente une énorme quantité de données. Nous souhaitions également conserver une ambiance de travail interne, sans que tout notre travail soit public au moindre git push. Cette approche nous permet également de limiter le risque d'exposer des problèmes de sécurité pour nos clients, par inattention suite à une erreur humaine.

En revanche nous allons bien proposer un second dépôt Git cap-collectif/cap-collectif, cette fois-ci 100% open source et qui contient l'intégralité du code qui permet aux plateformes de fonctionner, cette série d'articles détaille comment nous avons mis ça en place, étape par étape !

Étape 1, retirer les secrets du code

Cette première étape est celle qui nous a demandé le plus de travail, pour plusieurs raisons :

  • Les secrets utilisés par les développeurs, la CI et la production étaient essentiellement en dur dans le code, la plupart du temps en valeur par défaut pour les variables d'environnement de Symfony (ex: env(SYMFONY_MANDRILL_API_KEY): TOTO) mais parfois en dur dans le code des services ;
  • Les certificats et secrets utilisés dans les SSO de nos clients étaient en dur dans le code ;
  • Certains IPs de notre infrastructure étaient présents dans le code pour sécuriser des accès.
  • Des identifiants d'API GitHub étaient dans le code afin d'accéder à nos forks de dépendances présentent sur des dépôts Git privés ;

Nous avons procédé de la manière suivante :

  • En environement de dev : mise en place d'un fichier .env.local accessible sur 1Password pour notre équipe technique qui contient la configuration par défaut de l'environnement de développement. Pour la suite, on envisage la mise en place d'un Vault pour notre équipe technique et pour l'Open Source génèrer ce fichier avec des valeurs par défaut, qui devront être remplacées par les contributeurs.

  • En environement de test : ajout d'un mécanisme de chargement de certaines variables d'environnement sur notre conteneur de test CircleCI. Pour cela, on utilise la clé environment de notre configuration docker-compose pour injecter les variables utiles :

# testing.yml
services:
  application:
    environment:
      # Set the following variables on CircleCI
      # https://app.circleci.com/settings/project/github/cap-collectif/platform/environment-variables
      SYMFONY_MANDRILL_API_KEY: ${SYMFONY_MANDRILL_API_KEY}
  • En environement de production : On a modifié la création d'image docker, en ajoutant avec des arguments de build --build-arg. Lors de la création de l'image docker, on injecte les variables d'environnement directement depuis notre intégration continue.
docker build \
  --file infrastructure/services/remote/Dockerfile . \
  --build-arg SYMFONY_MANDRILL_API_KEY=$SYMFONY_MANDRILL_API_KEY

On récupère la valeur de l'argument dans le Dockerfile, afin de l'injecter en variable d'environnement dans l'image :

# Dockerfile
ARG SYMFONY_MANDRILL_API_KEY="INSERT_A_REAL_SECRET"
ENV SYMFONY_MANDRILL_API_KEY=$SYMFONY_MANDRILL_API_KEY

Ensuite nous avons mis en place un fichier passwords.txt qui recense tous les secrets que nous avons nettoyés, qu'il faudra supprimer plus tard de l'historique du dépôt Git.

On liste ainsi un secret sur chaque ligne :

# passwords.txt
toto
tata
1234

C'est là que le travail chronophage commence puisqu'il faut identifier chaque secret problématique puis :

  • L'ajouter dans un fichier passwords.txt qu'on réutilisera plus tard (étape 3) ;
  • Ajouter la variable d'environnement et sa valeur en développement, CI et production ;
  • Mettre à jour le code du dépôt Git pour supprimer la valeur présente en dur dans le code ;
  • Renouveler le secret et mettre à jour sa valeur sur les différents environnements, sans rien casser.

Évidemment, identifier tous les secrets de presque un million de lignes de code, n'est pas une chose aisée… Une fois que nous pensions avoir corrigé l'essentiel, nous avons utilisé GitGuardian afin de faciliter la détection des problèmes. Cet outil nous a permis d'analyser non seulement l'état actuel du code mais également tout l'historique du dépôt Git, de quoi faire bien grandir notre fichier passwords.txt !

L'autre bonne nouvelle, c'est que GitGuardian nous permet désormais de s'assurer qu'aucun nouveau secret ne sera ajouté en dur dans le code de la branche principale puisque la CI échouera, parfait ! Cette fonctionnalité est désormais également intégrée à GitHub.

Pour les configurations des SSO de nos clients nous avons opté selon les technologies :

  • Une configuration directement intégrée au back-office de l'instance du client ;
  • Une configuration en volume docker depuis notre infrastructure, lorsque nous avons manqué de temps.

Après avoir retiré tous les secrets du code de notre branche principale, je peux vous dire qu'on adoptera cette approche dès le début, si on a l'occasion de démarrer un nouveau projet, open source ou pas !

Étape 2, retirer les données sensibles

Maintenant que GitGuardian ne nous remonte plus de problèmes de sécurité, on a bien avancé mais on n’est pas encore tout à fait prêt à open sourcer !

Retirer les données sensibles est l'étape la plus hasardeuse car il nous a fallu identifier tous les fichiers comités par erreur ou par fainéantise…

Je vais être honnête, on a retrouvé un peu de tout :

  • des fichiers csv, xls ;
  • des dumps SQL ;
  • des données personnelles (emails, nom de compte) utilisées pour des imports de données en production ;
  • le code complet de certaines dépendances…

Les bonnes pratiques que nous avons mises en place progressivement n'étaient pas appliquées en 2014, au tout début de l'aventure, lorsque nous n'étions qu'une équipe de 2 stagiaires et qu'il n'y avait qu'un seul objectif : aller le plus vite possible…

Pour nous faciliter le travail de recherche, nous avons utilisé plusieurs commandes :

git ls-files '*.sql'

Pour vérifier la présence d'une extension dans le dépôt Git

git rev-list --objects --all |
  git cat-file --batch-check='%(objecttype) %(objectname) %(objectsize) %(rest)' |
  sed -n 's/^blob //p' |
  sort --numeric-sort --key=2 |
  cut -c 1-12,41- |
  $(command -v gnumfmt || echo numfmt) --field=2 --to=iec-i --suffix=B --padding=7 --round=nearest

Pour lister les plus gros fichiers comités dans l'historique du dépôt Git

Cela nous a permis d'identifier des exécutables ou des énormes fichiers qui avaient été comités puis retiré dans la même Pull Request, sans que nous ne nous en rendions compte lors des revues de code.

Même si nous ne sommes pas 100% sûrs d'avoir pu supprimer tous les fichiers sensibles, nous pensons avoir listé un maximum de fichiers, qui vont nous être utiles pour la prochaine étape, la réécriture de l'historique !

Étape 3, réécrire l'historique de notre dépôt Git privé

Réécrire l'historique d'un dépôt Git n'est pas simple, alors on va utiliser BFG repo cleaner, un outil très complet et très performance, pour nous faciliter la tâche.

Afin de démarrer une réécriture d'historique, on va utiliser un dépôt miroir Git, pour cela clonez le dépôt dans un dossier platform-mirror avec l'option --mirror :

git clone --mirror [email protected]:cap-collectif/platform.git platform-mirror

Ce dépôt miroir ne contient pas réellement les fichiers sources, mais permet quand même la réécriture de l'historique.

On va utiliser quelques commandes qui se basent sur les noms exacts :

bfg --delete-folders "{cert,jwt,dist}" platform-mirror

On supprime des dossiers (cert, jwt et dist) complets qui sont sensibles.

bfg --delete-files '*.{exe,sql,graphql.js,zip,xls,xlsx}' platform-mirror

On supprime les fichiers en se basant sur l'extension lorsqu'il n'y a aucune raison de les conserver dans l'historique.

Enfin on supprime les mots de passe et autres secrets identifiés lors de l'étape 1 dans le fichier passwords.txt :

bfg --replace-text passwords.txt platform-mirror

Chaque ligne du fichier passwords.txt sera remplacée dans l'historique par ***REMOVED*** dans l'intégralité du code source

Une fois que toutes vos opérations de réécriture ont été effectuées, chaque commit de l'historique a été nettoyé. Il faut confirmer la suppression en lançant :

git reflog expire --expire=now --all && git gc --prune=now --aggressive

Puis si vous êtes prêts à publier la réécriture, il ne vous reste qu'à faire un git push.

Je vous recommande quand même de bien vérifier en amont que tout c'est bien passé. Pour cela modifions le fichier platform-mirror/config afin de changer origin par un dépôt Git de test comme platform-mirror :

[remote "origin"]
	url = [email protected]:cap-collectif/platform-mirror.git
	fetch = +refs/*:refs/*
	mirror = true

Après un git push vous pourrez explorer le résultat de la réécriture et lancer GitGuardian à nouveau pour vérifier qu'aucun problème n'est détecté cette fois-ci 👍

Le jour J

Pour réaliser cette réécriture nous avons mis en pause les envois de code sur notre dépôt Git le temps d'une demi-journée (la réécriture peut prendre un temps assez conséquent). Une fois celle-ci terminée et envoyée sur GitHub, chaque développeur a fait les manipulations suivantes avant de reprendre le travail :

# On backup l'environnement de dev
mv platform platform-old-backup

# On fait un clone du dépôt après la réécriture
git clone [email protected]:cap-collectif/platform.git

# On réinstalle tous les fichiers non-comités…
cp platform-old-backup/.env.local platform/.env.local

# On relance l'infra et tout est bon !

Après l'envoi de la réécriture, toutes les branches ont été mises à jour, les développeurs peuvent donc à nouveau envoyer des modifications sur le dépôt Git normalement. Si des modifications ont été effectuées par des développeurs pendant la réécriture (elles n'ont donc jamais été envoyées), il faut en revanche les appliquer sur le nouveau dépôt, par exemple en générant un patch avec git format-patch.

Nous avons eu besoin de plusieurs réécritures de notre dépôt Git suite à quelques oublis mais globalement le processus, bien que stressant, n'a pas été si génant pour le quotidien de l'équipe technique.

Partie 2, la publication d'un dépôt open source : Coming soon.

Publié le