SPIFFE et mTLS avec cert-manager
En 2024, j’ai écrit un article sur Istio, un service Mesh qui permet de gérer la communication entre les microservices. Dans cet article, nous avons un peu creusé le fonctionnement du mTLS avec les sidecars (lien vers le chapitre). C’était un article assez pratique mais nous ne sommes pas allés très loin dans les détails. Aujourd’hui, nous allons approfondir cette partie en explorant SPIFFE, qui est le framework de référence pour la gestion des identités des workloads pour sécuriser les échanges.
SPIFFE (Secure Production Identity Framework for Everyone) est un standard open-source qui définit un format d’identité à base de certificats X.509 pour les workloads dans les environnements distribués. C’est à travers ce standard que les applications pourront récupérer des certificats liés à leur identité et les utiliser pour valider les identités des autres applications.
À l’inverse de Kerberos, SPIFFE est plutôt adapté aux architectures micro-services où la même machine possède plusieurs identités en fonction du service qu’elle exécute (surtout dans un contexte de conteneurs où chacun d’entre-eux doit avoir sa propre identité), là où Kerberos est (même si je n’en ai aucune expérience) plus adapté aux architectures monolithiques où la machine possède une seule identité.
Ce framework est utilisé dans des services Mesh comme Istio, Linkerd, Consul Connect, DAPR et même cilium. Dans un Mesh, ce fonctionnement est caché dans les proxies (souvent Envoy) qui sont déployés en sidecar, ce qui veut dire que les applications ne sont pas conscientes de ce fonctionnement.
Dans le contexte d’un Istio : lorsqu’un sidecar Envoy va communiquer avec un nouveau service, il va requêter Istiod pour obtenir un certificat se chargeant d’authentifier les échanges auprès de Citadel (le service gérant ces identités). Ainsi, dû à la nature du mTLS, l’expéditeur ET le destinataire vont pouvoir s’authentifier mutuellement.
Revoir le fonctionnement d’un service Mesh serait un peu répétitif, on va plutôt se concentrer sur SPIFFE et comment l’implémenter dans nos applications dans un contexte Kubernetes. Mais avant de commencer, je dois quand même vous prévenir que je ne recommanderais pas forcément cet article pour un usage en production. En effet, nous allons implémenter SPIFFE de manière très basique, sans les fonctionnalités avancées que l’on peut trouver dans des solutions comme SPIRE, une implémentation prod-ready qui intègre le nécessaire pour faire du SPIFFE avec toutes les fonctionnalités (là où ce que nous allons faire se limite uniquement sur le mTLS et l’identité des workloads). De plus, notre implémentation nécessitera d’adapter le code de nos applications (le cas échéant, nous devrions utiliser des proxy comme Envoy).
Ainsi, voici ce que nous n’allons pas faire (et même qui pourrait être incompatible avec notre setup):
- De la fédération pour autoriser des identités externes à accéder à nos services.
- Gérer un datastore externe au cluster.
- Des instances Nested (e.g. une instance SPIFFE globale qui va fournir les certificats pour des sous-instances).
La raison à cela est que nous allons utiliser cert-manager, un contrôleur Kubernetes qui gère les certificats X.509, habituellement pour générer les certificats SSL pour un ingress-controler. Dès que j’ai découvert qu’il était possible de l’utiliser pour faire du SPIFFE, j’ai voulu le mettre au centre de l’article. Il est très bien documenté, facile à mettre en place, et parfaitement intégré à Kubernetes.
Installation de Cert-manager
Cert-manager est un contrôleur Kubernetes qui gère les certificats X.509. Il permet de créer, renouveler et révoquer des certificats automatiquement. Très utile pour générer des certificats TLS via une annotation dans un Ingress, par exemple.
Si vous êtes déjà familié avec cert-manager, sachez quand même que nous allons customiser un peu son fonctionnement : il n’aura pas le “Approver” activé. Il s’agit d’un requirement en gras sur la documentation de cert-manager pour faire du SPIFFE.
Mais… qu’est ce que ça implique ?
Concrètement, lorsqu’on demande un certificat via cert-manager, il va l’approuver automatiquement, on peut le voir dans les CertificateRequest
dès qu’on créé un objet Certificate
.
conditions:
- lastTransitionTime: "2025-06-06T21:41:23Z"
message: Certificate request has been approved by cert-manager.io
reason: cert-manager.io
status: "True"
type: Approved # <--- ICI
Pendant des années, je me suis toujours contenté d’ignorer cette partie (it works, why bother ?), mais en fait, c’est une feature assez intéressante. Je vous fait un petit résumé de ce que j’ai découvert.
En pratique, j’ai pas l’impression que les gens tweak beaucoup l’Approver, c’est utile dans un cluster où chaque équipe a son propre tenant et où on veut éviter qu’elles puissent créer des certificats n’importe comment. En modifiant ce composant, on peut créer des ressources CertificateRequestPolicy
qui vont permettre de valider les demandes de certificats uniquement si elles respectent certaines règles. Par exemple, on peut vérifier que le nom de domaine du certificat demandé est bien celui du cluster (e.g. je veux que ce cluster puisse générer des certificats pour *.prod-01.une-tasse-de.cafe
et pas pour *.prod-02.une-tasse-de.cafe
, chaque cluster aura une règle différente). C’est un exemple assez simple, mais on peut jouer sur la majorité des champs du certificat (évitant ainsi les erreurs de configuration et donc le rate-limiting lorsque trop de requêtes en échec sont envoyées à l’ACME server).
Envie d’en savoir plus ? Ça se passe ici.
Voilà, peut-être que vous vous en fichez du SPIFFE, mais vous aurez peut‑être appris quelque chose sur cert-manager. Revenons à nos moutons et installons notre cert-manager en désactivant l’approbation automatique des certificats.
helm repo add jetstack https://charts.jetstack.io --force-update
helm upgrade -i cert-manager jetstack/cert-manager \
--namespace cert-manager --create-namespace \
--set disableAutoApproval=true \
--set crds.enabled=true
L’objectif de l’installation de ce chart Helm via ces paramètres est :
- D’installer cert-manager dans le namespace
cert-manager
- De désactiver l’approbation automatique des certificats (comme dit précédemment)
Information
Si vous utilisez déjà cert-manager pour votre HTTPS et que vous ne voulez pas toucher à son fonctionnement classique, vous pouvez aussi préciser que certains certificats venant d’Issuer/ClusterIssuer précis doivent être approuvés automatiquement, par exemple :
--set approveSignerNames[0]="issuers.cert-manager.io/cloudflare*" \
--set approveSignerNames[1]="clusterissuers.cert-manager.io/letsencrypt-staging"
Mais ça, je vous laisse le découvrir par vous-même, dans mon cluster de dev : je n’utilise pas d’ingress donc je vais me contenter de désactiver l’approbation automatique pour tous les certificats.
Maintenant, créons un ClusterIssuer qui va nous permettre de générer des certificats auto-signés (si vous n’avez pas déjà un ClusterIssuer en autosigné). Une fois fait, nous allons pouvoir créer un certificat racine (CA) qui sera utilisé pour créer les certificats pour nos workloads.
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: selfsigned-issuer
spec:
selfSigned: {}
---
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: trust-domain-root-ca
namespace: cert-manager
spec:
isCA: true
commonName: trust-domain-root-ca
secretName: root-secret
privateKey:
algorithm: ECDSA
size: 256
issuerRef:
name: selfsigned-issuer
kind: ClusterIssuer
group: cert-manager.io
Mais avant de continuer, remarquons que nous avons créé un certificat.. sans que l’approval automatique soit activé. On va devoir approuver manuellement le certificat de la CA racine.
$ kubectl get certificate -n cert-manager
NAME READY SECRET AGE
trust-domain-root-ca False root-secret 11m
beh oui on est marron là.
L’option la plus simple est de passer par la cli de cert-manager pour l’approuver manuellement :
$ brew install cmctl # nix-shell -p cmctl
$ cmctl approve trust-domain-root-ca-1 -n cert-manager
Approved CertificateRequest 'cert-manager/trust-domain-root-ca-1'
Après ça, notre certificat devrait être prêt :
$ kubectl get certificate -n cert-manager
NAME READY SECRET AGE
trust-domain-root-ca True root-secret 22m
Maintenant que nous avons notre CA pour SPIFFE, il ne reste plus qu’à l’utiliser dans un issuer qui sera utile pour générer les certificats SPIFFE pour nos workloads. On va alors créer un ClusterIssuer
qui va utiliser cette CA racine pour signer les certificats SPIFFE.
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: trust-domain-root
spec:
ca:
secretName: root-secret # Présent dans le namespace cert-manager
Trust-Manager
Nous avons notre CA racine, nous allons à présent pouvoir créer des certificats SPIFFE pour nos workloads (chacun aura sa clé publiques et privées, ainsi qu’une CA pour valider les certificats des autres).
Avant de pouvoir délivrer les certificats SPIFFE, nous devons trouver une méthode pour propager cette CA racine. C’est pour cela, que nous allons utiliser l’opérateur trust-manager. Il permet de créer des Bundles
(une nouvelle Custom Resource) qui vont contenir les certificats de confiance (les CA) qui pourront être utilisés par les workloads pour vérifier les certificats qu’ils reçoivent.
helm repo add jetstack https://charts.jetstack.io --force-update
helm upgrade trust-manager jetstack/trust-manager \
--install \
--namespace cert-manager \
--wait
Durant son installation, il va lui-même créer un certificat avec un Issuer SelfSigned (et non un clusterIssuer), nous ne l’utiliserons pas mais l’installation risque d’échouer si on ne l’approuve pas.
cmctl approve -n cert-manager trust-manager-1
Oui, cet article est pas très GitOps-friendly… :(
Passons ce fâcheux détail : on va à présent créer un Bundle ! Et lister notre CA racine dans celui-ci.
apiVersion: trust.cert-manager.io/v1alpha1
kind: Bundle
metadata:
name: coffee-bundle
namespace: cert-manager
spec:
sources:
- secret:
name: "root-secret"
key: "ca.crt"
target:
configMap:
key: "ca.crt"
L’effet de ce Bundle est de créer un ConfigMap qui contiendront le certificat de la CA racine que nous avons créé précédemment. Ce ConfigMap sera ensuite utilisé par les workloads pour vérifier les identités des requêtes qu’ils reçoivent.
kubectl get cm -A | grep coffee
cert-manager coffee-bundle 1 3m8s
default coffee-bundle 1 3m8s
kube-node-lease coffee-bundle 1 3m8s
kube-public coffee-bundle 1 3m8s
kube-system coffee-bundle 1 3m8s
Information
Oui là j’ai été un peu bourrin et j’ai mis en destination tous les namespaces, mais on aurait pu restreindre le Bundle à un namespace précis en utilisant namespaceSelector
:
namespaceSelector:
matchLabels:
spiffee: "enabled"
L’usage de trust-manager est totalement facultatif, nous aurions pu nous contenter de créer ces ConfigMap à la main, ou de passer par Reflector (un autre opérateur pour dupliquer une ressource dans plusieurs namespace). L’avantage de trust-manager est qu’il accepte en source des secrets, pour créer des ConfigMap (car notre CA est forcément générée dans un secret).
CSI Driver SPIFFE
Maintenant que nous avons notre CA racine et notre Bundle, nous allons pouvoir passer au vif du sujet : fournir aux pods Kubernetes les certificats correspondant à leur identité SPIFFE.
Si on compare avec Istio, il y a un composant nommé Citadel qui est responsable de la gestion des identités et des certificats aux proxies. Dans notre cas, nous n’avons pas Citadel (et encore moins de Envoy), mais nous avons un composant équivalent qui va nous permettre de monter les certificats SPIFFE dans les Pods Kubernetes : le CSI Driver SPIFFE de Cert-manager. C’est un CSI (Container Storage Interface) qui permet de monter les identités SPIFFE dans les Pods. Il va permettre de créer des volumes qui, une fois le pod schedule, vont automatiquement générer des certificats (via une CertificateRequest) pour chaque Pod qui en a besoin. Il va aussi permettre de stocker ces certificats dans un volume qui sera monté dans le Pod.
C’est exactement pour ce composant qu’on a dû désactiver l’approbation automatique des certificats durant l’installation de cert-manager car le CSI Driver SPIFFE va créer des certificats SPIFFE pour chaque Pod qui en a besoin, et on ne veut pas qu’ils soient approuvés automatiquement sans vérification. La documentation est très claire à ce sujet.
Ainsi, pour toute demande de certificat SPIFFE, nous allons devoir utiliser l’approver intégré au CSI-Driver (qui n’est pas le même que celui activé dans cert-manager). L’approver s’assure que les demandes respectent les critères suivants :
- des usages de clé acceptables (Key Encipherment, Digital Signature, Client Auth, Server Auth) ;
- une durée demandée qui correspond à la durée imposée (par défaut 1 heure) ;
- aucune SAN ou autre attribut identifiable, à l’exception d’un unique URI SAN ;
- un URI SAN correspondant à l’identité SPIFFE du ServiceAccount ayant créé la CertificateRequest ;
- un SPIFFE ID Trust Domain correspondant à celui configuré au démarrage.
Cet approver n’est utilisé que pour les demandes de certificats SPIFFE et n’est en aucun cas lié aux demandes de certificats classiques (HTTPS, etc.).
Le CSI Driver SPIFFE permet de monter automatiquement dans chaque Pod Kubernetes des certificats SPIFFE uniques (générés et renouvelés individuellement pour chaque Pod avant leur expiration), assurant ainsi une gestion transparente et sécurisée des identités.
# values.yaml
app:
trustDomain: spiffe.une-tasse-de.cafe
issuer:
name: trust-domain-root
kind: ClusterIssuer
group: cert-manager.io
driver:
volumes:
- name: root-cas
configMap:
name: coffee-bundle
volumeMounts:
- name: root-cas
mountPath: /var/run/secrets/cert-manager-csi-driver-spiffe
sourceCABundle: /var/run/secrets/cert-manager-csi-driver-spiffe/ca.crt
Ici, nous créons le trust-domain spiffe.une-tasse-de.cafe
et nous spécifions l’issuer qui va être utilisé pour signer les certificats SPIFFE (c’est celui qui utilise notre CA racine). Nous spécifions aussi que le driver va monter le ConfigMap créé par trust-manager.
Vous pouvez ensuite installer le chart avec :
helm upgrade cert-manager-csi-driver-spiffe jetstack/cert-manager-csi-driver-spiffe \
--install \
--namespace cert-manager \
-f values.yaml
Le CSI Driver SPIFFE va utiliser cette CA pour signer les certificats SPIFFE qu’il va créer pour chaque Pod qui en a besoin. Testons cela de suite :
apiVersion: v1
kind: ServiceAccount
metadata:
name: ubuntu-spiffe
namespace: default
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: create-certificaterequests
namespace: default
rules:
- apiGroups: ["cert-manager.io"]
resources: ["certificaterequests"]
verbs: ["create"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: ubuntu-spiffe
namespace: default
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: create-certificaterequests
subjects:
- kind: ServiceAccount
name: ubuntu-spiffe
namespace: default
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: ubuntu-spiffe
namespace: default
labels:
app: ubuntu-spiffe
spec:
replicas: 1
selector:
matchLabels:
app: ubuntu-spiffe
template:
metadata:
labels:
app: ubuntu-spiffe
spec:
serviceAccountName: ubuntu-spiffe
containers:
- name: ubuntu-spiffe
image: ubuntu
imagePullPolicy: IfNotPresent
command: [ "sleep", "1000000" ]
volumeMounts:
- mountPath: "/var/run/secrets/spiffe.io"
name: spiffe
securityContext:
runAsUser: 1000
runAsGroup: 1000
volumes:
- name: spiffe
csi:
driver: spiffe.csi.cert-manager.io
readOnly: true
volumeAttributes:
spiffe.csi.cert-manager.io/fs-group: "1000"
Notez que nous avons défini un ServiceAccount
et un RoleBinding
afin d’autoriser le Pod à créer des ressources CertificateRequest
. Cette étape est essentielle, car le CSI Driver SPIFFE génère un certificat SPIFFE pour chaque Pod selon ses besoins, et il requiert ces droits pour fonctionner correctement. Par ailleurs, c’est ce ServiceAccount
qui servira d’identité SPIFFE (SVID) au Pod.
Les pods ayant le même ServiceAccount
auront le même SVID, c’est via cette méthode que les identités sont mappées à un ou plusieurs pods.
$ kubectl get certificaterequests.cert-manager.io
NAME APPROVED DENIED READY ISSUER REQUESTER AGE
8730086b-1bd2-4b52-a3cf-db451070d2a2 True True trust-domain-root system:serviceaccount:default:ubuntu-spiffe 6m2s
La demande de certificat a été approuvée et le certificat est prêt !
On devrait maintenant avoir des certificats SPIFFE dans le Pod dans le répertoire /var/run/secrets/spiffe.io
:
$ kubectl exec -n default $(kubectl get pod -n default -l app=ubuntu-spiffe -o jsonpath='{.items[0].metadata.name}') -- ls /var/run/secrets/spiffe.io/
ca.crt tls.crt tls.key
$ kubectl exec -n default $(kubectl get pod -n default -l app=ubuntu-spiffe -o jsonpath='{.items[0].metadata.name}') -- cat /var/run/secrets/spiffe.io/tls.crt | openssl x509 -text | grep URI
URI:spiffe://spiffe.une-tasse-de.cafe/ns/default/sa/ubuntu-spiffe
Ce que ça veut dire, c’est que le pod a bien sa propre identité SPIFFE dans sa clé publique, qui est unique et spécifique à ce SA, on peut voir que celle-ci est basée sur le trustDomain
que nous avons spécifié lors de l’installation du CSI Driver SPIFFE ainsi que le namespace et le nom du ServiceAccount
utilisé par le Pod. (spiffe://${trustDomain}/ns/${namespace}/sa/${serviceAccountName}
).
Créer une application qui utilise SPIFFE
Designons une simple application qui va utiliser ces certificats SPIFFE pour communiquer en mTLS. On va créer 2 pods, un client et un serveur. Ce client va faire une simple requête HTTPS et le serveur va juste lui répondre avec un message.
Pour rappel, l’intéret de notre setup est de faire du mTLS afin de valider l’identité de l’autre (c’est d’ailleurs pour cette raison que nous avons configuré le CSI-Driver SPIFFE pour qu’il utilise les ConfigMap créées par le trust-manager).
Voici un extrait du code du serveur qui va vérifier l’identité du client à travers son certificat SPIFFE :
if r.TLS == nil || len(r.TLS.PeerCertificates) == 0 {
http.Error(w, "Client certificate required", http.StatusUnauthorized)
return
}
peerCert := r.TLS.PeerCertificates[0]
id, err := x509svid.IDFromCert(peerCert)
if err != nil {
log.Printf("Error extracting client's SPIFFE ID: %v", err)
http.Error(w, "Invalid SPIFFE identity", http.StatusUnauthorized)
return
}
log.Printf("Request received from client with SPIFFE identity: %s", id.String())
if id.String() != "spiffe://spiffe.une-tasse-de.cafe/ns/default/sa/client-spiffe" {
http.Error(w, "Unauthorized", http.StatusForbidden)
return
}
Ici, dès qu’un client se connecte au serveur, on va d’abord vérifier qu’il a bien un certificat TLS, puis on va extraire son SPIFFE ID et vérifier qu’il correspond à celui que nous attendons. Si ce n’est pas le cas, on renvoie une erreur 403.
Cette authentification se base sur le SPIFFE ID (SVID) du client, qui est unique et spécifique aux pods ayant le ServiceAccount
client-spiffe
dans le namespace default
.
Pour le client, on va faire une requête HTTPS vers le serveur en utilisant son certificat. Le client va aussi vérifier que le certificat du serveur est valide et qu’il correspond à l’identité SPIFFE attendue.
func initializeSpiffeClient() (*http.Client, error) {
log.Println("Loading TLS certificates...")
clientSVID, err := tls.LoadX509KeyPair(svidSocketPath+"/tls.crt", svidSocketPath+"/tls.key")
if err != nil {
return nil, fmt.Errorf("unable to load client SVID: %w", err)
}
caBundleBytes, err := os.ReadFile(svidSocketPath + "/ca.crt")
if err != nil {
return nil, fmt.Errorf("unable to load CA bundle: %w", err)
}
trustDomainCAs := x509.NewCertPool()
if !trustDomainCAs.AppendCertsFromPEM(caBundleBytes) {
return nil, errors.New("failed to add CAs to the pool")
}
expectedServerSpiffeID := "spiffe://spiffe.une-tasse-de.cafe/ns/default/sa/server-spiffe"
tlsConfig := &tls.Config{
Certificates: []tls.Certificate{clientSVID},
InsecureSkipVerify: true,
VerifyPeerCertificate: func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
if len(rawCerts) == 0 {
return errors.New("server certificate not presented")
}
peerCert, err := x509.ParseCertificate(rawCerts[0])
if err != nil {
return fmt.Errorf("unable to parse server certificate: %w", err)
}
verifyOpts := x509.VerifyOptions{Roots: trustDomainCAs}
if _, err := peerCert.Verify(verifyOpts); err != nil {
return fmt.Errorf("invalid server certificate chain: %w", err)
}
id, err := x509svid.IDFromCert(peerCert)
if err != nil {
return fmt.Errorf("unable to extract SPIFFE ID: %w", err)
}
if id.String() != expectedServerSpiffeID {
return fmt.Errorf("unexpected server SPIFFE ID: expected %q, got %q", expectedServerSpiffeID, id.String())
}
return nil
},
}
client := &http.Client{
Transport: &http.Transport{TLSClientConfig: tlsConfig},
Timeout: 10 * time.Second,
}
return client, nil
}
De la même manière que pour le serveur, on va charger le certificat SPIFFE du client et le CA Bundle dans la configuration TLS du client afin de pouvoir vérifier que le certificat du serveur est valide et qu’il correspond à l’identité SPIFFE attendue.
Mais au fur et à mesure que le temps passe, on va se rendre compte que le client ne peut plus se connecter au serveur :
Get "https://spiffe-server.default.svc.cluster.local:8443": invalid server certificate chain: x509: certificate has expired or is not yet valid: current time 2025-06-08T09:32:02Z is after 2025-06-08T09:09:09Z
Pourtant, si on regarde le certificat du serveur, on peut voir qu’il est valide coté serveur :
$ cat /var/run/secrets/spiffe.io/tls.crt | openssl x509 -text
...
Validity
Not Before: Jun 8 09:24:27 2025 GMT
Not After : Jun 8 10:24:27 2025 GMT
...
Donc, si on récapitule :
- Le client a un certificat SPIFFE valide
- Le serveur a un certificat SPIFFE valide
- Mais lorsque le client essaie de se connecter au serveur, il reçoit une erreur de certificat expiré.
Je ne vais pas garder le suspense plus longtemps, le serveur charge son certificat SPIFFE en mémoire au démarrage, mais il n’est jamais mis à jour. Donc, si le certificat SPIFFE est renouvelé, le serveur ne le saura pas et continuera à utiliser l’ancien certificat, qui est expiré.
Pour résoudre ce problème, il faut que le serveur recharge son certificat SPIFFE régulièrement, ça n’est pas bien complexe, mais il s’agit d’un point d’attention important à prendre en compte lors de l’implémentation de ce framework dans une application (j’imagine que cette étape est réalisée automatiquement si on utilise les librairies officielles).
Une solution quick-win serait de faire un rollout du pod serveur, ce qui va le forcer à re-générer un certificat SPIFFE valide, mais je ne suis pas sûr que quiconque ait envie de faire ça à chaque fois que le certificat SPIFFE expire. 😅
Pas le choix, nous devons intégrer cette logique de rechargement du certificat dans notre application.
func (cm *CertificateManager) StartAutoReload(interval time.Duration) {
go func() {
ticker := time.NewTicker(interval)
defer ticker.Stop()
for range ticker.C {
if err := cm.LoadCertificates(); err != nil {
log.Printf("Error reloading certificates: %v", err)
}
}
}()
log.Printf("Certificate auto-reload started with interval of %s", interval)
}
func (cm *CertificateManager) GetCertificate(*tls.ClientHelloInfo) (*tls.Certificate, error) {
cm.mu.RLock()
defer cm.mu.RUnlock()
return &cm.serverCert, nil
}
tlsConfig := &tls.Config{
GetCertificate: certManager.GetCertificate, // L'usage de GetCertificate permet de recharger le certificat à chaque nouvelle connexion
ClientAuth: tls.RequireAndVerifyClientCert,
ClientCAs: certManager.GetClientCAs(),
MinVersion: tls.VersionTLS12,
}
certManager.StartAutoReload(certReloadInterval) // démarre la routine de rechargement du certificat
Une fois implémenté, on peut voir que le serveur recharge son certificat SPIFFE toutes les minutes (oui j’ai un peu abusé, on pourrait mettre 20 minutes pour avoir un peu plus de temps avant l’expiration du certificat).
Attendons une heure pour voir si le certificat est bien rechargé…
Parfait ! Le client peut à nouveau se connecter au serveur et recevoir une réponse sans que nous ayons à faire quoi que ce soit.
Beh, mission réussie 🤩 !
Si jamais vous voulez récupérer le code complet de l’application, il est disponible sur GitHub.
Conclusion
Nous n’avons fait qu’effleurer les possibilités offertes par SPIFFE, mais réussir à le mettre en œuvre uniquement avec cert-manager et quelques opérateurs associés est déjà plus fun et intéressant que de se baser sur des solutions all-in-one.
Pour un usage en production, une alternative plus simple à maintenir pourrait être d’utiliser les fonctionnalités de Cilium (si vous l’utilisez déjà comme CNI). Cilium permet d’intégrer SPIFFE de façon totalement transparente pour les applications, en orchestrant un serveur SPIRE (voir la documentation). Cela implique toutefois de stocker les certificats racines dans un PVC, ce qui n’est pas encore idéal (espérons qu’une gestion via CustomResource soit possible à l’avenir). Bien que cette fonctionnalité soit encore en bêta, elle repose sur un vrai serveur SPIRE et offre donc une solution plus robuste que notre approche ici.
En résumé, expérimenter SPIFFE de cette manière est très instructif, et le faire directement avec cert-manager est plutôt séduisant ! Si j’ai l’occasion de pousser ce POC plus loin, j’aimerais intégrer des proxies Envoy pour reproduire le fonctionnement d’Istio et déléguer la gestion des certificats SPIFFE à des sidecars.
Merci d’avoir lu cet article et bon café ! ☕️