CertManager is a tool that automatically creates, manages, and renews SSL/TLS certificates for your websites and services running inside a Kubernetes cluster. Think of it as a smart helper that keeps your padlock (the green lock icon in browsers) always up to date, so you never have to worry about certificates expiring.
???? Why does this matter? Without CertManager: You must manually create, install, and renew every certificate yourself. If you forget, users see scary 'Not Secure' warnings. With CertManager: Everything is automatic. Certificates are renewed 30 days before they expire — no human action needed. |
? Before You Start Make sure you have completed the CertManager Installation Guide before following this deployment guide. You will need access to your Kubernetes cluster via You will also need a Cloudflare account (or another DNS provider) with API access. |
There are two ways to create a certificate using CertManager. Choose the method that best fits your situation:
| Method | When to Use It |
|---|---|
| Method 1 — Certificate Resource | You want full control and specify every detail yourself (recommended for most users). |
| Method 2 — Ingress Annotation | You want CertManager to do everything automatically when you set up a website route. |
In this method you write a small configuration file that tells CertManager exactly what kind of certificate you need. CertManager then goes and fetches it for you.
Use this when you only need a certificate for one domain, for example: example.com.
Step 1 — Create the certificate file
Open a text editor and create a new file called certificate.yaml. Paste the following content, replacing the values shown in CAPITALS with your own details:
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: example-com-cert # A name for this certificate (you choose)
namespace: default # The Kubernetes namespace to use
spec:
secretName: example-com-tls # Where the certificate will be stored
duration: 2160h # Certificate is valid for 90 days
renewBefore: 720h # Renew it 30 days before it expires
issuerRef:
name: custom-acme-issuer # Name of your ClusterIssuer
kind: ClusterIssuer
group: cert-manager.io
dnsNames:
- example.com # YOUR domain name
commonName: example.com # Same as above
privateKey:
algorithm: RSA
size: 2048
rotationPolicy: Always # Creates a new key every renewal
???? What do these settings mean?
|
Step 2 — Apply (submit) the file to your cluster
Open your terminal and run this command:
kubectl apply -f certificate.yaml
Step 3 — Watch the certificate being created
Run this command to see live status updates:
kubectl get certificate example-com-cert -w
You will see output like this:
NAME READY SECRET AGE
example-com-cert False example-com-tls 5s ? Still working...
example-com-cert True example-com-tls 45s ? Done! Certificate is ready.
? What to expect
The process usually takes between 30 seconds and 2 minutes. |
Step 4 — Verify the certificate details
kubectl describe certificate example-com-cert
Step 5 — View the stored certificate secret
kubectl get secret example-com-tls -o yaml
Step 6 — Extract the certificate files (optional)
If you need to download the actual certificate files to your computer, run these commands one by one:
# Save the certificate
kubectl get secret example-com-tls -o jsonpath='{.data.tls\.crt}' | base64 -d > cert.pem
# Save the private key
kubectl get secret example-com-tls -o jsonpath='{.data.tls\.key}' | base64 -d > key.pem
# Save the CA certificate
kubectl get secret example-com-tls -o jsonpath='{.data.ca\.crt}' | base64 -d > ca.pem
# Verify the certificate looks correct
openssl x509 -in cert.pem -text -noout
Use this when you want one certificate that covers several different domain names, for example: example.com, www.example.com, mail.example.com.
Create a file called certificate-multi.yaml with the following content:
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: example-multi-cert
namespace: default
spec:
secretName: example-multi-tls
duration: 2160h
renewBefore: 720h
issuerRef:
name: custom-acme-issuer
kind: ClusterIssuer
dnsNames:
- example.com
- www.example.com
- mail.example.com
- api.example.com
kubectl apply -f certificate-multi.yaml
A wildcard certificate covers ALL subdomains of your domain at once. For example, *.example.com covers www.example.com, api.example.com, app.example.com — and any other subdomain you might create in the future.
? Important Note Wildcard certificates require DNS-01 challenge validation, which means your DNS provider (e.g. Cloudflare) must already be configured in CertManager. Make sure your ClusterIssuer is set up correctly before proceeding. |
Create a file called certificate-wildcard.yaml:
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: wildcard-example-cert
namespace: default
spec:
secretName: wildcard-example-tls
duration: 2160h
renewBefore: 720h
issuerRef:
name: custom-acme-issuer
kind: ClusterIssuer
dnsNames:
- "*.example.com" # Covers ALL subdomains
- example.com # Also covers the main domain
kubectl apply -f certificate-wildcard.yaml
This is the easiest method! You simply add a special label (called an 'annotation') to your website's routing configuration (called an 'Ingress'). CertManager reads this label and automatically creates and manages the certificate for you.
Create a file called ingress.yaml. The key line is the annotation: cert-manager.io/cluster-issuer.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: example-ingress
namespace: default
annotations:
# This line tells CertManager to create a certificate automatically
cert-manager.io/cluster-issuer: custom-acme-issuer
cert-manager.io/duration: '2160h' # 90 days validity
cert-manager.io/renew-before: '720h' # Renew 30 days early
kubernetes.io/ingress.class: nginx # Your ingress controller
spec:
tls:
- hosts:
- example.com
- www.example.com
secretName: example-ingress-tls # CertManager creates this for you
rules:
- host: example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: example-service
port:
number: 80
- host: www.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: example-service
port:
number: 80
kubectl apply -f ingress.yaml
Once you apply this file, CertManager will automatically do all of the following for you:
# Check that CertManager created a certificate
kubectl get certificate
# Check that the secret was created
kubectl get secret example-ingress-tls
One of the biggest advantages of CertManager is that certificates are renewed automatically. You do not need to do anything. However, this section explains how renewal works and how to force a renewal if you ever need to.
CertManager watches all your certificates 24/7. When a certificate is 30 days away from expiry, CertManager automatically starts the renewal process. The process is identical to the original issuance — it happens in the background without any downtime.
???? Default Renewal Timeline Certificate valid for: 90 days Renewal starts: 30 days before expiry Your action required: NONE — fully automatic! |
# Watch all certificates across the cluster
kubectl get certificate -A -w
# Check details and history of a specific certificate
kubectl describe certificate example-com-cert
# Read CertManager's renewal log messages
kubectl logs -n cert-manager -l app=cert-manager -f | grep 'renewal'
Sometimes you may need to renew a certificate right now — for example, if it was compromised or you updated your domain names. There are three ways to do this:
When you delete the secret, CertManager detects it is missing and immediately creates a new one:
kubectl delete secret example-com-tls
???? Note CertManager will recreate the secret automatically within a few seconds. Your service may briefly use no certificate during this short window. |
This tells CertManager to issue a temporary certificate while the real one renews:
kubectl annotate certificate example-com-cert \
cert-manager.io/issue-temporary-certificate='true' --overwrite
This is the most thorough approach — it completely resets the certificate:
kubectl delete certificate example-com-cert
kubectl apply -f certificate.yaml
If 30 days is not long enough for your needs, you can customise when renewal starts. The example below renews 60 days before expiry instead:
spec:
duration: 2160h # 90-day certificate
renewBefore: 1440h # Start renewing 60 days before expiry
Sometimes DNS changes take a long time to spread across the internet. If CertManager times out while waiting for DNS, you can increase the wait time.
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: long-propagation-cert
namespace: default
annotations:
cert-manager.io/issue-temporary-certificate: 'false'
spec:
secretName: long-propagation-tls
duration: 2160h
renewBefore: 720h
issuerRef:
name: custom-acme-issuer
kind: ClusterIssuer
dnsNames:
- slow-dns.example.com
Open the CertManager deployment for editing:
kubectl edit deployment cert-manager -n cert-manager
Find the 'args' section inside the container spec and add these lines:
args:
- --v=2
- --dns01-check-retry-period=60s # Check DNS every 60s (default was 10s)
- --max-concurrent-challenges=10 # Process 10 challenges at the same time
- --dns01-recursive-nameservers=8.8.8.8:53,1.1.1.1:53 # Use Google & Cloudflare DNS
- --dns01-recursive-nameservers-only=true
kubectl rollout restart deployment cert-manager -n cert-manager
kubectl rollout status deployment cert-manager -n cert-manager
If your domains are spread across different DNS providers (e.g. some on Cloudflare, some on AWS Route53), you can configure a single ClusterIssuer to handle them all automatically using 'solvers'.
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: multi-provider-issuer
spec:
acme:
server: https://www.freessl.in/acme-new/acme/directory
email: admin@example.com
privateKeySecretRef:
name: custom-acme-account-key
externalAccountBinding:
keyID: YOUR_EAB_KEY_ID
keySecretRef:
name: acme-eab-credentials
key: eab-hmac-key
keyAlgorithm: HS256
solvers:
# Use Cloudflare for .com domains
- dns01:
cloudflare:
apiTokenSecretRef:
name: cloudflare-api-token
key: api-token
selector:
dnsZones:
- 'example.com'
- '*.example.com'
# Use AWS Route53 for .net domains
- dns01:
route53:
region: us-east-1
accessKeyID: AKIAXXXXXXXXXXXXXXXX
secretAccessKeySecretRef:
name: route53-credentials
key: secret-access-key
selector:
dnsZones:
- 'example.net'
- '*.example.net'
CertManager uses Cloudflare to prove you own your domain by creating and deleting special DNS records automatically. This section explains how to set it up securely.
For security, always create a token with the minimum permissions needed. Follow these steps in Cloudflare:
| Setting | Value |
|---|---|
| Permissions | Zone ? DNS ? Edit |
| Zone Resources | Include ? Specific zone ? example.com |
| Client IP Filtering | (Optional) Add your Kubernetes cluster IP addresses |
| TTL | (Optional) Set an expiration date for the token |
Never put your API token directly in a YAML file. Store it as a Kubernetes secret:
kubectl create secret generic cloudflare-api-token-scoped \
--namespace=cert-manager \
--from-literal=api-token='YOUR_SCOPED_TOKEN_HERE'
If you manage multiple domains on different Cloudflare accounts, create a separate secret for each:
# Secret for first domain/account
kubectl create secret generic cloudflare-zone1 \
--namespace=cert-manager \
--from-literal=api-token='TOKEN_FOR_ZONE1'
# Secret for second domain/account
kubectl create secret generic cloudflare-zone2 \
--namespace=cert-manager \
--from-literal=api-token='TOKEN_FOR_ZONE2'
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: cloudflare-multi-zone
spec:
acme:
server: https://www.freessl.in/acme-new/acme/directory
email: admin@example.com
privateKeySecretRef:
name: custom-acme-account-key
externalAccountBinding:
keyID: YOUR_EAB_KEY_ID
keySecretRef:
name: acme-eab-credentials
key: eab-hmac-key
keyAlgorithm: HS256
solvers:
- dns01:
cloudflare:
apiTokenSecretRef:
name: cloudflare-zone1
key: api-token
selector:
dnsZones:
- 'domain1.com'
- dns01:
cloudflare:
apiTokenSecretRef:
name: cloudflare-zone2
key: api-token
selector:
dnsZones:
- 'domain2.com'
? About Cloudflare Proxy (Orange Cloud) Good news: You do NOT need to change any Cloudflare proxy settings. CertManager automatically creates a special TXT record called This record is always created as DNS-only (grey cloud), regardless of your other settings. Once domain ownership is verified, CertManager deletes the TXT record automatically. |
These are recommended, real-world patterns used in production environments. Choose the pattern that matches your infrastructure needs.
Use this pattern when multiple teams share the same Kubernetes cluster and each team needs their own isolated certificate issuer.
???? When to use this You have multiple tenants (teams, customers) in one cluster and want each team to have separate API keys, separate DNS access, and separate billing. |
# Issuer lives in tenant-a namespace (not cluster-wide)
apiVersion: cert-manager.io/v1
kind: Issuer
metadata:
name: tenant-a-issuer
namespace: tenant-a
spec:
acme:
server: https://www.freessl.in/acme-new/acme/directory
email: tenant-a@example.com
privateKeySecretRef:
name: tenant-a-acme-key
externalAccountBinding:
keyID: TENANT_A_EAB_KEY
keySecretRef:
name: tenant-a-eab
key: eab-hmac-key
keyAlgorithm: HS256
solvers:
- dns01:
cloudflare:
apiTokenSecretRef:
name: tenant-a-cf-token
key: api-token
---
# Certificate for tenant A, using their scoped Issuer
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: tenant-a-cert
namespace: tenant-a
spec:
secretName: tenant-a-tls
issuerRef:
name: tenant-a-issuer
kind: Issuer # 'Issuer' not 'ClusterIssuer'
dnsNames:
- tenant-a.example.com
This is the recommended pattern for individual applications. Each app gets its own certificate, and the Ingress controller handles everything automatically.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: app1-ingress
namespace: production
annotations:
cert-manager.io/cluster-issuer: custom-acme-issuer
nginx.ingress.kubernetes.io/force-ssl-redirect: 'true' # Always use HTTPS
spec:
tls:
- hosts:
- app1.example.com
secretName: app1-tls
rules:
- host: app1.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: app1-service
port:
number: 80
Instead of creating a certificate for each app, create one wildcard certificate and share it across all your namespaces. This saves on certificate requests and simplifies management.
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: wildcard-shared
namespace: cert-manager
spec:
secretName: wildcard-shared-tls
duration: 2160h
renewBefore: 720h
issuerRef:
name: custom-acme-issuer
kind: ClusterIssuer
dnsNames:
- "*.example.com"
- example.com
Reflector is a small helper tool that automatically copies secrets to other namespaces. Install it with Helm:
helm repo add emberstack https://emberstack.github.io/helm-charts
helm install reflector emberstack/reflector --namespace kube-system
Tell Reflector which namespaces are allowed to use this certificate:
kubectl annotate secret wildcard-shared-tls \
-n cert-manager \
reflector.v1.k8s.emberstack.com/reflection-allowed='true' \
reflector.v1.k8s.emberstack.com/reflection-allowed-namespaces='prod,staging,dev'
Once Reflector copies the secret, any namespace can use it in an Ingress:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: app-ingress
namespace: prod
spec:
tls:
- hosts:
- app.example.com
secretName: wildcard-shared-tls # Auto-replicated by Reflector
rules:
- host: app.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: app-service
port:
number: 80
It is important to know when a certificate is about to expire, especially in case the automatic renewal fails for any reason. This section shows you how to set up monitoring.
Run these commands anytime to see the health of all your certificates:
# List all certificates across all namespaces
kubectl get certificate -A
# Show certificates sorted by expiry date (closest first)
kubectl get certificate -A -o json | \
jq -r '.items[] | select(.status.notAfter != null) |
[.metadata.namespace, .metadata.name, .status.notAfter] | @tsv' | \
sort -k3
CertManager exposes its own metrics that Prometheus can scrape. This lets you build dashboards and automated alerts.
| Component | Metrics URL |
|---|---|
| CertManager Controller | http://cert-manager:9402/metrics |
| CertManager Webhook | http://cert-manager-webhook:9402/metrics |
| Metric Name | What It Tells You |
|---|---|
certmanager_certificate_expiration_timestamp_seconds | When each certificate expires |
certmanager_certificate_ready_status | Whether each certificate is healthy (1 = good, 0 = problem) |
certmanager_http_acme_challenge_total | How many ACME challenges have been processed |
certmanager_controller_sync_call_count | How active the controller is |
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: cert-manager
namespace: cert-manager
spec:
selector:
matchLabels:
app: cert-manager
endpoints:
- port: tcp-prometheus-servicemonitor
interval: 60s
These alert rules will send a notification if a certificate is expiring soon or is not working:
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
name: cert-manager-alerts
namespace: cert-manager
spec:
groups:
- name: cert-manager
interval: 60s
rules:
# WARNING: Certificate expiring within 21 days
- alert: CertificateExpiryWarning
expr: (certmanager_certificate_expiration_timestamp_seconds - time()) < (21 * 24 * 3600)
for: 24h
labels:
severity: warning
annotations:
summary: 'Certificate {{ $labels.name }} expiring soon'
description: 'Certificate will expire in {{ $value | humanizeDuration }}'
# CRITICAL: Certificate not working for 10+ minutes
- alert: CertificateNotReady
expr: certmanager_certificate_ready_status == 0
for: 10m
labels:
severity: critical
annotations:
summary: 'Certificate {{ $labels.name }} not ready'
description: 'Certificate has been not ready for 10 minutes'
This section covers the most common problems and how to fix them. If a certificate is not working, follow the debugging steps in order.
kubectl describe certificate example-com-cert
Look for the 'Events' section at the bottom. It will usually tell you exactly what went wrong.
An 'Order' is CertManager's internal request to the ACME server:
# List all orders
kubectl get orders -A
# See details of a specific order
kubectl get order -n default
kubectl describe order <order-name> -n default
A 'Challenge' is CertManager's attempt to prove you own the domain:
# List all challenges
kubectl get challenges -A
# See details of a specific challenge
kubectl describe challenge <challenge-name> -n default
| Challenge Status | What It Means |
|---|---|
| pending | Challenge created, waiting for DNS record to appear |
| processing | DNS record found, waiting for ACME server to validate |
| valid | Challenge passed — certificate is being issued |
| invalid | Challe
|