Protéger ses secrets des LLM
Sujet intermédiaireAu vu des échanges que je peux avoir avec des collègues ou dans les commentaires sur Linkedin, il semble que beaucoup d'entre nous partagions la même inquiétude sur la protection de nos variables d'environnement.
Dans cet article, on commence par décrire ce sujet, puis on propose des moyens pour protéger ses variables d'environnement tout en nous laissant une grande liberté de travail.
Le problème
Mettons que j'ai construit un SaaS ou n'importe quel site contenant un certain nombre de secrets. Par exemple :
- clé secrète Supabase, qui me permet d'avoir accès à toute ma base de données : contacts, réservations, etc.
- secret du webhook Stripe : qui permet de reconnaître les événements envoyés par Stripe, notamment les notifications de paiement
- clé Resend : clé secrète également qui me permet de consommer mon budget mail Resend.
Si j'enregistre ces secrets dans un fichier .env ou .env.local , n'importe quel harnais est en mesure de les lire, nativement. Problème, à moins d'avoir son LLM en local, ces secrets vont alors se retrouver sur les serveurs du fournisseur : Anthropic, OpenAi, etc.
La méthode la plus simple pour protéger ces secrets est alors de ne jamais les définir en local, et de ne les mettre que sur mon serveur (par exemple Vercel).
C'est la solution qui est appliquée par un certain nombre de personne et la solution que j'employais jusqu'à il y a encore quelques semaines.
Toutefois, elle est peu satisfaisante :
- elle gène beaucoup la partie développement : les tests ne peuvent être réalisés qu'en production.
- on ne peut faire réaliser automatiquement par notre harnais les migrations de bases de données
- etc.
Les cas d'usages où l'on souhaiterait que notre IA puisse accéder à ces secrets sont en réalité légions.
Au final, on peut séparer les trois attitudes à l'égard des fichiers d'environnement, en :
- le "yolo", je donne tous mes secrets à l'IA
- le "parano", je mets les secrets sur l'hébergeur
- le serein, je mets trois couches de protections, que je décris ci-dessous.
La solution
Voici la solution que j'expérimente depuis quelques semaines et qui semble à la fois procurer la sécurité nécessaire et la flexibilité d'usage.
Les secrets sont protégés à 3 niveaux : des règles "deny", un "hook", une consigne.
A/ les règles "deny"
Commençons par le plus simple. Si je veux que Claude ou n'importe quel harnais ne puisse par lire mes variables d'environnement il faut commencer par lui interdire simplement cette capacité.
Pour Claude, voilà une ligne "deny" à intégrer dans votre fichier ~/.claude/settings.json :
{"permissions": {
"deny": [
"Edit(**/.env)",
"Edit(**/.env.*)",
"Read(**/.env)",
"Read(**/.env.*)"
]
},
}
Que fait cette ligne ? Et bien, comme elle l'indique, elle ajoute des règles "Read" et "Edit" sur les fichiers d'environnement.
Un point important :
- la règle "Read" couvre l'outil "Read", mais aussi, en best effort, "Grep" et "Glob". En "best effort", c'est le terme de la documentation officiel de Claude. Elle essaie au mieux d'appliquer les règles sur ces outils, mais ne peut le garantir. Read reçoit un chemin précis, donc c'est facile de garantir que l'outil est fiable; Grep et Glob reçoivent des dossiers donc Claude doit filtrer chaque fichier trouvé au passage. La doc ne promet pas que le filtrage soit parfaitement fiable. Il doit être très légèrement probabiliste, mais pas de panique, notre hook plus bas va attraper encore un certains nombres de cas.
- la rêgle "Edit" couvre tous les outils qui modifient les fichiers, donc "Edit", "Write", et "NotebookEdit".
Ces règles sont elles utiles ? Oui.
Sont elles suffisantes ? Non. Car Claude va tout de suite tenter de les contourner. Par exemple en utilisant l'outil "bash" pour lire les variables. (par exemple une commande bash qui lance : cat .env )
Comment bloquer l'outil bash sur les fichier d'environnement ? Via un hook.
B/ Le hook
Un hook est un fichier qui s’exécute de façon déterministe (obligatoire). C'est la grande différence avec les outils qui sont demandés par Claude, qui eux, sont probabilistes. Les règles deny ci-dessus sont majoritairement déterministes également (mis à part le petit laïus ci-dessus sur le fait que les règles sont légèrement probabilistes pour les outils Glob et Grep).
Pour couvrir un plus grand nombre d'alertes sur la lecture de secrets, on rajoute donc aux règles deny, un hook qui va interdire l’exécution de certaines actions en lien avec les fichiers d'environnement.
Le voici ci-dessous. Et voici son emplacement : ~/.claude/hooks/protect-env.sh
#!/usr/bin/env bash
# PreToolUse hook: keep secrets from .env files out of Claude's context.
# Exit code 2 blocks the tool call; stderr is shown to Claude as the reason.
input=$(cat)
tool=$(jq -r '.tool_name' <<<"$input")
# A .env file path (".env", ".env.local"...), but not "process.env" nor ".env.example"
env_file_re='(^|[^A-Za-z0-9_])\.env(\.[A-Za-z0-9_-]+)*([^A-Za-z0-9_.-]|$)'
env_example_re='\.env\.example'
# Logging environment variables
log_env_re='console\.[a-z]+\([^)]*process\.env'
block() {
echo "Blocked by .claude/hooks/protect-env.sh: $1" >&2
exit 2
}
case "$tool" in
Bash)
cmd=$(jq -r '.tool_input.command // ""' <<<"$input")
stripped=$(sed -E "s/$env_example_re//g" <<<"$cmd")
grep -Eq "$env_file_re" <<<"$stripped" && block "command touches a .env file"
grep -Eq '(^|[^A-Za-z0-9_-])printenv([^A-Za-z0-9_-]|$)' <<<"$cmd" && block "printenv dumps the environment"
grep -Eq '(^|[;&|(]|&&|\|\|)[[:space:]]*env[[:space:]]*($|[;&|)])' <<<"$cmd" && block "bare env dumps the environment"
grep -Eq "$log_env_re" <<<"$cmd" && block "command logs process.env"
;;
Edit|Write)
content=$(jq -r '(.tool_input.new_string // "") + "\n" + (.tool_input.content // "")' <<<"$input")
grep -Eq "$log_env_re" <<<"$content" && block "code logs process.env"
;;
esac
exit 0
Décrivons maintenant ce que fait ce hook. Globalement il va rechercher un certain nombre de pattern dans la commande qui doit être lancée. Si la commande contient ces pattern, l’exécution de la commande est bloquée.
Et montrons maintenant dans le tableau ci-dessous le résultat de l’exécution du hook sur un certain nombre d'actions.
| Action | Résultat |
|---|---|
| cat .env.local | bloqué |
| node --env-file=.env.local script.js | bloqué |
| git commit -m "ignore .env" | bloqué (le texte contient le nom) |
| cat .env.example | autorisé |
| printenv | bloqué |
| npm run build | autorisé |
| écrire console.log(process.env.KEY) | bloqué |
| écrire const key = process.env.KEY | autorisé |
On peut être satisfait d'avoir maintenant éliminé le cas n°1 : cat .env.local qui n'était pas couvert par la règle deny. On a aussi bien couvert la commande printenv , très problématique pour nous. Et on empêche aussi Claude de contourner écrivant dans un fichier une commande qui elle-même écrirait le nom de la commande : console.log(process.env.KEY) .
Mais notons que nous avons aussi rajouté quelques faux positifs : les cas 2 et 3, qu'a priori nous aurions plutôt voulu conserver. C'est le coût (pour moi relativement minimal) de ce genre de règles.
En réalité, au début, j'avais commencé à travailler seulement avec les règles deny et le hook, mais.. Claude continuait à essayer de contourner pour faire son travail. J'ai donc rajouté une dernière couche : une consigne générale.
C/ La consigne
Voici une consigne à inclure dans son fichier ~/AGENTS.md :
Secrets et variables d'environnement (règle absolue) :
- Ne lis jamais un fichier `.env*` (sauf `.env.example`) et n'affiche jamais la valeur d'une variable d'environnement, même de façon détournée : variable intermédiaire, `JSON.stringify`, message d'erreur, route de debug, fichier de test, sortie de script, encodage (base64, découpage…).
- Pour vérifier qu'une variable est définie, teste seulement sa présence (`Boolean(process.env.X)`), jamais sa valeur.
- Le hook `~/.claude/hooks/protect-env.sh` bloque toute commande Bash dont le texte cite un fichier `.env*` (sauf `.env.example`). Évitons un faux positif lié aux messages de commit : n'écris pas `.env*` dans une commande Bash, y compris dans un message de commit.
- Si un hook ou une règle de permission bloque une action liée aux secrets, ne cherche pas à le contourner : arrête-toi et demande-moi. Par exemple, pour un script qui a besoin des variables, écris-le dans le projet et donne-moi la commande `! node --env-file=.env.local ...` à lancer moi-même.
Où mettre cette consigne ?
Personnellement je la mets au niveau le plus global (~/AGENTS.md). Ce fichier est ensuite repris dans tous mes harnais via un lien symbolique.
Qu'est-ce à dire ? Et bien pour mes trois harnais Claude code, Codex et Pi, tout simplement,
- ~/.claude/CLAUDE.md contient
@~/AGENTS.md, c'est à dire dans la syntaxe Anthropic un lien symbolique vers le fichier global. - ~/.codex/AGENTS.md est un lien symbolique linux vers ~/AGENTS.md
- ~/.pi/AGENTS.md ne contient rien. Mais nativement, Pi va lire tous les AGENTS.md des dossiers parents du dossier dans lequel on travaille. Comme je travaille toujours dans un dossier enfant du dossier user
~, le fichier global est donc bien toujours pris en compte.
Maintenant, discutons du fonctionnement de cette consigne.
Un point majeur qui différencie celle-ci du hook et des règles deny est qu'elle n'est pas du tout déterministe (obligatoire), mais au contraire totalement au gré du modèle. L'intérêt de cette règle est de rajouter à Claude, ou son modèle en général, une interdiction sur son intention.
"Ne lis jamais un fichier `.env*`". En bloquant son intention, on fait confiance au modèle pour ne pas tenter de détourner les règles strictes qu'on lui a mis ci-dessus, via le hook et les règles deny.
C'est probablement pour moi la plus importantes des règles, et probablement celle qui va permettre d'éviter le plus de contournement. Bien sûr, elle n'est pas parfaite, car non déterministe, mais c'est, de toutes façons, quelque chose dont on doit faire le deuil. On ne pourra jamais être parfaitement certain d'avoir tout couvert, le risque zéro n'existant pas.
Un point qui n'est pas négligeable : le quatrième tiret de la consigne. Au démarrage je ne l'avais pas et ... Claude continuait de contourner les blocages du hook. Maintenant que ce tiret existe, claude s'arrête bien et me demande avant de contourner ! Donc, très utile et important ! Le troisième tiret lui, évite quelques faux positifs du hook.
Si vous avez aimé cet article, partagez-le sur vos réseaux !
À bientôt, sur le blog de Cours-socrate.ai
Le meilleur moyen d'apprendre, c'est de faire.
Inscrivez-vous au cours : le lundi soir, un groupe de maximum 10, Paris ou en visio.