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¶
# 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 |