Skip to main content

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:

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

  2. 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:

  1. It doesn't use the context manager.
  2. 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. Read paramiko.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

paramiko_jump-0.1.3.tar.gz (12.0 kB view details)

Uploaded Source

Built Distribution

paramiko_jump-0.1.3-py3-none-any.whl (9.4 kB view details)

Uploaded Python 3

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

Hashes for paramiko_jump-0.1.3.tar.gz
Algorithm Hash digest
SHA256 4627887e84ad73c7cbe948366f49dc11392e573a6b37d8c7f7f3f5837513a90b
MD5 a087d41bc9c356bbf85ad644a5d83292
BLAKE2b-256 77c0c425cd5fab722a0b666238ce4ddd701ec605d8d04ab9e23db08f693cebee

See more details on using hashes here.

File details

Details for the file paramiko_jump-0.1.3-py3-none-any.whl.

File metadata

File hashes

Hashes for paramiko_jump-0.1.3-py3-none-any.whl
Algorithm Hash digest
SHA256 585060a832a99a164926a2e5a8a72ce7cda3aae6c09143e0adab8ed9aabfd0f1
MD5 f0b4e935a9ba40ded05165627a49bc08
BLAKE2b-256 85eef6aedf9b7c3832207fc4a062396bb33ae6837580fd82458e9615f4fbc77a

See more details on using hashes here.

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page