Protect your Node.js API with nginx and SSL client certificates
Maxime Thoonsen4 min read
The problem we had
We had to secure some routes of our Node.js API so only trusted servers could call them. There are other solutions to do so, but for this project, we had to do it using SSL client certificates.
We followed the really nice tutorial from Nate Good that explained how to do it with nginx and FastCGI.
I decided to do a similar tutorial for nginx and Node.js. The following will help you build the same thing in your development environment.
If you don’t really know how nginx works, have a look at my blog post about the basics of nginx.
The certificates
First you need to create a CA key. What’s a CA? Let’s ask Wikipedia:
In cryptography, a certificate authority or certification authority (CA) is an entity that issues digital certificates.
This key will be used to sign client or server certificates. Signing a certificate is a way to say “I trust” this client or server.
# Create the CA Key and Certificate for signing Client Certs
openssl genrsa -des3 -out ca.key 4096
openssl req -new -x509 -days 365 -key ca.key -out ca.crt
Then you can create your server and client certificate.
# Create the Server Key, CSR, and Certificate
openssl genrsa -des3 -out server.key 1024
openssl req -new -key server.key -out server.csr
# Create the Client Key and CSR
openssl genrsa -des3 -out client.key 1024
openssl req -new -key client.key -out client.csr
Finally, you can sign your certificates with the CA you just made.
# We're self signing our own server cert here. This is a no-no in production.
openssl x509 -req -days 365 -in server.csr -CA ca.crt -CAkey ca.key -set_serial 01 -out server.crt
# Sign the client certificate with our CA cert. Unlike signing our own server cert, this is what we want to do.
openssl x509 -req -days 365 -in client.csr -CA ca.crt -CAkey ca.key -set_serial 01 -out client.crt
When you create your Certificate Signing Request (CSR), your web server application will prompt you for information about your organization
and your web server.
This information is used to create the certificate’s Distinguished Name (DN) allowing you to know which client is doing the request.
In our case, the client is a server we trust.
The nginx configuration
Here is a working nginx conf
server {
listen 8443;
ssl on;
server_name node-protected-app;
#Classic part of ssl
ssl_certificate /var/www/server.crt;
ssl_certificate_key /var/www/server.key;
#Here we say that we trust clients that have signed their certificate with the CA certificate.
ssl_client_certificate /var/www/ca.crt;
#We can choose here if we allow only authenticated requests or not. In our case it's optional
ssl_verify_client optional;
location / {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
#If the client certificate was verified against our CA the header VERIFIED
#will have the value of 'SUCCESS' and 'NONE' otherwise
proxy_set_header VERIFIED $ssl_client_verify;
#If you want to get the DN information in your headers
proxy_set_header DN $ssl_client_s_dn;
proxy_pass http://127.0.0.1:3000;
}
}
Using restify
Finally, here is a short implementation using restify in CoffeeScript:
restify = require 'restify'
exerciseApi = restify.createServer(
name: 'nginx-exercise'
)
# API Routes
exerciseApi.get '/', (req, res) ->
res.send 'Hello from the api'
# API Routes
exerciseApi.get '/protected-route', (req, res) ->
console.log req.headers
return res.send(401) if req.headers.verified isnt 'SUCCESS'
res.send 'This is a serious content'
exerciseApi.startServer = (port = 3000, callback) ->
console.log "Starting server on port "+port
exerciseApi.listen port, callback
exerciseApi.startServer()
It’s very simple, we just have to check the header to allow or not the request.
I have updated my nginx sandbox, if you want to try all of this
in a clean environment.
If you do so, inside the Vagrant you can test with cURl that your API is behaving as expected:
curl -k --key client.key --cert client.crt https://localhost:8443/protected-route
Using loopback
Let’s imagine you have a model with a dogs entity.
If you want to allow some specific actions to be done only by a trusted client, you can do the following:
//common/models/dogs.js
module.exports = function(Dogs) {
Dogs.beforeRemote('upsert', function(ctx, instance, callback) {
console.log("headers", ctx.req.headers);
console.log("verified", ctx.req.headers.verified);
//Here you can test the headers
});
};
Let’s say that you have the following dog.json
fixture file like:
{
"name": "Pluto",
"owner": "Mickey"
}
You can simulate an upsert with:
curl -X PUT -F "metadata=<dog.json;type=application/json" https://127.0.0.1:8443/api/dogs --key client.key --cert client.crt -k
SSL inside Node.js
Node can run a web server. It’s also possible to use SSL and SSL client authentication directly with Node.js. There is another blog post from Nate Good about it.
An issue you could encounter with cURL
On our project, we had some problems using curl with the certificates. We ended up making our tests with httpie which is also a nice tool.