Paramiko extension providing support for 2FA / MFA, SSH Proxying
Project description
paramiko-jump -- Multi-Factor Authentication and SSH Proxying with Paramiko
The Problem
"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.
The Solution
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, allowing for keyboard-interactive authentication as well as fully-automated authentication through 2FA / MFA infrastructure. This package includes authentication handlers which can handle either use case, and more.
Installing paramiko-jump
Paramiko-jump is available on PyPI, so you can readily install it with pip:
pip install paramiko-jump
Usage Examples
You Can Authenticate However You Want
Within this document are numerous examples of how to use paramiko-jump to authenticate to a remote host as well as how to use it to proxy SSH sessions, but not all possible permutations are documented here. It's important to know that you can use any authentication scheme demonstrated in these examples, with or without the proxying feature, as long as your SSH infrastructure supports it.
Quick Start: I need to solve MFA but I don't need SSH Proxying features
If you don't need the Jump Host features but DO need to handle multi-factor authentication, these next examples are for you.
You can use the simple_auth_handler
or MultiFactorAuthHandler
to handle your
authentication to a single host without ever proxying another SSH session through it ('jumping'.)
Just pick an authentication approach and go.
Authentication Handlers
In order to successfully authenticate with infrastructure requiring keyboard-interactive and/or multi-factor authentication, you will probably want to explicitly pass in auth_handler= during client construction. You can use the included handlers, or you can write your own.
Authentication Handler Example 1: Keyboard-Interactive (Any Authentication Scheme)
A basic handler callable is included, and should work for most keyboard-interactive use cases. In this example, we supply a username and will be prompted for any passwords/tokens that are needed in order to authenticate.
If you choose to use the simple_auth_handler
, paramiko will prompt you for any passwords or
tokens that are required for authentication, and will ignore other authentication methods (such as
SSH keys) even if you pass in authentication-related parameters. It will even ignore any
password value you pass in, because it's expecting to get it via the handler.
Keyboard-Interactive Authentication using the simple_auth_handler
from paramiko_jump import SSHJumpClient, simple_auth_handler
with SSHJumpClient(auth_handler=simple_auth_handler) as jumper:
jumper.connect(
hostname='somehost.example.com',
username='username',
look_for_keys=False,
)
stdin, stdout, stderr = jumper.exec_command('uptime')
output = stdout.readlines()
print(output)
Authentication Handler Example 2: MultiFactor Authentication using the MultiFactorAuthHandler
The MultiFactorAuthHandler
class is a more advanced handler that can be used to accomplish
complex authentication sessions with automation -- even through MFA infrastructure. This is
accomplished by seeding the handler with a sequence of responses which will be required during
the authentication session, such as a password and OTP.
It goes without saying that, you must figure out what your MFA infrastructure is expecting and provide the correct responses in the correct order. This is a powerful tool, but it can take a bit of tinkering to get it right for your environment.
In this next example, my MFA infrastucture is first going to require that I authenticate with my password, and then I have to enter '1' to instruct the infrastructure to push an authentication request to my mobile authenticator.
Multi-Factor Authentication using the MultiFactorAuthHandler
from paramiko_jump import SSHJumpClient, MultiFactorAuthHandler
handler = MultiFactorAuthHandler()
handler.add('password') # Just add strings to the handler in the order they are needed
handler.add('1')
with SSHJumpClient(auth_handler=handler) as jumper:
jumper.connect(
hostname='somehost.example.com',
username='username',
look_for_keys=False,
)
stdin, stdout, stderr = jumper.exec_command('uptime')
output = stdout.readlines()
print(output)
Simpler Authentication Schemes
In general, unless you want keyboard interactive authentication (See: simple_auth_handler
),
or you want to authenticate through 2FA/MFA infrastructure (See: MultiFactorAuthHandler
),
you probably want to authenticate to a host in one of the following ways:
Simple Authentication Example 1: Use an SSH key with ssh-agent
If you have an SSH key, you can use it to authenticate instead of using keyboard-interactive authentication.
By default, paramiko will even use your ssh-agent, if one is running.
In this example, the private key is managed by ssh-agent, but you can add a passphrase= parameter to the connect() call if you have a passphrase-protected key and aren't using ssh-agent.
SSH Key Authentication using ssh-agent
from paramiko_jump import SSHJumpClient
with SSHJumpClient() as jumper:
jumper.connect(
hostname='somehost.example.com',
username='username',
look_for_keys=True,
)
Simple Authentication Example 2: Use an SSH key with a passhphrase
In this example, we have an SSH key, and we have a passphrase to unlock it, that we've somehow fetched from a secure secrets store and exposed to Python in a secure manner.
SSH Key Authentication using a passphrase-protected key, ssh-agent ignored
from paramiko_jump import SSHJumpClient
with SSHJumpClient() as jumper:
jumper.connect(
hostname='somehost.example.com',
username='username',
passphrase='mykeypassphrase',
look_for_keys=True,
allow_agent=False,
)
Simple Authentication Example 3: Ignore SSH keys, use Username/Password authentication
In this example, we may have an SSH key configured, but in order to succesfully authenticate with the remote host, we need to use a username and password. We explicitly tell paramiko to ignore any keys it finds, because we don't want it to try to use them as the keys will fail to authenticate.
Basic (Username/Password) Authentication
from paramiko_jump import SSHJumpClient
with SSHJumpClient() as jumper:
jumper.connect(
hostname='somehost.example.com',
username='username',
password='password',
look_for_keys=False,
)
SSH Proxying / "Jump Host" Support
If you need to proxy your SSH session through a Jump Host, this is how to do it. Note that the
following examples are primarily using the simple_auth_handler
for authentication, but
you can use any authentication scheme you like for either Jump Host or Target Host, as long as
your SSH infrastructure supports it.
SSH Proxying Example 1a: Connect to a single Target Host through a Jump Host
In this example, we connect to a Jump Host, then connect to a Target Host through it.
We are using keyboard-interactive authentication on the Jump Host by way of
auth_handler=simple_auth_handler
and we are passing pre-determined username and password
credentials to the Target Host.
Keyboard-Interactive Authentication on the Jump Host, Basic Authentication on the Target Host
from paramiko_jump import SSHJumpClient, simple_auth_handler
##
# Because I'm passing in ```simple_auth_handler```, I will be prompted for any passwords or
# tokens that are required for authentication. This is how to do keyboard-interactive SSH
# authentication.
##
with SSHJumpClient(auth_handler=simple_auth_handler) as jumper:
jumper.connect(
hostname='jump-host',
username='jump-user',
look_for_keys=False,
)
##
# Now I instantiate a session for the Jump Host <-> Target
# Host connection, and inject the jump_session to use for
# proxying.
##
with SSHJumpClient(jump_session=jumper) as target:
target.connect(
hostname='target-host',
username='target-user',
password='target-password',
look_for_keys=False,
allow_agent=False,
)
stdin, stdout, stderr = target.exec_command('sh ip int br')
output = stdout.readlines()
print(output)
SSH Proxying Example 1b: Connect to a single Target Host through a Jump Host
This example is functionally equivalent to Example 1a, with two key changes:
- It doesn't use the context manager.
- It applies paramiko.AutoAddPolicy() as the missing host key policy to the Jump Host and Target Host connections -- this tells the SSH processes to 'auto accept' the host key if it's not already known (be cautious!)
Keyboard-Interactive Authentication on the Jump Host, Basic Authentication on the Target Host
import paramiko
from paramiko_jump import SSHJumpClient, simple_auth_handler
jumper = SSHJumpClient(auth_handler=simple_auth_handler)
jumper.set_missing_host_key_policy(paramiko.AutoAddPolicy())
jumper.connect(
hostname='jump-host',
username='jump-user',
look_for_keys=False,
)
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,
)
stdin, stdout, stderr = target.exec_command('sh ip int br')
output = stdout.readlines()
print(output)
target.close()
SSH Proxying Example 2: Open one jump channel, connect to multiple Target Hosts
MFA on the Jump Host, Basic Authentication on the Target Hosts
import os
from paramiko_jump import SSHJumpClient, MultiFactorAuthHandler
password = os.getenv('JUMP_HOST_PASSWORD')
handler = MultiFactorAuthHandler()
handler.add(password)
handler.add('1')
with SSHJumpClient(auth_handler=handler) as jumper:
jumper.connect(
hostname='jump-host',
username='username',
look_for_keys=False,
)
with SSHJumpClient(jump_session=jumper) as target1:
target1.connect(
hostname='target-host1',
username='username',
password='password',
look_for_keys=False,
)
_, stdout, _ = target1.exec_command('uptime')
output = stdout.readlines()
print(output)
with SSHJumpClient(jump_session=jumper) as target2:
target2.connect(
hostname='target-host2',
username='username',
password='password',
look_for_keys=False,
)
_, stdout, _ = target2.exec_command('uptime')
output = stdout.readlines()
print(output)
with SSHJumpClient(jump_session=jumper) as target3:
target2.connect(
hostname='target-host3',
username='username',
password='password',
look_for_keys=False,
)
_, stdout, _ = target2.exec_command('uptime')
output = stdout.readlines()
print(output)
SSH Proxying Example 3: Multiple-Hop SSH "Virtual Circuit"
You can build a 'virtual circuit' out of multiple SSH connections, each one proxying through the previous.
This example lacks authentication parameters, and exists to illustrate the concept.
from paramiko_jump import SSHJumpClient
circuit = []
hop1 = SSHJumpClient()
hop1.connect('host1')
circuit.append(hop1)
hop2 = SSHJumpClient(jump_session=hop1)
hop2.connect('host2')
circuit.append(hop2)
hop3 = SSHJumpClient(jump_session=hop2)
hop3.connect('host3')
circuit.append(hop3)
hop4 = SSHJumpClient(jump_session=hop3)
hop4.connect('host4')
circuit.append(hop4)
target = SSHJumpClient(jump_session=hop4)
target.connect('target')
circuit.append(target)
stdin, stdout, stderr = target.exec_command('uptime')
for session in reversed(circuit):
session.close()
Troubleshooting
Authentication Issues
-
Remember to use the Authentication Handlers (or make your own) to help manage your more complex use cases. Injecting the correct handler can make all the difference in the world.
-
If you have injected an authentication handler, Paramiko will use that to handle the authentication process, and it will take precedence over other authentication parameters you pass in.
-
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. Readparamiko.SSHClient._auth
for more insight into how this works. -
If you aren't using key-based authentication, it's safest to explicitly set
look_for_keys=False
. This will prevent Paramiko from trying to use any keys it finds, which can take it down the wrong code path.
Missing Host Key Errors
See SSH Proxying Example 1b for an example of how to use paramiko.AutoAddPolicy()
to
automatically accept unknown host keys. This is a dangerous practice, but it can be useful
for testing and development.
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
File details
Details for the file paramiko_jump-0.1.3.tar.gz
.
File metadata
- Download URL: paramiko_jump-0.1.3.tar.gz
- Upload date:
- Size: 12.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/5.1.1 CPython/3.12.6
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | 4627887e84ad73c7cbe948366f49dc11392e573a6b37d8c7f7f3f5837513a90b |
|
MD5 | a087d41bc9c356bbf85ad644a5d83292 |
|
BLAKE2b-256 | 77c0c425cd5fab722a0b666238ce4ddd701ec605d8d04ab9e23db08f693cebee |
File details
Details for the file paramiko_jump-0.1.3-py3-none-any.whl
.
File metadata
- Download URL: paramiko_jump-0.1.3-py3-none-any.whl
- Upload date:
- Size: 9.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/5.1.1 CPython/3.12.6
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | 585060a832a99a164926a2e5a8a72ce7cda3aae6c09143e0adab8ed9aabfd0f1 |
|
MD5 | f0b4e935a9ba40ded05165627a49bc08 |
|
BLAKE2b-256 | 85eef6aedf9b7c3832207fc4a062396bb33ae6837580fd82458e9615f4fbc77a |