certmanager-dns01-gke-private-cluster

Par divinevideo · divine-mobile

Résoudre les challenges DNS01 ACME de cert-manager bloqués en état "pending" avec le message "DNS record not yet propagated" dans les clusters privés GKE, même lorsque les enregistrements TXT existent dans Cloudflare DNS. À utiliser quand : (1) les challenges cert-manager restent en "pending" pendant des heures avec des échecs de vérification de propagation, (2) `dig` depuis l'extérieur du cluster affiche les bons enregistrements TXT mais cert-manager ne peut pas les vérifier, (3) utilisation du solver Cloudflare DNS01 dans un cluster privé GKE avec Cloud NAT, (4) Google Cloud intercepte les requêtes DNS vers 8.8.8.8 et renvoie NXDOMAIN pour les enregistrements gérés par Cloudflare, (5) même `--dns01-recursive-nameservers` avec 1.1.1.1 ne résout pas le problème de vérification de propagation malgré des enregistrements TXT vérifiables depuis des pods busybox dans le même namespace. Couvre le flux de débogage complet et le contournement manuel avec certbot.

npx skills add https://github.com/divinevideo/divine-mobile --skill certmanager-dns01-gke-private-cluster

É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: true mais state: pending pendant 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.com depuis l'extérieur retourne la valeur correcte
  • nslookup de busybox depuis l'intérieur du cluster retourne aussi la valeur correcte
  • La ressource Certificate affiche Ready: False avec reason: 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.com avec des valeurs différentes
  • Les root apps ArgoCD avec automated.enabled: false ne se synchro-automatisent pas — vous devez déclencher manuellement via kubectl 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

Skills similaires