DevOpsIndex

TLS, SSL, and Encryption

Encryption — The Big Picture (Generic)

Before TLS specifics, understand the three types of encryption and why HTTPS needs all three.

1. Symmetric Encryption — One Shared Key

Both sides use the same key to lock and unlock.

graph LR
    PLAIN1["Hello World<br/>(plaintext)"]
    KEY1["🔑 Key: ABC123"]
    ENC1["Xk39dP#!<br/>(ciphertext)"]
    DEC1["Hello World<br/>(plaintext)"]

    PLAIN1 -->|"encrypt with KEY1"| ENC1
    ENC1 -->|"decrypt with KEY1"| DEC1
    KEY1 -.->|"same key used both ways"| ENC1

Problem: How do you share the key? If an attacker intercepts it, they can decrypt everything.

Fast: AES-256-GCM encrypts ~1 GB/s. Used for all bulk data.

2. Asymmetric Encryption — Public/Private Key Pair

Two mathematically linked keys. Public key encrypts, private key decrypts. The private key never leaves the server.

graph TD
    SERVER["Server generates key pair once:<br/>🔓 Public key  (share with everyone)<br/>🔒 Private key (NEVER share, stays on server)"]

    subgraph "Anyone can send a secret message"
        ALICE["Alice has a secret: 'my password'"]
        ALICE -->|"encrypt with server's PUBLIC key 🔓"| CIPHER["Xk39dP#!<br/>(ciphertext)"]
        CIPHER -->|"send over internet"| SERVER2["Server decrypts with PRIVATE key 🔒"]
        SERVER2 --> PLAIN2["'my password'<br/>(recovered)"]
    end

    subgraph "Attacker intercepts but cannot decrypt"
        ATTACKER["Eve intercepts: Xk39dP#!"]
        ATTACKER -->|"has public key 🔓<br/>but NOT private key 🔒"| FAIL["❌ Cannot decrypt"]
    end

Problem: 100× slower than symmetric. Can't use for bulk data.

3. How HTTPS Combines Both (Hybrid Encryption)

HTTPS uses asymmetric only to agree on a shared key, then uses symmetric for all data:

sequenceDiagram
    participant C as Your Browser
    participant S as Server (e.g. google.com)

    Note over C,S: Step 1 — Key Exchange (Asymmetric, happens once)
    S->>C: "Here's my PUBLIC KEY 🔓 (inside the certificate)"
    C->>C: Generate a random session key: 🔑 "7f3a9c..."
    C->>S: Encrypt session key with server's PUBLIC KEY 🔓<br/>→ send "Xk8dP#!" (only server can decrypt)
    S->>S: Decrypt with PRIVATE KEY 🔒<br/>→ recovers session key 🔑 "7f3a9c..."

    Note over C,S: Both now have the same session key 🔑 — nobody else does

    Note over C,S: Step 2 — Actual Data (Symmetric, blazing fast)
    C->>S: "GET /profile" encrypted with 🔑 session key
    S->>C: "200 OK {name: Alice}" encrypted with 🔑 session key

    Note over C,S: If attacker intercepts step 1 — only sees encrypted session key<br/>Cannot decrypt without private key 🔒

Summary of what each does in HTTPS:

Crypto type Used for Why
Asymmetric (RSA/ECDH) Key exchange only Solves the key-sharing problem
Symmetric (AES-256-GCM) All actual data Fast enough for gigabytes
Hashing (SHA-256) Certificate signatures, MAC Verify nothing was tampered with

Symmetric vs Asymmetric — Side by Side

graph TD
    subgraph SYM["Symmetric Encryption"]
        SA["🔑 One key<br/>Same key encrypts AND decrypts<br/>AES-256-GCM, ChaCha20<br/>Speed: ~1 GB/s<br/>Problem: how to share the key?"]
    end

    subgraph ASYM["Asymmetric Encryption"]
        AS["🔓🔒 Key pair<br/>PUBLIC key encrypts<br/>PRIVATE key decrypts<br/>RSA-2048, ECDSA, ECDH<br/>Speed: ~10 MB/s<br/>No key-sharing problem"]
    end

    subgraph HYBRID["Hybrid (what TLS does)"]
        HY["Use ASYM to exchange a symmetric key securely<br/>Then use SYM for all data<br/>Best of both: security + speed"]
    end

    SYM -->|"solves speed"| HYBRID
    ASYM -->|"solves key exchange"| HYBRID

graph TD
    subgraph SYM["Symmetric — one key"]
        A1["AES-256-GCM<br/>Same key encrypts and decrypts<br/>Fast: ~1 GB/s on modern CPU<br/>Problem: how to share the key securely?"]
    end
    subgraph ASYM["Asymmetric — key pair"]
        A2["RSA-2048 / ECDSA / ECDH<br/>Public key encrypts, private key decrypts<br/>Slow: ~10 MB/s<br/>No key-sharing problem — public key is public"]
    end
    subgraph HYBRID["TLS uses HYBRID"]
        H1["ECDHE (asymmetric)<br/>Securely agree on a shared secret"]
        H2["AES-256-GCM (symmetric)<br/>Encrypt all actual data with that secret"]
        H1 --> H2
    end

Why hybrid? Asymmetric crypto solves the key exchange problem but is 100× slower than AES. TLS uses asymmetric only to agree on a shared session key, then switches to AES for all data.

Forward Secrecy (ECDHE): Each session generates a new ephemeral key pair. The server's long-term private key is never used to encrypt data — only to authenticate. If the private key is stolen years later, past sessions cannot be decrypted.


SSL vs TLS History

graph LR
    SSL2["SSL 2.0 (1995)<br/>BROKEN — DROWN attack<br/>Deprecated 1996"] --> SSL3
    SSL3["SSL 3.0 (1996)<br/>BROKEN — POODLE attack<br/>Deprecated 2015"] --> TLS10
    TLS10["TLS 1.0 (1999)<br/>BEAST, POODLE variants<br/>Deprecated 2021"] --> TLS11
    TLS11["TLS 1.1 (2006)<br/>Deprecated 2021"] --> TLS12
    TLS12["TLS 1.2 (2008)<br/>Current — widely deployed<br/>2 RTT handshake"] --> TLS13
    TLS13["TLS 1.3 (2018)<br/>CURRENT PREFERRED<br/>1 RTT, forward secrecy mandatory"]

Minimum acceptable today: TLS 1.2. Prefer TLS 1.3.


TLS 1.2 Handshake (2 RTT)

sequenceDiagram
    participant C as Client
    participant S as Server

    Note over C,S: Round Trip 1
    C->>S: ClientHello<br/>TLS version: 1.2<br/>Random: 32 bytes<br/>Cipher suites: [TLS_ECDHE_RSA_AES256_GCM_SHA384, ...]<br/>Extensions: SNI=google.com, ALPN=h2

    S->>C: ServerHello<br/>Chosen cipher: TLS_ECDHE_RSA_AES256_GCM_SHA384<br/>Random: 32 bytes
    S->>C: Certificate<br/>*.google.com cert + intermediate CA chain
    S->>C: ServerKeyExchange<br/>ECDHE public key (ephemeral)<br/>Signed with server private key
    S->>C: ServerHelloDone

    Note over C,S: Round Trip 2
    C->>C: Verify cert chain against OS root store
    C->>C: Generate pre-master secret from ECDHE keys
    C->>C: Derive session keys (AES key + MAC key)
    C->>S: ClientKeyExchange (ECDHE public key)
    C->>S: ChangeCipherSpec (switch to encryption)
    C->>S: Finished (HMAC of all handshake messages)

    S->>S: Derive same session keys
    S->>C: ChangeCipherSpec
    S->>C: Finished

    Note over C,S: Encrypted data flows — 2 RTTs spent
    C->>S: HTTP GET / (encrypted with AES-256-GCM)

TLS 1.3 Handshake (1 RTT)

sequenceDiagram
    participant C as Client
    participant S as Server

    Note over C,S: Single Round Trip
    C->>S: ClientHello<br/>TLS 1.3<br/>key_share: ECDHE public key (X25519)<br/>supported_groups: X25519, P-256<br/>SNI: google.com<br/>ALPN: h2

    Note over S: Server derives keys NOW from client's key_share
    S->>C: ServerHello + key_share (server ECDHE public key)
    S->>C: EncryptedExtensions (ALPN negotiated)
    S->>C: Certificate (now ENCRYPTED — leaks no metadata)
    S->>C: CertificateVerify (signature over handshake transcript)
    S->>C: Finished (HMAC of transcript)

    Note over C: Client derives same keys, verifies cert, sends Finished
    C->>S: Finished
    C->>S: HTTP GET / (already encrypted — sent with Finished!)

    Note over C,S: 1 RTT total

    Note over C,S: 0-RTT Resumption (optional, replay risk)
    C->>S: ClientHello + early_data (HTTP GET immediately)
    Note over S: No handshake needed — use pre-shared session ticket
    S->>C: HTTP Response

TLS 1.3 vs TLS 1.2:

Feature TLS 1.2 TLS 1.3
Handshake RTTs 2 1 (0 with resumption)
Forward secrecy Optional (RSA key exchange exists) Mandatory (ECDHE only)
Certificate visibility Plaintext to network Encrypted
Weak ciphers RC4, 3DES, MD5 allowed All removed
Session resumption Session ID or ticket PSK (pre-shared key)
Key exchange RSA or ECDHE ECDHE only (X25519, P-256)

TLS Certificate Chain

graph TD
    ROOT["Root CA Certificate<br/>DigiCert Global Root G2<br/>Self-signed<br/>Pre-installed in OS/browsers<br/>Private key stored OFFLINE (air-gapped HSM)"]
    INTER["Intermediate CA Certificate<br/>DigiCert TLS RSA SHA256 2020 CA1<br/>Signed by Root CA<br/>Used for day-to-day signing"]
    LEAF["Leaf Certificate<br/>*.google.com<br/>Public key for TLS<br/>SANs: google.com, www.google.com<br/>Valid: 2024-01-01 to 2025-01-01<br/>Signed by Intermediate CA"]

    ROOT -->|"signs"| INTER
    INTER -->|"signs"| LEAF
    LEAF -->|"presented during TLS handshake"| SERVER["google.com"]

Certificate fields:

Subject:    CN=*.google.com
Issuer:     CN=DigiCert TLS RSA SHA256 2020 CA1
SANs:       DNS:*.google.com, DNS:google.com
Not Before: 2024-01-15 00:00:00 UTC
Not After:  2025-02-14 23:59:59 UTC
Public Key: EC 256-bit (P-256)
Signature:  SHA256WithRSA

Why 3 levels? Root CA private keys are air-gapped — never online. Intermediate CA does daily signing. If an intermediate is compromised, revoke it without touching the root. The root change would require every OS/browser to update their trust store.

Certificate Validation Steps

flowchart TD
    RECV["Client receives certificate"] --> SIG
    SIG["Verify signature chain<br/>Intermediate signed Leaf?<br/>Root signed Intermediate?"] --> TRUST
    TRUST["Root CA in OS trust store?<br/>/etc/ssl/certs/ or system keychain"] --> EXPIRY
    EXPIRY["Not Before <= now <= Not After?"] --> SAN
    SAN["SAN matches requested hostname?<br/>*.google.com matches google.com?"] --> REVOKE
    REVOKE["Not revoked?<br/>CRL or OCSP check"] --> OK["Certificate VALID<br/>proceed with handshake"]
    SIG & TRUST & EXPIRY & SAN & REVOKE -->|"any fail"| ERR["TLS handshake FAILED<br/>connection aborted"]

Debugging TLS

# Full TLS handshake inspection
openssl s_client -connect google.com:443 -servername google.com
# Shows: cipher, cert chain, handshake details

# Check TLS version and cipher
openssl s_client -connect google.com:443 -tls1_3 2>/dev/null | grep "Protocol\|Cipher"

# Certificate expiry check
openssl s_client -connect google.com:443 2>/dev/null | openssl x509 -noout -dates

# Check all SANs on a certificate
openssl s_client -connect google.com:443 2>/dev/null | openssl x509 -noout -text | grep -A2 "Subject Alternative"

# Test specific TLS version (should fail for 1.0/1.1 on modern servers)
openssl s_client -connect google.com:443 -tls1   # TLS 1.0 — should fail
openssl s_client -connect google.com:443 -tls1_2 # TLS 1.2 — should work
openssl s_client -connect google.com:443 -tls1_3 # TLS 1.3 — should work

# curl with verbose TLS info
curl -v --tlsv1.3 https://google.com 2>&1 | grep -i "TLS\|SSL\|cert\|cipher"

Diffie-Hellman Key Exchange — The Mathematics

Diffie-Hellman solves a fundamental problem: two parties can agree on a shared secret over a public channel without ever transmitting the secret itself. Anyone eavesdropping sees only public values and cannot derive the secret.

Classic DH (Finite Field)

Public parameters (known to everyone, including attackers):

p = a large prime number (e.g., 2048-bit)
g = a generator (primitive root mod p, typically g=2 or g=5)

The exchange:

sequenceDiagram
    participant A as Alice (Client)
    participant NET as Public Network (attacker can see)
    participant B as Bob (Server)

    Note over A,B: Public params agreed in advance: p=23, g=5

    A->>A: Pick secret a=6 (never transmitted)
    A->>A: Compute A = g^a mod p = 5^6 mod 23 = 8
    A->>NET: Send A=8 (public)
    NET->>B: A=8

    B->>B: Pick secret b=15 (never transmitted)
    B->>B: Compute B = g^b mod p = 5^15 mod 23 = 19
    B->>NET: Send B=19 (public)
    NET->>A: B=19

    A->>A: Shared secret = B^a mod p = 19^6 mod 23 = 2
    B->>B: Shared secret = A^b mod p = 8^15 mod 23 = 2
    Note over A,B: Both computed the same secret = 2
    Note over NET: Attacker sees p=23, g=5, A=8, B=19<br/>Cannot find secret without solving discrete logarithm

Why it works — the math:

Alice computes: s = B^a mod p = (g^b mod p)^a mod p = g^(ab) mod p
Bob computes:   s = A^b mod p = (g^a mod p)^b mod p = g^(ab) mod p

Both get g^(ab) mod p — the shared secret.
Attacker has A = g^a mod p and B = g^b mod p.
To find a from A = g^a mod p → Discrete Logarithm Problem.
No efficient algorithm known for large p (2048+ bits).

The Discrete Logarithm Problem: Given g, p, and A = g^a mod p, find a. Easy in one direction (exponentiation: fast), computationally infeasible in reverse for large primes.


ECDH — Elliptic Curve Diffie-Hellman (Used in TLS 1.3)

Classic DH uses modular arithmetic. ECDH uses points on an elliptic curve — same mathematical structure, but achieves equivalent security with much smaller keys.

Elliptic curve equation: y² = x³ + ax + b  (over a finite field)

Why elliptic curves?

Algorithm Key size for ~128-bit security Key size for ~256-bit security
Classic DH (finite field) 3072 bits 15360 bits
ECDH 256 bits 512 bits
RSA 3072 bits 15360 bits

Smaller keys → faster computation, smaller TLS handshake messages, less CPU.

Point multiplication on a curve:

graph LR
    G["Generator point G<br/>(public, on the curve)"] -->|"scalar multiplication"| PUB_A["Alice's public key<br/>A = a × G<br/>(a = Alice's private key scalar)"]
    G -->|"scalar multiplication"| PUB_B["Bob's public key<br/>B = b × G<br/>(b = Bob's private key scalar)"]
    PUB_A & PUB_B -->|"key agreement"| SECRET["Shared secret<br/>S = a × B = b × A = ab × G"]

The math:

Public curve parameters: curve equation + generator point G + field order n

Alice: pick private key a (random integer)
       compute public key A = a × G  (point multiplication on curve)

Bob:   pick private key b (random integer)
       compute public key B = b × G

Alice: shared secret = a × B = a × (b × G) = ab × G
Bob:   shared secret = b × A = b × (a × G) = ab × G

Both get the same point ab × G.
Attacker sees G, A, B. Must find a from A = a × G → Elliptic Curve Discrete Log Problem (ECDLP).
No efficient algorithm known.

Point multiplication (a × G) means: add point G to itself a times using the curve's addition law. Repeated doubling makes this O(log a) — fast. Reversing it (finding a given G and a × G) has no known polynomial-time algorithm.


ECDHE — Ephemeral ECDH (what TLS 1.3 actually uses)

ECDHE = ECDH + Ephemeral. A new key pair is generated for every TLS session.

sequenceDiagram
    participant C2 as Client
    participant S2 as Server

    Note over C2,S2: TLS 1.3 ECDHE with X25519 curve

    C2->>C2: Generate ephemeral private key c_priv (random, 32 bytes)
    C2->>C2: Compute public key c_pub = c_priv × G
    C2->>S2: ClientHello: key_share = c_pub

    S2->>S2: Generate ephemeral private key s_priv (random, 32 bytes)
    S2->>S2: Compute public key s_pub = s_priv × G
    S2->>S2: Shared secret = s_priv × c_pub = s_priv × c_priv × G
    S2->>C2: ServerHello: key_share = s_pub

    C2->>C2: Shared secret = c_priv × s_pub = c_priv × s_priv × G

    Note over C2,S2: Both derived same shared secret
    Note over C2,S2: Derive session keys via HKDF:
    Note over C2,S2: client_key = HKDF(shared_secret, "client key")
    Note over C2,S2: server_key = HKDF(shared_secret, "server key")

    Note over C2: c_priv discarded after handshake
    Note over S2: s_priv discarded after handshake
    Note over C2,S2: Forward secrecy: past sessions unrecoverable

Why "ephemeral" = forward secrecy:

  • Static DH: server uses the same private key for all sessions. Steal the private key → decrypt all past recorded traffic.
  • Ephemeral DH: server generates a new private key per session and discards it after the handshake. Steal the long-term private key → can only impersonate future connections, cannot decrypt past traffic. Each session's key material is gone forever.

X25519 — The Curve Used in TLS 1.3

TLS 1.3 mandates support for X25519 (Curve25519). Designed by Daniel J. Bernstein.

Curve equation: y² = x³ + 486662x² + x  (over the field of integers mod 2^255 - 19)
Base point G: u = 9
Field size: 2^255 - 19 (a Mersenne-like prime, chosen for fast arithmetic)
Key size: 256 bits
Security level: ~128 bits

Why X25519 over P-256 (NIST)?

X25519 P-256 (NIST)
Speed Faster (~2× on most CPUs) Slower
Side-channel resistance Designed to resist timing attacks Vulnerable without careful implementation
Standardized by IETF RFC 7748 NIST
Trust No NIST involvement (controversial) NIST-approved
TLS 1.3 Preferred Supported

From Shared Secret to Session Keys (HKDF)

The raw ECDHE output (a point on the curve) is not directly used as an AES key. It goes through HKDF (HMAC-based Key Derivation Function):

graph LR
    ECDHE_OUT["ECDHE shared secret<br/>(32 bytes, the x-coordinate of ab×G)"] --> HKDF_EXT
    HKDF_EXT["HKDF-Extract<br/>salt + IKM --> PRK<br/>(Pseudorandom Key)"] --> HKDF_EXP
    TRANSCRIPT["Handshake transcript hash<br/>(all messages so far, prevents replay)"] --> HKDF_EXP
    HKDF_EXP["HKDF-Expand<br/>PRK + label + context --> OKM<br/>(Output Key Material)"] --> KEYS
    KEYS["client_write_key (AES-256)<br/>server_write_key (AES-256)<br/>client_write_IV (96-bit nonce)<br/>server_write_IV (96-bit nonce)"]

The transcript hash binds the keys to this specific handshake — prevents man-in-the-middle attacks where an attacker replays a valid key exchange from a different session.

Summary of TLS 1.3 key derivation:

ECDHE_secret = s_priv × c_pub  (x-coordinate of the curve point)
early_secret = HKDF-Extract(0, PSK or 0)
handshake_secret = HKDF-Extract(derived(early_secret), ECDHE_secret)
master_secret = HKDF-Extract(derived(handshake_secret), 0)

client_handshake_traffic_secret = HKDF-Expand-Label(handshake_secret, "c hs traffic", transcript_hash)
server_handshake_traffic_secret = HKDF-Expand-Label(handshake_secret, "s hs traffic", transcript_hash)

mTLS — Mutual TLS Between Services

Standard TLS: client verifies server's certificate. mTLS: both sides present and verify certificates. The server also authenticates the client. This is the foundation of zero-trust service-to-service communication.

mTLS handshake vs TLS handshake

TLS (one-way):
  Client → Server: ClientHello
  Server → Client: Certificate (server proves identity)
  Client → Server: [key exchange, Finished]
  Connection established. Server identity verified. Client anonymous.

mTLS (two-way):
  Client → Server: ClientHello
  Server → Client: Certificate + CertificateRequest (server asks for client cert)
  Client → Server: Certificate + CertificateVerify (client proves identity)
  Server verifies client cert against its CA
  Connection established. Both sides authenticated.

cert-manager — automated certificate lifecycle

cert-manager is a Kubernetes controller that automates issuing, renewing, and rotating TLS certificates from multiple sources (Let's Encrypt, Vault, AWS PCA, self-signed CA).

# Install cert-manager
kubectl apply -f https://github.com/cert-manager/cert-manager/releases/latest/download/cert-manager.yaml

# Verify
kubectl get pods -n cert-manager

Self-signed CA for internal service mTLS:

# Step 1: Create a self-signed CA certificate
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: selfsigned-issuer
spec:
  selfSigned: {}

---
# Step 2: Issue a CA certificate from the self-signed issuer
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: internal-ca
  namespace: cert-manager
spec:
  isCA: true
  commonName: internal-ca
  secretName: internal-ca-secret
  privateKey:
    algorithm: ECDSA
    size: 256
  issuerRef:
    name: selfsigned-issuer
    kind: ClusterIssuer
    group: cert-manager.io

---
# Step 3: Create a CA issuer that uses this CA to sign service certs
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: internal-ca-issuer
spec:
  ca:
    secretName: internal-ca-secret   # references the CA cert Secret above

Issue a service certificate:

apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: payments-tls
  namespace: payments
spec:
  secretName: payments-tls-secret    # cert-manager creates this Secret
  duration: 24h                      # short-lived = more secure
  renewBefore: 8h                    # renew when 8h remain
  dnsNames:
    - payments-svc.payments.svc.cluster.local
    - payments-svc.payments.svc
    - payments-svc
  issuerRef:
    name: internal-ca-issuer
    kind: ClusterIssuer
# Mount the cert in your pod
spec:
  volumes:
    - name: tls
      secret:
        secretName: payments-tls-secret  # cert-manager keeps this up to date
  containers:
    - name: payments
      volumeMounts:
        - name: tls
          mountPath: /etc/tls
          readOnly: true
      env:
        - name: TLS_CERT_FILE
          value: /etc/tls/tls.crt
        - name: TLS_KEY_FILE
          value: /etc/tls/tls.key
        - name: CA_CERT_FILE
          value: /etc/tls/ca.crt

Implementing mTLS in a Go service

import (
    "crypto/tls"
    "crypto/x509"
    "os"
)

func newMTLSServer(certFile, keyFile, caFile string) (*http.Server, error) {
    // Load our own certificate and key
    cert, err := tls.LoadX509KeyPair(certFile, keyFile)
    if err != nil {
        return nil, err
    }

    // Load the CA that we use to verify client certificates
    caCert, err := os.ReadFile(caFile)
    if err != nil {
        return nil, err
    }
    caPool := x509.NewCertPool()
    caPool.AppendCertsFromPEM(caCert)

    tlsConfig := &tls.Config{
        Certificates: []tls.Certificate{cert},
        ClientAuth:   tls.RequireAndVerifyClientCert,  // enforce mTLS
        ClientCAs:    caPool,
        MinVersion:   tls.VersionTLS13,
    }

    return &http.Server{
        Addr:      ":8443",
        TLSConfig: tlsConfig,
    }, nil
}

func newMTLSClient(certFile, keyFile, caFile string) (*http.Client, error) {
    cert, _ := tls.LoadX509KeyPair(certFile, keyFile)
    caCert, _ := os.ReadFile(caFile)
    caPool := x509.NewCertPool()
    caPool.AppendCertsFromPEM(caCert)

    tlsConfig := &tls.Config{
        Certificates: []tls.Certificate{cert},  // present client cert
        RootCAs:      caPool,                    // verify server cert
        MinVersion:   tls.VersionTLS13,
    }
    return &http.Client{
        Transport: &http.Transport{TLSClientConfig: tlsConfig},
    }, nil
}

Certificate rotation — zero-downtime

cert-manager renews before expiry and updates the Secret. But pods that mounted the Secret at startup have the old cert in memory — they need to reload without restarting.

Option 1 — watch for file changes:

// Use inotify/fsnotify to reload certs when Secret is updated
watcher, _ := fsnotify.NewWatcher()
watcher.Add("/etc/tls/tls.crt")
go func() {
    for event := range watcher.Events {
        if event.Op&fsnotify.Write != 0 {
            newCert, _ := tls.LoadX509KeyPair(certFile, keyFile)
            tlsConfig.Certificates = []tls.Certificate{newCert}
            log.Println("TLS certificate rotated")
        }
    }
}()

Option 2 — use GetCertificate callback (preferred for servers):

tlsConfig := &tls.Config{
    GetCertificate: func(*tls.ClientHelloInfo) (*tls.Certificate, error) {
        // Called on every new TLS handshake — always reads the latest cert
        cert, err := tls.LoadX509KeyPair(certFile, keyFile)
        return &cert, err
    },
}
// Existing connections use the old cert; new connections use the new cert.
// No restart required.

Option 3 — Istio/Linkerd handle rotation transparently: Service meshes manage cert issuance and rotation for you. Sidecars hold the mTLS identity; your app code makes plain HTTP/gRPC calls; the sidecar wraps them in mTLS. Rotation is invisible to the app.

Let's Encrypt with cert-manager (public services)

apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-prod
spec:
  acme:
    email: ops@myorg.com
    server: https://acme-v02.api.letsencrypt.org/directory
    privateKeySecretRef:
      name: letsencrypt-prod-key
    solvers:
      - http01:                        # HTTP-01 challenge (port 80 reachable)
          ingress:
            class: nginx
      # Alternative: DNS-01 challenge (for wildcard certs / internal clusters)
      - dns01:
          route53:
            region: us-east-1
            hostedZoneID: Z123456789
            role: arn:aws:iam::123456789:role/cert-manager-dns

---
# Use the issuer in an Ingress annotation
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
  tls:
    - hosts: [api.example.com]
      secretName: api-tls-cert    # cert-manager auto-creates and renews this
  rules:
    - host: api.example.com
      ...

Debugging TLS/mTLS issues

# Test TLS connection and inspect certificate chain
openssl s_client -connect payments-svc:8443 \
  -servername payments-svc.payments.svc.cluster.local \
  -showcerts 2>/dev/null | openssl x509 -noout -text

# Test mTLS (present client cert)
openssl s_client -connect payments-svc:8443 \
  -cert /etc/tls/tls.crt \
  -key /etc/tls/tls.key \
  -CAfile /etc/tls/ca.crt

# Check cert expiry
openssl x509 -in /etc/tls/tls.crt -noout -dates
# notBefore=Jun  1 00:00:00 2026 GMT
# notAfter=Jun  2 00:00:00 2026 GMT  ← short-lived, 24h

# Check cert-manager certificate status
kubectl describe certificate payments-tls -n payments
# Conditions:
#   Ready: True   ← cert issued and valid
#   Ready: False  reason: Failed  ← look at Events for CA/DNS errors

# Check cert-manager logs for failures
kubectl logs -n cert-manager \
  -l app.kubernetes.io/component=controller --tail=100

# List all certificates and their expiry
kubectl get certificates -A
# NAME           READY   SECRET               AGE
# payments-tls   True    payments-tls-secret  2d