mrkeyoor.com_
Sat 08 Aug 17:42 UTC
PyPIUtilsupdated 08 Aug 2026

sshtunnel

Python wrapper around Paramiko that creates local SSH port forwards to services reachable from an SSH gateway. SSHTunnelForwarder can expose one or several remote host and port pairs on local TCP ports or Unix sockets, start forwarding threads, keep the SSH transport alive, and close everything through a context manager. A small CLI covers the same basic tunnel. It is convenience around SSH local forwarding, not a full SSH client, VPN, SOCKS proxy, or database driver.

Verdict

Do not choose sshtunnel for a new project in 2026. Its latest release is from 2021 and its unconstrained dependency can resolve to a Paramiko version that breaks its DSSKey references; preserve only audited, pinned legacy deployments while replacing it.

API stability3/5SSHTunnelForwarder, open_tunnel, start, stop, context management, and local_bind_port have barely changed, which helps old applications. That apparent stability hides dependency breakage: the package reaches into Paramiko key classes that were removed later, and published metadata does not constrain the compatible range. A frozen API is not dependable when its underlying SSH API moves.
Docs3/5Read the Docs and the README include network diagrams, password and key examples, nested gateways, multiple bind options, the CLI reference, and parameter documentation. They still mention easy_install, Python 2-era artifacts, DSS keys, and old URLs, and they do not warn prominently that modern Paramiko releases break current source. The examples are useful but no longer safe as complete setup guidance.
Maintenance1/5The latest PyPI and GitHub release is 0.4.0 from January 11, 2021. The repository received one push on August 27, 2025, but current source still refers to removed Paramiko DSSKey APIs and open issues request dependency updates and renewed development. This is not enough maintenance for an authentication and networking wrapper with millions of downstream installs.
Ecosystem2/5It sees roughly 6,695,944 weekly downloads and database examples often show the package, so legacy exposure is large. It relies on Paramiko and works with any client that can connect to a local port. Yet ecosystem compatibility is now the main failure: current Paramiko is 5.0.0, while sshtunnel's loose dependency and old key-class assumptions conflict with it.

Use it if

  • You are maintaining an existing application already pinned to a compatible Paramiko release and its sshtunnel behavior is understood
  • A short-lived Python process needs one local forward and a context manager is materially easier than supervising an OpenSSH subprocess
  • You need the dynamically assigned local port as a Python value before constructing a database or HTTP client
  • You can explicitly bind to localhost, verify the gateway host key, and close the forwarder on every exit path
Skip it if

Setup reality

Do not install sshtunnel 0.4.0 unpinned in a fresh environment. PyPI declares paramiko>=2.7.2 with no upper limit, so pip currently may select Paramiko 5.0.0. sshtunnel's source accesses paramiko.DSSKey while current Paramiko no longer exports it, matching an open compatibility report. Existing deployments commonly need an audited Paramiko pin below the DSA-removal major, but that pin also holds back SSH security and compatibility changes, which is why a maintained alternative or the system ssh client is a better new-project decision. The package itself declares no Python version floor and still ships a py2.py3 wheel from 2021, so metadata does not promise current Python support; an open report also notes a Python 3.14 syntax warning. If you must maintain it, lock both packages, build a clean-environment connection test, and plan a replacement. Configuration needs an SSH gateway address, username, key or password, and a remote bind address as seen from the gateway. Always set local_bind_address=('127.0.0.1', 0); the documented default can listen on all interfaces. Port 0 asks the OS for a free port, available only after start. Use a context manager so stop runs, and construct the database client inside it. Passwords, private-key passphrases, and host keys belong in a secret store, not source. Supplying ssh_host_key is the explicit server identity check; accepting any reachable gateway undermines SSH. Agent and ~/.ssh key discovery are enabled by default and can select an unexpected key, so disable or constrain them in production. Forwarding does not add database TLS, retries, pooling, or application authentication. Thread and socket cleanup has unresolved issue reports, so test shutdown and failure paths under your process manager.

Patterns

Lock the known legacy dependency rangepin-legacy-environment

# requirements.txt for an existing audited deployment only
sshtunnel==0.4.0
paramiko>=2.7.2,<4

Treat this as a temporary compatibility pin, not a new-project recommendation. Test the exact resolved Paramiko patch and plan migration.

Open a localhost-only tunnelforward-local-port

from sshtunnel import SSHTunnelForwarder

with SSHTunnelForwarder(
    ('bastion.example.com', 22),
    ssh_username='deploy',
    ssh_pkey='/run/secrets/deploy_key',
    remote_bind_address=('db.internal', 5432),
    local_bind_address=('127.0.0.1', 0),
) as tunnel:
    connect_database(host='127.0.0.1', port=tunnel.local_bind_port)

Use 127.0.0.1 explicitly. Port 0 selects a free port, and local_bind_port is valid after the context starts.

Pin the gateway host keyverify-host-key

with SSHTunnelForwarder(
    ('bastion.example.com', 22),
    ssh_username='deploy',
    ssh_pkey='/run/secrets/deploy_key',
    ssh_host_key='ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI...',
    remote_bind_address=('db.internal', 5432),
    local_bind_address=('127.0.0.1', 0),
) as tunnel:
    use(tunnel.local_bind_port)

Replace the placeholder with a trusted out-of-band key. Host-key verification is what detects an impersonated gateway.

Use only the configured private keydisable-key-discovery

tunnel = SSHTunnelForwarder(
    ('bastion.example.com', 22),
    ssh_username='deploy',
    ssh_pkey='/run/secrets/deploy_key',
    allow_agent=False,
    host_pkey_directories=[],
    remote_bind_address=('db.internal', 5432),
    local_bind_address=('127.0.0.1', 0),
)

Agent and ~/.ssh key discovery default on; disabling them makes production authentication selection explicit.

Open multiple forwards through one gatewayforward-multiple-services

with SSHTunnelForwarder(
    ('bastion.example.com', 22),
    ssh_username='deploy',
    ssh_pkey='/run/secrets/deploy_key',
    remote_bind_addresses=[('db.internal', 5432), ('cache.internal', 6379)],
    local_bind_addresses=[('127.0.0.1', 0), ('127.0.0.1', 0)],
) as tunnel:
    db_port, cache_port = tunnel.local_bind_ports

Remote and local bind lists are positional and must align; use the plural local_bind_ports property.

Manage a long-lived forwarderstart-and-stop-manually

tunnel = SSHTunnelForwarder(
    ('bastion.example.com', 22),
    ssh_username='deploy',
    remote_bind_address=('service.internal', 8080),
    local_bind_address=('127.0.0.1', 0),
)
try:
    tunnel.start()
    serve_through(tunnel.local_bind_port)
finally:
    tunnel.stop()

Prefer a context manager. If lifecycle must be manual, stop in finally so exceptions do not leave forwarding threads and sockets behind.

Send SSH keepaliveskeep-transport-alive

tunnel = SSHTunnelForwarder(
    ('bastion.example.com', 22),
    ssh_username='deploy',
    set_keepalive=30.0,
    remote_bind_address=('db.internal', 5432),
    local_bind_address=('127.0.0.1', 0),
)

Keepalives can detect broken paths but do not reconnect the tunnel or retry interrupted application operations.

Read a specific SSH config fileuse-ssh-config

tunnel = SSHTunnelForwarder(
    'bastion-alias',
    ssh_config_file='/etc/myapp/ssh_config',
    remote_bind_address=('db.internal', 5432),
    local_bind_address=('127.0.0.1', 0),
)

Do not assume full OpenSSH config compatibility; unresolved issues report gaps around Include and ProxyJump behavior.

Open a CLI tunneluse-cli-forward

python -m sshtunnel \
  -U deploy \
  -K /run/secrets/deploy_key \
  -L 127.0.0.1:15432 \
  -R db.internal:5432 \
  bastion.example.com

Always include 127.0.0.1 in -L. Omitting the host can expose the forwarded database on all interfaces.

Log tunnel setup detailsenable-debug-logging

import logging
from sshtunnel import create_logger, SSHTunnelForwarder

logger = create_logger(loglevel=logging.DEBUG)
tunnel = SSHTunnelForwarder(
    ('bastion.example.com', 22),
    ssh_username='deploy',
    remote_bind_address=('db.internal', 5432),
    local_bind_address=('127.0.0.1', 0),
    logger=logger,
)

Debug logs may reveal hostnames, ports, usernames, and key paths. Restrict and remove them after diagnosis.

Alternatives

PackageRegistryPick it when
paramikoPyPIYou already use Paramiko and can implement or adapt its maintained forwarding example without the stale wrapper
asyncsshPyPIYou need maintained asyncio-native SSH connections and local or remote port forwarding
fabricPyPIYour real workflow combines SSH command execution, deployment tasks, and connection management rather than only one tunnel