Set Up a Secure Node.js Web Application

Karl Düüna
Security and Node.js
13 min readFeb 15, 2016

--

Every application has to live somewhere — a server, a phone, a device — an environment. Before we start worrying if we have used secure coding practises and avoided common mistakes we must secure the foundation of our application otherwise all our effort in avoiding injection or request forgery and other attacks will be for naught.

In this blog post we will look at setting up our Node.js application server. We will start with a clean server, configure some basic security features, configure a Nginx proxy, create a SSL certificate and run our application. More specifically we will be looking at:

  • disabling the ROOT account and enabling key based authentication
  • creating a low privilege account and running our services under it
  • setting up systems to fork our service and to restart the service if the server should reboot
  • configuring a proxy in front of our server to handle file serving and SSL with A+ quality
  • obtaining a legitimate SSL certificate for our service
  • configuring our firewall

It’s going to be a long ride, so hold on.

While this blog post will not cover all the necessary topics for a secure server and is not suitable for large scale deployment it will nonetheless provide a decent starting point.

For a more in depth look on Node.js security read my book Secure Your Node.js Web Application: Keep Attackers Out and Users Happy

Set up the Server

To deploy our application somewhere we will first need a server. I will be using DigitalOcean as my host but since I won’t be using their “One Click Apps” the hosting partner doesn’t really matter. Fast forward a minute and I have a Debian 8.2 server up and running. I have also received my root password on my email along with the IP address. This is the state we will be starting out from.

Using the password from my email logging in with the root account will immediately ask me to change the password and after that I am officially in my server. First things first let’s update our server software.

root# apt-get update 
root# apt-get upgrade

Keeping your server software up to date is an important part of security hygiene. Be sure to do this regularly or set up an autoupdate procedure using UnattendedUpdates for Debian systems or AutoUpdates for Red Hat systems.

Setting up Accounts

There are 3 main things we need to do in order to proceed securely:

  1. Create separate user account(s) — this is so that we can track people coming going from the server.
  2. Use key based authentication — key based authentication is much securer than password based authentication.
  3. Disable direct root account login over SSH — this will stop people from logging in as root accounts and instead have to sudo.

Create new account

Let’s begin by creating a new account. It can be done easily by using the following commands:

root# useradd -c "Joe User" -s /bin/bash -m joe 
root# passwd joe

The first command will create a user called joe and the second will set the user’s password and unlock the created user.

Or you can use the adduser joe command, which will prompt you for the information.

Now test your user and make sure you can log in

Use key based authentication

We need to get rid of the insecure password based authentication that is the default. For this we will need to configure our server for key based authentication — we need to add our public key to the accounts authorized_keys file.

For this we will create a “.ssh” folder under the user’s home directory and a file called “authorized_keys” under it.

root# mkdir ~joe/.ssh 
root# chmod 700 ~joe/.ssh
root# touch ~joe/.ssh/authorized_keys

Now we need to add our public key to the file — make sure you copy the key without whitespaces. (If you are unfamiliar with creating SSH keys, then have a look at this article by GitHub).

root# nano ~joe/.ssh/authorized_keys

Next let’s grant ownership of the file to the account, so that they can later change their own settings.

root# chmod 600 ~joe/.ssh/authorized_keys 
root# chown -R joe:joe ~joe/.ssh

And once again test if we can log in

Disable root account login

Before we actually disable root account login we want to make sure that accounts that need to can actually elevate their privileges to access root privileged operations through sudo.

Since we are running Debian our system won’t have sudo installed so we will need to install it manually.

root# apt-get install -y sudo

Now that we have sudo installed we can configure who has access to the sudo command through the visudo utility.

root# visudo

Find the part that deals with User privileges and add our user there

# User privilege specification 
root ALL=(ALL:ALL) ALL
joe ALL=(ALL:ALL) ALL

Now our “joe” can use sudo command_name to run a command with root privileges by providing their password. Great — we no longer need to allow root account login over SSH. To disable this we will need to modify the “/etc/ssh/ssh_config” file.

root# nano /etc/ssh/sshd_config

We need to add/change so that we have the following configurations in our file

PasswordAuthentication no 
PermitRootLogin no

If we have done that we should reload our ssh service configuration

root# /etc/init.d/ssh reload

Our root account won’t be able to log in any more and other accounts won’t be able to use password authentication by default.

We have now set up our main user account “joe” who has sudo access, disabled the root account and are using key-based authentication. All is well with the world, so let’s move on.

Set up Node Service

Next we are going to set up our Nodejs process. For that we will need to install Node.js, create an unprivileged user
and run our node service as the unprivileged user.

First let’s start by installing Node.js by the official guide.
Since our Debian doesn’t have curl we’ll start by installing that.

joe# sudo apt-get install -y curl 
joe# curl -sL https://deb.nodesource.com/setup_5.x | sudo -E bash -
joe# sudo apt-get install -y nodejs

Now we have Node v5.x installed (Currently 5.4), lets move on to creating our run user.

joe# sudo useradd -r -s /bin/false --home /var/appdata/fooapp fooapp

With this command we’ll create a user who is not able to log in, and has a home folder under “/var/appdata/fooapp”.
And since we won’t add this user to the sudoers file they will not have any sudo capabilities. This is not the lowest privileged user, but for starters it is good enough.

Next we’ll create the folder and app file for testing.

joe# sudo mkdir -p /var/appdata/fooapp 
joe# sudo nano /var/appdata/fooapp/app.js

We can now test that this app would actually run — start it up by

joe# node /var/appdata/fooapp/app.js

After we have verified that we are able to visit “http://yourIp:2765" and see a “hello” we can move on. We need to make sure our process can handle shutdowns and crashes. For this we need to set up a process manager and startup scripts. We also want to have a process manager so that we can cluster our service. I’ll be using systemd and pm2.

First I need to install some dependencies so that I can build the pm2 module.

joe# sudo apt-get install -y build-essential git

Then I’ll install the pm2 module under the fooapp directory.

joe# cd /var/appdata/fooapp 
joe# sudo npm install pm2

Now that we have installed everything that we need let’s grant all the files over to our fooapp user.

joe# sudo chown -R fooapp:fooapp /var/appdata/fooapp

Next we need to set up our script so that when the server restarts our application will also be restarted.

We could use the pm2 startup command to generate the startup script, but it is known to be somewhat problematic and it did not work as expected for me, so instead I’m going to use the systemd directly for this. First we are going to create a file in the “/etc/systemd/system/multi-user.target.wants/” folder for our service.

joe# sudo nano /etc/systemd/system/multi-user.target.wants/fooapp.service

After which we need to reload the configuration and start our service.

joe# sudo systemctl daemon-reload 
joe# sudo systemctl start fooapp.service

And now all that’s left is to test that we can still visit “http://yourIp:2765".

We have now set up our service to run as a low privilege user with systemd to restart our application when the server restarts and pm2 to handle our service day to day restarts and clustering.

Set up a Proxy

A common and a very dangerous mistake people do is run the node process with root privileges. Why do they do that? Well because binding to a low port number (like 80 or 443) requires high privileges.

We have already been smarter about that by running our service on a high port and using a low privilege user. However nobody is going to like adding a port number to our domain name, so we need to proxy the requests. The crudest way would be to set up a firewall port forwarding, but we are instead going to set up a Nginx server, because:

  1. We can then run Node.js on a high port — this means we can limit the rights it has
  2. Nginx is better at SSL handshakes — setting up an SSL connection is a resource heavy operation, we don’t want our single thread to handle it.
  3. Nginx is better for serving static files — serving files also is better handled by the multi threaded Nginx.
  4. HTTP2 — newer versions of Nginx already support HTTP2 protocol.

The default Nginx version that ships to the Debian 8.2 distro is 1.8.0, the newest Nginx version at the time of writing is 1.9.9.

Of course we want the newest, because as of 1.9.5 we will have HTTP2 support and so can enjoy the benefits of the future.

For this we will need to update the “/etc/apt/sources.list”, upgrade OpenSSL and install the Nginx server. Because Debian Jessie ships with OpenSSL v1.0.1 and for HTTP2 we need at least 1.0.2 then we must update the library to a newer version. To do that we will add the “sid” suite to the sources list and install openssl by specifying the suite.

joe# echo "deb http://ftp.debian.org/debian sid main" | sudo tee -a /etc/apt/sources.list 
joe# sudo apt-get update
joe# sudo apt-get -t sid install -y openssl

Now running openssl version should return “OpenSSL 1.0.2e 3 Dec 2015”.

Or the same on Ubuntu 14.04

joe# echo "deb http://security.ubuntu.com/ubuntu wily-security main" | sudo tee -a /etc/apt/sources.list
joe# sudo apt-get update
joe# sudo apt-get -t wily-security install -y openssl

Let’s move on to installing Nginx. We have to add the Nginx package repository to the sources list so apt-get can find it. We’ll start by adding the nginx signing keys to our apt-get.

joe# wget http://nginx.org/keys/nginx_signing.key 
joe# sudo apt-key add nginx_signing.key

And then we’ll update the sources.list file

joe# echo "deb http://nginx.org/packages/mainline/debian/ jessie nginx" | sudo tee -a /etc/apt/sources.list
joe# echo "deb-src http://nginx.org/packages/mainline/debian/ jessie nginx" | sudo tee -a /etc/apt/sources.list

Or for Ubuntu 14.04

joe# echo "deb http://nginx.org/packages/mainline/ubuntu/ trusty nginx" | sudo tee -a /etc/apt/sources.list
joe# echo "deb-src http://nginx.org/packages/mainline/ubuntu/ trusty nginx" | sudo tee -a /etc/apt/sources.list

And now let’s install the newest Nginx

joe# sudo apt-get update 
joe# sudo apt-get install nginx

We can verify that our server is running by visiting the IP address/domain in the browser. We should see the Nginx welcome message. Now that we have set up our Nginx we need to configure it, however before we configure the proxy let’s obtain a SSL certificate using the default configuration.

Let’s Encrypt with letsencrypt

One of the greatest innovations and ease of use improvements of last year was the emergence of Let’s Encrypt initiative. Let’s Encrypt is part of the HTTPS everywhere collective and it allows domain owners to quickly obtain SSL certificates for free. As of 2nd of December 2015 it is in public beta, so everyone can use it. So shall we to obtain a valid SSL cert.

This does require for you to have a valid domain name, that is pointing to this server instance. I will be using “example.com” in my examples.

In order to do that we’ll install the letsencrypt tool.
We’ll start in our home folder (/home/joe)

joe# git clone https://github.com/letsencrypt/letsencrypt 
joe# cd letsencrypt

Next well use the letsencrypt-auto script to do everything for us. Since the support for Nginx is still very rough and buggy we’ll instead use the — webroot option to obtain our certificates. Nginx’s default webroot folder is “/usr/share/nginx/html” so we will use that.

joe# ./letsencrypt-auto certonly --webroot --webroot-path /usr/share/nginx/html --renew-by-default --email example@example.com --text --agree-tos -d www.example.com -d example.com

This will ask for sudo permissions and install a bunch of dependencies for the client — can take quite a while.

After running this command you should see a message about recovery and that the certs have been obtained and where they are located. (Yes it really is that simple)

Now let’s move on to configuring our Proxy to use HTTPS and talk to our Node.js application.

Configure Nginx With SSL

In the last two sections we set up our Nginx server and obtained a SSL certificate which we can use to set up our secure web server. Not only will we be setting up our proxy and HTTPS, but we will also configure our Nginx to have more secure settings.

We will begin by creating a new stronger Diffie-Hellman key (the default is 1024, we’ll substitute with 4096)

joe# sudo mkdir /etc/nginx/ssl 
joe# cd /etc/nginx/ssl
joe# sudo openssl dhparam -out dhparam.pem 4096

Next we are going to remove our old default configuration file and create our own for our service.

joe# sudo rm /etc/nginx/conf.d/default.conf 
joe# sudo nano /etc/nginx/conf.d/fooapp.conf

The contents of the nginx configuration should be something like as follows:

Next we need to restart our Nginx service.

joe# sudo /etc/init.d/nginx restart

And now we can validate that everything is working by going to “http://example.com" and seeing if it redirects to HTTPS and shows our “hello” message.

We can also visit SSL Labs website to verify that our SSL setup is secure — we should see something like:

We now have our service running using proper SSL and under low privileges. But we are not yet done there is still a bit to go — we are missing a firewall.

Set up Firewall

Currently we have no firewall setup. Our Node service is directly accessible from the web by specifying the port number. This also means that other services that we might install on the machine (like the database etc) will be open to the web.

This is not good from the security standpoint — so we will configure our firewall to only allow traffic in and out of our server that we have deemed fit.

In linux the firewall is configured using iptables. We will create the ruleset files and then import them to our iptables. The following configuration is just an example and not suitable for all servers.

Note: Do be careful when dealing with firewall — there is a good chance that you can lock yourself out of your instance if you do non-atomic updates on the settings. In DigitalOcean you can then use the console on the website to log in and fix the situation, but this is not the case in all hosting providers.

First we’ll create ‘/tmp/ip4’

joe# nano /tmp/ip4

With the following contents

Then we’ll create ‘/tmp/ip6’ and block all IPv6 traffic since I won’t be using it. If you want to support IPv6 traffic you need to modify the IPv4 configuration and configure the ip6tables.

joe# nano /tmp/ip6 

Now that we have set up the rules it’s time to import them to our firewall configuration. We will first flush all previous
configuration that might be there and then import the new ones.

joe# sudo iptables --flush 
joe# sudo iptables-restore < /tmp/ip4
joe# sudo ip6tables-restore < /tmp/ip6

Now we have our firewall settings imported we can verify them using

sudo iptables -L

However our firewall rules will be reset once we do a server restart. In order to have persistent firewall rules we need to
install iptables-persistent

joe# sudo apt-get install iptables-persistent

This will ask if we want to save current rules, for which the answer is Yes.

Now we have set up our firewall we should remove the temporary files.

joe# rm /tmp/{ip4,ip6}

And all that is left is to verify that our site is accessible and if we try to access the service directly it will fail.

Wrapping Up

In this blog post we looked at how to configure our server from the ground up:

  • We disabled the ROOT account and enabled key based authentication
  • We created and ran our service using a low privilege account
  • We set up systems to fork our service and to restart the service if the server should reboot
  • We configured a proxy in front of our server to handle file serving and SSL with A+ quality
  • We obtained a legitimate SSL certificate for our service
  • We configured our firewall

While this might seem like a lot there are quite a few things that we didn’t cover in this blog post:

  • We didn’t set up automatic security update installation procedures
  • We didn’t analyse and clear our server of unused services
  • We didn’t deal with any database communication and securing in this post
  • We didn’t set up proper log aggregation with analysis/signing and notifications
  • We didn’t set up a backup system with encryption and signing

However even these services aside our setup is now much more secure than simply copying our files onto a server and running sudo node app and be done with it.

This post is part of the Node.js Web Application Security Series. In this series of blog posts we will look at various topics of web application security and how they can be addressed in Node.js applications.

Knowledge about web security is something every decent web developer should know. Even if you are not security oriented the knowledge will help in designing more robust and secure systems, which in turn makes you a better developer overall.

On that note I recommend you buy and read my book “Secure Your Node.js Web Application”, which provides more in depth knowledge about Web Application Security and implementing security mechanisms in Node.js applications.

Originally published at nodeswat.com on February 15, 2016.

Edit to add OpenSSL and Nginx installation for Ubuntu 14.04

--

--

Entrepreneur & Hacker by heart. CTO of http://www.nodeswat.com — researching and developing scalable & secure #nodejs apps