Security

Install cert-manager and Issue TLS Certificates Automatically

Intermediate20 min to complete14 min read

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
cert-manager
TLS
Kubernetes
Let's Encrypt
Helm
Security

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):

bash
helm repo add jetstack https://charts.jetstack.io
helm repo update

Step 2: Create the cert-manager namespace

bash
kubectl create namespace cert-manager

Step 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:

bash
helm install cert-manager jetstack/cert-manager \
  --namespace cert-manager \
  --version v1.20.2 \
  --set crds.enabled=true

Check 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:

bash
kubectl get pods -n cert-manager

Expected 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:

bash
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
16EOF

Check the issuer is ready:

bash
kubectl describe clusterissuer letsencrypt-staging

You 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:

bash
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
14EOF

Watch the certificate get issued:

bash
kubectl describe certificate my-app-staging -n default

Wait 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:

bash
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
16EOF

Step 8: Issue a production certificate

Switch the issuerRef to letsencrypt-prod:

bash
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
14EOF

Step 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:

yaml
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: 80

When 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.