Brian Hicks
Brian Hicks

Reputation: 6403

Multi-factor authentication (password and key) with Paramiko

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?

Upvotes: 5

Views: 7731

Answers (5)

tanutapi
tanutapi

Reputation: 1113

Here is an example code that authenticates with "password" first and then "publickey"

import threading
import paramiko

# Define server credentials
hostname = '127.0.0.1'
port = 22
username = 'username'
password = 'password'
private_key_path = '/path/to/id_rsa'

try:
    # Initialize transport
    transport = paramiko.Transport((hostname, port), disabled_algorithms={'pubkeys': ['rsa-sha2-256', 'rsa-sha2-512']})
    transport.connect()

    # First authenticate with username and password
    transport.auth_password(username=username, password=password)
    print("Password authentication successful.")

    # Next authenticate with the ssh public key
    # Load the private key
    private_key = paramiko.RSAKey.from_private_key_file(private_key_path)
    publickey_auth_event = threading.Event()
    publickey_auth_handler = paramiko.auth_handler.AuthHandler(transport)
    transport.auth_handler = publickey_auth_handler
    transport.lock.acquire()
    publickey_auth_handler.auth_event = publickey_auth_event
    publickey_auth_handler.auth_method = "publickey"
    publickey_auth_handler.username = username
    publickey_auth_handler.private_key = private_key
    userauth_message = paramiko.message.Message()
    userauth_message.add_string('ssh-userauth')
    userauth_message.rewind()
    publickey_auth_handler._parse_service_accept(userauth_message)
    transport.lock.release()
    publickey_auth_handler.wait_for_response(publickey_auth_event)
    print("Public key authentication successful.")

    # Open an SFTP session
    sftp = paramiko.SFTPClient.from_transport(transport)
    print("SFTP session established.")

    # List files in the remote directory
    print("Remote files:", sftp.listdir())

    # Close the connection
    sftp.close()
    transport.close()

except Exception as e:
    print(f"Authentication failed: {e}")

Upvotes: 0

Rafael Zayas
Rafael Zayas

Reputation: 2451

I was able to use this solution on paramiko's github found from a still-open issue (at the time of writing this). I'll include the original solution from that github issue, and the slight variant i use in my own code.

From jacky15 in that github issue:

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:

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)    

Upvotes: 0

Martin Prikryl
Martin Prikryl

Reputation: 202078

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.

Upvotes: -2

osekmedia
osekmedia

Reputation: 683

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.

Upvotes: 18

Brandon
Brandon

Reputation: 21

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.

Upvotes: 2

Related Questions