Golang: a dockerized gRPC service using TLS

In a previous article we saw how to write and run a gRPC service with Docker. Now it is time to make it secure with TLS.

TLS

As defined in Wikipedia,

Transport Layer Security (TLS) is a cryptographic protocol designed to provide communications security over a computer network. The protocol is widely used in applications such as emailinstant messaging, and voice over IP, but its use in securing HTTPS remains the most publicly visible.

These are the most common types of certificates:

  • self-signed: the company generates its own certificate;
  • domain validated: validation is performed by a certificate authority. It checks whether the domain exists and validates the domain’s registered data;
  • fully authenticated: the certificate authority checks if the certificate requester owns the domain of it is legally assigned; It check also if the requester has the right permissions to request the certificate.

We’ll use a self-signed certificate in this example.

Generating certificates

We’ll use OpenSSL. Here’s our script:

#!/bin/bash

# 1. Generate CA's private key and self-signed certificate
openssl req -x509 -nodes -sha256 -days 365 -newkey rsa:2048 -keyout cert/ca-key.pem -out cert/ca-cert.pem  -subj "/C=BR/ST=Minas Gerais/L=Divinopolis/O=Up The Irons Certificate Authority/CN=*.uptheirons.com.br/emailAddress=uptheirons@ca.com.br"

# 2. Generate server's private key and certificate signing request (CSR)
openssl req -new -sha256 -keyout cert/server-key.pem -nodes -out cert/server-req.pem -subj "/C=BR/ST=Minas Gerais/L=Belo Horizonte/O=Poetry Service/CN=*.tiago.poetryservice.com/emailAddress=poetryservice@service.com.br"

# 3. Use CA's private key to sign server's CSR and get back the signed certificate
openssl x509 -req -in cert/server-req.pem -sha256 -days 60 -CA cert/ca-cert.pem -CAkey cert/ca-key.pem -CAcreateserial -out cert/server-cert.pem -extfile cert/config/server-ext.cnf

# 4. Generate client's private key and certificate signing request (CSR)
openssl req -new -sha256 -keyout cert/client-key.pem  -nodes -out cert/client-req.pem -subj "/C=BR/ST=Minas Gerais/L=Passos/O=Poetry Service client/CN=*.tiago.poetryservice.client.com/emailAddress=client.poetryservice@service.com.br"

# 5. Use CA's private key to sign client's CSR and get back the signed certificate
openssl x509 -req -in cert/client-req.pem -sha256 -days 60 -CA cert/ca-cert.pem -CAkey cert/ca-key.pem -CAcreateserial -out cert/client-cert.pem -extfile cert/config/client-ext.cnf

echo "Finished."

Enabling TLS in our server

We need to do:

  1. Load the certificate generated by our fictitious certificate authority, who signed client’s certificate;
  2. Load server’s certificate and private key;
  3. Create a tls.Config{} that will be used in grpc.ServerOption{}.

The complete code:


// Copyright (c) 2022 Tiago Melo. All rights reserved.
// Use of this source code is governed by the MIT License that can be found in
// the LICENSE file.
package server

import (
    "context"
    "crypto/tls"
    "crypto/x509"
    "encoding/json"
    "os"

    "bitbucket.org/tiagoharris/docker-grpc-service-tls-tutorial/poetrydb"
    poetry "bitbucket.org/tiagoharris/docker-grpc-service-tls-tutorial/proto"
    "github.com/pkg/errors"
    "google.golang.org/grpc"
    "google.golang.org/grpc/credentials"
    "google.golang.org/grpc/reflection"
    "google.golang.org/protobuf/encoding/protojson"
)

const (
    caCert     = "cert/ca-cert.pem"
    serverCert = "cert/server-cert.pem"
    serverKey  = "cert/server-key.pem"
)

// These global variables makes it easy
// to mock them in unit tests.
var (
    loadTlsConfig      = _loadTlsConfig
    readFile           = os.ReadFile
    appendCertsFromPEM = func(certPool *x509.CertPool, pemCerts []byte) (ok bool) {
        return certPool.AppendCertsFromPEM(pemCerts)
    }
    loadX509KeyPair   = tls.LoadX509KeyPair
    getRandomPoetries = func(n int) (*poetrydb.PoetryResponse, error) {
        return poetrydb.Random(n)
    }
    jsonMarshal        = json.Marshal
    protojsonUnmarshal = protojson.Unmarshal
)

// server implements operations defined in poetry.proto.
type server struct {
    *grpc.Server
}

// _loadTlsConfig does the heavy lifting of configuring
// and loading TLS config.
func _loadTlsConfig() (credentials.TransportCredentials, error) {
    // Load certificate of the CA who signed client's certificate
    pemClientCA, err := readFile(caCert)
    if err != nil {
        return nil, errors.Wrap(err, "loading CA's certificate")
    }
    certPool := x509.NewCertPool()
    if !appendCertsFromPEM(certPool, pemClientCA) {
        return nil, errors.New("failed to add client CA's certificate")
    }
    // Load server's certificate and private key
    serverCert, err := loadX509KeyPair(serverCert, serverKey)
    if err != nil {
        return nil, errors.Wrap(err, "loading server's certificate and private key")
    }
    // Create the credentials and return it
    config := &tls.Config{
        Certificates: []tls.Certificate{serverCert},
        ClientAuth:   tls.RequireAndVerifyClientCert,
        ClientCAs:    certPool,
    }
    return credentials.NewTLS(config), nil
}

// New creates a new gRPC server implementation.
func New() (*server, error) {
    creds, err := loadTlsConfig()
    if err != nil {
        return nil, errors.Wrap(err, "loading TLS config")
    }
    opts := []grpc.ServerOption{grpc.Creds(creds)}
    grpcServer := grpc.NewServer(opts...)
    srv := &server{grpcServer}
    poetry.RegisterProtobufServiceServer(grpcServer, srv)
    reflection.Register(grpcServer)
    return srv, nil
}

// RandomPoetries returns a random number of poetries.
func (s *server) RandomPoetries(ctx context.Context, in *poetry.RandomPoetriesRequest) (*poetry.PoetryResponse, error) {
    pr := new(poetry.PoetryResponse)
    poetries, err := getRandomPoetries(int(in.NumberOfPoetries))
    if err != nil {
        return nil, errors.Wrap(err, "requesting random poetries")
    }
    json, err := jsonMarshal(poetries)
    if err != nil {
        return nil, errors.Wrap(err, "marshalling json")
    }
    if err := protojsonUnmarshal(json, pr); err != nil {
        return nil, errors.Wrap(err, "unmarshalling proto")
    }
    return pr, nil
}

Enabling TLS in a Golang client

Steps:

  1. Load the certificate generated by our fictitious certificate authority, who signed server’s certificate;
  2. Load client’s certificate and private key;
  3. Create a tls.Config{} that will be used in grpc.WithTransportCredentials() option.

The complete code:


// Copyright (c) 2022 Tiago Melo. All rights reserved.
// Use of this source code is governed by the MIT License that can be found in
// the LICENSE file.
package main

import (
    "context"
    "crypto/tls"
    "crypto/x509"
    "flag"
    "fmt"
    "log"
    "os"

    poetry "bitbucket.org/tiagoharris/docker-grpc-service-tls-tutorial/proto"
    "github.com/pkg/errors"
    "google.golang.org/grpc"
    "google.golang.org/grpc/credentials"
)

func main() {
    ctx := context.Background()
    log := log.New(os.Stdout, "gRPC CLIENT : ", log.LstdFlags|log.Lmicroseconds|log.Lshortfile)

    if err := run(ctx, log); err != nil {
        log.Println("main: error:", err)
        os.Exit(1)
    }
}

func run(ctx context.Context, log *log.Logger) error {
    port := flag.Int("port", 4000, "server's port")
    flag.Parse()
    log.Println("main: Initializing gRPC client")
    defer log.Println("main: Completed")
    // Load certificate of the CA who signed server's certificate
    pemServerCA, err := os.ReadFile("cert/ca-cert.pem")
    if err != nil {
        return errors.Wrap(err, "loading CA's certificate")
    }
    certPool := x509.NewCertPool()
    if !certPool.AppendCertsFromPEM(pemServerCA) {
        return errors.New("failed to add server CA's certificate")
    }
    // Load client's certificate and private key
    clientCert, err := tls.LoadX509KeyPair("cert/client-cert.pem", "cert/client-key.pem")
    if err != nil {
        return errors.Wrap(err, "loading client's certificate and private key")
    }
    // Create the credentials and return it
    config := &tls.Config{
        Certificates: []tls.Certificate{clientCert},
        RootCAs:      certPool,
    }
    conn, err := grpc.DialContext(ctx, fmt.Sprintf("localhost:%d", *port), grpc.WithBlock(), grpc.WithTransportCredentials(credentials.NewTLS(config)))
    if err != nil {
        return errors.Wrap(err, "dialing")
    }
    client := poetry.NewProtobufServiceClient(conn)
    res, err := client.RandomPoetries(ctx, &poetry.RandomPoetriesRequest{NumberOfPoetries: int32(2)})
    if err != nil {
        return errors.Wrap(err, "calling 'client.RandomPoetries()'")
    }
    for _, poetry := range res.Poetries {
        fmt.Println("\nTitle: ", poetry.Title)
        fmt.Println("Author: ", poetry.Author)
        fmt.Print("\n")
        for _, line := range poetry.Lines {
            fmt.Printf("\t%s\n", line)
        }
        fmt.Print("\n")
    }
    return nil
}

Running the server


$ make run

It will generate the certs, build the app’s image and run it in a Docker container:


GRPC SERVER : 2022/12/26 17:05:45.778730 main.go:20: main: initializing gRPC server

GRPC SERVER : 2022/12/26 17:05:45.780478 main.go:47: main: gRPC server listening on :4000

Consuming the service with the Golang client


$ make client

gRPC CLIENT : 2022/12/26 14:50:30.212078 client.go:34: main: Initializing gRPC client

Title:  Farmer's Boy

Author:  John Clare

	He waits all day beside his little flock

	And asks the passing stranger what's o'clock,

	But those who often pass his daily tasks

	Look at their watch and tell before he asks.

	He mutters stories to himself and lies

	Where the thick hedge the warmest house supplies,

	And when he hears the hunters far and wide

	He climbs the highest tree to see them ride--

	He climbs till all the fields are blea and bare

	And makes the old crow's nest an easy chair.

	And soon his sheep are got in other grounds--

	He hastens down and fears his master come,

	He stops the gap and keeps them all in bounds

	And tends them closely till it's time for home.

Title:  The Truth About hHorace

Author:  Eugene Field

	It is very aggravating

	To hear the solemn prating

	Of the fossils who are stating

	That old Horace was a prude;

	When we know that with the ladies

	He was always raising Hades,

	And with many an escapade his

	Best productions are imbued.



	There's really not much harm in a

	Large number of his carmina,

	But these people find alarm in a

	Few records of his acts;

	So they'd squelch the muse caloric,

	And to students sophomoric

	They d present as metaphoric

	What old Horace meant for facts.



	We have always thought 'em lazy;

	Now we adjudge 'em crazy!

	Why, Horace was a daisy

	That was very much alive!

	And the wisest of us know him

	As his Lydia verses show him,--

	Go, read that virile poem,--

	It is No. 25.



	He was a very owl, sir,

	And starting out to prowl, sir,

	You bet he made Rome howl, sir,

	Until he filled his date;

	With a massic-laden ditty

	And a classic maiden pretty

	He painted up the city,

	And Maecenas paid the freight!

gRPC CLIENT : 2022/12/26 14:50:30.513159 client.go:73: main: Completed

Consuming the service with BloomRPC

Bloomrpc is a very nice GUI for consuming gRPC services.

After loading the .proto file, click in TLS, next to the green locker:

No alt text provided for this image

Then, load the certificate generated by our fictitious certificate authority, the client key and the client signed certificate. Don’t forget to inform that localhost is our target ssl domain:

No alt text provided for this image

Here we go:

No alt text provided for this image

Download the source

Here: https://bitbucket.org/tiagoharris/docker-grpc-service-tls-tutorial