If your TLS certificate is stuck, expired, or never issued, the fix is hiding inside one Kubernetes resource. Such as the Certificate, the CertificateRequest, the Order, or the Challenge. This guide provides cert-manager troubleshooting for ACME challenge problems step by step, using the actual resource states and events.
If you have not set up cert-manager yet, you can follow our cert-manager setup guide for K3s.
How cert-manager Issues a Certificate
Before you fix anything, you need to know the chain of objects cert-manager creates. This is the map you will use for the rest of this cert-manager troubleshooting guide.
- You create a
Certificate resource, or an Ingress annotation creates one for you.
- cert-manager creates a
CertificateRequest from that Certificate.
- For ACME issuers like Let's Encrypt, the CertificateRequest creates an
Order.
- The Order creates one
Challenge per domain name, HTTP-01 or DNS-01.
- When the Challenge passes, the Order finalizes, and a signed certificate is saved into a Kubernetes
Secret.
If a certificate is stuck, you can follow this chain top to bottom with kubectl describe until you find the object with the error.
Check the Certificate Object First
You must start every diagnosis session at the top of the resource chain, with the Certificate object itself. For this purpose, run:
kubectl get certificate -A
In the output, look at the READY column. If it says False, describe it to see the conditions and events:
kubectl describe certificate <cert-name> -n <namespace>
Common conditions you will see include:
Issuing=True: A new certificate is being requested right now.
Ready=False, Reason=DoesNotExist: No Secret exists yet; check the CertificateRequest.
Ready=False, Reason=Expired: The old certificate expired, and renewal failed; check the Order and Challenge.
Also, you can use cmctl for a full status chain in one command:
cmctl status certificate <cert-name> -n <namespace>
This prints the Certificate, its CertificateRequest, Order, and Challenge together. It saves you from running four separate kubectl describe commands.
CertificateRequest Stuck or Denied
If the Certificate is not ready, you must check the CertificateRequest it created. To do this, run:
kubectl get certificaterequest -n <namespace>kubectl describe certificaterequest <name> -n <namespace>
Look at the conditions field for these common problems:
Denied=True: An approver, like an Approval policy, rejected the request. Check your RBAC and approval policies.
InvalidRequest=True: The certificate signer refused the request because of a bad private key type or an unsupported field. Read the message text in the event.
- No CertificateRequest at all: This means the Issuer or ClusterIssuer referenced in the Certificate spec does not exist or has a typo in its name.
If you deleted a CertificateRequest by mistake, cert-manager will create a new one, because the Certificate resource is the source of truth.
Order Resource Failures
An Order is only created when you use an ACME issuer, like Let's Encrypt. Most DNS and network problems show up right here, so this is a key step in cert-manager troubleshooting ACME challenge work. Run the commands below:
kubectl get order -n <namespace>kubectl describe order <order-name> -n <namespace>
Watch the State field:
pending: Waiting on Challenges to complete.
errored: The ACME server rejected the order due to a bad domain name or account problem.
invalid: One or more Challenges failed permanently.
If the state is stuck on pending for more than a few minutes, move to the Challenge resource in the next step.
Challenge Resource Failures: HTTP-01 and DNS-01
This is the most important step in cert-manager troubleshooting. Almost every "certificate never issued" problem comes back to this point.
kubectl get challenge -n <namespace>kubectl describe challenge <challenge-name> -n <namespace>
The Reason field in the events tells you exactly what Let's Encrypt saw when it tried to validate your domain.
HTTP-01 Reachability Checks
For HTTP-01, Let's Encrypt makes an HTTP request to http://<your-domain>/.well-known/acme-challenge/<token> on port 80. If this fails, check:
curl -v http://<your-domain>/.well-known/acme-challenge/<token> dig +short <your-domain> kubectl get pods -n <namespace> | grep acme
Common causes include a firewall blocking port 80, a CDN like Cloudflare proxying traffic before the challenge pod can respond, or an Ingress controller that is not routing the /.well-known/acme-challenge/ path correctly.
If you use Cloudflare, temporarily set the DNS record to "DNS only" (grey cloud) while the challenge runs, or switch to DNS-01 instead.
DNS-01 Propagation Checks
For DNS-01, cert-manager creates a TXT record and waits for it to be visible everywhere before telling Let's Encrypt to check it.
dig +short TXT _acme-challenge.<your-domain> @8.8.8.8dig +short TXT _acme-challenge.<your-domain> @1.1.1.1 kubectl logs -n cert-manager deploy/cert-manager -f | grep -i dns
If the TXT record does not appear, you must check two things. Your DNS API token needs edit access, not just read, and the domain in your Issuer must match your real DNS zone. cert-manager retries for a few minutes on its own. If it still fails after 10 minutes, the token is usually the problem.
Webhook Connectivity Errors
Some cert-manager problems have nothing to do with ACME. Instead, they come from the internal webhook, which is a component that checks cert-manager's own resources.
You will see an error like Internal error occurred: failed calling webhook "webhook.cert-manager.io" or context deadline exceeded.
First, confirm the webhook Pod is running with the command below:
kubectl get pods -n cert-manager -l app=webhook
Then run the command below to test if the API server can actually reach it:
kubectl -n cert-manager port-forward deploy/cert-manager-webhook 10250curl -k https://localhost:10250/healthz
If you get x509: certificate signed by unknown authority, the problem is the caBundle in the webhook configuration. You must check that cainjector is running and has no errors, then confirm the CA bundle matches:
kubectl get validatingwebhookconfigurations cert-manager-webhook -o yaml | grep caBundlekubectl -n cert-manager get secret cert-manager-webhook-ca -o yaml
A timeout error, context deadline exceeded, means a network problem. This happens in clusters with strict network policies or in private GKE clusters that block traffic from the API server to Pods on certain ports. As a temporary fix, raise webhook.timeoutSeconds to 30 in your Helm values while you sort out the network issue.
Rate Limit Errors from Let's Encrypt
If your Order or Challenge event shows too many certificates already issued or too many new orders recently, you have hit a Let's Encrypt rate limit. As of the latest published limits:
- New orders per account: 300 every 3 hours.
- Certificates per registered domain: 50 every 7 days.
- Certificates for the same set of hostnames: 5 every 7 days.
- Authorization failures per identifier per account: 5 per hour.
Canceling a certificate does not free up your limit; it's already used. Two easy fixes include using Let's Encrypt's staging URL (https://acme-staging-v02.api.letsencrypt.org/directory) while testing, then switch to production once it works. Also, avoid deleting and recreating the same Certificate again and again, since each try uses up your weekly quota.
Wrong or Misconfigured Issuer
Many cert-manager troubleshooting tickets turn out to be a simple mismatch between the Certificate and its Issuer. Run the commands below:
kubectl get issuer -n <namespace>kubectl get clusterissuerkubectl describe clusterissuer <name>
Check three things, including the kind field in the Certificate spec matches the Issuer or ClusterIssuer correctly, the name is spelled right, and the Issuer itself shows Ready=True in its own status.
An Issuer stuck at Ready=False means a bad ACME account registration or an unreachable DNS provider API.
Secret Ownership and Failed Renewals
Sometimes the Certificate and Challenge look healthy, but the Secret never updates, or a renewal silently fails. Run the following command:
kubectl get secret <secret-name> -n <namespace> -o yaml | grep -A5 annotations
Look for the cert-manager.io/certificate-name label on the Secret. If it's missing or wrong, another tool or a manual kubectl apply overwrote it. By default, cert-manager won't touch a Secret it doesn't own.
To fix this, delete the bad Secret and let cert-manager make a new one, or set secretTemplate ownership in the Certificate spec.
To force an immediate renewal instead of waiting for the scheduled time, you can run:
cmctl renew <cert-name> -n <namespace>
If renewal keeps failing after that, delete the CertificateRequest to force a clean retry:
kubectl delete certificaterequest <name> -n <namespace>
Useful cmctl Diagnostic Commands
If you have not already installed cmctl, you can use the commands below to install the latest cmctl binary:
CMCTL_VERSION=$(curl -s https://api.github.com/repos/cert-manager/cmctl/releases/latest | grep '"tag_name"' | cut -d'"' -f4)ARCH=$(dpkg --print-architecture)curl -fsSL "https://github.com/cert-manager/cmctl/releases/download/${CMCTL_VERSION}/cmctl_linux_${ARCH}" -o /tmp/cmctlsudo install -o root -g root -m 0755 /tmp/cmctl /usr/local/bin/cmctlcmctl version
Here are some useful cmctl diagnostic commands that you can use for cert-manager troubleshooting:
cmctl check api cmctl status certificate <name> -n <ns> cmctl renew <name> -n <ns> cmctl x509 view-secret <secret-name> -n <ns>
Upgrading to the Latest Stable cert-manager
If you suspect your cert-manager version itself is the problem, you can upgrade to the current stable release, which is currently v1.21.1:
helm repo add jetstack https://charts.jetstack.io --force-update helm upgrade --install \ cert-manager oci://quay.io/jetstack/charts/cert-manager \ --version v1.21.1 \ --namespace cert-manager \ --create-namespace \ --set crds.enabled=true
After upgrading, confirm every Pod is healthy before testing new certificates:
kubectl get pods -n cert-managercmctl check api
Conclusion
Certificate problems aren't random. Each one sits at a clear point in the Certificate → CertificateRequest → Order → Challenge chain, and the events tell you what went wrong. Once you're comfortable with reading kubectl describe and cmctl status, cert-manager troubleshooting takes minutes. Keep DNS credentials updated, watch your Let's Encrypt limits, and check the webhook after any network change.
Also, unreliable infrastructure causes ACME failures that look like cert-manager bugs but are really connectivity problems. Keep your certificate automation on a stable Kubernetes node with reliable DNS and public connectivity, so HTTP-01 and DNS-01 checks pass on the first try.
For more detailed information on reading Certificate, CertificateRequest, and Issuer events, check the official cert-manager docs.