QCecuring - Enterprise Security Solutions

cert-manager Complete Setup Guide: Automated TLS Certificates in Kubernetes

Kubernetes 11 May, 2026 · 07 Mins read

Install and configure cert-manager for automated TLS certificate management in Kubernetes. Covers Issuers, ClusterIssuers, Let's Encrypt, Vault PKI, DNS-01 challenges, wildcard certs, and production troubleshooting.


Every Kubernetes service that exposes HTTPS needs a TLS certificate. Manually creating secrets, tracking expiry dates, and rotating certificates across dozens of namespaces doesn’t scale. cert-manager turns certificate management into a declarative Kubernetes-native operation — you define what you want, and it handles issuance, renewal, and rotation automatically.

This guide takes you from zero to production-ready cert-manager, covering installation, issuer configuration, certificate resources, DNS challenges for wildcards, and the troubleshooting steps you’ll need when certificates get stuck in a “not ready” state.


How cert-manager Works

cert-manager runs as a set of controllers in your cluster that watch for Certificate resources and fulfill them by communicating with certificate authorities:

Flowchart showing top-down process flow

Core concepts:

ResourceScopePurpose
IssuerNamespaceDefines how to obtain certificates (CA config, credentials)
ClusterIssuerCluster-wideSame as Issuer but available to all namespaces
CertificateNamespaceDeclares a desired certificate (domain, issuer, secret name)
CertificateRequestNamespaceInternal — represents a single issuance attempt
OrderNamespaceInternal — tracks ACME order lifecycle
ChallengeNamespaceInternal — tracks ACME challenge solving

Installation

# Add the Jetstack Helm repository
helm repo add jetstack https://charts.jetstack.io
helm repo update

# Install cert-manager with CRDs
helm install cert-manager jetstack/cert-manager \
  --namespace cert-manager \
  --create-namespace \
  --version v1.17.1 \
  --set crds.enabled=true \
  --set prometheus.enabled=true \
  --set webhook.timeoutSeconds=30

# Verify installation
kubectl get pods -n cert-manager

Expected output:

NAME                                       READY   STATUS    RESTARTS   AGE
cert-manager-7f4d5b8c9b-x2k4l            1/1     Running   0          45s
cert-manager-cainjector-6c8b4d9f7-m8n2p   1/1     Running   0          45s
cert-manager-webhook-5d9b7c6f4-q9r3t      1/1     Running   0          45s

Method 2: kubectl apply (Quick Start)

# Install cert-manager with all CRDs in one command
kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.17.1/cert-manager.yaml

# Wait for pods to be ready
kubectl wait --for=condition=Ready pods --all -n cert-manager --timeout=120s

Verify Installation

# Check CRDs are installed
kubectl get crd | grep cert-manager

# Expected:
# certificaterequests.cert-manager.io
# certificates.cert-manager.io
# challenges.acme.cert-manager.io
# clusterissuers.cert-manager.io
# issuers.cert-manager.io
# orders.acme.cert-manager.io

# Test with cmctl (cert-manager CLI)
# Install: https://cert-manager.io/docs/reference/cmctl/
cmctl check api

Configuring Issuers

Let’s Encrypt (ACME) — Production + Staging

Always test with staging first. Let’s Encrypt has strict rate limits on production.

# cluster-issuer-letsencrypt.yaml
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-staging
spec:
  acme:
    server: https://acme-staging-v02.api.letsencrypt.org/directory
    email: platform-team@example.com
    privateKeySecretRef:
      name: letsencrypt-staging-account-key
    solvers:
      - http01:
          ingress:
            ingressClassName: nginx
---
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-prod
spec:
  acme:
    server: https://acme-v02.api.letsencrypt.org/directory
    email: platform-team@example.com
    privateKeySecretRef:
      name: letsencrypt-prod-account-key
    solvers:
      - http01:
          ingress:
            ingressClassName: nginx
kubectl apply -f cluster-issuer-letsencrypt.yaml

# Verify issuer is ready
kubectl get clusterissuer
# NAME                  READY   AGE
# letsencrypt-staging   True    10s
# letsencrypt-prod      True    10s

DNS-01 Challenge (For Wildcards and Private Networks)

HTTP-01 requires your cluster to be publicly reachable. DNS-01 works for:

  • Wildcard certificates (*.example.com)
  • Internal services not exposed to the internet
  • Environments behind firewalls

Cloudflare DNS example:

# Create API token secret
apiVersion: v1
kind: Secret
metadata:
  name: cloudflare-api-token
  namespace: cert-manager
type: Opaque
stringData:
  api-token: "your-cloudflare-api-token-here"
---
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-dns
spec:
  acme:
    server: https://acme-v02.api.letsencrypt.org/directory
    email: platform-team@example.com
    privateKeySecretRef:
      name: letsencrypt-dns-account-key
    solvers:
      - dns01:
          cloudflare:
            apiTokenSecretRef:
              name: cloudflare-api-token
              key: api-token
        selector:
          dnsZones:
            - "example.com"

AWS Route53 DNS example:

apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-route53
spec:
  acme:
    server: https://acme-v02.api.letsencrypt.org/directory
    email: platform-team@example.com
    privateKeySecretRef:
      name: letsencrypt-route53-account-key
    solvers:
      - dns01:
          route53:
            region: us-east-1
            # Uses IRSA (IAM Roles for Service Accounts) or instance profile
            # For IRSA, annotate the cert-manager service account:
            # eks.amazonaws.com/role-arn: arn:aws:iam::123456789:role/cert-manager-route53

HashiCorp Vault PKI Issuer

For internal certificates signed by your private CA in Vault:

apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: vault-pki
spec:
  vault:
    server: https://vault.internal.example.com
    path: pki_int/sign/server-cert
    caBundle: <base64-encoded-vault-ca-cert>
    auth:
      kubernetes:
        role: cert-manager
        mountPath: /v1/auth/kubernetes
        serviceAccountRef:
          name: cert-manager

Self-Signed and CA Issuers (Dev/Testing)

# Self-signed issuer (for bootstrapping)
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: selfsigned
spec:
  selfSigned: {}
---
# Create a CA certificate using the self-signed issuer
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: internal-ca
  namespace: cert-manager
spec:
  isCA: true
  commonName: "Internal Dev CA"
  duration: 87600h  # 10 years
  secretName: internal-ca-key-pair
  issuerRef:
    name: selfsigned
    kind: ClusterIssuer
---
# CA issuer that signs certificates with the above CA
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: internal-ca
spec:
  ca:
    secretName: internal-ca-key-pair

Requesting Certificates

Method 1: Certificate Resource (Explicit)

apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: api-tls
  namespace: production
spec:
  secretName: api-tls-secret
  issuerRef:
    name: letsencrypt-prod
    kind: ClusterIssuer
  duration: 2160h    # 90 days
  renewBefore: 720h  # Renew 30 days before expiry
  commonName: api.example.com
  dnsNames:
    - api.example.com
    - api-v2.example.com
  privateKey:
    algorithm: ECDSA
    size: 256
    rotationPolicy: Always

Method 2: Ingress Annotation (Automatic)

The simplest approach — annotate your Ingress and cert-manager creates the Certificate automatically:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: api-ingress
  namespace: production
  annotations:
    cert-manager.io/cluster-issuer: "letsencrypt-prod"
    cert-manager.io/private-key-algorithm: "ECDSA"
    cert-manager.io/private-key-size: "256"
spec:
  ingressClassName: nginx
  tls:
    - hosts:
        - api.example.com
      secretName: api-tls-secret  # cert-manager creates this
  rules:
    - host: api.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: api-service
                port:
                  number: 8080

Method 3: Gateway API (Modern)

apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: api-gateway
  namespace: production
  annotations:
    cert-manager.io/cluster-issuer: "letsencrypt-prod"
spec:
  gatewayClassName: nginx
  listeners:
    - name: https
      protocol: HTTPS
      port: 443
      hostname: "api.example.com"
      tls:
        mode: Terminate
        certificateRefs:
          - name: api-tls-secret  # cert-manager creates this

Wildcard Certificate

apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: wildcard-tls
  namespace: production
spec:
  secretName: wildcard-tls-secret
  issuerRef:
    name: letsencrypt-dns  # Must use DNS-01 for wildcards
    kind: ClusterIssuer
  dnsNames:
    - "example.com"
    - "*.example.com"
  privateKey:
    algorithm: ECDSA
    size: 256

Certificate Lifecycle and Renewal

cert-manager automatically renews certificates before they expire. The renewal timeline:

Flowchart showing left-to-right process flow

Default behavior:

  • duration: 90 days (2160h) — matches Let’s Encrypt
  • renewBefore: 30 days (720h) — triggers renewal 30 days before expiry
  • rotationPolicy: Always — generates a new private key on each renewal (recommended)

Monitoring Renewal

# Check certificate status
kubectl get certificates -A

# Detailed status including renewal time
kubectl describe certificate api-tls -n production

# Look for:
#   Status:
#     Conditions:
#       Type: Ready
#       Status: True
#     Not After: 2026-08-09T12:00:00Z
#     Renewal Time: 2026-07-10T12:00:00Z

Troubleshooting

Certificate Stuck in “Not Ready”

The most common issue. Follow the resource chain to find where it’s stuck:

# Step 1: Check Certificate status
kubectl describe certificate api-tls -n production
# Look at Events and Conditions

# Step 2: Check CertificateRequest
kubectl get certificaterequest -n production
kubectl describe certificaterequest api-tls-xxxxx -n production

# Step 3: For ACME issuers, check Order
kubectl get orders -n production
kubectl describe order api-tls-xxxxx -n production

# Step 4: Check Challenge (if Order is pending)
kubectl get challenges -n production
kubectl describe challenge api-tls-xxxxx -n production

Flowchart showing top-down process flow

HTTP-01 Challenge Failing

# Check if the solver pod is running
kubectl get pods -n cert-manager -l acme.cert-manager.io/http01-solver=true

# Check if the temporary ingress was created
kubectl get ingress -A | grep cm-acme

# Test the challenge URL manually (from outside the cluster)
curl -v http://api.example.com/.well-known/acme-challenge/test-token

# Common fixes:
# 1. Wrong ingressClassName in the solver config
# 2. Firewall blocking port 80 from Let's Encrypt
# 3. DNS not pointing to the cluster's ingress IP
# 4. Ingress controller not routing /.well-known paths

DNS-01 Challenge Failing

# Check cert-manager controller logs
kubectl logs -n cert-manager deployment/cert-manager -f | grep -i "dns\|challenge\|error"

# Common errors:
# "failed to determine Route53 hosted zone ID" → IAM permissions
# "Timeout" → DNS propagation delay
# "Forbidden" → API token lacks zone edit permissions

# Verify DNS propagation manually
dig -t TXT _acme-challenge.api.example.com @8.8.8.8

Issuer Not Ready

# Check issuer status
kubectl get clusterissuer
kubectl describe clusterissuer letsencrypt-prod

# Common causes:
# - ACME account registration failed (email invalid, server unreachable)
# - Vault connection refused (wrong URL, auth misconfigured)
# - Secret referenced in issuer doesn't exist

# Check cert-manager logs for issuer errors
kubectl logs -n cert-manager deployment/cert-manager | grep -i "issuer\|error\|failed"

Secret Not Updating After Renewal

# Check if the secret exists and has recent data
kubectl get secret api-tls-secret -n production -o jsonpath='{.metadata.annotations}'

# Force re-issuance by deleting the secret (cert-manager recreates it)
kubectl delete secret api-tls-secret -n production

# Or trigger renewal with cmctl
cmctl renew api-tls -n production

Rate Limit Errors (Let’s Encrypt)

# Check order status for rate limit messages
kubectl describe order -n production | grep -i "rate\|limit\|too many"

# Let's Encrypt rate limits (as of 2026):
# - 50 certificates per registered domain per week
# - 5 duplicate certificates per week
# - 300 new orders per account per 3 hours

# Fix: Use staging issuer for testing, consolidate domains into fewer certs

Production Configuration

Resource Limits

# values.yaml for Helm
resources:
  requests:
    cpu: 50m
    memory: 128Mi
  limits:
    cpu: 500m
    memory: 512Mi

# For large clusters (1000+ certificates)
replicaCount: 2
podDisruptionBudget:
  enabled: true
  minAvailable: 1

Prometheus Monitoring

# cert-manager exposes metrics on :9402/metrics
# Key metrics to alert on:

# Certificate expiry (alert if < 7 days and not renewing)
# certmanager_certificate_expiration_timestamp_seconds

# Failed issuance attempts
# certmanager_certificate_ready_status{condition="False"}

# ACME client errors
# certmanager_http_acme_client_request_count{status="error"}

Prometheus alert rules:

apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
  name: cert-manager-alerts
  namespace: monitoring
spec:
  groups:
    - name: cert-manager
      rules:
        - alert: CertificateNotReady
          expr: certmanager_certificate_ready_status{condition="False"} == 1
          for: 30m
          labels:
            severity: warning
          annotations:
            summary: "Certificate {{ $labels.name }} in {{ $labels.namespace }} is not ready"

        - alert: CertificateExpiringSoon
          expr: (certmanager_certificate_expiration_timestamp_seconds - time()) < 604800
          for: 1h
          labels:
            severity: critical
          annotations:
            summary: "Certificate {{ $labels.name }} expires in less than 7 days"

Multi-Cluster Considerations

For organizations running cert-manager across many clusters:

ChallengeSolution
Inconsistent issuer configGitOps (ArgoCD/Flux) to sync ClusterIssuers across clusters
No central visibilityExport cert-manager metrics to central Prometheus/Grafana
Rate limits hit across clustersUse DNS-01 with separate accounts per cluster, or a private CA
Secret sprawlUse external-secrets-operator to sync certs to a central vault
Wildcard sharingIssue wildcard in one cluster, replicate secret to others

When cert-manager handles issuance per-cluster but you need a unified view of certificate health across 10, 50, or 100 clusters, a centralized CLM platform provides the cross-cluster visibility that cert-manager alone can’t.


cert-manager vs Alternatives

Featurecert-managerAWS ACMGoogle Managed CertsTraefik Built-in
Multi-CA supportYes (ACME, Vault, private CA, Venafi)AWS onlyGoogle onlyACME only
Wildcard certsYes (DNS-01)YesNoYes (DNS-01)
Private CAYesACM Private CA ($$$)Certificate Authority ServiceNo
Multi-clusterPer-cluster installPer-regionPer-projectPer-instance
Custom renewalConfigurableFixed (60 days)FixedFixed
mTLS supportYes (with trust-manager)LimitedNoLimited
GitOps friendlyYes (CRDs)Terraform/CDKTerraformConfig file
CostFree (OSS)Free (public) / $400/mo (private)Free (public)Free

FAQ

Q: What’s the difference between an Issuer and a ClusterIssuer?

An Issuer is namespace-scoped — it can only issue certificates within its own namespace. A ClusterIssuer is cluster-scoped and can issue certificates in any namespace. Use ClusterIssuers for shared CAs (Let’s Encrypt) and namespace Issuers for team-specific private CAs or when you need different credentials per namespace.

Q: How do I use cert-manager with Istio service mesh?

cert-manager integrates with Istio via the istio-csr agent. Instead of Istio’s built-in CA (istiod), istio-csr requests certificates from cert-manager for workload identity. This lets you use any cert-manager issuer (Vault, private CA) for mesh mTLS certificates.

helm install istio-csr jetstack/cert-manager-istio-csr \
  --namespace cert-manager \
  --set "app.certmanager.issuer.name=vault-pki"

Q: Can cert-manager handle certificate rotation without downtime?

Yes. When cert-manager renews a certificate, it updates the Kubernetes Secret in-place. Ingress controllers (Nginx, Traefik, Envoy) watch for Secret changes and reload the new certificate without dropping connections. Set rotationPolicy: Always to generate a new private key on each renewal for forward secrecy.

Q: How do I migrate existing certificates to cert-manager?

  1. Create the Certificate resource pointing to the same secretName as your existing TLS secret
  2. cert-manager will detect the existing secret and adopt it
  3. It will renew the certificate when renewBefore is reached
  4. Alternatively, delete the old secret and let cert-manager issue a fresh one

Q: Why is my certificate stuck in “Issuing” state?

Check the CertificateRequest and Order resources. Common causes: ACME challenge failing (firewall, DNS), issuer credentials expired, rate limits hit, or the webhook is rejecting the request. Run kubectl describe on each resource in the chain (Certificate → CertificateRequest → Order → Challenge) to find the specific error.

Q: How many certificates can cert-manager handle?

cert-manager has been tested with 10,000+ Certificate resources in a single cluster. Performance depends on the issuer — Let’s Encrypt rate limits are the bottleneck, not cert-manager itself. For private CAs (Vault, internal CA), throughput is limited by the CA’s signing capacity.


Related Reading:

Enterprise Certificate Management for K8s

When cert-manager handles issuance but you need visibility across 50 clusters — centralized CLM fills the gap.

Request Demo

Related Insights

SSL/TLS

Apache SSL/TLS Configuration Guide: Complete Setup & Hardening

Configure Apache HTTPD with SSL/TLS from scratch — mod_ssl setup, VirtualHost HTTPS, cipher hardening, HSTS, OCSP stapling, Let's Encrypt with Certbot, SNI multi-site hosting, and mTLS client authentication. Working configs for Ubuntu/Debian and RHEL/CentOS.

By Sneha gupta

15 May, 2026 · 06 Mins read

SSL/TLSPractical GuidesDevOps

DevOps

Certificate Expiry Monitoring with Prometheus & Grafana: Complete Setup

Set up certificate expiry monitoring using Prometheus exporters (x509-certificate-exporter, Blackbox exporter, cert-manager metrics), PromQL alerting rules, Grafana dashboards, and AlertManager notifications for Slack and PagerDuty.

By Sneha gupta

15 May, 2026 · 05 Mins read

DevOpsPractical GuidesSSL/TLS

SSL/TLS

Fix 'Keystore Was Tampered With, or Password Was Incorrect' in Java

Fix the Java keystore error caused by wrong password, JKS/PKCS12 type mismatch, or corrupted keystore file. Includes recovery steps and keytool commands.

By Shivam sharma

15 May, 2026 · 03 Mins read

SSL/TLSTroubleshootingDevOps

Ready to Secure Your Enterprise?

Experience how our cryptographic solutions simplify, centralize, and automate identity management for your entire organization.

Stay ahead on cryptography & PKI

Get monthly insights on certificate management, post-quantum readiness, and enterprise security. No spam.

We respect your privacy. Unsubscribe anytime.