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.

alt text

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).

alt text

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.

Source

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.

alt text

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.

alt text


Mais au fur et à mesure que le temps passe, on va se rendre compte que le client ne peut plus se connecter au serveur :

alt text

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). alt text

Attendons une heure pour voir si le certificat est bien rechargé…

alt text

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é ! ☕️