4

I've got the following code:

import paramiko
policy = paramiko.client.WarningPolicy()
client = paramiko.client.SSHClient()
client.set_missing_host_key_policy(policy)
username = '...'
password = '...'
file_path = '...'
pkey = paramiko.RSAKey.from_private_key_file(file_path)
client.connect('...', username=username, password=password, pkey=key)
sftp = client.open_sftp() 

From the docs, it seems like it should work. Everything works successfully, but when the code hits client.open_sftp it bombs with a SSHException: Unable to open channel. and the transport (from client.get_transport) is active but not authenticated. I'm also having trouble enabling debug logging for this (I'm trying logging.getLogger('paramiko').setLevel(logging.DEBUG) without success.)

Any ideas on where I can start to debug this very vague error message?

Martin Prikryl
  • 188,800
  • 56
  • 490
  • 992
Brian Hicks
  • 6,213
  • 8
  • 51
  • 77

4 Answers4

13

Sorry for the late response but this problem was really hard to find any information on so i wanted to post a solution for anyone else stuck on this issue.

After pulling my hair out trying to solve this I found a solution thanks to some code posted by Doug Ellwanger and Daniel Brownridge. The problem seems to be caused by the way the multi-factor authentication is handled using more of an interactive style.

import paramiko
import threading

... 

username = '...'
password = '...'
file_path = '...'
pkey = paramiko.RSAKey.from_private_key_file(file_path)
sftpClient = multifactor_auth('...', 22, username, pkey, password)

...

def multifactor_auth_sftp_client(host, port, username, key, password):
    #Create an SSH transport configured to the host
    transport = paramiko.Transport((host, port))
    #Negotiate an SSH2 session
    transport.connect()
    #Attempt authenticating using a private key
    transport.auth_publickey(username, key)
    #Create an event for password auth
    password_auth_event = threading.Event()
    #Create password auth handler from transport
    password_auth_handler = paramiko.auth_handler.AuthHandler(transport)
    #Set transport auth_handler to password handler
    transport.auth_handler = password_auth_handler
    #Aquire lock on transport
    transport.lock.acquire()
    #Register the password auth event with handler
    password_auth_handler.auth_event = password_auth_event
    #Set the auth handler method to 'password'
    password_auth_handler.auth_method = 'password'
    #Set auth handler username
    password_auth_handler.username = username
    #Set auth handler password
    password_auth_handler.password = password
    #Create an SSH user auth message
    userauth_message = paramiko.message.Message()
    userauth_message.add_string('ssh-userauth')
    userauth_message.rewind()
    #Make the password auth attempt
    password_auth_handler._parse_service_accept(userauth_message)
    #Release lock on transport
    transport.lock.release()
    #Wait for password auth response
    password_auth_handler.wait_for_response(password_auth_event)
    #Create an open SFTP client channel
    return transport.open_sftp_client()

I hope this helps, it worked for my project.

osekmedia
  • 633
  • 7
  • 14
  • 1
    Paramiko can usually handle correctly on its own (at least in most cases). No need for such a complicated solution. For some example or a rare cases when Paramiko fails, see [Paramiko/Python: Keyboard interactive authentication](https://stackoverflow.com/q/55498370/850848) - though even then there are simpler solutions then the low-level code in this answer. – Martin Prikryl Jun 27 '19 at 07:53
  • 2
    Thanks, it works for me. To make your answer complete, also add "import threading" to the code. – KP87 Jan 26 '21 at 15:11
  • 1
    Thanks!! I tried several ways but your answer is the only that works! – k.ronnakrit Aug 09 '21 at 16:00
  • To complement my comment above, I have added [an answer here](https://stackoverflow.com/q/28837089/850848#68949359) with more details. – Martin Prikryl Sep 07 '21 at 11:00
  • This is the only answer that works for me too. – herophuong Sep 30 '21 at 01:06
  • Would you please explain *exactly* what kind of multi-factor authentication this code implements? For example, does the server generate some kind of challenge for every new connection that you have to process and answer? – chrisinmtown Jul 11 '23 at 14:14
2

In your script you declare

pkey = paramiko.RSAKey.from_private_key_file(file_path)

and then instead of pkey, you have pkey = key.

Not sure what key is coming from but that might be an issue.

eyllanesc
  • 235,170
  • 19
  • 170
  • 241
Brandon
  • 21
  • 2
0

I was able to use this solution found from (at the time of writing this) still open issue on paramiko's github. I'm wishing nothing but the best for jacky15. I'll include their original answer and the slight variant i use in my own code.

From jacky15:

import paramiko
paramiko.util.log_to_file('/path/to/log')
hostname = 'server.name'
port = 12345
username = 'username'
password = 'password' 
pkey = paramiko.RSAKey.from_private_key_file('/path/to/key')

transport = paramiko.Transport((hostname, port))
transport.connect()

# auth the public key as usual, auth service now is activated on server 
transport.auth_publickey(username=username, key=pkey)

# try to send another userauth request without request auth service
m = paramiko.Message()
m.add_byte(paramiko.common.cMSG_USERAUTH_REQUEST)
m.add_string(username)
m.add_string('ssh-connection')
m.add_string('password')
m.add_boolean(False)
py3_password = paramiko.py3compat.bytestring(password)
m.add_string(py3_password)
transport._send_message(m)

# now it works! : )
sftp_client = paramiko.SFTPClient.from_transport(transport)

from my code (you can figure out the attributes I have):

transport = paramiko.Transport((self.hostname, self.port))
transport.start_client(event=None, timeout=30)
transport.get_remote_server_key()
my_key = paramiko.RSAKey.from_private_key_file(self.hostkey)
transport.auth_publickey(self.username, my_key)
m = paramiko.Message()
m.add_byte(paramiko.common.cMSG_USERAUTH_REQUEST)
m.add_string(self.username)
m.add_string('ssh-connection')
m.add_string('password')
m.add_boolean(False)
py3_password = paramiko.py3compat.bytestring(self.password)
m.add_string(py3_password)
transport._send_message(m)
self.connection = paramiko.SFTPClient.from_transport(transport)    
Rafael Zayas
  • 2,061
  • 1
  • 18
  • 20
-2

The Paramiko high-level API, SSHClient can handle common two-factor authentication on its own. For example for key and password authentication, use:

ssh = paramiko.SSHClient()
ssh.connect(
    "example.com", username="username", password="password",
    key_filename="/path/to/key")

So the complicated code in the answer by @osekmedia is usually not needed.

I know of only two scenarios, where it can "help":

  1. SSHClient by default verifies the host key. You may mistake failure to verify the host key with failure of the two-factor authentication. They are not related. It's just that the low-level Transport API, that the @osekmedia's code uses, does not verify the host key, what avoids your actual problem. But that's a security flaw. For a correct solution, see Paramiko "Unknown Server".

  2. You might think that you are using password authentication, while you actually use keyboard-interactive authentication. Normally Paramiko can handle keyboard-interactive authentication, even if you mistakenly ask for password authentication. But with some obscure servers, this does not work, see Password authentication in Python Paramiko fails, but same credentials work in SSH/SFTP client. In such case, the following code should do:

    username = "username"
    
    transport = paramiko.Transport('example.com') 
    transport.connect(username=username)
    
    key = paramiko.RSAKey.from_private_key_file("/path/to/key")
    transport.auth_publickey(username, key)
    
    def handler(title, instructions, fields):
        if len(fields) > 1:
            raise SSHException("Expecting one field only.")
        return ["password"]
    
    transport.auth_interactive(username, handler)
    

    Note that the above code uses Transport, so it by default bypasses host key verification. Use hostkey argument of the Transport.connect to correct that.

Martin Prikryl
  • 188,800
  • 56
  • 490
  • 992