How to create Mutual TLS client in Go with self-signed SSL TLS certificate

Author: Ganesh Velrajan

Last Updated: Tue, Sep 12, 2023

In this tutorial, you’ll create a simple mTLS client using Go’s standard crypto/tls library. You’ll also create a self-signed SSL TLS X.509 client certificate using the OpenSSL tool. You’ll then use the certficate to run the Golang mTLS client and connect it to a Golang mTLS server.

Finally, you’ll also create a simple mTLS based HTTPS web client that enforces mutual TLS authentication using SSL client certificate.

Let’s get started.

Create SSL TLS X.509 certificate

Before you could create the mTLS client and server, you need to create a self-signed SSL/TLS X.509 certificate for the mTLS client and server application. For this, please refer to the following tutorial:

BastionXP is a free open-source based SSL TLS X.509 certificate management software to automatically generate, renew and manage SSL X.509 certificates for various applications(web server, web clients, database, web apps, workloads, devices) in your orgnaization.

Once you have created the SSL certificate and key, you can move on to the next section.

We’ll discuss two types of mTLS client and server examples written in Go in the below sections:

  • How to create a TCP based mTLS server and mTLS client application
  • How to create an HTTPS based mTLS server and mTLS client application.

Simple Golang TCP mTLS Server Example

Here’s is a simple Golang TCP mTLS server that we will use to connect our Golang mTLS client.

// mtls-server.go
package main

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

var (
	CACertFilePath = "/home/user/.bsh/tls_root_ca.crt"
	CertFilePath   = "/home/user/.bsh/tls_server.crt"
	KeyFilePath    = "/home/user/.bsh/tls_server.key"
)

func clientConnHandler(conn net.Conn) {
	defer conn.Close()
	_, err := conn.Write([]byte("Hello,World!\n"))
	if err != nil {
		panic(err)
	}
}

func main() {
	// load tls configuration
	cert, err := tls.LoadX509KeyPair(CertFilePath, KeyFilePath)
	if err != nil {
		panic(err)
	}

	// Configure the server to trust TLS client certs issued by a CA.
	certPool := x509.SystemCertPool()
	if caCertPEM, err := ioutil.ReadFile(CACertFilePath); err != nil {
		panic(err)
	} else if ok := certPool.AppendCertsFromPEM(caCertPEM); !ok {
		panic("invalid cert in CA PEM")
	}

	tlsConfig := &tls.Config{
		ClientAuth:   tls.RequireAndVerifyClientCert,  // enforce mTLS client authentication.
		ClientCAs:    certPool,
		Certificates: []tls.Certificate{cert},
	}

	listener, err := tls.Listen("tcp", ":4443", tlsConfig)
	if err != nil {
		panic(err)
	}

	fmt.Println("mTLS server listening on port: 4443")

	defer listener.Close()

	for {
		// listen for incoming connections and serve
		conn, err := listener.Accept()
		if err != nil {
			panic(err)
		}
		go clientConnHandler(conn)
	}
}

Here we are using the Go’s crypto/tls library to read and load the SSL TLS X.509 certificate and key pair from the files in disk. We then create a TLS config and set the TLS certificate field to the loaded TLS certificate and key pair.

We also enforce the TLS client applications to provide an SSL client certificate to establish an mTLS connection by setting the ClientAuth in the TLS config to RequireAndVerifyClientCert.

Next, we create a simple TCP based TLS server instance with the following configuration:

  • The IP address and TCP port where the TCP TLS server would listen. In this example, the TLS server would listen on IP address 0.0.0.0 and TCP port 4443.
  • The TLS configuration for the mTLS server.

We finally, invoke the listener.Accept() method on the TLS listener - to make the TCP mTLS server instance listen on the configured IP address and port.

Let’s execute the mTLS server program and run it in the background.


$ go run mtls-server.go &

Now, we’ll use the below mTLS client application written in Go to connect to the above mTLS server.

Simple Golang TCP mTLS Client Example

Here’s is a simple Golang TCP mTLS client example.

// mtls-client.go
package main
import (
	"crypto/tls"
	"crypto/x509"
	"fmt"
	"io"
	"io/ioutil"
	"net"
	"time"
)
var (
	CACertFilePath = "/home/user/.bsh/tls_root_ca.crt"
	CertFilePath   = "/home/user/.bsh/tls_client.crt"
	KeyFilePath    = "/home/user/.bsh/tls_client.key"
)
func main() {
	// load tls configuration
	cert, err := tls.LoadX509KeyPair(CertFilePath, KeyFilePath)
	if err != nil {
		panic(err)
	}
	// Configure the client to trust TLS server certs issued by a CA.
	certPool, err := x509.SystemCertPool()
	if err != nil {
		panic(err)
	}
	if caCertPEM, err := ioutil.ReadFile(CACertFilePath); err != nil {
		panic(err)
	} else if ok := certPool.AppendCertsFromPEM(caCertPEM); !ok {
		panic("invalid cert in CA PEM")
	}
	tlsConfig := &tls.Config{
		RootCAs:      certPool,
		Certificates: []tls.Certificate{cert},
	}
	conn, err := tls.DialWithDialer(&net.Dialer{Timeout: 15 * time.Second}, "tcp", "localhost:4443", tlsConfig)
	if err != nil {
		fmt.Println("Failed connecting to mTLS server: ", err)
		return
	}
	defer conn.Close()
	buf, err := io.ReadAll(conn)
	if err != nil {
		panic(err)
	}
	fmt.Println("Msg: ", string(buf))
}

We create a TLS config and set the TLS certificate field with the loaded client TLS certificate and key pair. We also load the CA certificate into the system's trusted CA certpool and add it to the TLS Config.

Later, we invoke the TLS Dialer with the TLS Config created above. This will initiate an mTLS connection to the mTLS server listening on the localhost tcp port 4443.

Once the mTLS connection is successfully established, the client reads the data sent from the server over the encrypted communication channel and prints it on the screen.

Let’s execute the mTLS client program and try connecting to the mTLS server.


$ go run mtls-client.go
Hello,World!

Now that we have successfully setup a TCP based mTLS server and mTLS client, let’s move on to create a mTLS HTTPS server and mTLS HTTPS client example.

Golang HTTPS Server with mutual TLS (mTLS) authentication

Here is a simple HTTPS server with mutual authenticaton TLS (mTLS) setup:

// https-server-mtls.go
package main
import (
	"crypto/tls"
	"crypto/x509"
	"io/ioutil"
	"log"
	"net/http"
)
var (
	CACertFilePath = "/home/user/ca-cert.pem"
	CertFilePath   = "/home/user/server-cert.pem"
	KeyFilePath    = "/home/user/server-key.pem"
)
func httpRequestHandler(w http.ResponseWriter, req *http.Request) {
	w.Write([]byte("Hello,World!\n"))
}
func main() {
	// load tls certificates
	serverTLSCert, err := tls.LoadX509KeyPair(CertFilePath, KeyFilePath)
	if err != nil {
		log.Fatalf("Error loading certificate and key file: %v", err)
	}
	// Configure the server to trust TLS client cert issued by your CA.
	certPool := x509.NewCertPool()
	if caCertPEM, err := ioutil.ReadFile(CACertFilePath); err != nil {
		panic(err)
	} else if ok := certPool.AppendCertsFromPEM(caCertPEM); !ok {
		panic("invalid cert in CA PEM")
	}
	tlsConfig := &tls.Config{
		ClientAuth:   tls.RequireAndVerifyClientCert,
		ClientCAs:    certPool,
		Certificates: []tls.Certificate{serverTLSCert},
	}
	server := http.Server{
		Addr:      ":4443",
		Handler:   http.HandlerFunc(httpRequestHandler),
		TLSConfig: tlsConfig,
	}
	defer server.Close()
	log.Fatal(server.ListenAndServeTLS("", ""))
}

What we do in this Go HTTPS web server:

  • Enforce HTTPS client authentication. A HTTP client that connects without a client TLS X.509 certificate will be rejected. The tls.Connfig.ClientAuth field is set with the flag tls.RequireAndVerifyClientCert to enforce HTTPS client authentication.

  • Make the HTTPS server to trust the Certificate Authority(CA). So that the HTTPS server accepts any HTTPS client connecting with a TLS X.509 client certificate issued by this CA. We create a new certficate pool using x509.NewCertPool(), add the CA certificate to the certificate pool and finally add the certifiicate pool to the tls.Config.

Test HTTPS server with Curl command

Now, let’s test the above HTTPS server, enforcing mutual TLS authentication(mTLS), with the curl utlity. If we run the curl command as we did in the previous example, it will throw an error as shown below:


$ curl https://localhost:4443 --insecure
curl: (56) OpenSSL SSL_read: error:14094412:SSL routines:ssl3_read_bytes:sslv3 alert bad certificate, errno 0

This is because the curl command is not using any client TLS certificate to connect with the mTLS based HTTPS server. We need to pass the client certificate and key as arguments to the curl command as shown below:


$ curl https://localhost:4443 --insecure --cert client-cert.pem --key client-key.pem
Hello,World!

Now that you have a fully functioning mutual TLS HTTPS server, you can move on to create a mTLS HTTPS client with SSL client certificate in Go.

Golang mTLS HTTPS web client example with X.509 SSL Client Certificate

Here is a simple mTLS HTTPS web client in Go that uses SSL client certificate authentication.

//https-client-mtls.go
package main
import (
	"crypto/tls"
	"crypto/x509"
	"fmt"
	"io"
	"io/ioutil"
	"log"
	"net/http"
)
var (
	CACertFilePath = "/home/user/ca-cert.pem"
	CertFilePath   = "/home/user/client-cert.pem"
	KeyFilePath    = "/home/user/client-key.pem"
)
func httpsClient(url string) []byte {
	// load tls certificates
	clientTLSCert, err := tls.LoadX509KeyPair(CertFilePath, KeyFilePath)
	if err != nil {
		log.Fatalf("Error loading certificate and key file: %v", err)
		return nil
	}
	// Configure the client to trust TLS server certs issued by a CA.
	certPool, err := x509.SystemCertPool()
	if err != nil {
		panic(err)
	}
	if caCertPEM, err := ioutil.ReadFile(CACertFilePath); err != nil {
		panic(err)
	} else if ok := certPool.AppendCertsFromPEM(caCertPEM); !ok {
		panic("invalid cert in CA PEM")
	}
	tlsConfig := &tls.Config{
		RootCAs:      certPool,
		Certificates: []tls.Certificate{clientTLSCert},
	}
	tr := &http.Transport{
		TLSClientConfig: tlsConfig,
	}
	client := &http.Client{Transport: tr}
	resp, err := client.Get(url)
	if err != nil {
		panic(err)
	}
	defer resp.Body.Close()
	fmt.Println("Response status:", resp.Status)
	msg, _ := io.ReadAll(resp.Body)
	return msg
}

func main() {
	// hello, world.
	msg := httpsClient("https://localhost:4443")
	fmt.Println("Msg: ", string(msg))
}
$ go run https-client-mtls.go
Hello,World!

Automate SSL Certificate Management using BastionXP

BastionXP PKI/CA simplifies and automates the management of SSH and SSL/TLS X.509 certificates(both server and client certificates) without affecting the end user workflow.

BastionXP automatically generates new server and client SSH, SSL/TLS X.509 certificates after an end user successfully authenticates via a 2FA (two-factor authentication) enabled SSO provider such as Microsoft Azure 365, Google G-Suite, Okta, GitHub, Keycloak or any SSO provider.

BastionXP PKI/CA with built-in Role Based Access Control (RBAC), issues short-lived SSH and SSL/TLS client certificates to end users, so that IT admins have fine-grained control over who can access what resources in your organization and for how long.

Start Your Free Trial Now

Try BastionXP for free with no commitments. No credit card required.