Skip to content

TLS & PKI Cheat Sheet

Name origin: TLS = Transport Layer Security, the successor to SSL (Secure Sockets Layer). SSL was created by Netscape in 1995; TLS 1.0 (1999) was essentially SSL 3.1 renamed by the IETF. PKI = Public Key Infrastructure. Despite SSL being deprecated since 2015, people still say "SSL certificate" — the correct term is "TLS certificate." TLS 1.3 (2018) is the current standard; it reduced the handshake from 2 round-trips to 1.

TLS Handshake (Simplified)

Client                          Server
  │                               │
  │──── ClientHello ─────────────►│  (supported ciphers, TLS version)
  │                               │
  │◄─── ServerHello + Cert ──────│  (chosen cipher, server certificate)
  │                               │
  │  Verify cert chain            │
  │  Generate pre-master secret   │
  │                               │
  │──── Key Exchange ────────────►│
  │                               │
  │◄──► Encrypted traffic ──────►│

Certificate Chain

Root CA (self-signed, in trust store)
  └── Intermediate CA (signed by Root)
       └── Server Cert (signed by Intermediate)

# Full chain file order:
1. Server certificate
2. Intermediate certificate(s)
3. (Root CA usually NOT included — client has it)

Remember: Certificate chain order matters: Server cert first, then intermediate(s), root CA last (or omitted). Getting this wrong is the #1 cause of "unable to verify the first certificate" errors. The server sends its cert + intermediates; the client matches the chain against its trusted root store. Mnemonic: "Leaf to Root" — like reading a family tree from child to ancestor.

OpenSSL Quick Commands

# Check cert expiry
openssl x509 -noout -dates -in cert.pem

# View full cert details
openssl x509 -noout -text -in cert.pem

# Check cert from a live server
openssl s_client -connect example.com:443 -servername example.com </dev/null 2>/dev/null | \
  openssl x509 -noout -dates

# Verify cert chain
openssl verify -CAfile ca.pem -untrusted intermediate.pem server.pem

# Generate self-signed cert (testing only)
openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem \
  -days 365 -nodes -subj "/CN=test.local"

# Generate CSR
openssl req -new -newkey rsa:4096 -keyout key.pem -out csr.pem \
  -nodes -subj "/CN=myapp.example.com"

# Check if key matches cert
openssl x509 -noout -modulus -in cert.pem | md5sum
openssl rsa -noout -modulus -in key.pem | md5sum
# (must match)

# Decode base64 K8s secret cert
kubectl get secret tls-secret -o jsonpath='{.data.tls\.crt}' | \
  base64 -d | openssl x509 -noout -text

cert-manager

# ClusterIssuer for Let's Encrypt
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-prod
spec:
  acme:
    server: https://acme-v2.api.letsencrypt.org/directory
    email: ops@example.com
    privateKeySecretRef:
      name: letsencrypt-prod-key
    solvers:
    - http01:
        ingress:
          class: nginx
---
# Certificate request
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: myapp-tls
  namespace: production
spec:
  secretName: myapp-tls-secret
  issuerRef:
    name: letsencrypt-prod
    kind: ClusterIssuer
  dnsNames:
  - myapp.example.com
  - www.myapp.example.com
  renewBefore: 360h  # 15 days before expiry

cert-manager Debugging Chain

Certificate → CertificateRequest → Order → Challenge
                                   HTTP-01 or DNS-01
# Check certificate status
kubectl get certificate -A
kubectl describe certificate <name> -n <ns>

# Check certificate request
kubectl get certificaterequest -n <ns>

# Check ACME orders and challenges
kubectl get orders -n <ns>
kubectl get challenges -n <ns>

# cert-manager logs
kubectl logs -n cert-manager deploy/cert-manager --tail=100

# Force renewal
kubectl cert-manager renew <cert-name> -n <ns>

# Status check
kubectl cert-manager status certificate <name> -n <ns>

One-liner: Quick cert expiry check from the command line: openssl s_client -connect example.com:443 </dev/null 2>/dev/null | openssl x509 -noout -dates. This connects to the live server and shows the certificate's Not Before and Not After dates without needing local files.

ACME Challenge Types

Type How It Works Requires Best For
HTTP-01 File served at /.well-known/acme-challenge/ Port 80 accessible Simple web apps
DNS-01 TXT record on _acme-challenge.domain DNS API access Wildcards, private clusters

Ingress TLS Annotations

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  annotations:
    cert-manager.io/cluster-issuer: "letsencrypt-prod"
spec:
  tls:
  - hosts:
    - myapp.example.com
    secretName: myapp-tls-secret   # cert-manager creates this
  rules:
  - host: myapp.example.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: myapp
            port: { number: 80 }

Monitoring & Alerting

# Prometheus alert: cert expiring within 14 days
- alert: CertificateExpiringSoon
  expr: certmanager_certificate_expiration_timestamp_seconds - time() < 14 * 24 * 3600
  labels:
    severity: warning
  annotations:
    summary: "Certificate {{ $labels.name }} expires in less than 14 days"

Common Errors

Error Cause Fix
certificate has expired Renewal failed Check cert-manager logs, force renew
unable to verify the first certificate Missing intermediate Include full chain in cert
certificate is not valid for Wrong SAN/CN Check dnsNames in Certificate spec
ACME challenge failed Port 80 blocked / DNS wrong Check firewall, DNS resolution
rate limit exceeded Too many Let's Encrypt requests Wait, use staging issuer for testing