Cert-Manager TLS
Manage TLS certificates with Cert-Manager in Kubernetes. This lesson covers automated certificate lifecycle, issuance, and renewal using Let's Encrypt, with hands-on exercises and troubleshooting.
Focus: manage tls certificates with cert-manager
Picture this: you've deployed your production application on Kubernetes and pointed a shiny domain at it, but visitors see ERR_CERT_AUTHORITY_INVALID — the dreaded red warning. Manually generating self-signed certificates or juggling renewals every 90 days with Let's Encrypt is not only tedious but also error-prone. That's exactly the pain Cert-Manager eliminates: it automates the entire lifecycle of TLS certificates inside your cluster, from request to renewal, so your ingress traffic is always encrypted without manual intervention.
The problem this lesson solves
Securing Kubernetes ingresses with HTTPS requires valid TLS certificates. While you can create TLS secrets manually using kubectl create secret tls, this approach does not scale:
- Short-lived certificates (Let's Encrypt's 90-day validity) demand regular manual renewals.
- Certificate revocation or expiry causes service outages or browser warnings.
- Mixed environments (staging, production) require different issuers, complicating management.
Cert-Manager solves all of these by acting as a certificate operator — it watches your custom resources (Certificate, Issuer, ClusterIssuer) and automatically provisions, renews, and manages TLS secrets based on your policies.
Core concept / mental model
Think of Cert-Manager as a robot assistant that sits inside your Kubernetes cluster and talks to Certificate Authorities (CAs) on your behalf. You tell it: "I want a certificate for myapp.example.com that expires every 90 days, and please use Let's Encrypt as the issuer." Cert-Manager then:
- Sends a certificate signing request (CSR) to the CA.
- Completes the required domain validation challenge (HTTP-01 or DNS-01).
- Stores the resulting private key + certificate as a Kubernetes Secret.
- Automatically renews the secret before expiry (typically 30 days before).
Key resources involved:
- Issuer / ClusterIssuer — defines which CA to trust (e.g., Let's Encrypt staging, production, internal CA). Use
ClusterIssuerfor cluster-wide certificates. - Certificate — the desired certificate specification (domain, secret name, issuer reference).
- Order & Challenge — internal resources Cert-Manager uses to complete validation.
Pro tip: Always start with Let's Encrypt's staging environment — it has no rate limits for testing. Switch to production only after verifying issuance works correctly.
How it works step by step
The certificate lifecycle managed by Cert-Manager follows a predictable flow:
- Install Cert-Manager in its own namespace (
cert-manager). It deploys controllers and webhook components. - Create an Issuer or ClusterIssuer for your desired CA (e.g., Let's Encrypt ACME).
- Create a Certificate resource referencing the issuer and the domain(s) you need.
- Cert-Manager sends the CSR to the CA and presents the validation challenge.
- Upon successful validation, the CA signs the certificate, and Cert-Manager stores the result as a TLS Secret in the same namespace as the Certificate resource.
- Reference the secret in your Ingress or Gateway API resource.
- Cert-Manager watches the expiry and automatically renews the certificate (default: 2/3 of the validity period before expiry).
If using HTTP-01 challenge, your Ingress must be publicly reachable on port 80 and correctly route the challenge path (
/.well-known/acme-challenge/) to the Cert-Manager solver pod. DNS-01 works even with private clusters but requires cloud DNS credentials.
Hands-on walkthrough
Let's install Cert-Manager and create a real TLS certificate for a domain you control. We'll use Let's Encrypt staging.
1. Install Cert-Manager
# Add the Jetstack Helm repository
helm repo add jetstack https://charts.jetstack.io
helm repo update
# Install cert-manager with CRDs included
helm install cert-manager jetstack/cert-manager \
--namespace cert-manager \
--create-namespace \
--version v1.15.0 \
--set installCRDs=true
Verify installation:
kubectl get pods --namespace cert-manager
# Expected output: cert-manager-xxx, cert-manager-webhook-xxx, cert-manager-cainjector-xxx (all Running)
2. Create a ClusterIssuer for Let's Encrypt staging
# staging-cluster-issuer.yaml
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-staging
spec:
acme:
# The ACME server URL
server: https://acme-staging-v02.api.letsencrypt.org/directory
# Email for account registration
email: admin@example.com
privateKeySecretRef:
name: letsencrypt-staging-account-key
solvers:
- http01:
ingress:
class: nginx
Apply it:
kubectl apply -f staging-cluster-issuer.yaml
3. Create a Certificate resource and an Ingress
# example-tls-certificate.yaml
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: myapp-tls
namespace: default
spec:
secretName: myapp-tls-secret
issuerRef:
name: letsencrypt-staging
kind: ClusterIssuer
commonName: myapp.example.com
dnsNames:
- myapp.example.com
# ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: myapp-ingress
annotations:
cert-manager.io/cluster-issuer: letsencrypt-staging
spec:
ingressClassName: nginx
rules:
- host: myapp.example.com
http:
paths:
- pathType: Prefix
path: "/"
backend:
service:
name: myapp-service
port:
number: 80
tls:
- hosts:
- myapp.example.com
secretName: myapp-tls-secret
Apply both:
kubectl apply -f example-tls-certificate.yaml
kubectl apply -f ingress.yaml
4. Verify the certificate is issued
# Check the Certificate status
kubectl get certificate myapp-tls -o wide
# Check the secret was created
kubectl get secret myapp-tls-secret
# Describe to see the conditions
kubectl describe certificate myapp-tls
Expected output snippet from describe:
Status:
Conditions:
Last Transition Time: 2025-04-13T10:00:00Z
Message: Certificate is up to date and has not expired
Reason: Ready
Status: True
Type: Ready
Now your ingress automatically serves the TLS certificate. When it expires, Cert-Manager will renew it without any action from you.
Compare options / when to choose what
| Approach | Pros | Cons | Best for |
|---|---|---|---|
Manual TLS Secret (kubectl create secret tls) |
No extra dependency, simple for dev | No automatic renewal, manual expiry management | Single-dev clusters, throwaway |
| Cert-Manager with HTTP-01 | Zero config for public ingresses, fully automated | Requires public IP:80 reachable, rate limited per domain | Production with public load balancers |
| Cert-Manager with DNS-01 | Works for private/internal domains, wildcard certs | Requires cloud DNS API credentials, more complex setup | Internal services, wildcard TLS |
| External CA (e.g., Vault) | Full control, compliance | Higher operational overhead, requires secrets management | Enterprise with strict PKI policies |
Variations to explore:
- Use ClusterIssuer instead of namespace-scoped Issuer to share CA config across all namespaces.
- Combine Cert-Manager with External DNS for fully automated DNS-01 (e.g., for wildcard certificates *.example.com).
- For internal clusters, configure a custom CA cert as a Issuer (non-ACME).
Troubleshooting & edge cases
Certificate stuck in "pending" state
kubectl describe certificate myapp-tls
Look for events like:
- Waiting on CertificateRequest → check the ACME Order: kubectl describe order -n default | grep -A10 -i status
- Failed to verify domain → your Ingress must route /.well-known/acme-challenge/ to the solver pod. Ensure the ingress controller is working.
- Rate limit exceeded → switch from Let's Encrypt prod to staging or wait.
HTTP-01 challenge fails on private networks
If your cluster is not reachable from the internet, HTTP-01 cannot work. Instead, use DNS-01 by:
1. Creating a secret with cloud DNS API credentials (e.g., AWS route53, Google Cloud DNS).
2. Configuring the ACME solver with dns01 provider.
Example snippet for AWS Route53:
solvers:
- dns01:
route53:
region: us-east-1
accessKeyIDSecretRef:
name: route53-credentials
key: access-key-id
secretAccessKeySecretRef:
name: route53-credentials
key: secret-access-key
Secret not created after certificate is issued
Ensure the secretName in the Certificate resource matches the secretName in the Ingress TLS section. Also check that the Secret namespace is the same as the Certificate resource.
Common mistakes:
- Using the production Let's Encrypt URL for initial testing — you will hit rate limits quickly. Always start with staging.
- Forgetting to annotate the Ingress with cert-manager.io/cluster-issuer — without this, Cert-Manager does not auto-issue certificates for the ingress.
- Using a mismatched commonName vs dnsNames — Cert-Manager will issue only for listed dnsNames. commonName is deprecated but used as a fallback.
- Not creating the Issuer in the same namespace as the Certificate (when using Issuer instead of ClusterIssuer).
What you learned & what's next
You now understand how to manage TLS certificates with Cert-Manager — from installation, configuring an ACME issuer, to automatic certificate provisioning for Ingress resources. You've seen how to troubleshoot common issues like pending orders and private network limitations.
Key takeaways:
- Cert-Manager eliminates manual certificate management via Kubernetes-native custom resources.
- Use ClusterIssuer for cluster-wide issuers and Certificate resources to define what you need.
- HTTP-01 is the simplest for public ingresses; DNS-01 supports wildcards and private clusters.
- Let's Encrypt staging is essential for testing without rate limits.
- Annotate your Ingress to enable automatic certificate injection.
- Always verify certificate readiness with kubectl describe certificate.
Next step: In the following lesson, you'll explore Secrets management with External Secrets Operator — extending automation beyond TLS to database credentials and API tokens. You'll integrate with cloud vaults like AWS Secrets Manager and GCP Secret Manager.
Practice recap
As a hands-on challenge, repeat the walkthrough but instead of HTTP-01, modify the ClusterIssuer to use DNS-01 with your cloud DNS provider (or simulate with a dummy credential). Create a wildcard Certificate for *.example.com and verify the TLS secret contains a matching wildcard. Finally, delete the Certificate and confirm the automatic cleanup removes the secret and ingress update.
Common mistakes
- Using Let's Encrypt production URL for initial testing — always start with staging
https://acme-staging-v02.api.letsencrypt.org/directoryto avoid rate limits. - Forgetting to add the
cert-manager.io/cluster-issuerannotation on the Ingress — without it, Cert-Manager does not automatically create or update the TLS secret. - Mismatching
commonNameanddnsNames— the certificate will only cover the domains listed indnsNames;commonNameis deprecated and ignored by modern browsers. - Using a namespace-scoped
Issuerbut creating theCertificatein a different namespace — the Certificate will never fetch the issuer configuration and will stay pending.
Variations
- Use DNS-01 challenge with cloud providers (AWS Route53, Google CloudDNS, Azure DNS) instead of HTTP-01 — enables wildcard certificates and works for private/internal clusters.
- Deploy a non-ACME Issuer using your own internal CA by providing a pre-generated CA certificate and key — useful for internal-only PKI without internet access.
- Integrate Cert-Manager with External Secrets Operator and cloud vaults for a fully automated, central certificate issuance pipeline across multiple clusters.
Real-world use cases
- Automatically provision Let's Encrypt TLS certificates for every new production Ingress in a multi-tenant Kubernetes cluster without manual ops intervention.
- Secure internal microservices with TLS using a private CA (via Cert-Manager) and DNS-01 challenges in a self-hosted data center cluster.
- Deploy a wildcard certificate
*.example.comfor all subdomains of a SaaS platform, auto-renewed every 90 days, with zero downtime during renewal.
Key takeaways
- Cert-Manager automates the entire lifecycle of TLS certificates: issuance, renewal, and secret management — no manual kubectl needed.
- Two main custom resources:
ClusterIssuer(cluster-wide CA config) andCertificate(domain + issuer reference). - HTTP-01 is the simplest challenge, but requires public ingress on port 80; DNS-01 works for private clusters and wildcard certs.
- Always test with Let's Encrypt staging first, then switch to production to avoid rate limiting.
- Annotate your Ingress with
cert-manager.io/cluster-issuerto enable automatic certificate provisioning and injection. - Monitor certificate status with
kubectl describe certificateand check Order/Challenge resources for debugging.
Keep learning
Related tutorials, quizzes, and articles for this topic.
Discussion
Questions, corrections, and tips help everyone reading this page.
0 comments
Add a comment
No comments yet — start the thread.