Manual certificate management is a recipe for outages. Certificates expire at 3 AM on a holiday weekend. Renewal processes live in tribal knowledge. Teams deploy services without HTTPS because “it’s too complicated.”

cert-manager automates everything. Define what certificates you need, and cert-manager handles issuance, renewal, and Kubernetes Secret management. Forever.

This is one of the first things I install in every cluster.

How cert-manager Works

flowchart TD
    subgraph cluster["Kubernetes Cluster"]
        CM["cert-manager"]
        CERT["Certificate<br/>Resource"]
        SECRET["TLS Secret"]
        INGRESS["Ingress"]
    end

    subgraph external["External"]
        LE["Let's Encrypt<br/>ACME Server"]
        DNS["DNS Provider"]
    end

    CERT -->|"watches"| CM
    CM -->|"creates"| SECRET
    CM <-->|"ACME protocol"| LE
    CM <-->|"DNS challenge"| DNS
    SECRET -->|"mounts"| INGRESS
  1. You create a Certificate resource
  2. cert-manager requests a certificate from the issuer (Let’s Encrypt, Vault, etc.)
  3. cert-manager completes the challenge (HTTP-01 or DNS-01)
  4. cert-manager stores the certificate in a Kubernetes Secret
  5. Your Ingress/Gateway uses the Secret for TLS

Renewal happens automatically 30 days before expiration.

Installation

helm repo add jetstack https://charts.jetstack.io
helm repo update

helm install cert-manager jetstack/cert-manager \
  --namespace cert-manager \
  --create-namespace \
  --set installCRDs=true

For GitOps with ArgoCD:

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: cert-manager
  namespace: argocd
spec:
  project: default
  source:
    repoURL: https://charts.jetstack.io
    chart: cert-manager
    targetRevision: v1.14.0
    helm:
      values: |
        installCRDs: true
        prometheus:
          servicemonitor:
            enabled: true
  destination:
    server: https://kubernetes.default.svc
    namespace: cert-manager
  syncPolicy:
    syncOptions:
      - CreateNamespace=true

Issuers

An Issuer tells cert-manager where to get certificates. Two scopes:

  • Issuer — Works in one namespace
  • ClusterIssuer — Works across all namespaces

Let’s Encrypt (ACME)

The most common setup — free certificates from Let’s Encrypt:

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

Important: Start with staging 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: admin@example.com
    privateKeySecretRef:
      name: letsencrypt-staging-account-key
    solvers:
      - http01:
          ingress:
            class: nginx

HTTP-01 vs DNS-01 Challenges

HTTP-01: Let’s Encrypt verifies you control the domain by placing a file at /.well-known/acme-challenge/:

solvers:
  - http01:
      ingress:
        class: nginx

Works for publicly accessible services. Quick and simple.

DNS-01: Let’s Encrypt verifies via DNS TXT record:

solvers:
  - dns01:
      cloudflare:
        email: admin@example.com
        apiTokenSecretRef:
          name: cloudflare-api-token
          key: api-token

Required for:

  • Wildcard certificates (*.example.com)
  • Internal services not publicly accessible
  • Split-horizon DNS

Cloudflare DNS-01 Setup

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

Cloudflare API token needs Zone:DNS:Edit permission.

Private CA

For internal services, use your own CA:

# Create CA key pair
apiVersion: v1
kind: Secret
metadata:
  name: ca-key-pair
  namespace: cert-manager
type: kubernetes.io/tls
data:
  tls.crt: <base64-encoded-ca-cert>
  tls.key: <base64-encoded-ca-key>
---
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: internal-ca
spec:
  ca:
    secretName: ca-key-pair

Or let cert-manager create a self-signed CA:

apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: selfsigned-issuer
spec:
  selfSigned: {}
---
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: my-ca
  namespace: cert-manager
spec:
  isCA: true
  commonName: my-ca
  secretName: my-ca-secret
  privateKey:
    algorithm: ECDSA
    size: 256
  issuerRef:
    name: selfsigned-issuer
    kind: ClusterIssuer
---
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: my-ca-issuer
spec:
  ca:
    secretName: my-ca-secret

HashiCorp Vault

Integrate with Vault for PKI:

apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: vault-issuer
spec:
  vault:
    server: https://vault.vault:8200
    path: pki/sign/my-role
    auth:
      kubernetes:
        role: cert-manager
        mountPath: /v1/auth/kubernetes
        secretRef:
          name: cert-manager-vault-token
          key: token

Requesting Certificates

Certificate Resource

Explicitly request a certificate:

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

Ingress Annotation (Automatic)

Let cert-manager create certificates automatically from Ingress:

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

cert-manager sees the annotation, creates a Certificate resource, and populates the Secret.

Gateway API

For Gateway API instead of Ingress:

apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: main
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
  gatewayClassName: cilium
  listeners:
    - name: https
      port: 443
      protocol: HTTPS
      hostname: "*.example.com"
      tls:
        mode: Terminate
        certificateRefs:
          - name: wildcard-example-com-tls

Wildcard Certificates

For *.example.com, you need DNS-01:

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

Note: Include both *.example.com and example.com — wildcard doesn’t cover the apex domain.

Cross-Namespace Secrets

Certificates live in one namespace, but you might need them elsewhere. Options:

Reflector (External Controller)

Use kubernetes-reflector:

apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: shared-cert
  namespace: cert-manager
  annotations:
    reflector.v1.k8s.emberstack.com/reflection-allowed: "true"
    reflector.v1.k8s.emberstack.com/reflection-allowed-namespaces: "production,staging"
spec:
  secretName: shared-cert-tls
  # ...

trust-manager

cert-manager’s official solution for CA bundle distribution:

helm install trust-manager jetstack/trust-manager \
  --namespace cert-manager
apiVersion: trust.cert-manager.io/v1alpha1
kind: Bundle
metadata:
  name: my-ca-bundle
spec:
  sources:
    - secret:
        name: my-ca-secret
        key: ca.crt
  target:
    configMap:
      key: ca-bundle.crt
    namespaceSelector:
      matchLabels:
        trust-bundle: enabled

Monitoring and Troubleshooting

Check Certificate Status

kubectl get certificates -A
kubectl describe certificate api-example-com -n production

Check Certificate Requests

kubectl get certificaterequests -A
kubectl describe certificaterequest api-example-com-xyz -n production

Check Orders and Challenges

kubectl get orders -A
kubectl get challenges -A

# Debug a stuck challenge
kubectl describe challenge api-example-com-xyz-123 -n production

Prometheus Metrics

cert-manager exports metrics for alerting:

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

# Alert on failed renewals
- alert: CertificateRenewalFailed
  expr: certmanager_certificate_ready_status{condition="False"} == 1
  for: 1h
  labels:
    severity: critical
  annotations:
    summary: "Certificate {{ $labels.name }} is not ready"

Common Issues

Challenge stuck pending:

  • HTTP-01: Ingress not routing to cert-manager solver
  • DNS-01: API credentials wrong or missing permissions

Rate limited:

  • Too many certificates requested from Let’s Encrypt
  • Use staging issuer for testing
  • Consolidate into wildcard certificates

Secret not updating:

  • Check Certificate resource status
  • Check CertificateRequest and Order resources
  • Look at cert-manager logs: kubectl logs -n cert-manager deploy/cert-manager

Best Practices

1. Use ClusterIssuers

Unless you need namespace isolation, ClusterIssuers reduce duplication:

apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-prod
# Available to all namespaces

2. Separate Staging and Production

Always have both issuers:

# letsencrypt-staging for testing
# letsencrypt-prod for production

3. Monitor Expiration

Even with automation, monitor as a safety net:

certmanager_certificate_expiration_timestamp_seconds - time() < 604800

4. Use DNS-01 for Internal Services

If your services aren’t publicly accessible, HTTP-01 won’t work.

5. Standardize Secret Names

Convention like ${service}-tls makes it predictable:

secretName: api-example-com-tls

My Production Setup

# ClusterIssuer for Let's Encrypt with Cloudflare DNS
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-prod
spec:
  acme:
    server: https://acme-v02.api.letsencrypt.org/directory
    email: certs@example.com
    privateKeySecretRef:
      name: letsencrypt-prod
    solvers:
      - dns01:
          cloudflare:
            apiTokenSecretRef:
              name: cloudflare-api-token
              key: api-token
        selector:
          dnsZones:
            - "example.com"

---
# Wildcard certificate for all services
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: wildcard-example-com
  namespace: cert-manager
spec:
  secretName: wildcard-example-com-tls
  issuerRef:
    name: letsencrypt-prod
    kind: ClusterIssuer
  dnsNames:
    - "*.example.com"
    - "example.com"

Key decisions:

  • DNS-01 via Cloudflare — Works for everything, including wildcards
  • Wildcard certificate — One cert for all subdomains, fewer rate limit concerns
  • Central namespace — Certificate in cert-manager namespace, reflected where needed
  • Prometheus monitoring — Alert before things break

Why This Matters

TLS isn’t optional anymore. Browsers warn on HTTP. APIs require HTTPS. Internal services need encryption for zero trust.

Manual certificate management doesn’t scale:

  • Renewal dates get forgotten
  • Different teams use different processes
  • New services launch without HTTPS

cert-manager makes TLS the default:

  • Define once, renew forever
  • Same process for every team
  • Integrates with GitOps

This is automation where it matters most — security that works without thinking about it.


The best security is the security you don’t have to remember. cert-manager makes TLS automatic, so you can focus on what actually matters.