Automating SSL Certificates in Kubernetes with cert-manager
Let me paint a picture you might recognize: you have 15 Kubernetes clusters, 300 Ingress resources, and a shared spreadsheet tracking certificate expiration dates. Every 90 days, someone from the ops team spends a morning running through that spreadsheet, logging into clusters, renewing certs manually, and praying nothing breaks. One day they’re on vacation. One cert gets missed. Production is down for 45 minutes before anyone figures out why.
cert-manager exists to make that entire scenario impossible. It’s a native Kubernetes controller that treats certificates as first-class resources — declarative, automated, and integrated directly with your Ingress. Once you’ve set it up, certificate renewal happens silently in the background, and that spreadsheet becomes a piece of history.
If you want to see what this looks like at scale in a real organization, our case study on certificate automation walks through a team that went from 40 hours of monthly manual certificate work to near-zero with cert-manager and a few complementary tools.
What Is cert-manager?
cert-manager is a native Kubernetes controller that automates the management and issuance of TLS certificates from various sources. It works as a Kubernetes API extension, adding Custom Resources that allow you to define certificates declaratively.
Key Advantages
Full Automation
- Automatic certificate issuance
- Automatic renewal before expiration
- Integration with Kubernetes Ingress
- Zero manual intervention
Support for Multiple Sources
- Let’s Encrypt (ACME)
- HashiCorp Vault
- Venafi
- Private CA (self-signed)
- External issuers (custom integrations)
Kubernetes-Native
- Declarative configuration
- CRDs (Custom Resource Definitions)
- kubectl integration
- GitOps ready
Installing cert-manager
Prerequisites
Before installation, make sure you have:
- Kubernetes cluster (version 1.22+)
- kubectl configured
- Cluster admin rights
- Helm 3.x (optional, but recommended)
Installation via Helm
The simplest way is to use Helm:
# Add Helm repository
helm repo add jetstack https://charts.jetstack.io
helm repo update
# Install CRDs
kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.13.3/cert-manager.crds.yaml
# Install cert-manager
helm install cert-manager jetstack/cert-manager \
--namespace cert-manager \
--create-namespace \
--version v1.13.3
# Verify installation
kubectl get pods -n cert-manager
Installation via kubectl
If you prefer pure YAML:
# Install cert-manager with all components
kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.13.3/cert-manager.yaml
# Check status
kubectl get pods -n cert-manager
kubectl get crds | grep cert-manager
After installation, you should see three pods:
- cert-manager
- cert-manager-cainjector
- cert-manager-webhook
Configuring Let’s Encrypt
ClusterIssuer vs Issuer
cert-manager offers two types of resources for issuing certificates:
Issuer - works within a single namespace ClusterIssuer - works cluster-wide (recommended)
Configuring ClusterIssuer for Let’s Encrypt
Let’s start with the staging environment (for testing):
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-staging
spec:
acme:
# Let's Encrypt staging server (higher limits, test certificates)
server: https://acme-staging-v02.api.letsencrypt.org/directory
email: admin@example.com # Your email for notifications
# Secret to store ACME private key
privateKeySecretRef:
name: letsencrypt-staging
# HTTP-01 challenge
solvers:
- http01:
ingress:
class: nginx # or traefik, depending on Ingress Controller
For production:
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
solvers:
- http01:
ingress:
class: nginx
Apply the configuration:
kubectl apply -f clusterissuer-staging.yaml
kubectl apply -f clusterissuer-prod.yaml
# Check status
kubectl get clusterissuer
kubectl describe clusterissuer letsencrypt-prod
Automatic Certificates for Ingress
Basic Ingress Configuration
The simplest way is to add annotations to Ingress:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: myapp-ingress
annotations:
cert-manager.io/cluster-issuer: "letsencrypt-prod"
# Optional: HTTP -> HTTPS redirect
nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
spec:
ingressClassName: nginx
tls:
- hosts:
- myapp.example.com
secretName: myapp-tls # cert-manager will create this Secret automatically
rules:
- host: myapp.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: myapp-service
port:
number: 80
After applying:
kubectl apply -f ingress.yaml
# Monitor certificate issuance process
kubectl get certificate
kubectl describe certificate myapp-tls
# Check events
kubectl get events --sort-by='.lastTimestamp'
Manual Certificate Management
You can also create a Certificate resource directly:
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: myapp-cert
namespace: default
spec:
secretName: myapp-tls
issuerRef:
name: letsencrypt-prod
kind: ClusterIssuer
commonName: myapp.example.com
dnsNames:
- myapp.example.com
- www.myapp.example.com
# Automatic renewal 30 days before expiration
renewBefore: 720h # 30 days
DNS-01 Challenge for Wildcard Certificates
For wildcard certificates (*.example.com) you must use DNS-01 challenge:
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-dns
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: admin@example.com
privateKeySecretRef:
name: letsencrypt-dns
solvers:
- dns01:
cloudflare:
email: admin@example.com
apiTokenSecretRef:
name: cloudflare-api-token
key: api-token
Create Secret with Cloudflare API token:
kubectl create secret generic cloudflare-api-token \
--from-literal=api-token='your-cloudflare-api-token'
Certificate for wildcard:
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: wildcard-cert
spec:
secretName: wildcard-tls
issuerRef:
name: letsencrypt-dns
kind: ClusterIssuer
dnsNames:
- "example.com"
- "*.example.com"
Monitoring and Debugging
Checking Certificate Status
# List all certificates
kubectl get certificate --all-namespaces
# Certificate details
kubectl describe certificate myapp-tls
# CertificateRequest status
kubectl get certificaterequest
kubectl describe certificaterequest <name>
# Order status (ACME)
kubectl get order
kubectl describe order <name>
# Challenge status
kubectl get challenge
kubectl describe challenge <name>
cert-manager Logs
# Main controller
kubectl logs -n cert-manager deploy/cert-manager
# Webhook
kubectl logs -n cert-manager deploy/cert-manager-webhook
# CA injector
kubectl logs -n cert-manager deploy/cert-manager-cainjector
# Follow logs in real-time
kubectl logs -n cert-manager deploy/cert-manager -f
Common Issues
Issue 1: Certificate in “Pending” state
# Check CertificateRequest
kubectl get certificaterequest
kubectl describe certificaterequest <name>
# Check Order
kubectl get order
kubectl describe order <name>
Common causes:
- Incorrect DNS configuration
- Domain not accessible by Let’s Encrypt
- Ingress Controller issues
Issue 2: HTTP-01 challenge fails
# Check if challenge pod was created
kubectl get pods | grep cm-acme-http-solver
# Check if Ingress for challenge was created
kubectl get ingress
Solution:
- Check if domain points to cluster
- Check if port 80 is open
- Check Ingress Controller logs
Issue 3: Too many requests (rate limiting)
Let’s Encrypt has limits:
- 50 certificates per domain weekly
- 5 duplicates weekly
Solution:
- Use staging during testing
- Plan production deployments
Integration with Monitoring
cert-manager exposes Prometheus metrics out of the box, which means your certificate state becomes part of your existing observability stack. For a full walkthrough of SSL certificate monitoring including dashboards, alert rules, and Alertmanager configuration, see our Prometheus and Grafana SSL monitoring guide.
Prometheus Metrics
cert-manager exports metrics:
apiVersion: v1
kind: Service
metadata:
name: cert-manager-metrics
namespace: cert-manager
labels:
app: cert-manager
spec:
ports:
- name: metrics
port: 9402
targetPort: 9402
selector:
app: cert-manager
ServiceMonitor for Prometheus Operator:
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: cert-manager
namespace: cert-manager
spec:
selector:
matchLabels:
app: cert-manager
endpoints:
- port: metrics
interval: 30s
Grafana Dashboard
Import dashboard with ID: 11001 (cert-manager overview)
Key metrics:
certmanager_certificate_expiration_timestamp_seconds- expiration datecertmanager_certificate_ready_status- readiness statuscertmanager_http_acme_client_request_count- ACME requestscertmanager_controller_sync_call_count- sync calls
Alerting
Example Prometheus alert:
groups:
- name: cert-manager
rules:
- alert: CertificateExpiryIn7Days
expr: (certmanager_certificate_expiration_timestamp_seconds - time()) / 86400 < 7
for: 1h
labels:
severity: warning
annotations:
summary: "Certificate {{ $labels.name }} expires in less than 7 days"
- alert: CertificateNotReady
expr: certmanager_certificate_ready_status == 0
for: 10m
labels:
severity: critical
annotations:
summary: "Certificate {{ $labels.name }} is not ready"
Advanced Configurations
Private CA (Private Certificate Authority)
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: private-ca
spec:
ca:
secretName: ca-key-pair
Creating CA:
# Generate CA key and certificate
openssl genrsa -out ca.key 4096
openssl req -new -x509 -key ca.key -out ca.crt -days 3650
# Create Secret
kubectl create secret tls ca-key-pair \
--cert=ca.crt \
--key=ca.key \
--namespace=cert-manager
HashiCorp Vault Integration
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: vault-issuer
spec:
vault:
server: https://vault.example.com
path: pki/sign/example-dot-com
auth:
kubernetes:
mountPath: /v1/auth/kubernetes
role: cert-manager
secretRef:
name: cert-manager-vault-token
key: token
Certificate Policies
Enforcing organizational standards:
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: myapp-cert
spec:
secretName: myapp-tls
issuerRef:
name: letsencrypt-prod
kind: ClusterIssuer
dnsNames:
- myapp.example.com
# Minimum key length
privateKey:
algorithm: RSA
size: 4096
# Additional extensions
usages:
- digital signature
- key encipherment
- server auth
- client auth
Best Practices
1. Use Namespace for cert-manager
# Everything in dedicated namespace
kubectl create namespace cert-manager
2. Resource Limits
resources:
limits:
cpu: 100m
memory: 128Mi
requests:
cpu: 50m
memory: 64Mi
3. Backup Secrets
# Backup certificates
kubectl get secrets -n default -o yaml > certificates-backup.yaml
# Or use Velero for automatic backups
4. Staging before Production
Always test on Let’s Encrypt staging before production.
5. Monitoring and Alerting
Monitor:
- Expiration dates
- Certificate status
- Renewal errors
6. Documentation
Maintain documentation:
- List of used issuers
- Manual renewal procedures (in case of failure)
- Contacts and escalation
Migration from Existing Certificates
Import Existing Certificate
# Create Secret with existing certificate
kubectl create secret tls existing-cert \
--cert=path/to/cert.crt \
--key=path/to/cert.key
# Create Certificate managed by cert-manager
kubectl apply -f - <<EOF
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: existing-cert
spec:
secretName: existing-cert
issuerRef:
name: letsencrypt-prod
kind: ClusterIssuer
dnsNames:
- example.com
EOF
Gradual Migration
- Install cert-manager alongside existing solution
- Configure ClusterIssuers
- Test on staging environment
- Migrate individual applications
- Monitor and validate
- Remove old solution
Case Study: Migrating 50+ Applications
Challenge: Company had 50+ applications in Kubernetes, each with manually managed certificates.
Solution:
- cert-manager installation
- ClusterIssuers configuration (prod + staging)
- Migration script for all Ingress
- Automatic annotation addition
- Grafana monitoring
Results:
- 95% reduction in certificate management time
- 0 incidents with expired certificates in a year
- Unified approach to SSL/TLS
- Full renewal automation
Migration script code:
#!/bin/bash
# migrate-ingresses.sh
for ingress in $(kubectl get ingress -A -o json | jq -r '.items[] | "\(.metadata.namespace)/\(.metadata.name)"'); do
namespace=$(echo $ingress | cut -d/ -f1)
name=$(echo $ingress | cut -d/ -f2)
echo "Migrating $namespace/$name"
kubectl annotate ingress $name -n $namespace \
cert-manager.io/cluster-issuer=letsencrypt-prod \
--overwrite
echo "Done: $namespace/$name"
done
Summary
cert-manager is essential infrastructure for any Kubernetes cluster handling HTTPS traffic. Once it’s running, the mental overhead of certificate management drops to near zero — renewals happen automatically, failures surface in Prometheus, and your Ingress annotations become self-documenting. The 90%+ reduction in ops work is real; teams that make this switch genuinely don’t think about certificates anymore.
The path forward: start with Helm installation, configure your ClusterIssuers (staging first, always), annotate your Ingress resources, and wire up the Prometheus metrics to your existing monitoring stack. Document the configuration in your runbook so the next engineer doesn’t have to reverse-engineer it.
And for anything cert-manager doesn’t cover — legacy systems, third-party services, APIs you depend on but don’t control — CrtMgr handles the external monitoring side. Between the two tools, you have full coverage.
Automation is key. With cert-manager and proper monitoring, you’ll never lose sleep over expired certificates again.