cert-manager: Deep Dive
Why Automate TLS Certificates?
Section titled “Why Automate TLS Certificates?”Every service running over HTTPS needs a TLS certificate. In Kubernetes, that means someone has to create the certificate, load it into a Secret, mount it into the pod, and renew it before it expires. Multiply that by dozens of services and you have a full-time job nobody wants.
Manual certificate management breaks in predictable ways:
- Expiration surprises. A certificate expires on a Friday evening. Alerts fire. Someone scrambles to generate a new one while users hit browser warnings.
- Inconsistent processes. Team A uses OpenSSL scripts. Team B copies certs from a wiki page. Team C asks the security team and waits three days.
- Secrets sprawl. Certificates get pasted into ConfigMaps, baked into container images, or committed to git repos. Each copy is a potential leak.
- Rotation resistance. Short-lived certificates are best practice, but nobody rotates a 90-day cert by hand across 40 services every quarter.
cert-manager solves all of this. It runs as a controller inside your cluster, watches for Certificate custom resources, requests certs from a configured authority, stores them as Kubernetes Secrets, and renews them automatically before they expire. You declare what you want. cert-manager handles the rest.
The Trust Chain Model
Section titled “The Trust Chain Model”TLS certificates work on a chain of trust. A Certificate Authority (CA) signs certificates. Clients trust those certificates because they trust the CA. cert-manager models this chain using Issuers and Certificates.
This demo builds a four-layer trust chain:
Self-Signed ClusterIssuer (bootstraps the CA) | v CA Certificate (our own Certificate Authority) | v CA-backed ClusterIssuer (signs application certificates) | v Application Certificates (TLS certs your services actually use)Each layer exists for a specific reason.
Why not just self-sign everything?
Section titled “Why not just self-sign everything?”Self-signed certificates are not signed by any trusted authority. Each one is its own root of trust. Every client would need to explicitly trust every individual certificate. Add a new service, and you need to distribute trust for its cert to every consumer.
A CA fixes this. All certificates signed by the CA are trusted by anyone who trusts the CA root. Trust one thing, trust everything it signs. That is how TLS works in the real world, and that is what we model here.
Layer 1: Self-Signed ClusterIssuer
Section titled “Layer 1: Self-Signed ClusterIssuer”We face a chicken-and-egg problem. We need an issuer to create our CA certificate, but the CA does not exist yet. The self-signed issuer solves this. It can sign its own certificates without any external dependency.
apiVersion: cert-manager.io/v1kind: ClusterIssuermetadata: name: selfsigned-issuerspec: selfSigned: {}This issuer exists solely to bootstrap the CA. Application certificates should never reference it directly.
Layer 2: CA Certificate
Section titled “Layer 2: CA Certificate”The self-signed issuer creates a Certificate Authority. The isCA: true field is critical. It tells cert-manager (and the resulting X.509 certificate) that this certificate is allowed to sign other certificates.
apiVersion: cert-manager.io/v1kind: Certificatemetadata: name: demo-ca namespace: cert-managerspec: isCA: true commonName: demo-ca subject: organizations: - "Demo Corp" secretName: demo-ca-secret privateKey: algorithm: ECDSA size: 256 issuerRef: name: selfsigned-issuer kind: ClusterIssuer group: cert-manager.io duration: 87600h # 10 years renewBefore: 720h # 30 daysThe CA lives in the cert-manager namespace. Its private key is stored in a Secret named demo-ca-secret. This key signs every certificate downstream, so protecting this Secret matters. In production, consider restricting RBAC access to it or using an external CA where the key never enters the cluster at all.
Layer 3: CA-backed ClusterIssuer
Section titled “Layer 3: CA-backed ClusterIssuer”With the CA certificate stored in a Secret, we create a ClusterIssuer that uses it to sign certificates. This is the issuer your applications will reference.
apiVersion: cert-manager.io/v1kind: ClusterIssuermetadata: name: demo-ca-issuerspec: ca: secretName: demo-ca-secretThe secretName points to the Secret created by the CA Certificate in Layer 2. From this point forward, any Certificate resource that references demo-ca-issuer gets a cert signed by our CA. The self-signed issuer’s job is done.
Layer 4: Application Certificates
Section titled “Layer 4: Application Certificates”This is where the value lands. Teams request TLS certificates by creating Certificate resources. cert-manager does everything else.
cert-manager Custom Resource Definitions
Section titled “cert-manager Custom Resource Definitions”cert-manager extends the Kubernetes API with several CRDs. Two are essential to understand.
ClusterIssuer (and Issuer)
Section titled “ClusterIssuer (and Issuer)”A ClusterIssuer defines how certificates are signed. It is the configuration for a certificate authority.
ClusterIssuer vs Issuer: The only difference is scope. A ClusterIssuer is available cluster-wide, to any namespace. An Issuer is namespace-scoped. Use ClusterIssuer when a platform team manages certificate signing centrally. Use Issuer when teams need isolated, per-namespace signing authorities.
Key spec fields:
| Field | Description |
|---|---|
spec.selfSigned | Signs certificates with their own private key. Used for bootstrapping. |
spec.ca.secretName | Points to a Kubernetes Secret containing a CA cert and key. |
spec.acme | Configures ACME protocol (Let’s Encrypt). See ACME section below. |
spec.vault | Configures HashiCorp Vault PKI backend. |
spec.venafi | Configures Venafi Trust Protection Platform. |
A ClusterIssuer reports its readiness via a status condition. Check it with:
kubectl get clusterissuer demo-ca-issuer -o wideCertificate
Section titled “Certificate”A Certificate is a request for a TLS certificate. It declares what you want: the DNS names, the lifetime, the key algorithm, which issuer should sign it.
Here is the simple certificate from this demo:
apiVersion: cert-manager.io/v1kind: Certificatemetadata: name: myapp-tls namespace: demo-appsspec: secretName: myapp-tls-secret commonName: myapp.example.com dnsNames: - myapp.example.com - www.myapp.example.com issuerRef: name: demo-ca-issuer kind: ClusterIssuer group: cert-manager.io duration: 2160h # 90 days renewBefore: 360h # 15 days privateKey: algorithm: RSA size: 2048Key spec fields explained:
| Field | Description |
|---|---|
secretName | Name of the Kubernetes Secret where cert-manager stores the issued certificate. Created automatically. |
commonName | The CN field in the X.509 certificate. Most modern TLS implementations ignore this in favor of dnsNames, but some legacy systems still require it. |
dnsNames | Subject Alternative Names (SANs). These are the hostnames the certificate is valid for. This is what browsers and modern TLS clients actually check. |
issuerRef.name | Which Issuer or ClusterIssuer signs this certificate. |
issuerRef.kind | Either Issuer or ClusterIssuer. |
issuerRef.group | Always cert-manager.io. |
duration | How long the certificate is valid. Specified as a Go duration string (e.g., 2160h for 90 days). |
renewBefore | How long before expiry cert-manager should start renewal. 360h means renew 15 days before the cert expires. |
privateKey.algorithm | RSA or ECDSA. See Private Key Algorithms below. |
privateKey.size | Key size. For RSA: 2048 or 4096. For ECDSA: 256 or 384. |
isCA | When true, the issued certificate can sign other certificates. Used only for CA certificates, not application certs. |
subject.organizations | Sets the Organization (O) field in the X.509 subject. |
CertificateRequest (internal)
Section titled “CertificateRequest (internal)”When you create a Certificate, cert-manager creates a CertificateRequest behind the scenes. This is the actual signing request sent to the issuer. You rarely interact with CertificateRequests directly, but they are useful for debugging:
kubectl get certificaterequest -n demo-appsIf a Certificate is stuck in a non-ready state, the CertificateRequest often contains the error message explaining why.
How Ingress Annotations Trigger Automatic Certificate Creation
Section titled “How Ingress Annotations Trigger Automatic Certificate Creation”Creating a Certificate resource for every service works, but cert-manager offers something more convenient. Add a single annotation to an Ingress and cert-manager creates the Certificate automatically.
Here is the relevant portion of the Ingress from this demo:
apiVersion: networking.k8s.io/v1kind: Ingressmetadata: name: hello-app namespace: demo-apps annotations: cert-manager.io/cluster-issuer: "demo-ca-issuer"spec: ingressClassName: nginx tls: - hosts: - hello.example.com secretName: hello-app-tls rules: - host: hello.example.com http: paths: - path: / pathType: Prefix backend: service: name: hello-app port: number: 80What happens when you apply this
Section titled “What happens when you apply this”- cert-manager’s ingress-shim controller watches for Ingress resources with cert-manager annotations.
- It sees
cert-manager.io/cluster-issuer: "demo-ca-issuer"and thetlsblock. - It automatically creates a Certificate resource with:
dnsNamesset to the hosts listed intls[].hostssecretNameset totls[].secretNameissuerRefpointing todemo-ca-issuer
- cert-manager issues the certificate and stores it in the Secret.
- The Ingress controller picks up the Secret and serves TLS.
No separate Certificate manifest needed.
Available annotations
Section titled “Available annotations”| Annotation | Description |
|---|---|
cert-manager.io/cluster-issuer | Use a ClusterIssuer to sign the certificate. |
cert-manager.io/issuer | Use a namespace-scoped Issuer instead. |
cert-manager.io/duration | Override the default certificate duration. |
cert-manager.io/renew-before | Override the default renewal window. |
cert-manager.io/common-name | Set a specific common name. |
cert-manager.io/private-key-algorithm | Set the private key algorithm (RSA or ECDSA). |
When to use annotations vs explicit Certificate resources
Section titled “When to use annotations vs explicit Certificate resources”Use Ingress annotations when the certificate maps one-to-one with an Ingress. This is the common case. One Ingress, one hostname, one cert.
Use explicit Certificate resources when you need fine-grained control: specific key algorithms, custom subjects, certificates for non-Ingress use cases (mTLS, internal service communication), or certificates that span multiple Ingresses.
Certificate Renewal Mechanics
Section titled “Certificate Renewal Mechanics”cert-manager continuously monitors every Certificate resource. It compares the current time against the certificate’s expiry date minus the renewBefore window. When the certificate enters that window, cert-manager triggers a renewal.
How duration and renewBefore interact
Section titled “How duration and renewBefore interact”Consider this certificate from the demo:
spec: duration: 2160h # 90 days renewBefore: 360h # 15 days- The certificate is valid for 90 days from issuance.
- cert-manager starts the renewal process 15 days before expiry (at the 75-day mark).
- The effective “usable” lifetime is 75 days before a renewal attempt.
For the short-lived certificate in the demo:
spec: duration: 1h renewBefore: 30m- Valid for 1 hour.
- Renewal starts at the 30-minute mark.
- You can watch cert-manager renew it by inspecting events:
kubectl -n demo-apps describe certificate short-lived-certWhat happens during renewal
Section titled “What happens during renewal”- cert-manager creates a new CertificateRequest.
- The issuer signs it and returns a new certificate.
- cert-manager updates the existing Secret in place with the new
tls.crtandtls.key. - The old certificate’s data is overwritten. There is no rotation period where both are valid.
How applications pick up renewed certificates
Section titled “How applications pick up renewed certificates”This depends on the application:
- Ingress controllers (NGINX, Traefik, etc.) typically watch Secrets and reload automatically. No pod restart needed.
- Applications mounting Secrets as volumes see the updated files eventually (kubelet syncs projected volumes periodically, usually within 60-90 seconds). The application must watch for file changes to pick up the new cert without restarting.
- Applications reading Secrets via the API need to re-read the Secret to get the new cert.
- Applications that load the cert once at startup need a pod restart.
Choosing renewal windows
Section titled “Choosing renewal windows”| Certificate Type | Suggested duration | Suggested renewBefore |
|---|---|---|
| Internal CA | 87600h (10 years) | 720h (30 days) |
| Application (internal) | 2160h (90 days) | 360h (15 days) |
| Application (public) | 2160h (90 days) | 720h (30 days) |
| Short-lived / zero-trust | 24h | 8h |
Shorter lifetimes reduce the blast radius of a compromised key but increase the frequency of renewals. Match the window to your risk tolerance and your applications’ ability to pick up new certs.
What cert-manager Stores in Kubernetes Secrets
Section titled “What cert-manager Stores in Kubernetes Secrets”When cert-manager issues a certificate, it creates (or updates) a Kubernetes Secret of type kubernetes.io/tls. That Secret contains three keys:
| Key | Contents | Purpose |
|---|---|---|
tls.crt | The issued certificate in PEM format, followed by any intermediate CA certificates (the full chain). | Presented to clients during the TLS handshake. |
tls.key | The private key in PEM format. | Used by the server to decrypt traffic. Never shared with clients. |
ca.crt | The CA certificate that signed tls.crt. | Used by clients to verify the certificate chain. Useful for mTLS setups where clients need to trust the CA. |
You can inspect any of these:
# View the certificate detailskubectl -n demo-apps get secret myapp-tls-secret \ -o jsonpath='{.data.tls\.crt}' | base64 -d | \ openssl x509 -noout -subject -issuer -dates -ext subjectAltName
# View the CA certificatekubectl -n demo-apps get secret myapp-tls-secret \ -o jsonpath='{.data.ca\.crt}' | base64 -d | \ openssl x509 -noout -subject -issuer
# Check that the private key matches the certificatekubectl -n demo-apps get secret myapp-tls-secret \ -o jsonpath='{.data.tls\.key}' | base64 -d | \ openssl ec -noout -text 2>/dev/null || echo "RSA key"Secret ownership
Section titled “Secret ownership”cert-manager sets an owner reference on the Secret pointing back to the Certificate resource. If you delete the Certificate, the Secret gets garbage-collected. This is usually what you want, but be aware of it if you are migrating certificate management between tools.
Secret naming
Section titled “Secret naming”The Secret name comes from spec.secretName in the Certificate resource. If a Secret with that name already exists and was not created by cert-manager, cert-manager will not overwrite it. It will report an error on the Certificate status. Name your Secrets deliberately to avoid collisions.
ACME Issuers for Production
Section titled “ACME Issuers for Production”The demo uses a CA issuer, which is great for internal services. For public-facing services, you want certificates from a publicly trusted CA. Let’s Encrypt provides free, automated certificates using the ACME (Automatic Certificate Management Environment) protocol.
CA issuer vs ACME issuer
Section titled “CA issuer vs ACME issuer”| Aspect | CA Issuer (this demo) | ACME Issuer (Let’s Encrypt) |
|---|---|---|
| Trust model | You manage the CA. Clients must explicitly trust it. | Let’s Encrypt is already trusted by all browsers. |
| Signing | Direct. cert-manager signs locally using the CA key in a Secret. | Remote. cert-manager talks to Let’s Encrypt servers over HTTPS. |
| Domain validation | None. If you can create a Certificate resource, you get a cert. | Required. You must prove you own the domain. |
| Use case | Internal services, development, testing. | Public websites, APIs accessible from the internet. |
| Cost | Free (self-managed). | Free (Let’s Encrypt). |
How ACME works
Section titled “How ACME works”Instead of signing certificates directly, ACME proves domain ownership through a challenge:
HTTP-01 challenge: cert-manager serves a token at http://yourdomain/.well-known/acme-challenge/token. Let’s Encrypt makes an HTTP request to verify it can reach it. Simple. Works for most cases.
apiVersion: cert-manager.io/v1kind: ClusterIssuermetadata: name: letsencrypt-prodspec: acme: server: https://acme-v02.api.letsencrypt.org/directory email: admin@yourcompany.com privateKeySecretRef: name: letsencrypt-account-key solvers: - http01: ingress: ingressClassName: nginxDNS-01 challenge: cert-manager creates a TXT DNS record. Let’s Encrypt queries DNS to verify it exists. Required for wildcard certificates (*.yourcompany.com). Needs a DNS provider integration (Route53, Cloudflare, Google Cloud DNS, etc.).
apiVersion: cert-manager.io/v1kind: ClusterIssuermetadata: name: letsencrypt-wildcardspec: acme: server: https://acme-v02.api.letsencrypt.org/directory email: admin@yourcompany.com privateKeySecretRef: name: letsencrypt-account-key solvers: - dns01: route53: region: us-east-1 hostedZoneID: Z0123456789ABCDEFACME-specific CRDs
Section titled “ACME-specific CRDs”When you use an ACME issuer, cert-manager creates two additional resources behind the scenes:
- Order: Tracks the certificate order with Let’s Encrypt. Contains the status of the request.
- Challenge: Tracks each domain validation challenge. Shows whether the challenge is pending, processing, or completed.
These are useful for debugging failed issuance:
kubectl get orders -n demo-appskubectl get challenges -n demo-appsStaging vs production
Section titled “Staging vs production”Always test with Let’s Encrypt staging first. The staging server has relaxed rate limits and issues certificates from an untrusted CA (perfect for testing).
# Staging (for testing)server: https://acme-staging-v02.api.letsencrypt.org/directory
# Production (for real certificates)server: https://acme-v02.api.letsencrypt.org/directoryLet’s Encrypt production has rate limits: 50 certificates per registered domain per week, 5 duplicate certificates per week, and 300 new orders per account per 3 hours. Hit a limit during testing and you could be locked out.
Private Key Algorithms: RSA vs ECDSA
Section titled “Private Key Algorithms: RSA vs ECDSA”This demo uses both algorithms. The CA certificate uses ECDSA. The application certificate uses RSA. The choice matters.
privateKey: algorithm: RSA size: 2048RSA is the older, widely compatible option. RSA 2048 is the minimum considered secure today. RSA 4096 provides a larger security margin but produces bigger certificates and slower handshakes.
Strengths:
- Universal compatibility. Every TLS client and server supports RSA.
- Well-understood. Decades of deployment experience.
Weaknesses:
- Large key sizes. A 2048-bit RSA key is 256 bytes. A 4096-bit key is 512 bytes.
- Slower operations. Key generation, signing, and verification are slower than ECDSA at equivalent security levels.
privateKey: algorithm: ECDSA size: 256ECDSA (Elliptic Curve Digital Signature Algorithm) provides equivalent security with much smaller keys. ECDSA P-256 (size 256) provides roughly the same security as RSA 3072.
Strengths:
- Small keys and signatures. A P-256 key is 32 bytes.
- Faster operations. Signing and verification are significantly faster.
- Lower bandwidth. Smaller certificates mean faster TLS handshakes.
Weaknesses:
- Slightly less universal. Very old clients (pre-2014) may not support it. This is rarely a concern in 2026.
- Implementation complexity. ECDSA signing requires a good source of randomness. A bad random number generator can leak the private key. This is a library-level concern, not something you worry about in cert-manager.
When to use each
Section titled “When to use each”| Scenario | Recommendation | Reason |
|---|---|---|
| CA certificates | ECDSA 256 | Smaller, faster. CA certs are verified frequently. |
| Internal application certs | ECDSA 256 | Performance and size advantages. All modern internal clients support it. |
| Public-facing services | RSA 2048 or ECDSA 256 | RSA if you need compatibility with very old clients. ECDSA otherwise. |
| Legacy system integration | RSA 2048 | Maximum compatibility. |
| High-throughput services | ECDSA 256 | Faster TLS handshakes reduce latency at scale. |
This demo’s choice of ECDSA for the CA and RSA for the application cert is intentional. It shows both in action. In practice, if all your clients support ECDSA (and they almost certainly do), use ECDSA everywhere.
Mapping This Demo to Production
Section titled “Mapping This Demo to Production”The demo uses a self-signed CA, which is fine for development and internal services where you control the trust store. Moving to production changes one thing: where the CA comes from. Everything downstream (Certificate resources, Ingress annotations, automatic renewal) works identically.
What stays the same
Section titled “What stays the same”- Certificate resources look exactly the same. The
specfields (dnsNames,duration,renewBefore,privateKey) do not change. - Ingress annotations work the same. Just change the issuer name.
- Secret structure is identical.
tls.crt,tls.key,ca.crt. - Renewal mechanics are identical. cert-manager handles it regardless of issuer type.
What changes
Section titled “What changes”| Demo | Production |
|---|---|
| Self-signed bootstrap to create a CA | Import an enterprise CA, connect to Vault, or use ACME |
| Single CA issuer for everything | Multiple issuers for different use cases |
| No domain validation | ACME requires HTTP-01 or DNS-01 challenges |
| Trust is manual (you add the CA to trust stores) | Public CAs are already trusted by browsers |
Common production patterns
Section titled “Common production patterns”Pattern 1: Enterprise internal CA
Your security team provides a CA cert and key. Import them as a Secret, point a ClusterIssuer at it. No self-signed bootstrapping needed.
kubectl create secret tls company-ca-secret \ --cert=company-ca.crt \ --key=company-ca.key \ --namespace=cert-managerapiVersion: cert-manager.io/v1kind: ClusterIssuermetadata: name: company-ca-issuerspec: ca: secretName: company-ca-secretPattern 2: Vault PKI
If the CA private key should never enter Kubernetes (a common security requirement), use the Vault issuer. cert-manager talks to Vault’s PKI secrets engine to request signed certificates. The CA key stays in Vault.
apiVersion: cert-manager.io/v1kind: ClusterIssuermetadata: name: vault-issuerspec: vault: server: https://vault.internal.company.com path: pki/sign/k8s-certs auth: kubernetes: role: cert-manager mountPath: /v1/auth/kubernetesPattern 3: Multiple issuers
Most real deployments end up with more than one issuer. Internal services use a CA issuer. Public services use Let’s Encrypt. Teams select the right issuer in their annotation or Certificate spec.
# Public-facing servicescert-manager.io/cluster-issuer: "letsencrypt-prod"
# Internal servicescert-manager.io/cluster-issuer: "company-ca-issuer"The platform team manages the issuers. Application teams never touch certificate files. They just pick an issuer name and declare their DNS names. cert-manager handles everything else.
Summary of cert-manager Resources
Section titled “Summary of cert-manager Resources”| Resource | Purpose | Scope |
|---|---|---|
| ClusterIssuer | Defines how certificates are signed (CA, ACME, Vault, etc.) | Cluster-wide |
| Issuer | Same as ClusterIssuer but namespace-scoped | Single namespace |
| Certificate | Requests a TLS certificate, stored in a Secret | Namespace |
| CertificateRequest | Internal: the actual signing request sent to the issuer | Namespace |
| Order | ACME-specific: tracks the certificate order | Namespace |
| Challenge | ACME-specific: tracks domain validation | Namespace |
Summary of Issuer Types
Section titled “Summary of Issuer Types”| Issuer Type | Use Case | CA Key Location |
|---|---|---|
| SelfSigned | Bootstrapping, development | N/A (signs itself) |
| CA | Internal certs, this demo | Kubernetes Secret |
| Vault | Strict key management | HashiCorp Vault |
| ACME | Public-facing services (Let’s Encrypt) | External CA |
| Venafi | Enterprise certificate platforms | Venafi TPP/Cloud |