Let's Encrypt with cert-manager: HTTP-01 vs DNS-01
Objective
Let's Encrypt issues publicly trusted certificates for free, over the ACME protocol, after you prove you control the domain. cert-manager automates that proof with one of two challenge types: HTTP-01 (serve a token on http://<domain>/.well-known/acme-challenge/...) or DNS-01 (publish a token in a TXT record). They have different requirements, and picking the wrong one is the main reason certificates stay READY False. This concept covers an ACME ClusterIssuer, both challenges, staging vs production, rate limits, and the shorter certificate lifetimes Let's Encrypt is rolling out.
Use Cases
- Public HTTPS for an API and a login page on a node with a public IP.
- Certificates for a cluster that is not reachable from the internet (office network, VPN only).
- Wildcard certificates (
*.example.com). - Understanding a challenge stuck in
pending.
Deep Dive
An ACME ClusterIssuer
plaintextapiVersion: cert-manager.io/v1 kind: ClusterIssuer metadata: {name: letsencrypt-staging} spec: acme: server: https://acme-staging-v02.api.letsencrypt.org/directory email: [email protected] privateKeySecretRef: {name: letsencrypt-staging-account} solvers: - http01: ingress: {ingressClassName: public}
Start with staging. Its certificates are not trusted by browsers, but its rate limits are generous, so a misconfiguration does not lock you out. Switch server to https://acme-v02.api.letsencrypt.org/directory (and use a separate issuer name and account Secret) once staging certificates are issued successfully.
Then annotate an Ingress exactly as with any issuer: cert-manager.io/cluster-issuer: letsencrypt-prod.
HTTP-01
cert-manager creates a temporary Ingress (or HTTPRoute) and solver Pod that serves the token. Let's Encrypt's validation servers, from the public internet, fetch:
plaintexthttp://api.example.com/.well-known/acme-challenge/<token>
Requirements:
- Public DNS
A/AAAAfor the hostname pointing to the node or its load balancer. - Port 80 reachable from the internet (validation always starts on port 80; redirects to 443 are followed).
- The solver's Ingress must be served by the controller you name in
ingressClassName.
Limits: no wildcards, one validation per hostname, and it simply cannot work for a cluster that the internet cannot reach. If a firewall allows only 443, HTTP-01 fails.
DNS-01
cert-manager creates a TXT record _acme-challenge.example.com through your DNS provider's API, Let's Encrypt queries public DNS, cert-manager removes the record afterwards.
plaintextsolvers: - dns01: cloudflare: apiTokenSecretRef: name: cloudflare-api-token # in the cert-manager namespace for a ClusterIssuer key: api-token selector: dnsZones: [example.com]
cert-manager supports Cloudflare, Route 53, Google Cloud DNS, Azure DNS, DigitalOcean, RFC 2136 (BIND and friends) and more through webhooks.
Advantages:
- Works for clusters with no inbound internet access at all, because only DNS is involved. This is the usual answer for internal services that still want publicly trusted certificates (the hostname must exist in a public zone, but it can resolve to a private IP).
- Wildcards (
*.example.com) are only possible with DNS-01.
Costs:
- An API token with permission to edit DNS lives in the cluster. Scope it to the one zone and, where the provider allows, to
TXTrecords. - Propagation delay: cert-manager checks that the record is visible before asking for validation. Split-horizon DNS (an internal server answering for the same zone) can make that self-check fail; point cert-manager's checks at public resolvers with
--dns01-recursive-nameservers=1.1.1.1:53,8.8.8.8:53and--dns01-recursive-nameservers-only.
Choosing
| Situation | Challenge |
|---|---|
| Public node, port 80 open, individual hostnames | HTTP-01 |
| Internal or VPN-only cluster | DNS-01 |
| Wildcard certificate | DNS-01 |
| DNS provider without API | HTTP-01, or a CNAME delegation of _acme-challenge to a zone you can automate |
One issuer can hold several solvers with selectors (for example HTTP-01 by default and DNS-01 for dnsZones: [internal.example.com]).
Debugging a pending certificate
The chain of objects is Certificate -> CertificateRequest -> Order -> Challenge. The Challenge has the useful message:
plaintextkubectl get certificate,certificaterequest,order,challenge -A kubectl describe challenge <name>
Typical reasons: connection refused or timeout on port 80 (HTTP-01 not reachable), 404 (the solver Ingress is not served by the controller, wrong class), DNS record not yet propagated (DNS-01 self-check), rateLimited (see below).
Rate limits and lifetimes
Production Let's Encrypt limits certificates per registered domain per week and duplicate certificates, among others. Debugging against production is the fastest way to hit them; that is what staging is for.
Lifetimes are getting shorter, which makes automation mandatory rather than convenient:
- Since 13 May 2026, the opt-in
tlsserverACME profile issues 45-day certificates. - From 10 February 2027 the default
classicprofile moves to 64 days, and from 16 February 2028 to 45 days.
cert-manager renews at two thirds of the lifetime by default, so it adapts on its own; what must not exist is any manual step or monitoring that assumes 90 days.
Trade-offs
- HTTP-01 vs DNS-01. HTTP-01 needs no credentials in the cluster but requires inbound port 80 and public reachability. DNS-01 works anywhere and supports wildcards, but stores a DNS-editing credential in the cluster.
- Wildcard vs per-host certificates. One wildcard simplifies configuration; its key is shared by every service using it, so a leak affects all of them.
- Public certificates vs a private CA. Public certificates need no trust distribution; a private CA works without any external dependency or rate limit.