Frontend Labs
⋯

Day 41 — Mutual TLS (mTLS)

Learning Objectives

  • Implement zero-trust service authentication: both sides verify each other
  • Extract client identity from certificates — the foundation of your service mesh
  • Understand why mTLS > API keys for service-to-service auth

Key Concepts

Standard TLS (one-way):
  Server proves its identity to client via certificate
  Client is anonymous (authenticated by other means: API key, JWT, etc.)

mTLS (mutual):
  Server proves its identity to client via certificate
  Client ALSO proves its identity via certificate
  Both signed by a shared CA → only authorized clients can connect

Zero Trust with mTLS:
  No API keys to rotate, lose, or steal
  Certificate expiry enforces rotation automatically
  Identity is cryptographic, not a secret string
  Used by: Istio, Linkerd, Cloudflare Access, Google BeyondCorp

Exercise: mTLS Server with Identity Extraction

package main

import (
    "crypto/tls"
    "crypto/x509"
    "fmt"
    "io/ioutil"
    "net/http"
    "os"
)

func loadCACertPool(caFile string) (*x509.CertPool, error) {
    caPEM, err := os.ReadFile(caFile)
    if err != nil { return nil, err }
    pool := x509.NewCertPool()
    if !pool.AppendCertsFromPEM(caPEM) {
        return nil, fmt.Errorf("failed to parse CA cert from %s", caFile)
    }
    return pool, nil
}

func buildMTLSServerConfig(certFile, keyFile, caFile string) (*tls.Config, error) {
    cert, err := tls.LoadX509KeyPair(certFile, keyFile)
    if err != nil { return nil, err }

    caPool, err := loadCACertPool(caFile)
    if err != nil { return nil, err }

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

        MinVersion: tls.VersionTLS12,

        // Called after mTLS handshake — extract client identity
        VerifyPeerCertificate: func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
            if len(verifiedChains) == 0 {
                return fmt.Errorf("no verified certificate chain")
            }
            // Additional custom checks beyond standard verification
            clientCert := verifiedChains[0][0]
            fmt.Printf("[mTLS] client identity: %s\n", clientCert.Subject.CommonName)
            return nil
        },
    }, nil
}

func clientIdentityFromRequest(r *http.Request) string {
    if r.TLS == nil || len(r.TLS.VerifiedChains) == 0 {
        return ""
    }
    return r.TLS.VerifiedChains[0][0].Subject.CommonName
}

func main() {
    tlsConfig, err := buildMTLSServerConfig("server.crt", "server.key", "ca.crt")
    if err != nil {
        fmt.Println("TLS config:", err)
        return
    }

    mux := http.NewServeMux()
    mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        identity := clientIdentityFromRequest(r)
        if identity == "" {
            http.Error(w, "missing client certificate", http.StatusForbidden)
            return
        }

        // Inject identity into response for downstream services
        w.Header().Set("X-Client-Identity", identity)
        fmt.Fprintf(w, `{"authenticated_as":%q,"path":%q}`, identity, r.URL.Path)
    })

    srv := &http.Server{
        Addr:      ":8443",
        Handler:   mux,
        TLSConfig: tlsConfig,
    }

    fmt.Println("mTLS server on :8443")
    fmt.Println("Test: curl --cert client.crt --key client.key --cacert ca.crt https://localhost:8443/")
    srv.ListenAndServeTLS("", "") // certs already in TLSConfig
}

Test:

# First generate certs (Day 40)
go run ../day40-cert-gen/main.go

# Test with valid client cert — should succeed
curl --cert client.crt --key client.key --cacert ca.crt https://localhost:8443/

# Test without client cert — should fail (403)
curl --cacert ca.crt https://localhost:8443/

# Test with wrong CA client cert — should fail (TLS handshake error)

Real-World Context

mTLS is how Google's zero-trust BeyondCorp works. Instead of VPN (trust everything inside the network), each service has a cert. Compromising one service doesn't compromise others — the attacker gets that service's cert, not network-level access. Istio automates mTLS between all pods in Kubernetes.


Generated from week6.md · 8 Oct 2026 16:36