Skip to content

cert-manager: Deep Dive

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.


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.

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.

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/v1
kind: ClusterIssuer
metadata:
name: selfsigned-issuer
spec:
selfSigned: {}

This issuer exists solely to bootstrap the CA. Application certificates should never reference it directly.

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/v1
kind: Certificate
metadata:
name: demo-ca
namespace: cert-manager
spec:
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 days

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

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/v1
kind: ClusterIssuer
metadata:
name: demo-ca-issuer
spec:
ca:
secretName: demo-ca-secret

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

This is where the value lands. Teams request TLS certificates by creating Certificate resources. cert-manager does everything else.


cert-manager extends the Kubernetes API with several CRDs. Two are essential to understand.

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:

FieldDescription
spec.selfSignedSigns certificates with their own private key. Used for bootstrapping.
spec.ca.secretNamePoints to a Kubernetes Secret containing a CA cert and key.
spec.acmeConfigures ACME protocol (Let’s Encrypt). See ACME section below.
spec.vaultConfigures HashiCorp Vault PKI backend.
spec.venafiConfigures Venafi Trust Protection Platform.

A ClusterIssuer reports its readiness via a status condition. Check it with:

Terminal window
kubectl get clusterissuer demo-ca-issuer -o wide

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/v1
kind: Certificate
metadata:
name: myapp-tls
namespace: demo-apps
spec:
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: 2048

Key spec fields explained:

FieldDescription
secretNameName of the Kubernetes Secret where cert-manager stores the issued certificate. Created automatically.
commonNameThe CN field in the X.509 certificate. Most modern TLS implementations ignore this in favor of dnsNames, but some legacy systems still require it.
dnsNamesSubject Alternative Names (SANs). These are the hostnames the certificate is valid for. This is what browsers and modern TLS clients actually check.
issuerRef.nameWhich Issuer or ClusterIssuer signs this certificate.
issuerRef.kindEither Issuer or ClusterIssuer.
issuerRef.groupAlways cert-manager.io.
durationHow long the certificate is valid. Specified as a Go duration string (e.g., 2160h for 90 days).
renewBeforeHow long before expiry cert-manager should start renewal. 360h means renew 15 days before the cert expires.
privateKey.algorithmRSA or ECDSA. See Private Key Algorithms below.
privateKey.sizeKey size. For RSA: 2048 or 4096. For ECDSA: 256 or 384.
isCAWhen true, the issued certificate can sign other certificates. Used only for CA certificates, not application certs.
subject.organizationsSets the Organization (O) field in the X.509 subject.

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:

Terminal window
kubectl get certificaterequest -n demo-apps

If 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/v1
kind: Ingress
metadata:
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: 80
  1. cert-manager’s ingress-shim controller watches for Ingress resources with cert-manager annotations.
  2. It sees cert-manager.io/cluster-issuer: "demo-ca-issuer" and the tls block.
  3. It automatically creates a Certificate resource with:
    • dnsNames set to the hosts listed in tls[].hosts
    • secretName set to tls[].secretName
    • issuerRef pointing to demo-ca-issuer
  4. cert-manager issues the certificate and stores it in the Secret.
  5. The Ingress controller picks up the Secret and serves TLS.

No separate Certificate manifest needed.

AnnotationDescription
cert-manager.io/cluster-issuerUse a ClusterIssuer to sign the certificate.
cert-manager.io/issuerUse a namespace-scoped Issuer instead.
cert-manager.io/durationOverride the default certificate duration.
cert-manager.io/renew-beforeOverride the default renewal window.
cert-manager.io/common-nameSet a specific common name.
cert-manager.io/private-key-algorithmSet 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.


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.

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:
Terminal window
kubectl -n demo-apps describe certificate short-lived-cert
  1. cert-manager creates a new CertificateRequest.
  2. The issuer signs it and returns a new certificate.
  3. cert-manager updates the existing Secret in place with the new tls.crt and tls.key.
  4. 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.
Certificate TypeSuggested durationSuggested renewBefore
Internal CA87600h (10 years)720h (30 days)
Application (internal)2160h (90 days)360h (15 days)
Application (public)2160h (90 days)720h (30 days)
Short-lived / zero-trust24h8h

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:

KeyContentsPurpose
tls.crtThe issued certificate in PEM format, followed by any intermediate CA certificates (the full chain).Presented to clients during the TLS handshake.
tls.keyThe private key in PEM format.Used by the server to decrypt traffic. Never shared with clients.
ca.crtThe 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:

Terminal window
# View the certificate details
kubectl -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 certificate
kubectl -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 certificate
kubectl -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"

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.

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.


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.

AspectCA Issuer (this demo)ACME Issuer (Let’s Encrypt)
Trust modelYou manage the CA. Clients must explicitly trust it.Let’s Encrypt is already trusted by all browsers.
SigningDirect. cert-manager signs locally using the CA key in a Secret.Remote. cert-manager talks to Let’s Encrypt servers over HTTPS.
Domain validationNone. If you can create a Certificate resource, you get a cert.Required. You must prove you own the domain.
Use caseInternal services, development, testing.Public websites, APIs accessible from the internet.
CostFree (self-managed).Free (Let’s Encrypt).

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/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-prod
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: admin@yourcompany.com
privateKeySecretRef:
name: letsencrypt-account-key
solvers:
- http01:
ingress:
ingressClassName: nginx

DNS-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/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-wildcard
spec:
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: Z0123456789ABCDEF

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:

Terminal window
kubectl get orders -n demo-apps
kubectl get challenges -n demo-apps

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/directory

Let’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.


This demo uses both algorithms. The CA certificate uses ECDSA. The application certificate uses RSA. The choice matters.

privateKey:
algorithm: RSA
size: 2048

RSA 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: 256

ECDSA (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.
ScenarioRecommendationReason
CA certificatesECDSA 256Smaller, faster. CA certs are verified frequently.
Internal application certsECDSA 256Performance and size advantages. All modern internal clients support it.
Public-facing servicesRSA 2048 or ECDSA 256RSA if you need compatibility with very old clients. ECDSA otherwise.
Legacy system integrationRSA 2048Maximum compatibility.
High-throughput servicesECDSA 256Faster 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.


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.

  • Certificate resources look exactly the same. The spec fields (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.
DemoProduction
Self-signed bootstrap to create a CAImport an enterprise CA, connect to Vault, or use ACME
Single CA issuer for everythingMultiple issuers for different use cases
No domain validationACME requires HTTP-01 or DNS-01 challenges
Trust is manual (you add the CA to trust stores)Public CAs are already trusted by browsers

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.

Terminal window
kubectl create secret tls company-ca-secret \
--cert=company-ca.crt \
--key=company-ca.key \
--namespace=cert-manager
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: company-ca-issuer
spec:
ca:
secretName: company-ca-secret

Pattern 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/v1
kind: ClusterIssuer
metadata:
name: vault-issuer
spec:
vault:
server: https://vault.internal.company.com
path: pki/sign/k8s-certs
auth:
kubernetes:
role: cert-manager
mountPath: /v1/auth/kubernetes

Pattern 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 services
cert-manager.io/cluster-issuer: "letsencrypt-prod"
# Internal services
cert-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.


ResourcePurposeScope
ClusterIssuerDefines how certificates are signed (CA, ACME, Vault, etc.)Cluster-wide
IssuerSame as ClusterIssuer but namespace-scopedSingle namespace
CertificateRequests a TLS certificate, stored in a SecretNamespace
CertificateRequestInternal: the actual signing request sent to the issuerNamespace
OrderACME-specific: tracks the certificate orderNamespace
ChallengeACME-specific: tracks domain validationNamespace
Issuer TypeUse CaseCA Key Location
SelfSignedBootstrapping, developmentN/A (signs itself)
CAInternal certs, this demoKubernetes Secret
VaultStrict key managementHashiCorp Vault
ACMEPublic-facing services (Let’s Encrypt)External CA
VenafiEnterprise certificate platformsVenafi TPP/Cloud