Erlang

Secure Shell for Your Erlang Node

2016-01-14 by Marc Sugiyama

Secure Shell for Your Erlang Node

Say you want to connect to the shell of your Erlang node. It's easy. You connect a remote shell from another Erlang node - if you're on the same subnet - and know the cookie - and have Erlang installed. OK, maybe not that easy.

How about connecting via ssh? You don't need to know the cookie, you don't need Erlang installed, and you can connect from anywhere.

Here's how you add an ssh server to your Erlang node.

Unlocking the Key

With Public Key Encryption (PKE) the cryptographic key has a private key and a public key. Messages encrypted with the public key can only be decrypted with the private key. You have to keep the private key secret but you can freely distribute the public key. It's difficult (or at least very time consuming) to guess the private key from the public key which makes this kind of encryption secure.

We'll use PKE to control access to our Erlang node. Used this way, the client copies its public key onto the server. The client holds onto the corresponding private key. When connecting, ssh uses the public key of the client and the private key from the client to authenticate the client. The server also holds onto its own private key and copies its public key into the client's known_host file. This way, the client can tell if it's talking to the right host.

ssh:daemon/[1,2,3]

Starting the Erlang ssh daemon (sshd), the process that listens for ssh client connections, is easy. The ssh:daemon/[1,2,3]starts sshd. The hard part is getting it configured properly so it accepts connections from our clients.

The key configuration parameters are:

  • Port - the listener port number
  • system_dir - directory holding the host's keys
  • user_dir - directory holding the authorized_keys file

If you're fmiliar with ssh on Unix/Linux systems, /etc/ssh is the same as system_dir and ~/.ssh on the server is the same as user_dir. For simplicity, we'll use the same directory for user_dir and system_dir.

You can also specify the functions that implement the interactive shell, but when not told otherwise, ssh:daemon/[1,2,3]uses the standard Erlang shell. That's what we'll do here.

Creating keys

For this exercise, we'll use a new public and private key rather than one you might already have in your ~/.ssh directory.

$ mkdir ssh_client
$ ssh-keygen -t rsa -f ssh_client/id_rsa
Generating public/private rsa key pair.
Enter passphrase (empty for no passphrase): 
Enter same passphrase again: 
Your identification has been saved in ssh_client/id_rsa.
Your public key has been saved in ssh_client/id_rsa.pub.
The key fingerprint is:
SHA256:FzL7BUTwozLLhNrghMt5mEHsqKi/xcmMKNVEZuuXhwI me@My-MacBook-Pro.local
The key's randomart image is:
[...]

Use a blank passphrase so you won't have to type a passphrase when you connect.

Next we need public and private keys for the server.

$ mkdir ssh_dir
$ ssh-keygen -t rsa -f ssh_dir/ssh_host_rsa_key
Generating public/private rsa key pair.
Enter passphrase (empty for no passphrase): 
Enter same passphrase again: 
Your identification has been saved in ssh_dir/ssh_host_rsa_key.
Your public key has been saved in ssh_dir/ssh_host_rsa_key.pub.
The key fingerprint is:
SHA256:NA6WEnFOLSIBBDHZGgx2zrAsVTwciA1f5pPToxmBdts me@My-MacBook-Pro.local
The key's randomart image is:
[...]

Again, use a blank passphrase.

Finally we need a authorized_keys file. Users who want to connect to our server put their client's public key in the authorized_keys file. For the first key, you can just copy the public key you made in ssh_client into the authorized_keys file.

% cp ssh_client/id_rsa.pub ssh_dir/authorized_keys

Testing our setup

To test out our setup, we can start the ssh daemon from the Erlang shell:

$ erl
Erlang/OTP 17 [erts-6.4] [source] [64-bit] [smp:8:8] [async-threads:10] [hipe] [kernel-poll:false]
Eshell V6.4  (abort with ^G)
1> ssh:start().
ok
2> ssh:daemon(11111, [{system_dir, "ssh_dir"}, {user_dir, "ssh_dir"}]).
{ok,<0.44.0>}
3> 

11111 is the port number. We'll use the same directory, ssh_dir for system_dir and user_dir.

Then from another terminal window, try connecting to the server using an ssh client:

$ mkdir .ssh_erlang
$ ssh 127.0.0.1 -p 11111 -i ssh_client/id_rsa -o UserKnownHostsFile=.ssh_erlang/known_hosts
The authenticity of host '[127.0.0.1]:11111 ([127.0.0.1]:11111)' can't be established.
RSA key fingerprint is SHA256:NA6WEnFOLSIBBDHZGgx2zrAsVTwciA1f5pPToxmBdts.
Are you sure you want to continue connecting (yes/no)? yes
Warning: Permanently added '[127.0.0.1]:11111' (RSA) to the list of known hosts.
Eshell V6.4  (abort with ^G)
1> i().
Pid                   Initial Call                          Heap     Reds Msgs
Registered            Current Function                     Stack              
<0.0.0>               otp_ring0:start/2                     2586     3900    0
init                  init:loop/1                              2              
<0.3.0>               erlang:apply/2                        6772   402481    0
erl_prim_loader       erl_prim_loader:loop/3                   6              
<0.6.0>               gen_event:init_it/6                   2586      527    0
[...]
2> 

Hey, it works!

Some details: Use -p to tell ssh the port where the server is listening. The -i is your private key. UserKnownHostsFile is the file where the ssh client remembers the keys of the hosts you've connected to in the past. Usually ssh reads your private key and remembers the known hosts in the ~/.ssh directory.

If you can't connect, make sure the permissions are set correctly on the private key files, directories, and authorized_keys file. Only the user should have read/write permission.

Also, be careful how you exit the ssh client. Don't do q(). as that'll stop the Erlang node. Instead you can:

2> exit().

to stop the shell, which will terminate the connection. Alternatively you can close the connection from the client using ~.(tilde dot).

Starting up

How can we start sshd in your node automatically?

  1. Add ssh as an application dependency in the .app file.
  2. Add code to the Mod:start/2 callback in your application module to start sshd.
  3. Add the system keys and authorized_keys files to your applications priv directory.

Example Mod:start/2 application callback:

start(_StartType, _StartArgs) ->
    {ok, _} = start_ssh(),
    my_sup:start_link().

start_ssh() ->
    Port = 11111,
    PrivDir = code:priv_dir(my_application),
    ssh:daemon(Port, [
        {system_dir, filename:join([PrivDir, "ssh_dir"])},
        {user_dir, filename:join([PrivDir, "ssh_dir"])}]).

Finally, put the ssh_dir in the my_application's priv directory. You might also want to parameterize the port number by adding an application environment variable.

Wrapping in an application

There's a simpler way. I've wrapped ssh inside an application you can include as a rebar dependency. Once you do, your node has an ssh shell. The repository is here: https://github.com/ivanos/erl_sshd.

enhancements

erl_sshd has basic support for adding ssh to your node. It'd be great to see some enhancements:

  • pass through more ssh:deamon/[2,3] options
  • real password management
  • ability to use an external authentication mechanism and example implementations
  • user key management

 

 

 

Go back to the blog

×

Request more information:

* Denotes required
×

Thank you for your message

We sent you a confirmation email to let you know we received it. One of our colleagues will get in touch shortly.
Have a nice day!