View Source NervesSSH (nerves_ssh v1.0.0)

This library wraps Erlang/OTP's SSH daemon to make it easier to use reliably with Nerves devices.

Most importantly, it makes it possible to segment failures in other OTP applications from terminating the daemon and it recovers from rare scenarios where the daemon terminates without automatically restarting.

It can be started automatically as an OTP application or hooked into a supervision tree of your creation. Most Nerves users start it automatically as an OTP application. This is easy, but may be limiting and it requires that you use the application environment. See the following sections for options:

Starting as an OTP application

If you're using :nerves_pack v0.4.0 or later, you don't need to do anything except verify the :nerves_ssh configuration in your config.exs (see below). If you are not using :nerves_pack, add :nerves_ssh to your mix dependency list:

def deps do
  [
    {:nerves_ssh, "~> 0.1.0", targets: @all_targets}
  ]
end

And then include it in :shoehorn's :init list:

config :shoehorn,
  init: [:nerves_runtime, :vintage_net, :nerves_ssh]

:nerves_ssh will work if you do not add it to the :init list. However, if your main OTP application stops, OTP may stop :nerves_ssh, and that would make your device inaccessible via SSH.

Starting as part of one of your supervision trees

If you want to do this, make sure that you do NOT specify :nerves_ssh in your config.exs. The :nerves_ssh key decides whether or not to automatically launch based on this.

Then when specifying the children for your supervisor, add NervesSSH like this:

    {NervesSSH, nerves_ssh_options}

The nerves_ssh_options should be a NervesSSH.Options struct. See the Configuration section option fields that you may specify. Calling NervesSSH.Options.with_defaults(my_options_list) to build the nerves_ssh_options value is one way of getting reasonable defaults.

Configuration

NervesSSH supports the following configuration items:

  • :authorized_keys - a list of SSH authorized key file string
  • :user_passwords - a list of username/password tuples (stored in the clear!)
  • :port - the TCP port to use for the SSH daemon. Defaults to 22.
  • :subsystems - a list of SSH subsystems specs to start. Defaults to SFTP and ssh_subsystem_fwup
  • :system_dir - where to find host keys. Defaults to "/data/nerves_ssh"
  • :shell - the language of the shell (:elixir, :erlang, :lfe, or :disabled). Defaults to :elixir.
  • :exec - the language to use for commands sent over ssh (:elixir, :erlang, lfe, or :disabled). Defaults to :elixir.
  • :iex_opts - additional options to use when starting up IEx
  • :daemon_option_overrides - additional options to pass to :ssh.daemon/2. These take precedence and are unchecked. Be careful using this since it can break other options.

SSH host keys

SSH identifies itself to clients using a host key. Clients can record the key and use it to detect man-in-the-middle attacks and other shenanigans on future connections. Host keys are stored in the :system_dir (see configuration) and named ssh_host_rsa_key, ssh_host_ed25519_key, etc.

NervesSSH will create a host key the first time it starts if one does not exist. The key will be stored in :system_dir. Be aware that the host key is not encrypted or protected so anyone with access to the device can get it if they choose.

If the :system_dir is not writable, NervesSSH will create an in-memory host key so that users can still log in. In fact, even if the file system is writable, NervesSSH will verify the host key before using it and recreate it if corrupt. The goal is that broken host keys to not result in a situation where it's impossible to log into a device. Your SSH client complaining about the host key changing will be the hint that something is wrong.

NervesSSH currently supports Ed25519 and RSA host keys.

If you rewrite your MicroSD cards often and don't want to get SSH client errors, add the following to your ~/.ssh/config:

Host nerves.local
    UserKnownHostsFile /dev/null
    StrictHostKeyChecking no

Authentication

It's possible to set up a number of authentication strategies with the Erlang SSH daemon. Currently, only simple public key and username/password authentication setups are supported by :nerves_ssh. Both of them work fine for getting started. As needs become more sophisticated, you can pass options to :daemon_option_overrides.

Public key authentication

Public ssh keys can be specified so that matching clients can connect. These come from files like your ~/.ssh/id_rsa.pub or ~/.ssh/id_ecdsa.pub that were created when you created your ssh keys. If you haven't done this, the following article may be helpful. Here's an example that uses the application config:

config :nerves_ssh,
  authorized_keys: [
    "ssh-rsa
AAAAB3NzaC1yc2EAAAADAQABAAAAgQDBCdMwNo0xOE86il0DB2Tq4RCv07XvnV7W1uQBlOOE0ZZVjxmTIOiu8XcSLy0mHj11qX5pQH3Th6Jmyqdj",
    "ssh-rsa
AAAAB3NzaC1yc2EAAAADAQABAAACAQCaf37TM8GfNKcoDjoewa6021zln4GvmOiXqW6SRpF61uNWZXurPte1u8frrJX1P/hGxCL7YN3cV6eZqRiF"
  ]

Here's another way that may work well for you that avoids needing to commit your keys:

config :nerves_ssh,
  authorized_keys: [
    File.read!(Path.join(System.user_home!, ".ssh/id_rsa.pub"))
  ]

See NervesSSH.add_authorized_key/1 and NervesSSH.remove_authorized_key/1 for managing public keys at runtime.

Username/password authentication

The SSH console uses public key authentication by default, but it can be configured for usernames and passwords via the :user_passwords key. This has the form [{"username", "password"}, ...]. Keep in mind that passwords are stored in the clear. This is not recommended for most situations.

config :nerves_ssh,
  user_passwords: [
    {"username", "password"}
  ]

You can use NervesSSH.add_user/2 and NervesSSH.remove_user/1 for managing credentials at runtime, but they are not saved to disk so restarting NervesSSH will cause them to be lost (such as a reboot or daemon crash)

Upgrade from NervesFirmwareSSH

If you are migrating from :nerves_firmware_ssh, or updating to :nerves_pack >= 0.4.0, you will need to make a few changes to your existing project.

  1. Generate a upload.sh script by running mix firmware.gen.script (if you don't already have one)

    • This is necessary because you will no longer have access to your old mix upload command because nerves_firmware_ssh is being removed from the project.
  2. Change all :nerves_firmware_ssh config values to :nerves_ssh. A command like this would probably do the trick:

     grep -RIl nerves_firmware_ssh config/ | xargs sed -i 's/nerves_firmware_ssh/nerves_ssh/g'
    
  3. Compile your new firmware that includes :nerves_ssh (or updated :nerves_pack)

    • NOTE Compiling your new firmware for the first time will generate a warning about the old upload.sh script still being around. You can ignore that this one time because you will need it for uploading to an existing device still using port 8989.
  4. Upload your new firmware with :nerves_ssh using the old upload.sh script (or whatever other method you have been using for OTA firmware updates)

  5. After the new firmware with :nerves_ssh is on the device, then you'll need to generate the new upload.sh script with mix firmware.gen.script, or see SSHSubsystemFwup for other supported options

Goals

  • [X] Support public key authentication
  • [X] Support username/password authentication
  • [X] Device generated server certificate and key
  • [ ] Device generated username/password

Summary

Functions

Add an SSH public key to the authorized keys

Add a user credential to the SSH daemon

Returns a specification to start this module under a supervisor.

Read the configuration options

Return information on the running ssh daemon.

Remove an SSH public key from the authorized keys

Remove a user credential from the SSH daemon

Functions

Link to this function

add_authorized_key(name \\ NervesSSH, key)

View Source
@spec add_authorized_key(GenServer.name(), String.t()) :: :ok

Add an SSH public key to the authorized keys

This also persists the key to {USER_DIR}/authorized_keys so that it can be used after restarting.

Call configuration/0 to get the current list of authorized keys.

Example:

iex> NervesSSH.add_authorized_key("ssh-ed25519 AAAAC3NzaC...")
Link to this function

add_user(name \\ NervesSSH, user, password)

View Source
@spec add_user(GenServer.name(), String.t(), String.t() | nil) :: :ok

Add a user credential to the SSH daemon

Setting password to "" or nil will effectively be passwordless authentication for this user

Returns a specification to start this module under a supervisor.

See Supervisor.

Link to this function

configuration(name \\ NervesSSH)

View Source
@spec configuration(GenServer.name()) :: NervesSSH.Options.t()

Read the configuration options

@spec info(GenServer.name()) :: {:ok, keyword()} | {:error, :bad_daemon_ref}

Return information on the running ssh daemon.

See ssh.daemon_info/1.

Link to this function

remove_authorized_key(name \\ NervesSSH, key)

View Source
@spec remove_authorized_key(GenServer.name(), String.t()) :: :ok

Remove an SSH public key from the authorized keys

This looks for an exact match. Call configuration/0 to get the list of authorized keys to find those to remove. The {USER_DIR}/authorized_keys will be updated to save the change.

Link to this function

remove_user(name \\ NervesSSH, user)

View Source
@spec remove_user(GenServer.name(), String.t()) :: :ok

Remove a user credential from the SSH daemon