Install cert-manager and Issue TLS Certificates Automatically
Install cert-manager with Helm, configure a Let's Encrypt ClusterIssuer with HTTP-01 validation, and issue your first automatically-renewed TLS certificate — so you never deal with certificate expiry again.
Before you begin
- A running Kubernetes cluster
- kubectl configured with cluster-admin access
- Helm 3 installed
- An nginx-ingress controller (for HTTP-01 challenge)
- A domain you control with DNS pointing to your ingress
Managing TLS certificates manually — generating CSRs, submitting to a CA, downloading PEM files, creating Secrets, tracking expiry dates — is the kind of toil that adds up. cert-manager eliminates all of it. You declare a Certificate resource, cert-manager handles the ACME challenge with Let's Encrypt, provisions the certificate into a Kubernetes Secret, and renews it 30 days before it expires.
This tutorial installs cert-manager, configures both staging and production Let's Encrypt issuers, and issues a real certificate for a domain you control.
Step 1: Add the Jetstack Helm repository
cert-manager is published by Jetstack (now part of Venafi):
helm repo add jetstack https://charts.jetstack.io
helm repo updateStep 2: Create the cert-manager namespace
kubectl create namespace cert-managerStep 3: Install cert-manager
Pin a specific version to avoid unexpected upgrades. The crds.enabled=true flag installs the CRDs (Certificate, Issuer, ClusterIssuer, CertificateRequest) as part of the Helm release:
helm install cert-manager jetstack/cert-manager \
--namespace cert-manager \
--version v1.20.2 \
--set crds.enabled=trueCheck the cert-manager releases page for the latest version before running this. The crds.enabled flag was renamed from installCRDs in v1.12 — older docs may show --set installCRDs=true.
The OCI registry (oci://quay.io/jetstack/charts/cert-manager) is the canonical source of truth for cert-manager charts. The helm repo add method is a delayed mirror and both work for standard installs.
Step 4: Verify all pods are running
Three pods must reach Running status: the controller, the webhook, and the cainjector:
kubectl get pods -n cert-managerExpected output:
NAME READY STATUS RESTARTS
cert-manager-7b4d4b7d8c-x8p2q 1/1 Running 0
cert-manager-cainjector-6b9d7b8d8c-r4n7l 1/1 Running 0
cert-manager-webhook-5c9b9b9b8c-k2j9m 1/1 Running 0
Do not proceed until all three are Running. The webhook must be healthy before issuers will work.
Step 5: Create a staging ClusterIssuer
Always start with Let's Encrypt staging. The staging CA issues untrusted certificates (your browser will warn). Staging has very high rate limits designed for testing — far higher than production — so you can iterate freely without exhausting the 50 certificates/domain/week production limit. (Note: staging is not truly unlimited; it just has much higher thresholds.)
Replace ops@example.com with your email address:
1cat <<EOF | kubectl apply -f -
2apiVersion: cert-manager.io/v1
3kind: ClusterIssuer
4metadata:
5 name: letsencrypt-staging
6spec:
7 acme:
8 server: https://acme-staging-v02.api.letsencrypt.org/directory
9 email: ops@example.com
10 privateKeySecretRef:
11 name: letsencrypt-staging-key
12 solvers:
13 - http01:
14 ingress:
15 ingressClassName: nginx
16EOFCheck the issuer is ready:
kubectl describe clusterissuer letsencrypt-stagingYou should see Status: True, Type: Ready, and Reason: ACMEAccountRegistered in the conditions. If you see Reason: ErrInitIssuer, check that your email is valid and the ACME server is reachable.
Step 6: Test with a staging certificate
Request a certificate for a domain you control. cert-manager will create an HTTP-01 challenge pod and Ingress, wait for Let's Encrypt to validate it, then provision the certificate into my-app-staging-tls:
1cat <<EOF | kubectl apply -f -
2apiVersion: cert-manager.io/v1
3kind: Certificate
4metadata:
5 name: my-app-staging
6 namespace: default
7spec:
8 secretName: my-app-staging-tls
9 issuerRef:
10 name: letsencrypt-staging
11 kind: ClusterIssuer
12 dnsNames:
13 - my-app.example.com
14EOFWatch the certificate get issued:
kubectl describe certificate my-app-staging -n defaultWait for:
Status:
Conditions:
- Type: Ready
Status: "True"
Message: Certificate is up to date and has not expired
This typically takes 30–90 seconds. If it stalls, check kubectl describe certificaterequest -n default and kubectl describe challenge -n default for error messages.
Step 7: Create the production ClusterIssuer
Once staging works end-to-end, create the production issuer:
1cat <<EOF | kubectl apply -f -
2apiVersion: cert-manager.io/v1
3kind: ClusterIssuer
4metadata:
5 name: letsencrypt-prod
6spec:
7 acme:
8 server: https://acme-v02.api.letsencrypt.org/directory
9 email: ops@example.com
10 privateKeySecretRef:
11 name: letsencrypt-prod-key
12 solvers:
13 - http01:
14 ingress:
15 ingressClassName: nginx
16EOFStep 8: Issue a production certificate
Switch the issuerRef to letsencrypt-prod:
1cat <<EOF | kubectl apply -f -
2apiVersion: cert-manager.io/v1
3kind: Certificate
4metadata:
5 name: my-app-tls
6 namespace: default
7spec:
8 secretName: my-app-tls
9 issuerRef:
10 name: letsencrypt-prod
11 kind: ClusterIssuer
12 dnsNames:
13 - my-app.example.com
14EOFStep 9: Use the certificate in an Ingress
Reference the Secret in your Ingress resource. The cert-manager.io/cluster-issuer annotation tells cert-manager to manage this certificate automatically — you don't even need to create a Certificate resource separately:
1apiVersion: networking.k8s.io/v1
2kind: Ingress
3metadata:
4 name: my-app
5 namespace: default
6 annotations:
7 cert-manager.io/cluster-issuer: letsencrypt-prod
8spec:
9 ingressClassName: nginx
10 tls:
11 - hosts:
12 - my-app.example.com
13 secretName: my-app-tls
14 rules:
15 - host: my-app.example.com
16 http:
17 paths:
18 - path: /
19 pathType: Prefix
20 backend:
21 service:
22 name: my-app
23 port:
24 number: 80When you apply this Ingress, cert-manager detects the annotation and automatically creates the Certificate resource, requests it from Let's Encrypt, and keeps it renewed.
What you built
cert-manager is watching your cluster for Certificate and Ingress resources. New certificates are issued within minutes of creation. Renewal happens automatically at 2/3 through each certificate's lifetime — for a standard 90-day Let's Encrypt certificate that is 30 days before expiry. You can override this with spec.renewBefore on the Certificate resource. No cron jobs, no manual certificate management, no expiry surprises. The annotation-based workflow on Ingress means any engineer adding a new service gets TLS automatically without knowing anything about cert-manager.
We built Podscape to simplify Kubernetes workflows like this — logs, events, and cluster state in one interface, without switching tools.
Struggling with this in production?
We help teams fix these exact issues. Our engineers have deployed these patterns across production environments at scale.