How to use SSHEx to connect using private key from Phoenix Liveview

I have an Elixir Phoenix Live View instance up and running.

I am using the following code to SSH into a server from my Live View, and it works.

SSHEx.connect ip: 'whatever.com', user: 'me', password: 'password'

My problem is that I want to log into a server that does not have a password and requires a private key.
I want to use SSHEx because it returns a pid that I can pass around and (hopefully) perform file manipulation etc. If there is a way to easily do ssh and do file manipulation with a different library I am open to all suggestions.

I have the private key and I can log in using System.cmd.

System.cmd("ssh", ["-i", "~/.ssh/id_rsa_nop", "root@whatever", "ls"])

This may log me in, but I am unsure how to use this to edit files, download files etc.

node-ssh is really easy for me to understand and I haven’t found an equivalent in Elixir.

node-ssh - npm (npmjs.com)

Is this discussion in the issue helpful Unable to use Custom RSA SSH keys - `:public_key.pem_entry_decode` raises with Case clause error Ā· Issue #22 Ā· rubencaro/sshex Ā· GitHub

1 Like

Would it be possible for you to try SSHKit rather than SSHEx?
It is more recently updated, and I believe the usage is somewhat straight forward. I don’t do anything special to make use of private keys.

An example ā€˜from the tin’:

[:ok] =
  "example.io"
  |> SSHKit.context()
  |> SSHKit.download("remote.txt", as: "local.txt")

On my mac, I had to use user_interaction: true to be able to send commands/download.

iex(10)> SSHKit.context("localhost", user_interaction: true) |> SSHKit.download("test1.exs", as: "fetched.exs")

09:18:45.485 [debug] client will use strict KEX ordering
[:ok]

Creating a context allows you to do things with that context. Normal unix pipelines, or multiple commands against the same context:

iex(11)> ctx = SSHKit.context("localhost", user_interaction: true)
%SSHKit.Context{
  hosts: [%SSHKit.Host{name: "localhost", options: [user_interaction: true]}],
  env: nil,
  path: nil,
  umask: nil,
  user: nil,
  group: nil
}

iex(12)> SSHKit.download(ctx, "test1.exs")

09:20:30.745 [debug] client will use strict KEX ordering
[:ok]
i
iex(13)> SSHKit.run(ctx, "ps -ef | head -1")

09:21:07.302 [debug] client will use strict KEX ordering
[{:ok, [stdout: "  UID   PID  PPID   C STIME   TTY           TIME CMD\n"], 0}]

I used ctx for multiple remote actions, and was able to use normal linux pipes within the run context.

I hope this helps.

2 Likes

That library seems to combine SSH and SFTP handling - Erlang splits them into separate modules ssh and ssh-sftp. SSHEx only wraps the former.

One library that wraps the other half is SFTPClient:

https://hexdocs.pm/sftp_client/readme.html

I found it when looking for other Elixir projects that had SSH-key-related issues, and it had one that sounds similar to your issue:

2 Likes

I’m use SSH — librarian v0.2.0 (also erlang :ssh wrapper) for my project, it has custom private key handling.

To be clear, I’m asking if anyone knows working syntax to authenticate with SSH and specify the directory of the private key.
I’ve mulled over libraries and documentation and I can’t figure it out. Supposedly all these libraries do it but none of them are explicit as the node.js library I mentioned.

I assume it would be something simple like:
Fake example:
SSHLib host: whatever.com private_key_dir: .ssh/whatever

Beloq is a working sytnax example for how to do it with the System.cmd:

System.cmd("ssh", ["-i", "~/.ssh/id_rsa_nop", "root@whatever", "ls"])

I don’t see a direct working example of this sort in any of the SSH documentation, just confusing mentions that it is possible.

Here is example using SSH — librarian v0.2.0

      SSH.connect(
        "server.host",
        user: "server.user",
        port: "server.port",
        identity: "identity path eg: ~/.ssh/id_rsa_nop",
        save_accepted_host: false,
        silently_accept_hosts: true,
        user_interaction: false
      )
2 Likes

@sucipto Thanks, this is all I was looking for. Perfect!

1 Like

Glad you found your solution.

I think the key is, SSHKit and others automatically discover it, as it should be in a ā€˜well known’ location. This is why there’s very little documetation on finding/setting it.
I see that, if you needed to specify a location other than ~/.ssh, there is at least this module:

Documentation is there on how to set up the location , which can then be fed to SSHKit.

The solution you marked as good for you is a simpler approach, but librarian did not work in my use case. I hope it fits your needs.

I’ll try SSHKit again later and look at the lib you linked to.
Thank you

key = File.open!("path/to/keyfile.pem")
known_hosts = File.open!("path/to/known_hosts")
cb = SSHClientKeyAPI.with_options(
  identity: key,
  known_hosts: known_hosts,
  silently_accept_hosts: true
)

Very interesting topic. I was looking for a ssh package just recently, but haven’t found what I was looking for. Does anyone know if there is a ssh package which supports multiplexing of an existing connection?

Interesting question.
I focused on connecting to dozens of machines in parallel. Hadn’t looked into multiplexing into the same machine.

The base Erlang ssh facilities handle that, but I haven’t found how to do it with SSHKit.
My question would be, same application, or separate applications or instances?

The erlang :ssh facilties, within the same process allow for:

:ssh.start()
{:ok, connection_ref} = :ssh.connect("localhost", 22, [])
1..5 |> Enum.each(fn _ ->
  {:ok, channel} = :ssh_connection.session_channel(connection_ref, :infinity)
  :ssh_connection.exec(connection_ref, channel, "echo 'ok' ; sleep 10; date", :infinity)
end )
# wait 10 seconds then
flush()

That will establish five channels to the same connection reference and make five separate calls (could do this asnyc as well). If you look at netstat for tcp with port 22, you’ll see that only one connection was established.

It would be possible, if this were a networked solution, to register that connection_ref globally for the hostname, so that it could be used by any process that needs to make a ssh connection to the registered host. If set up supervised, then it would be possible for the connection to be re-established if subsequently disconnecetd.

Not a package, but possibly an approach for you, depending on your needs.

1 Like

@ sucipto

If I want to use this library to SSH into a machine that I do have the password for, how would I do it?

I would expect I could do something like:

      {:ok, conn} = SSH.connect(
        "whatever.com",  
        user: "wktdev",
        password: "1234",

      )

This does obviously not work. As always, docs make it more complicated than it needs to be for simple things.

Any idea?
Thank you.

In my case, i need to accept host (same as when connect to new host using ssh client cli, they ask add host to known_hosts)

SSH.connect!("test.laptop.local", username: "username", password: "passs", silently_accept_hosts: true)

The complete docs is on erlang doc: ssh — ssh v5.2

1 Like

Thanks for this. I have another question. I don’t understand how you derived that answer from that portion of the documentation. If I knew how to read the docs it would save me a lot of time but they look so cryptic, I just end up asking questions.

How exactly did you derive that answer from that portion of the docs?
Do you already know Erlang?

I know this sounds like a stupid question but I’ve never had this problem with Node/JS.

Thank you.

Replied on your another topic: https://elixirforum.com/t/how-to-read-documentation/64082/7

It take a few days for me to understand this too, and a saw your post about same problem, so I just reply from what I know.

Nope, I came from Node/JS too,

I tried using that code to log in and it throws an error.

The error is not because of a bad connection on my part, when I use SSHEx it works as expected.

This works and connects:

{:ok, sshex_conn} = SSHEx.connect ip: 'whatever.com', user: 'root', password: '1234'

This throws a connection error:

{:ok, ssh_pid} = SSH.connect!("whatever.com", username: "root", password: "1234", silently_accept_hosts: true)

Any ideas?
Thanks again.

It’s seems librarian doesn’t accept password options, you can use my fork here as deps:

{:librarian,
       git: "https://github.com/sukacipta/librarian.git",
       ref: "master"}

I’ve update to allow password options. here updated example:

conn =
  SSH.connect!("localhost",
    user: "sucipto",
    password: ~c"yourpassword",
    silently_accept_hosts: true,
  )

Note: my previous example is wrong to use username options, it should be user. And when use password option, should wrap with ~c"" sigil

Thank you

1 Like