In the previous post of this series we saw how we could perform RPC on top of a multiplexed duplex stream. In this article we’re going to show you how you increase the security of the client and the server applications by encrypting your communication channel and adding authentication.
Enjoy!
So far our TCP server has communicated with clients without any kind of security: it lets any client connect to it and it communicates with them through a clear-text channel, making it prone to someone eavesdropping on the network. These may not be a big problem for your office refrigerator application, but it can surely be if your data is sensitive, or you can’t let just every and anyone use the service.
Let’s first address this last problem: encrypting the communication channel.
Creating self-signed certificates
In this example we will create a self-signed certificate. This is a certificate that was not issued by a recognised Certificate Authority (CA), which means that the clients won’t be able to verify this certificate unless they have access to the public certificate we’re about to issue.
If you can distribute the server public certificate to the clients, this is the most flexible approach where you don’t have to rely on a third party to issue your certificate.
Before you can generate a certificate you need to have the openssl
command-line tool installed. Many Linux distributions, and MacOS, come with the openssl
program installed.
Creating a Certification Authority
Now you’re going to create your own Certification Authority, which you will later use to create the server and client certificates.
Once you have that installed, create a certs/ca
directory, and in it, generate the CA private key:
This creates a suitable private key and writes it to the private-key.pem
file.
Next we’re going to create the CA public certificate based on the private key:
This outputs the CA certificate into the certificate.pem
file.
Creating the server key and certificate
Now we need to issue the server certificate, but first let’s create the private keys inside the certs/server
directory:
Next, create a Certificate Signing Request (CSR) file using your private key:
The purpose of this CSR is to “request” a certificate. That is, if you wanted a CA to sign your certificate, you could give them this file to process and they would give you back a certificate.
However, here you will self-sign your certificate, again using your CA private key:
Adding TLS support to the server
Now we need to modify the server to use TLS instead of raw TCP:
fridge_server.js:
Here you can see that we’re using tls.createServer(options)
to create our server, passing in some options:
- key: the server private key, loaded as a raw buffer
- cert: the server certificate, also loaded as a raw buffer
When a client successfully establishes a secure connection, the server emits a secureConnection
event, which is what we want to listen to now.
The server also emits a connect
event, but that gives you a unencrypted TCP socket as before, which is not what you need now.
Using TLS on the client
Now, let’s enable TLS also on the client:
We’re now using tls.createConnection(options)
to connect to the server, passing in some options:
- host: hostname from the command-line arguments, the same as before;
- port: TCP port number from the command-line arguments, same as before;
- rejectUnauthorized:
false
, telling the client that it should not check the server certificate validity. This option is here because, for now, we just wish to have an encrypted channel – we're not looking into authenticating the server (yet).
Authenticating the server
Let’s now allow the client to authenticate the server by making just some small changes to the tls.connect
options:
Here we’ve added one attribute to the config: ca
, where we define the root Certification Authorities that the client will take into account when validating the server certificate. Here we pass in the root public certificate, admitting that the client trusts the certificates signed by it.
Also, we change rejectUnauthorized
from false
to true. If, when establishing a secure connection, the client cannot, for some reason, verify that the server certificate was issued by any of the trusted Certification Authorities (or any of their child CAs) or that the server name does not match the common name field (CN) in the certificate data, the connection emits an error and closes.
If, by chance, you choose to run this server in a different host name, you will have to reissue the certificate, changing its CN field.
You can test this by launching the server on a command-line window:
and then launching the client on another:
Authenticating the client
For the server to authenticate the client we need to use our CA to issue a new certificate. First though, we need to create a client certificate:
Next, create a Certificate Signing Request (CSR) file using your private key, just like you did when you requested the server one:
Let’s then use our CA to create the signed certificate:
Now we can use it on the client, modifying the client options to add the private key and the public certificate:
On the server side, let’s change some options to force the server to get the client certificate and validate the certificate against our CA:
Revoking access
Once one client certificate is issued, our server will accept it forever. To be able to reject any given client with a valid certificate, we’d have to keep a list of supported client names (white list) or a list of banned client names (black list). To make things simpler for now, let’s keep a white list in the server memory, against which we check the client certificate before accepting a connection.
On the server, we can then simply create this client white list, containing only our one client name for now:
Now, in the connection handler we can fetch the client certificate and then use the clientAllowed
function to check whether we can proceed:
This method of whitelisting client names requires that you restart your process every time there is a change. You can implement a gossip protocol over the network where all the changes (inserts or removals, in this case) in a given list are automatically propagated to all of the nodes within a certain amount of time. For more information you can check another book in this series named “Configuration Patterns”.
Next Article
This was the last article on the subject of Networking Patterns.
You can find all the previous posts on this topic here:
- Building a TCP service using Node.js
- Using a remote emitter
- Multiplexing streams
- Turning a stream into an RCP channel
In our next article in this series we’ll start taking a look at queues and how you can use them streamline and share asynchronous work between processes.Next Article
In our next article for this series we’ll start taking a look at queues and how you can use them streamline and share asynchronous work between processes.
This article was extracted from the Networking Patterns, a book from the Node Patterns series.