How to MTLS in golang

Add to bookmarks

Sat Mar 21 2020

What is MTLS

Mtls, short for Mutual Transport Layer Security, is a form of bi-directional security between two services where the TLS protocol is applied in both directions. The difference between MTLS and the standard form of browser HTTPS is, when you try to access an HTTPS/TLS endpoint, the client attempts to verify the presented certificate from the server, but in MTLS, the verification process is done by both the client and server for the corresponding other side of the connection.

What we will cover

In this tutorial, we will cover how MTLS can be handled in golang services with Rest and how we can set up golang services in a Kubernetes cluster to manually perform TLS checks.

Prerequisites

  • golang basics
  • TLS understanding. A good tutorial is here

Creating a golang server and a client

The first thing we want to do is create a golang server that serves our route and a golang client that connects to it. Create two files in the project, server/server.go and client/client.go and add the following to it.

// server/server.go

package main

import (
    "fmt"
    "log"
    "net/http"
)

func main() {
    // set up handler to listen to root path
    handler := http.NewServeMux()
    handler.HandleFunc("/", func(writer http.ResponseWriter, request *http.Request) {
        log.Println("new request")
        fmt.Fprintf(writer, "hello world \n")
    })

    // serve on port 9090 of local host
    server := http.Server{
        Addr:    ":9090",
        Handler: handler,
    }

    if err := server.ListenAndServe(); err != nil {
        log.Fatalf("error listening to port: %v", err)
    }
}

Then for the client

// client/client.go

package main

import (
    "fmt"
    "io/ioutil"
    "log"
    "net/http"
    "time"
)

func main(){
    client := http.Client{
        Timeout: time.Minute * 3,
    }

    resp,err := client.Get("http://localhost:9090")
    if err != nil {
        log.Fatalf("error making get request: %v", err)
    }

    body,err := ioutil.ReadAll(resp.Body)
    if err != nil{
        log.Fatalf("error reading response: %v", err)
    }
    fmt.Println(string(body))
}

Now if you run the server.go file and the client.go file you should have the client file print

hello world

because it successfully connects to the server.

Next thing you need to do is add the domain example.test to your hosts file and have it point to the localhost at 127.0.0.1, we are doing that because example.test is what we are going to be using to generate our certificates

Adding TLS to the server.

So now it's time to secure the communication between our server and client.

Generating our rootCA file.

The first thing we need to do to add mTLS to the connection is to generate a self-signed rootCA file that would be used to sign both the server and client cert. Alternatively, we can use different CA files to sign the server and client, but for our use case, we would use a single CA.

Run the following command on a terminal in the <project root>/certs folder

openssl req -newkey rsa:2048 -nodes -x509 -days 365 -out ca.crt -keyout ca.key 

You will be asked to fill in some information regarding the new certificate.

Generating a 2048 bit RSA private key
..................................+++
.+++
writing new private key to 'ca.key'
-----
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) []:NG     
State or Province Name (full name) []:Lagos
Locality Name (eg, city) []:Lagos
Organization Name (eg, company) []:Example
Organizational Unit Name (eg, section) []:Example
Common Name (eg, fully qualified host name) []:example.test
Email Address []:[email protected]

In this example dummy details were used to set up the certificate, the only thing to note among this is the common name.

Generating server certificate.

Now we need to create a certificate for our server that will be signed using the rootCA we created earlier.

Firstly create a key with:

$ openssl genrsa -out server.key 2048

Then generate the Certificate Signing Request (.csr file) with the command.

$ openssl req -new -key server.key -days 365 -out server.csr 
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) []:NG   
State or Province Name (full name) []:Lagos
Locality Name (eg, city) []:Lagos
Organization Name (eg, company) []:Example
Organizational Unit Name (eg, section) []:Example
Common Name (eg, fully qualified host name) []:example.test
Email Address []:[email protected]

Please enter the following 'extra' attributes
to be sent with your certificate request
## set the password to empty for this demo
A challenge password []:

Then sign it with the following command:

openssl x509  -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out server.crt -days 365 -sha256

Generating the client certificate.

Now that we have generated the client certificate we need to do the same for the client certificate. Run the following set of commands like you did the server:

$ openssl genrsa -out client.key 2048
## answer the questions for this step
$ openssl req -new -key client.key -days 365 -out client.csr
$ openssl x509  -req -in client.csr -CA ca.crt -CAkey ca.key -out client.crt -days 365 -sha256 -CAcreateserial
$ rm client.csr

Now with this, we should have a client certificate ready to use.

Serving the endpoint with TLS

Now if you run ls in the cert directory, you should have the following files in there

ca.crt      ca.key      server.crt  server.csr  server.key

Now modify the server/server.go file like below

...

    // serve the endpoint with tls encryption
    if err := server.ListenAndServeTLS("../cert/server.crt", "../cert/server.key"); err != nil {
        log.Fatalf("error listening to port: %v", err)
    }

...

and the client file should look like

...
    // change the address to match the common name of the certificate
    resp,err := client.Get("https://example.test:9090")
    if err != nil {
        log.Fatalf("error making get request: %v", err)
    }
...

Now if you try to run the server and client, you should get an error from both of them

## client error
2020/03/20 09:02:26 error making get request: Get https://example.test:9090: x509: certificate signed by unknown authority
exit status 1
## server error
2020/03/20 09:02:26 http: TLS handshake error from 127.0.0.1:64212: remote error: tls: bad certificate

The client does not trust the server certificate, to make it trust it, we add our rootCA to the client configuration like so

...
import (
    "crypto/tls"
    "crypto/x509"
    "fmt"
    "io/ioutil"
    "log"
    "net/http"
    "time"
)
...
func main() {
    cert, err := ioutil.ReadFile("../cert/ca.crt")
    if err != nil {
        log.Fatalf("could not open certificate file: %v", err)
    }

    caCertPool := x509.NewCertPool()
    caCertPool.AppendCertsFromPEM(cert)
    client := http.Client{
        Timeout: time.Minute * 3,
        Transport: &http.Transport{
            TLSClientConfig: &tls.Config{
                RootCAs: caCertPool,
            },
        },
    }
....
}

Now you should get a hello world response if you run the client again.

Verifying the client certificate.

Now we have gotten the client to verify the server's certificate based on the rootCA passed. The next step is to request and verify the client's certificate.

What we need to do is to update the server config like so:

// server/server.go
package main

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

func main() {
    ...

    // load CA certificate file and add it to list of client CAs
    caCertFile, err := ioutil.ReadFile("../cert/ca.crt")
    if err != nil {
        log.Fatalf("error reading CA certificate: %v", err)
    }
    certPool := x509.NewCertPool()
    certPool.AppendCertsFromPEM(caCertFile)

    // serve on port 9090 of local host
    server := http.Server{
        Addr:    ":9090",
        Handler: handler,
        TLSConfig: &tls.Config{
            ClientAuth: tls.RequireAndVerifyClientCert,
            ClientCAs:  certPool,
            MinVersion: tls.VersionTLS12,
        },
    }

    // serve the endpoint with tls encryption
    ...
}

Now if you rerun the server and try to connect, you will get the following errors from the client and server respectively

## client error
2020/03/20 11:28:21 error making get request: Get https://example.test:9090: remote error: tls: bad certificate
exit status 1
## server error
2020/03/20 11:28:21 http: TLS handshake error from 127.0.0.1:50024: tls: client didn't provide a certificate

From the error, you can see the client did not provide a certificate when we explicitly requested a certificate to be presented to the server via ClientAuth: tls.RequireAndVerifyClientCert,.

To fix it, we simply need to update the client to present its certificate. Now update your client file with this code.

// client/client.go

package main

import (
    "crypto/tls"
    "crypto/x509"
    "fmt"
    "io/ioutil"
    "log"
    "net/http"
    "time"
)

func main() {
    cert, err := ioutil.ReadFile("../cert/ca.crt")
    if err != nil {
        log.Fatalf("could not open certificate file: %v", err)
    }
    caCertPool := x509.NewCertPool()
    caCertPool.AppendCertsFromPEM(cert)

    certificate, err := tls.LoadX509KeyPair("../cert/client.crt", "../cert/client.key")
    if err != nil {
        log.Fatalf("could not load certificate: %v", err)
    }

    client := http.Client{
        Timeout: time.Minute * 3,
        Transport: &http.Transport{
            TLSClientConfig: &tls.Config{
                RootCAs:      caCertPool,
                Certificates: []tls.Certificate{certificate},
            },
        },
    }

    ...
}

Now if you run the client.go file again, you should receive the appropriate hello world response. All we did was add our client certificates to the tls Config of the client.

And now we have successfully set up mTLS between a pair of client and server services.

In the next article, we will be exploring how to set MTLS with GRPC and deploying on a kubernetes cluster. Subscribe to be informed!

Enjoy!