Cert Manager

Draft

Table of Contents

Deploy

...

Understanding

Cert-manager’s certificates controller watches this CRD. When it sees a new Certificate, it begins the issuing workflow. Internally, cert-manager converts your Certificate into a more specific CertificateRequest object, representing a single signing operation. This object asks the issuer (Let’s Encrypt, via cert-manager’s ACME client) to sign a CSR.

Then cert-manager’s ACME client (in the controller) contacts Let’s Encrypt. It connects to the ACME server defined in your ClusterIssuer.

server: https://acme-v02.api.letsencrypt.org/directory

Cert-manager models ACME challenges as Kubernetes resources:

  • Order — represents Let’s Encrypt’s pending order for one or more domain names.
  • Challenge — represents the actual proof task. For HTTP-01 method, place a file at a URL.

The Challenge controller inside cert-manager now deploys a temporary HTTP solver stack:

  • Pod — a minimal web server serving the challenge token from the .well-known/acme-challenge/ path.
  • Service — exposing that pod on port 8089 (ClusterIP or NodePort).
  • Ingress — routing http://<your-domain>/.well-known/acme-challenge/<token> to that Service.

After the Ingress and pod are ready, cert-manager performs a self-check:

GET http://domain.com/.well-known/acme-challenge/<token>

If it gets 200 OK and the correct token content, it marks the challenge ready. Let’s Encrypt then performs the same HTTP GET request from their own infrastructure. If they see the same 200 OK response, they mark the challenge as valid.

Once Let’s Encrypt reports the challenge valid, cert-manager automatically finalizes the ACME order:

  • Sends the CSR from the CertificateRequest.
  • Receives the signed certificate chain.
  • Writes the resulting PEM files into the Kubernetes Secret you defined.

Once the certificate is issued, cert-manager automatically deletes:

  • The Challenge resource
  • The temporary solver Ingress, Service, and Pod
  • Leaving only the Certificate, CertificateRequest, Order, and the resulting TLS Secret.

DNS-01

...

HTTP-01

This method is good when you do not own a domain.

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

Certificate:

apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: foreign-domain-cert
  namespace: app-dev # The same namespace where your application ingress declaration was created.
spec:
  secretName: foreign-domain-tls
  duration: 2160h
  renewBefore: 360h
  dnsNames:
  - example.com
  - '*.example.com'
  issuerRef:
    name: letsencrypt-http
    kind: ClusterIssuer

Debugging HTTP-01

kubectl get challenge -A
kubectl get certificate -A
kubectl get ingressclass
kubectl describe certificate <CERTIFICATE_NAME> -n <NAMESPACE>

Verify DNS resolution:

host domain.com

Search for cm-acme-http-solver-XXX solver ingress and services:

kubectl -n <NAMESPACE> get ingress
kubectl -n <NAMESPACE> get svc
kubectl -n <NAMESPACE> get ingress cm-acme-http-solver-XXX -o yaml
kubectl -n <NAMESPACE> get endpoints cm-acme-http-solver-XXX -o yaml
kubectl -n <NAMESPACE> get pods -l acme.cert-manager.io/http01-solver=true -o wide

Check cert manager logs:

kubectl -n cert-manager logs -f <CERT_MANAGER_POD>

October 15, 2025