50. mTLS: client-certificate authentication end to end
Goal: go one level past Chapter 35's server-only TLS — require the client to present a trusted certificate too, the mechanism behind service-to-service auth in zero-trust architectures.
# 1. Create a private CA
openssl req -x509 -newkey rsa:4096 -days 3650 -nodes \
-keyout ca-key.pem -out ca-cert.pem -subj "/CN=My Test CA"
# 2. Server certificate, signed by the CA
openssl req -newkey rsa:2048 -nodes -keyout server-key.pem -out server-req.pem \
-subj "/CN=localhost"
openssl x509 -req -in server-req.pem -CA ca-cert.pem -CAkey ca-key.pem \
-CAcreateserial -out server-cert.pem -days 365
# 3. Client certificate, also signed by the same CA
openssl req -newkey rsa:2048 -nodes -keyout client-key.pem -out client-req.pem \
-subj "/CN=trusted-client"
openssl x509 -req -in client-req.pem -CA ca-cert.pem -CAkey ca-key.pem \
-CAcreateserial -out client-cert.pem -days 365
// server.go
func main() {
caCert, err := os.ReadFile("ca-cert.pem")
if err != nil {
log.Fatal(err)
}
caPool := x509.NewCertPool()
if !caPool.AppendCertsFromPEM(caCert) {
log.Fatal("failed to parse CA certificate")
}
tlsConfig := &tls.Config{
ClientAuth: tls.RequireAndVerifyClientCert, // reject any client with no valid cert
ClientCAs: caPool,
}
srv := &http.Server{
Addr: ":8443",
TLSConfig: tlsConfig,
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
cn := r.TLS.PeerCertificates[0].Subject.CommonName
fmt.Fprintf(w, "hello, %s — you presented a valid client cert\n", cn)
}),
}
log.Fatal(srv.ListenAndServeTLS("server-cert.pem", "server-key.pem"))
}
// client.go
func main() {
cert, err := tls.LoadX509KeyPair("client-cert.pem", "client-key.pem")
if err != nil {
log.Fatal(err)
}
caCert, err := os.ReadFile("ca-cert.pem")
if err != nil {
log.Fatal(err)
}
caPool := x509.NewCertPool()
if !caPool.AppendCertsFromPEM(caCert) {
log.Fatal("failed to parse CA certificate")
}
client := &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
Certificates: []tls.Certificate{cert},
RootCAs: caPool,
},
},
}
resp, err := client.Get("https://localhost:8443/")
if err != nil {
log.Fatal(err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
log.Fatal(err)
}
fmt.Println(string(body))
}
tls.RequireAndVerifyClientCert is the setting that turns this from
"encrypted" into "mutually authenticated" — without it, TLS still
encrypts the connection, but the server accepts any client, which is
plain TLS, not mTLS. r.TLS.PeerCertificates[0].Subject.CommonName is
how the server learns who connected — the certificate itself is the
credential, no password or token exchanged at all.