Skip to content

cert-manager Reference: ClusterIssuer, Certificate, Ingress TLS & Vault PKI

cert-manager automates TLS certificate management in Kubernetes — requesting, renewing, and distributing certificates from Let’s Encrypt, Vault, and other issuers. This covers the resources and patterns you need in production.

1. Installation & Core Resources

Install cert-manager and understand the CRDs
# Install via Helm (recommended):
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                      # creates CRDs automatically
  
# Or via kubectl (specific version — don't use :latest in prod):
kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.14.4/cert-manager.yaml

# Verify installation:
kubectl get pods -n cert-manager              # controller + webhook + cainjector
kubectl get crds | grep cert-manager.io       # 6 CRDs should appear

# Core resources:
# Issuer       — namespaced: issues certs within one namespace
# ClusterIssuer — cluster-wide: issues certs across all namespaces
# Certificate  — request for a TLS cert (cert-manager creates it)
# CertificateRequest — internal (auto-created by cert-manager)
# Order / Challenge — Let's Encrypt ACME flow (auto-managed)
# Secret       — stores the issued TLS cert (auto-created by cert-manager)

# Install cmctl (cert-manager CLI):
brew install cmctl                            # macOS
cmctl check api                               # verify cert-manager is working

2. ClusterIssuers — Let’s Encrypt

HTTP-01 and DNS-01 challenge configuration
# HTTP-01 challenge (default — needs port 80 accessible):
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              # IMPORTANT: use a real monitored email
    privateKeySecretRef:
      name: letsencrypt-prod-key          # cert-manager creates this Secret
    solvers:
      - http01:
          ingress:
            class: nginx                  # must match your IngressClass name
            # or: ingressClassName: nginx

# Staging issuer for testing (high rate limits — use this first!):
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-key
    solvers:
      - http01:
          ingress:
            class: nginx

# DNS-01 challenge (for wildcard certs or private clusters with no public HTTP):
# Requires DNS provider API access (Route53, Cloudflare, GCP, Azure DNS, etc.)
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-prod-dns
spec:
  acme:
    server: https://acme-v02.api.letsencrypt.org/directory
    email: admin@example.com
    privateKeySecretRef:
      name: letsencrypt-prod-dns-key
    solvers:
      - dns01:
          cloudflare:
            email: admin@example.com
            apiTokenSecretRef:
              name: cloudflare-api-token
              key: api-token
Always test with letsencrypt-staging first. Let’s Encrypt production has a rate limit of 5 duplicate certificates per week. Hitting it blocks all renewal for your domain for 7 days.

3. Certificate Resource

Request certificates explicitly (without Ingress annotation)
# Explicit Certificate resource (most control):
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: my-app-tls
  namespace: production
spec:
  secretName: my-app-tls-secret          # Secret cert-manager will create/update
  issuerRef:
    name: letsencrypt-prod
    kind: ClusterIssuer
  commonName: my-app.example.com          # main domain
  dnsNames:
    - my-app.example.com
    - www.my-app.example.com              # SAN (Subject Alternative Name)
    - "*.my-app.example.com"              # wildcard — requires DNS-01 challenge
  duration: 2160h                         # 90 days (Let's Encrypt default)
  renewBefore: 360h                       # renew 15 days before expiry
  usages:
    - server auth                         # TLS server certificate
    - client auth                         # add if also used for mTLS
  privateKey:
    algorithm: ECDSA                      # faster than RSA, same security level
    size: 256                             # P-256

# Check cert status:
kubectl get certificate -n production     # READY=True = cert issued + stored
kubectl describe certificate my-app-tls -n production  # events + next renewal time
kubectl get secret my-app-tls-secret -n production -o yaml  # tls.crt + tls.key

# Inspect the cert:
kubectl get secret my-app-tls-secret -n production \
  -o jsonpath='{.data.tls\.crt}' | base64 -d | openssl x509 -text -noout | grep -A2 "Validity"
  
# Force renewal:
cmctl renew my-app-tls -n production

4. Ingress Annotation (auto-cert)

Let cert-manager create certs automatically from Ingress
# The annotation approach — cert-manager watches Ingresses and auto-creates Certificate resources:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: my-app
  namespace: production
  annotations:
    cert-manager.io/cluster-issuer: "letsencrypt-prod"    # or issuer for namespaced
    # cert-manager.io/common-name: "my-app.example.com"  # optional, defaults to first host
    nginx.ingress.kubernetes.io/ssl-redirect: "true"
spec:
  ingressClassName: nginx
  tls:
    - hosts:
        - my-app.example.com
      secretName: my-app-tls-secret      # cert-manager creates this Secret
  rules:
    - host: my-app.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: my-app
                port:
                  number: 80

# After applying:
kubectl get certificate -n production    # cert-manager created Certificate automatically
kubectl describe ingress my-app -n production  # check TLS section + events

# Annotation vs explicit Certificate:
# Annotation: simpler, less control (no duration/renewBefore/privateKey config)
# Explicit Certificate: full control, needed for wildcard certs + non-Ingress use cases (gRPC, etc.)

5. Vault Issuer

Issue internal certificates from HashiCorp Vault PKI
# Vault PKI setup (in Vault):
vault secrets enable pki
vault write pki/root/generate/internal \
  common_name=example.com ttl=87600h
vault write pki/config/urls \
  issuing_certificates="https://vault:8200/v1/pki/ca" \
  crl_distribution_points="https://vault:8200/v1/pki/crl"
vault write pki/roles/cert-manager \
  allowed_domains=example.com allow_subdomains=true max_ttl=72h
vault write auth/kubernetes/role/cert-manager \
  bound_service_account_names=cert-manager \
  bound_service_account_namespaces=cert-manager \
  policies=pki-policy ttl=1h

# ClusterIssuer pointing to Vault:
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: vault-issuer
spec:
  vault:
    server: https://vault.vault.svc.cluster.local:8200
    path: pki/sign/cert-manager           # Vault PKI role endpoint
    auth:
      kubernetes:
        role: cert-manager
        mountPath: /v1/auth/kubernetes
        serviceAccountRef:
          name: cert-manager              # cert-manager's own service account

6. Troubleshooting

Debug certificate issuance failures
# Step 1: Check Certificate status
kubectl get certificate -A             # find non-Ready certs
kubectl describe certificate  -n 
# Look at: Status.Conditions, Events

# Step 2: Check CertificateRequest
kubectl get certificaterequest -n 
kubectl describe certificaterequest  -n 

# Step 3: For ACME, check Order and Challenge
kubectl get order -n 
kubectl describe order  -n   # shows challenge status
kubectl get challenge -n 
kubectl describe challenge  -n 
# HTTP-01: "Presented challenge using http-01 challenge mechanism" → check Ingress created
# DNS-01: Check DNS propagation (may take 1-5 min)

# Step 4: Check cert-manager controller logs
kubectl logs -n cert-manager deploy/cert-manager --tail=50
kubectl logs -n cert-manager deploy/cert-manager-webhook --tail=20

# Common failures:
# - CertificateRequest: "error: 429 urn:ietf:params:acme:error:rateLimited"
#   → Switch to staging issuer, hit rate limit. Wait 1 week or use staging.
# - Challenge: "Error presenting challenge: port 80 not reachable"
#   → HTTP-01 needs public access on port 80. Check Ingress + firewall.
# - Certificate stuck Pending: check cert-manager controller logs for DNS errors.
# - Webhook timeout: kubectl get pods -n cert-manager → ensure all 3 pods Running.

# Force re-issue (delete Certificate + cert-manager recreates):
kubectl delete secret my-app-tls-secret -n production
# cert-manager detects missing Secret and issues a new cert automatically
Run cmctl status certificate my-app-tls -n production for a single-command status summary including renewal time, issuer, and challenge status.

Track cert-manager, Kubernetes, and infrastructure releases.
ReleaseRun monitors Kubernetes, Docker, and 13+ DevOps technologies.

Related: Kubernetes YAML Reference | HashiCorp Vault Reference | Nginx Reference

🔍 Free tool: K8s YAML Security Linter — check your cert-manager Certificate and Issuer manifests for K8s security misconfigurations.

Founded

2023 in London, UK

Contact

hello@releaserun.com