Vos microservices partagent un VPC, donc ils se font confiance. Cette confiance est une autorité ambiante : la permission d’invoquer un service n’est accordée par aucune preuve cryptographique, mais par la topologie réseau. Un attaquant qui franchit le périmètre en hérite tout.
Les services peuvent absolument s’authentifier sans autorité ambiante. La question plus difficile est de savoir pourquoi votre plateforme vous fait sentir que c’est une trahison.
À quoi ressemble l’autorité ambiante en production
La plupart de l’« authentification » entre services n’est en réalité qu’une segmentation réseau. Les services vivent dans un sous-réseau privé, derrière une API gateway, ou à l’intérieur d’un cluster Kubernetes avec un service mesh. L’hypothèse est simple : si la requête provient d’une IP interne, elle est légitime.
Ce modèle s’effondre quand le périmètre s’effondre. Un pod compromis, un mouvement latéral après une fuite d’identifiants, ou une injection dans la chaîne d’approvisionnement place l’attaquant à l’intérieur de la zone de confiance. À ce stade, chaque service est accessible et la plupart ne demanderont pas de preuve d’identité. L’attaquant n’a pas besoin de voler plus d’identifiants. Il hérite simplement de l’autorité ambiante du réseau.
Une authentification réelle entre services signifie que chaque appelant prouve qui il est, et que chaque appelé vérifie cette preuve. Le réseau n’est qu’un transport. Il ne devrait pas être un identifiant.
Comment fonctionne l’authentification par capabilities entre services
Il existe deux approches pratiques : l’identité cryptographique (chaque service a un nom vérifiable) et les capability tokens (chaque requête porte une autorisation inforgeable et limitée).
Identité cryptographique avec mTLS
En TLS mutuel, le client et le serveur présentent tous deux des certificats X.509. Le serveur vérifie le certificat du client avant de traiter la requête. Le client vérifie le certificat du serveur avant d’envoyer des données. Aucun des deux côtés ne fait confiance à l’autre à cause de l’adresse IP.
Le défi infrastructurel est la distribution des certificats. SPIFFE et SPIRE résolvent cela en donnant à chaque workload une identité cryptographique dérivée de ses propriétés, pas de son emplacement. Un pod exécutant le payments-api reçoit un SPIFFE ID comme spiffe://prod.example.com/payments-api. Le certificat est à courte durée de vie et renouvelé automatiquement.
Voici à quoi cela ressemble en Go :
package main
import (
"crypto/tls"
"crypto/x509"
"fmt"
"log"
"net/http"
"os"
)
// loadClientTLS returns a config that verifies the server AND presents a client cert.
func loadClientTLS(caCert, clientCert, clientKey []byte) *tls.Config {
pool := x509.NewCertPool()
pool.AppendCertsFromPEM(caCert)
cert, err := tls.X509KeyPair(clientCert, clientKey)
if err != nil {
log.Fatalf("failed to load client cert: %v", err)
}
return &tls.Config{
Certificates: []tls.Certificate{cert},
RootCAs: pool,
// Do not skip verification based on IP or DNS name alone.
InsecureSkipVerify: false,
}
}
func main() {
caCert, _ := os.ReadFile("ca.crt")
clientCert, _ := os.ReadFile("service.crt")
clientKey, _ := os.ReadFile("service.key")
client := &http.Client{
Transport: &http.Transport{
TLSClientConfig: loadClientTLS(caCert, clientCert, clientKey),
},
}
resp, err := client.Get("https://orders-api.internal:8443/orders")
if err != nil {
log.Fatal(err)
}
fmt.Println(resp.Status)
}
La ligne critique est InsecureSkipVerify: false. Sans elle, vous revenez à la confiance ambiante. Avec elle, le client refuse de se connecter à moins que le serveur ne présente un certificat valide signé par la CA. Le serveur fait de même pour le client.
Capability tokens pour une autorisation limitée
Les certificats prouvent l’identité. Les capabilities prouvent l’autorisation. Dans de nombreux systèmes, vous voulez les deux : prouver qui vous êtes, puis prouver que vous êtes autorisé à faire cette chose spécifique.
Un capability token lie une identité, une action et une ressource dans un package inforgeable. Le service le crée. Le service le vérifie. Il n’y a pas de magasin de sessions central à interroger.
import hmac
import hashlib
import secrets
import time
SECRET = secrets.token_bytes(32)
def mint_service_capability(
caller_id: str,
service: str,
action: str,
resource: str,
ttl_seconds: int = 300
) -> str:
"""Mint a time-bound capability for a specific service action."""
expires = int(time.time()) + ttl_seconds
payload = f"{caller_id}:{service}:{action}:{resource}:{expires}"
sig = hmac.new(SECRET, payload.encode(), hashlib.sha256).hexdigest()[:24]
return f"{payload}:{sig}"
def verify_service_capability(
token: str,
expected_service: str,
expected_action: str,
expected_resource: str
) -> bool:
"""Verify a capability token is unexpired and correctly scoped."""
try:
caller_id, svc, action, resource, expires, sig = token.rsplit(":", 5)
except ValueError:
return False
if int(expires) < time.time():
return False
if svc != expected_service or action != expected_action or resource != expected_resource:
return False
payload = f"{caller_id}:{svc}:{action}:{resource}:{expires}"
expected_sig = hmac.new(SECRET, payload.encode(), hashlib.sha256).hexdigest()[:24]
return hmac.compare_digest(sig, expected_sig)
# Service A requests a capability to call Service B.
token = mint_service_capability("payments-api", "orders-api", "read", "order-123")
# Service B verifies the capability before handling the request.
if not verify_service_capability(token, "orders-api", "read", "order-123"):
raise PermissionError("Invalid or expired capability")
Le token est autonome et à courte durée de vie. Service B n’a pas besoin d’appeler un serveur d’autorisation. Il vérifie simplement le HMAC et l’expiration. La portée est explicite : ce token n’est valide que pour lire order-123 depuis orders-api.
Pourquoi c’est plus difficile que ça ne devrait l’être
La technologie existe. La difficulté est opérationnelle.
Amorcer la confiance est le premier problème. Si chaque service a besoin d’un certificat, quelque chose doit émettre ces certificats. Ce quelque chose, l’autorité de certification ou le serveur SPIRE, devient une nouvelle cible. Vous n’avez pas éliminé la confiance. Vous l’avez concentrée.
Il y a aussi le problème de la révocation. Si un service est compromis, comment invalider son identité ? Les certificats mTLS peuvent être à courte durée de vie, ce qui aide, mais révoquer un certificat spécifique nécessite toujours de distribuer une liste de révocation de certificats ou d’utiliser OCSP. Les capability tokens expirent rapidement, ce qui limite les dégâts, mais une clé de signature volée permet à un attaquant de créer un nombre illimité de tokens.
Les performances sont une autre préoccupation. Les handshakes TLS ajoutent de la latence. Dans un service mesh avec des sidecars, le surcoût est généralement acceptable (quelques millisecondes), mais sur les chemins à haut débit, ces millisecondes s’accumulent. Les capability tokens évitent un aller-retour vers un serveur d’authentification, mais la vérification HMAC n’est pas gratuite à grande échelle.
Où l’autorité ambiante a encore du sens
Chaque appel interne n’a pas besoin de preuve cryptographique. Une tâche cron qui s’exécute dans le même container que le client de base de données n’a pas besoin de mTLS pour parler à localhost. Les health checks entre un load balancer et un nœud n’ont pas besoin de capability tokens.
L’objectif n’est pas d’éliminer toute autorité ambiante. L’objectif est de savoir où vit votre autorité ambiante et si le rayon d’explosion est acceptable. Un VPC partagé avec 200 services et aucune authentification interne est un rayon d’explosion large et invisible. Une socket Unix locale avec les permissions de fichier 0600 est un rayon d’explosion petit et explicite.
Comment commencer à éliminer l’autorité ambiante des appels entre services
Vous n’avez pas besoin d’un service mesh dès le premier jour. Commencez par les services qui touchent des données sensibles ou qui se trouvent aux limites de confiance.
Tout d’abord, identifiez vos services délégués. Quels services détiennent des privilèges que les autres n’ont pas ? Quelles API internes seraient dangereuses si elles étaient appelées par le mauvais service ? Ce sont vos candidats.
Deuxièmement, ajoutez du mTLS à la couche API. Si vous utilisez un reverse proxy comme Envoy ou NGINX, vous pouvez terminer le mTLS là sans changer le code applicatif. Le proxy passe un en-tête avec l’identité du client vérifiée à l’upstream. L’upstream vérifie cet en-tête.
Troisièmement, limitez la portée de vos tokens. Si vous utilisez déjà des JWTs pour l’authentification entre services, arrêtez de mettre role: service dans le payload. Mettez allowed_services: ["orders-api"] et allowed_resources: ["order:*"]. Rendez l’autorisation aussi spécifique que l’opération.
Questions fréquentes sur l’authentification entre services sans autorité ambiante
Le mTLS suffit-il, ou ai-je besoin de capabilities aussi ?
Le mTLS prouve l’identité. Les capabilities prouvent l’autorisation. Ils résolvent des problèmes différents. Un service avec un certificat valide ne devrait toujours pas être autorisé à supprimer des données arbitraires. Utilisez le mTLS pour la sécurité du transport et l’identité, les capabilities pour le contrôle d’accès.
Qu’en est-il des service meshes comme Istio ou Linkerd ?
Les service meshes automatisent le mTLS et l’identité entre les pods. C’est un moyen pratique d’éliminer l’autorité ambiante sans réécrire chaque service. Le mesh gère la rotation des certificats, l’attestation d’identité et le chiffrement du trafic. Le compromis est la complexité et un nouveau plan de contrôle à sécuriser.
Est-ce que cela fonctionne pour les fonctions serverless ?
Oui, mais c’est plus difficile. Les fonctions serverless sont éphémères, donc la rotation des certificats et l’attestation d’identité doivent se produire au moment de l’invocation. Les fournisseurs de cloud offrent une workload identity pour cela (AWS IAM Roles for Service Accounts, GCP Workload Identity). Ceux-ci sont de type capability : la fonction reçoit un token limité à ce qu’elle est autorisée à faire, pas un identifiant global.
Quelle est la différence entre un compte de service et une capability ?
Un compte de service est une identité qui porte de l’autorité ambiante par défaut. Si Service A s’exécute en tant que payments-sa, il peut généralement faire tout ce que payments-sa est autorisé à faire. Une capability est limitée à une action spécifique sur une ressource spécifique. Même si la capability est volée, elle ne peut pas être rejouée contre une cible différente.
Commencez avec le périmètre que vous avez déjà
Votre VPC n’a jamais été une limite de sécurité. C’était une limite réseau que nous avons fait passer pour une limite de sécurité. Éliminer l’autorité ambiante signifie traiter chaque appel entre services comme s’il franchissait cette limite. L’identité cryptographique et les capabilities limitées sont les outils. La partie difficile est d’admettre que l’ancien modèle était de la commodité déguisée en sécurité.
Choisissez une API interne. Ajoutez la vérification du certificat client. Observez ce qui casse. La plupart du temps, ce qui casse est une hypothèse que votre code posait sans que vous le sachiez.