Skip to content
Codeloom
Kubernetes

Kubernetes cert-manager: Automate TLS Certificates

Set up cert-manager in Kubernetes to automate TLS certificate provisioning with Let's Encrypt. Covers Issuers, Certificates, Ingress integration.

·7 min read · By Codeloom
Intermediate 12 min read

What you'll learn

  • Install and configure cert-manager in your cluster
  • Create Issuers and ClusterIssuers for Let's Encrypt
  • Automate TLS certificates for Ingress resources
  • Troubleshoot common certificate issues

Prerequisites

  • A running Kubernetes cluster
  • kubectl configured and working
  • Basic Ingress knowledge — see /blog/kubernetes-ingress-basics

TLS certificates are a requirement for any production web service. Managing them manually — generating, deploying, renewing, and rotating — is tedious and error-prone. cert-manager automates the entire lifecycle. It provisions certificates from authorities like Let’s Encrypt, stores them as Kubernetes Secrets, and renews them before they expire.

Once set up, you add a single annotation to your Ingress and cert-manager handles the rest.

How cert-manager Works

cert-manager runs as a set of controllers in your cluster. The workflow is:

  1. You create a Certificate resource (or annotate an Ingress).
  2. cert-manager creates an Order to request the certificate from a CA.
  3. The CA issues a Challenge (HTTP-01 or DNS-01) to verify domain ownership.
  4. cert-manager solves the challenge automatically.
  5. The signed certificate is stored in a Kubernetes Secret.
  6. cert-manager renews the certificate before it expires (default: 30 days before expiry).

Installation

Install cert-manager with its CRDs:

# Install via kubectl
# kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.15.0/cert-manager.yaml

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

Verify the installation:

# kubectl get pods -n cert-manager
# NAME                                       READY   STATUS    RESTARTS   AGE
# cert-manager-7f4d5b5b9-xxxxx              1/1     Running   0          60s
# cert-manager-cainjector-6b8d7c5d5-xxxxx   1/1     Running   0          60s
# cert-manager-webhook-8b5f7c6d4-xxxxx      1/1     Running   0          60s

All three pods must be running: the main controller, the CA injector, and the webhook.

Setting Up Issuers

An Issuer tells cert-manager where to get certificates. There are two scopes:

  • Issuer — works in a single namespace.
  • ClusterIssuer — works across all namespaces.

For most setups, use a ClusterIssuer.

Let’s Encrypt with HTTP-01

HTTP-01 challenges work by placing a file at http://yourdomain/.well-known/acme-challenge/. Your Ingress controller must be reachable from the internet.

# cluster-issuer-staging.yaml
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-staging
spec:
  acme:
    email: your-email@example.com
    server: https://acme-staging-v02.api.letsencrypt.org/directory
    privateKeySecretRef:
      name: letsencrypt-staging-key
    solvers:
      - http01:
          ingress:
            class: nginx
# cluster-issuer-production.yaml
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-prod
spec:
  acme:
    email: your-email@example.com
    server: https://acme-v02.api.letsencrypt.org/directory
    privateKeySecretRef:
      name: letsencrypt-prod-key
    solvers:
      - http01:
          ingress:
            class: nginx

Always start with the staging issuer. Let’s Encrypt has rate limits on production, and you do not want to hit them while testing.

# kubectl apply -f cluster-issuer-staging.yaml
# kubectl apply -f cluster-issuer-production.yaml
# kubectl get clusterissuer

Let’s Encrypt with DNS-01

DNS-01 challenges verify domain ownership by creating a DNS TXT record. This works for wildcard certificates and domains not reachable from the internet.

# cluster-issuer-dns.yaml
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-dns
spec:
  acme:
    email: your-email@example.com
    server: https://acme-v02.api.letsencrypt.org/directory
    privateKeySecretRef:
      name: letsencrypt-dns-key
    solvers:
      - dns01:
          cloudflare:
            email: your-email@example.com
            apiTokenSecretRef:
              name: cloudflare-api-token
              key: api-token

Create the API token secret:

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

cert-manager supports many DNS providers: AWS Route 53, Google Cloud DNS, Azure DNS, Cloudflare, DigitalOcean, and more.

Requesting Certificates via Ingress

The easiest way to get a certificate is to annotate your Ingress:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: web-app
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
  ingressClassName: nginx
  tls:
    - hosts:
        - app.example.com
      secretName: app-example-com-tls
  rules:
    - host: app.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: web-app
                port:
                  number: 80

When you apply this Ingress, cert-manager automatically:

  1. Creates a Certificate resource.
  2. Initiates the ACME challenge.
  3. Stores the signed certificate in the app-example-com-tls Secret.
  4. Your Ingress controller picks up the Secret and serves TLS.

Multiple Domains

spec:
  tls:
    - hosts:
        - app.example.com
        - www.example.com
      secretName: example-com-tls
    - hosts:
        - api.example.com
      secretName: api-example-com-tls

Each tls entry gets its own certificate and Secret.

Requesting Certificates Directly

For more control, create a Certificate resource explicitly:

apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: app-example-com
  namespace: default
spec:
  secretName: app-example-com-tls
  issuerRef:
    name: letsencrypt-prod
    kind: ClusterIssuer
  dnsNames:
    - app.example.com
    - www.example.com
  duration: 2160h    # 90 days
  renewBefore: 720h  # Renew 30 days before expiry

Wildcard Certificates

Wildcard certificates require DNS-01 challenges:

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

The bare domain example.com is listed separately because a wildcard only covers subdomains, not the root.

Checking Certificate Status

# View all certificates
# kubectl get certificates -A

# Detailed status
# kubectl describe certificate app-example-com

# Check the certificate secret
# kubectl get secret app-example-com-tls -o yaml

# View certificate details
# kubectl get secret app-example-com-tls -o jsonpath='{.data.tls\.crt}' | base64 -d | openssl x509 -text -noout

Key status fields to check:

# kubectl get certificate app-example-com -o yaml
# status:
#   conditions:
#     - type: Ready
#       status: "True"
#       message: Certificate is up to date and has not expired
#   notAfter: "2026-10-05T12:00:00Z"
#   notBefore: "2026-07-07T12:00:00Z"
#   renewalTime: "2026-09-05T12:00:00Z"

Troubleshooting

When a certificate is not ready, follow the chain: Certificate -> CertificateRequest -> Order -> Challenge.

# Check the certificate
# kubectl describe certificate app-example-com

# Check the certificate request
# kubectl get certificaterequest -n default
# kubectl describe certificaterequest app-example-com-xxxxx

# Check the order
# kubectl get order -n default
# kubectl describe order app-example-com-xxxxx

# Check the challenge
# kubectl get challenge -n default
# kubectl describe challenge app-example-com-xxxxx

Common Issues

Challenge stuck in pending:

The HTTP-01 challenge needs the solver pod to be reachable at port 80. Check:

  • Is your Ingress controller running?
  • Are DNS records pointing to your load balancer?
  • Are there firewall rules blocking port 80?
# Check solver pod
# kubectl get pods -n default -l acme.cert-manager.io/http01-solver=true
# kubectl logs <solver-pod-name>

Rate limiting:

Let’s Encrypt limits production certificate issuance to 50 per registered domain per week. Use the staging issuer for testing:

# Switch to staging for testing
annotations:
  cert-manager.io/cluster-issuer: letsencrypt-staging

Permission errors with DNS-01:

Ensure your cloud provider API credentials have the right permissions to create DNS records. For AWS Route 53, the IAM policy needs route53:ChangeResourceRecordSets and route53:GetChange.

Webhook timeout:

If cert-manager webhook is not ready, certificate creation fails. Check webhook logs:

# kubectl logs -n cert-manager deploy/cert-manager-webhook

Certificate for Non-Ingress Workloads

You can use cert-manager certificates with any workload that needs TLS:

apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: grpc-server-cert
  namespace: default
spec:
  secretName: grpc-server-tls
  issuerRef:
    name: letsencrypt-prod
    kind: ClusterIssuer
  dnsNames:
    - grpc.example.com
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: grpc-server
spec:
  template:
    spec:
      containers:
        - name: grpc-server
          image: myregistry/grpc-server:v1
          volumeMounts:
            - name: tls
              mountPath: /etc/tls
              readOnly: true
      volumes:
        - name: tls
          secret:
            secretName: grpc-server-tls

The application reads /etc/tls/tls.crt and /etc/tls/tls.key to serve TLS.

Self-Signed and Internal CA

For internal services that do not need a public CA:

# Self-signed issuer for bootstrapping
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: selfsigned
spec:
  selfSigned: {}
---
# Create a CA certificate
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: internal-ca
  namespace: cert-manager
spec:
  isCA: true
  commonName: internal-ca
  secretName: internal-ca-key
  issuerRef:
    name: selfsigned
    kind: ClusterIssuer
  duration: 87600h # 10 years
---
# Use the CA to issue certificates
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: internal-ca-issuer
spec:
  ca:
    secretName: internal-ca-key

Now you can issue certificates signed by your internal CA for service-to-service TLS.

Wrapping Up

cert-manager eliminates the manual work of TLS certificate management in Kubernetes. The setup path is straightforward:

  1. Install cert-manager into your cluster.
  2. Create a ClusterIssuer for Let’s Encrypt (start with staging).
  3. Annotate your Ingress resources or create Certificate objects.
  4. cert-manager handles issuance, storage, and renewal automatically.

Key points to remember:

  • Use HTTP-01 for standard web services reachable from the internet.
  • Use DNS-01 for wildcard certificates or internal services.
  • Always test with the staging issuer before switching to production.
  • Follow the Certificate -> CertificateRequest -> Order -> Challenge chain when troubleshooting.
  • cert-manager renews certificates automatically, but monitor the Ready condition to catch issues early.