Aller au contenu principal

Git : git-crypt

· 11 minutes de lecture

Un dépôt d'infrastructure décrit tout ce qu'il faut pour reconstruire un service, sauf les secrets (mots de passe, jetons d'API, clés privées), qui restent dans un .env ignoré par Git, copié à la main d'une machine à l'autre, rarement sauvegardé. Les versionner en clair n'est pas envisageable ; les laisser hors du dépôt casse la promesse « le dépôt suffit à tout reconstruire ». git-crypt comble cet écart : certains fichiers sont chiffrés au moment du commit et déchiffrés au checkout, de façon transparente pour qui possède la clé.

Principe : les filtres Git​

git-crypt ne modifie pas Git. Il s'appuie sur un mécanisme natif, les filtres déclarés dans .gitattributes :

  • le filtre clean s'exécute quand un fichier passe du répertoire de travail vers l'index (git add) : c'est la version nettoyée qui est stockée dans les objets Git ;
  • le filtre smudge s'exécute dans l'autre sens, au checkout : le blob stocké est transformé avant d'être écrit sur disque ;
  • le pilote de diff textconv convertit un blob avant affichage par git diff ou git log -p.

git-crypt branche le chiffrement sur clean, le déchiffrement sur smudge, et un déchiffrement à la volée sur textconv.

 répertoire de travail          index / objets Git            dépôt distant
┌────────────────────┐ clean ┌────────────────────┐ push ┌──────────────┐
│ .env (clair) │ ──────▶ │ blob chiffré │ ─────▶ │ blob chiffré │
│ DB_PASSWORD=... │ ◀────── │ \0GITCRYPT\0... │ ◀───── │ │
└────────────────────┘ smudge └────────────────────┘ fetch └──────────────┘

La sélection des fichiers se fait dans .gitattributes, versionné avec le reste :

# Tous les fichiers .env, à n'importe quelle profondeur
**/.env filter=git-crypt diff=git-crypt
# Un fichier précis
secrets/prod.key filter=git-crypt diff=git-crypt
# Un sous-arbre complet : dir/** et non dir/ ni dir/*
secrets/** filter=git-crypt diff=git-crypt

La syntaxe est celle de .gitignore, à une exception près : un répertoire seul (secrets/) ne chiffre pas son contenu, et secrets/* ne descend pas dans les sous-répertoires. .gitattributes ne doit jamais être chiffré lui-même ; la ligne .gitattributes !filter !diff l'exclut si un joker trop large le couvre.

Le lien entre l'attribut et le binaire est écrit dans .git/config par git-crypt init ou unlock (filter.git-crypt.clean, .smudge, .required, diff.git-crypt.textconv). Ce fichier n'étant pas versionné, un clone neuf laisse les fichiers protégés chiffrés : c'est la dégradation gracieuse, un contributeur sans clé peut cloner et committer le reste du dépôt.

Modèle cryptographique​

Chaque fichier est chiffré en AES-256 en mode CTR, avec un vecteur d'initialisation synthétique dérivé d'un HMAC-SHA1 du contenu du fichier. Le chiffrement est donc déterministe : un même contenu produit toujours le même chiffré.

Ce déterminisme est imposé par Git : si chaque passage du filtre clean produisait un chiffré différent, git status signalerait en permanence tous les fichiers protégés comme modifiés. Le mode retenu est prouvé sémantiquement sûr face à une attaque à clair choisi déterministe : il ne révèle que l'égalité ou non de deux fichiers. Conséquences concrètes :

  • égalité visible : deux .env identiques produisent le même blob, donc le même identifiant d'objet : un observateur sait que deux services partagent les mêmes secrets, sans les lire ;
  • historique visible : le moment où un fichier change, et sa taille, restent observables dans git log ;
  • pas de compression delta : chaque version est stockée en entier, sans impact pour quelques .env, notable pour des fichiers volumineux ;
  • pas d'intégrité du dépôt : quiconque peut écrire dans le dépôt peut modifier .gitattributes pour désactiver le chiffrement des commits suivants ; les commits ou tags signés couvrent ce risque.

Tout ce qui n'est pas du contenu de fichier reste en clair : noms de fichiers, messages de commit, cibles de liens symboliques, sous-modules. Un fichier nommé aws-root-credentials.env annonce son contenu même chiffré.

Clé symétrique ou utilisateurs GPG​

git-crypt init génère une clé de chiffrement stockée dans .git/git-crypt/, hors de l'historique. Deux façons de la partager coexistent.

Clé symétrique exportée. La clé est écrite dans un fichier binaire, à transmettre par un canal sûr (gestionnaire de mots de passe, coffre de secrets). Aucune dépendance à GPG, aucun fichier ajouté au dépôt.

# Exporter la clé dans un fichier (droits 0600 recommandés)
git-crypt export-key ~/.config/git-crypt/infra.key

# Exporter sur la sortie standard, par exemple pour l'encoder en base64
git-crypt export-key - | base64 -w0

Utilisateurs GPG. La clé du dépôt est chiffrée pour la clé publique de chaque utilisateur autorisé, puis commitée dans .git-crypt/ à la racine (--no-commit pour différer le commit).

# Autoriser un utilisateur par empreinte complète (identifiants courts à éviter)
git-crypt add-gpg-user 0123456789ABCDEF0123456789ABCDEF01234567

Le mode GPG évite de faire circuler un fichier de clé mais impose un trousseau GPG sur chaque poste et runner ; le mode symétrique se prête mieux à l'automatisation. L'option -k NOM de init, unlock, export-key et add-gpg-user permet de gérer plusieurs clés dans un même dépôt (par exemple une clé ci et une clé prod), chaque motif de .gitattributes utilisant alors filter=git-crypt-NOM diff=git-crypt-NOM.

Mise en place​

# Initialiser git-crypt dans le dépôt (génère la clé locale)
git-crypt init

# Déclarer les motifs AVANT d'ajouter le moindre secret
echo '**/.env filter=git-crypt diff=git-crypt' >> .gitattributes
git add .gitattributes
git commit -m "Chiffre les fichiers .env via git-crypt"

# Retirer .env de .gitignore s'il y figurait, puis ajouter les secrets
git add api/.env worker/.env
git commit -m "Ajoute les fichiers de secrets chiffrés"

# Vérifier ce qui est chiffré
git-crypt status -e

Sur une autre machine, après un clone :

# Déchiffrer le répertoire de travail avec la clé symétrique
git-crypt unlock ~/.config/git-crypt/infra.key

# Ou, en mode GPG, avec la clé privée présente dans le trousseau
git-crypt unlock

# Rechiffrer le répertoire de travail (retire la clé locale)
git-crypt lock

unlock exige un répertoire de travail propre, puisqu'il réécrit les fichiers protégés ; lock aussi, sauf avec -f au prix des modifications en cours. Une fois débloqué, le dépôt s'utilise normalement : un serveur de déploiement déverrouillé une fois reçoit les secrets à jour à chaque git pull, le filtre smudge déchiffrant au passage.

Le piège du fichier commité avant la règle​

Les filtres ne s'appliquent qu'au moment où un fichier passe dans l'index. Un fichier ajouté avant que la règle .gitattributes n'existe est stocké en clair, et le reste même après l'ajout de la règle. git-crypt status détecte ce cas :

not encrypted: .gitattributes
encrypted: api/.env
encrypted: legacy.env *** WARNING: staged/committed version is NOT ENCRYPTED! ***

Warning: one or more files is marked for encryption via .gitattributes but
was staged and/or committed before the .gitattributes file was in effect.
Run 'git-crypt status' with the '-f' option to stage an encrypted version.

git-crypt status -f place une version chiffrée dans l'index, à committer. L'outil prévient lui-même que les versions en clair déjà commitées subsistent dans l'historique : le secret est compromis et doit être changé à la source. Réécrire l'historique (git filter-repo) réduit l'exposition sans effacer les clones et forks existants.

Révocation et rotation : la limite majeure​

git-crypt ne propose aucune révocation : ni del-gpg-user symétrique à add-gpg-user, ni rotation de la clé symétrique. La raison est structurelle : quiconque a détenu la clé déchiffre tout l'historique antérieur, et une copie du dépôt suffit à y accéder indéfiniment.

Le départ d'un membre d'équipe ou la fuite d'un fichier de clé impose donc de changer tous les secrets contenus dans les fichiers chiffrés, seule mesure réellement protectrice, puis, éventuellement, de repartir d'une nouvelle clé pour les commits futurs. git-crypt convient ainsi à un périmètre de confiance stable, beaucoup moins à une équipe dont la composition change souvent.

Usage en CI​

Un runner éphémère part d'un clone neuf, donc chiffré. La méthode courante consiste à stocker la clé symétrique encodée en base64 dans un secret du dépôt :

# Poste local : produire la valeur à coller dans le secret GIT_CRYPT_KEY
git-crypt export-key - | base64 -w0
- name: Unlock git-crypt
env:
GIT_CRYPT_KEY: ${{ secrets.GIT_CRYPT_KEY }}
run: |
sudo apt-get update -qq && sudo apt-get install -y -qq git-crypt
# Décoder la clé dans un fichier temporaire du runner
echo "$GIT_CRYPT_KEY" | base64 -d > "$RUNNER_TEMP/git-crypt.key"
git-crypt unlock "$RUNNER_TEMP/git-crypt.key"
# Supprimer la clé dès le déverrouillage terminé
rm -f "$RUNNER_TEMP/git-crypt.key"

Le fichier de clé est binaire : l'encodage base64 évite toute altération dans un secret texte. Une pull_request issue d'un fork ne reçoit pas les secrets, ce qui fait échouer l'étape ; et tout job détenant la clé accède à tous les secrets du dépôt, pas seulement à ceux dont il a besoin. Un exemple complet d'utilisation dans une chaîne de validation et de déploiement Compose est décrit dans l'article déploiement Compose par GitHub Actions.

Autres pièges​

  • Outils qui lisent le répertoire de travail. Hooks pre-commit, linters et scanners de secrets voient les fichiers en clair, pas les blobs chiffrés : un hook detect-private-key bloque le commit d'un .env contenant une clé PEM pourtant chiffrée dans l'objet Git. Les chemins couverts par git-crypt doivent en être exclus.
  • Revue de code aveugle. Sur GitHub ou GitLab, les fichiers chiffrés apparaissent comme binaires. Une modification de secret n'est pas relisible en pull request ; seul un git diff local sur un dépôt débloqué l'affiche, grâce au textconv.
  • Conflits de fusion. Deux branches modifiant le même fichier chiffré produisent un conflit sur des blobs binaires, sans marqueurs de conflit exploitables : la résolution se fait à la main sur les versions en clair.
  • Patches et clients graphiques. git apply n'applique pas un patch en clair à un fichier chiffré (git diff --no-textconv --binary produit un patch chiffré), et certains clients Git tiers n'invoquent pas correctement les filtres.
  • Fichiers non couverts. Un fichier de configuration contenant des empreintes de mots de passe ou des identifiants de client OIDC, hors des motifs de .gitattributes, reste lisible dans tout l'historique.

Comparaison avec les alternatives​

Critèregit-cryptAnsible VaultSOPSSecret Kubernetes
GranularitéFichier entierFichier ou variable (encrypt_string)Valeur (clés en clair)Objet de l'API
Transparence GitTotale (filtres)Aucune, édition via ansible-vaultAucune, édition via sopsHors Git
Gestion des clésClé symétrique ou GPGMot de passe (--vault-id)age, PGP, AWS/GCP KMS, Azure Key Vault, HashiCorp Vaultetcd, chiffrement au repos optionnel
Diff lisible en revueNon (binaire)NonOui pour les clés, pas les valeursSans objet
RévocationAucuneChangement de mot de passe (rekey), historique exposéRetrait d'une clé (updatekeys) et rotation de la clé de données (rotate)RBAC
ConsommateurTout outil lisant le fichierAnsible uniquementTout outil via sops exec-env/decrypt, intégrations Flux/HelmPods

Ansible Vault chiffre en AES-256 avec une clé dérivée d'un mot de passe ; les fichiers ne sont déchiffrés qu'en mémoire par Ansible, ce qui convient quand Ansible est le seul consommateur mais pas à un .env lu directement par Docker Compose.

SOPS chiffre les valeurs d'un fichier YAML, JSON, ENV ou INI en laissant les clés en clair : la modification d'une variable DB_PASSWORD est visible en revue, pas sa valeur. La clé de données est chiffrée pour plusieurs destinataires (age, PGP, KMS cloud) définis dans .sops.yaml. updatekeys suivi de sops rotate constitue une vraie rotation pour les versions futures, l'historique restant lisible par l'ancien destinataire. En contrepartie, chaque édition passe par la commande sops.

Les Secrets Kubernetes ne chiffrent rien côté dépôt : un manifeste de Secret est encodé en base64. Le versionner impose un chiffrement tiers (SOPS, Sealed Secrets) ou une synchronisation depuis un coffre externe.

git-crypt se distingue par sa transparence : il convient aux dépôts contenant quelques fichiers sensibles consommés tels quels (.env, clés, certificats), au sein d'un groupe de confiance stable.

Application / Projet lié​

Mis en pratique dans le projet2023 → aujourd'huiHomeLabChiffrement de l'ensemble des fichiers .env des stacks Docker Compose par le motif **/.env et une clé symétrique, déverrouillage unique sur l'hôte de déploiement (secrets mis à jour par git pull) et déverrouillage à la volée dans le workflow de validation des pull requests. Les secrets d'Authelia, y compris la clé privée de signature OIDC, transitent par ce mécanisme.Docker ComposeTraefikAutheliaGitHub ActionsRenovatePrometheusgit-crypt

Conclusion​

git-crypt ramène les secrets dans le dépôt sans changer les habitudes Git, au prix de propriétés à connaître : chiffrement déterministe qui révèle l'égalité et l'historique des modifications, métadonnées en clair, et absence de révocation. Vérifier git-crypt status avant le premier commit d'un secret, et traiter toute fuite de clé comme une fuite de tous les secrets de l'historique, couvre l'essentiel des risques.