API Gateway Security using Mutual TLS Authentication - A Step-By-Step Guide

Author: Ganesh Velrajan

Last Updated: Thu, Sep 28, 2023

In this tutorial, you’ll learn how to create a simple HTTPS based API gateway server using Go’s standard net/http library and gin/gonic mux library. You’ll also learn how to create a self-signed SSL TLS X.509 server and client certificates using BastionXP CA. You’ll then use the certficates to configure the API gateway server to perform Mutual TLS Authentication(mTLS).

Mutual TLS will mandate the API gateway server and API clients to perform two-way authentication - meaning, they’ll authenticate each other using SSL certificates before establishing a secure encrypted connection between them.

Let’s get started.

Create SSL TLS X.509 certificates

Before you could create the API gateway server, you need to create a self-signed SSL/TLS X.509 certificate for the HTTPS based API gateway 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 organization.

For this tutorial, we’ll create the server and client certificates using BastionXP CA as shown below:

Server TLS certificate

$ bsh login  --auth-server ca.example.com  --no-auth --host api.example.com

Downloaded long-lived SSH & TLS certificates for the host.

Client TLS certificate

$ bsh login  --auth-server ca.example.com  --no-auth --user bob

Downloading certificates... Please wait.
Successfully downloaded short-lived certificates.
Your roles are: [].  Your access expires in 8 hours.

Certificates will be placed in the following location of the user’s home directory: /home/user/.bsh/

Once you have created the server and client TLS X.509 certificates and keys, you can move on to the next section.

Simple Golang HTTPS API Gateway Example

Here’s is a simple Golang based API gateway server example that uses HTTPS:

// api-gateway.go
package main
import (
	"crypto/tls"
	"crypto/x509"
	"fmt"
	"io/ioutil"
	"log"
	"net/http"
	"time"
	"github.com/gin-contrib/cors"
	"github.com/gin-gonic/gin"
)

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

func AddEmployeeHandler(ctx *gin.Context) {
	empId := ctx.Param("emp_id")
	empName := ctx.Param("emp_name")
	if empId == "" || empName == "" {
		ctx.JSON(http.StatusBadRequest, nil)
		return
	}
	fmt.Printf("Add Emp ID: %s Emp Name: %s", empId, empName)
	EmployeeID = empId
	EmployeeName = empName
	ctx.JSON(http.StatusOK, "OK")
}

func GetEmployeeHandler(ctx *gin.Context) {
	empId := ctx.Param("emp_id")
	if empId == "" {
		log.Println("Invalid Emp ID: ", empId)
		ctx.JSON(http.StatusBadRequest, nil)
		return
	}
	if EmployeeID == empId {
		ctx.JSON(http.StatusOK, gin.H{"emp_id": EmployeeID, "emp_name": EmployeeName})
	} else {
		ctx.JSON(http.StatusBadRequest, "Invalid employee ID.")
	}
}

func main() {
	router := gin.New() // gin.Default() prints debug logs by default
	router.Use(cors.New(cors.Config{
		AllowOrigins:     []string{"*"},
		AllowMethods:     []string{"GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"},
		AllowHeaders:     []string{"Origin", "Authorization", "Content-Type", "Accept", "Accept-Language"},
		ExposeHeaders:    []string{"Content-Length"},
		AllowCredentials: true,
		MaxAge:           24 * time.Hour,
	}))
	// API URL should be:  https://api.example.com/v1/employee
	private := router.Group("/v1")
	{
		private.POST("/employee/:emp_id/:emp_name", AddEmployeeHandler)
		private.GET("/employee/:emp_id", GetEmployeeHandler)
	}
	// load tls certificates
	serverTLSCert, err := tls.LoadX509KeyPair(CertFilePath, KeyFilePath)
	if err != nil {
		log.Fatalf("error opening certificate and key file for control connection. Error %v", err)
		return
	}

	// Configure the server to trust TLS client certs issued by the 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:   router,
		TLSConfig: tlsConfig,
	}
	defer server.Close()
	log.Fatal(server.ListenAndServeTLS("", ""))
}

[Note: For simplicity, the above program stores the employee data in global variables. In the real world, you will store the data in a database. ]

Description:

Here we are using the Go’s crypto/tls library to read and load the SSL TLS X.509 server 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. Next we create a http.Server instance with the following configuration:

  • The IP address and TCP port where the HTTPS web server would listen. In this example, the HTTPS server would listen on IP address 0.0.0.0 and TCP port 4443.
  • The HTTP request handler function to be invoked when a HTTP request is received from a client. We use the gin-gonic/gin HTTP request multiplexer(mux) library to create a router and add it to the HTTPS request handler. We have added two API routes to the router - namely, a POST and a GET API.
  • TLS Configuration with the following constraints:
    • 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 mTLS 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.

We finally, invoke the server.ListenAndServerTLS() method - to make the HTTPS server instance listen on the configured IP address and port specified in the server.Addr field, using the TLS certificates configured in server.TLSConfig field.

Run the Go API gateway server

Let’s run the Go API gateway server using the following command:

$ go run api-gateway.go
[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
 - using env:   export GIN_MODE=release
 - using code:  gin.SetMode(gin.ReleaseMode)

[GIN-debug] POST   /v1/employee/:emp_id/:emp_name --> main.AddEmployeeHandler (2 handlers)
[GIN-debug] GET    /v1/employee/:emp_id      --> main.GetEmployeeHandler (2 handlers)
```

Test using curl as an mTLS API client

For simplicity, we’ll use the curl utility to make the API requests throught this tutorial.

[Note: Alternatively, you can use the Postman tool to make an API request. You can find more info about the tool here: How to use Postman to perform mTLS authentication using SSL client certificates ]

We’ll ask curl to make a POST request to the API gateway server using the API /v1/employee/123/bob. This will invoke the AddEmployeeHanlder() method in the API gateway server.

The curl utility will print the API response message OK from the server.

We’ll pass the client certificate using the --cert flag, client private key using the --key flag, BastionXP Root CA certificate using the --cacert flags as arguments to the curl command so that it will act like an mTLS API client.

 $ curl -X POST https://api.example.com:4443/v1/employee/123/bob --cert ~/.bsh/tls_client.crt --key ~/.bsh/tls_client.key --cacert ~/.bsh/tls_root_ca.crt
 "OK"

Now, let’s try to get the employee info from the API gateway by making an HTTP GET request for the following API: /v1/employee/123

 $ curl https://api.example.com:4443/v1/employee/123 --cert ~/.bsh/tls_client.crt --key ~/.bsh/tls_client.key --cacert ~/.bsh/tls_root_ca.crt
 {"emp_id":"123","emp_name":"bob"}

We received the employee information in the HTTP Response and curl prints it on the screen for us.

So far so good.

API Security Experiment #1:

Next, let’s skip providing the CA certificate as part of the curl command and see what happens:

 $ curl https://api.example.com:4443/v1/employee/123 --cert ~/.bsh/tls_client.crt --key ~/.bsh/tls_client.key 
 curl: (60) SSL certificate problem: unable to get local issuer certificate
 More details here: https://curl.se/docs/sslcerts.html

 curl failed to verify the legitimacy of the server and therefore could not
 establish a secure connection to it. To learn more about this situation and
 how to fix it, please visit the web page mentioned above.

As expected, curl was not able to verify the authenticity of the server certificate because it doesn’t know the CA that issued the server certificate. So it cannot trust the server certificate.

API Security Experiment #2:

Next, let’s skip providing the client certificate and key as part of the curl command but provide the CA certificate and see what happens:

 $ curl https://api.example.com:4443/v1/employee/123  --cacert ~/.bsh/tls_root_ca.crt
 curl: (56) LibreSSL SSL_read: LibreSSL/3.3.6: error:1404C412:SSL routines:ST_OK:sslv3 alert bad certificate, errno 0

The complaint from the curl is very cryptic. We couldn’t comprehend much information from the error message, other than the bad certificate message we understand.

Let’s take a look at the error message thrown on the server side for missing the client certificate.

...
...
[GIN-debug] POST   /v1/employee/:emp_id/:emp_name --> main.AddEmployeeHandler (2 handlers)
[GIN-debug] GET    /v1/employee/:emp_id      --> main.GetEmployeeHandler (2 handlers)

Add Emp ID: 123 Emp Name: bob
2023/09/23 19:35:27 http: TLS handshake error from 127.0.0.1:63276: remote error: tls: unknown certificate authority
2023/09/23 19:35:45 http: TLS handshake error from 127.0.0.1:63279: tls: client didn't provide a certificate

As expected, the server threw a very bold and clear error message saying: "tls: client didn't provide a certificate"

The API gateway server was configured to perform a mTLS client authentication [tls.RequireAndVerifyClientCert] using the CA certificate pool provided. As a result, the API gateway squarely rejected the API request from the client that didn’t provide a client TLS certificate.

Now that you have a fully functioning mutual TLS HTTPS based API server, you can move on to create a mTLS HTTPS API client in Go. Please refer to the following tutorial: HTTPS Client (mTLS Client) Example in Golang using Self-Signed SSL/TLS Client Certificate

Automate SSL Certificate Management using BastionXP CA

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 CA simplifies enabling mutual TLS authentication for applications, virutal appliances, microservices and workloads in 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 granular 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.