How to create Mutual TLS server 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 mutual TLS or mTLS server using Go’s standard crypto/tls library. You’ll also create a self-signed SSL TLS X.509 server certificate using the OpenSSL tool. You’ll then use the certficate to run the Golang mTLS server.

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

Let’s get started.

Create SSL TLS X.509 certificate

Before you could create the mTLS server, you need to create a self-signed SSL/TLS X.509 certificate for the HTTPS web server. 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 server SSL certificate and the key, you can move on to the next section.

We’ll discuss two types of mTLS server and client 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 example:

// 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.

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

// 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))
}

Let’s run the mTLS client application 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.

Enforce mutual TLS authentication using BastionXP CA within your organization.

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.