An open-source Kubernetes controller that automates the issuance, renewal, and management of TLS certificates from multiple certificate authorities.
Table of Contents#
- Overview
- Architecture
- Installation
- Issuers and ClusterIssuers
- HTTP-01 Challenge
- DNS-01 Challenge
- Certificate Resources
- Wildcard Certificates
- Renewal Process
- Ingress Integration
- Troubleshooting
- See Also
- Sources
1. Overview#
Cert-Manager adds certificate management capabilities to Kubernetes by introducing custom resource definitions (CRDs) for certificates, issuers, and certificate requests. It handles the full lifecycle of TLS certificates: requesting, validating, issuing, and renewing, all without manual intervention.
Supported issuers:
- Let's Encrypt (ACME protocol, HTTP-01 and DNS-01 challenges)
- HashiCorp Vault (PKI secrets engine)
- Venafi (cloud and TPP)
- Self-signed (for internal/dev use)
- CA (from a Kubernetes Secret containing a CA key pair)
- External issuers via webhook solvers
2. Architecture#
| Component | Role |
|---|---|
cert-manager-controller | Main controller that watches Certificate, Issuer, and CertificateRequest resources |
cert-manager-webhook | Validates and mutates cert-manager CRDs via admission webhooks |
cert-manager-cainjector | Injects CA bundles into webhook configurations and CRDs |
CRD hierarchy:
ClusterIssuer / Issuer
└── Certificate
└── CertificateRequest
└── Order (ACME only)
└── Challenge (ACME only)3. Installation#
3.1 Prerequisites#
- Kubernetes cluster v1.24+
kubectlconfigured for the target cluster- Helm 3 (recommended)
3.2 Install 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 \
--version v1.16.2 \
--set crds.enabled=true3.3 Install via kubectl#
kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.16.2/cert-manager.yaml3.4 Verify Installation#
kubectl get pods -n cert-managerAll three pods (controller, webhook, cainjector) must be Running.
Test with a self-signed issuer:
kubectl apply -f - <<EOF
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: test-selfsigned
spec:
selfSigned: {}
---
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: test-cert
namespace: cert-manager
spec:
secretName: test-cert-tls
issuerRef:
name: test-selfsigned
kind: ClusterIssuer
commonName: test.example.com
dnsNames:
- test.example.com
EOF
kubectl describe certificate test-cert -n cert-manager
kubectl delete certificate test-cert -n cert-manager
kubectl delete clusterissuer test-selfsigned4. Issuers and ClusterIssuers#
An Issuer is namespace-scoped; a ClusterIssuer is cluster-wide.
4.1 Let's Encrypt (Staging)#
Use staging for testing 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: <your-email>
privateKeySecretRef:
name: letsencrypt-staging-key
solvers:
- http01:
ingress:
class: nginx4.2 Let's Encrypt (Production)#
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-prod
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: <your-email>
privateKeySecretRef:
name: letsencrypt-prod-key
solvers:
- http01:
ingress:
class: nginx4.3 CA Issuer (Internal PKI)#
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: internal-ca
spec:
ca:
secretName: ca-key-pairThe Secret ca-key-pair must contain tls.crt and tls.key.
5. HTTP-01 Challenge#
HTTP-01 proves domain ownership by serving a token at http://<domain>/.well-known/acme-challenge/<token>.
Requirements:
- Port 80 must be publicly accessible
- An Ingress controller must be running
- Each domain needs a separate challenge (no wildcards)
solvers:
- http01:
ingress:
class: nginx
serviceType: ClusterIPSelector to use different solvers per domain:
solvers:
- selector:
dnsNames:
- <domain-1>
http01:
ingress:
class: nginx
- selector:
dnsNames:
- <domain-2>
http01:
ingress:
class: traefik6. DNS-01 Challenge#
DNS-01 proves domain ownership by creating a TXT record at _acme-challenge.<domain>. Preferred for production because it supports wildcard certificates and does not require port 80 access.
6.1 Cloudflare#
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-dns-cloudflare
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: <your-email>
privateKeySecretRef:
name: letsencrypt-dns-key
solvers:
- dns01:
cloudflare:
apiTokenSecretRef:
name: cloudflare-api-token
key: api-tokenCreate the token Secret:
apiVersion: v1
kind: Secret
metadata:
name: cloudflare-api-token
namespace: cert-manager
type: Opaque
stringData:
api-token: <cloudflare-api-token>6.2 Route53 (AWS)#
solvers:
- dns01:
route53:
region: <aws-region>
hostedZoneID: <zone-id>
accessKeyIDSecretRef:
name: route53-credentials
key: access-key-id
secretAccessKeySecretRef:
name: route53-credentials
key: secret-access-key6.3 Other DNS Providers#
Cert-Manager supports many providers natively (Google Cloud DNS, Azure DNS, DigitalOcean, RFC2136) and external providers via webhook solvers. See the supported DNS01 providers list for details.
6.4 Webhook Solver (Generic)#
For unsupported DNS providers, deploy a webhook solver:
helm install cert-manager-webhook-<provider> <repo>/<chart> \
--namespace cert-managerThen reference it in the issuer:
solvers:
- dns01:
webhook:
groupName: <webhook-group>
solverName: <solver-name>
config:
apiKey: <key>7. Certificate Resources#
7.1 Basic Certificate#
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: <cert-name>
namespace: <namespace>
spec:
secretName: <secret-name>
issuerRef:
name: letsencrypt-prod
kind: ClusterIssuer
commonName: <domain>
dnsNames:
- <domain>
- www.<domain>7.2 Certificate with Custom Duration#
spec:
duration: 2160h # 90 days
renewBefore: 360h # Renew 15 days before expiry
secretName: <secret-name>
issuerRef:
name: letsencrypt-prod
kind: ClusterIssuer
dnsNames:
- <domain>7.3 Certificate with Private Key Options#
spec:
secretName: <secret-name>
privateKey:
algorithm: ECDSA
size: 256
rotationPolicy: Always
issuerRef:
name: letsencrypt-prod
kind: ClusterIssuer
dnsNames:
- <domain>8. Wildcard Certificates#
Wildcard certificates require DNS-01 validation (HTTP-01 cannot validate wildcards):
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: wildcard-example
namespace: <namespace>
spec:
secretName: wildcard-example-tls
issuerRef:
name: letsencrypt-dns-cloudflare
kind: ClusterIssuer
dnsNames:
- <domain>
- "*.<domain>"Note: The wildcard *.<domain> covers one level of subdomain only (e.g., app.<domain> but not sub.app.<domain>). Include the bare domain separately if needed.
9. Renewal Process#
Cert-Manager automatically renews certificates before they expire:
- The controller checks certificate expiry based on
renewBefore(default: 2/3 of the certificate lifetime) - When renewal is needed, a new
CertificateRequestis created - The ACME order and challenge flow runs again
- On success, the Secret is updated with the new certificate
- Applications using the Secret pick up the new cert (may require pod restart or signal)
Monitor renewal status:
# Check certificate status and expiry
kubectl get certificates -A
# Detailed status including renewal time
kubectl describe certificate <cert-name> -n <namespace>
# Check certificate requests
kubectl get certificaterequest -A
# View ACME orders and challenges
kubectl get orders -A
kubectl get challenges -AForce a manual renewal:
kubectl cert-manager renew <cert-name> -n <namespace>Requires the kubectl cert-manager plugin:
kubectl krew install cert-manager10. Ingress Integration#
10.1 Automatic Certificate via Ingress Annotation#
The simplest approach; cert-manager watches for annotated Ingress resources and creates Certificate resources automatically:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: <ingress-name>
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
tls:
- hosts:
- <domain>
secretName: <domain>-tls
rules:
- host: <domain>
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: <service-name>
port:
number: 8010.2 Using an Existing Certificate#
If you create the Certificate resource manually, reference the same secretName in the Ingress tls block without the annotation.
Troubleshooting#
| Issue | Cause | Solution |
|---|---|---|
Certificate stuck in Not Ready | Challenge failed or order timed out | Run kubectl describe certificate <name> and kubectl get challenges -A to see the failure reason |
| HTTP-01 challenge fails with 404 | Ingress not routing /.well-known/acme-challenge/ correctly | Verify Ingress class matches the solver config; check Ingress controller logs |
| DNS-01 challenge fails with NXDOMAIN | TXT record not created or DNS propagation delay | Verify DNS credentials; check cert-manager-controller logs; increase --dns01-recursive-nameservers-only |
| Webhook validation errors on apply | Webhook pod not ready or CA bundle missing | Wait for cert-manager-webhook pod to be Running; check cainjector logs |
certificate has been denied | Approval controller rejected the request | Check RBAC for CertificateRequest approval; see kubectl describe certificaterequest |
| Secret not updated after renewal | Old Secret still referenced; rotation policy set to Never | Set privateKey.rotationPolicy: Always; restart pods consuming the Secret |
| Rate limited by Let's Encrypt | Too many certificates for same domain in a short period | Use staging issuer for testing; check Let's Encrypt rate limits |
acme: error code 400 | Invalid email, domain, or account key | Verify email address and domain ownership; delete the ACME account Secret and re-create |
| Wildcard cert fails with HTTP-01 | HTTP-01 does not support wildcards | Switch to a DNS-01 solver for wildcard domains |
| Multiple solvers conflict | Solver selector not matching the correct domain | Add explicit selector.dnsNames to each solver to target specific domains |