Implementing SSL/TLS Auth in gRPC

letsdote.ch avatar

·

·

SSL/TLS Auth in gRPC

gRPC supports various authentication mechanisms like SSL/TLS, ALTS (Application Layer Transport Security), and token based authentication. In this post, we will be implementing SSL/TLS auth in gRPC systems. We will begin by understanding the basics of SSL authentication, and also generate required key and certificate files to implement in our example.

In the previous blog post, while covering the basics of gRPC communication with Go, we introduced an example of a calculator server and client. The client-server gRPC communication in that example was not secure. If you check the client code here, it attempts to connect to the server in an insecure manner.

In real world situations, this poses a very high risk on multiple fronts. Thus to secure this communication, we implement certificate based authentication – also known as SSL/TLS based authentication. In this post, I cover the bare bones of implementing SSL/TLS Auth in gRPC based machine-to-machine communication.

As a spoiler, this post is not really about gRPC. But it helps in understanding how to implement certificate based authentication in a distributed architecture.

Full code of the example discussed in this post can be found in below link.

What is SSL/TLS based authentication?

The basic idea here is to make use of certificates to authenticate clients against the server. Since these authentication mechanisms depend on certificate files, it becomes inherently difficult for any attacker to crack them. Humans generally don’t use SSL/TLS based auth as it would be quite cumbersome to maintain multiple certificates for multiple platforms. However, this makes it very suitable for securing machine to machine communication.

Implementing SSL/TLS based auth in gRPC relies on a Certificate Authority (CA), without which it would be impossible for the mechanism to authenticate any request, in spite of possessing valid certificates. CA is responsible to sign and distribute certificates to clients and servers. These certificates, when used while exchanging messages, are used for validation.

SSL Certificates preparation for gRPC calculator server

To enable SSL based authentication in gRPC client server communication, we first have to create the required certificates. The steps below summarise the process of doing the same.

  1. Establish a CA – This step creates key and certificate for the CA
  2. Create a server key
  3. Create a certificate signing request (csr) for server, using server key
  4. Sign the server certificate signing request (csr) using CA key, to generate server certificate
  5. Create client key
  6. Create a certificate signing request (csr) for client, using client key
  7. Sign the client certificate signing request (csr) using CA key, to generate client certificate

To generate these certificates, you can use the openssl tool via your command line on any system. Below commands generate the CA key and CA certificate files.

Copied!
openssl genrsa -out ca-key.pem 2048 openssl req -new -x509 -days 365 -key ca-key.pem -out ca-cert.pem -subj "/CN=CA"

The next set of commands generate server key and certificate files. Note that in the 2nd and 3rd command, we are making use of a server.conf file, which contains SSL related configurations. These options would otherwise be provided as openssl CLI params.

Copied!
openssl genrsa -out server-key.pem 2048 openssl req -new -key server-key.pem -out server.csr -config server.conf openssl x509 -req -days 365 -in server.csr -CA ca-cert.pem -CAkey ca-key.pem -CAcreateserial -out server-cert.pem -extensions v3_req -extfile server.conf

The steps to create client key and certificate files are similar.

Copied!
openssl genrsa -out client-key.pem 2048 openssl req -new -key client-key.pem -out client.csr -config client.conf openssl x509 -req -days 365 -in client.csr -CA ca-cert.pem -CAkey ca-key.pem -CAcreateserial -out client-cert.pem -extensions v3_req -extfile client.conf

The diagram below shows the number of files involved in the above process.

Updating server code to use SSL certificates

Note that we have not touched the calc.proto file at all to implement the SSL authentication. If you are not aware of the example we are discussing here, refer to this blog post where we establish the same.

At this point, the Go server code simply listens on a specific port for incoming client connections. We register a gRPC server on this server, which essentially means that we are enabling all the callable functions as part of the gRPC interface.

Implementing SSL/TLS auth on this gRPC server, requires us to do some work before the server starts to listen, so that the server listens “securely”. Observe the code below to understand how certificates are loaded, and an explanation of the same would follow.

Copied!
func main() {    // Load server certificate and private key    cert, err := tls.LoadX509KeyPair("../certs/server-cert.pem", "../certs/server-key.pem")    if err != nil {        log.Fatalf("failed to load server certificates: %v", err)    }    // Create a certificate pool and add the client's CA certificate    certPool := x509.NewCertPool()    ca, err := ioutil.ReadFile("../certs/ca-cert.pem")    if err != nil {        log.Fatalf("failed to read ca certificate: %v", err)    }    if ok := certPool.AppendCertsFromPEM(ca); !ok {        log.Fatal("failed to append client certs")    }    // Create the TLS credentials    creds := credentials.NewTLS(&tls.Config{        Certificates: []tls.Certificate{cert},        ClientAuth:   tls.RequireAndVerifyClientCert,        ClientCAs:    certPool,        MinVersion:   tls.VersionTLS12,    })    lis, err := net.Listen("tcp", ":50051")    if err != nil {        log.Fatalf("failed to listen: %v", err)    }    s := grpc.NewServer(grpc.Creds(creds))    pb.RegisterCalculatorServer(s, &serverArray)    log.Printf("Server listening at %v", lis.Addr())    if err := s.Serve(lis); err != nil {        log.Fatalf("failed to serve: %v", err)    } }
  1. First we create a cert object using the Go tls module. Here we read the server certificate and key files to create a cert object.
  2. Next, we need to establish trust between server’s CA and client’s CA. In our case, both the server and client certificates are signed by the same CA, but we still need to make this server aware and trust the client connections explicitly. In the events where clients may use a different CA to generate their certificates, this step needs to be executed to create a cert pool of all those CA certs.
  3. Finally, we use the gRPC credentials module to generate credentials. Notice that we have specified the certPool we created in the previous step to create this credential for authentication purposes.
  4. The rest of the code remains the same except, while registering the gRPC server, we now use credentials (grpc.Creds(creds)) to secure incoming connections.

Sending SSL authenticated requests from the gRPC client

Similar to the old server implementation, the client code was insecure as well. In fact, it is quite expressive about it. If you now try to access the Add() function from the server, something interesting happens. Obviously, the operation is not successful, but the error message throws some light on the insecure.NewCredentials() function used in the insecure implementation.

Copied!
2025/02/11 15:15:46 could not calculate: rpc error: code = Unavailable desc = connection error: desc = "error reading server preface: EOF" exit status 1

Here, the insecure.NewCredentials() function tries to bypass the security requirements. In general, this is a handy way to test out the functionality with lesser auth complexity. Thus, instead of facing any auth related UNAVAILABLE type of errors, it instead says error reading server preface: EOF.

This means that the client is trying to dial in to the server using a plain TCP connection, and server expects to begin by a TLS handshake. This is a good starting point for this section, as we can now begin securing connection requests from the gRPC client. The the updated client code looks like below, explanation follows.

Copied!
func main() {    // Load client certificate and private key    cert, err := tls.LoadX509KeyPair("../certs/client-cert.pem", "../certs/client-key.pem")    if err != nil {        log.Fatalf("failed to load client certificates: %v", err)    }    // Create a certificate pool and add the server's CA certificate    certPool := x509.NewCertPool()    ca, err := ioutil.ReadFile("../certs/ca-cert.pem")    if err != nil {        log.Fatalf("failed to read ca certificate: %v", err)    }    if ok := certPool.AppendCertsFromPEM(ca); !ok {        log.Fatal("failed to append ca certs")    }    // Create the TLS credentials    creds := credentials.NewTLS(&tls.Config{        Certificates: []tls.Certificate{cert},        RootCAs:      certPool,        ServerName:   "localhost",    })    conn, err := grpc.Dial("localhost:50051", grpc.WithTransportCredentials(creds))    if err != nil {        log.Fatalf("did not connect: %v", err)    }    defer conn.Close()    c := pb.NewCalculatorClient(conn)    ctx, cancel := context.WithTimeout(context.Background(), time.Second)    defer cancel()    // Make the gRPC call    r, err := c.Add(ctx, &pb.AddRequest{Num1: 5, Num2: 3})    if err != nil {        log.Fatalf("could not calculate: %v", err)    }    log.Printf("Result: %d", r.GetResult()) }

As we can see, similar to the calculator server code, we need to do some ground work to implement SSL/TLS auth for gRPC clients before they can connect to the server.

  1. The main function first loads the client certificate and key files.
  2. Creates a cert pool with server’s CA certificates (in this example, both client and server have the same CA). This helps clients to know whom (which server) they are talking to, and avoids man-in-the-middle attacks. This is also known as m-TLS (mutual TLS), where the validation happens on both sides before a secure communication channel is established.
  3. Similar to server code, we prepare the credentials using the cert pool above, and supply the same while dialing in to the server.

Run the server, and then run the client code that accesses the Add() function on the server. The client should be able to access the calculator functionality as shown below – in a secure way!

Dig into the SSL/TLS details

The output shown above demonstrates secure, but a generic output even though we have been implementing SSL/TLS auth in this gRPC setup. One looking into SSL/TLS auth cannot really say much about it. Let us update the client code and try to print some certificate details. Note: This by default is a security bad practice, but we are doing it here for demonstration purposes.

The credentials.NewTLS accepts a tls.Config object, which exposes a VerifyConnection function, which executes a callback function with tls.ConnectionState object while generating the credentials. Write a function to print the details of this object as shown below.

Copied!
func verifyConnection(state tls.ConnectionState) {    log.Printf("=== TLS Connection Details ===")    log.Printf("Version: %x", state.Version)    log.Printf("CipherSuite: %s", tls.CipherSuiteName(state.CipherSuite))    log.Printf("HandshakeComplete: %t", state.HandshakeComplete)    log.Printf("Server Name: %s", state.ServerName)    for i, cert := range state.PeerCertificates {        log.Printf("Peer Certificate [%d]:", i)        log.Printf("  Subject: %s", cert.Subject)        log.Printf("  Issuer: %s", cert.Issuer)        log.Printf("  Valid from: %s", cert.NotBefore)        log.Printf("  Valid until: %s", cert.NotAfter)    } }

The verifyConnection() function prints details like TLS Version, CipherSuite, state of the TLS Handshake, server name, etc. It also prints details about the peer certificate. Modify the code to create credentials in the main function as shown below, and call the verifyConnection() function.

Copied!
// Create TLS config with verification callbacks    tlsConfig := &tls.Config{        ServerName:   "localhost",        Certificates: []tls.Certificate{cert},        RootCAs:      certPool,        MinVersion:   tls.VersionTLS12,        VerifyConnection: func(cs tls.ConnectionState) error {            verifyConnection(cs)            return nil        },    }    // Create the TLS credentials    creds := credentials.NewTLS(tlsConfig)

We have also updated our logic to call the Add() function multiple times. Find the full code here. Re-run the server and client code and observe the client output below.

This is just to show how you can further investigate the certificate implementation. Working with certificates can get tricky especially for beginners. But it offers a much more solid auth mechanism that exists today.

Leave a Reply

Your email address will not be published. Required fields are marked *