Project 4: Mutual TLS (mTLS) Mesh - Identity at the Wire
Project 4: Mutual TLS (mTLS) Mesh - Identity at the Wire
Build a complete mTLS infrastructure where services authenticate each other cryptographically, eliminating network-based trust entirely.
Quick Reference
| Attribute | Value |
|---|---|
| Difficulty | Level 3: Advanced |
| Time Estimate | 1 Week |
| Language | Go (Alternatives: Python, Rust) |
| Prerequisites | Basic TLS understanding, Go programming, command-line proficiency |
| Key Topics | PKI, X.509 Certificates, Certificate Authorities, TLS Handshake, SPIFFE |
| Knowledge Area | Cryptography / Distributed Systems |
| Tools | OpenSSL, PKI concepts, SPIFFE |
| Main Book | โZero Trust Networksโ by Gilman & Barth |
1. Learning Objectives
By completing this project, you will:
- Master PKI fundamentals: Build and operate your own Certificate Authority from scratch
- Understand X.509 certificates deeply: Know every field and its security implications
- Implement mutual authentication: Both client proves identity to server AND server proves identity to client
- Automate certificate lifecycle: Issue, rotate, and revoke certificates programmatically
- Design for Zero Trust: Eliminate network-based trust through cryptographic identity
- Debug TLS issues: Diagnose certificate chain problems, SAN mismatches, and expiry issues
- Apply SPIFFE concepts: Understand workload identity in modern cloud-native environments
2. Theoretical Foundation
2.1 Why mTLS is the Gold Standard for Zero Trust
In traditional TLS (what you use when visiting https://example.com), only the server proves its identity. The client verifies โIโm talking to the real example.comโ but the server has no cryptographic proof of who the client is.
Mutual TLS (mTLS) adds client certificates, creating bidirectional authentication:
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Standard TLS vs Mutual TLS โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ โ
โ STANDARD TLS (One-Way): โ
โ โโโโโโโโโโ โโโโโโโโโโ โ
โ โ Client โ โโโโ "Who are you?" โโโโโโโโถ โ Server โ โ
โ โ โ โโโโ Server Certificate โโโโ โ โ โ
โ โ โ (Proves server ID) โ โ โ
โ โโโโโโโโโโ โโโโโโโโโโ โ
โ โ Client knows server is legitimate โ
โ โ Server has NO IDEA who client is โ
โ โ
โ MUTUAL TLS (Two-Way): โ
โ โโโโโโโโโโ โโโโโโโโโโ โ
โ โ Client โ โโโโ "Who are you?" โโโโโโโโถ โ Server โ โ
โ โ โ โโโโ Server Certificate โโโโ โ โ โ
โ โ โ โโโโ Client Certificate โโโโถ โ โ โ
โ โ โ (Proves client ID) โ โ โ
โ โโโโโโโโโโ โโโโโโโโโโ โ
โ โ Client knows server is legitimate โ
โ โ Server knows client is legitimate โ
โ โ Both identities are CRYPTOGRAPHICALLY proven โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Why this matters for Zero Trust:
- An attacker on the network cannot impersonate either party
- Traffic cannot be sniffed (encrypted with session keys derived from handshake)
- No reliance on network position - identity is in the certificate, not the IP address
- Every connection is authenticated, regardless of source network
2.2 Public Key Infrastructure (PKI) Fundamentals
PKI is the framework that makes certificate-based authentication possible. At its core, it solves the key distribution problem: How do you securely share public keys with entities youโve never met?
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ PKI Trust Hierarchy โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ Root CA โ โ
โ โ (Self-Signed) โ โ
โ โ HIGHLY PROTECTED โ โ
โ โ (Offline/HSM) โ โ
โ โโโโโโโโโโโโฌโโโโโโโโโโโ โ
โ โ โ
โ Signs certificates โ
โ โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ โ โ โ
โ โผ โผ โผ โ
โ โโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโ โ
โ โ Intermediate CA โ โ Intermediate CA โ โ Intermediate CA โ โ
โ โ (Services) โ โ (Users) โ โ (Devices) โ โ
โ โโโโโโโโโโฌโโโโโโโโโ โโโโโโโโโโฌโโโโโโโโโ โโโโโโโโโโฌโโโโโโโโโ โ
โ โ โ โ โ
โ โผ โผ โผ โ
โ โโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโ โ
โ โ payment-service โ โ alice@corp.com โ โ laptop-001 โ โ
โ โ certificate โ โ certificate โ โ certificate โ โ
โ โโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโ โ
โ โ
โ Certificate Chain for payment-service: โ
โ [payment-service cert] โ [Intermediate CA] โ [Root CA] โ
โ โ
โ Verification: "I trust Root CA. Root CA signed Intermediate. โ
โ Intermediate signed payment-service. Therefore, โ
โ I trust payment-service." โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Key PKI Concepts:
| Concept | Definition | Security Implication |
|---|---|---|
| Root CA | The ultimate trust anchor, self-signed | Compromise = entire PKI compromised |
| Intermediate CA | Signed by Root, signs end-entity certs | Limits blast radius if compromised |
| End-Entity Cert | The actual service/user certificate | Whatโs presented during handshake |
| Certificate Chain | Path from end-entity to trusted root | Must be complete and valid |
| Trust Store | Collection of trusted root certificates | What CA certs your system trusts |
2.3 X.509 Certificate Structure
X.509 is the standard format for public key certificates. Understanding its structure is essential for debugging TLS issues:
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ X.509 Certificate Structure โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ TBSCertificate (To Be Signed - the actual certificate data) โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค โ
โ โ โ โ
โ โ Version: 3 (v3) โ โ
โ โ Serial Number: Unique ID (e.g., 17:32:A4:...) โ โ
โ โ Signature Algorithm: sha256WithRSAEncryption โ โ
โ โ โ โ
โ โ Issuer: CN=My Root CA, O=MyOrg โ โ
โ โ (WHO signed this cert) โ โ
โ โ โ โ
โ โ Validity: โ โ
โ โ Not Before: 2024-01-01 00:00:00 UTC โ โ
โ โ Not After: 2024-01-02 00:00:00 UTC โ SHORT-LIVED! โ โ
โ โ โ โ
โ โ Subject: CN=payment-service โ โ
โ โ (WHO this cert identifies) โ โ
โ โ โ โ
โ โ Subject Public Key: <RSA 2048-bit public key> โ โ
โ โ โ โ
โ โ Extensions (X.509 v3): โ โ
โ โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ โ Subject Alternative Name (SAN): โ CRITICAL FOR mTLS โโ โ
โ โ โ DNS: payment-service.prod.local โโ โ
โ โ โ DNS: payment.internal โโ โ
โ โ โ URI: spiffe://trust-domain/ns/prod/sa/payment โโ โ
โ โ โ IP: 10.0.1.50 โโ โ
โ โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโคโ โ
โ โ โ Key Usage: โโ โ
โ โ โ digitalSignature, keyEncipherment โโ โ
โ โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโคโ โ
โ โ โ Extended Key Usage: โโ โ
โ โ โ TLS Web Server Authentication (for server certs) โโ โ
โ โ โ TLS Web Client Authentication (for client certs) โโ โ
โ โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโคโ โ
โ โ โ Basic Constraints: โโ โ
โ โ โ CA: FALSE (this is an end-entity, not a CA) โโ โ
โ โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ
โ Signature Algorithm: sha256WithRSAEncryption โ
โ Signature: <CA's signature over TBSCertificate> โ
โ (This proves the CA issued this cert) โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Critical Fields for mTLS:
| Field | Purpose | Common Issues |
|---|---|---|
| Subject Alternative Name (SAN) | Lists all valid identities for this cert | Connection fails if hostname not in SAN |
| Extended Key Usage | Restricts what the cert can be used for | Client auth requires clientAuth EKU |
| Validity Period | When cert is valid | Short-lived certs (hours) reduce breach impact |
| Issuer | Who signed this cert | Must chain to trusted CA |
2.4 Certificate Signing Request (CSR)
A CSR is how an entity requests a certificate from a CA. It contains the public key and requested identity, signed by the corresponding private key (proving ownership):
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ CSR Generation and Signing Flow โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ โ
โ Step 1: Service Generates Key Pair โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ payment-service โ โ
โ โ โ โ
โ โ Private Key: โโโโโโโโโโโโโโโโ (NEVER leaves service) โ โ
โ โ Public Key: ABCD1234... (Goes in CSR) โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ โ
โ Step 2: Create and Sign CSR โ โ
โ โผ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ Certificate Signing Request (CSR) โ โ
โ โ โ โ
โ โ Subject: CN=payment-service โ โ
โ โ Public Key: ABCD1234... โ โ
โ โ Requested SANs: payment-service.prod.local โ โ
โ โ Signature: <signed with private key> โ Proves key ownership โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ โ
โ Step 3: Submit to CA โ โ
โ โผ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ Certificate Authority โ โ
โ โ โ โ
โ โ 1. Verify CSR signature (proves key ownership) โ โ
โ โ 2. Validate identity (is this really payment-service?) โ โ
โ โ 3. Apply policy (cert duration, allowed SANs, etc.) โ โ
โ โ 4. Sign certificate with CA private key โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ โ
โ Step 4: Return Signed Certificate โ โ
โ โผ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ X.509 Certificate โ โ
โ โ โ โ
โ โ Subject: CN=payment-service โ โ
โ โ Issuer: CN=My CA โ โ
โ โ Valid: 2024-01-01 to 2024-01-02 (24 hours) โ โ
โ โ SANs: payment-service.prod.local โ โ
โ โ CA Signature: <signed by CA> โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
2.5 The TLS Handshake: Standard vs Mutual
Understanding the TLS handshake is critical for debugging connection issues:
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ TLS 1.3 Handshake (Standard) โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ โ
โ Client Server โ
โ โ โ โ
โ โ โโโโโโโโโโโ ClientHello โโโโโโโโโโโโโโโโโโโโโโโถ โ โ
โ โ - Supported cipher suites โ โ
โ โ - Supported TLS versions โ โ
โ โ - Client random (32 bytes) โ โ
โ โ - Key share (for key exchange) โ โ
โ โ โ โ
โ โ โโโโโโโโโโโโ ServerHello โโโโโโโโโโโโโโโโโโโโโโโ โ โ
โ โ - Chosen cipher suite โ โ
โ โ - Server random (32 bytes) โ โ
โ โ - Key share (for key exchange) โ โ
โ โ โ โ
โ โ โโโโโโ EncryptedExtensions โโโโโโโโโโโโโโโโโโโโ โ โ
โ โ โโโโโโ Certificate (Server's) โโโโโโโโโโโโโโโโ โ โ Server โ
โ โ โโโโโโ CertificateVerify โโโโโโโโโโโโโโโโโโโโโโ โ proves โ
โ โ โโโโโโ Finished โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ identity โ
โ โ โ โ
โ โ โโโโโโโโโโโ Finished โโโโโโโโโโโโโโโโโโโโโโโโโโถ โ โ
โ โ โ โ
โ โ โโโโโโโโโโโโโ Application Data โโโโโโโโโโโโโโโโถ โ โ
โ โ (Encrypted with session keys) โ โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ TLS 1.3 Handshake (Mutual TLS) โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ โ
โ Client Server โ
โ โ โ โ
โ โ โโโโโโโโโโโ ClientHello โโโโโโโโโโโโโโโโโโโโโโโถ โ โ
โ โ โ โ
โ โ โโโโโโโโโโโโ ServerHello โโโโโโโโโโโโโโโโโโโโโโโ โ โ
โ โ โโโโโโ EncryptedExtensions โโโโโโโโโโโโโโโโโโโโ โ โ
โ โ โโโโโโ CertificateRequest โโโโโโโโโโโโโโโโโโโโ โ โ Server โ
โ โ (Server asks for client cert) โ requests โ
โ โ โโโโโโ Certificate (Server's) โโโโโโโโโโโโโโโโ โ client โ
โ โ โโโโโโ CertificateVerify โโโโโโโโโโโโโโโโโโโโโโ โ auth โ
โ โ โโโโโโ Finished โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ โ
โ โ โ โ
โ โ โโโโโโโโโโโ Certificate (Client's) โโโโโโโโโโโถ โ โ Client โ
โ โ โโโโโโโโโโโ CertificateVerify โโโโโโโโโโโโโโโโถ โ proves โ
โ โ (Signed with client's private key) โ identity โ
โ โ โโโโโโโโโโโ Finished โโโโโโโโโโโโโโโโโโโโโโโโโโถ โ โ
โ โ โ โ
โ โ โโโโโโโโโโโโโ Application Data โโโโโโโโโโโโโโโโถ โ โ
โ โ โ โ
โ RESULT: Both parties have cryptographic proof of identity โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
What Happens at Each Step:
| Message | Purpose | Whatโs Verified |
|---|---|---|
| ClientHello | Initiate handshake, propose parameters | - |
| ServerHello | Accept parameters, provide key share | - |
| CertificateRequest | Server asks client to authenticate | - |
| Certificate (Server) | Server sends its cert chain | Client verifies chain to trusted CA |
| CertificateVerify (Server) | Proves server owns private key | Signature over handshake transcript |
| Certificate (Client) | Client sends its cert chain | Server verifies chain to trusted CA |
| CertificateVerify (Client) | Proves client owns private key | Signature over handshake transcript |
| Finished | Confirms handshake integrity | MAC over entire handshake |
2.6 SPIFFE: Workload Identity for the Cloud
SPIFFE (Secure Production Identity Framework for Everyone) standardizes workload identity:
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ SPIFFE Architecture โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ โ
โ SPIFFE ID Format: โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ spiffe://trust-domain/path/to/workload โ โ
โ โ โโโโโโโโโฌโโโโโโโโโโ โโโโโโโโโโโฌโโโโโโโโ โ โ
โ โ Trust Domain Workload Path โ โ
โ โ (your org) (identifies specific workload) โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ
โ Examples: โ
โ spiffe://example.com/ns/production/sa/payment-service โ
โ spiffe://example.com/region/us-west/app/frontend โ
โ spiffe://example.com/cluster/k8s-prod/pod/api-server-abc123 โ
โ โ
โ SVID (SPIFFE Verifiable Identity Document): โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ X.509 Certificate with SPIFFE ID in SAN: โ โ
โ โ โ โ
โ โ Subject: (can be empty or generic) โ โ
โ โ SAN URI: spiffe://example.com/ns/prod/sa/payment โ โ
โ โ Valid: Short-lived (1 hour typical) โ โ
โ โ Issuer: SPIFFE-compliant CA โ โ
โ โ โ โ
โ โ The SPIFFE ID in the URI SAN IS the identity. โ โ
โ โ Traditional Subject field is not used for identity. โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ
โ SPIRE (SPIFFE Runtime Environment): โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ โ โ
โ โ โโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโ โ โ
โ โ โ SPIRE Serverโโโโ Attestation โโโโถโ SPIRE Agent โ โ โ
โ โ โ (CA + DB) โ โ (per node) โ โ โ
โ โ โโโโโโโโโโโโโโโ โโโโโโโโฌโโโโโโโ โ โ
โ โ โ โ โ
โ โ Unix Socket API โ โ
โ โ โ โ โ
โ โ โโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโ โ โ
โ โ โผ โผ โผ โ โ
โ โ โโโโโโโโโโโโ โโโโโโโโโโโโ โโโโโโโโโโโโ โ
โ โ โ Workload โ โ Workload โ โ Workload โ โ
โ โ โ A โ โ B โ โ C โ โ
โ โ โโโโโโโโโโโโ โโโโโโโโโโโโ โโโโโโโโโโโโ โ
โ โ โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Why SPIFFE Matters:
- Platform-agnostic identity: Works across Kubernetes, VMs, bare metal
- Automatic rotation: SPIRE handles cert lifecycle
- Attestation-based: Workloads prove identity through multiple signals
- Zero Trust native: No implicit trust based on network
2.7 Certificate Revocation: When Things Go Wrong
What happens when a private key is compromised or a certificate needs to be invalidated before expiry?
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Certificate Revocation Methods โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ โ
โ METHOD 1: Certificate Revocation Lists (CRLs) โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ โ โ
โ โ CA publishes list of revoked certificate serial numbers: โ โ
โ โ โ โ
โ โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ โ
โ โ โ CRL (Certificate Revocation List) โ โ โ
โ โ โ โ โ โ
โ โ โ Issuer: CN=My CA โ โ โ
โ โ โ This Update: 2024-01-15 00:00:00 โ โ โ
โ โ โ Next Update: 2024-01-16 00:00:00 โ โ โ
โ โ โ โ โ โ
โ โ โ Revoked Certificates: โ โ โ
โ โ โ Serial: 1234:5678 Revoked: 2024-01-14 Key Compromise โ โ
โ โ โ Serial: ABCD:EF01 Revoked: 2024-01-15 Superseded โ โ
โ โ โ โ โ โ
โ โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ โ
โ โ โ โ
โ โ Problems: โ โ
โ โ - CRLs grow large over time (linear growth) โ โ
โ โ - Clients must download entire CRL โ โ
โ โ - Stale data between updates โ โ
โ โ โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ
โ METHOD 2: Online Certificate Status Protocol (OCSP) โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ โ โ
โ โ Client OCSP Responder CA โ โ
โ โ โ โ โ โ โ
โ โ โ "Is cert 1234 valid?"โ โ โ โ
โ โ โโโโโโโโโโโโโโโโโโโโโโโโถ โ โ โ
โ โ โ โ (checks status) โ โ โ
โ โ โโโโโโโโโโโโโโโโโโโโโโโโ โ โ โ
โ โ โ "Good" / "Revoked" โ โ โ โ
โ โ โ โ
โ โ Advantages: Real-time, small response size โ โ
โ โ Problems: Privacy (CA knows what certs you're checking) โ โ
โ โ Availability (if OCSP down, what to do?) โ โ
โ โ โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ
โ METHOD 3: Short-Lived Certificates (Zero Trust Approach) โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ โ โ
โ โ Issue certificates valid for 1 hour instead of 1 year. โ โ
โ โ No revocation needed - just don't renew. โ โ
โ โ โ โ
โ โ Timeline: โ โ
โ โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค โ โ
โ โ 0 15m 30m 45m 60m โ โ
โ โ โ โ โ โ โ โ โ
โ โ โโโโโโโโโโโโโโโโ Cert 1 โโโโโโโโโโโโโโโโโโโค โ โ
โ โ โโโโโโโโโโโโโโโโ Cert 2 โโโโโโโโโโโโโโโโโโโค โ โ
โ โ (overlap for smooth rotation) โ โ
โ โ โ โ
โ โ Compromise at 35m? Cert expires at 60m anyway. โ โ
โ โ Maximum exposure: remaining validity period. โ โ
โ โ โ โ
โ โ This is the SPIFFE/Istio/modern cloud-native approach. โ โ
โ โ โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
2.8 Identity Bootstrapping: The Chicken-and-Egg Problem
How does a new service prove who it is to get its first certificate?
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Identity Bootstrapping Methods โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ โ
โ THE PROBLEM: โ
โ New service starts โ needs certificate to communicate โ
โ But CA asks: "Prove you are payment-service" โ
โ Service has no cert yet to prove anything! โ
โ โ
โ SOLUTION 1: Pre-shared Token/Secret โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ 1. Operator pre-provisions a one-time token โ โ
โ โ 2. Service presents token to CA โ โ
โ โ 3. CA verifies token, issues certificate โ โ
โ โ 4. Token is invalidated โ โ
โ โ โ โ
โ โ Used by: Vault, manual PKI โ โ
โ โ Problem: How do you securely distribute the token? โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ
โ SOLUTION 2: Platform Attestation (SPIRE approach) โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ โ โ
โ โ Service proves identity by WHERE it's running: โ โ
โ โ โ โ
โ โ Kubernetes: Pod UID, Service Account, Namespace โ โ
โ โ AWS: Instance ID, IAM Role, Security Groups โ โ
โ โ GCP: Service Account, Project, Zone โ โ
โ โ Linux: Process UID, binary hash, parent process โ โ
โ โ โ โ
โ โ โโโโโโโโโโโโโ โโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโ โ โ
โ โ โ Workload โ โโโโถ โ SPIRE Agent โ โโโโถ โ Kubernetes โ โ โ
โ โ โ โ โ โ โ API โ โ โ
โ โ โโโโโโโโโโโโโ โโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโ โ โ
โ โ โ โ โ โ โ
โ โ โ โ "Pod abc123 in namespace โ โ
โ โ โ โ prod with SA payment-svc" โ โ
โ โ โ โ โ โ โ
โ โ โ โผ โ โ โ
โ โ โ โโโโโโโโโโโโโโโโ โ โ โ
โ โ โ โ SPIRE Server โโโโโโโโโโโโโโ โ โ
โ โ โ โ (matches to โ โ โ
โ โ โ โ SPIFFE ID) โ โ โ
โ โ โ โโโโโโโโโโโโโโโโ โ โ
โ โ โ โ โ โ
โ โ โโโโ SVID (cert) โโโโโ โ โ
โ โ โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ
โ SOLUTION 3: Instance Identity Documents (Cloud Provider) โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ AWS: Instance Identity Document signed by AWS โ โ
โ โ GCP: Instance Identity Token from metadata server โ โ
โ โ Azure: Managed Identity tokens โ โ
โ โ โ โ
โ โ CA trusts cloud provider's attestation. โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
3. Complete Project Specification
3.1 What You Will Build
A complete mTLS mesh system consisting of:
- Mini Certificate Authority (CA): Issues and manages short-lived X.509 certificates
- Two Services: Server and client that communicate exclusively via mTLS
- Automatic Certificate Rotation: Refresh certificates before expiry without connection drops
- Certificate Inspection CLI: Debug and verify certificate chains
3.2 Functional Requirements
FR-1: Mini CA
- Generate and secure a Root CA key pair
- Accept Certificate Signing Requests (CSRs) via API
- Issue certificates with configurable validity (default: 1 hour)
- Include SPIFFE-style URIs in Subject Alternative Names
- Maintain an in-memory list of issued certificates
- Support certificate revocation (mark as revoked)
FR-2: Server Service
- Expose an HTTPS endpoint requiring client certificates
- Validate client certificates against the CAโs root
- Extract and log the clientโs identity from the certificate
- Reject connections from unknown/expired/revoked certificates
FR-3: Client Service
- Obtain a certificate from the CA at startup
- Present certificate when connecting to the server
- Validate the serverโs certificate against the CAโs root
- Successfully complete an mTLS handshake
FR-4: Certificate Rotation
- Monitor certificate expiry time
- Automatically request new certificate before expiry (e.g., at 50% lifetime)
- Swap certificates without dropping active connections
- Log rotation events
FR-5: Certificate Inspector CLI
- Parse and display certificate details
- Verify certificate chains
- Check certificate validity (dates, signatures)
- Display SPIFFE IDs from SAN URIs
3.3 Non-Functional Requirements
- Security: Private keys never leave their respective components
- Availability: Certificate rotation must not cause connection failures
- Observability: All TLS events must be loggable
- Portability: Run on any system with Go installed
3.4 System Architecture Overview
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ mTLS Mesh Architecture โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ Mini CA โ โ
โ โ โโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโ โ โ
โ โ โ Root Key Pair โ โ CSR API โ โ Issued Certs DB โ โ โ
โ โ โ (Protected) โ โ /api/sign โ โ (In-Memory) โ โ โ
โ โ โโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโ โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ โ
โ โโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโ โ
โ โ CA Root Certificate โ โ
โ โ (Distributed to all) โ โ
โ โโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโ โ
โ โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ โ โ โ
โ โผ โ โผ โ
โ โโโโโโโโโโโโโโโโโโโ โ โโโโโโโโโโโโโโโโโโโ โ
โ โ Server Service โโโโโโโโ mTLS โโโโโโโโโโโโโโโถโ Client Service โ โ
โ โ โ โ โ โ โ
โ โ - Server cert โ โ โ - Client cert โ โ
โ โ - Server key โ โ โ - Client key โ โ
โ โ - CA root (for โ โ โ - CA root (for โ โ
โ โ client verify)โ โ โ server verify)โ โ
โ โ - Rotation โ โ โ - Rotation โ โ
โ โ goroutine โ โ goroutine โ โ
โ โโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโ โ
โ โ
โ Data Flow: โ
โ 1. Service generates key pair locally โ
โ 2. Service creates CSR, sends to CA โ
โ 3. CA validates and signs, returns certificate โ
โ 4. Service uses cert+key for mTLS connections โ
โ 5. Rotation monitor refreshes cert before expiry โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
4. Real World Outcome
When you complete this project, hereโs exactly what youโll see:
Terminal 1: Start the Mini CA
$ ./mtls-ca --port 8443 --cert-duration 1h
[CA] Generating Root CA key pair (RSA-4096)...
[CA] Root CA certificate created:
Subject: CN=mtls-mesh-root-ca
Serial: 1A:2B:3C:4D:5E:6F:70:81
Valid: 2024-01-15 00:00:00 UTC to 2034-01-15 00:00:00 UTC
Key Usage: Certificate Sign, CRL Sign
[CA] API server listening on :8443
[CA] Endpoints:
POST /api/v1/sign - Submit CSR, receive signed certificate
GET /api/v1/ca - Download CA root certificate
POST /api/v1/revoke - Revoke a certificate
[CA] Ready to issue certificates.
Terminal 2: Start the Server Service
$ ./mtls-server --ca-url http://localhost:8443 --listen :9443
[SERVER] Generating service key pair (ECDSA P-256)...
[SERVER] Creating CSR for identity: spiffe://mesh.local/service/api-server
[SERVER] Requesting certificate from CA at http://localhost:8443/api/v1/sign...
[CA] (on CA side) Received CSR:
Subject: CN=api-server
Requested SAN: spiffe://mesh.local/service/api-server
[SERVER] Certificate issued:
Serial: 2B:3C:4D:5E:6F:70:81:92
Issuer: CN=mtls-mesh-root-ca
Subject: CN=api-server
SAN URI: spiffe://mesh.local/service/api-server
Valid: 2024-01-15 10:00:00 UTC to 2024-01-15 11:00:00 UTC
Expires in: 59m 59s
[SERVER] Starting mTLS server on :9443
[SERVER] Client certificate required: YES
[SERVER] Trusted CA: CN=mtls-mesh-root-ca
[SERVER] Certificate rotation scheduled:
Next rotation: 2024-01-15 10:30:00 UTC (30 minutes)
[SERVER] Ready to accept mTLS connections.
Terminal 3: Start the Client Service
$ ./mtls-client --ca-url http://localhost:8443 --server https://localhost:9443
[CLIENT] Generating service key pair (ECDSA P-256)...
[CLIENT] Creating CSR for identity: spiffe://mesh.local/service/payment-worker
[CLIENT] Requesting certificate from CA...
[CLIENT] Certificate issued:
Serial: 3C:4D:5E:6F:70:81:92:A3
Subject: CN=payment-worker
SAN URI: spiffe://mesh.local/service/payment-worker
Valid: 2024-01-15 10:00:05 UTC to 2024-01-15 11:00:05 UTC
[CLIENT] Initiating mTLS connection to localhost:9443...
[CLIENT] TLS Handshake Details:
Cipher Suite: TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384
TLS Version: TLS 1.3
Server Identity: spiffe://mesh.local/service/api-server
Our Identity: spiffe://mesh.local/service/payment-worker
[CLIENT] Connection established! Server verified our identity.
[CLIENT] Sending request: GET /api/status
[CLIENT] Response: 200 OK
Body: {"status":"healthy","caller":"spiffe://mesh.local/service/payment-worker"}
[CLIENT] mTLS mesh communication successful!
Terminal 2 (Server) - Showing Client Connection
[SERVER] New connection from 127.0.0.1:54321
[SERVER] Client certificate presented:
Subject: CN=payment-worker
SAN URI: spiffe://mesh.local/service/payment-worker
Issuer: CN=mtls-mesh-root-ca
Serial: 3C:4D:5E:6F:70:81:92:A3
[SERVER] Client identity verified: spiffe://mesh.local/service/payment-worker
[SERVER] Handling request: GET /api/status
[SERVER] Response sent to payment-worker: 200 OK
Certificate Inspection with OpenSSL
# View client certificate details
$ openssl x509 -in client.crt -text -noout
Certificate:
Data:
Version: 3 (0x2)
Serial Number:
3c:4d:5e:6f:70:81:92:a3
Signature Algorithm: ecdsa-with-SHA256
Issuer: CN = mtls-mesh-root-ca
Validity
Not Before: Jan 15 10:00:05 2024 GMT
Not After : Jan 15 11:00:05 2024 GMT
Subject: CN = payment-worker
Subject Public Key Info:
Public Key Algorithm: id-ecPublicKey
Public-Key: (256 bit)
pub:
04:8a:3b:...
ASN1 OID: prime256v1
NIST CURVE: P-256
X509v3 extensions:
X509v3 Subject Alternative Name:
URI:spiffe://mesh.local/service/payment-worker
X509v3 Key Usage: critical
Digital Signature, Key Encipherment
X509v3 Extended Key Usage:
TLS Web Client Authentication
X509v3 Basic Constraints: critical
CA:FALSE
Signature Algorithm: ecdsa-with-SHA256
30:45:02:21:...
Testing with curl
# This succeeds - mTLS with valid client cert
$ curl --cacert ca.crt \
--cert client.crt \
--key client.key \
https://localhost:9443/api/status
{"status":"healthy","caller":"spiffe://mesh.local/service/payment-worker"}
# This fails - no client certificate
$ curl --cacert ca.crt https://localhost:9443/api/status
curl: (56) OpenSSL SSL_read: error:14094412:SSL routines:ssl3_read_bytes:sslv3 alert bad certificate
# This fails - wrong CA (server doesn't trust this CA)
$ curl --cacert other-ca.crt \
--cert other-client.crt \
--key other-client.key \
https://localhost:9443/api/status
curl: (60) SSL certificate problem: unable to get local issuer certificate
Certificate Rotation in Action
# 30 minutes later...
[SERVER] Certificate rotation triggered (50% lifetime reached)
[SERVER] Generating new CSR...
[SERVER] New certificate issued:
Serial: 4D:5E:6F:70:81:92:A3:B4
Valid: 2024-01-15 10:30:00 UTC to 2024-01-15 11:30:00 UTC
[SERVER] Hot-swapping certificate...
[SERVER] Active connections: 3 (will use new cert for new connections)
[SERVER] Certificate rotation complete.
[SERVER] Next rotation: 2024-01-15 11:00:00 UTC
The Core Question Youโre Answering
โHow can services prove their identity to each other cryptographically, eliminating the need to trust the network?โ
This question strikes at the heart of Zero Trust networking. In traditional architectures, being โinside the networkโ grants implicit trust - if a service can reach another service, itโs assumed to be legitimate. mTLS flips this model entirely: every connection must prove identity through cryptographic certificates, regardless of network position.
Your challenge is to build a system where:
- A service cannot communicate without possessing a valid certificate signed by a trusted authority
- Both the caller and the callee verify each otherโs identity before exchanging any data
- Network position (IP address, subnet, VPN) provides zero trust - only the certificate matters
- Compromised credentials expire automatically through short-lived certificates
Concepts You Must Understand First
Before diving into implementation, ensure you have a solid grasp of these foundational concepts:
1. X.509 Certificates and PKI (Public Key Infrastructure)
What to know:
- X.509 is the standard format for public key certificates used in TLS
- A certificate binds a public key to an identity (like โpayment-serviceโ)
- The binding is attested by a Certificate Authorityโs signature
- PKI is the hierarchy of trust: Root CA -> Intermediate CA -> End-Entity Certificates
Key questions to answer:
- What fields in an X.509 certificate are critical for mTLS?
- Why do we use intermediate CAs instead of signing everything with the root?
- What makes a certificate โtrustedโ?
2. TLS Handshake: Standard vs Mutual
What to know:
- Standard TLS: Only the server proves its identity (what you see with HTTPS)
- Mutual TLS: Both client AND server prove identity via certificates
- The handshake establishes: identity verification, cipher agreement, session key derivation
Key questions to answer:
- At what point in the handshake does the client send its certificate?
- What triggers the client to send a certificate (hint: CertificateRequest)?
- How does each party prove it owns the private key corresponding to its certificate?
3. Certificate Signing Requests (CSR)
What to know:
- A CSR is how you request a certificate from a CA
- It contains your public key and requested identity (Subject, SANs)
- The CSR is signed with your private key, proving you own the key pair
- The CA validates the CSR and creates a signed certificate
Key questions to answer:
- Why does the CSR need to be signed by the requestor?
- What fields from the CSR end up in the final certificate?
- Can the CA add fields not in the CSR? (Yes - like validity period, extensions)
4. SPIFFE/SPIRE Workload Identity
What to know:
- SPIFFE provides a standard identity format:
spiffe://trust-domain/path - SPIFFE IDs are placed in the URI SAN of X.509 certificates (SVIDs)
- SPIRE is the reference implementation: attestation, identity issuance, rotation
Key questions to answer:
- Why use SPIFFE IDs instead of traditional Subject (CN) fields?
- How does a workload prove its identity to get its first certificate?
- What is โattestationโ and why is it necessary?
5. Certificate Rotation and Lifecycle
What to know:
- Short-lived certificates (hours, not years) reduce breach impact
- Rotation must happen before expiry to avoid service disruption
- Atomic certificate swap prevents connection failures during rotation
Key questions to answer:
- Whatโs a good rotation threshold? (Common: 50% of lifetime)
- How do active connections handle certificate rotation?
- What happens if rotation fails? (Retry logic, fallback)
Questions to Guide Your Design
Work through these questions before writing code. Theyโll shape your architecture:
Certificate Authority Design
- Where will the CA private key be stored? In memory? On disk? In an HSM? What are the tradeoffs?
- How will you generate serial numbers? Must be unique - what happens if you reuse one?
- What certificate validity period will you use? 1 hour? 24 hours? Why?
- Will you support certificate revocation? If not, why do short-lived certs make this acceptable?
Identity Model
- How will you structure SPIFFE IDs? What path format? (e.g.,
/ns/namespace/sa/serviceaccount) - How will a service prove it should receive a particular identity? (The bootstrapping problem)
- Will you validate requested identities, or trust the requestor?
mTLS Configuration
- What TLS version will you require? TLS 1.2 minimum? TLS 1.3 only?
- What
ClientAuthmode will you use?RequireAndVerifyClientCertis the only truly secure option for mTLS - How will you configure the trust store? RootCAs vs ClientCAs - do you understand the difference?
Rotation Strategy
- When will rotation trigger? Fixed schedule? Percentage of lifetime?
- How will you atomically swap certificates? What synchronization is needed?
- What happens to existing connections during rotation?
Thinking Exercise
Before writing any code, work through this exercise on paper or a whiteboard:
Trace the Complete mTLS Handshake
Draw the message flow between Client and Server for a TLS 1.3 mTLS connection. For each message, note:
- ClientHello
- What does the client send?
- What capabilities is it advertising?
- ServerHello + EncryptedExtensions
- What has the server decided?
- Why are extensions encrypted now?
- CertificateRequest
- What is the server asking for?
- What CAs will the clientโs cert need to chain to?
- Certificate (Server) + CertificateVerify
- Whatโs in the Certificate message?
- What does CertificateVerify prove? (Not just โI have a certโ)
- Certificate (Client) + CertificateVerify
- Why is this step necessary for mTLS?
- What exactly is the client signing?
- Finished (both sides)
- What does Finished prove about the handshake?
Bonus: What would happen if an attacker intercepted all these messages and replayed them later?
Hints in Layers
If you get stuck, reveal hints progressively. Try to solve each phase before looking at hints.
Layer 1: Getting Started
Hint: First step with OpenSSL
Start by generating certificates manually with OpenSSL before writing any Go code. This will help you understand what your CA needs to produce:
# Generate a CA key and self-signed cert
openssl ecparam -name prime256v1 -genkey -noout -out ca.key
openssl req -new -x509 -key ca.key -out ca.crt -days 365 -subj "/CN=My CA"
Once you can generate, sign, and verify certificates manually, translating to Go becomes straightforward.
Layer 2: Goโs crypto/tls Configuration
Hint: The critical tls.Config settings for mTLS
For the server to require client certificates:
tlsConfig := &tls.Config{
ClientAuth: tls.RequireAndVerifyClientCert, // This is the key!
ClientCAs: caCertPool, // CAs that can sign client certs
}
For the client to present its certificate:
tlsConfig := &tls.Config{
Certificates: []tls.Certificate{clientCert}, // Your cert + key
RootCAs: caCertPool, // CAs that can sign server certs
}
Common mistake: Using RootCAs on the server for client verification - thatโs wrong! RootCAs is for outgoing connections, ClientCAs is for verifying incoming client certs.
Layer 3: Adding SPIFFE IDs to Certificates
Hint: URI SANs in Go's x509 package
SPIFFE IDs go in the URI Subject Alternative Name. In Go:
import "net/url"
spiffeID, _ := url.Parse("spiffe://mesh.local/service/payment")
template := &x509.Certificate{
// ... other fields ...
URIs: []*url.URL{spiffeID}, // This adds the SPIFFE ID as a URI SAN
}
To extract a SPIFFE ID from a certificate:
for _, uri := range cert.URIs {
if uri.Scheme == "spiffe" {
fmt.Println("SPIFFE ID:", uri.String())
}
}
Layer 4: Certificate Rotation
Hint: Using GetCertificate for dynamic certificates
Instead of setting Certificates directly, use a callback:
tlsConfig := &tls.Config{
GetCertificate: func(chi *tls.ClientHelloInfo) (*tls.Certificate, error) {
cm.mu.RLock()
defer cm.mu.RUnlock()
return cm.currentCert, nil // Return the latest cert
},
}
For client-side rotation, use GetClientCertificate:
tlsConfig := &tls.Config{
GetClientCertificate: func(cri *tls.CertificateRequestInfo) (*tls.Certificate, error) {
cm.mu.RLock()
defer cm.mu.RUnlock()
return cm.currentCert, nil
},
}
The rotation goroutine atomically swaps cm.currentCert - new connections automatically use the new cert.
Layer 5: Debugging Certificate Issues
Hint: Common error messages and their causes
| Error | Likely Cause |
|---|---|
certificate signed by unknown authority |
CA not in trust store (RootCAs or ClientCAs) |
bad certificate |
Client didnโt send cert, or server rejected it |
certificate is valid for X, not Y |
Hostname not in certificateโs SAN |
certificate has expired |
Check NotAfter, also check system clock! |
certificate specifies incompatible key usage |
Missing clientAuth or serverAuth EKU |
Debug with OpenSSL:
# Verbose connection test
openssl s_client -connect localhost:9443 \
-cert client.crt -key client.key \
-CAfile ca.crt -debug
5. Solution Architecture
5.1 Component Design
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Component Architecture โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ mini-ca/ โ โ
โ โ โโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโ โ โ
โ โ โ main.go โ โ ca.go โ โ handlers.go โ โ โ
โ โ โ โ โ โ โ โ โ โ
โ โ โ - CLI flags โ โ - GenerateRootโ โ - POST /sign โ โ โ
โ โ โ - Start serverโ โ - SignCSR โ โ - GET /ca โ โ โ
โ โ โ โ โ - Track issuedโ โ - POST /revoke โ โ โ
โ โ โโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโ โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ mtls-server/ โ โ
โ โ โโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโ โ โ
โ โ โ main.go โ โ certmgr.go โ โ server.go โ โ โ
โ โ โ โ โ โ โ โ โ โ
โ โ โ - CLI flags โ โ - GenerateKey โ โ - mTLS config โ โ โ
โ โ โ - Bootstrap โ โ - CreateCSR โ โ - RequireClientCertโ โ โ
โ โ โ - Start serverโ โ - RequestCert โ โ - Identity extractโ โ โ
โ โ โ โ โ - Rotate() โ โ โ โ โ
โ โ โโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโ โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ mtls-client/ โ โ
โ โ โโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโ โ โ
โ โ โ main.go โ โ certmgr.go โ โ client.go โ โ โ
โ โ โ โ โ โ โ โ โ โ
โ โ โ - CLI flags โ โ - GenerateKey โ โ - mTLS config โ โ โ
โ โ โ - Bootstrap โ โ - CreateCSR โ โ - Dial with cert โ โ โ
โ โ โ - Make requestโ โ - RequestCert โ โ - Verify server โ โ โ
โ โ โ โ โ - Rotate() โ โ โ โ โ
โ โ โโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโ โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ shared/ โ โ
โ โ โโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโ โ โ
โ โ โ spiffe.go โ โ certutil.go โ โ x509ext.go โ โ โ
โ โ โ โ โ โ โ โ โ โ
โ โ โ - ParseID โ โ - PEM encode โ โ - Add SANs โ โ โ
โ โ โ - ValidateID โ โ - PEM decode โ โ - Add EKU โ โ โ
โ โ โ - FormatID โ โ - Chain verifyโ โ - Parse exts โ โ โ
โ โ โโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโ โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
5.2 Key Data Structures
// CertificateRequest represents a CSR submission
type CertificateRequest struct {
CSR []byte `json:"csr"` // PEM-encoded CSR
RequestedID string `json:"requested_id"` // SPIFFE ID to include in SAN
TTL Duration `json:"ttl"` // Requested validity duration
}
// CertificateResponse from the CA
type CertificateResponse struct {
Certificate []byte `json:"certificate"` // PEM-encoded cert
Chain [][]byte `json:"chain"` // Intermediate certs (if any)
ExpiresAt time.Time `json:"expires_at"`
Serial string `json:"serial"`
}
// IssuedCertificate tracked by the CA
type IssuedCertificate struct {
Serial *big.Int
Subject string
SpiffeID string
IssuedAt time.Time
ExpiresAt time.Time
Revoked bool
RevokedAt time.Time
}
// CertificateManager handles cert lifecycle
type CertificateManager struct {
mu sync.RWMutex
privateKey crypto.PrivateKey
certificate *tls.Certificate
caRoots *x509.CertPool
spiffeID string
caURL string
rotateAt time.Time
}
5.3 Certificate Rotation Algorithm
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Certificate Rotation Flow โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ โ
โ Certificate Timeline: โ
โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค โ
โ โ Issue 25% 50% 75% Expiry โ โ
โ โโโโโโโโโโโผโโโโโโโโโผโโโโโโโโโผโโโโโโโโโผ โ โ
โ โ โ โ โ โ โ โ
โ โ โ Rotation Window โ โ โ โ
โ โ โโโโโโโโโโโโโโโโโถโ โ โ โ
โ โ โ โ โ โ โ
โ โ โ Get new cert โ โ โ โ
โ โ โ before old โ โ โ โ
โ โ โ expires โ โ โ โ
โ โ
โ Rotation Process: โ
โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ 1. Timer fires at 50% of cert lifetime โ โ
โ โ โ โ
โ โ 2. Generate new key pair (optional - can reuse key) โ โ
โ โ โ โ
โ โ 3. Create new CSR with same identity โ โ
โ โ โ โ
โ โ 4. Submit to CA, receive new certificate โ โ
โ โ โ โ
โ โ 5. Atomically swap certificate in memory: โ โ
โ โ โ โ
โ โ manager.mu.Lock() โ โ
โ โ manager.certificate = newCert โ โ
โ โ manager.rotateAt = calculateNextRotation(newCert) โ โ
โ โ manager.mu.Unlock() โ โ
โ โ โ โ
โ โ 6. Existing connections continue with old cert (fine) โ โ
โ โ New connections use new cert โ โ
โ โ โ โ
โ โ 7. Schedule next rotation โ โ
โ โ โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
6. Phased Implementation Guide
Phase 1: Generate Root CA with OpenSSL (Day 1)
Before writing any Go code, understand the OpenSSL commands that your CA will replicate:
Task 1.1: Create the Root CA
# Create directory structure
mkdir -p pki/{root,intermediate,certs,private}
chmod 700 pki/private
# Generate Root CA private key (RSA 4096-bit)
openssl genrsa -out pki/private/root-ca.key 4096
chmod 600 pki/private/root-ca.key
# Create Root CA configuration file
cat > pki/root-ca.conf << 'EOF'
[req]
distinguished_name = req_distinguished_name
x509_extensions = v3_ca
prompt = no
[req_distinguished_name]
CN = mtls-mesh-root-ca
O = My Organization
C = US
[v3_ca]
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always,issuer
basicConstraints = critical, CA:TRUE, pathlen:1
keyUsage = critical, keyCertSign, cRLSign
EOF
# Generate self-signed Root CA certificate (10 years validity)
openssl req -new -x509 -sha256 \
-key pki/private/root-ca.key \
-out pki/root/root-ca.crt \
-days 3650 \
-config pki/root-ca.conf
# Verify the Root CA certificate
openssl x509 -in pki/root/root-ca.crt -text -noout
Expected Output:
Certificate:
Data:
Version: 3 (0x2)
Serial Number: ... (random)
Signature Algorithm: sha256WithRSAEncryption
Issuer: CN = mtls-mesh-root-ca, O = My Organization, C = US
Validity
Not Before: Jan 15 00:00:00 2024 GMT
Not After : Jan 15 00:00:00 2034 GMT
Subject: CN = mtls-mesh-root-ca, O = My Organization, C = US
...
X509v3 extensions:
X509v3 Basic Constraints: critical
CA:TRUE, pathlen:1
X509v3 Key Usage: critical
Certificate Sign, CRL Sign
Checkpoint: You have a self-signed Root CA certificate with proper CA extensions.
Phase 2: Sign Server and Client Certificates (Day 2)
Task 2.1: Create Server Certificate
# Generate server private key (ECDSA P-256 for efficiency)
openssl ecparam -name prime256v1 -genkey -noout -out pki/private/server.key
# Create server CSR configuration with SAN
cat > pki/server.conf << 'EOF'
[req]
distinguished_name = req_distinguished_name
req_extensions = v3_req
prompt = no
[req_distinguished_name]
CN = api-server
[v3_req]
subjectAltName = @alt_names
keyUsage = critical, digitalSignature, keyEncipherment
extendedKeyUsage = serverAuth, clientAuth
basicConstraints = CA:FALSE
[alt_names]
URI.1 = spiffe://mesh.local/service/api-server
DNS.1 = localhost
DNS.2 = api-server.mesh.local
IP.1 = 127.0.0.1
EOF
# Generate CSR
openssl req -new -sha256 \
-key pki/private/server.key \
-out pki/certs/server.csr \
-config pki/server.conf
# View the CSR
openssl req -in pki/certs/server.csr -text -noout
Task 2.2: Sign the Server Certificate with the CA
# Create signing configuration (for the CA to use)
cat > pki/sign-server.conf << 'EOF'
[v3_server]
subjectAltName = @alt_names
keyUsage = critical, digitalSignature, keyEncipherment
extendedKeyUsage = serverAuth, clientAuth
basicConstraints = CA:FALSE
[alt_names]
URI.1 = spiffe://mesh.local/service/api-server
DNS.1 = localhost
DNS.2 = api-server.mesh.local
IP.1 = 127.0.0.1
EOF
# Sign the CSR (1 hour validity for short-lived cert demo)
openssl x509 -req \
-in pki/certs/server.csr \
-CA pki/root/root-ca.crt \
-CAkey pki/private/root-ca.key \
-CAcreateserial \
-out pki/certs/server.crt \
-days 0 -hours 1 \
-sha256 \
-extfile pki/sign-server.conf \
-extensions v3_server
# Verify the certificate
openssl x509 -in pki/certs/server.crt -text -noout
# Verify the chain
openssl verify -CAfile pki/root/root-ca.crt pki/certs/server.crt
Task 2.3: Create Client Certificate
# Generate client private key
openssl ecparam -name prime256v1 -genkey -noout -out pki/private/client.key
# Create client CSR configuration
cat > pki/client.conf << 'EOF'
[req]
distinguished_name = req_distinguished_name
req_extensions = v3_req
prompt = no
[req_distinguished_name]
CN = payment-worker
[v3_req]
subjectAltName = URI:spiffe://mesh.local/service/payment-worker
keyUsage = critical, digitalSignature, keyEncipherment
extendedKeyUsage = clientAuth
basicConstraints = CA:FALSE
EOF
# Generate and sign client certificate
openssl req -new -sha256 \
-key pki/private/client.key \
-out pki/certs/client.csr \
-config pki/client.conf
openssl x509 -req \
-in pki/certs/client.csr \
-CA pki/root/root-ca.crt \
-CAkey pki/private/root-ca.key \
-CAcreateserial \
-out pki/certs/client.crt \
-days 0 -hours 1 \
-sha256 \
-extfile pki/client.conf \
-extensions v3_req
Checkpoint: You have CA, server cert, and client cert. All verify against the CA.
Phase 3: Configure Go Server with RequireAndVerifyClientCert (Days 3-4)
Task 3.1: Create the mTLS Server
// server/main.go
package main
import (
"crypto/tls"
"crypto/x509"
"fmt"
"io/ioutil"
"log"
"net/http"
)
func main() {
// Load CA certificate for client verification
caCert, err := ioutil.ReadFile("pki/root/root-ca.crt")
if err != nil {
log.Fatalf("Failed to load CA cert: %v", err)
}
caCertPool := x509.NewCertPool()
if !caCertPool.AppendCertsFromPEM(caCert) {
log.Fatal("Failed to parse CA cert")
}
// Load server certificate and key
serverCert, err := tls.LoadX509KeyPair(
"pki/certs/server.crt",
"pki/private/server.key",
)
if err != nil {
log.Fatalf("Failed to load server cert: %v", err)
}
// Configure TLS with mutual authentication
tlsConfig := &tls.Config{
Certificates: []tls.Certificate{serverCert},
ClientCAs: caCertPool,
ClientAuth: tls.RequireAndVerifyClientCert, // THE KEY SETTING!
MinVersion: tls.VersionTLS13,
}
// Create server with handler that extracts client identity
mux := http.NewServeMux()
mux.HandleFunc("/api/status", func(w http.ResponseWriter, r *http.Request) {
// Extract client identity from verified certificate
if r.TLS != nil && len(r.TLS.PeerCertificates) > 0 {
clientCert := r.TLS.PeerCertificates[0]
// Extract SPIFFE ID from SAN URIs
spiffeID := "unknown"
for _, uri := range clientCert.URIs {
if uri.Scheme == "spiffe" {
spiffeID = uri.String()
break
}
}
log.Printf("Request from client: %s (CN=%s)",
spiffeID, clientCert.Subject.CommonName)
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, `{"status":"healthy","caller":"%s"}`, spiffeID)
}
})
server := &http.Server{
Addr: ":9443",
Handler: mux,
TLSConfig: tlsConfig,
}
log.Println("Starting mTLS server on :9443")
log.Println("Client certificate: REQUIRED")
// ListenAndServeTLS with empty strings because certs are in TLSConfig
log.Fatal(server.ListenAndServeTLS("", ""))
}
Task 3.2: Create the mTLS Client
// client/main.go
package main
import (
"crypto/tls"
"crypto/x509"
"fmt"
"io"
"io/ioutil"
"log"
"net/http"
)
func main() {
// Load CA certificate for server verification
caCert, err := ioutil.ReadFile("pki/root/root-ca.crt")
if err != nil {
log.Fatalf("Failed to load CA cert: %v", err)
}
caCertPool := x509.NewCertPool()
if !caCertPool.AppendCertsFromPEM(caCert) {
log.Fatal("Failed to parse CA cert")
}
// Load client certificate and key
clientCert, err := tls.LoadX509KeyPair(
"pki/certs/client.crt",
"pki/private/client.key",
)
if err != nil {
log.Fatalf("Failed to load client cert: %v", err)
}
// Configure TLS with client certificate
tlsConfig := &tls.Config{
Certificates: []tls.Certificate{clientCert},
RootCAs: caCertPool,
MinVersion: tls.VersionTLS13,
}
// Create HTTP client with mTLS
client := &http.Client{
Transport: &http.Transport{
TLSClientConfig: tlsConfig,
},
}
// Make request
resp, err := client.Get("https://localhost:9443/api/status")
if err != nil {
log.Fatalf("Request failed: %v", err)
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
fmt.Printf("Response: %s\n", body)
fmt.Printf("TLS Version: %s\n", tlsVersionName(resp.TLS.Version))
fmt.Printf("Cipher Suite: %s\n", tls.CipherSuiteName(resp.TLS.CipherSuite))
}
func tlsVersionName(v uint16) string {
switch v {
case tls.VersionTLS13:
return "TLS 1.3"
case tls.VersionTLS12:
return "TLS 1.2"
default:
return "Unknown"
}
}
Checkpoint: Server requires client certificate. Client presents certificate. Both verify each other.
Phase 4: Programmatic Certificate Generation (Days 5-6)
Task 4.1: Build the Mini CA
// ca/ca.go
package main
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"math/big"
"net/url"
"time"
)
type CA struct {
rootCert *x509.Certificate
rootKey *ecdsa.PrivateKey
certSerial *big.Int
issuedCerts map[string]*IssuedCertificate
}
func NewCA() (*CA, error) {
// Generate CA key pair
rootKey, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
if err != nil {
return nil, err
}
// Generate serial number
serialNumber, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128))
if err != nil {
return nil, err
}
// Create CA certificate template
rootTemplate := &x509.Certificate{
SerialNumber: serialNumber,
Subject: pkix.Name{
CommonName: "mtls-mesh-root-ca",
Organization: []string{"My Organization"},
},
NotBefore: time.Now(),
NotAfter: time.Now().AddDate(10, 0, 0), // 10 years
KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign,
BasicConstraintsValid: true,
IsCA: true,
MaxPathLen: 1,
}
// Self-sign the CA certificate
rootCertDER, err := x509.CreateCertificate(
rand.Reader, rootTemplate, rootTemplate,
&rootKey.PublicKey, rootKey,
)
if err != nil {
return nil, err
}
rootCert, err := x509.ParseCertificate(rootCertDER)
if err != nil {
return nil, err
}
return &CA{
rootCert: rootCert,
rootKey: rootKey,
certSerial: big.NewInt(1),
issuedCerts: make(map[string]*IssuedCertificate),
}, nil
}
func (ca *CA) SignCSR(csrPEM []byte, spiffeID string, ttl time.Duration) ([]byte, error) {
// Parse CSR
block, _ := pem.Decode(csrPEM)
if block == nil {
return nil, fmt.Errorf("failed to decode CSR PEM")
}
csr, err := x509.ParseCertificateRequest(block.Bytes)
if err != nil {
return nil, fmt.Errorf("failed to parse CSR: %w", err)
}
// Verify CSR signature (proves key ownership)
if err := csr.CheckSignature(); err != nil {
return nil, fmt.Errorf("CSR signature invalid: %w", err)
}
// Generate serial number
ca.certSerial = new(big.Int).Add(ca.certSerial, big.NewInt(1))
// Parse SPIFFE ID
spiffeURI, err := url.Parse(spiffeID)
if err != nil {
return nil, fmt.Errorf("invalid SPIFFE ID: %w", err)
}
// Create certificate template
template := &x509.Certificate{
SerialNumber: ca.certSerial,
Subject: csr.Subject,
NotBefore: time.Now(),
NotAfter: time.Now().Add(ttl),
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth},
URIs: []*url.URL{spiffeURI},
DNSNames: []string{"localhost", csr.Subject.CommonName + ".mesh.local"},
IPAddresses: []net.IP{net.ParseIP("127.0.0.1")},
}
// Sign the certificate
certDER, err := x509.CreateCertificate(
rand.Reader, template, ca.rootCert,
csr.PublicKey, ca.rootKey,
)
if err != nil {
return nil, fmt.Errorf("failed to sign certificate: %w", err)
}
// Encode to PEM
certPEM := pem.EncodeToMemory(&pem.Block{
Type: "CERTIFICATE",
Bytes: certDER,
})
// Track issued certificate
ca.issuedCerts[ca.certSerial.String()] = &IssuedCertificate{
Serial: ca.certSerial,
Subject: csr.Subject.CommonName,
SpiffeID: spiffeID,
IssuedAt: time.Now(),
ExpiresAt: time.Now().Add(ttl),
}
return certPEM, nil
}
func (ca *CA) RootCertPEM() []byte {
return pem.EncodeToMemory(&pem.Block{
Type: "CERTIFICATE",
Bytes: ca.rootCert.Raw,
})
}
Checkpoint: CA can programmatically generate root cert and sign CSRs with SPIFFE IDs.
Phase 5: Automatic Certificate Rotation (Day 7)
Task 5.1: Implement Certificate Manager with Rotation
// shared/certmgr.go
package shared
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"sync"
"time"
)
type CertificateManager struct {
mu sync.RWMutex
privateKey *ecdsa.PrivateKey
certificate *tls.Certificate
caCert *x509.Certificate
spiffeID string
caURL string
stopCh chan struct{}
}
func NewCertificateManager(caURL, spiffeID string, caCertPEM []byte) (*CertificateManager, error) {
// Parse CA certificate
block, _ := pem.Decode(caCertPEM)
caCert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return nil, err
}
cm := &CertificateManager{
spiffeID: spiffeID,
caURL: caURL,
caCert: caCert,
stopCh: make(chan struct{}),
}
// Get initial certificate
if err := cm.obtainCertificate(); err != nil {
return nil, err
}
// Start rotation goroutine
go cm.rotationLoop()
return cm, nil
}
func (cm *CertificateManager) obtainCertificate() error {
// Generate new key pair
privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
return err
}
// Create CSR
csrTemplate := &x509.CertificateRequest{
Subject: pkix.Name{
CommonName: extractCN(cm.spiffeID),
},
}
csrDER, err := x509.CreateCertificateRequest(rand.Reader, csrTemplate, privateKey)
if err != nil {
return err
}
csrPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE REQUEST", Bytes: csrDER})
// Request certificate from CA (simplified - in reality, use HTTP client)
certPEM, err := cm.requestCertFromCA(csrPEM)
if err != nil {
return err
}
// Parse the certificate
block, _ := pem.Decode(certPEM)
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return err
}
// Create tls.Certificate
tlsCert := &tls.Certificate{
Certificate: [][]byte{cert.Raw, cm.caCert.Raw},
PrivateKey: privateKey,
Leaf: cert,
}
// Atomic swap
cm.mu.Lock()
cm.privateKey = privateKey
cm.certificate = tlsCert
cm.mu.Unlock()
log.Printf("[CertMgr] Certificate obtained. Expires: %v", cert.NotAfter)
return nil
}
func (cm *CertificateManager) rotationLoop() {
for {
cm.mu.RLock()
cert := cm.certificate.Leaf
cm.mu.RUnlock()
// Calculate rotation time (50% of lifetime)
lifetime := cert.NotAfter.Sub(cert.NotBefore)
rotateAt := cert.NotBefore.Add(lifetime / 2)
sleepDuration := time.Until(rotateAt)
if sleepDuration <= 0 {
// Already past rotation time, rotate now
sleepDuration = time.Second
}
select {
case <-time.After(sleepDuration):
log.Println("[CertMgr] Rotation triggered")
if err := cm.obtainCertificate(); err != nil {
log.Printf("[CertMgr] Rotation failed: %v. Retrying in 30s", err)
time.Sleep(30 * time.Second)
}
case <-cm.stopCh:
return
}
}
}
// GetCertificate returns the current certificate (called by tls.Config)
func (cm *CertificateManager) GetCertificate(chi *tls.ClientHelloInfo) (*tls.Certificate, error) {
cm.mu.RLock()
defer cm.mu.RUnlock()
return cm.certificate, nil
}
// GetClientCertificate returns the current certificate for client connections
func (cm *CertificateManager) GetClientCertificate(cri *tls.CertificateRequestInfo) (*tls.Certificate, error) {
cm.mu.RLock()
defer cm.mu.RUnlock()
return cm.certificate, nil
}
func (cm *CertificateManager) Stop() {
close(cm.stopCh)
}
Task 5.2: Use CertificateManager in Server
// server/main.go (updated)
func main() {
// ... load CA cert ...
cm, err := shared.NewCertificateManager(
"http://localhost:8443",
"spiffe://mesh.local/service/api-server",
caCertPEM,
)
if err != nil {
log.Fatal(err)
}
defer cm.Stop()
tlsConfig := &tls.Config{
GetCertificate: cm.GetCertificate, // Dynamic certificate!
ClientCAs: caCertPool,
ClientAuth: tls.RequireAndVerifyClientCert,
MinVersion: tls.VersionTLS13,
}
// ... rest of server setup ...
}
Checkpoint: Certificates rotate automatically. New connections use fresh certs.
7. Testing Strategy
7.1 Test Categories
| Category | Purpose | Examples |
|---|---|---|
| Unit Tests | Test certificate parsing, SPIFFE ID validation | ParseSPIFFEID correctly extracts components |
| Integration Tests | Test full mTLS handshake | Client + Server complete handshake successfully |
| Negative Tests | Verify rejection of invalid certs | Expired cert rejected, wrong CA rejected |
| Rotation Tests | Verify seamless rotation | Active connections survive rotation |
| Security Tests | Attack simulation | Man-in-the-middle fails, cert replay fails |
7.2 Critical Test Cases
Test 1: Basic mTLS Handshake
# Start server and client, verify successful communication
./mtls-server &
./mtls-client
# Expected: "status: healthy, caller: spiffe://..."
Test 2: Missing Client Certificate
curl --cacert pki/root/root-ca.crt https://localhost:9443/api/status
# Expected: TLS handshake failure (bad certificate)
Test 3: Expired Certificate
# Create certificate with 1 second validity, wait, try to use
# Expected: Certificate rejected as expired
Test 4: Wrong CA
# Create separate CA, issue cert from it, try to connect
# Expected: "certificate signed by unknown authority"
Test 5: Certificate Rotation Under Load
# Start continuous request loop
while true; do ./mtls-client; sleep 0.1; done &
# Wait for rotation to occur
sleep 1800 # 30 minutes for 1-hour certs at 50% rotation
# Verify no connection failures in logs
Test 6: SAN Mismatch
# Connect to hostname not in certificate's SAN
# Expected: "x509: certificate is valid for X, not Y"
7.3 OpenSSL Verification Commands
# Verify server with mTLS
openssl s_client -connect localhost:9443 \
-CAfile pki/root/root-ca.crt \
-cert pki/certs/client.crt \
-key pki/private/client.key \
-verify_return_error
# Check certificate chain
openssl verify -CAfile pki/root/root-ca.crt pki/certs/server.crt
# View certificate details
openssl x509 -in pki/certs/server.crt -text -noout | grep -A5 "Subject Alternative Name"
# Test TLS handshake timing
time openssl s_client -connect localhost:9443 </dev/null
8. Common Pitfalls and Debugging
8.1 Certificate Chain Issues
| Symptom | Cause | Solution |
|---|---|---|
| โcertificate signed by unknown authorityโ | CA not in trust store | Add CA cert to RootCAs (client) or ClientCAs (server) |
| โx509: certificate has expiredโ | Certificate past NotAfter | Issue new cert, check system clock |
| โx509: certificate is valid for X, not Yโ | Hostname not in SAN | Add correct DNS/IP/URI to certificate |
| โtls: bad certificateโ | Client didnโt provide cert | ClientAuth must be RequireAndVerifyClientCert |
| โx509: certificate specifies an incompatible key usageโ | Wrong EKU | Ensure serverAuth for server, clientAuth for client |
8.2 Debugging TLS Handshake
// Add to tls.Config for debugging
tlsConfig := &tls.Config{
// ... other settings ...
// Log handshake state (Go 1.16+)
VerifyConnection: func(cs tls.ConnectionState) error {
log.Printf("TLS Version: %x", cs.Version)
log.Printf("Cipher Suite: %s", tls.CipherSuiteName(cs.CipherSuite))
log.Printf("Server Name: %s", cs.ServerName)
for i, cert := range cs.PeerCertificates {
log.Printf("Peer Cert %d: %s", i, cert.Subject)
for _, uri := range cert.URIs {
log.Printf(" SAN URI: %s", uri)
}
}
return nil
},
}
8.3 OpenSSL Debugging
# Verbose TLS handshake debug
openssl s_client -connect localhost:9443 -debug -msg
# Show all certificate extensions
openssl x509 -in cert.pem -text -noout | grep -A100 "X509v3 extensions"
# Verify certificate against CA
openssl verify -verbose -CAfile ca.crt cert.pem
# Check if cert has required EKU
openssl x509 -in cert.pem -purpose -noout
8.4 Common Go Mistakes
// WRONG: Not including intermediate certs in chain
tlsCert := tls.Certificate{
Certificate: [][]byte{leafCert.Raw}, // Missing CA!
PrivateKey: key,
}
// CORRECT: Include full chain
tlsCert := tls.Certificate{
Certificate: [][]byte{leafCert.Raw, intermediateCert.Raw},
PrivateKey: key,
}
// WRONG: Using RootCAs for server's client verification
tlsConfig := &tls.Config{
RootCAs: caCertPool, // This is for OUTGOING connections!
ClientAuth: tls.RequireAndVerifyClientCert,
}
// CORRECT: Use ClientCAs for verifying client certificates
tlsConfig := &tls.Config{
ClientCAs: caCertPool, // This verifies INCOMING client certs
ClientAuth: tls.RequireAndVerifyClientCert,
}
9. Extensions and Challenges
9.1 Beginner Extensions
- Certificate Inspector CLI: Parse and display any X.509 cert with all fields
- JSON Output: Add
--format jsonfor machine-readable CA responses - Certificate Logging: Log all issued certificates to a file for audit
- Graceful Shutdown: Ensure rotation goroutine stops cleanly
9.2 Intermediate Extensions
- Intermediate CA: Add a layer between root and end-entity certs
- Certificate Revocation: Implement revocation API and check during handshake
- OCSP Responder: Build a simple OCSP server for revocation checking
- Multiple SANs: Support DNS, IP, and URI SANs from CSR
- Key Algorithm Choice: Support RSA-2048, RSA-4096, P-256, P-384
9.3 Advanced Extensions
- SPIRE Integration: Replace your CA with SPIRE for production-grade identity
- Hardware Security Module (HSM): Store CA key in simulated HSM (or real PKCS#11)
- Certificate Transparency: Log certificates to a CT log
- mTLS Sidecar Proxy: Build an Envoy-like proxy that handles mTLS for apps
- Workload Attestation: Implement Kubernetes or AWS attestation for bootstrapping
- Policy Engine Integration: Only issue certs based on policy (integrate with P2)
9.4 Challenge Problems
-
Zero-Downtime CA Rotation: How do you rotate the root CA without breaking all services?
-
Cross-Cluster mTLS: Two SPIFFE trust domains need to communicate. Implement federation.
-
Certificate Pinning: Implement cert pinning with graceful rotation support.
-
Audit Trail: Every certificate action (issue, revoke, rotate) must be cryptographically logged.
10. Books That Will Help
| Topic | Book | Chapter/Section |
|---|---|---|
| Zero Trust fundamentals | โZero Trust Networksโ by Gilman & Barth | Chapters 4-5 (Device Trust, Application Trust) |
| mTLS and service mesh | โZero Trust Networksโ by Gilman & Barth | Chapter 6 (Network) |
| TLS protocol deep dive | โSerious Cryptography, 2nd Edโ by Aumasson | Chapter 13 (TLS) |
| X.509 and PKI | โSecurity in Computing, 5th Edโ by Pfleeger | Chapter 7.2 (Public Key Infrastructure) |
| Applied cryptography | โCryptography Engineeringโ by Ferguson, Schneier, Kohno | Chapters 16-18 (Key Management) |
| Go crypto/tls | โNetwork Programming with Goโ by Woodbeck | Chapter 9 (TLS) |
| SPIFFE/SPIRE | โSolving the Bottom Turtleโ (free) | spiffe.io documentation |
| Certificate management | โBulletproof SSL and TLSโ by Ristic | Chapters on PKI deployment |
11. Interview Questions
After completing this project, youโll be prepared for these questions:
Conceptual Questions
- โExplain mutual TLS and why itโs important for zero trust.โ
- Expected: Both parties authenticate, not just server. Eliminates network-based trust.
- Bonus: Discuss how it prevents lateral movement after network breach.
- โWhat happens during a TLS handshake? Walk through mTLS specifically.โ
- Expected: ClientHello, ServerHello, Certificates, CertificateVerify, Finished
- Bonus: Explain CertificateRequest for client auth, key derivation
- โHow would you implement certificate rotation without downtime?โ
- Expected: Use GetCertificate callback, atomic swap, overlap period
- Bonus: Discuss connection draining, monitoring rotation success
- โWhat is SPIFFE and how does it solve workload identity?โ
- Expected: SPIFFE ID format, SVIDs, platform-agnostic identity
- Bonus: Explain SPIRE architecture, attestation plugins
- โWhy use short-lived certificates instead of CRLs/OCSP?โ
- Expected: Reduces blast radius, eliminates revocation complexity
- Bonus: Discuss 1-hour certs in Istio, SPIRE rotation
Troubleshooting Questions
- โA client is getting โcertificate signed by unknown authorityโ. How do you debug?โ
- Check if correct CA is in trust store
- Verify certificate chain is complete
- Check for intermediate CA issues
- Use
openssl verifyto trace the problem
- โmTLS works in staging but fails in production. What could be wrong?โ
- Clock skew (certificate not yet valid / expired)
- Different CA certificates
- Load balancer terminating TLS
- Missing SNI in client
- โHow would you design certificate issuance for a Kubernetes cluster?โ
- Use SPIRE with Kubernetes attestation
- Service account identity maps to SPIFFE ID
- Node agent handles attestation
- Short-lived certs with automatic rotation
12. Self-Assessment Checklist
Before considering this project complete, verify:
Understanding
- I can explain the difference between standard TLS and mTLS
- I can draw the TLS handshake with client authentication
- I understand X.509 certificate structure including SANs and EKUs
- I can explain why short-lived certificates are preferred in Zero Trust
- I understand the identity bootstrapping problem and solutions
- I can explain SPIFFE IDs and their purpose
Implementation
- My CA can generate a root certificate with correct extensions
- My CA can sign CSRs and add SPIFFE URIs to SANs
- My server requires and verifies client certificates
- My client presents certificates and verifies server certificates
- Certificate rotation works without connection interruption
- Error handling provides useful debugging information
Security
- Private keys never leave their generating component
- Certificates are short-lived (1 hour default)
- Certificate chain is complete and verifiable
- Invalid/expired/revoked certificates are rejected
- TLS 1.3 is enforced
Operations
- I can debug certificate issues using OpenSSL
- I can verify certificate chains manually
- I understand how to monitor certificate expiry
- I can explain how to rotate the CA key safely
13. Conclusion
This project teaches you the cryptographic foundation of Zero Trust: identity at the wire level. By building your own mTLS mesh, you understand not just how to configure TLS, but why each component exists and how they work together to eliminate network-based trust.
The skills from this project apply directly to:
- Service mesh implementation (Istio, Linkerd, Consul Connect)
- Cloud-native security (SPIFFE/SPIRE deployments)
- Enterprise PKI management
- Secure microservices architecture
- Security engineering interviews
After completing this project, youโll never again treat TLS as a black box. Youโll understand the handshake, the certificates, the chain of trust, and how to debug when things go wrong.
This guide was expanded from ZERO_TRUST_ARCHITECTURE_DEEP_DIVE.md. For the complete learning path, see the project index.