Kubernetes Course
TLS in Kubernetes
TLS is everywhere in a Kubernetes cluster — the API server, etcd, kubelets, ingress controllers, and inter-service communication all use it. This lesson covers how TLS certificates work in Kubernetes, how to automate certificate management with cert-manager, and how to terminate TLS at the Ingress layer.
Where TLS Lives in a Kubernetes Cluster
TLS in Kubernetes operates at two distinct layers that are easy to conflate: the cluster control plane (API server ↔ etcd, API server ↔ kubelets) which is configured at cluster bootstrap and rarely touched after, and the application layer (HTTPS for your services) which you manage continuously as certificates expire and applications are added.
Control Plane TLS
API server cert, etcd peer certs, kubelet serving certs. Generated at cluster creation (kubeadm, managed service). Stored in /etc/kubernetes/pki/. Rotated by the cluster, not by you.
Application TLS
TLS certificates for your services exposed via Ingress. Stored as Kubernetes Secrets of type kubernetes.io/tls. Managed by cert-manager or manually. This is what you deal with daily.
TLS Secrets
Kubernetes represents TLS certificates as Secrets of type kubernetes.io/tls. They contain exactly two keys: tls.crt (the certificate chain, PEM encoded) and tls.key (the private key, PEM encoded). Ingress controllers, service meshes, and application Pods all consume TLS secrets in this format.
# Create a TLS Secret from existing certificate files
kubectl create secret tls api-tls-cert \
--cert=path/to/tls.crt \
--key=path/to/tls.key \
--namespace payments
# tls.crt: the full certificate chain (leaf + intermediates)
# tls.key: the private key — never commit this to Git
# Inspect a TLS Secret
kubectl get secret api-tls-cert -n payments -o jsonpath='{.data.tls\.crt}' \
| base64 -d | openssl x509 -noout -text | grep -E "Subject:|Issuer:|Not After"
# Create a self-signed certificate for testing (not for production)
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
-keyout tls.key -out tls.crt \
-subj "/CN=api.company.com/O=company" \
-addext "subjectAltName=DNS:api.company.com"
kubectl create secret tls api-tls-cert --cert=tls.crt --key=tls.key -n payments
$ kubectl create secret tls api-tls-cert --cert=tls.crt --key=tls.key -n payments
secret/api-tls-cert created
$ kubectl get secret api-tls-cert -n payments
NAME TYPE DATA AGE
api-tls-cert kubernetes.io/tls 2 5s
$ kubectl get secret api-tls-cert -n payments -o jsonpath='{.data.tls\.crt}' \
| base64 -d | openssl x509 -noout -text | grep -E "Subject:|Not After"
Subject: CN = api.company.com, O = company
Not After : Mar 10 11:04:22 2026 GMT ← expires in 1 yearTLS Termination at the Ingress
The scenario: Your payment API is exposed via an Ingress. You want HTTPS with automatic HTTP→HTTPS redirect. The Ingress controller terminates TLS and forwards plain HTTP to the backend Service.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: payment-api
namespace: payments
annotations:
nginx.ingress.kubernetes.io/ssl-redirect: "true" # Redirect HTTP → HTTPS (301)
nginx.ingress.kubernetes.io/force-ssl-redirect: "true" # Even behind a proxy
spec:
ingressClassName: nginx
tls:
- hosts:
- api.company.com
secretName: api-tls-cert # The kubernetes.io/tls Secret containing the cert + key
# Must be in the SAME namespace as the Ingress
rules:
- host: api.company.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: payment-api
port:
number: 80 # Backend receives plain HTTP — TLS is terminated at the Ingress
What just happened?
TLS termination at the Ingress means the backend sees plain HTTP — The Ingress controller handles the TLS handshake, decrypts the traffic, and forwards unencrypted HTTP to the backend Pod on port 80. This simplifies the application (no TLS code needed) but means traffic between the Ingress controller and the Pod is unencrypted. Inside the cluster on a private network this is acceptable; for defence-in-depth, configure the Ingress to use HTTPS to the backend too (re-encryption).
The TLS Secret must be in the same namespace as the Ingress — Unlike many Kubernetes cross-namespace references, spec.tls[].secretName only looks in the Ingress's own namespace. If you have 10 namespaces all needing the same wildcard cert, you need the Secret replicated in each — or use cert-manager's Certificate resource which handles this for you.
cert-manager: Automated Certificate Lifecycle
cert-manager is the de-facto standard for certificate management in Kubernetes. It introduces two CRDs — Issuer/ClusterIssuer (where to get certs from) and Certificate (what cert to get) — and a controller that handles the full lifecycle: request, renewal, and storage as a TLS Secret.
The scenario: You want free, automatically-renewed Let's Encrypt certificates for all your Ingress hostnames. cert-manager handles the ACME challenge, obtains the certificate, stores it as a TLS Secret, and renews it 30 days before expiry — completely automatically.
# Install cert-manager (Helm)
helm repo add jetstack https://charts.jetstack.io
helm repo update
helm install cert-manager jetstack/cert-manager \
--namespace cert-manager \
--create-namespace \
--version v1.14.0 \
--set installCRDs=true # Install the CRDs (Certificate, Issuer, ClusterIssuer, etc.)
# ClusterIssuer: cluster-wide Let's Encrypt issuer via HTTP-01 challenge
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-prod
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory # Let's Encrypt production ACME endpoint
email: platform@company.com # Notified on expiry warnings
privateKeySecretRef:
name: letsencrypt-prod-account-key # ACME account private key — cert-manager manages this
solvers:
- http01:
ingress:
class: nginx # Use the nginx Ingress controller for HTTP-01 challenges
# HTTP-01: Let's Encrypt sends a request to http://yourdomain/.well-known/acme-challenge/...
# The Ingress controller routes this to cert-manager's challenge solver Pod
# Your domain must be publicly reachable for this to work
---
# Certificate: request a specific cert — cert-manager creates the TLS Secret automatically
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: api-tls-cert
namespace: payments
spec:
secretName: api-tls-cert # cert-manager creates/updates this TLS Secret
issuerRef:
name: letsencrypt-prod
kind: ClusterIssuer
dnsNames:
- api.company.com
- api-internal.company.com # Multiple SANs in one certificate
duration: 2160h # 90 days (Let's Encrypt maximum)
renewBefore: 720h # Renew 30 days before expiry
# cert-manager watches for expiry and renews automatically
$ kubectl apply -f clusterissuer.yaml -f certificate.yaml clusterissuer.cert-manager.io/letsencrypt-prod created certificate.cert-manager.io/api-tls-cert created $ kubectl get certificate -n payments NAME READY SECRET AGE api-tls-cert False api-tls-cert 12s ← False: challenge in progress $ kubectl describe certificate api-tls-cert -n payments Events: Normal Issuing Requesting new certificate Normal Generated Stored new private key in Secret "api-tls-cert" Normal Requested Created CertificateRequest "api-tls-cert-1" Normal Issuing The certificate has been successfully issued ✓ $ kubectl get certificate -n payments NAME READY SECRET AGE api-tls-cert True api-tls-cert 45s ← True: cert issued and stored ✓ $ kubectl get secret api-tls-cert -n payments NAME TYPE DATA AGE api-tls-cert kubernetes.io/tls 3 45s ← 3 keys: tls.crt, tls.key, ca.crt
What just happened?
cert-manager owns the full lifecycle — From the moment the Certificate resource is created, cert-manager handles everything: generating a private key, creating a CSR, completing the ACME challenge (temporarily adding a route to the Ingress for the challenge path), and storing the issued certificate as a TLS Secret. 30 days before expiry, it repeats the process automatically — no manual certificate renewal ever needed.
The shortcut: annotate the Ingress directly — Instead of creating a Certificate object separately, add cert-manager.io/cluster-issuer: letsencrypt-prod to your Ingress annotations. cert-manager detects this and automatically creates the Certificate object and TLS Secret for you — one fewer resource to manage.
# Shortcut: annotate the Ingress — cert-manager creates the Certificate automatically
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: payment-api
namespace: payments
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod # cert-manager sees this annotation
nginx.ingress.kubernetes.io/ssl-redirect: "true" # and creates a Certificate + TLS Secret
spec:
ingressClassName: nginx
tls:
- hosts:
- api.company.com
secretName: api-tls-cert # cert-manager will create this Secret automatically
rules:
- host: api.company.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: payment-api
port:
number: 80
Teacher's Note: Let's Encrypt staging vs production
Let's Encrypt has strict rate limits on its production endpoint — 5 duplicate certificate requests per week per domain. If you misconfigure the ClusterIssuer or Certificate and trigger repeated failed requests, you'll hit this limit and be blocked for a week.
Always create a staging ClusterIssuer first: use https://acme-staging-v02.api.letsencrypt.org/directory as the server. Staging has much more generous rate limits and issues real (but browser-untrusted) certificates. Test your full cert-manager setup against staging — confirm certificates are issued correctly, Ingress TLS works, and renewals fire — then switch to the production issuer for browser-trusted certs.
For internal services or corporate environments where Let's Encrypt isn't appropriate (no public DNS, air-gapped clusters), cert-manager supports private CA issuers: the CA issuer type uses a self-signed root CA stored as a Kubernetes Secret, and Vault issuer integrates with HashiCorp Vault's PKI secrets engine for enterprise PKI workflows.
Practice Questions
1. What Secret type must a Kubernetes Secret have for an Ingress controller to use it as a TLS certificate?
2. In cert-manager, which resource defines a cluster-wide certificate authority or ACME endpoint that can be used by Certificate objects in any namespace?
3. In a cert-manager Certificate resource, which field specifies how far ahead of expiry cert-manager should start the renewal process?
Quiz
1. An Ingress has a tls block referencing a TLS Secret. What protocol does the Ingress controller use when forwarding requests to the backend Service?
2. You're setting up cert-manager with Let's Encrypt for the first time. What should you do before using the production ACME endpoint?
3. What is the simplest way to get cert-manager to automatically provision a TLS certificate for an Ingress without creating a separate Certificate resource?
Up Next · Lesson 45
Kubernetes Security Best Practices
A consolidated checklist covering the security controls every production cluster should have — from RBAC hardening and Pod security to network policies and supply chain security. The capstone for Section III.