Configuring Nginx with client certificate authentication (mTLS)

Viktor Petersson
WoTTsecurity
Published in
6 min readSep 18, 2019

Required Skill Level: Medium to Expert

One of the cornerstones of Zero Trust Networking is Mutual TLS (known as mTLS). In simple terms, this means that each client is required to present a certificate to talk to the server. This is different compared to how your client (e.g. your web browser) only verifies the identity of the server. The client itself does not need to present any identity. The identity piece is normally solved using some kind of credential (such as a username/password or API token).

By replacing credentials with certificates, we are able to significantly improve the security (in particular with short-lived certificates, like the ones we offer), while also making the implementation easier (as it removes the need for API key/credential management).

In this article we will make this all more concrete by creating a sample implementation. The sample implementation will consist of a simple Python appserver, with an Nginx reverse proxy in front of it. Nginx will reject all connections without a valid certificate, and the appserver will then compare the certificate to a whitelist of devices that are allowed to talk to the server.

Depending on your implementation, you could either use two Raspberry Pis for this, or you could use a Debian virtual machine as the server, and a Raspberry Pi as the client. The latter would be a more realistic setup for a live installation. Moreover, in the latter example, you can for instance use Let’s Encrypt as the public SSL certificate. This is useful if you use the same Nginx server to serve content for other clients, and not just for mTLS.

Preparation

Before we begin, we first need to install the WoTT agent on both the server and client(s). You can find instruction on how to do this here.

Once you have the WoTT agent installed, we need to install both Docker CE and Docker Compose (you can install Docker Compose on a Raspberry Pi by just running apt update && apt install docker-compose). We use these to simplify the installation, as we are able to better pin the requirements.

Setting up the server

Let’s start by setting up the server. To save you the time (and potential typos), we have create a sample repo for this, so all you need to do is to clone the repository:

$ git clone https://github.com/WoTTsecurity/examples.git
$ cd examples/nginx-with-mtls-and-appserver
$ docker-compose build

Thanks to Docker and Docker Compose, this is all we need to kick off the demo. The only final thing we need to do is to add the device that we will be connecting from to the whitelist. You can find out the WoTT Device ID by running sudo wott-agent whoami on the device you’re connecting from.

With the Device ID at hand, simply run the following commands on the server:

$ echo "MyDeviceId.d.wott.local" >> appserver/whitelist.txt

We can now fire up the server by simply running:

$ docker-compose up

To test the connection from your client, we need to find out two things:

  • The IP of the server
  • The WoTT Device ID of the server

Let’s test the client

Armed with the above information, we can now turn to our trusty old friend curl.

First, let’s try connecting without passing on our certificate:

$ sudo curl \
--cacert /opt/wott/certs/ca.crt \
--resolve 'MyServerID.d.wott.local:443:a.b.c.d' \
https://MyServerId.d.wott.local
<html>
<head><title>400 No required SSL certificate was sent</title></head>
<body>
<center><h1>400 Bad Request</h1></center>
<center>No required SSL certificate was sent</center>
<hr><center>nginx/1.16.0</center>
</body>
</html>

Because we have configured Nginx to require an SSL certificate, the server will reject the connection, and you won’t even be able to reach the appserver that we reverse proxy to.

If we however pass on our certificate (and key), we are able to successfully access the appserver:

$ sudo curl \
--key /opt/wott/certs/client.key \
--cert /opt/wott/certs/client.crt \
--cacert /opt/wott/certs/ca.crt \
--resolve 'MyServerID.d.wott.local:443:a.b.c.d' \
https://MyServerId.d.wott.local
Access Granted!

That’s really cool, but what happened here?

There’s a lot to unpack in what we did above, so let’s start with the client and work our way forward.

Client

Let’s break down the curl command we used to successfully connect to the server.

These two lines tell curl to send the client certificate and key.

--key /opt/wott/certs/client.key
--cert /opt/wott/certs/client.crt

This is our cryptographic identity provided by WoTT. In short, this replaces the need for a username and pasword. It should however be said that the key is not sent to the server (unlike a password), but rather it is used for a cryptographic challenge (vastly simplified).

Next, we need to tell curl to use the WoTT CA certificate to verify the remote server against (since WoTT is not a public CA):

--cacert /opt/wott/certs/ca.crt

Lastly, we use a neat little feature in curl to tell it to map ‘MyServerID.d.wott.local’ to an IP address. We could instead have added this to our /etc/hosts file, but this is a quicker workaround when testing.

--resolve 'MyServerID.d.wott.local:443:192.168.X.Y'

Nginx

Let’s move on to Nginx. We use Nginx as a reverse proxy for the appserver that we will cover below. We do this for a few reasons. The first reason is simply because Nginx is battle tested and does the first level of screening. If for instance, the client fails to present a valid certificate, the request will not be forwarded to the appserver. Hence this is a nice safety net from possible bugs in the appserver code.

In this particular example, we also terminate the TLS connection in Nginx. Should we want to improve security further (and adopt proper Zero Trust Networking), we could encrypt the traffic Nginx and the appserver too (even if they are on the same host in this case).

If the certificate is valid, Nginx will then reverse proxy the connection to the appserver.

The Nginx configuration is fairly straight forward and can be found here. The most noteworthy lines in the configuration are these:

# This will return a 403 to all clients without a proper certificate
if ($ssl_client_verify != "SUCCESS") { return 403; }
# This tells Nginx what CA to verify against
ssl_client_certificate /opt/wott/certs/ca.crt;
# This tells Nginx to verify clients
ssl_verify_client on;

In theory, we could extend this further and write a LUA script to do further validation, and even incorporate the whitelisting that we will get to in the appserver section, but that’s something for another day.

Appserver

Assuming the client passed all validations, the request will be passed on to the appsever. The appserver is a simple Flask app. In essence, all it does is to provide some validations on the headers. Since Nginx will pass on various HTTP Headers to the appserver, we can use them to implement access control.

When a request hits the appserver, it will check the HTTP header Ssl-Client-Verify is set to ‘SUCCESS’. If it isn’t, the request will be rejected with an error message. In theory, this shouldn’t be possible, since Nginx should never forward such request, but when it comes to security it’s better to be safe than sorry.

Assuming the above condition is correct, the appserver will parse the Client ID (from the ‘Ssl-Client’ header) and compare it to a whitelist (whitelist.txt from above). The whitelist is a simple text file with one Device ID per line. Only if the Client ID (i.e. the WoTT Device ID) matches a record in the whitelist, the appserver will return “Access Granted!”.

While the appserver is far from ready for production usage, it should help illustrate the setup such that it could be adopted into most languages and frameworks.

Conclusion

Hopefully you found this tutorial useful and that it helped bring the concept of Zero Trust Networking and mTLS to life with a real-world example of how it can be implemented with a relatively small amount of code.

If you have any questions, please get in touch with us on Twitter or open a Github Issue if you found any issues.

Originally published at wott.io.

--

--

Viktor Petersson
WoTTsecurity

CEO and Co-founder of @WireLoad / @ScreenlyApp. #DigitalNomad #Entrepreneur #Speaker #Geek #Cloud #DevOps