asyncssh
asyncssh copied to clipboard
Connection failure if the remote requires Two-Factor Authentication (2FA)
Hi @ronf, thanks for this amazing project! I get Permission denied when trying to use AsyncSSH to connect to a remote which requires Two-Factor Authentication (2FA).
Test Code
import logging
import asyncssh
import asyncio as aio
async def ssh_test():
async with asyncssh.connect("c1f81187-cc18-451e-a611-34b82c4cd429", known_hosts=None):
pass
logging.basicConfig()
asyncssh.set_log_level('DEBUG')
asyncssh.set_debug_level(2)
if __name__ == "__main__":
print(aio.run(asyncssh.get_server_auth_methods("c1f81187-cc18-451e-a611-34b82c4cd429")))
aio.run(ssh_test())
Log
DEBUG:asyncssh:Reading config from "/private/home/xinyuanz/.ssh/config"
DEBUG:asyncssh:Reading config from "/private/home/xinyuanz/.ssh/fair_ssh/includes"
DEBUG:asyncssh:Reading config from "/private/home/xinyuanz/.ssh/fair_ssh/xinyuanz/config"
INFO:asyncssh:Fetching server auth methods from nlb-34b82c4cd429-8c30cef286658237.elb.us-east-1.amazonaws.com, port 22
INFO:asyncssh:[conn=0] Connected to SSH server at nlb-34b82c4cd429-8c30cef286658237.elb.us-east-1.amazonaws.com, port 22
INFO:asyncssh:[conn=0] Local address: 100.96.183.110, port 36196
INFO:asyncssh:[conn=0] Peer address: 34.224.194.26, port 22
DEBUG:asyncssh:[conn=0] Sending version SSH-2.0-AsyncSSH_2.12.0
DEBUG:asyncssh:[conn=0] Received version SSH-2.0-OpenSSH_8.2p1 Ubuntu-4ubuntu0.5
DEBUG:asyncssh:[conn=0] Requesting key exchange
DEBUG:asyncssh:[conn=0] Key exchange algs: curve25519-sha256,[email protected],curve448-sha512,ecdh-sha2-nistp521,ecdh-sha2-nistp384,ecdh-sha2-nistp256,ecdh-sha2-1.3.132.0.10,diffie-hellman-group-exchange-sha256,diffie-hellman-group14-sha256,diffie-hellman-group15-sha512,diffie-hellman-group16-sha512,diffie-hellman-group17-sha512,diffie-hellman-group18-sha512,[email protected],diffie-hellman-group14-sha1,rsa2048-sha256,ext-info-c
DEBUG:asyncssh:[conn=0] Host key algs: [email protected],[email protected],[email protected],[email protected],[email protected],[email protected],[email protected],[email protected],[email protected],[email protected],[email protected],[email protected],[email protected],ssh-ed25519,ssh-ed448,ecdsa-sha2-nistp521,ecdsa-sha2-nistp384,ecdsa-sha2-nistp256,ecdsa-sha2-1.3.132.0.10,rsa-sha2-256,rsa-sha2-512,[email protected],[email protected],[email protected],[email protected],ssh-rsa
DEBUG:asyncssh:[conn=0] Encryption algs: [email protected],[email protected],[email protected],aes256-ctr,aes192-ctr,aes128-ctr
DEBUG:asyncssh:[conn=0] MAC algs: [email protected],[email protected],[email protected],[email protected],[email protected],[email protected],[email protected],hmac-sha2-256,hmac-sha2-512,hmac-sha1,[email protected],[email protected],[email protected],[email protected],[email protected]
DEBUG:asyncssh:[conn=0] Compression algs: [email protected],none
DEBUG:asyncssh:[conn=0] Received key exchange request
DEBUG:asyncssh:[conn=0] Key exchange algs: curve25519-sha256,[email protected],ecdh-sha2-nistp256,ecdh-sha2-nistp384,ecdh-sha2-nistp521,diffie-hellman-group-exchange-sha256,diffie-hellman-group16-sha512,diffie-hellman-group18-sha512,diffie-hellman-group14-sha256
DEBUG:asyncssh:[conn=0] Host key algs: rsa-sha2-512,rsa-sha2-256,ssh-rsa,ecdsa-sha2-nistp256,ssh-ed25519
DEBUG:asyncssh:[conn=0] Client to server:
DEBUG:asyncssh:[conn=0] Encryption algs: aes128-cbc,aes192-cbc,aes256-cbc,aes128-ctr,aes192-ctr,aes256-ctr,[email protected],[email protected]
DEBUG:asyncssh:[conn=0] MAC algs: [email protected],[email protected],hmac-sha2-512,hmac-sha2-256
DEBUG:asyncssh:[conn=0] Compression algs: none,[email protected]
DEBUG:asyncssh:[conn=0] Server to client:
DEBUG:asyncssh:[conn=0] Encryption algs: aes128-cbc,aes192-cbc,aes256-cbc,aes128-ctr,aes192-ctr,aes256-ctr,[email protected],[email protected]
DEBUG:asyncssh:[conn=0] MAC algs: [email protected],[email protected],hmac-sha2-512,hmac-sha2-256
DEBUG:asyncssh:[conn=0] Compression algs: none,[email protected]
DEBUG:asyncssh:[conn=0] Beginning key exchange
DEBUG:asyncssh:[conn=0] Key exchange alg: curve25519-sha256
DEBUG:asyncssh:[conn=0] Client to server:
DEBUG:asyncssh:[conn=0] Encryption alg: [email protected]
DEBUG:asyncssh:[conn=0] MAC alg: [email protected]
DEBUG:asyncssh:[conn=0] Compression alg: [email protected]
DEBUG:asyncssh:[conn=0] Server to client:
DEBUG:asyncssh:[conn=0] Encryption alg: [email protected]
DEBUG:asyncssh:[conn=0] MAC alg: [email protected]
DEBUG:asyncssh:[conn=0] Compression alg: [email protected]
DEBUG:asyncssh:[conn=0] Requesting service ssh-userauth
DEBUG:asyncssh:[conn=0] Completed key exchange
DEBUG:asyncssh:[conn=0] Received extension info
DEBUG:asyncssh:[conn=0] server-sig-algs: ssh-ed25519,[email protected],ssh-rsa,rsa-sha2-256,rsa-sha2-512,ssh-dss,ecdsa-sha2-nistp256,ecdsa-sha2-nistp384,ecdsa-sha2-nistp521,[email protected]
DEBUG:asyncssh:[conn=0] Request for service ssh-userauth accepted
INFO:asyncssh:[conn=0] Beginning auth for user xinyuanz
DEBUG:asyncssh:[conn=0] Remaining auth methods: publickey
INFO:asyncssh:[conn=0] Aborting connection
INFO:asyncssh:[conn=0] Connection closed
['publickey']
DEBUG:asyncssh:Reading config from "/private/home/xinyuanz/.ssh/config"
DEBUG:asyncssh:Reading config from "/private/home/xinyuanz/.ssh/fair_ssh/includes"
DEBUG:asyncssh:Reading config from "/private/home/xinyuanz/.ssh/fair_ssh/xinyuanz/config"
INFO:asyncssh:Opening SSH connection to nlb-34b82c4cd429-8c30cef286658237.elb.us-east-1.amazonaws.com, port 22
INFO:asyncssh:[conn=1] Connected to SSH server at nlb-34b82c4cd429-8c30cef286658237.elb.us-east-1.amazonaws.com, port 22
INFO:asyncssh:[conn=1] Local address: 100.96.183.110, port 36198
INFO:asyncssh:[conn=1] Peer address: 34.224.194.26, port 22
DEBUG:asyncssh:[conn=1] Sending version SSH-2.0-AsyncSSH_2.12.0
DEBUG:asyncssh:[conn=1] Received version SSH-2.0-OpenSSH_8.2p1 Ubuntu-4ubuntu0.5
DEBUG:asyncssh:[conn=1] Requesting key exchange
DEBUG:asyncssh:[conn=1] Key exchange algs: curve25519-sha256,[email protected],curve448-sha512,ecdh-sha2-nistp521,ecdh-sha2-nistp384,ecdh-sha2-nistp256,ecdh-sha2-1.3.132.0.10,diffie-hellman-group-exchange-sha256,diffie-hellman-group14-sha256,diffie-hellman-group15-sha512,diffie-hellman-group16-sha512,diffie-hellman-group17-sha512,diffie-hellman-group18-sha512,[email protected],diffie-hellman-group14-sha1,rsa2048-sha256,ext-info-c
DEBUG:asyncssh:[conn=1] Host key algs: [email protected],[email protected],[email protected],[email protected],[email protected],[email protected],[email protected],[email protected],[email protected],[email protected],[email protected],[email protected],[email protected],ssh-ed25519,ssh-ed448,ecdsa-sha2-nistp521,ecdsa-sha2-nistp384,ecdsa-sha2-nistp256,ecdsa-sha2-1.3.132.0.10,rsa-sha2-256,rsa-sha2-512,[email protected],[email protected],[email protected],[email protected],ssh-rsa
DEBUG:asyncssh:[conn=1] Encryption algs: [email protected],[email protected],[email protected],aes256-ctr,aes192-ctr,aes128-ctr
DEBUG:asyncssh:[conn=1] MAC algs: [email protected],[email protected],[email protected],[email protected],[email protected],[email protected],[email protected],hmac-sha2-256,hmac-sha2-512,hmac-sha1,[email protected],[email protected],[email protected],[email protected],[email protected]
DEBUG:asyncssh:[conn=1] Compression algs: [email protected],none
DEBUG:asyncssh:[conn=1] Received key exchange request
DEBUG:asyncssh:[conn=1] Key exchange algs: curve25519-sha256,[email protected],ecdh-sha2-nistp256,ecdh-sha2-nistp384,ecdh-sha2-nistp521,diffie-hellman-group-exchange-sha256,diffie-hellman-group16-sha512,diffie-hellman-group18-sha512,diffie-hellman-group14-sha256
DEBUG:asyncssh:[conn=1] Host key algs: rsa-sha2-512,rsa-sha2-256,ssh-rsa,ecdsa-sha2-nistp256,ssh-ed25519
DEBUG:asyncssh:[conn=1] Client to server:
DEBUG:asyncssh:[conn=1] Encryption algs: aes128-cbc,aes192-cbc,aes256-cbc,aes128-ctr,aes192-ctr,aes256-ctr,[email protected],[email protected]
DEBUG:asyncssh:[conn=1] MAC algs: [email protected],[email protected],hmac-sha2-512,hmac-sha2-256
DEBUG:asyncssh:[conn=1] Compression algs: none,[email protected]
DEBUG:asyncssh:[conn=1] Server to client:
DEBUG:asyncssh:[conn=1] Encryption algs: aes128-cbc,aes192-cbc,aes256-cbc,aes128-ctr,aes192-ctr,aes256-ctr,[email protected],[email protected]
DEBUG:asyncssh:[conn=1] MAC algs: [email protected],[email protected],hmac-sha2-512,hmac-sha2-256
DEBUG:asyncssh:[conn=1] Compression algs: none,[email protected]
DEBUG:asyncssh:[conn=1] Beginning key exchange
DEBUG:asyncssh:[conn=1] Key exchange alg: curve25519-sha256
DEBUG:asyncssh:[conn=1] Client to server:
DEBUG:asyncssh:[conn=1] Encryption alg: [email protected]
DEBUG:asyncssh:[conn=1] MAC alg: [email protected]
DEBUG:asyncssh:[conn=1] Compression alg: [email protected]
DEBUG:asyncssh:[conn=1] Server to client:
DEBUG:asyncssh:[conn=1] Encryption alg: [email protected]
DEBUG:asyncssh:[conn=1] MAC alg: [email protected]
DEBUG:asyncssh:[conn=1] Compression alg: [email protected]
DEBUG:asyncssh:[conn=1] Requesting service ssh-userauth
DEBUG:asyncssh:[conn=1] Completed key exchange
DEBUG:asyncssh:[conn=1] Received extension info
DEBUG:asyncssh:[conn=1] server-sig-algs: ssh-ed25519,[email protected],ssh-rsa,rsa-sha2-256,rsa-sha2-512,ssh-dss,ecdsa-sha2-nistp256,ecdsa-sha2-nistp384,ecdsa-sha2-nistp521,[email protected]
DEBUG:asyncssh:[conn=1] Request for service ssh-userauth accepted
INFO:asyncssh:[conn=1] Beginning auth for user xinyuanz
DEBUG:asyncssh:[conn=1] Remaining auth methods: publickey
DEBUG:asyncssh:[conn=1] Preferred auth methods: gssapi-keyex,gssapi-with-mic,hostbased,publickey,keyboard-interactive,password
DEBUG:asyncssh:[conn=1] Trying public key auth with rsa-sha2-256 key
DEBUG:asyncssh:[conn=1] Signing request with rsa-sha2-256 key
DEBUG:asyncssh:[conn=1] Remaining auth methods: keyboard-interactive
DEBUG:asyncssh:[conn=1] Preferred auth methods: gssapi-keyex,gssapi-with-mic,hostbased,publickey,keyboard-interactive,password
INFO:asyncssh:[conn=1] Auth failed for user xinyuanz
INFO:asyncssh:[conn=1] Connection failure: Permission denied
INFO:asyncssh:[conn=1] Aborting connection
Traceback (most recent call last):
File "/private/home/xinyuanz/OneDevEx/test1.py", line 15, in <module>
aio.run(ssh_test())
File "/private/home/xinyuanz/miniconda3/envs/my-dev-env/lib/python3.10/asyncio/runners.py", line 44, in run
return loop.run_until_complete(main)
File "/private/home/xinyuanz/miniconda3/envs/my-dev-env/lib/python3.10/asyncio/base_events.py", line 646, in run_until_complete
return future.result()
File "/private/home/xinyuanz/OneDevEx/test1.py", line 6, in ssh_test
async with asyncssh.connect("c1f81187-cc18-451e-a611-34b82c4cd429", known_hosts=None):
File "/private/home/xinyuanz/miniconda3/envs/my-dev-env/lib/python3.10/site-packages/asyncssh/misc.py", line 274, in __aenter__
self._coro_result = await self._coro
File "/private/home/xinyuanz/miniconda3/envs/my-dev-env/lib/python3.10/site-packages/asyncssh/connection.py", line 7834, in connect
return await asyncio.wait_for(
File "/private/home/xinyuanz/miniconda3/envs/my-dev-env/lib/python3.10/asyncio/tasks.py", line 408, in wait_for
return await fut
File "/private/home/xinyuanz/miniconda3/envs/my-dev-env/lib/python3.10/site-packages/asyncssh/connection.py", line 447, in _connect
await options.waiter
asyncssh.misc.PermissionDenied: Permission denied
I expected a prompt asking for the authentication token (just like using the command line tool), but got denied directly. Please help! Thanks in advance!
AsyncSSH isn't intended to be an interactive SSH client, so it's never going to prompt you for anything. In particular, if you want to use password or keyboard-interactive auth, you will need to prompt the user yourself for the password and 2FA token. You can do this by implementing the kbdint_auth_requested() and kbdint_challenge_received() methods in a subclass of SSHClient. Here's more info on these methods:
def kbdint_auth_requested(self) -> MaybeAwait[Optional[str]]:
"""Keyboard-interactive authentication has been requested
This method should return a string containing a comma-separated
list of submethods that the server should use for
keyboard-interactive authentication. An empty string can be
returned to let the server pick the type of keyboard-interactive
authentication to perform. If keyboard-interactive authentication
is not supported, `None` should be returned.
By default, keyboard-interactive authentication is supported
if a password was provided when the :class:`SSHClient` was
created and it hasn't been sent yet. If the challenge is not
a password challenge, this authentication will fail. This
method and the :meth:`kbdint_challenge_received` method can be
overridden if other forms of challenge should be supported.
If blocking operations need to be performed to determine the
submethods to request, this method may be defined as a
coroutine.
:returns: A string containing the submethods the server should
use for authentication or `None` to move on to
another authentication method
"""
def kbdint_challenge_received(self, name: str, instructions: str,
lang: str, prompts: KbdIntPrompts) -> \
MaybeAwait[Optional[KbdIntResponse]]:
"""A keyboard-interactive auth challenge has been received
This method is called when the server sends a keyboard-interactive
authentication challenge.
The return value should be a list of strings of the same length
as the number of prompts provided if the challenge can be
answered, or `None` to indicate that some other form of
authentication should be attempted.
If blocking operations need to be performed to determine the
responses to authenticate with, this method may be defined
as a coroutine.
By default, this method will look for a challenge consisting
of a single 'Password:' prompt, and call the method
:meth:`password_auth_requested` to provide the response.
It will also ignore challenges with no prompts (generally used
to provide instructions). Any other form of challenge will
cause this method to return `None` to move on to another
authentication method.
:param name:
The name of the challenge
:param instructions:
Instructions to the user about how to respond to the challenge
:param lang:
The language the challenge is in
:param prompts:
The challenges the user should respond to and whether or
not the responses should be echoed when they are entered
:type name: `str`
:type instructions: `str`
:type lang: `str`
:type prompts: `list` of tuples of `str` and `bool`
:returns: List of string responses to the challenge or `None`
to move on to another authentication method
"""
You'll probably want kbdint_auth_requested() to return an empty string, to let the server pick the specific type of keyboard-interactive auth to use. Then, when the server challenges you, your kbdint_challenge_received() method should be called with the name, instructions, lang, and prompts arguments shown here. You'd then prompt the user using the values in the prompts argument and return a list of strings matching the length of prompts with your answers.
For your 2FA case, I'm not sure if you'll get a single challenge with two prompts or of it will do that as two different rounds of authentication. I don't have an SSH setup here that does 2FA auth.
Also, keep in mind that you'll want to keep your user prompting as async-friendly, if you don't want the whole event loop to stop running when prompting the user. That means if you want to use something like getpass(), you'll probably need to run that in an executor. See the ainput() example at https://gist.github.com/delivrance/675a4295ce7dc70f0ce0b164fcdbd798?permalink_comment_id=3590322#gistcomment-3590322 for an example of what this might look like.
Thank you so much for detailed explanation @ronf . It works!
In my use case, we would like to ssh to the remote multiple times, so we have to pass 2FA auth each time (very annoying and not compatible with pytest). Wonder if AsyncSSH supports or plans to support ControlMaster? Or is there an alternative way which can solve the problem?
Glad to hear it!
Regarding ControlMaster, AsyncSSH already has the ability to open multiple parallel sessions on a single connection, which is basically the same thing ControlMaster does. The only difference is that you must have all of the sessions opened from the same asyncio event loop (which generally means all from the same process & thread).
To take advantage of this, you just need to open a connection with something like asyncssh.connect() and then use the conn value you get back multiple times. You can open a mixture of interactive shells, single-command exec sessions, and subsystem requests (like sftp). You can also make multiple port forwarding requests over this same connection, authenticating only once for all of these requests.
Most SSH servers have a relatively small upper bound on the max number of simultaneous sessions (typically, something like 10), but that can potentially be adjusted on the server side if that's not enough (and ControlMaster would have the same issue).
Appreciate the explanation and solution! I have been using AsyncSSH in the ways you mentioned so far. However, in this particular use case, I cannot keep the connection alive because the project I am working on is a CLI tool and it can only obtain the connection when it is called. If I set ControlMaster, the background SSH process will jump in whenever I SSH to the host in the command line. So I do think it will be great if AsyncSSH can support ControlMaster or something similar.
OpenSSH is a standalone executable, so it has total control over whether it exits or stays running in the background after the session it was started for closes. This isn't the case with AsyncSSH -- it is a library, and it relies on the application using it to provide the Python interpreter and asyncio event loop. If that application code decides to exit, AsyncSSH really has no say in the matter, and all of the connections it was managing will end up getting closed.
It might be possible to do something using atexit(), but I don't think that would allow AsyncSSH to prevent the process from exiting. At best, it would have to do something like fork() (which is only available on UNIX systems) and preserve all the file descriptors associated with AsyncSSH while closing everything else. That's still dangerous, though, as there's no way to know what other resources the application may have created that aren't cleaned up prior to the fork. In fact, the application may never attempt to clean up these resources, counting on exiting the process to do so.
I'm just not seeing a good way to work around these issues. Even I were to create a standalone agent which AsyncSSH had total control over for this purpose, there would be the issue of copying the connection state from the application process to this agent process. Since AsyncSSH is a library, that has other challenges, like the fact that the application may have subclassed some of the AsyncSSH classes, and there'd be no way to get that custom code running over in the other interpreter. There'd be a lot of other state which would have to be copied over as well for the agent to be able to continue to read/write data on the open connections. AsyncSSH has no mechanism for anything like that, and doesn't need any such code today, as connections are never expected to used outside of a single Python interpreter and asyncio event loop.