Lesson 02 of 15 · Identity & Access
cert-manager in production
Automate TLS certificates inside Kubernetes with cert-manager: issuers for Let's Encrypt and your own private CA, Certificate resources, automatic renewal, and monitoring expiry before it bites.
Why cert-manager
Ingresses, Gateways, webhooks, internal mTLS: a platform has many certificates, each with an expiry date. cert-manager turns certificates into Kubernetes resources: you declare what you need, and it issues, stores (as a Secret) and renews them automatically.
Instead of queuing at the office every year to renew everyone's ID card, you hire an assistant (cert-manager) with a list: "Asha's card, Ben's card, the library door card". The assistant fetches each card, puts it in the right locker, and renews it a month before it expires, without anybody asking.
The objects
| Object | Meaning |
|---|---|
Issuer / ClusterIssuer |
Where certificates come from: Let's Encrypt (ACME), your CA, Vault, self-signed |
Certificate |
What you want: names, duration, which issuer, which Secret to store it in |
CertificateRequest, Order, Challenge |
Internal steps you inspect when something is stuck |
Install
$ helm repo add jetstack https://charts.jetstack.io && helm repo update
$ helm install cert-manager jetstack/cert-manager \
--namespace cert-manager --create-namespace --set crds.enabled=true
$ kubectl -n cert-manager get pods
(Older chart versions used --set installCRDs=true. Check the chart's documentation for your version.)
A private CA for internal services
Bootstrap a CA inside the cluster: a self-signed issuer creates a CA certificate, and a CA issuer signs everything else with it.
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: selfsigned-bootstrap
spec:
selfSigned: {}
---
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: platform-ca
namespace: cert-manager
spec:
isCA: true
commonName: platform-internal-ca
secretName: platform-ca
duration: 87600h # 10 years
privateKey:
algorithm: ECDSA
size: 256
issuerRef:
name: selfsigned-bootstrap
kind: ClusterIssuer
---
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: platform-ca
spec:
ca:
secretName: platform-ca # read from the cert-manager namespace for ClusterIssuers
Now any team can request a certificate:
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: orders-api-tls
namespace: shop
spec:
secretName: orders-api-tls
dnsNames:
- orders-api.shop.svc
- orders-api.shop.svc.cluster.local
duration: 2160h # 90 days
renewBefore: 360h # renew 15 days before expiry
issuerRef:
name: platform-ca
kind: ClusterIssuer
$ kubectl -n shop get certificate orders-api-tls
NAME READY SECRET AGE
orders-api-tls True orders-api-tls 12s
$ kubectl -n shop get secret orders-api-tls -o jsonpath='{.data.tls\.crt}' | base64 -d | openssl x509 -noout -subject -enddate
Public certificates with Let's Encrypt (ACME)
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-staging
spec:
acme:
server: https://acme-staging-v02.api.letsencrypt.org/directory
email: platform-team@example.com
privateKeySecretRef:
name: letsencrypt-staging-account
solvers:
- http01:
ingress:
ingressClassName: traefik # the class of your ingress controller
- HTTP-01: Let's Encrypt fetches a token over HTTP from your domain. Simple, but needs the domain publicly reachable on port 80, and doesn't do wildcards.
- DNS-01: cert-manager creates a TXT record through your DNS provider's API. Works for private services and wildcards.
With an issuer in place, annotate an Ingress and cert-manager creates the Certificate for you:
metadata:
annotations:
cert-manager.io/cluster-issuer: letsencrypt-staging
spec:
tls:
- hosts: [ "shop.example.com" ]
secretName: shop-example-com-tls
Switch to https://acme-v02.api.letsencrypt.org/directory (production) once the staging flow works.
Try it: a private CA on kind
- Install cert-manager with Helm.
- Apply the three private-CA objects above and check
kubectl get clusterissuersshowsREADY True. - Request
orders-api-tlsin a namespace, then decode the Secret and check the SANs and expiry with openssl. - Delete the Secret and watch cert-manager re-issue it within seconds.
- Set
duration: 1handrenewBefore: 30mon a test certificate and watchkubectl get certificate -wrenew it (look at thenotAfterfield inkubectl describe).
Going deeper: certificates at platform scale
- Monitor with cert-manager's Prometheus metrics (
certmanager_certificate_expiration_timestamp_seconds,certmanager_certificate_ready_status) and alert on certificates not Ready or expiring within 14 days. - Trust distribution: trust-manager (a cert-manager project) distributes CA bundles to namespaces as ConfigMaps, so apps can trust your internal CA.
- For a real private PKI, keep the root CA offline and give cert-manager an intermediate (or use Vault/a cloud private CA as the issuer).
- Stuck issuance? Follow the chain:
Certificate→CertificateRequest→Order→Challenge. The first one with a failing condition tells you why.
Recap
- cert-manager makes certificates declarative: Issuers/ClusterIssuers + Certificates → Secrets, renewed automatically.
- Private CA: self-signed bootstrap → CA certificate → CA ClusterIssuer.
- ACME: HTTP-01 (simple) or DNS-01 (private and wildcard); staging first.
- Monitor readiness and expiry; debug along Certificate → CertificateRequest → Order → Challenge.
This site is a public version of my personal engineering knowledge hub. It intentionally excludes confidential company information and internal operational details.