An open-source Kubernetes controller that automates the issuance, renewal, and management of TLS certificates from multiple certificate authorities.

Table of Contents#

  1. Overview
  2. Architecture
  3. Installation
  4. Issuers and ClusterIssuers
  5. HTTP-01 Challenge
  6. DNS-01 Challenge
  7. Certificate Resources
  8. Wildcard Certificates
  9. Renewal Process
  10. Ingress Integration
  11. Troubleshooting
  12. See Also
  13. Sources

1. Overview#

Cert-Manager adds certificate management capabilities to Kubernetes by introducing custom resource definitions (CRDs) for certificates, issuers, and certificate requests. It handles the full lifecycle of TLS certificates: requesting, validating, issuing, and renewing, all without manual intervention.

Supported issuers:

  • Let's Encrypt (ACME protocol, HTTP-01 and DNS-01 challenges)
  • HashiCorp Vault (PKI secrets engine)
  • Venafi (cloud and TPP)
  • Self-signed (for internal/dev use)
  • CA (from a Kubernetes Secret containing a CA key pair)
  • External issuers via webhook solvers

2. Architecture#

ComponentRole
cert-manager-controllerMain controller that watches Certificate, Issuer, and CertificateRequest resources
cert-manager-webhookValidates and mutates cert-manager CRDs via admission webhooks
cert-manager-cainjectorInjects CA bundles into webhook configurations and CRDs

CRD hierarchy:

ClusterIssuer / Issuer
  └── Certificate
        └── CertificateRequest
              └── Order (ACME only)
                    └── Challenge (ACME only)

3. Installation#

3.1 Prerequisites#

  • Kubernetes cluster v1.24+
  • kubectl configured for the target cluster
  • Helm 3 (recommended)

3.2 Install via Helm#

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.16.2 \
  --set crds.enabled=true

3.3 Install via kubectl#

kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.16.2/cert-manager.yaml

3.4 Verify Installation#

kubectl get pods -n cert-manager

All three pods (controller, webhook, cainjector) must be Running.

Test with a self-signed issuer:

kubectl apply -f - <<EOF
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: test-selfsigned
spec:
  selfSigned: {}
---
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: test-cert
  namespace: cert-manager
spec:
  secretName: test-cert-tls
  issuerRef:
    name: test-selfsigned
    kind: ClusterIssuer
  commonName: test.example.com
  dnsNames:
  - test.example.com
EOF

kubectl describe certificate test-cert -n cert-manager
kubectl delete certificate test-cert -n cert-manager
kubectl delete clusterissuer test-selfsigned

4. Issuers and ClusterIssuers#

An Issuer is namespace-scoped; a ClusterIssuer is cluster-wide.

4.1 Let's Encrypt (Staging)#

Use staging for testing to avoid rate limits:

apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-staging
spec:
  acme:
    server: https://acme-staging-v02.api.letsencrypt.org/directory
    email: <your-email>
    privateKeySecretRef:
      name: letsencrypt-staging-key
    solvers:
    - http01:
        ingress:
          class: nginx

4.2 Let's Encrypt (Production)#

apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-prod
spec:
  acme:
    server: https://acme-v02.api.letsencrypt.org/directory
    email: <your-email>
    privateKeySecretRef:
      name: letsencrypt-prod-key
    solvers:
    - http01:
        ingress:
          class: nginx

4.3 CA Issuer (Internal PKI)#

apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: internal-ca
spec:
  ca:
    secretName: ca-key-pair

The Secret ca-key-pair must contain tls.crt and tls.key.

5. HTTP-01 Challenge#

HTTP-01 proves domain ownership by serving a token at http://<domain>/.well-known/acme-challenge/<token>.

Requirements:

  • Port 80 must be publicly accessible
  • An Ingress controller must be running
  • Each domain needs a separate challenge (no wildcards)
solvers:
- http01:
    ingress:
      class: nginx
      serviceType: ClusterIP

Selector to use different solvers per domain:

solvers:
- selector:
    dnsNames:
    - <domain-1>
  http01:
    ingress:
      class: nginx
- selector:
    dnsNames:
    - <domain-2>
  http01:
    ingress:
      class: traefik

6. DNS-01 Challenge#

DNS-01 proves domain ownership by creating a TXT record at _acme-challenge.<domain>. Preferred for production because it supports wildcard certificates and does not require port 80 access.

6.1 Cloudflare#

apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-dns-cloudflare
spec:
  acme:
    server: https://acme-v02.api.letsencrypt.org/directory
    email: <your-email>
    privateKeySecretRef:
      name: letsencrypt-dns-key
    solvers:
    - dns01:
        cloudflare:
          apiTokenSecretRef:
            name: cloudflare-api-token
            key: api-token

Create the token Secret:

apiVersion: v1
kind: Secret
metadata:
  name: cloudflare-api-token
  namespace: cert-manager
type: Opaque
stringData:
  api-token: <cloudflare-api-token>

6.2 Route53 (AWS)#

solvers:
- dns01:
    route53:
      region: <aws-region>
      hostedZoneID: <zone-id>
      accessKeyIDSecretRef:
        name: route53-credentials
        key: access-key-id
      secretAccessKeySecretRef:
        name: route53-credentials
        key: secret-access-key

6.3 Other DNS Providers#

Cert-Manager supports many providers natively (Google Cloud DNS, Azure DNS, DigitalOcean, RFC2136) and external providers via webhook solvers. See the supported DNS01 providers list for details.

6.4 Webhook Solver (Generic)#

For unsupported DNS providers, deploy a webhook solver:

helm install cert-manager-webhook-<provider> <repo>/<chart> \
  --namespace cert-manager

Then reference it in the issuer:

solvers:
- dns01:
    webhook:
      groupName: <webhook-group>
      solverName: <solver-name>
      config:
        apiKey: <key>

7. Certificate Resources#

7.1 Basic Certificate#

apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: <cert-name>
  namespace: <namespace>
spec:
  secretName: <secret-name>
  issuerRef:
    name: letsencrypt-prod
    kind: ClusterIssuer
  commonName: <domain>
  dnsNames:
  - <domain>
  - www.<domain>

7.2 Certificate with Custom Duration#

spec:
  duration: 2160h    # 90 days
  renewBefore: 360h  # Renew 15 days before expiry
  secretName: <secret-name>
  issuerRef:
    name: letsencrypt-prod
    kind: ClusterIssuer
  dnsNames:
  - <domain>

7.3 Certificate with Private Key Options#

spec:
  secretName: <secret-name>
  privateKey:
    algorithm: ECDSA
    size: 256
    rotationPolicy: Always
  issuerRef:
    name: letsencrypt-prod
    kind: ClusterIssuer
  dnsNames:
  - <domain>

8. Wildcard Certificates#

Wildcard certificates require DNS-01 validation (HTTP-01 cannot validate wildcards):

apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: wildcard-example
  namespace: <namespace>
spec:
  secretName: wildcard-example-tls
  issuerRef:
    name: letsencrypt-dns-cloudflare
    kind: ClusterIssuer
  dnsNames:
  - <domain>
  - "*.<domain>"

Note: The wildcard *.<domain> covers one level of subdomain only (e.g., app.<domain> but not sub.app.<domain>). Include the bare domain separately if needed.

9. Renewal Process#

Cert-Manager automatically renews certificates before they expire:

  1. The controller checks certificate expiry based on renewBefore (default: 2/3 of the certificate lifetime)
  2. When renewal is needed, a new CertificateRequest is created
  3. The ACME order and challenge flow runs again
  4. On success, the Secret is updated with the new certificate
  5. Applications using the Secret pick up the new cert (may require pod restart or signal)

Monitor renewal status:

# Check certificate status and expiry
kubectl get certificates -A

# Detailed status including renewal time
kubectl describe certificate <cert-name> -n <namespace>

# Check certificate requests
kubectl get certificaterequest -A

# View ACME orders and challenges
kubectl get orders -A
kubectl get challenges -A

Force a manual renewal:

kubectl cert-manager renew <cert-name> -n <namespace>

Requires the kubectl cert-manager plugin:

kubectl krew install cert-manager

10. Ingress Integration#

10.1 Automatic Certificate via Ingress Annotation#

The simplest approach; cert-manager watches for annotated Ingress resources and creates Certificate resources automatically:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: <ingress-name>
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
  tls:
  - hosts:
    - <domain>
    secretName: <domain>-tls
  rules:
  - host: <domain>
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: <service-name>
            port:
              number: 80

10.2 Using an Existing Certificate#

If you create the Certificate resource manually, reference the same secretName in the Ingress tls block without the annotation.

Troubleshooting#

IssueCauseSolution
Certificate stuck in Not ReadyChallenge failed or order timed outRun kubectl describe certificate <name> and kubectl get challenges -A to see the failure reason
HTTP-01 challenge fails with 404Ingress not routing /.well-known/acme-challenge/ correctlyVerify Ingress class matches the solver config; check Ingress controller logs
DNS-01 challenge fails with NXDOMAINTXT record not created or DNS propagation delayVerify DNS credentials; check cert-manager-controller logs; increase --dns01-recursive-nameservers-only
Webhook validation errors on applyWebhook pod not ready or CA bundle missingWait for cert-manager-webhook pod to be Running; check cainjector logs
certificate has been deniedApproval controller rejected the requestCheck RBAC for CertificateRequest approval; see kubectl describe certificaterequest
Secret not updated after renewalOld Secret still referenced; rotation policy set to NeverSet privateKey.rotationPolicy: Always; restart pods consuming the Secret
Rate limited by Let's EncryptToo many certificates for same domain in a short periodUse staging issuer for testing; check Let's Encrypt rate limits
acme: error code 400Invalid email, domain, or account keyVerify email address and domain ownership; delete the ACME account Secret and re-create
Wildcard cert fails with HTTP-01HTTP-01 does not support wildcardsSwitch to a DNS-01 solver for wildcard domains
Multiple solvers conflictSolver selector not matching the correct domainAdd explicit selector.dnsNames to each solver to target specific domains

See Also#

Sources#