=====================
== Rase's basement ==
=====================
Simple and functional is beautiful

Golang mTLS

Responsibilities

Life has been very hectic. I’ve been juggling school, work and hobbies at the same time. There are many projects I’d like to work on but there is not enough time.

I’ve been very ambitious about this one project for a while now. This blog post is related to the project. Namely, how I’m going to implement mTLS authentication for the backend endpoints.

The backend will be running on my home server and the endpoints will be private and used by a handful of selected people.

This blog post will not be a blog post on what mTLS is and how it works. There are smarter people who explain the topic better. If you want a good blog post on the topic, check this out. This will simply be a blog post on how I implement mTLS for my use case.

Key generation for the signer

We use OpenSSL to generate a key pair for the “CA” which in this case is us.

openssl req -x509 -newkey rsa:2048 -keyout ca_unencrypted_private.pem -out ca_public.pem -days 365

The generated ca_public.pem will be trusted by our server-side code. Everything signed with the ca_unecrypted_private.pem key will be trusted since we only trust the certificate if it has been signed with the private key corresponding to ca_public.pem.

I also had to take out the password from ca_unencrypted_private.pem, since I could not figure out a way to load in the private key in my Go code. I know this is very insecure and I will find a way later.

openssl rsa -in ca_encrypted_private.pem -out unencrypted_private.key

Key generation for the client

Next, we generate a private key for the client.

openssl genrsa -out client.key 2048

Then, we generate a crt file from the private key.

openssl req -new -key client.key -out client.csr

Signing the client certificate

At this point we have generated everything we need to generate. Next we will just sign the clients certificate and it can then be used to access our endpoints.

On the server, we will run the following command.

openssl x509 -req -in client.csr -CA ca_public.pem -CAkey ca_unencrypted_private.key -CAcreateserial -out client.crt -days 365

We will also verify that the signing worked.

openssl verify -CAfile ca_public.pem client.crt

Server configuration

Setting up the server

My server uses the Gin web framework to handle the endpoints, request validation, etc.

I start off by configuring my server. This includes loggers, database connections, request validations and the Gin router.

I tend to create a struct for my Server because I like to couple the related information together.

type Server struct {
	logger    Logger         // Contains file handles to logging stuff (access logs, error logs, etc.)
	db        DBClient       // Contains all the DB stuff, not relevant to the blog post.
	ginRouter *gin.Engine
	https     *http.Server
}
func (s *Server) InitServer() {
	...                      // Init log file handles, database handles, etc.
	s.InitGinRouter()        // The stuff relevant to the blog post.
}
func main() {
	server := Server{}
	server.InitServer()
	defer server.DeInitServer()  // Free up handles, etc.

  
  ...                            // Creating endpoints, etc.
}

Make use of mTLS with Gin

Inside of InitGinRouter, I do multiple things.

  1. Add the “CA’s” public key to the pool of trusted authorities.
  2. Make it so the endpoints require a certificate that has been signed by a trusted authority (us).
func (s *Server) InitGinRouter() {
	s.ginRouter = gin.Default()
	...

	s.https = &http.Server{
		Addr:      ":443",
		Handler:   s.ginRouter,
		TLSConfig: s.InitMTLSAuthentication(),
	}
}
func (s *Server) InitMTLSAuthentication() *tls.Config {
	caCert, _ := os.ReadFile("./cert/ca_public.pem")

	// Creates a pool of trusted CA's
	caCertPool := x509.NewCertPool()
	// Adding our CA to the pool
	caCertPool.AppendCertsFromPEM(caCert)

	tlsConfig := &tls.Config{
		ClientCAs:  caCertPool,
		ClientAuth: tls.RequireAndVerifyClientCert,
	}

	return tlsConfig
}

That’s all we need. Now we will just run the server.

func main() {
  ...

	server.ginRouter.POST("/endpoint", func(c *gin.Context) {
		...
	})
	err := server.https.ListenAndServeTLS("./cert/ca_public.pem", "./cert/ca_unencrypted_private.key")
	if err != nil {
		log.Fatalf("ERROR STARTING SERVER: %v", err)
	}
}

Testing out the functionality

Let’s try to access the endpoints as a client.

Without a signed certificate:

Untitled

With a signed certificate:

Untitled