本文へ移動
cccskills
無料GitHub で公開

certmanager-dns01-gke-private-cluster

Fix cert-manager DNS01 ACME challenges stuck in "pending" state with "DNS record not yet propagated" inside GKE private clusters, even when TXT records exist in Cloudflare DNS. Use when: (1) cert-manager challenges show "pending" for hours with propagation check failures, (2) dig from outside cluster shows correct TXT records but cert-manager can't verify them, (3) Using Cloudflare DNS01 solver in a GKE private cluster with Cloud NAT, (4) Google Cloud intercepts 8.8.8.8 DNS queries returning NXDOMAIN for Cloudflare-managed records, (5) Even --dns01-recursive-nameservers with 1.1.1.1 doesn't fix the propagation check despite TXT records being verifiable from busybox pods in the same namespace. Covers the full debugging flow and certbot manual workaround.

インストール方法を見る

含まれるファイル(1)

  • SKILL.md6.9 KB

SKILL.md(原文)

インストールする前に、エージェントに与えられる指示の中身を確認できます。

Cert-Manager DNS01 Challenge Failure in GKE Private Clusters

Problem

Cert-manager DNS01 ACME challenges get stuck in "pending" state indefinitely inside GKE private clusters. The propagation check fails with "DNS record for X not yet propagated" even though the TXT records are correctly created in Cloudflare and verifiable from everywhere — including from inside the cluster using busybox pods.

Context / Trigger Conditions

  • cert-manager with Cloudflare DNS01 solver
  • GKE private cluster (private nodes, public endpoint) with Cloud NAT
  • Challenges show presented: true but state: pending for hours
  • cert-manager logs show: "propagation check failed" err="DNS record for \"example.com\" not yet propagated"
  • dig TXT _acme-challenge.example.com from outside returns correct value
  • busybox nslookup from inside cluster also returns correct value
  • Certificate resource shows Ready: False with reason: RequestChanged

Root Causes Discovered

1. Google Cloud intercepts DNS to 8.8.8.8

Inside GKE VPCs, DNS queries to 8.8.8.8 are intercepted by Google Cloud infrastructure. For Cloudflare-managed domains, this can return NXDOMAIN even when the record exists. This is because Google routes 8.8.8.8 through their internal DNS infrastructure which may have different resolution behavior than the public Google DNS service.

Verification:

# From inside cluster - returns NXDOMAIN
kubectl run dns-test --image=busybox:1.36 --rm -i --restart=Never -- \
  nslookup -type=TXT _acme-challenge.example.com 8.8.8.8

# From inside cluster - returns correct result
kubectl run dns-test --image=busybox:1.36 --rm -i --restart=Never -- \
  nslookup -type=TXT _acme-challenge.example.com 1.1.1.1

2. Cert-manager propagation check still fails with correct resolvers

Even after configuring --dns01-recursive-nameservers=1.1.1.1:53,1.0.0.1:53 and --dns01-recursive-nameservers-only=true, cert-manager's propagation check may still fail. The Go DNS library used by cert-manager (miekg/dns) behaves differently from busybox's nslookup. The exact cause is unclear but may relate to:

  • DNS response parsing differences between miekg/dns and system resolvers
  • TCP vs UDP DNS query differences
  • Internal cert-manager caching or timing issues
  • Cloud NAT interaction with DNS traffic patterns

Solution

Attempt 1: Configure recursive DNS resolvers (may not be sufficient)

Add to cert-manager Helm values:

dns01RecursiveNameservers: "1.1.1.1:53,1.0.0.1:53"
dns01RecursiveNameserversOnly: true

IMPORTANT: Do NOT use 8.8.8.8 — Google Cloud intercepts this inside GKE VPCs.

For ArgoCD-managed cert-manager (Helm chart), add to the Application valuesObject:

valuesObject:
  dns01RecursiveNameservers: "1.1.1.1:53,1.0.0.1:53"
  dns01RecursiveNameserversOnly: true

Attempt 2: Manual cert generation with certbot (reliable workaround)

If the resolver fix doesn't work, generate the cert locally and inject it:

# Install certbot with Cloudflare plugin
pipx install certbot
pipx inject certbot certbot-dns-cloudflare

# Get Cloudflare API token from cluster
CF_TOKEN=$(kubectl get secret cloudflare-api-token-secret -n cert-manager \
  -o jsonpath='{.data.api-token}' | base64 -d)

# Create credentials file
mkdir -p /tmp/certbot-cf
echo "dns_cloudflare_api_token = $CF_TOKEN" > /tmp/certbot-cf/cloudflare.ini
chmod 600 /tmp/certbot-cf/cloudflare.ini

# Generate certificate
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

# Inject into 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 -

# Clean up
rm -rf /tmp/certbot-cf

Verification

# Check the certificate served by the gateway
echo | openssl s_client -connect upload.example.com:443 \
  -servername upload.example.com 2>/dev/null | \
  openssl x509 -noout -subject -ext subjectAltName

# Test endpoint
curl -s https://upload.example.com/

Debugging Commands

# Check challenge status
kubectl get challenges -n nginx-gateway

# Check challenge details (key = expected TXT value)
kubectl get challenge <name> -n nginx-gateway \
  -o jsonpath='domain: {.spec.dnsName}, key: {.spec.key}, presented: {.status.presented}'

# Check cert-manager args
kubectl get deployment cert-manager -n cert-manager \
  -o jsonpath='{.spec.template.spec.containers[0].args}'

# Check certificate status
kubectl get certificate wildcard-tls -n nginx-gateway -o yaml

# Check what cert is currently in the secret
kubectl get secret wildcard-tls-secret -n nginx-gateway \
  -o jsonpath='{.data.tls\.crt}' | base64 -d | \
  openssl x509 -noout -subject -ext subjectAltName

# Test DNS from inside cluster
kubectl run dns-test --image=busybox:1.36 --rm -i --restart=Never -- \
  nslookup -type=TXT _acme-challenge.example.com 1.1.1.1

# Delete stale order to force refresh
kubectl delete order <order-name> -n nginx-gateway

Notes

  • The manually generated cert expires after 90 days and won't auto-renew
  • cert-manager will eventually overwrite the manually injected secret when/if it successfully issues its own cert — this is fine and desired
  • When requesting both wildcard (*.example.com) and base (example.com), ACME requires separate authorizations that both use _acme-challenge.example.com TXT records with different values
  • ArgoCD root apps with automated.enabled: false won't auto-sync — you need to trigger manually via kubectl patch app root -n argocd --type merge -p '{"operation":{"sync":...}}'
  • The cert-manager container is distroless (no shell) — you can't exec into it to debug DNS

レビュー

まだレビューはありません。使ってみた感想をお寄せください。

同じリポジトリのスキル

概要と使いどころ

Fix ArgoCD ExternalSecret deployment failing with "namespace X is not permitted in project Y". Use when: (1) ExternalSecret shows OutOfSync in ArgoCD but won't sync, (2) ArgoCD application status shows "namespace X is not permitted in project 'infrastructure'", (3) ExternalSecret targets a namespace managed by a different ArgoCD project, (4) Using apps-of-apps pattern with separate infrastructure and application projects.

日本語の概要は準備中です。原文の説明を表示しています。

divinevideo/divine-mobile2662026年10月13日 更新

Art direction for any content — reads text, PDF, Word, HTML, PPT, then proposes 2-3 creative directions with photography style, mood, and visual language. After selection, generates AI image prompts and visual briefs section-by-section. Use when the user shares content and needs visual direction, image sourcing, or creative direction for any material.

日本語の概要は準備中です。原文の説明を表示しています。

divinevideo/divine-mobile2662026年10月13日 更新

Fix "Null check operator used on a null value" errors when an object is set to null during an async await. Use when: (1) Object reference is nullified while awaiting, (2) Code accesses object with ! after await returns, (3) Cancel/dispose operations run concurrently with async operations on same object. Solution: capture local reference before await.

日本語の概要は準備中です。原文の説明を表示しています。

divinevideo/divine-mobile2662026年10月13日 更新

Add custom metadata headers (x-amz-meta-*) to AWS v4 signed requests for GCS S3-compatible API. Use when: (1) Adding custom metadata to GCS uploads via S3 API, (2) Getting signature mismatch errors after adding new headers, (3) x-amz-meta-* headers being ignored or causing 403 errors. Custom headers MUST be included in canonical headers and signed headers list.

日本語の概要は準備中です。原文の説明を表示しています。

divinevideo/divine-mobile2662026年10月13日 更新

Fix password/secret authentication failures caused by trailing newlines when creating Google Cloud secrets (or similar) with bash here-strings. Use when: (1) Password authentication fails with correct password, (2) Secret created with `<<< "value"` syntax, (3) Error like "password authentication failed" or "invalid token" despite correct value. Bash here-strings (`<<<`) add a trailing newline that corrupts secrets.

日本語の概要は準備中です。原文の説明を表示しています。

divinevideo/divine-mobile2662026年10月13日 更新

Fix silent video/media processing failures caused by URL extraction code that filters on file extensions (.mp4, .webm, .webp). Use when: (1) Media moderation, transcoding, or analysis silently skips files from Blossom or content-addressed storage servers, (2) URL extraction from Nostr event tags (imeta, r tags) drops URLs without recognized extensions, (3) CDN fallback URLs append .mp4 but the actual server uses extensionless content-addressed paths like /{sha256}. Common in Nostr video events (kind 34236) where different clients use different URL formats.

日本語の概要は準備中です。原文の説明を表示しています。

divinevideo/divine-mobile2662026年10月13日 更新

divinevideo のスキルをすべて見る

このスキルの問題を報告する