"How do I use paramiko to SSH to a remote host while proxying through a jump host? Also, my jump host requires two-factor authentication!"
This seems to be a surprisingly common problem with a lot of not-very-working solutions. I figured I'd share my attempt with the world.
A simple class, SSHJumpClient
, which derives from paramiko.SSHClient
and implements two additional features:
- Easy chaining of SSH connections, supported through object injection. This enables the programmer to build a 'stack' of proxied SSH sessions, and tunnel commands through infrastructure as-needed.
- Easy authentication scheme override, forcing a keyboard-interactive authentication approach to be used. This should support most 2FA / MFA infrastructure approaches to SSH authentication. The keyboard-interactive authentication handler is injected, permitting easy integration with more advanced use cases.
In this example, we use keyboard-interactive authentication on the Jump Host, and we tell Paramiko to 'auto add' (and accept) unknown Host Keys. (What could possibly go wrong?)
import paramiko from paramiko_jump import SSHJumpClient, simple_auth_handler # My Jump Host requires keyboard-interactive multi-factor # authentication, so I use auth_handler=. Otherwise, I could # use paramiko.SSHClient here. with SSHJumpClient(auth_handler=simple_auth_handler) as jumper: jumper.set_missing_host_key_policy(paramiko.AutoAddPolicy()) jumper.connect( hostname='jump-host', username='jump-user', ) # Now I instantiate a session for the Jump Host <-> Target # Host connection, and inject the jump_session to use for # proxying. target = SSHJumpClient(jump_session=jumper) target.set_missing_host_key_policy(paramiko.AutoAddPolicy()) target.connect( hostname='target-host', username='target-user', password='target-password', look_for_keys=False, allow_agent=False, ) _, stdout, _ = target.exec_command('sh ip int br') print(stdout.read().decode()) target.close()
from getpass import getpass import paramiko from paramiko_jump import SSHJumpClient, simple_auth_handler with SSHJumpClient(auth_handler=simple_auth_handler) as jumper: jumper.connect( hostname='jump-host', username='jump-user', ) target1 = SSHJumpClient(jump_session=jumper) target1.connect( hostname='target-host1', username='username', password='password', look_for_keys=False, allow_agent=False, ) _, stdout, _ = target1.exec_command('sh ver') print(stdout.read().decode()) target1.close() target2 = SSHJumpClient(jump_session=jumper) target2.connect( hostname='target-host2', username='username', password='password', look_for_keys=False, allow_agent=False, ) _, stdout, _ = target2.exec_command('sh ip int br') print(stdout.read().decode()) target2.close()
circuit = [] hop1 = SSHJumpClient() hop1.connect('host') circuit.append(hop1) hop2 = SSHJumpClient(jump_session=hop1) hop2.connect('host') circuit.append(hop2) hop3 = SSHJumpClient(jump_session=hop2) hop3.connect('host') circuit.append(hop3) hop4 = SSHJumpClient(jump_session=hop3) hop4.connect('host') circuit.append(hop4) target = SSHJumpClient(jump_session=hop4) target.connect('host') circuit.append(target) target.exec_command('uptime') for session in reversed(circuit): session.close()
In order to successfully authenticate with infrastructure requiring keyboard-interactive multi-factor authentication, you will probably want to explicitly pass in auth_handler= during client construction. A basic handler callable is included, and should work for most use cases:
from paramiko_jump import simple_auth_handler
When troubleshooting authentication failures, remember that Paramiko will be authenticating as a client on each 'hop', and that it has strong preferences over which authentication scheme it will be using. You can control authentication behavior by passing various parameters to the `connect()`
call. Read `paramiko.SSHClient._auth`
for more insight into how this works.