Échec du Challenge DNS01 Cert-Manager dans les Clusters GKE Privés
Problème
Les challenges ACME DNS01 de cert-manager restent bloqués en état « pending » indéfiniment dans les clusters GKE privés. La vérification de propagation échoue avec « DNS record for X not yet propagated » même si les enregistrements TXT sont correctement créés dans Cloudflare et vérifiables de partout — y compris depuis l'intérieur du cluster via des pods busybox.
Contexte / Conditions de Déclenchement
- cert-manager avec solveur DNS01 Cloudflare
- Cluster GKE privé (nœuds privés, endpoint public) avec Cloud NAT
- Les challenges affichent
presented: truemaisstate: pendingpendant des heures - Les logs de cert-manager montrent :
"propagation check failed" err="DNS record for \"example.com\" not yet propagated" dig TXT _acme-challenge.example.comdepuis l'extérieur retourne la valeur correctenslookupde busybox depuis l'intérieur du cluster retourne aussi la valeur correcte- La ressource Certificate affiche
Ready: Falseavecreason: RequestChanged
Causes Racines Découvertes
1. Google Cloud intercepte DNS vers 8.8.8.8
À l'intérieur des VPC GKE, les requêtes DNS vers 8.8.8.8 sont interceptées par l'infrastructure Google Cloud. Pour les domaines gérés par Cloudflare, cela peut retourner NXDOMAIN même quand l'enregistrement existe. C'est parce que Google achemine 8.8.8.8 via son infrastructure DNS interne qui peut avoir un comportement de résolution différent du service Google DNS public.
Vérification :
# Depuis l'intérieur du cluster - retourne NXDOMAIN
kubectl run dns-test --image=busybox:1.36 --rm -i --restart=Never -- \
nslookup -type=TXT _acme-challenge.example.com 8.8.8.8
# Depuis l'intérieur du cluster - retourne le résultat correct
kubectl run dns-test --image=busybox:1.36 --rm -i --restart=Never -- \
nslookup -type=TXT _acme-challenge.example.com 1.1.1.1
2. La vérification de propagation cert-manager échoue même avec des resolvers corrects
Même après configuration de --dns01-recursive-nameservers=1.1.1.1:53,1.0.0.1:53 et --dns01-recursive-nameservers-only=true, la vérification de propagation cert-manager peut toujours échouer. La bibliothèque DNS Go utilisée par cert-manager (miekg/dns) se comporte différemment de nslookup de busybox. La cause exacte est unclear mais peut être liée à :
- Les différences de parsing des réponses DNS entre miekg/dns et les resolvers système
- Les différences entre les requêtes DNS TCP et UDP
- Les problèmes de cache ou de timing internes cert-manager
- L'interaction de Cloud NAT avec les patterns de trafic DNS
Solution
Tentative 1 : Configurer les resolvers DNS récursifs (peut ne pas être suffisant)
Ajouter aux valeurs Helm de cert-manager :
dns01RecursiveNameservers: "1.1.1.1:53,1.0.0.1:53"
dns01RecursiveNameserversOnly: true
IMPORTANT : N'utilisez PAS 8.8.8.8 — Google Cloud intercepte cela à l'intérieur des VPC GKE.
Pour cert-manager géré par ArgoCD (chart Helm), ajouter à l'Application valuesObject :
valuesObject:
dns01RecursiveNameservers: "1.1.1.1:53,1.0.0.1:53"
dns01RecursiveNameserversOnly: true
Tentative 2 : Génération manuelle de certificat avec certbot (contournement fiable)
Si la correction du resolver ne fonctionne pas, générer le cert localement et l'injecter :
# Installer certbot avec le plugin Cloudflare
pipx install certbot
pipx inject certbot certbot-dns-cloudflare
# Obtenir le token API Cloudflare du cluster
CF_TOKEN=$(kubectl get secret cloudflare-api-token-secret -n cert-manager \
-o jsonpath='{.data.api-token}' | base64 -d)
# Créer le fichier de credentials
mkdir -p /tmp/certbot-cf
echo "dns_cloudflare_api_token = $CF_TOKEN" > /tmp/certbot-cf/cloudflare.ini
chmod 600 /tmp/certbot-cf/cloudflare.ini
# Générer le certificat
certbot certonly \
--dns-cloudflare \
--dns-cloudflare-credentials /tmp/certbot-cf/cloudflare.ini \
--dns-cloudflare-propagation-seconds 30 \
-d '*.example.com' -d 'example.com' \
--non-interactive --agree-tos --email admin@example.com \
--config-dir /tmp/certbot-cf/config \
--work-dir /tmp/certbot-cf/work \
--logs-dir /tmp/certbot-cf/logs \
--key-type ecdsa --elliptic-curve secp256r1
# Injecter dans le cluster
kubectl create secret tls wildcard-tls-secret \
--cert=/tmp/certbot-cf/config/live/example.com/fullchain.pem \
--key=/tmp/certbot-cf/config/live/example.com/privkey.pem \
-n nginx-gateway --dry-run=client -o yaml | kubectl apply -f -
# Nettoyer
rm -rf /tmp/certbot-cf
Vérification
# Vérifier le certificat servi par la gateway
echo | openssl s_client -connect upload.example.com:443 \
-servername upload.example.com 2>/dev/null | \
openssl x509 -noout -subject -ext subjectAltName
# Tester l'endpoint
curl -s https://upload.example.com/
Commandes de Debugging
# Vérifier le statut du challenge
kubectl get challenges -n nginx-gateway
# Vérifier les détails du challenge (key = valeur TXT attendue)
kubectl get challenge <name> -n nginx-gateway \
-o jsonpath='domain: {.spec.dnsName}, key: {.spec.key}, presented: {.status.presented}'
# Vérifier les args de cert-manager
kubectl get deployment cert-manager -n cert-manager \
-o jsonpath='{.spec.template.spec.containers[0].args}'
# Vérifier le statut du certificat
kubectl get certificate wildcard-tls -n nginx-gateway -o yaml
# Vérifier quel cert est actuellement dans le secret
kubectl get secret wildcard-tls-secret -n nginx-gateway \
-o jsonpath='{.data.tls\.crt}' | base64 -d | \
openssl x509 -noout -subject -ext subjectAltName
# Tester DNS depuis l'intérieur du cluster
kubectl run dns-test --image=busybox:1.36 --rm -i --restart=Never -- \
nslookup -type=TXT _acme-challenge.example.com 1.1.1.1
# Supprimer l'order stale pour forcer un refresh
kubectl delete order <order-name> -n nginx-gateway
Notes
- Le cert généré manuellement expire après 90 jours et ne se renouvellera pas automatiquement
- cert-manager va éventuellement écraser le secret injecté manuellement quand/si il émet avec succès son propre cert — c'est normal et souhaité
- Quand on demande à la fois le wildcard (
*.example.com) et la base (example.com), ACME nécessite des authorizations séparées qui utilisent toutes deux des enregistrements TXT_acme-challenge.example.comavec des valeurs différentes - Les root apps ArgoCD avec
automated.enabled: falsene se synchro-automatisent pas — vous devez déclencher manuellement viakubectl patch app root -n argocd --type merge -p '{"operation":{"sync":...}}' - Le conteneur cert-manager est distroless (pas de shell) — vous ne pouvez pas executer dedans pour déboguer DNS