Kubernetes Lesson 44 – TLS in Kubernetes | Dataplexa
Networking, Ingress & Security · Lesson 44

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 year

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