Skip to main content

Overview

cert-manager automates the management and issuance of TLS certificates in Kubernetes. It integrates with Let’s Encrypt and other certificate authorities to automatically provision, renew, and manage certificates for your applications.

Installation

Prerequisites

  1. Install Helm (if not already installed):
# Download and install Helm
curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash

# Verify installation
helm version

Install cert-manager

Install cert-manager using Helm:
# Add the Jetstack Helm repository
helm repo add jetstack https://charts.jetstack.io

# Update your local Helm chart repository cache
helm repo update

# Install cert-manager with CRDs
helm install cert-manager jetstack/cert-manager \
  --namespace cert-manager \
  --create-namespace \
  --version v1.14.0 \
  --set installCRDs=true
The --set installCRDs=true flag automatically installs the Custom Resource Definitions (CRDs) required by cert-manager.

Verify Installation

# Check that cert-manager pods are running
kubectl get pods -n cert-manager

# Expected output:
# NAME                                      READY   STATUS    RESTARTS   AGE
# cert-manager-*                           1/1     Running   0          1m
# cert-manager-cainjector-*                1/1     Running   0          1m
# cert-manager-webhook-*                   1/1     Running   0          1m

ClusterIssuer Configuration

A ClusterIssuer is a cluster-wide resource that represents a certificate authority from which signed certificates can be obtained.

Let’s Encrypt Production Issuer

Create a ClusterIssuer for Let’s Encrypt production:
issuer.yml
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-prod
spec:
  acme:
    # Let's Encrypt production server
    server: https://acme-v02.api.letsencrypt.org/directory
    
    # Email for certificate expiration notifications
    email: [email protected]
    
    # Secret to store the ACME account private key
    privateKeySecretRef:
      name: letsencrypt-prod
    
    # ACME challenge solver configuration
    solvers:
    - http01:
        ingress:
          class: nginx
Apply the ClusterIssuer:
kubectl apply -f issuer.yml

Let’s Encrypt Staging Issuer (Testing)

For testing, use the staging environment to avoid rate limits:
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-staging
spec:
  acme:
    # Let's Encrypt staging server (higher rate limits)
    server: https://acme-staging-v02.api.letsencrypt.org/directory
    email: [email protected]
    privateKeySecretRef:
      name: letsencrypt-staging
    solvers:
    - http01:
        ingress:
          class: nginx
Always test with the staging issuer first! Let’s Encrypt production has strict rate limits: 50 certificates per registered domain per week.

ACME Challenge Methods

Proves domain ownership by serving a specific file over HTTP:
solvers:
- http01:
    ingress:
      class: nginx
Requirements:
  • Port 80 must be accessible from the internet
  • Domain must resolve to your cluster’s ingress controller
Pros: Simple, works with most setups Cons: Requires port 80 access, doesn’t support wildcard certificates

DNS-01 Challenge (For Wildcard Certificates)

Proves domain ownership by creating a DNS TXT record:
solvers:
- dns01:
    cloudDNS:
      project: my-gcp-project
      serviceAccountSecretRef:
        name: clouddns-dns01-solver-sa
        key: key.json
Pros: Supports wildcard certificates, doesn’t require public ingress Cons: Requires DNS provider API access, slower validation

Certificate Management

Automatic Certificate with Ingress

The simplest method - cert-manager automatically creates and manages certificates:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: ingress-nginx
  annotations:
    # Tell cert-manager to issue a certificate
    cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
  ingressClassName: nginx
  tls:
    - hosts:
        - exchange.jogeshwar.xyz
      # Secret where certificate will be stored
      secretName: exchange-tls
  rules:
    - host: exchange.jogeshwar.xyz
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: exchange-service
                port:
                  number: 80
When you apply this Ingress:
  1. cert-manager detects the cert-manager.io/cluster-issuer annotation
  2. Automatically creates a Certificate resource
  3. Initiates ACME challenge with Let’s Encrypt
  4. Stores the issued certificate in the exchange-tls Secret
  5. NGINX Ingress uses the certificate for TLS termination
This is the recommended approach! You don’t need to manually create Certificate resources.

Manual Certificate Resource (Advanced)

For more control, explicitly create a Certificate resource:
certificate.yml
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: exchange-cert
  namespace: default
spec:
  # Secret where certificate will be stored
  secretName: exchange-tls
  
  # Certificate duration before renewal
  duration: 2160h # 90 days
  renewBefore: 360h # 15 days
  
  # Subject information
  subject:
    organizations:
      - My Company
  
  # Certificate properties
  isCA: false
  privateKey:
    algorithm: RSA
    encoding: PKCS1
    size: 2048
  
  # Valid usages
  usages:
    - server auth
    - client auth
  
  # Domain names
  dnsNames:
    - exchange.jogeshwar.xyz
    - www.exchange.jogeshwar.xyz
  
  # Reference to ClusterIssuer
  issuerRef:
    name: letsencrypt-prod
    kind: ClusterIssuer
    group: cert-manager.io
With the Ingress annotation method, you typically don’t need separate Certificate resources. cert-manager creates them automatically.

Verification and Management

Check ClusterIssuer Status

# List all cluster issuers
kubectl get clusterissuer

# Describe issuer for detailed status
kubectl describe clusterissuer letsencrypt-prod
Look for Ready: True in the status.

Check Certificate Status

# List certificates in a namespace
kubectl get certificate -n default

# Describe certificate for details
kubectl describe certificate exchange-cert

# Check certificate ready condition
kubectl get certificate exchange-cert -o jsonpath='{.status.conditions[?(@.type=="Ready")].status}'

Inspect Certificate Secret

# View the certificate secret
kubectl get secret exchange-tls -n default -o yaml

# Decode and inspect the certificate
kubectl get secret exchange-tls -n default -o jsonpath='{.data.tls\.crt}' | \
  base64 --decode | \
  openssl x509 -text -noout

Check Certificate Requests

# List certificate requests (tracks issuance process)
kubectl get certificaterequest -n default

# View certificate request details
kubectl describe certificaterequest <request-name>

Check ACME Challenges

# List active challenges
kubectl get challenge -n default

# Describe challenge to see validation status
kubectl describe challenge <challenge-name>

Certificate Renewal

cert-manager automatically renews certificates before they expire:
  • Default renewal: 30 days before expiration (for 90-day Let’s Encrypt certs)
  • Configurable via renewBefore field in Certificate spec
# Force certificate renewal
kubectl delete secret exchange-tls
# cert-manager will automatically recreate it

# Or use cert-manager kubectl plugin
kubectl cert-manager renew exchange-cert

Troubleshooting

Certificate Not Issuing

  1. Check cert-manager logs:
kubectl logs -n cert-manager -l app=cert-manager
  1. Check certificate status:
kubectl describe certificate exchange-cert
  1. Check certificate request:
kubectl describe certificaterequest

HTTP-01 Challenge Failing

# Check challenge status
kubectl describe challenge

# Common issues:
# - Domain doesn't resolve to your cluster
# - Port 80 not accessible
# - Ingress class mismatch
# - Firewall blocking traffic

# Test domain resolution
nslookup exchange.jogeshwar.xyz

# Test HTTP access
curl http://exchange.jogeshwar.xyz/.well-known/acme-challenge/test

Rate Limit Errors

If you hit Let’s Encrypt rate limits:
  1. Switch to staging issuer for testing
  2. Wait for the rate limit window to reset
  3. Check current limits: https://letsencrypt.org/docs/rate-limits/

Delete Old Certificates

If you need to clean up and start fresh:
# Delete the certificate resource
kubectl delete certificate exchange-cert

# Delete the corresponding secret
kubectl delete secret exchange-tls

# cert-manager will recreate them based on Ingress annotations

Best Practices

Use Ingress Annotations

Let cert-manager manage certificates automatically via Ingress annotations rather than manual Certificate resources

Test with Staging

Always test certificate issuance with Let’s Encrypt staging before switching to production

Monitor Expiration

Set up alerts for certificate expiration even with auto-renewal

Backup Secrets

Back up certificate Secrets as part of your disaster recovery plan

Multiple Domains

Single Certificate, Multiple Domains

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

Separate Certificates per Domain

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

Additional Resources

Build docs developers (and LLMs) love