Frontend Labs
⋯

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.


Generated from real-life-golang-guide.md · 8 Oct 2026 16:36