Aller au contenu
Sécurité medium

Lade : injecter les secrets automatiquement avec des hooks shell

21 min de lecture

Vous devez manuellement charger vos secrets avant chaque commande, un source .env, un export VAULT_TOKEN=..., un eval $(op signin), et vous finissez par tout mettre dans .zshrc par paresse. Lade (prononcé /leɪd/) est une CLI open source qui injecte automatiquement les secrets dans vos processus grâce à des hooks shell transparents. Quand vous entrez dans un répertoire projet, Lade charge les secrets. Quand la commande se termine, il les nettoie.

Ce guide vous apprend à installer Lade, configurer les sources de secrets, utiliser les hooks shell automatiques et personnaliser l'injection par commande et par utilisateur. Il s'adresse aux développeurs qui veulent que leurs secrets soient toujours disponibles sans jamais être exposés.

  • Ce qu'est Lade et comment fonctionnent les hooks shell
  • Comment configurer un fichier lade.yml avec le raw loader, le file loader et les vault loaders
  • Comment filtrer les secrets par commande grâce aux regex
  • Comment gérer les secrets par utilisateur dans une équipe
  • Comment générer des fichiers de secrets temporaires (YAML, JSON) nettoyés automatiquement
  • L'injection manuelle avec lade inject pour les scripts et la CI

Lade est une CLI cross-platform (Linux, macOS) développé en Rust. Il intercepte chaque commande que vous tapez dans le shell grâce à des hooks (preexec/precmd) et injecte les secrets correspondants dans les variables d'environnement du processus, puis les supprime quand la commande se termine.

Le principe repose sur la durée de vie des secrets. Un fichier .env persiste sur le disque entre les sessions, survit aux redémarrages et finit régulièrement dans un dépôt Git par inadvertance. Lade charge les valeurs depuis leur coffre d'origine au lancement de la commande, les expose dans l'environnement du processus, puis disparaît avec lui : les secrets n'existent que pendant l'exécution, et rien n'est écrit sur le disque.

AspectDétail
ObjectifInjecter les secrets automatiquement via des hooks shell, les nettoyer après exécution
ApprocheHooks shell (preexec/precmd) + injection par regex de commande
LoadersRaw (valeurs en clair), File (JSON/YAML/TOML/INI), Vault, Infisical, 1Password, Doppler, Passbolt
FiltragePar regex de commande (ex: terraform.*, python.*, .* pour tout)
Per-userSecrets différents selon l'utilisateur (équipe)
Sortie fichierGénération de fichiers YAML/JSON temporaires, supprimés après exécution
LicenceApache-2.0
LangageRust

Lade utilise les mécanismes natifs de votre shell pour intercepter chaque commande :

  1. Avant l'exécution (preexec) : Lade remonte l'arborescence de répertoires à la recherche de fichiers lade.yml, agrège les secrets qui correspondent à la commande que vous êtes sur le point d'exécuter, et les injecte dans les variables d'environnement.

  2. Après l'exécution (precmd) : Lade supprime toutes les variables d'environnement qu'il a injectées, les secrets ne persistent jamais dans la session shell.

Ce mécanisme fonctionne avec Zsh, Bash et Fish.

  • Un système Linux ou macOS
  • Un shell compatible : Zsh, Bash ou Fish
  • Rust/Cargo pour l'installation via Cargo (recommandé)
  • Optionnel : un vault (HashiCorp Vault, 1Password CLI, Infisical, Doppler, Passbolt)
  1. Installer Lade

    Fenêtre de terminal
    cargo install lade --locked

    L'option --locked impose le Cargo.lock publié par le projet : les dépendances compilées sont exactement celles que l'auteur a testées, et non les dernières versions disponibles au moment de votre build. C'est la différence entre une compilation reproductible et une compilation qui peut embarquer une dépendance publiée entre-temps.

    Le README amont propose aussi un curl ... | bash sur installer.sh : ne l'utilisez pas, il exécute du code distant sans aucune vérification d'intégrité. Si vous préférez un binaire pré-compilé à la compilation Rust, les pages de release GitHub publient depuis la v0.15.3 une archive et son fichier .sha256 par plateforme, à contrôler avec sha256sum --check avant extraction.

  2. Installer les hooks shell

    Cette commande ajoute les hooks dans votre profil shell (.zshrc, .bashrc ou config.fish) :

    Fenêtre de terminal
    lade install
  3. Redémarrer votre shell

    Fenêtre de terminal
    exec $SHELL
  4. Vérifier l'installation

    Fenêtre de terminal
    lade --version

    Résultat attendu :

    lade 0.13.0

Le fichier lade.yml se place à la racine de votre projet. Sa structure est simple : une regex de commande comme clé, et les secrets comme valeurs.

lade.yml
.*:
DB_HOST: localhost
DB_PORT: "5432"
DB_USER: devuser
DB_PASSWORD: s3cr3t_p4ssw0rd
API_KEY: sk-test-1234567890abcdef

La clé .* est une regex qui matche toutes les commandes. Chaque variable est injectée dans l'environnement de chaque commande exécutée dans ce répertoire (ou ses sous-répertoires).

La puissance de Lade réside dans le filtrage par regex de commande. Vous pouvez injecter des secrets différents selon l'outil utilisé :

lade.yml
# Secrets communs à toutes les commandes
.*:
DB_HOST: localhost
DB_PORT: "5432"
DB_USER: devuser
# Secrets pour Terraform uniquement
terraform.*:
TF_VAR_db_password: s3cr3t_terraform
AWS_REGION: eu-west-1
AWS_ACCESS_KEY_ID: AKIAIOSFODNN7EXAMPLE
# Secrets pour Python/Node uniquement
python.*|node.*:
API_KEY: sk-test-1234567890abcdef
DB_PASSWORD: s3cr3t_p4ssw0rd

Avec cette configuration :

  • terraform plan reçoit DB_HOST, DB_PORT, DB_USER, TF_VAR_db_password, AWS_REGION et AWS_ACCESS_KEY_ID
  • python3 app.py reçoit DB_HOST, DB_PORT, DB_USER, API_KEY et DB_PASSWORD
  • ls reçoit uniquement DB_HOST, DB_PORT et DB_USER

Cette approche limite l'exposition : Terraform n'a pas accès à l'API key, et Python n'a pas accès aux credentials AWS.

Lade supporte 7 loaders pour récupérer les secrets depuis différentes sources.

Le loader par défaut injecte les valeurs telles quelles. Préfixer une valeur par ! force l'utilisation du raw loader (utile pour les valeurs qui commencent par un protocole comme vault://) :

lade.yml
.*:
SIMPLE_VALUE: hello_world
ESCAPED_VALUE: "!vault://this-is-not-a-vault-ref"

Le file loader lit les secrets depuis un fichier JSON, YAML, TOML ou INI local. Le paramètre query utilise la syntaxe JMESPath pour extraire des champs spécifiques :

lade.yml
.*:
DB_HOST: "file://secrets.json?query=.database.host"
DB_PORT: "file://secrets.json?query=.database.port"
DB_PASSWORD: "file://secrets.json?query=.database.password"

Avec le fichier secrets.json :

secrets.json
{
"database": {
"host": "db.prod.example.com",
"port": 5432,
"password": "pr0d_p4ss"
}
}

Le chemin peut être relatif au répertoire du lade.yml, commencer par ~ ou $HOME, ou être absolu.

C'est le loader le plus utilisé en entreprise, et le seul point d'attention est qu'il ne parle pas directement à l'API : Lade délègue au CLI vault installé sur votre poste. Concrètement, votre token Vault reste géré par vault login et son fichier ~/.vault-token, Lade ne le stocke jamais. Corollaire : si votre token a expiré, Lade échouera sans message explicite côté Vault.

lade.yml
.*:
DB_PASSWORD: "vault://vault.company.com/secret/myapp/DB_PASSWORD"
API_KEY: "vault://vault.company.com/secret/myapp/API_KEY"

Le format est vault://DOMAINE/MOUNT/CLE/CHAMP. L'authentification utilise le CLI vault local (vous devez être connecté via vault login).

Le piège de ce loader tient au domaine, qui s'écrit sans le suffixe /api : Lade l'ajoute lui-même au moment de l'appel. Le second champ n'est pas le nom lisible du projet mais son identifiant technique, celui qui apparaît dans l'URL de l'interface web.

lade.yml
.*:
DB_PASSWORD: "infisical://app.infisical.com/PROJECT_ID/production/DB_PASSWORD"

Le format est infisical://DOMAINE/PROJECT_ID/ENVIRONNEMENT/NOM_SECRET. Lade ajoute automatiquement /api au domaine. L'authentification utilise le CLI infisical local.

Le domaine de l'URI correspond au compte 1Password, pas au serveur : il s'écrit sous la forme my.1password.eu ou my.1password.com selon la région d'hébergement de votre organisation. Se tromper de suffixe produit une erreur d'authentification et non une erreur de secret introuvable, ce qui rend le diagnostic déroutant.

lade.yml
.*:
DB_PASSWORD: "op://my.1password.eu/DevVault/Database/password"
API_KEY: "op://my.1password.eu/DevVault/APIKeys/production"

Le format est op://DOMAINE/VAULT/ITEM/CHAMP. L'authentification utilise soit une session op active, soit un OP_SERVICE_ACCOUNT_TOKEN (pour la CI).

Pour utiliser un token de service stocké dans un autre vault (lookup récursif) :

lade.yml
.*:
.:
1password_service_account: "vault://vault.company.com/secret/1password/service-token"
SECRET: "op://my.1password.eu/DevVault/Item/field"

Doppler organise ses secrets en projet puis environnement (dev, staging, production), et l'URI reprend exactement cette hiérarchie. Comme pour Vault, l'accès passe par le CLI doppler authentifié localement : Lade ne gère aucun jeton propre.

lade.yml
.*:
DB_PASSWORD: "doppler://api.doppler.com/my-project/production/DB_PASSWORD"

Passbolt se distingue des autres coffres par son adressage : on ne référence pas un secret par son nom mais par un identifiant de ressource au format UUID, qu'il faut relever dans l'interface web. Le dernier segment désigne le champ à extraire de la ressource, le plus souvent password.

lade.yml
.*:
DB_PASSWORD: "passbolt://passbolt.company.com/RESOURCE_ID/password"

La colonne à regarder en premier est « CLI requis » : elle conditionne ce que vous devez installer sur chaque poste de développement et dans chaque runner de CI. Seuls le raw loader et le file loader fonctionnent sans dépendance externe, ce qui en fait les deux seules options utilisables dans un conteneur minimal.

LoaderURICLI requisAuthentification
RawValeur directeNonAucune
Filefile://chemin?query=.champNonAucune
Vaultvault://domaine/mount/clé/champvaultvault login
Infisicalinfisical://domaine/projet/env/secretinfisicalinfisical login
1Passwordop://domaine/vault/item/champopSession ou service account
Dopplerdoppler://domaine/projet/env/secretdopplerdoppler login
Passboltpassbolt://domaine/resource_id/champpassboltCLI Passbolt authentifié

Pour les scripts non-interactifs et les pipelines CI, utilisez lade inject :

Fenêtre de terminal
# Injecter les secrets et lancer une commande
lade inject -- python3 app.py
# Équivalent à "teller run" pour les utilisateurs de Teller
lade inject -- npm start
# Avec un chemin absolu si nécessaire
lade inject -- /usr/bin/python3 app.py

Résultat :

Lade loaded: DB_PASSWORD, API_KEY, DB_HOST, DB_PORT, DB_USER.
Connexion à localhost:5432
User: devuser
API Key: configurée

Lade affiche les noms des variables chargées (sans les valeurs) sur stderr, puis exécute la commande.

Quand plusieurs membres d'une équipe partagent le même projet, chacun peut avoir des secrets différents. Lade résout automatiquement l'utilisateur courant :

lade.yml
.*:
DB_HOST: localhost
DB_PORT: "5432"
DB_PASSWORD:
alice: alice_db_password
bob: bob_db_password
.: default_password

Avec cette configuration :

  • bob reçoit DB_PASSWORD=bob_db_password
  • alice reçoit DB_PASSWORD=alice_db_password
  • Tout autre utilisateur reçoit DB_PASSWORD=default_password
  • Si la clé "." est absente, les autres utilisateurs ne reçoivent rien

Par défaut, Lade utilise le nom du compte système. La commande lade user permet de forcer une autre identité, ce qui sert dans deux cas concrets : vérifier qu'un collègue recevra bien les bons secrets sans emprunter son poste, et faire correspondre un runner de CI (dont le compte s'appelle souvent root ou runner) à une entrée nommée ci dans le lade.yml. Le réglage est persistant : il survit à la fermeture du shell, d'où l'importance de lade user --reset une fois le test terminé.

Fenêtre de terminal
# Voir l'utilisateur courant
lade user
# Changer d'utilisateur (utile pour tester)
lade user alice
# Revenir à l'utilisateur OS
lade user --reset

Par défaut, Lade injecte les secrets en variables d'environnement. Vous pouvez aussi les écrire dans un fichier temporaire (YAML ou JSON) qui sera automatiquement supprimé après l'exécution de la commande :

lade.yml
.*:
.:
file: secrets.yml
DB_HOST: localhost
DB_PORT: "5432"
DB_PASSWORD: s3cr3t_p4ssw0rd

Quand vous exécutez une commande, Lade :

  1. Crée le fichier secrets.yml avec les secrets
  2. Exécute la commande
  3. Supprime le fichier secrets.yml

Le format du fichier est déterminé par l'extension (.yml/.yaml pour YAML, .json pour JSON).

Les hooks sont gérés par les commandes on et off :

Fenêtre de terminal
# Désactiver les hooks (temporaire, session courante)
eval "$(lade off)"
# Réactiver les hooks
eval "$(lade on)"
# Installer les hooks (permanent, modifie .zshrc/.bashrc)
lade install
# Désinstaller les hooks (permanent)
lade uninstall

La commande eval "$(lade off)" est utile pour déboguer : si une commande échoue de façon inattendue, désactivez les hooks pour vérifier si Lade est en cause.

Deux choses coûtent du temps dans un pipeline GitLab : la compilation Rust de Lade à chaque job, et l'authentification du CLI de votre coffre. Pour la première, mettez en cache ~/.cargo ou construisez une image de base contenant déjà le binaire. Pour la seconde, le VAULT_TOKEN doit être une variable masquée et protégée du projet, jamais une valeur en clair dans le .gitlab-ci.yml.

.gitlab-ci.yml
deploy:
stage: deploy
image: rust:1.78-slim
before_script:
- cargo install lade --locked
script:
- lade inject -- ./deploy.sh
variables:
VAULT_TOKEN: $CI_VAULT_TOKEN

Trois précautions structurent ce workflow. L'action checkout est épinglée par SHA et non par tag, car un tag peut être redéplacé vers un autre commit. Le bloc permissions: {} au niveau du workflow retire tous les droits par défaut du GITHUB_TOKEN, chaque job ne redemandant que ce dont il a besoin. Enfin persist-credentials: false empêche checkout de laisser le jeton dans .git/config, où n'importe quelle étape suivante pourrait le relire.

.github/workflows/deploy.yml
name: Deploy
on: [push]
permissions: {}
jobs:
deploy:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Install Lade
run: cargo install lade --locked
- name: Deploy with secrets
run: lade inject -- ./deploy.sh
env:
VAULT_TOKEN: ${{ secrets.VAULT_TOKEN }}

Les deux outils répondent au même besoin mais pas au même moment de la journée. Lisez ce tableau en gardant deux lignes en tête : le nettoyage et le filtrage. Ce sont les seules capacités que Teller ne rattrape pas, et elles comptent surtout sur un poste de développement où le shell reste ouvert des heures. À l'inverse, les lignes Scan secrets et Template désignent des usages que Lade ne couvre pas du tout : ce ne sont pas des lacunes, c'est un périmètre différent.

CritèreLadeTeller
InjectionHooks shell automatiques ou lade injectteller run (toujours explicite)
NettoyageAutomatique après chaque commandeLes secrets persistent dans le processus
FiltragePar regex de commandeTous les secrets sont injectés
Per-userOui, natifNon
Scan secretsNonOui (teller scan)
Redact logsNonOui (teller redact)
ExportNon (sortie fichier uniquement)JSON, YAML, env, CSV
TemplateNonOui (moteur Tera)
ProvidersVault, 1Password, Infisical, Doppler, Passbolt, FileVault, AWS SM, AWS SSM, GCP SM, Consul, dotenv
Héritage configRécursif (remonte l'arborescence)Fichier unique .teller.yml

En résumé : Lade est idéal pour le workflow développeur quotidien grâce aux hooks transparents et au filtrage par commande. Teller excelle pour le scanning de secrets et la manipulation programmatique (export, redact, template, copy).

La quasi-totalité des incidents avec Lade se ramène à trois questions, à poser dans cet ordre. Le binaire est-il trouvé ? Les hooks sont-ils actifs dans ce contexte, sachant qu'ils ne se déclenchent jamais hors session interactive ? La regex de commande correspond-elle bien à ce que vous tapez ? Le message Lade loaded: sert de point de repère : s'il s'affiche, la configuration est lue et le problème se situe en aval, sur la résolution d'un loader ou sur la regex.

SymptômeCause probableSolution
lade: command not foundBinaire pas dans le PATHVérifier avec which lade ou ajouter ~/.cargo/bin au PATH
Secrets non injectés en shell interactifHooks non installésExécuter lade install puis redémarrer le shell
Secrets non injectés en scriptHooks inactifs en non-interactifUtiliser lade inject -- <commande>
command failed avec lade injectCommande non trouvée dans le PATHUtiliser le chemin absolu : /usr/bin/python3
Mauvais secret per-userUtilisateur Lade différent de l'OSVérifier avec lade user, réinitialiser avec lade user --reset
Conflit entre fichiers lade.ymlHéritage récursif des répertoires parentsLe lade.yml le plus proche du CWD a priorité
vault:// non résoluCLI vault non authentifiéExécuter vault login avant d'utiliser Lade
op:// non résoluCLI op non authentifiéExécuter eval $(op signin) ou configurer un OP_SERVICE_ACCOUNT_TOKEN
Fichier de sortie non crééBloc .: absent ou mal formatéVérifier que .: { file: secrets.yml } est sous le bon regex
Error: file already existsUn fichier du même nom traîne dans le répertoire (exécution précédente interrompue)Supprimer le fichier avant de relancer : Lade refuse d'écraser une sortie existante
Lade loaded: mais variables absentesRegex de commande ne matche pasVérifier que le regex matche la commande (.*: pour tout matcher)
  • Lade injecte les secrets automatiquement grâce à des hooks shell, vous tapez simplement votre commande, sans wrapper ni préfixe.

  • Les secrets sont nettoyés après chaque commande : ils n'existent que pendant la durée de vie du processus enfant.

  • Le filtrage par regex de commande permet de n'exposer que les secrets nécessaires à chaque outil : Terraform reçoit les credentials AWS, Python reçoit les clés API, rien de plus.

  • Les secrets par utilisateur permettent à chaque membre de l'équipe d'avoir ses propres credentials tout en partageant le même lade.yml dans Git.

  • 7 loaders couvrent les principaux vaults du marché : Vault, 1Password, Infisical, Doppler, Passbolt, plus un file loader et un raw loader.

  • lade inject est l'équivalent de teller run pour les environnements non-interactifs (CI/CD, scripts).

Ce site vous est utile ?

Sachez que moins de 1% des lecteurs soutiennent ce site.

Je maintiens +700 guides gratuits, sans pub ni tracking. Un soutien, même symbolique, m'aide à couvrir l'hébergement et à garder ces ressources gratuites. Merci pour votre appui.

Le formulaire ne s'affiche pas ? Ouvrir Ko-fi dans un onglet.

Abonnez-vous et suivez mon actualité DevSecOps sur LinkedIn