Manage an SSH daemon and its subsystems on Nerves devices. It has the following features:
- Automatic startup of the SSH daemon on initialization
- Ability to hook the SSH daemon into a supervision tree of your choosing
- Easy setup of SSH firmware updates for Nerves
- Easy shell and exec setup for Erlang, Elixir, and LFE
- Some protection from easy-to-make mistakes that would cause ssh to not be available
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:
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.
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.
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 to22
.:subsystems
- a list of SSH subsystems specs to start. Defaults to SFTP andssh_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 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
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 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.
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)
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.
-
Generate a
upload.sh
script by runningmix 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 becausenerves_firmware_ssh
is being removed from the project.
- This is necessary because you will no longer have access to your old
-
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'
-
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.
- NOTE Compiling your new firmware for the first time will generate a
warning about the old
-
Upload your new firmware with
:nerves_ssh
using the oldupload.sh
script (or whatever other method you have been using for OTA firmware updates) -
After the new firmware with
:nerves_ssh
is on the device, then you'll need to generate the newupload.sh
script withmix firmware.gen.script
, or see SSHSubsystemFwup for other supported options
- Support public key authentication
- Support username/password authentication
- Device generated server certificate and key
- Device generated username/password