← All posts

Les trois vies de ma clé de signature JWT

Authagonal·July 17, 2026
authjwtkey-managementkmsvaultecdsaoidc

Une seule clé privée signe chaque jeton émis par notre serveur d'authentification. Perdez-la, et un attaquant fabrique un jeton valide pour n'importe quel utilisateur, sans aucun mot de passe, et vos journaux affichent une connexion parfaitement normale. C'est le secret le plus précieux de tout le système, et au cours de son existence, cette clé a déménagé deux fois : elle est née à l'intérieur du processus applicatif, puis a été exilée dans un coffre-fort dont elle ne peut plus sortir, puis réincarnée en un type de clé entièrement différent. Chaque déplacement a eu lieu sur un émetteur en production, avec des jetons déjà en circulation et des validateurs mettant en cache un état que nous ne contrôlons pas. Voici ce qu'exigent réellement ces déplacements, et celui qui nous a le plus appris est celui que nous n'avons pas annoncé.

Première vie : la clé que le serveur gardait dans sa poche

Au commencement, la clé vivait là où elle était utilisée. Le serveur générait une clé RSA-2048, la conservait en mémoire et signait ses jetons avec RS256. C'est la configuration par défaut presque partout, et cela convient parfaitement jusqu'au moment où l'on prononce le mot garde à voix haute. La clé privée était accessible à tout ce qui pouvait lire la mémoire du processus, sa configuration, ou une sauvegarde de l'une ou de l'autre. Nous voulions pouvoir dire à nos clients qu'une base de données divulguée n'expose rien d'exploitable, et une clé de signature située à un seul processus de distance de cette base faisait de cette affirmation un mensonge. La clé devait quitter les lieux.

Deuxième vie : la clé qui a emménagé dans un coffre-fort dont elle ne peut pas sortir

Nous avons introduit une couture — un ISignatureProvider que le serveur appelle chaque fois qu'il a besoin d'une signature — et placé un KMS derrière. La clé est désormais générée à l'intérieur du moteur transit de Vault et n'est jamais exportée. L'application ne la détient pas ; elle envoie au coffre-fort les octets à signer et reçoit une signature en retour. Elle peut utiliser la clé mais ne peut pas l'exfiltrer.

Cette distinction est tout l'enjeu. Un vidage mémoire, un fichier de configuration qui fuit, une sauvegarde divulguée — aucun d'eux ne contient plus la clé, car la clé n'a jamais été dans aucun de ces endroits. Compromettre l'application permet désormais à un attaquant de demander au coffre-fort de signer, ce que nous pouvons limiter en débit, auditer et révoquer. Cela ne lui donne plus la clé elle-même, contre quoi nous ne pouvions rien faire une fois qu'elle avait disparu.

Voici la partie que nous n'avons pas mise dans les notes de version. Le même commit qui a déplacé la clé dans le coffre-fort a aussi, discrètement, divisé par deux notre nombre d'itérations PBKDF2, le faisant passer de 100 000 à 50 000. Un seul diff, deux changements de sécurité pointant dans des directions opposées : le titre annonçait « signer dans le KMS », et dissimulé en dessous se trouvait un facteur de travail du hachage de mot de passe réduit de moitié, sur une ligne que personne ne regardait parce que l'histoire portait sur autre chose. Les deux relèvent du code de sécurité, si bien qu'ils ont tous deux été relus comme un unique changement « plus sécurisé » et validés sur la foi de la bonne moitié.

Le correctif tenait en une ligne. La leçon, non. Un diff étiqueté plus sécurisé reste un diff, et les constantes sensibles pour la sécurité qu'il contient — nombres d'itérations, tailles de clés, délais d'expiration, noms d'algorithmes — n'héritent d'aucune auréole du message de commit. Nous traitons désormais un nombre de facteur de travail exactement comme nous traitons un choix d'algorithme : quelque chose que l'on affirme et que l'on vérifie, et non quelque chose que l'on croit sur parole parce que le changement qui l'entourait était une bonne idée.

Troisième vie : la clé qui a rapetissé pour aller plus vite

Avec la signature derrière un KMS, chaque signature devient un aller-retour réseau, et la signature RSA est du genre coûteux. La clé a donc changé d'espèce : RS256 sur RSA-2048 est devenu ES256 sur la courbe P-256. La signature sur courbe elliptique est environ un ordre de grandeur moins coûteuse que RSA, une signature P-256 fait 64 octets là où celle de RSA-2048 en fait 256, et l'ensemble de clés publiques que les validateurs téléchargent rétrécit d'autant. Clé plus petite, signature plus petite, JWKS plus petit, jetons plus rapides — tout cela en choisissant simplement une meilleure courbe.

Le piège, c'est qu'on ne peut pas permuter d'un coup l'algorithme de signature sur un émetteur en production. Chaque jeton déjà émis a été signé en RS256 et doit continuer à se valider jusqu'à son expiration. Chaque validateur a votre ancienne clé publique en cache. Changez l'algorithme d'un seul geste, et vous invalidez au même instant tous les jetons en circulation et tous les ensembles de clés en cache — une panne que l'on s'inflige à soi-même, déguisée en mise à niveau.

Ce qui a fonctionné, c'est de cesser de faire comme s'il n'y avait jamais eu qu'une seule clé. Le magasin de clés est devenu agnostique vis-à-vis de l'algorithme : il détient la clé RSA et la clé EC en même temps, et publie les deux dans le JWKS, chacune sous son propre identifiant de clé et son propre algorithme. Un sélecteur de clé active distinct décide quelle clé signe les nouveaux jetons, et il s'écarte de lui-même de l'ancienne clé RSA dès qu'une clé EC existe — aucun indicateur de configuration, aucun déploiement calé pour coïncider avec une rotation. Pendant le recouvrement, le document de découverte annonce les deux clés publiques, si bien que les jetons signés avec l'une ou l'autre continuent de se valider. Une fois le dernier jeton RS256 expiré, la découverte n'annonce plus que ES256 et la validation fixe ValidAlgorithms = ES256, ce qui claque aussi la porte au nez des attaques par confusion d'algorithme qui cherchent à convaincre un validateur d'accepter le mauvais schéma. La migration est un fondu enchaîné que l'émetteur exécute sur lui-même, pas un interrupteur que quiconque actionne.

La leçon, factorisée

Une clé de signature ressemble à une constante : un secret, défini une fois, référencé pour toujours. La nôtre ne l'était pas. Au cours de sa vie, elle a changé de garde, du processus vers un KMS, et elle a changé d'espèce, de RSA à la courbe elliptique, et les deux déplacements ont dû se faire avec des jetons en circulation et des validateurs mettant en cache des choses qui ne nous appartiennent pas. La propriété qui a rendu chaque déplacement survivable était la même les deux fois : le système n'a jamais supposé qu'il existait exactement une clé, un algorithme ou un seul foyer. Construisez le magasin de clés pour qu'il détienne plusieurs clés et qu'il annonce honnêtement lesquelles il détient, et la rotation — de l'endroit où vit la clé ou du type de clé — cesse d'être une panne que l'on planifie pour devenir une propriété que l'émetteur gère de lui-même.

Si vous exploitez un service d'authentification, l'objet qui signe vos jetons devrait être le plus ennuyeux, le plus inspectable et le moins astucieux que vous possédiez. Il a fallu trois vies au nôtre pour y parvenir. Chacune en valait la peine.