How to secure gRPC apps with server side TLS

securing grpc with server side tls

Today, we will discuss securing our gRPC applications by enabling TLS on the server end and testing its functionality locally or at the pod/server using tools such as gRPCurl. TLS is a standard norm for all modern applications, but it’s of even higher importance when it comes to HTTP/2-based applications employing technologies such as gRPC. 

Brief Description

In this article, we talk about how we can use tls.LoadX509KeyPair method in golang to create a tls configuration and apply it to our server options before starting our gRPC application. This post also covers a utility called gRPCurl that is helpful in testing and debugging our tls server configuration. The post covers creating and passing those certificates to the gRPCurl utility to test the configurations

Introduction

There are three main ways to configure a gRPC application’s security: insecure TLS, server-side TLS, and mutual TLS. 

The insecure one is where our application does not have any TLS configuration. Since gRPC uses HTTP/2, we configure the TLS directly on the application servers using numerous options such as NGINX, Envoy, etc. 

mTLS is the most secure option for an application, as both the server and client present the certificate for authentication, providing a very safe channel for connectivity. However, this article will only focus on learning how to enable TLS on the server side. In upcoming posts, we can go over how to implement the same using mTLS. 

TLS for gRPC:

I assume you have an existing gRPC application up and running; first, we would need to generate the certificates, get them signed by the CA, and configure them for our application. 

Generate Certificates:

We can generate a certificate in two ways: by creating a CSR or Certificate Signing Request and handing it over to the information security team or the signing authority for validation. Once validated, we can apply those to the servers. The other way which we are going to talk about is to have self-signed certificates. To get started you would need openssl installed on your system.

Steps to generate certificates:

Create a CA private key and self-signed certificate:
openssl req -x509 -newkey rsa:4096 -nodes -days 365 -keyout ca-key.pem -out ca-cert.pem -subj "/C=IN/ST=KA/L=Bangalore/O=Research/OU=BLOG/CN=*.developer04.com/emailAddress=contact@email.com"
Generate web server’s Private Key and CSR:
openssl req -newkey rsa:4096 -nodes -keyout server-key.pem -out server-req.pem -subj "/C=IN/ST=KA/L=Bangalore/O=Research/OU=BLOG/CN=*.developer04.com/emailAddress=contact@email.com"
Sign the Web Server certificates and generate certificates for us:
openssl x509 -req -in server-req.pem -CA ca-cert.pem -CAkey ca-key.pem -CAcreateserial -out server-cert.pem -extfile server-ext.conf
Verification:

You can verify the contents of your certificate using the following command

openssl x509 -in server-cert.crt -noout -text 

You will have an output looking like this: 

grpc openssl certificate
grpc certificate

If you want CA to validate your certificates, then you can create a request using the following command: 

openssl req -new -keyout private_key.key -out server-csr.csr -config openssl.cnf

The content of openssl.cnf is as follows:

#OpenSSL configuration
[req]
default_bits = 2048
prompt = no
default_md = sh256
distinguished_name = dn
req_extensions = req_ext

[dn]
#these are your DN fields
CN = <domain_name>
C=<Country>  
ST=<State>
L=<Locality>
O=<Organization Name>
OU=<Organizational Unit> 
CN=<domain_name> 

[req_ext]
subjectAltName = @alt_names

[alt_names]
DNS.1 = <alternative_domain_names>
TLS Integration

Let’s integrate these certificates with our code base and see how they perform when implementing them. 

First, we must create a loadTLSCredentials() function that returns transport credentials and an error state if the certificate read fails. It should look something like this:

func loadTLSCredentials() (credentials.TransportCredentials, err) {
 serverCertificate, error := tls.LoadX509KeyPair('pathToServerCertFile', 'pathToServerKeyFile')

 if error != nil {
 //set your error object and return 
 }

 config := &tls.Config {
 certificates: []tls.Certificate(serverCertificate)
 clientAuth : tls.NoClientCert,
 }

 return credentials.NewTLS(config), nil
}

Over here, we are trying to access the certificate and key files stored on the local storage. To have these files available on your servers, you can store them on a key server or secret server. We can get the files in byte form or the entire certificate in string format. By doing this activity, our code changes slightly, and instead of accessing the local filesystem, we would then use the string data and read the certificates.

cert, err := tls.X509KeyPair([]byte(certString), []byte(keyString))
if err != nil {
 //handle your error case here
}

The rest of the code remains the same. 

We will now add the TLS configuration to our server configuration options. 

/.../

tlsCreds, err := loadTLSCredentials()

if err != nil {

//return and shutdown the server with exit

}

serverOptions = append(serverOptions, grpc.Creds(tlsCredentials))

server := grpc.NewServer(serverOptions...)

/.../

If everything goes well, our server will successfully use the TLS configurations. If you face any errors, please check your certificates and ensure you have mentioned the correct path, name, and key file, as that’s where most of the issues are hiding.

Testing

There are multiple ways by which one can test the server configuration; we will talk about how we can use a famous utility called gRPCurl to connect and list the server protos. But first, let’s install the gRPCurl utility, 

on macOS, you can use the brew install grpcurl command; for other platforms, you can download one of the relevant libraries and install the utility on your system. https://github.com/fullstorydev/grpcurl/releases

Connecting to the server 

Once the server is up and running, we can first try to connect to it without using any certificates or security. 

grpcurl --plaintext localhost:<port> list

Upon a successful request, you will see a list of available protos of your services on the server. This method does not bother about whether or not certificates are valid or even present and would connect to the server and give a response.  

Next, we will attempt a secure connection, 

grpcurl --insecure --cacert <path_to_crt_file> localhost:<port> list

If the service has TLS enabled and a certificate, you will get a response similar to what we saw earlier. Unlike the –plaintext flag, the –insecure flag bypasses the validation of the certificates but does not ignore their presence.

Next, we will attempt to connect with the service using a secure method with a valid certificate and confirm that our services are correctly configured with TLS certificates.

grpcurl --cacert <path_to_crt_file> localhost:<port> list 

At this stage, if anything mismatches in the CA chain, the certificate presented by the server, or the overall TLS implementation, we will get an error describing the issue. Otherwise, we will have a successful response on the terminal. 

If you are testing the functionality on your server, ensure the endpoint is not of the local host but the FQDN of the server. As I mentioned earlier, any mismatch in the configuration would render the call invalid, and gRPCurl will throw an error indicating the same. 

Suppose you want to test the validity of your configuration locally using the QA / Sandbox certificate. In that case, you can edit the /etc/hosts file, point your server URL to localhost, and provide the server’s FQDN in the gRPCurl request. This way, you will have a valid FQDN pointing to the local host server per the certificate. 

Conclusion 

We can configure our gRPC application to use TLS on the server end and securely serve the traffic to the allowed or valid clients. Please feel free to add your comments for any suggestion you may have.

author avatar
Tushar Sharma