Skip to content

Instantly share code, notes, and snippets.

@ckoch-cars
Last active February 9, 2025 02:06
Show Gist options
  • Save ckoch-cars/15a88fcb0bc2f4abbed09b49879edf31 to your computer and use it in GitHub Desktop.
Save ckoch-cars/15a88fcb0bc2f4abbed09b49879edf31 to your computer and use it in GitHub Desktop.
Erlang ssh connect with SSH key
# Specifically we are trying to do 2 things with the erlang `:ssh` module:
# Make a connection (use :ssh.connect/4`) and pass in the ssh key from something other than a file on disk
# And then with an open connection, bind the connection to a local port (i.e. setup a ssh tunnel)
# use :ssh.tcpip_tunnel_to_server/5 or :ssh.tcpip_tunnel_to_server/6
# Example (working) code:
defmodule SSHTunnel do
@local_port 9999
@remote_port 1234
require Logger
@behaviour :ssh_client_key_api
def start(remote_host, user) do
:ssh.start()
ssh_key = System.get_env("TUNNEL_SSH_KEY")
case :ssh.connect(String.to_charlist(remote_host), 22,
silently_accept_hosts: true,
user_interaction: false,
auth_methods: ~c"publickey",
user: String.to_charlist(user),
key_cb: {__MODULE__, user_key: ssh_key}
) do
{:ok, conn_pid} ->
Logger.info("Successfully connected to #{remote_host}")
%{conn_pid: conn_pid}
{:error, reason} ->
Logger.error("Failed to connect: #{inspect(reason)}")
{:error, reason}
end
end
def stop(%{conn_pid: conn_pid}) do
:ssh.close(conn_pid)
Logger.info("SSH tunnel to #{@remote_port} closed")
:ssh.stop()
end
@doc """
Given a ref/pid to an open SSH connection; configure a tunnel to bind a local port #{@local_port}
to the port #{@remote_port} on the remote host.
"""
def setup_port_forwarding(%{conn_pid: conn_pid}) do
case :ssh.tcpip_tunnel_to_server(conn_pid, :loopback, @local_port, :loopback, @remote_port) do
{:ok, @local_port} ->
Logger.info(
"Port forwarding from local port #{@local_port} to remote port #{@remote_port}"
)
error ->
Logger.error(inspect(error))
end
end
@doc "this is a required but indirectly used callback"
@impl :ssh_client_key_api
def add_host_key(_hostnames, _key, _connect_opts) do
:ok
end
@doc "this is a required but indirectly used callback"
@impl :ssh_client_key_api
def is_host_key(_key, _host, _algorithm, _connect_opts) do
true
end
@doc "Read an SSH key from something other than a file on disk i.e. an env var or app config"
@impl :ssh_client_key_api
def user_key(_algorithm, opts) do
data = opts[:key_cb_private][:user_key]
case :ssh_file.decode(data, :public_key) do
[{key, _comments} | _rest] ->
{:ok, key}
{:error, :key_decode_failed} ->
Logger.error("key_decode_failed")
{:error, :key_decode_failed}
other ->
Logger.error("Unexpected return value from :ssh_file.decode/2 #{inspect(other)}")
{:error, :ssh_client_key_api_unable_to_decode_key}
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment