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 port4443
. - 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 arouter
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 flagtls.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 thetls.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.