Je publie ce post-mortem parce qu'il s'est passé sur un de mes projets. Pas un client. Un POC perso. Et c'est précisément le moment où l'on baisse la garde — donc le moment où il faut écrire publiquement ce qu'on a raté, pour ne pas que d'autres le rejouent.

Le moment du choc

Mardi matin, j'ouvre ma boîte mail. Une notification du fournisseur de l'API LLM que j'utilisais sur un POC. Je lis trois fois pour être sûr : 798 € consommés en 48 heures sur une clé que je pensais cantonnée à mes propres tests.

Aucun utilisateur réel sur ce POC. Pas de trafic. Juste moi qui débuggais le matin et le soir. Et pourtant, des centaines de milliers de requêtes étaient passées. Quelqu'un — ou plutôt quelque chose — utilisait ma clé.

L'erreur n°1 : un fichier que j'ai zappé à la revue

C'est un truc que je connais. Quinze ans que je code, je sais qu'une variable préfixée VITE_*, NEXT_PUBLIC_* ou REACT_APP_* est injectée dans le bundle public. Réflexe acquis, pas un truc que j'oublie sur du code « sérieux ».

Sauf que là, c'était un POC. Avec un agent IA qui produit beaucoup de code, vite. Plusieurs dizaines de fichiers générés en quelques minutes. Je n'ai pas la capacité — personne ne l'a — de tout relire ligne à ligne à ce rythme. Je me suis concentré sur le plus sensible côté sécurité : la logique métier, les appels d'API serveur, les règles d'accès.

Et un fichier de config côté front est passé sous le radar. Bêtement. Une clé Gemini qui s'est retrouvée injectée dans le bundle JavaScript public, accessible à n'importe qui ouvrant les DevTools — ou téléchargeant simplement le bundle.

Des bots scannent en continu les sites publics à la recherche de patterns connus de clés (AIzaSy…, sk-…, pk_live_…). Une fois la clé trouvée, ils la testent immédiatement, puis la revendent ou la consomment eux-mêmes. Délai entre la mise en ligne du bundle et la première requête frauduleuse : moins de deux heures. Mesuré sur les logs.

Le piège, ce n'est pas que je ne savais pas. C'est qu'à la vitesse de la génération assistée, l'œil saute exactement le bon endroit. La responsabilité reste entière de mon côté : c'est à moi d'avoir une discipline d'audit que la rapidité ne fait pas sauter.

L'erreur n°2 : alerte ≠ limite

Le pire ? Je pensais avoir mis un garde-fou. Quand j'avais activé la facturation chez le fournisseur, je m'étais dit : « je règle une limite à 50 € pour être tranquille ». Sauf qu'en pratique, j'avais réglé une alerte, pas une limite. Deux UI très proches dans la console. Deux comportements radicalement différents.

  • Une alerte envoie un email quand un seuil est dépassé. Elle ne coupe rien.
  • Une limite coupe la consommation au seuil, point.

L'email d'alerte est bien parti. Mais comme la consommation a explosé en quelques heures, l'email est arrivé alors que j'avais déjà 600 € de dépassement. Et j'ai bouclé en revendant à 800 € avant de pouvoir révoquer la clé.

Ce qu'il aurait fallu faire

La règle est simple, et elle ne souffre aucune exception : une clé secrète ne va jamais dans un bundle qui sera servi à un navigateur ou compilé dans une app mobile. Jamais. Même pour un POC. Même pour aller vite.

L'alternative :

  1. Une route backend qui wrappe l'appel. Cloud Function, API serveur, Edge Worker. Le client appelle votre route. Votre route appelle le fournisseur avec la clé secrète, qui ne quitte jamais le serveur.
  2. Pour les flux temps réel (WebRTC, Realtime API), un ephemeral token : le serveur génère un jeton de session court (quelques minutes), valable pour un seul usage, qui est passé au client. La clé maîtresse ne sort pas.

Coût en complexité : une route Cloud Function de 30 lignes. Coût économisé : l'incident à 800 €. Et surtout : la tranquillité d'esprit de pouvoir publier un POC sans être obligé de monitorer la facturation toutes les six heures.

Les exceptions légitimes

Pour qu'on soit clairs : certaines clés sont conçues pour vivre côté client. Ne tombez pas dans la paranoïa qui consiste à tout backender. Vivent côté front :

  • Les Firebase Web API keys (elles identifient un projet, ne donnent pas d'accès — la sécurité passe par les Firestore rules + App Check).
  • Les reCAPTCHA site keys.
  • Les Stripe publishable keys (pk_live_* ou pk_test_*) — jamais les sk_*.
  • Les Google Maps API keys restreintes par HTTP referrer.

Toutes les autres — Anthropic, OpenAI, Vertex, Stripe secret, etc. — sont des clés serveur. Sans exception.

La règle : si la clé donne directement accès à une API qui facture, elle ne va pas dans un bundle. Si elle ne fait qu'identifier un projet et que la sécurité est ailleurs (rules, App Check, restriction par domaine), elle peut y aller.

Bloquer la clé avant qu'elle n'atterrisse dans un fichier

Un scan en CI, c'est bien. Mais c'est tard. La clé est déjà dans le repo, déjà dans l'historique git, déjà potentiellement publique si quelqu'un a fait git push entre-temps. Plus on bloque en amont, mieux on dort.

Le moment vraiment en amont, quand on travaille avec un agent IA comme Claude Code, c'est l'instant où l'agent s'apprête à écrire un fichier. Si on intercepte là, le secret n'arrive même pas dans le projet.

J'ai donc installé un hook PreToolUse dans ma configuration Claude Code globale. Concrètement, c'est un script Python qui s'exécute avant chaque action Write ou Edit de l'agent, et qui scanne le contenu à écrire :

  • Variables d'environnement front dangereuses : préfixes VITE_*, NEXT_PUBLIC_*, REACT_APP_*, EXPO_PUBLIC_*, PUBLIC_*, combinés avec API_KEY, SECRET, TOKEN, etc.
  • Clés en dur : Anthropic, OpenAI, Stripe secret, AWS, GitHub, Google, Slack, GitLab, clés PEM.

Si un pattern match, le hook renvoie un code exit 2 et l'écriture est bloquée. L'agent reçoit le message d'erreur en clair : « secret détecté ; déplacer la clé côté serveur, ou ajouter un faux positif à l'allowlist ». L'agent corrige et propose une alternative — généralement une route backend.

Deux raffinements importants :

  • Allowlist dans ~/.claude/secrets-allowlist.txt pour les clés publiques par design (Firebase web, Stripe pk_*, reCAPTCHA, Google Maps restreintes par referrer). Une regex par ligne, commentaires en #.
  • Skip des fichiers de doc (.md, .txt, README, CHANGELOG, CLAUDE.md) pour qu'un article comme celui-ci, qui parle de patterns de clés, ne déclenche pas le hook à chaque relecture.

Coût d'installation : un fichier Python d'une centaine de lignes, une déclaration de cinq lignes dans ~/.claude/settings.json. Coût en latence : invisible, le scan tourne en quelques millisecondes.

Effet observé : depuis que ce hook tourne, l'agent IA ne peut tout simplement plus me trahir sur ce sujet. Et plus subtil : je n'ai plus à y penser. La règle est dans l'outil, pas dans ma vigilance.

Le scan automatisé avant chaque deploy

Depuis cet incident, j'ajoute systématiquement un script de scan dans tous mes projets — appelé en CI ou en pre-deploy. Quelques lignes de bash qui inspectent le bundle final à la recherche des patterns de clés connues :

# scripts/security-smoke.sh — extrait
grep -rE "sk-(proj-|live-|test-)[A-Za-z0-9]{20,}" dist/
grep -rE "AIzaSy[A-Za-z0-9_-]{33}" dist/ | grep -v firebase
grep -rE "pk_live_[A-Za-z0-9]{24,}" dist/  # Stripe public, OK
grep -rE "sk_live_[A-Za-z0-9]{24,}" dist/  # Stripe secret, ALERTE

Si le grep trouve une clé sensible, le script exit 1 et la CI casse. Le deploy est bloqué. Trente secondes par build. Aucune raison de ne pas le mettre.

Leçons pour un PO ou un chef de projet

Cet incident est technique en surface, mais ses causes sont de gouvernance. Trois choses qu'un PO ou un CP devrait imposer sur tout projet, dès le premier jour :

  1. Inventaire des secrets dès le cadrage. Quelles clés vont être manipulées ? Lesquelles facturent à l'usage ? Lesquelles vivent côté client, lesquelles côté serveur ? Un tableau d'une page suffit.
  2. Garde-fous facturation explicites. Pour chaque service à l'usage : a-t-on une limite dure (pas une alerte) configurée ? Captures d'écran à l'appui dans la doc projet.
  3. Hook au niveau de l'agent IA. Si l'équipe utilise Claude Code, Cursor ou équivalent, configurer un hook qui bloque l'écriture de patterns de clés sensibles. Le secret n'atteint plus le repo, point.
  4. Scan pre-deploy automatisé. Filet de sécurité supplémentaire dans la CI, pour rattraper ce qui aurait pu passer (commit manuel, hook désactivé, machine d'un nouvel arrivant).

Quand un projet est mené avec un agent IA qui code à grande vitesse, ces garde-fous sont encore plus critiques. L'agent ne sait pas — par défaut — qu'une variable VITE_* finit dans le bundle. Si vous ne lui dites pas, et si personne ne relit, l'erreur arrive. C'est le rôle du PO de poser ces règles dans le contexte projet (le fameux CLAUDE.md ou équivalent), pour que l'agent les respecte automatiquement.

Ce que je vérifie désormais

Avant toute mise en ligne — POC, prototype, démo client, n'importe quoi qui touche un domaine public — je passe une checklist de cinq minutes :

  • ✓ Le hook PreToolUse Claude Code est actif : toute tentative d'écrire un pattern de clé sensible est bloquée à la source.
  • ✓ Aucune variable VITE_*, NEXT_PUBLIC_*, REACT_APP_*, EXPO_PUBLIC_* ne contient un secret qui facture.
  • ✓ Toutes les clés serveur sont en variables d'environnement backend, jamais commitées.
  • ✓ Pour chaque service facturé à l'usage : une hard limit est configurée, pas seulement une alerte.
  • ✓ Le script security-smoke.sh tourne en CI et casse le build si une clé sensible est détectée.
  • ✓ Les logs de consommation sont monitorés pendant les 48 premières heures post-mise en ligne.

Cinq lignes. Cinq minutes. Huit cents euros sauvés. Et, plus important encore : l'esprit libre pour penser au produit, pas aux fuites.

L'IA agentique permet d'aller dix fois plus vite. C'est aussi pour ça qu'elle peut briser dix fois plus vite. Le rôle du pilote — PO, lead dev, chef de projet — c'est de poser les garde-fous avant que l'agent n'accélère, pas après.

Un projet à sécuriser ?

Vous lancez un POC, une démo, un MVP, et vous voulez être sûr de ne pas répéter cette erreur ? Envoyez-moi deux lignes sur ce que vous construisez. Je vous dis ce qu'il faut verrouiller avant de pousser en ligne.

Écrivez-moi