In this post, I’m going to share how I set up a Node.js server behind an AWS load balancer that can receive client certificates.
For those not in the know, client certificates are used all over healthcare—they form the trust backbone of networks like DIRECT, Commonwell, and The Sequoia Project. In most places where APIs are consumed, there is only one certificate pair—the server certificate. Some magic called a “TLS Handshake” happens, and using only the one pair, all traffic is encrypted.
Client certificates add a second pair of certificates, and as you can imagine, they belong to the client. They present an older (but still relatively sane and secure) method authentication. The process goes something like this:
- Some kind of manual exchange of the client certificate public key happens, and gets stored on the server application. This is usually done in the context of a phone call/e-mail conversation.
- The client is configured to use said public key, and private key making requests to the server.
- Incoming requests to the server need to be examined for the client certificate sent as part of the HTTPS request.
- The server needs to look up the sender via the public key already on file.
- The server needs to validate that the sender also has the private key, usually through the form of a signature.
Long story short, it’s similar in complexity to an OAuth style API authentication scheme, but relies on two organizations having a trust relationship ahead of time to share a public key.
Setting up a test environment
To test https with client certificates, you’ll need a client that supports it.
The desktop (non-chrome) version of postman supports them, or you can write a really basic node application like this one to test:
This part was pretty easy, but Anders Brownworth wrote a great article with a lot of great examples.
To integrate the auth with our other API auth schemes, we use passport.js, and I used a great passport module: passport-client-cert. Passport offers nice flexibility for us, since we can mix and match authentication requirements with ease. For example, if we wanted to stack requiring a client certificate, and our traditional bearer tokens, no new code needs to be written—it’s just configuration!
Inside our application, our customer success team has the ability to paste in the public key for a Redox source, so we store both the public key and the fingerprint to the database.
The fingerprint is indexed, and we first inspect that to find the source trying to authenticate. Fingerprints come in different forms, but it’s basically a hash of the public key. It’s hard but not impossible to find collisions for a fingerprint, so some kind of application-level validation needs to be done. We use Saml20 by leandrob to validate that the sending application also has the private key.
Our normal API services use Amazon’s ALB load balancers to do SSL termination, so I knew from the outset that they would not work. A load balancer is necessary because it provides a place to put in firewall rules and automatically spreads itself out over AZs, and behind the scenes, we use container orchestration to run the Node.js application out across a handful of EC2 instances.
After googling for a bit, I learned that a Classic load balancer could be used to provide a TCP passthrough. The configuration of the load balancer was straightforward enough – we use troposphere for all over our infrastructure, and I was able to quickly deploy and test the new load balancer (and corresponding EC2 autoscaling group).
During testing, I ran into restrictions on using ports below 1024 on MacOS. At the end of the day, I ended up using a port other than 443 and proxying it between the load balancer and the container. This makes developing locally much less cool since I need to use https://0.0.0.0:1337 for my postman/test app, but much easier and safer than running as root.
Actually getting certificates was also fun. Since we don’t terminate SSL on the load balancer, I used Digicert to sign certificates for this new application. And since the application runs in Docker containers, I opted to store the server certificates in very tightly locked down S3 bucket.
Client certificates are a neat and slightly complex way to do authentication. Compared to OAuth, there are not established best practices for how a server should implement the lookup (the passport-client-cert module has you write your own).
It’s important to understand how they work and expect some kind of signature for the certificate if you’re going to implement something similar. Of course, our customers are already benefiting from our implementation 😉