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 email, instant 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:
- Load the certificate generated by our fictitious certificate authority, who signed client’s certificate;
- Load server’s certificate and private key;
- Create a
tls.Config{}
that will be used ingrpc.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:
- Load the certificate generated by our fictitious certificate authority, who signed server’s certificate;
- Load client’s certificate and private key;
- Create a
tls.Config{}
that will be used ingrpc.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:
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:
Here we go:
Download the source
Here: https://bitbucket.org/tiagoharris/docker-grpc-service-tls-tutorial