How to authenticate on an sFTP server using a custom SSH key?

I’m using sftp_ex to connect to an sFTP server, authenticating with my SSH key, and it works out of the box - provided the key is at .ssh/id_rsa and that .ssh/id_rsa.pub has been set up on the remote server.

But, I don’t know how to specify a different SSH key. For example if my key is at .ssh/custom_key how can I use this one?

Also, in my app I’m going to have the private key as an ENV variable, how can I use that instead of having the file at .ssh?

1 Like

The first thing that strikes my eye is that the documentation for SftpEx.connect says

Other available options can be found at Erlang -- ssh

If you look at that documentation then you can pass the type client_options(). client_options() in turn includes the type common_option() and common_option() include key_cb_common_option()

That is the “key callback” options and seems to let you specify a module that follows the ssh_client_key_api. As near as I can tell that means you have to create a module that has a method called user_key that accepts an algorithm specification (like RSA, DSA, etc) and returns the private key that corresponds to that.

You could create such a module. In the implementation of user_key you could take the value of the key in the environment variable and put it into the format recognized by Erlang’s :public_key module. In a recent case I had the key in pem format and I had to combine :public_key.pem_decode and :public_key.pem_entry_decode to extract a key from pem data. user_key would return that key.

Then you pass the name of that module to the SftpEx.connect function as [key_cb: YourModule]

Now… having said all that, I just got that by reading the docs… I haven’t actually tried it.

1 Like

Thanks for your help @easco - for the record we found an easier solution, by using the user_dir option to specify an alternative directory where the id_rsa private key is stored in the server where we connect from.

We also found that when connecting it tried to write the known_hosts file in that directory, so you can either give this directory write permissions, or pre-populate it with this file so it doesn’t need to be updated.

1 Like

For note, if you don’t want it to generate a known_hosts file then if you pass in the hosts public keys in the proper option (silently_accept_hosts or something like that) then it will only use those, but it will not accept any others either, which can be quite good for security.

2 Likes

In case anyone comes across this in the future - I spent a few minutes to figure this out, @easco’s solution works great - here’s how it looks:

Otherwise - followed his idea pretty closely. For brevity, didn’t post include error handling, but this is what it would look like: https://github.com/i22-digitalagentur/sftp_client/blob/215ff5bcaeae6ebdd77c04d619c33a20c55eb12c/lib/sftp_client/key_provider.ex

Hi Jaime! By any chance, did you ultimately end up using an ENV variable successfully for this?

I would be potentially interested, to e.g. deal with read-only GitHub deploy keys.

Thanks!

Hey Thibaut!

No, we finally chose the user_dir option instead, see my reply on Feb '19 above :slight_smile:

1 Like

Just to fill in a few more blanks here. If you use @jrissler 's gist you need to tie it into the rest of your code.

{:ok, conn} =
      SftpEx.connect(
        host: @host,
        user: @user,
        key_cb: App.KeyCb
    )

Also, bonus pro tip on SftpEx and the underlying Erlang code. You need to first open the connection which will create the file on the SFTP server and give you back a handle which you can then use as your file reference after that. Then you can stream, upload, download, etc. The docs for this lib are wrong as per my usage.

{:ok, handle} <- SftpEx.open(conn, "/" <> upload_destination)
stream = File.stream!("filename.txt")
    |> Stream.into(SftpEx.stream!(connection, handle))
    |> Stream.run