"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.