Portal | Level: L1: Foundations | Topics: cert-manager | Domain: Kubernetes
cert-manager — Primer¶
Why This Matters¶
Manual TLS certificate management does not scale. With three services you can track renewal dates in a spreadsheet. With 30 services across 4 clusters, you cannot. Missed renewals cause outages. Manually copying certs into Kubernetes Secrets is error-prone and leaks private keys through shell history and CI logs.
Who made it: cert-manager was created by Jetstack (founded 2014), acquired by Venafi in 2020. It became a CNCF incubating project in 2022 — the first project focused entirely on certificate lifecycle management to join the foundation.
cert-manager automates the full certificate lifecycle in Kubernetes:
- Issuance: requests certificates from Let's Encrypt, an internal CA, Vault PKI, or self-signed
- Renewal: renews automatically at 2/3 of validity (default), before expiry
- Storage: writes certs into Kubernetes Secrets that pods and Ingresses consume
- Ingress integration: annotate an Ingress and cert-manager provisions the cert automatically
Without cert-manager, an operator must remember renewal dates, run certbot, copy PEM files into Secrets, and restart Ingress controllers — for every certificate, in every cluster, forever.
Core Concepts¶
1. Architecture¶
cert-manager runs as a set of controllers in the cert-manager namespace:
cert-manager-controller — core reconciliation loop
cert-manager-cainjector — injects CA bundles into webhooks
cert-manager-webhook — validates CRD resources
The controllers watch cert-manager CRDs and drive the certificate lifecycle:
Certificate (desired state)
└─► CertificateRequest (one-shot signing request)
└─► Order (ACME order for domain validation)
└─► Challenge (HTTP-01 or DNS-01 validation)
2. CRDs¶
| CRD | Purpose |
|---|---|
Certificate |
Declares a desired cert: domain, issuer, Secret name, duration |
Issuer |
Namespace-scoped issuer configuration (Let's Encrypt, CA, Vault, etc.) |
ClusterIssuer |
Cluster-scoped issuer — accessible from any namespace |
CertificateRequest |
Auto-created per-signing-request (usually don't create manually) |
Order |
ACME order lifecycle object (auto-created) |
Challenge |
ACME challenge (HTTP-01 or DNS-01) — auto-created |
3. Installation¶
# Install via Helm (recommended for production)
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 \
--version v1.14.4
# Verify
kubectl get pods -n cert-manager
# NAME READY STATUS RESTARTS AGE
# cert-manager-5c47f46f57-p8wq7 1/1 Running 0 60s
# cert-manager-cainjector-6659d6844-xq8gj 1/1 Running 0 60s
# cert-manager-webhook-547c9d6d-v5lbt 1/1 Running 0 60s
# Install kubectl plugin
kubectl apply -f https://github.com/cert-manager/cmctl/releases/latest/download/cmctl.yaml
# Or via krew:
kubectl krew install cert-manager
4. Issuers¶
Self-signed Issuer (useful for testing or bootstrapping a CA):
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: selfsigned-issuer
spec:
selfSigned: {}
CA Issuer (sign with your own CA certificate):
# First, create a Secret holding the CA cert + key
kubectl create secret tls my-ca-secret \
--cert=ca.crt \
--key=ca.key \
-n cert-manager
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: ca-issuer
spec:
ca:
secretName: my-ca-secret
Let's Encrypt (ACME) — HTTP-01:
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-prod
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: ops@example.com
privateKeySecretRef:
name: letsencrypt-prod-account-key
solvers:
- http01:
ingress:
class: nginx
Let's Encrypt (ACME) — DNS-01 with Route53:
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-dns01
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: ops@example.com
privateKeySecretRef:
name: letsencrypt-dns01-account-key
solvers:
- dns01:
route53:
region: us-east-1
hostedZoneID: Z1XXXXXXXXXXXXX # optional but faster
accessKeyIDSecretRef:
name: route53-credentials
key: access-key-id
secretAccessKeySecretRef:
name: route53-credentials
key: secret-access-key
Let's Encrypt — DNS-01 with Cloudflare:
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-cloudflare
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: ops@example.com
privateKeySecretRef:
name: letsencrypt-cloudflare-account-key
solvers:
- dns01:
cloudflare:
email: ops@example.com
apiTokenSecretRef:
name: cloudflare-api-token
key: api-token
Let's Encrypt — DNS-01 with GCP Cloud DNS:
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-gcpdns
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: ops@example.com
privateKeySecretRef:
name: letsencrypt-gcpdns-key
solvers:
- dns01:
cloudDNS:
project: my-gcp-project
serviceAccountSecretRef:
name: clouddns-sa-key
key: key.json
Vault PKI Issuer:
# First enable and configure Vault PKI:
# vault secrets enable pki
# vault write pki/root/generate/internal common_name=example.com ttl=87600h
# vault write pki/roles/example-com allowed_domains=example.com allow_subdomains=true max_ttl=72h
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: vault-issuer
spec:
vault:
server: https://vault.example.com
path: pki/sign/example-com
auth:
kubernetes:
mountPath: /v1/auth/kubernetes
role: cert-manager
secretRef:
name: cert-manager-vault-token
key: token
5. Certificate Resources¶
Basic Certificate:
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: example-com-tls
namespace: default
spec:
secretName: example-com-tls # Secret that will hold the cert
issuerRef:
name: letsencrypt-prod
kind: ClusterIssuer
commonName: example.com
dnsNames:
- example.com
- www.example.com
duration: 2160h # 90 days (Let's Encrypt default)
renewBefore: 720h # Renew 30 days before expiry (1/3 of 90 days)
Gotcha: A wildcard
*.example.comdoes NOT cover the apex domainexample.comitself. Always include both*.example.comandexample.comindnsNamesor you will get certificate errors when users visit the bare domain.
Wildcard Certificate (requires DNS-01):
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: wildcard-example-com
namespace: default
spec:
secretName: wildcard-example-com-tls
issuerRef:
name: letsencrypt-dns01
kind: ClusterIssuer
dnsNames:
- "*.example.com"
- example.com # include apex too — wildcard doesn't cover it
duration: 2160h
renewBefore: 720h
Certificate with IP SAN:
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: internal-service-tls
namespace: production
spec:
secretName: internal-service-tls
issuerRef:
name: ca-issuer
kind: ClusterIssuer
dnsNames:
- internal-svc.production.svc.cluster.local
ipAddresses:
- 10.0.1.50
duration: 8760h # 1 year for internal CA certs
renewBefore: 720h
6. Ingress Annotation Auto-Provisioning¶
The simplest way to use cert-manager: annotate your Ingress and cert-manager creates the Certificate automatically.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: myapp-ingress
namespace: default
annotations:
cert-manager.io/cluster-issuer: "letsencrypt-prod"
# cert-manager.io/issuer: "my-namespace-issuer" # for namespace-scoped Issuer
spec:
ingressClassName: nginx
tls:
- hosts:
- myapp.example.com
secretName: myapp-example-com-tls # cert-manager creates this Secret
rules:
- host: myapp.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: myapp
port:
number: 80
7. Certificate Lifecycle and Renewal¶
cert-manager renews certificates at renewBefore time before expiry. Default behavior:
Certificate issued with 90-day validity
→ cert-manager stores in Secret
→ at 60 days remaining (after 30 days), renews automatically
→ new cert stored in same Secret
→ pods/Ingress pick up new cert (may need restart if mounting as file)
Check certificate status:
# Via kubectl
kubectl get certificate -A
# NAME READY SECRET AGE
# example-com-tls True example-com-tls 15d
kubectl describe certificate example-com-tls
# Status:
# Conditions:
# Type: Ready — Status: True
# Message: Certificate is up to date and has not expired
# Via cert-manager kubectl plugin
kubectl cert-manager status certificate example-com-tls
# Check expiry directly from the Secret
kubectl get secret example-com-tls -o jsonpath='{.data.tls\.crt}' | \
base64 -d | openssl x509 -noout -dates
# notBefore=Jan 1 00:00:00 2024 GMT
# notAfter=Apr 1 00:00:00 2024 GMT
8. HTTP-01 vs DNS-01 Challenge Comparison¶
| Factor | HTTP-01 | DNS-01 |
|---|---|---|
| Mechanism | cert-manager creates a pod/Ingress route at /.well-known/acme-challenge/ |
cert-manager creates a _acme-challenge TXT record |
| Wildcard certs | Not supported | Supported |
| Private clusters | Requires public ingress | Works on air-gapped clusters |
| DNS provider API | Not needed | Required |
| Speed | Faster (seconds to verify) | Slower (DNS propagation, 30–120s) |
| Use case | Single-name certs on public clusters | Wildcards, private clusters |
Remember: Mnemonic for challenge types: "HTTP for Hosts, DNS for Domains (wildcards)." If you need
*.example.com, DNS-01 is the only option — HTTP-01 cannot validate wildcard certificates per the ACME specification (RFC 8555).
9. Troubleshooting Failed Certificate Issuance¶
When a Certificate is stuck in False/Pending:
# Step 1: Check Certificate status
kubectl describe certificate myapp-tls -n default
# Look for: Status.Conditions and Events
# Step 2: Check the CertificateRequest
kubectl get certificaterequest -n default
kubectl describe certificaterequest myapp-tls-xxxxx -n default
# Step 3: Check the Order (ACME)
kubectl get order -n default
kubectl describe order myapp-tls-xxxxx -n default
# Look for: Status, Reason, Message
# Step 4: Check the Challenge
kubectl get challenge -n default
kubectl describe challenge myapp-tls-xxxxx -n default
# Common issue: "Waiting for DNS record to propagate"
# Common issue: "Error presenting challenge: Route53 access denied"
# Common issue: "HTTP challenge solver pod not running"
# Step 5: Check cert-manager controller logs
kubectl logs -n cert-manager deployment/cert-manager -f --tail=100
# Step 6: Check if HTTP-01 solver is reachable
curl -v http://myapp.example.com/.well-known/acme-challenge/test-probe
# Step 7: Force renewal (delete the Secret to trigger re-issuance)
kubectl delete secret myapp-tls -n default
# cert-manager detects missing Secret and re-issues
# Step 8: Use cmctl for quick checks
kubectl cert-manager check api # verify cert-manager API is healthy
10. Monitoring Certificate Expiry with Prometheus¶
cert-manager exposes Prometheus metrics:
# Scrape config for Prometheus
scrape_configs:
- job_name: cert-manager
static_configs:
- targets: ['cert-manager.cert-manager.svc:9402']
Key metrics:
# Certificates expiring within 7 days
certmanager_certificate_expiration_timestamp_seconds - time() < 604800
# Certificates in Ready=False state
certmanager_certificate_ready_status{condition="False"} == 1
# Alert rule
groups:
- name: cert-manager
rules:
- alert: CertificateExpiringSoon
expr: |
(certmanager_certificate_expiration_timestamp_seconds - time()) / 86400 < 14
for: 1h
labels:
severity: warning
annotations:
summary: "Certificate {{ $labels.name }} expires in {{ $value | humanizeDuration }}"
- alert: CertificateNotReady
expr: certmanager_certificate_ready_status{condition="False"} == 1
for: 10m
labels:
severity: critical
annotations:
summary: "Certificate {{ $labels.name }} in namespace {{ $labels.namespace }} is not ready"
Quick Reference¶
# Install cert-manager
helm install cert-manager jetstack/cert-manager \
--namespace cert-manager --create-namespace \
--set installCRDs=true --version v1.14.4
# List all certificates and their status
kubectl get certificate -A
# Check certificate expiry
kubectl get secret <secret-name> -o jsonpath='{.data.tls\.crt}' | \
base64 -d | openssl x509 -noout -dates
# Force renewal
kubectl cert-manager renew <certificate-name>
# Check ACME challenge status
kubectl get challenge -A
# Tail controller logs
kubectl logs -n cert-manager deployment/cert-manager -f
# Delete Secret to trigger re-issuance
kubectl delete secret <tls-secret-name>
Wiki Navigation¶
Prerequisites¶
- Kubernetes Ops (Production) (Topic Pack, L2)
- TLS & Certificates Ops (Topic Pack, L1)