tutorial // Apr 01, 2022

How to SSH Into a Server Using Node.js

How to set up a server on Digital Ocean, create an SSH key, and use the `node-ssh` package to SSH into that server using your SSH key.

How to SSH Into a Server Using Node.js

Getting Started

For this tutorial, we're going to use CheatCode's full-stack JavaScript framework, Joystick. Joystick brings together a front-end UI framework with a Node.js back-end for building apps.

To begin, we'll want to install Joystick via NPM. Make sure you're using Node.js 16+ before installing to ensure compatibility (give this tutorial a read first if you need to learn how to install Node.js or run multiple versions on your computer):

Terminal

npm i -g @joystick.js/cli

This will install Joystick globally on your computer. Once installed, next, let's create a fresh project:

Terminal

joystick create app

After a few seconds, you will see a message logged out to cd into your new project and run joystick start. Before you run that, we need to install one dependency, node-ssh:

Terminal

cd app && npm i node-ssh

Once you have this installed, you're ready to start your app:

Terminal

joystick start

After this, your app should be running and we're ready to get started.

Generating an SSH Key

In order to demonstrate using SSH to communicate with a server, it's best to start with making sure we have an SSH key on hand. While you can SSH into a server using a username and password, this should be avoided as passwords alone are more vulnerable to attack than an SSH file.

To begin, we're going to generate an SSH key using the ED25519 standard which is a newer, faster, more secure cryptography standard. First, we want to create a new folder at the root of the app generated for us by joystick start called private and then inside of that folder, we'll create another called ssh:

Terminal

mkdir private
cd private
mkdir ssh
cd ssh

Once you've cd'd into /private/ssh from the root of your app, next, we want to generate an SSH key:

Terminal

ssh-keygen -t ed25519 -C "ryan.glover@cheatcode.co"

On your computer, you should have a built-in tool called ssh-keygen. Like the name suggests, it's used to generate SSH keys. Here, we're calling ssh-keygen passing two flags: -t which stands for the "type" of key to generate (here, an ed25519 key) and then -C which stands for "comment" (here, we use this to input our email address as the comment is appended to the end of our public key and hints at its original intent).

This will prompt you with a few questions (hit enter/return after typing your answer for each)...

  1. For the "Enter file in which to save the key" prompt, you want to enter ./<your-email-address> where <your-email-address> should be replaced with the email address you want to use for this key (e.g., ./ryan.glover@cheatcode.co). Note: the ./ at the beginning is important as it ensures the file is stored in the private/ssh folder we just created.
  2. Next, you will be prompted to enter a passphrase. This is highly recommended. Adding a passphrase to your SSH key adds another layer of security so that, in the event your SSH key is leaked/exposed, the attacker would also need the password for the key to use it. Make note of the password you enter as we'll use it later.
  3. Next, you will be prompted to confirm the password you input from step #2.

After this is complete, you should see something like this printed to the terminal:

Terminal

Your identification has been saved in ./ryan.glover@cheatcode.co
Your public key has been saved in ./ryan.glover@cheatcode.co.pub
The key fingerprint is:
SHA256:VUwq60W7bY4hWW/rmr4LdvggZ5Vg+JNwGo9nONfe5hs ryan.glover@cheatcode.co
The key's randomart image is:
+--[ED25519 256]--+
|           oo    |
|       .   o.    |
|      + = +      |
|       @ O o     |
|      = S B      |
|       * O =     |
|      . @ = E    |
|       = * X o   |
|         .O=*.   |
+----[SHA256]-----+

More importantly, you should also see two files in private/ssh: private/ssh/<your-email-address> and private/ssh/<your-email-address>.pub. The first being your private key and the latter being your public key.

The distinction here is important. As we'll see in a bit, we'll give our .pub or "public key" to the host where our server lives. Later, when we "SSH into" our server, we'll pass our private key along with the request. Behind the scenes, our host will check to see if it has a public key corresponding to that private key. If it does and the signatures match one another (and the password is correct), our request will be allowed to go through.

Creating a Digital Ocean Droplet

In order to demonstrate using SSH, we need a remote server that we can actually communicate with. For our example, we're going to set up a droplet on Digital Ocean (Droplet is Digital Ocean's brand name for a server instance). Our goal will be to get access to a server—more specifically, its IP address—and use that in our SSH requests.

First, if you don't already have a Digital Ocean account, head over to the signup page and create an account.

9p93YPBRTt8ZcxLO/qrxqGwLH9IIYaOce.0
Creating an Account on Digital Ocean

Once you have your account setup and verified, we want to head to the projects dashboard and in the top-right corner, click the "Create" button and from the dropdown, "Droplets."

9p93YPBRTt8ZcxLO/qut3DZsRitUieUIh.0
The "Create Droplets" menu option in the Digital Ocean dashboard.

From the next screen, we need to select the following options:

  1. Under "Choose an image" we want to select the first box "Ubuntu" and make sure that the "20.04 (LTS) x64" option is choosen in the dropdown at the bottom of that box.
  2. Under "Choose a plan" we want to select "Basic" and then under "CPU options" select "Regular with SSD" and the first "$5/mo" option with 1GB/1CPU.
  3. Under "Choose a datacenter region" select whichever region is closest to you (I'm selecting "New York 1" for this tutorial).
  4. Under "Authentication" make sure "SSH keys" is selected and then in the box beneath this, click the "New SSH Key" button. This will reveal a new window prompting you for "SSH key content" and a "Name." For "SSH key content," you want to paste in the contents of the <your-email-address>.pub file from your private/ssh folder and for "Name," you want to enter your email address.
9p93YPBRTt8ZcxLO/4sLOXaRPvU2GXz0A.0
Adding an SSH key on Digital Ocean
  1. Optionally, toward the bottom, under "Choose a hostname" enter a more friendly name than the auto-generated one you get (e.g., "ssh-tutorial" or "cheatcode-tutorial") so you remember what it's for.
  2. Click the green "Create Droplet" button.

After this, you will be redirected back to your projects dashboard. You should see a loading bar for the Droplet you just created but if you don't, hit refresh and it should appear. Once it does, click on its name to reveal its dashboard:

9p93YPBRTt8ZcxLO/urzEKQvORxU9lf21.0
Viewing your Droplet on Digital Ocean.

Once you see this, you're all set! Now that we have a server we can SSH into, next, we want to jump into our app code and learn how to use SSH via Node.js.

Wiring up a getter to SSH into our server

Now for the fun part. In order to demonstrate the process of using SSH to connect to our server, we're going to wire up a getter in our Joystick app. In Joystick, getters are a way to quickly define REST API routes that respond to HTTP GET requests. Getters are flexible because they can be called directly as plain HTTP endpoints, or, via the get() function built into the @joystick.js/ui and @joystick.js/node packages.

From the root of the app, we want to open up the /api/index.js file that was generated for us when we ran joystick create app earlier. This file is known as the "schema" for our API in Joystick. Inside, you will see a plain JavaScript object being exported with two properties pre-defined on it: getters and setters.

In a Joystick app, getters contains the definitions for the getter endpoints you want defined in your app (again, these are HTTP GET endpoints) and setters contains the definitions for the setter endpoints you want defined in your app (these are HTTP POST endpoints). The former is intended to "get" or read data in your app while the latter is intended to create, update, and delete data in your app.

In this file, we're going to define a getter called serverFileTree. The goal of this getter will be to SSH into our server and run the Linux ls -al command which lists out all of the files in the root directory (more on this in a bit) of the machine we're SSH'ing into. If we get a list back, we can confirm we've successfully made a connection.

/api/index.js

import joystick from '@joystick.js/node';
import { NodeSSH } from 'node-ssh';

export default {
  getters: {
    serverFileTree: {
      get: async () => {
        const ssh = new NodeSSH();

        await ssh.connect({
          host: joystick?.settings?.private?.ssh?.ipAddress,
          username: 'root',
          privateKey: `${process.cwd()}/private/ssh/ryan.glover@cheatcode.co`,
          passphrase: joystick?.settings?.private?.ssh?.passphrase,
        });

        const result = await ssh.execCommand(`ls -al`, { cwd: '/', options: { pty: true } });

        return result?.stdout;
      },
    },
  },
  setters: {},
};

Because we don't need much code, we've output the full implementation here. Starting at the top, we want to import two things:

  1. joystick from the @joystick.js/node package which we'll use to access our application's settings.
  2. { NodeSSH } from node-ssh which will help us establish an authenticated SSH connection to our server and execute commands on it.

Down in our existing getters object, we've added a property serverFileTree which is the name of our getter and to it, we've assigned an object which will define that getter. On that object, we've added a single property get which is assigned to a function.

That function get() is what's called automatically by Joystick whenever a request is made to the serverFileTree getter. Like we explained above, this can be done via the get() function in @joystick.js/ui and @joystick.js/node like get('serverFileTree'), or, directly via an HTTP request like http://localhost:2600/api/_getters/serverFileTree (the /api/_getters/<getter-name> part in that URL is automatically generated for us by Joystick).

Inside of that function, our goal is to "get" some data and return it. That data can come from anywhere. In this case, we want to SSH into the server we set up earlier, execute a command on it, and then return the output from executing that command from our getter.

To do that, first, we need to create an instance of NodeSSH with new NodeSSH(). This gives us a fresh "workspace" (so to speak) for connecting to our server and running our commands on it. Here, we take that instance and store it in a variable ssh.

Next, in front of the function passed to our get property, we've added the keyword async to allow us to use the short-hand await syntax when working with JavaScript Promises. We're doing this here because we expect the methods from the node-ssh package to return JavaScript Promises.

Our first—and most important step—is to establish a connection back to our server. To do it we call to await ssh.connect() passing an options object with:

  • host which is the IP address of the server we want to connect to.
  • username which is the username on the server we're connecting to that we want to use (in this case, we're using the root user provided by Ubuntu—the operating system we told Digital Ocean to install on our server).
  • privateKey which is the path to the private key file we generated earlier (remember, we gave the public key part of this to Digital Ocean earlier). Here, the process.cwd() is retrieving the Node.js "current working directory" path which we expect to be the full path to the app folder we created with joystick create app. We concatenate this together with /private/ssh/<your-email-address> to point to our SSH private key.
  • passphrase the password that you entered when generating your SSH key.

Calling out the elephant in the room, we have two lines here that likely don't make sense: joystick?.settings?.private?.ssh?.ipAddress and joystick?.settings?.private?.ssh?.passphrase. Here, we're pulling values from our settings file which we haven't discussed yet.

/settings.development.json

{
  "config": {
    "databases": [
      {
        "provider": "mongodb",
        "users": true,
        "options": {}
      }
    ],
    "i18n": {
      "defaultLanguage": "en-US"
    },
    "middleware": {},
    "email": {
      "from": "",
      "smtp": {
        "host": "",
        "port": 587,
        "username": "",
        "password": ""
      }
    }
  },
  "global": {},
  "public": {},
  "private": {
    "ssh": {
      "ipAddress": "<ip address goes here>",
      "passphrase": "<ssh key password goes here>"
    }
  }
}

If we open up that file, at the bottom under the private object, we want to add another object ssh and on that object, define two properties set to strings: ipAddress and passphrase. As noted here, we'll populate these with the IP address (denoted in the Digital Ocean dashboard as ipv4: 167.99.145.55 near the top of your Droplet's summary page) of our server and the password you entered when generating your SSH key.

/api/index.js

import joystick from '@joystick.js/node';
import { NodeSSH } from 'node-ssh';

export default {
  getters: {
    serverFileTree: {
      get: async () => {
        const ssh = new NodeSSH();

        await ssh.connect({
          host: joystick?.settings?.private?.ssh?.ipAddress,
          username: 'root',
          privateKey: `${process.cwd()}/private/ssh/ryan.glover@cheatcode.co`,
          passphrase: joystick?.settings?.private?.ssh?.passphrase,
        });

        const result = await ssh.execCommand(`ls -al`, { cwd: '/', options: { pty: true } });

        return result?.stdout;
      },
    },
  },
  setters: {},
};

Once your settings are updated and saved, finally, we're ready to run commands on our server. To do it, we just need to call await ssh.execCommand(). To that function, as a string for the first argument, we pass the command we want to run and then as the second argument, an options object for the request. Here, we're setting two: cwd to / (which is saying "when you execute this command, execute it from the absolute root of the server") and pty: true which tells node-ssh to allow for text input/output and is required for certain commands to work using this process.

With that, we store our call in a variable const result which we expect to contain an object with a stdout (standard output) and stderr (standard error) property, both of which are strings of output from running the command on the server.

Finally, because we can trust the command we're running should function without error, from our getter we return result?.stdout. With this, we should have a working SSH connection back to our server. If we open up a web browser and visit http://localhost:2600/api/_getters/serverFileTree after a short delay we should see the output of the command returned to the browser.

Wrapping Up

In this tutorial, we learned how to create an SSH key pair, set up a server on Digital Ocean, and connect to that server using SSH. We learned how to create a getter endpoint in a Joystick app and how to use the node-ssh package from that getter to run commands on the remote server and return its output as the response of the endpoint.

Written By
Ryan Glover

Ryan Glover

CEO/CTO @ CheatCode