使用 Paramiko 进行多因素身份验证(密码和密钥)

Posted

技术标签:

【中文标题】使用 Paramiko 进行多因素身份验证(密码和密钥)【英文标题】:Multi-factor authentication (password and key) with Paramiko 【发布时间】:2021-09-30 19:12:22 【问题描述】:

我有以下代码:

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() 

从文档看来,它应该可以工作。一切正常,但是当代码到达client.open_sftp 时,它会使用SSHException: Unable to open channel. 进行轰炸,并且传输(来自client.get_transport)处于活动状态但未经过身份验证。我也无法为此启用调试日志记录(我正在尝试logging.getLogger('paramiko').setLevel(logging.DEBUG),但没有成功。)

关于我可以从哪里开始调试这个非常模糊的错误消息有什么想法吗?

【问题讨论】:

【参考方案1】:

Paramiko 高级 API,SSHClient 可以自行处理常见的两因素身份验证。例如对于密钥和密码认证,使用:

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

所以通常不需要@osekmedia 回答中的复杂代码。

我只知道两种情况,它可以“帮助”:

    SSHClient 默认验证主机密钥。您可能会将验证主机密钥失败与两因素身份验证失败误认为是失败。它们不相关。只是@osekmedia 的代码使用的低级Transport API 不会验证主机密钥,从而避免了您的实际问题。但这是一个安全漏洞。如需正确解决方案,请参阅Paramiko "Unknown Server"。

    您可能认为您使用的是密码身份验证,而实际上您使用的是键盘交互身份验证。通常 Paramiko 可以处理键盘交互身份验证,即使您错误地要求密码身份验证。但是对于一些不起眼的服务器,这不起作用,请参阅Paramiko/Python: Keyboard interactive authentication。在这种情况下,下面的代码应该这样做:

    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)
    

    请注意,上面的代码使用Transport,所以它默认绕过主机密钥验证。使用Transport.connecthostkey 参数来纠正这个问题。

【讨论】:

【参考方案2】:

很抱歉回复晚了,但是这个问题真的很难找到任何信息,所以我想为其他陷入这个问题的人发布一个解决方案。

在尝试解决这个问题后,我找到了一个解决方案,这要感谢 Doug Ellwanger 和 Daniel Brownridge 发布的一些代码。问题似乎是由使用更多交互方式处理多因素身份验证的方式引起的。

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()

我希望这会有所帮助,它适用于我的项目。

【讨论】:

Paramiko 通常可以自行正确处理(至少在大多数情况下)。不需要这么复杂的解决方案。对于 Paramiko 失败的一些示例或罕见情况,请参阅Paramiko/Python: Keyboard interactive authentication - 尽管即使这样,也有比此答案中的低级代码更简单的解决方案。 谢谢,它对我有用。为了使您的答案完整,还要在代码中添加“导入线程”。 谢谢!!我尝试了几种方法,但你的答案是唯一有效的! 为了补充我上面的评论,我添加了an answer here 以提供更多详细信息。 这也是唯一对我有用的答案。【参考方案3】:

在你的脚本中声明

pkey = paramiko.RSAKey.from_private_key_file(file_path)

然后你有pkey = key而不是pkey。

不确定来自哪个密钥,但这可能是个问题。

【讨论】:

以上是关于使用 Paramiko 进行多因素身份验证(密码和密钥)的主要内容,如果未能解决你的问题,请参考以下文章

Shiro,多因素身份验证

使用 Spring Boot 2 和 Spring Security 5 进行多因素身份验证

双因素身份认证

重置密码且未配置两因素设备时如何获得两因素身份验证

如何使用 Python 的 Paramiko 模块 ssh 进入需要两次密码验证的服务器?

ASP.Net Identity 2 使用 SMS 密码登录 - 不使用双因素身份验证