A TLS certificate is not just a lock icon; it’s an active, dynamic negotiation between two parties, and the challenge is keeping that negotiation fresh and secure without manual intervention.

Let’s watch cert-manager handle this. Imagine we have a simple Nginx deployment that needs TLS.

# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-app
spec:
  replicas: 1
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:latest
        ports:
        - containerPort: 80
        # This is where the magic will happen: we'll mount the TLS certs
        volumeMounts:
        - name: tls-certs
          mountPath: "/etc/nginx/ssl"
          readOnly: true
      volumes:
      - name: tls-certs
        secret:
          secretName: nginx-tls # This is the secret cert-manager will create

And the Nginx configuration to use those certs:

# nginx.conf (simplified for example)
server {
    listen 443 ssl;
    server_name localhost;

    ssl_certificate /etc/nginx/ssl/tls.crt;
    ssl_certificate_key /etc/nginx/ssl/tls.key;

    location / {
        root /usr/share/nginx/html;
        index index.html index.htm;
    }
}

Now, how do we tell cert-manager to get us a certificate for nginx-app and put it in that nginx-tls secret? We use an Issuer or ClusterIssuer (for cluster-wide scope) and a Certificate resource.

# issuer.yaml
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@example.com
    privateKeySecretRef:
      name: letsencrypt-staging-private-key
    solvers:
    - http01:
        ingress:
          class: nginx # Assuming you have an ingress controller

This ClusterIssuer is configured to talk to Let’s Encrypt’s staging environment (so we don’t burn through rate limits) using the ACME protocol. It will use the HTTP01 challenge, which requires an ingress controller to respond to challenges.

# certificate.yaml
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: nginx-tls-cert
  namespace: default # Must match the namespace of the deployment
spec:
  secretName: nginx-tls # This is the secret name referenced in the deployment
  dnsNames:
  - nginx.example.com # The actual domain name(s) the cert should be valid for
  issuerRef:
    name: letsencrypt-staging
    kind: ClusterIssuer

This Certificate resource tells cert-manager: "I need a certificate for nginx.example.com, and I want you to store it in a secret named nginx-tls in the default namespace. Use the letsencrypt-staging ClusterIssuer to get it."

Once you apply these YAMLs (kubectl apply -f issuer.yaml -f certificate.yaml), cert-manager will:

  1. See the Certificate resource.
  2. Consult the specified ClusterIssuer.
  3. Initiate an ACME challenge with the chosen issuer.
  4. If the challenge passes, it obtains the certificate.
  5. It then creates or updates the nginx-tls Secret in the default namespace with tls.crt and tls.key files.
  6. Your Nginx deployment, mounted to /etc/nginx/ssl, will automatically pick up these new files.

The real power is that this isn’t a one-time event. When the certificate approaches expiration (typically 30 days before), cert-manager will automatically renew it and update the nginx-tls secret. Your application never needs to know about the certificate lifecycle.

The most surprising true thing about this setup is that your application never directly interacts with the certificate authority. It only ever sees a Kubernetes Secret, and the rotation mechanism is entirely managed by cert-manager and the Certificate resource’s desired state.

Consider the CertificateRequest resource. When cert-manager needs to get a certificate, it doesn’t directly ask the issuer. Instead, it creates a CertificateRequest object. This object contains the desired certificate signing request (CSR) and the details of the issuer to use. The actual issuer (like an ACME solver or a webhook) then processes this CertificateRequest, fulfills the challenge, and creates a Certificate resource upon success. This separation of concerns allows for modularity and extensibility, enabling custom issuers or complex approval workflows before a certificate is actually issued.

The next concept you’ll want to wrap your head around is how to manage different types of challenges (besides HTTP01), like DNS01, and how to handle private CAs for internal services.

Want structured learning?

Take the full Helm course →