Runs Termite terminal applications over SSH.
Add Termite.SSH to your application's supervision tree. Each accepted SSH
shell starts one temporary child under an internal DynamicSupervisor. The
configured entrypoint receives the connection's Termite.SSH.Session in its
startup options.
Example
children = [
{Termite.SSH,
name: MyApp.SSH,
port: 2222,
auth:
{:public_key,
[{System.fetch_env!("SSH_USER"), System.fetch_env!("SSH_AUTHORIZED_KEYS_DIR")}]},
system_dir: Application.app_dir(:my_app, "priv/ssh"),
entrypoint: {MyApp.TerminalSession, []}}
]
Supervisor.start_link(children, strategy: :one_for_one)The entrypoint must provide start_link/1 or child_spec/1. The session is
inserted into its keyword options under :session, replacing any existing
value. The authenticated SSH username is available as session.username:
def start_link(opts) do
session = Keyword.fetch!(opts, :session)
Task.start_link(fn ->
terminal = Termite.SSH.terminal(session)
run_terminal_application(terminal)
Termite.SSH.disconnect(session)
end)
endOptions
The following options are required:
:auth- authentication configuration. Use{:public_key, users_or_verifier}or{:password, verifier}outside local development. See "Authentication" below.:noneand plaintext credential lists are intended for local development only and requireallow_insecure_auth: true.:system_dir- directory containing at least onessh_host_*_keyhost key. Runmix termite.ssh.gen_host_keyto create a development key.:entrypoint-{module, keyword_options}used to start each session.
Optional settings are:
:port- listening port. Defaults to2222.:ip- listening address. Defaults to the loopback address{127, 0, 0, 1}.:allow_insecure_auth- permits:noneor a plaintext credential list when set totrue. Defaults tofalse. Using it logs a warning.:name- name for the SSH server process.:session_supervisor_name- explicit name for the internal session supervisor. For example, when:nameisMyApp.SSH, this defaults toMyApp.SSH.SessionSupervisor.:max_sessions- maximum simultaneous SSH connections. Defaults to100.:max_channels- maximum active channels per connection. Defaults to1.:terminal_attach_timeout- milliseconds allowed for the entrypoint to attach its terminal after the shell request, or:infinity. Defaults to5_000.:hello_timeout- milliseconds allowed for the client's first SSH message. Defaults to10_000.:negotiation_timeout- milliseconds allowed for key exchange and authentication. Defaults to30_000.:max_initial_idle_time- milliseconds an authenticated connection may wait before opening its first channel. Defaults to10_000.:idle_time- milliseconds a connection may remain open without any channels. Defaults to60_000.:max_reader_queue_length- maximum number of messages allowed in the terminal reader's mailbox before an input-flooding client is disconnected. Defaults to64.:max_terminal_widthand:max_terminal_height- upper bounds for client-supplied terminal dimensions. Defaults to500by200.
Authentication
Public-key authentication associates each SSH username with a directory
containing an OpenSSH-compatible authorized_keys or authorized_keys2
file:
auth: {:public_key, [{"alice", "/srv/my_app/ssh/alice"}]}A password verifier is an arity-four function:
auth:
{:password,
fn username, password, peer, state ->
MyApp.Accounts.verify_ssh_password(username, password, peer, state)
end}The verifier receives binary username and password values, the peer address,
and per-connection state. It must return the same values accepted by OTP's
pwdfun/4: a boolean, :disconnect, or {boolean, new_state}. Implement
password verification in constant time and apply rate limiting across
connections.
Public keys can instead be authorized dynamically with an arity-two verifier:
auth:
{:public_key,
fn username, public_key ->
MyApp.Accounts.authorized_ssh_key?(username, public_key)
end}It receives a binary username and OTP's decoded public-key value on every authentication check and must return a boolean. Keep the lookup bounded and free of side effects because SSH clients may present the same key more than once. Exceptions, throws, and exits deny authentication.
Security
auth: :none accepts every client without authentication, while a plaintext
credential list keeps passwords directly in application configuration.
Either form requires allow_insecure_auth: true. The daemon configures no
SSH subsystems or TCP forwarding, rejects every command execution request,
and only exposes the Termite CLI channel.
Summary
Types
SSH authentication configuration.
Option accepted by start_link/1.
Password verifier called by the SSH authentication layer.
Result returned by a password verifier.
Public-key verifier called by the SSH authentication layer.
Functions
Returns a specification to start this module under a supervisor.
Requests that the SSH channel for session be closed.
Starts an SSH server linked to the caller.
Starts and attaches a Termite terminal for session.
Types
@type auth_option() :: :none | [{String.t(), String.t()}] | {:password, password_auth_fun()} | {:public_key, public_key_auth_fun() | [{String.t(), String.t()}]}
SSH authentication configuration.
@type option() :: {:port, pos_integer()} | {:ip, :inet.ip_address()} | {:allow_insecure_auth, boolean()} | {:auth, auth_option()} | {:system_dir, String.t()} | {:entrypoint, {module(), keyword()}} | {:name, GenServer.name()} | {:session_supervisor_name, GenServer.name()} | {:max_sessions, pos_integer()} | {:max_channels, pos_integer()} | {:terminal_attach_timeout, timeout()} | {:hello_timeout, timeout()} | {:negotiation_timeout, timeout()} | {:max_initial_idle_time, timeout()} | {:idle_time, timeout()} | {:max_reader_queue_length, pos_integer()} | {:max_terminal_width, pos_integer()} | {:max_terminal_height, pos_integer()}
Option accepted by start_link/1.
@type password_auth_fun() :: (String.t(), String.t(), {:inet.ip_address(), :inet.port_number()}, term() -> password_auth_result())
Password verifier called by the SSH authentication layer.
Result returned by a password verifier.
@type public_key_auth_fun() :: (String.t(), :public_key.public_key() -> boolean())
Public-key verifier called by the SSH authentication layer.
Functions
Returns a specification to start this module under a supervisor.
See Supervisor.
@spec disconnect(Termite.SSH.Session.t()) :: term()
Requests that the SSH channel for session be closed.
Session entrypoints should call this after their terminal application exits
normally. Client disconnects are delivered to the terminal reader as
{:signal, :hup}.
@spec start_link([option()]) :: GenServer.on_start()
Starts an SSH server linked to the caller.
This function is normally invoked by a supervisor. See the module documentation for required options and defaults.
@spec terminal(Termite.SSH.Session.t()) :: %Termite.Terminal{ adapter: term(), reader: term(), size: term() }
Starts and attaches a Termite terminal for session.
Call this from the session entrypoint process. Input, output, resize signals, and disconnect signals are routed through the session's SSH channel.