mTLS : arrête de partager des clés API, authentifie-toi comme un grand
Pourquoi j'ai viré les shared secrets et basculé sur l'authentification mutuelle par certificat

J’ai débarqué sur un projet y a quelques mois. Belle stack Kubernetes. Microservices bien découpés. Tout ce qu’il faut. Sauf que dans un coin, y avait un secret Kubernetes appelé internal-api-key. Monté dans onze services. Onze. Personne savait qui l’avait créé. Personne savait lesquels des onze en avaient vraiment besoin. Et tu veux deviner depuis quand il avait pas été roté ? Deux ans. Deux putains d’années.
Partager une API key entre services, c’est de la merde. C’est un secret bearer : celui qui a la string est trusté, point barre. Ça prouve rien sur qui appelle. Ça se rotate jamais parce que c’est trop chiant. Et le jour où ça fuit — dans un log, un crash dump, un sidecar compromis — tout le monde peut faire semblant d’être tout le monde.
J’ai tout déchiré. J’ai mis du mTLS partout. Et voilà pourquoi.
Lexique
C’est là mes définitions et si elles vous déplaisent, cassez-vous je suis pas là pour discuter lexique.
- mTLS: TLS mais des deux côtés. Le client aussi montre patte blanche avec un certificat.
- X.509: Le format standard des certificats. Contient qui t’es, ta clé publique, qui a signé ton cert, et jusqu’à quand c’est valide.
- CA (Certificate Authority): L’autorité qui signe les certificats. Sans elle, personne peut prouver qu’il est pas un imposteur.
- SPIFFE: Standard d’identité pour workloads. Ça donne des URIs comme
spiffe://cluster.local/ns/payments/sa/checkout. - SVID: Le certificat qui porte l’identité SPIFFE. Court durée, rotation automatique.
- SDS: Service Discovery Service — le truc qui distribue les certs aux proxies Envoy sans les balancer dans les pods.
Le vrai problème des secrets partagés
Une API key partagée entre services, ça a trois problèmes fondamentaux :
- C’est bearer. Tu possèdes la string, t’es trusté. Y a pas d’identité du caller. N’importe quel service qui a la clé peut se faire passer pour n’importe quel autre.
- Ça se rotate jamais. Parce que pour rotate une clé partagée par onze services, faut une coordination de ouf. Déploiement synchronisé, risque de casse. Du coup on laisse pourrir.
- Ça prouve rien. Un log qui affiche
Authorization: Bearer xxxxxte dit pas si c’est le service checkout ou un attaquant qui a chopé la clé dans un crash dump.
Et le pire dans tout ça : même avec une API key par service, t’as toujours le problème du bearer token. Un JWT signé, c’est mieux, mais ça reste un token qui peut fuiter et être réutilisé jusqu’à son expiration.
mTLS : comment ça marche
mTLS, c’est du TLS standard (le même que ton navigateur utilise pour HTTPS), avec une différence : les deux parties s’authentifient.
Dans un HTTPS classique, c’est le client qui vérifie l’identité du serveur. Le serveur présente son certificat, le client regarde si c’est signé par une CA connue, et hop.
Avec mTLS, le serveur demande aussi un certificat au client. Si le client peut pas en présenter un valide, la connexion est coupée avant d’échanger le moindre octet.
Le handshake en TLS 1.3 :
Client Serveur
| |
|--- Je veux me connecter (ciphers) ------>|
| |
|<---- Voilà mon certificat |
|<---- Et au fait, prouve qui t'es --------| ← "CertificateRequest"
|<---- Signature handshake ----------------|
|<---- OK c'est bon -----------------------|
| |
|---- Voilà MON certificat -------------->| ← Le client montre son identité
|---- Signature handshake -----------------| ← Preuve qu'il a la clé privée
|---- OK c'est bon -----------------------|
| |
|=========== Données chiffrées ==========>|
Ce qui est important : le serveur vérifie que le certificat du client est signé par une CA qu’il truste, qu’il est pas expiré, et que le client possède bien la clé privée (grâce à la signature du handshake). Si un de ces checks pète, la connexion est refusée. Pas de 403, pas de message d’erreur — la connexion TCP est juste fermée.
L’architecture : PKI privée et identités courte durée
Pour que mTLS marche, faut une Private PKI. Pas besoin de Let’s Encrypt ou d’une CA publique — tu déploies ta propre hiérarchie.
[Root CA] — offline, dans un coffre
|
[Intermediate CA] — tourne dans le cluster
|
+-- [Certificat checkout] (durée 24h)
+-- [Certificat ledger] (durée 24h)
+-- [Certificat payments] (durée 24h)
La Root CA est gardée offline. Elle sert à signer l’Intermediate CA une fois par an, puis on la planque. L’Intermediate CA tourne dans le cluster et signe les certificats des workloads.
Chaque certificat de workload :
- Est lié à un ServiceAccount Kubernetes
- Porte une identité SPIFFE dans le SAN :
spiffe://cluster.local/ns/payments/sa/checkout - Expire en 24h (parfois moins pour les workloads critiques)
- Est roté automatiquement sans downtime
- Stocké en tmpfs — la clé privée est jamais écrite sur le disque
Pourquoi 24h ? Parce qu’un certificat qui expire vite, c’est un certificat qui se révoque tout seul par le temps. Pas besoin de CRL, pas besoin d’OCSP — tu laisses le temps faire le boulot.
Déploiement avec Istio
Bon, faire du mTLS à la main, c’est la galère. Générer les certificats, les distribuer, les roter, gérer les expirations. C’est un job à temps plein.
Un service mesh comme Istio automatise tout ça. Et le meilleur : zéro code à changer dans ton application.
Étape 1 : Installer Istio
0$ istioctl install --set profile=demo -y
1$ kubectl label namespace default istio-injection=enabledÉtape 2 : Déployer un service
Tu déploies normalement. La seule chose importante : tu créés un ServiceAccount dédié. C’est lui qui va porter l’identité SPIFFE.
0apiVersion: v1
1kind: ServiceAccount
2metadata:
3 name: checkout
4---
5apiVersion: apps/v1
6kind: Deployment
7metadata:
8 name: checkout
9spec:
10 selector:
11 matchLabels:
12 app: checkout
13 template:
14 metadata:
15 labels:
16 app: checkout
17 spec:
18 serviceAccountName: checkout
19 containers:
20 - name: app
21 image: checkout:latest
22 ports:
23 - containerPort: 8080Istio injecte automatiquement un sidecar Envoy dans chaque pod. Ce sidecar intercepte tout le trafic, gère le handshake mTLS, et roté les certificats tout seul. Ton application continue à parler HTTP à localhost comme si de rien n’était.
Étape 3 : Passer en PERMISSIVE
Avant de tout casser, tu commences en PERMISSIVE, où Istio accepte à la fois le trafic chiffré et le trafic en clair. Ça permet de migrer sans downtime.
0$ kubectl apply -f - <<EOF
1apiVersion: security.istio.io/v1beta1
2kind: PeerAuthentication
3metadata:
4 name: default
5 namespace: istio-system
6spec:
7 mtls:
8 mode: PERMISSIVE
9EOFÉtape 4 : Vérifier que mTLS est actif
0$ kubectl exec deploy/checkout -- curl -s http://ledger:8080/ -v 2>&1 | grep "x-forwarded-client-cert"
1> X-Forwarded-Client-Cert: By=spiffe://cluster.local/ns/default/sa/checkout...Le header X-Forwarded-Client-Cert est ajouté par Envoy et contient l’identité SPIFFE du caller. Si tu vois ça, mTLS est actif.
Tu peux aussi checker les métriques :
0$ kubectl exec deploy/checkout -- curl http://localhost:15090/metrics | grep istio_requests_totalSi tout le trafic est en mutual_tls, t’es bon pour passer à la suite.
Étape 5 : Passer en STRICT namespace par namespace
Tu commences par les plus critiques — ceux qui traitent des données sensibles, des paiements, etc.
0$ kubectl apply -f - <<EOF
1apiVersion: security.istio.io/v1beta1
2kind: PeerAuthentication
3metadata:
4 name: default
5 namespace: payments
6spec:
7 mtls:
8 mode: STRICT
9EOFEn STRICT, tout trafic qui arrive sans mTLS est refusé. Pas de 403. Pas de message. La connexion TCP est fermée au niveau handshake.
Étape 6 : Ajouter des autorisations
mTLS prouve qui appelle. C’est l’identité. Mais pour contrôler ce que chaque service a le droit de faire, faut une AuthorizationPolicy.
0apiVersion: security.istio.io/v1beta1
1kind: AuthorizationPolicy
2metadata:
3 name: ledger-allow
4 namespace: payments
5spec:
6 selector:
7 matchLabels:
8 app: ledger
9 rules:
10 - from:
11 - source:
12 principals:
13 - "cluster.local/ns/payments/sa/checkout"
14 to:
15 - operation:
16 methods: ["POST"]
17 paths: ["/v1/entries"]Cette règle dit : seul le service checkout (identifié par son ServiceAccount) peut POST sur /v1/entries du service ledger. Tout le reste est rejeté.
Et c’est là que la combinaison mTLS + autorisation devient puissante : même si un attaquant compromet un service dans le namespace monitoring, il peut pas appeler ledger — son identité SPIFFE correspond pas.
Les pièges à connaître
Validation du mauvais certificat. Certaines implémentations itèrent sur la liste des certificats envoyés par le client et acceptent le premier qui matche. Sauf que le TLS sous-jacent n’a vérifié QUE le premier. Un attaquant peut envoyer un certificat valide pas à lui en premier, puis le sien en second, et se faire authentifier sous une fausse identité. Toujours prendre le premier certificat de la liste.
SSRF via les extensions de certificat. Si tu résous les URLs contenues dans les certificats (AIA, CRL Distribution Points) sans vérifier la signature d’abord, un attaquant peut te faire faire des requêtes HTTP vers ses serveurs. Ne résous jamais les extensions avant d’avoir validé la chaîne de confiance.
mTLS ≠ autorisation. mTLS prouve qui est au bout du fil. Ça ne dit pas ce qu’il a le droit de faire. Un service checkout légitime mais compromis peut accéder à tout ce que son identité transport permet. Toujours mettre une couche d’autorisation au-dessus.
Pourquoi t’as pas le choix
Si t’as plus de cinq microservices qui causent entre eux et que t’utilises encore des API keys partagées, t’as un problème. Un jour un log va fuiter, un dump va traîner, un sidecar va être compromis, et tout ton château de cartes s’écroule.
mTLS avec un service mesh, c’est :
- Zéro code à changer
- Des certificats qui se rotent tout seuls
- Une identité cryptographique pour chaque service
- Du chiffrement de bout en bout partout
Et ça prend une après-midi à mettre en place.
Vire les secrets internal-api-key. Deploie un L4-Mesh de ton choix, configure le mTLS. Tout continue de marcher, personne a changé une ligne de code, et maintenant chaque connexion est authentifiée et chiffrée.
Les clés API partagées, c’est fini. Si tu veux du zero trust, commence par là.
Et souviens-toi : mTLS c’est qui. Les autorisations c’est quoi. Les deux ensemble, tu dors tranquille.