All Articles

cert-manager Certificate Not Ready: Causes and Fixes

cert-manager Certificate stuck in a non-Ready state is a common Kubernetes TLS issue. This guide covers every root cause — DNS challenges, RBAC, rate limits, and issuer problems — with step-by-step fixes.

DevOpsBoysMar 16, 20266 min read
Share:Tweet

You installed cert-manager, created a Certificate resource, and your pod is waiting for TLS to come up. But the Certificate stays False. The Ingress shows no TLS secret. Your HTTPS doesn't work.

This is one of the most frustrating Kubernetes issues because the failure chain is long: cert-manager → ACME challenge → DNS → Let's Encrypt → certificate. Any link can break silently.

This guide walks through every common cause and how to fix it.


Step 1: Understand the Chain

Before debugging, understand how cert-manager issues a certificate:

Certificate → CertificateRequest → Order → Challenge
     │              │                │         │
     ▼              ▼                ▼         ▼
  You create    cert-manager      ACME      DNS-01 or
  this          creates this      server    HTTP-01
  1. You create a Certificate (or cert-manager creates one via Ingress annotation)
  2. cert-manager creates a CertificateRequest
  3. cert-manager creates an ACME Order with Let's Encrypt
  4. Let's Encrypt issues a Challenge (HTTP-01 or DNS-01)
  5. cert-manager satisfies the challenge
  6. Let's Encrypt issues the certificate
  7. cert-manager stores it as a Kubernetes Secret

Any step can fail. The key is knowing where.


Step 2: Run These Diagnostic Commands

Always run these in order:

bash
# 1. Check the Certificate status
kubectl describe certificate <cert-name> -n <namespace>
 
# 2. Check the CertificateRequest
kubectl describe certificaterequest -n <namespace>
 
# 3. Check the Order (ACME)
kubectl describe order -n <namespace>
 
# 4. Check the Challenge
kubectl describe challenge -n <namespace>
 
# 5. Check cert-manager controller logs
kubectl logs -n cert-manager -l app=cert-manager --tail=100
 
# 6. Check cert-manager webhook logs
kubectl logs -n cert-manager -l app=webhook --tail=50

The describe output on each resource has an Events or Status.Conditions section. Read it carefully — it points directly to the failure.


Cause 1: HTTP-01 Challenge Failing (Wrong Ingress or Network)

HTTP-01 is the most common challenge type. Let's Encrypt sends an HTTP request to http://your-domain.com/.well-known/acme-challenge/<token> and expects a specific response.

Symptoms

$ kubectl describe challenge
Status:
  Presented:  false
  Reason:     Waiting for HTTP-01 challenge propagation
  State:      pending
Events:
  Warning  PresentError   2m  cert-manager  Error presenting challenge:
           service "cm-acme-http-solver-xxxxx" already exists

Or:

Error: 403 Forbidden. The value provided for the challenge was incorrect.

Diagnoses

Check if the solver pod is running:

bash
kubectl get pods -n <namespace> | grep cm-acme-http-solver

It should show Running. If it's not there or it's in Pending, the challenge can't be served.

Check if the Ingress for the solver was created:

bash
kubectl get ingress -n <namespace> | grep cm-acme

Test the challenge URL manually:

bash
# Get the challenge token path
kubectl describe challenge -n <namespace>
# Look for: "Token: abc123..."
 
# Test from outside your cluster
curl http://your-domain.com/.well-known/acme-challenge/abc123

Fixes

Fix 1: Ensure your Ingress allows HTTP traffic

cert-manager needs to serve HTTP on port 80. If you're redirecting all HTTP to HTTPS, the challenge fails.

yaml
# nginx ingress — allow HTTP for ACME challenge
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: my-app
  annotations:
    nginx.ingress.kubernetes.io/ssl-redirect: "false"    # temporarily disable
    cert-manager.io/cluster-issuer: "letsencrypt-prod"
spec:
  tls:
    - hosts:
        - myapp.example.com
      secretName: myapp-tls
  rules:
    - host: myapp.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: my-app
                port:
                  number: 80

Fix 2: Check your Ingress class annotation

yaml
metadata:
  annotations:
    kubernetes.io/ingress.class: "nginx"      # or your ingress class
    cert-manager.io/cluster-issuer: "letsencrypt-prod"

Fix 3: Verify DNS points to your cluster's external IP

bash
# Check what IP your domain resolves to
nslookup myapp.example.com
 
# Check your LoadBalancer IP
kubectl get svc -n ingress-nginx

They must match.


Cause 2: DNS-01 Challenge Failing

For wildcard certificates (e.g., *.example.com), you must use DNS-01 challenges. cert-manager creates a TXT record in your DNS zone.

Symptoms

Events:
  Warning  PresentError  5m  cert-manager  Error presenting challenge:
           Failed to create TXT record: AccessDenied

Fix: Check API credentials for your DNS provider

For Route53:

yaml
apiVersion: v1
kind: Secret
metadata:
  name: route53-credentials
  namespace: cert-manager
type: Opaque
stringData:
  secret-access-key: "<your-aws-secret-key>"
---
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-prod
spec:
  acme:
    server: https://acme-v02.api.letsencrypt.org/directory
    email: admin@example.com
    privateKeySecretRef:
      name: letsencrypt-prod-account-key
    solvers:
      - dns01:
          route53:
            region: us-east-1
            hostedZoneID: Z1234567890
            accessKeyID: AKIAIOSFODNN7EXAMPLE
            secretAccessKeySecretRef:
              name: route53-credentials
              key: secret-access-key

For Cloudflare:

yaml
solvers:
  - dns01:
      cloudflare:
        email: admin@example.com
        apiTokenSecretRef:
          name: cloudflare-api-token
          key: api-token

Verify cert-manager can reach the DNS API by checking logs:

bash
kubectl logs -n cert-manager -l app=cert-manager | grep -i "dns01\|route53\|cloudflare"

Cause 3: Wrong or Missing ClusterIssuer

Symptoms

Events:
  Warning  IssuerNotFound  2m  cert-manager
           Referenced ClusterIssuer "letsencrypt-prod" not found

Or the Issuer is in a different namespace:

Warning  IssuerNotFound  Referenced Issuer does not exist

Fix

Check your issuer exists:

bash
kubectl get clusterissuer
kubectl get issuer -n <namespace>

A correct ClusterIssuer for production:

yaml
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-prod
spec:
  acme:
    server: https://acme-v02.api.letsencrypt.org/directory
    email: admin@example.com       # must be a real email
    privateKeySecretRef:
      name: letsencrypt-prod-account-key
    solvers:
      - http01:
          ingress:
            class: nginx           # must match your ingress controller class

Common mistake: Using letsencrypt-staging in production (certificates aren't trusted by browsers).

IssuerURLTrusted by browsers
staginghttps://acme-staging-v02.api.letsencrypt.org/directoryNo
prodhttps://acme-v02.api.letsencrypt.org/directoryYes

Cause 4: Let's Encrypt Rate Limits

Let's Encrypt has strict rate limits. If you've been doing a lot of testing:

  • 5 duplicate certificates per week
  • 50 certificates per registered domain per week
  • 5 failures per account per hostname per hour

Symptoms

Error: too many certificates already issued for exact set of domains
Error: too many failed authorizations recently

Fix

Switch to the staging issuer while testing:

yaml
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-staging
spec:
  acme:
    server: https://acme-staging-v02.api.letsencrypt.org/directory
    email: admin@example.com
    privateKeySecretRef:
      name: letsencrypt-staging-key
    solvers:
      - http01:
          ingress:
            class: nginx

Use staging until your setup works. Then switch to letsencrypt-prod. Staging issues unlimited certificates (but they're not browser-trusted, only for testing).


Cause 5: RBAC Issues

cert-manager needs RBAC permissions to create secrets, ingresses, and manage ACME resources.

Symptoms

Events:
  Warning  ErrInitIssuer  cert-manager  Error initializing issuer:
           error getting secret: secrets "letsencrypt-prod-account-key" is forbidden:
           User "system:serviceaccount:cert-manager:cert-manager" cannot get resource "secrets"

Fix

This usually means cert-manager wasn't installed with cluster-level permissions. Reinstall with Helm:

bash
helm install cert-manager jetstack/cert-manager \
  --namespace cert-manager \
  --create-namespace \
  --version v1.14.0 \
  --set installCRDs=true \
  --set global.leaderElection.namespace=cert-manager

If you installed manually, verify the ClusterRoleBinding exists:

bash
kubectl get clusterrolebinding | grep cert-manager

Cause 6: Webhook Not Ready

cert-manager has a validating webhook. If it's not ready, certificate creation fails silently.

Symptoms

Error from server (InternalError): error when creating "certificate.yaml":
Internal error occurred: failed calling webhook "webhook.cert-manager.io":
Post "https://cert-manager-webhook.cert-manager.svc:443/...": dial tcp: ...
connection refused

Fix

Check the webhook pod:

bash
kubectl get pods -n cert-manager
kubectl logs -n cert-manager -l app=webhook --tail=50

If the webhook pod is unhealthy, restart it:

bash
kubectl rollout restart deployment cert-manager-webhook -n cert-manager

Wait 30 seconds, then try creating the Certificate again.


Quick Reference: Debug Checklist

bash
# 1. Certificate status
kubectl get certificate -n <namespace>
kubectl describe certificate <name> -n <namespace>
 
# 2. CertificateRequest
kubectl get certificaterequest -n <namespace>
kubectl describe certificaterequest <name> -n <namespace>
 
# 3. ACME Order
kubectl get order -n <namespace>
kubectl describe order <name> -n <namespace>
 
# 4. Challenge (the actual HTTP or DNS verification)
kubectl get challenge -n <namespace>
kubectl describe challenge <name> -n <namespace>
 
# 5. cert-manager logs
kubectl logs -n cert-manager deployment/cert-manager --tail=100
 
# 6. Verify TLS secret was created
kubectl get secret <secret-name> -n <namespace>
kubectl describe secret <secret-name> -n <namespace>
 
# 7. Check certificate details once issued
kubectl get secret <secret-name> -n <namespace> -o jsonpath='{.data.tls\.crt}' | \
  base64 -d | openssl x509 -noout -dates -subject

Full Working Setup (Copy-Paste)

bash
# Install cert-manager
helm repo add jetstack https://charts.jetstack.io
helm repo update
helm install cert-manager jetstack/cert-manager \
  --namespace cert-manager \
  --create-namespace \
  --version v1.14.0 \
  --set installCRDs=true
yaml
# 1. ClusterIssuer
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-prod
spec:
  acme:
    server: https://acme-v02.api.letsencrypt.org/directory
    email: your-email@example.com
    privateKeySecretRef:
      name: letsencrypt-prod-key
    solvers:
      - http01:
          ingress:
            class: nginx
---
# 2. Ingress with TLS annotation
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: my-app
  namespace: production
  annotations:
    cert-manager.io/cluster-issuer: "letsencrypt-prod"
    nginx.ingress.kubernetes.io/ssl-redirect: "true"
spec:
  ingressClassName: nginx
  tls:
    - hosts:
        - myapp.example.com
      secretName: myapp-tls-secret
  rules:
    - host: myapp.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: my-app
                port:
                  number: 80

cert-manager will automatically detect the annotation and issue a certificate. Check kubectl get certificate -n production after a few minutes.


Learn More

Want hands-on Kubernetes security labs covering cert-manager, Vault, Network Policies, and more? KodeKloud's Kubernetes security courses give you real cluster environments — not just documentation reading.

Newsletter

Stay ahead of the curve

Get the latest DevOps, Kubernetes, AWS, and AI/ML guides delivered straight to your inbox. No spam — just practical engineering content.

Related Articles

Comments