J'ai voulu gérer les certificats TLS dans mes clusters KubeVirt. Surprise
Quatrième épisode de la série "KubeVirt + k0smotron, ou comment chaque feature Kubernetes déclenche une investigation réseau". Après les clusters, l'ingress, et le monitoring, j'avais une idée géniale : automatiser les certificats TLS avec cert-manager. Je me disais que ça ne pouvait pas être si compliqué.
Je dis ça à chaque fois.
Le contexte (de plus en plus court, j'ai appris)
À ce stade de la série :
- Des clusters Kubernetes guests tournent dans un cluster hôte via KubeVirt + k0smotron
- Chaque cluster guest a un ingress Contour avec une IP physique (10.2.6.220/29)
- Prometheus + Grafana sont exposés via cet ingress
- Tout est accessible en HTTP
Ce dernier point est un problème. HTTP, c'est bien pour le développement. Pour la production, on veut HTTPS. Et pour HTTPS, il faut des certificats. Et pour les certificats, il y a cert-manager.
Plan :
- Déployer cert-manager dans le cluster guest
- Créer un
ClusterIssuerLet's Encrypt - Annoter les Ingress avec
cert-manager.io/cluster-issuer - Profiter
Spoiler : le problème arrive dès l'étape 1.
Acte 1 — cert-manager s'installe... enfin presque
On se connecte au cluster guest et on installe cert-manager dans le cluster guest — pas dans le cluster hôte :
export KUBECONFIG=.kube/guest-01.conf
helm repo add jetstack https://charts.jetstack.io && helm repo update
helm install cert-manager jetstack/cert-manager \
--namespace cert-manager --create-namespace \
--set crds.enabled=true
Les pods démarrent bien :
kubectl get pods -n cert-manager
cert-manager-xxxxxxxx-xxxxx 1/1 Running
cert-manager-cainjector-xxxxx-xxxxx 1/1 Running
cert-manager-webhook-xxxxx-xxxxx 1/1 Running
Trois pods Running. Pas de CrashLoop. Pas d'erreur de probe. Cert-manager ne tourne pas en hostNetwork: true, ses probes ciblent localhost nativement — pour une fois, rien à patcher.
Mais Helm ne revient pas. Il attend. 30 secondes. 60 secondes. Et finalement :
Error: INSTALLATION FAILED: failed post-install step: ...
context deadline exceeded
Le job cert-manager-startupapicheck (un hook Helm post-install) a échoué. Ses logs :
"Not ready" err="Internal error occurred: failed calling webhook
"webhook.cert-manager.io": failed to call webhook:
Post "https://cert-manager-webhook.cert-manager.svc:443/mutate?timeout=30s":
context deadline exceeded"
Le problème ne s'est pas invité à l'étape 3. Il était là dès l'étape 1.
Acte 2 — Le webhook bloque tout, dès le premier apply
Helm est planté. On essaie quand même d'avancer manuellement. On crée un ClusterIssuer self-signed — la ressource cert-manager la plus simple, zéro dépendance externe :
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: selfsigned
spec:
selfSigned: {}
kubectl apply -f selfsigned-issuer.yaml
Error from server (InternalError): error when creating "selfsigned-issuer.yaml":
Internal error occurred: failed calling webhook "webhook.cert-manager.io":
failed to call webhook:
Post "https://cert-manager-webhook.cert-manager.svc:443/validate?timeout=30s":
context deadline exceeded (Client.Timeout exceeded while awaiting headers)
Même erreur. Le webhook bloque toutes les ressources cert-manager — pas seulement les certificats, les issuers aussi. On ne peut rien créer.
Explication : c'est le même problème architectural que metrics-server et node-exporter, sous une autre forme. L'apiserver du cluster guest (le pod kmc-cluster-guest-01-0) tourne dans le cluster hôte. Quand il doit valider une ressource cert-manager, il appelle le webhook — exposé via une ClusterIP dans le réseau du cluster guest. Le pod kmc ne peut pas joindre cette ClusterIP.
kubectl apply ClusterIssuer
↓
guest apiserver (kmc — cluster HÔTE)
↓
POST https://cert-manager-webhook.cert-manager.svc:443/validate
↓
résolution : ClusterIP cluster GUEST (10.96.x.x)
↓
FAIL — non routable depuis le réseau du cluster hôte
↓
context deadline exceeded
Ce n'est pas un bug cert-manager — le webhook de validation est un mécanisme de sécurité normal. C'est l'architecture nested qui rend ce webhook injoignable. C'est le troisième composant qu'on voit tomber pour la même raison : metrics-server (kube-aggregation), node-exporter (probes), cert-manager (webhook). La liste s'allonge.
Acte 3 — Les solutions
Solution 1 — Contourner le webhook (rapide, deux étapes)
--set webhook.enabled=false n'existe plus dans les versions récentes du chart — le schema le rejette :
Error: values don't meet the specifications of the schema(s):
- at '/webhook': additional properties 'enabled' not allowed
La bonne approche en deux étapes :
Étape 1 — Désactiver le startupapicheck pour que helm install termine :
helm install cert-manager jetstack/cert-manager \
--namespace cert-manager --create-namespace \
--set crds.enabled=true \
--set startupapicheck.enabled=false
Le webhook est toujours là, mais Helm ne bloque plus sur le check post-install.
Étape 2 — Passer le webhook en failurePolicy: Ignore et réduire son timeout :
for WHC in validatingwebhookconfiguration mutatingwebhookconfiguration; do
kubectl patch ${WHC} cert-manager-webhook \
--type=json \
-p='[
{"op":"replace","path":"/webhooks/0/failurePolicy","value":"Ignore"},
{"op":"replace","path":"/webhooks/0/timeoutSeconds","value":5}
]'
done
Avec failurePolicy: Ignore, l'apiserver tente d'appeler le webhook, attend le timeout, puis laisse passer la requête. Sans réduire timeoutSeconds, chaque kubectl apply prend 30 secondes (le timeout par défaut) avant de réussir. En le passant à 5 secondes, c'est plus supportable — même si toujours pas instantané.
kubectl apply -f selfsigned-issuer.yaml
# clusterissuer.cert-manager.io/selfsigned created
# (5 secondes de délai — le webhook essaie, timeout, ignore)
Avantage : cert-manager reste dans un état proche de la normale. Inconvénient : chaque apply de ressource cert-manager prend timeoutSeconds avant de passer. Pour des opérations automatisées (GitOps), c'est acceptable. Pour le développement interactif, c'est pénible.
Solution 2 — Exposer le webhook via NodePort (propre)
Le pod kmc est dans le cluster hôte. Les workers du cluster guest sont des pods virt-launcher dans le cluster hôte. Ces pods ont des IPs dans le CIDR du cluster hôte (10.244.x.x), joignables depuis kmc.
On peut donc exposer le webhook en NodePort sur les workers, et reconfigurer le ValidatingWebhookConfiguration pour cibler directement l'IP d'un worker + le NodePort :
# Exposer le webhook en NodePort
kubectl patch svc cert-manager-webhook -n cert-manager \
--type=merge -p '{"spec":{"type":"NodePort"}}'
# Récupérer le NodePort et l'IP d'un worker
NODEPORT=$(kubectl get svc cert-manager-webhook -n cert-manager \
-o jsonpath='{.spec.ports[0].nodePort}')
WORKER_IP=$(kubectl get nodes -o jsonpath='{.items[0].status.addresses[?(@.type=="InternalIP")].address}')
# Reconfigurer les webhooks pour utiliser l'URL directe
kubectl patch validatingwebhookconfiguration cert-manager-webhook \
--type=json -p="[
{\"op\": \"replace\",
\"path\": \"/webhooks/0/clientConfig\",
\"value\": {\"url\": \"https://${WORKER_IP}:${NODEPORT}/validate-cert-manager-io-v1-certificate\",
\"caBundle\": \"$(kubectl get secret cert-manager-webhook-ca -n cert-manager -o jsonpath='{.data.tls\.crt}')\"}}
]"
C'est fonctionnel mais fragile — si le node tombe, l'URL pointe dans le vide.
Solution 3 — CA interne + cert-manager en mode self-signed (recommandée)
Pour un cluster privé non accessible depuis Internet, Let's Encrypt (HTTP-01) ne fonctionnera pas de toute façon — l'ACME challenge requiert que les serveurs de Let's Encrypt puissent joindre http://votre-domaine/.well-known/acme-challenge/xxx. Ce n'est pas le cas sur 10.2.6.220.
La bonne approche pour un cluster interne : une CA auto-gérée.
# 1. ClusterIssuer self-signed pour bootstrapper la CA
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: selfsigned
spec:
selfSigned: {}
---
# 2. Certificat CA racine
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: internal-ca
namespace: cert-manager
spec:
isCA: true
commonName: guest-01-internal-ca
secretName: internal-ca-secret
privateKey:
algorithm: ECDSA
size: 256
issuerRef:
name: selfsigned
kind: ClusterIssuer
---
# 3. ClusterIssuer basé sur la CA
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: internal-ca
spec:
ca:
secretName: internal-ca-secret
Une fois la CA en place, les Certificate sont créés localement par cert-manager, sans aucun appel sortant. Pas de webhook externe, pas de ACME, pas de problème réseau.
# Certificat Grafana
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: grafana-tls
namespace: monitoring
spec:
secretName: grafana-tls
issuerRef:
name: internal-ca
kind: ClusterIssuer
dnsNames:
- grafana.guest-01.mon-domaine.fr
Et l'ingress Contour :
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: grafana
namespace: monitoring
annotations:
cert-manager.io/cluster-issuer: internal-ca
spec:
tls:
- hosts:
- grafana.guest-01.mon-domaine.fr
secretName: grafana-tls
rules:
- host: grafana.guest-01.mon-domaine.fr
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: grafana
port:
number: 3000
Résultat : Grafana accessible en HTTPS, certificat signé par la CA interne. Les navigateurs afficheront un avertissement (CA non reconnue publiquement) — on peut distribuer le certificat racine aux machines de l'équipe pour supprimer cet avertissement.
Acte 4 — Et Let's Encrypt alors ?
Si le domaine est public et que le cluster guest est accessible depuis Internet (via le VIP 10.2.6.220 routé publiquement), HTTP-01 fonctionne. Sinon, DNS-01 est la seule option — on délègue la preuve de propriété du domaine à un enregistrement DNS TXT, sans que l'ACME server ait besoin de joindre le cluster.
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-dns
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: admin@mon-domaine.fr
privateKeySecretRef:
name: letsencrypt-account-key
solvers:
- dns01:
cloudflare: # adapter selon le provider DNS
email: admin@mon-domaine.fr
apiTokenSecretRef:
name: cloudflare-api-token
key: api-token
DNS-01 est compatible avec les clusters privés. La preuve de propriété se fait uniquement via DNS — cert-manager crée un enregistrement TXT temporaire, Let's Encrypt le vérifie, le certificat est émis.
L'inconvénient : il faut que le provider DNS supporte l'API (Cloudflare, Route53, OVH, Scaleway, Gandi...). Si le DNS est géré manuellement ou sur un provider non supporté, c'est compliqué.
Ce que j'aurais voulu savoir avant (épisode 4)
1. Le webhook cert-manager a le même problème que metrics-server et les autres. L'apiserver du cluster nested ne peut pas joindre les webhooks enregistrés dans le cluster guest. Soit on désactive le webhook, soit on l'expose via NodePort sur les workers (IPs joignables depuis kmc), soit on contourne en utilisant des issuers qui n'en ont pas besoin (selfSigned, CA).
2. HTTP-01 ne fonctionne pas sur un cluster privé. Let's Encrypt doit pouvoir joindre le cluster. Si l'IP n'est pas publique, il faut DNS-01 ou une CA interne.
3. Une CA interne est souvent la meilleure solution pour un cluster privé. Pas de dépendance externe, pas de quota ACME, certificats émis en quelques secondes, zéro appel sortant. Le seul coût : distribuer la CA aux clients qui doivent faire confiance aux certificats.
4. cert-manager peut tourner sans webhook.
--set webhook.enabled=false. Moins de validation, mais ça fonctionne si les ressources sont correctes. Acceptable pour un cluster de développement.
Et maintenant ?
Le cluster guest a désormais :
- Un control plane k0s en pod sur le cluster hôte : OK
- Des workers en VMs KubeVirt avec Cilium : OK
- Un ingress Contour accessible via IP physique : OK
- Prometheus + Grafana exposés via cet ingress : OK
- Certificats TLS via CA interne (cert-manager) : OK
kubectl top— et on s'en accommode toujours très bien : KO
Pour le prochain cluster, tout ça se déploie via Helm + le chart kubevirt-cluster. La plateforme devient un produit.
La prochaine étape : gérer le multi-tenant — plusieurs équipes, plusieurs clusters, isolation réseau et RBAC. Je me dis que ça ne peut pas être si compliqué.
Je dis ça à chaque fois. Et pourtant, j'continue.
Ce sujet vous intéresse ? Clusters multi-tenant, KubeVirt, plateformes Kubernetes as a Service, automatisation de bootstrap ? N'hésitez pas à me contacter directement.
Hervé Leclerc — Alterway
#Kubernetes #KubeVirt #CertManager #TLS #HTTPS #CloudNative #Infrastructure #GitOps #CAPI #RetourDExperience
Découvrez les derniers articles d'alter way
- J'ai voulu faire du kubectl top dans mon cluster KubeVirt. L'univers a refusé
- J'ai voulu exposer mes clusters KubeVirt sur Internet. J'aurais dû m'en douter
- J'ai voulu faire du Kubernetes dans Kubernetes
- J'ai voulu gérer les certificats TLS dans mes clusters KubeVirt. Surprise
- Industrialiser le RAG pour booster l’IA générative : l’approche pragmatique en 2025
- VMWare --> Open Source