Mutual TLS (mTLS) is often pitched as a security enhancement for gRPC, but its real power lies in its ability to enforce identity between services, making them not just secure, but also auditable and controllable at a granular level.

Let’s see mTLS in action between a gRPC client and server. We’ll use Go for this example, as its standard library and tooling make certificate management straightforward.

First, we need to generate some certificates. We’ll create a Certificate Authority (CA) that will sign both the server’s and the client’s certificates.

# Create a directory for our certs
mkdir certs
cd certs

# Generate CA private key and certificate
openssl genrsa -out ca.key 2048
openssl req -x509 -new -nodes -key ca.key -sha256 -days 365 -out ca.crt -subj "/CN=MyTestCA"

# Generate server private key and certificate signing request (CSR)
openssl genrsa -out server.key 2048
openssl req -new -key server.key -out server.csr -subj "/CN=localhost"

# Sign the server CSR with the CA
openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out server.crt -days 365 -sha256

# Generate client private key and CSR
openssl genrsa -out client.key 2048
openssl req -new -key client.key -out client.csr -subj "/CN=my-grpc-client"

# Sign the client CSR with the CA
openssl x509 -req -in client.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out client.crt -days 365 -sha256

# Clean up CSRs (optional)
rm server.csr client.csr

Now, let’s set up a simple gRPC server. This server will load the CA certificate to verify incoming client certificates, and its own server certificate and key to present to clients.

package main

import (
	"context"
	"crypto/tls"
	"crypto/x509"
	"fmt"
	"io/ioutil"
	"log"
	"net"

	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials"
	pb "google.golang.org/grpc/examples/helloworld/helloworld" // Assuming you have a helloworld proto
)

const (
	port         = ":50051"
	caCertPath   = "ca.crt"
	serverCertPath = "server.crt"
	serverKeyPath  = "server.key"
)

// server is used to implement helloworld.GreeterServer.
type server struct {
	pb.UnimplementedGreeterServer
}

// SayHello implements helloworld.GreeterServer.
func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
	log.Printf("Received: %v", in.GetName())
	return &pb.HelloReply{Message: "Hello " + in.GetName()}, nil
}

func main() {
	// Load the CA certificate
	caCert, err := ioutil.ReadFile(caCertPath)
	if err != nil {
		log.Fatalf("failed to read ca cert: %v", err)
	}
	caCertPool := x509.NewCertPool()
	if !caCertPool.AppendCertsFromPEM(caCert) {
		log.Fatalf("failed to append ca cert to pool")
	}

	// Load server certificate and key
	serverCert, err := tls.LoadX509KeyPair(serverCertPath, serverKeyPath)
	if err != nil {
		log.Fatalf("failed to load server key pair: %v", err)
	}

	// Create TLS config for the server
	tlsConfig := &tls.Config{
		Certificates: []tls.Certificate{serverCert},
		ClientCAs:    caCertPool,
		ClientAuth:   tls.RequireAndVerifyClientCert, // Crucial for mTLS
	}

	// Create gRPC server with TLS credentials
	grpcServer := grpc.NewServer(grpc.Creds(credentials.NewTLS(tlsConfig)))

	// Register your service
	pb.RegisterGreeterServer(grpcServer, &server{})

	// Start listening
	lis, err := net.Listen("tcp", port)
	if err != nil {
		log.Fatalf("failed to listen: %v", err)
	}

	log.Printf("Server listening on %v", lis.Addr())
	if err := grpcServer.Serve(lis); err != nil {
		log.Fatalf("failed to serve: %v", err)
	}
}

The key part here is tlsConfig.ClientAuth: tls.RequireAndVerifyClientCert. This tells the server that it must request a client certificate and that it must verify that certificate against the ClientCAs pool (our ca.crt). If the client doesn’t present a valid certificate signed by our CA, the connection will be rejected before any gRPC calls can even be attempted.

Now, for the client. The client needs to load its own certificate and key, and also the CA certificate so it can verify the server’s certificate.

package main

import (
	"crypto/tls"
	"crypto/x509"
	"fmt"
	"io/ioutil"
	"log"

	"golang.org/x/net/context"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials"
	pb "google.golang.org/grpc/examples/helloworld/helloworld"
)

const (
	serverAddr   = "localhost:50051"
	caCertPath   = "ca.crt"
	clientCertPath = "client.crt"
	clientKeyPath  = "client.key"
)

func main() {
	// Load the CA certificate
	caCert, err := ioutil.ReadFile(caCertPath)
	if err != nil {
		log.Fatalf("failed to read ca cert: %v", err)
	}
	caCertPool := x509.NewCertPool()
	if !caCertPool.AppendCertsFromPEM(caCert) {
		log.Fatalf("failed to append ca cert to pool")
	}

	// Load client certificate and key
	clientCert, err := tls.LoadX509KeyPair(clientCertPath, clientKeyPath)
	if err != nil {
		log.Fatalf("failed to load client key pair: %v", err)
	}

	// Create TLS config for the client
	tlsConfig := &tls.Config{
		Certificates: []tls.Certificate{clientCert},
		RootCAs:      caCertPool, // Verify the server's cert against our CA
	}

	// Create gRPC client with TLS credentials
	// Note: We use grpc.WithTransportCredentials for client-side TLS
	conn, err := grpc.Dial(serverAddr, grpc.WithTransportCredentials(credentials.NewTLS(tlsConfig)))
	if err != nil {
		log.Fatalf("did not connect: %v", err)
	}
	defer conn.Close()
	c := pb.NewGreeterClient(conn)

	// Call the server
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()
	r, err := c.SayHello(ctx, &pb.HelloRequest{Name: "gRPC User"})
	if err != nil {
		log.Fatalf("could not greet: %v", err)
	}
	log.Printf("Greeting: %s", r.GetMessage())
}

On the client side, tlsConfig.RootCAs: caCertPool is what allows the client to trust the server’s certificate, ensuring it’s talking to the legitimate server we intended. The Certificates: []tls.Certificate{clientCert} part is what the client presents to the server for verification.

When you run the server and then the client, you should see the "Greeting: Hello gRPC User" message. If you try to run the client without the client.crt and client.key (or with incorrect ones), the server will reject the connection with an rpc error: code = Unavailable desc = connection error: desc = "transport: Error while dialing dial tcp [::1]:50051: x509: certificate signed by unknown authority" or similar, because the server couldn’t verify the client’s identity. Conversely, if the client doesn’t trust the server’s certificate (e.g., by providing a wrong caCertPath or no RootCAs), it will also fail.

The real magic of mTLS in a microservices environment is that the CN (Common Name) in the client certificate (/CN=my-grpc-client in our example) becomes the identity of the client. Your server can then inspect this identity to make authorization decisions. For instance, you might configure the server to only allow calls from clients whose CN is "my-grpc-client" or is part of a specific organizational unit. This is far more robust than relying on IP addresses or shared secrets, as it’s cryptographically proven. The CN field, when used in conjunction with certificate chains, provides a verifiable, auditable trail of who is talking to whom.

Want structured learning?

Take the full Tls-ssl course →