mrkeyoor.com_
Tue 22 Sept 18:51 UTC
PyPIUtilsupdated 22 Sept 2026

sshtunnel review

sshtunnel 0.4.0 is a Paramiko wrapper for local SSH port forwarding. `SSHTunnelForwarder` connects to a gateway, binds one or more local TCP ports or Unix sockets, and forwards them to addresses visible from that gateway. It exposes assigned ports to Python code and can start and stop through a context manager. The package does not provide a VPN, SOCKS proxy, database client, or full OpenSSH feature set. Its current release is still the January 2021 build.

Verdict

sshtunnel 0.4.0 installed in 0.3 seconds and imported in 0.83 seconds in our August 22 sandbox, but its open-ended Paramiko dependency can now resolve to 5.0.0, which removed a key class its source references. Preserve a tested legacy pin if necessary; do not select it for a new 2026 service.

We installed it

Lab card: what happened when we installed sshtunnelScreenshot of sshtunnel documentation
Install✓ · 0.3s8 packages on disk · 22 MB
Importimport sshtunnel in 0.83s · pure Python
Known vulns0(pip-audit)

Answers from our run

Does sshtunnel install cleanly?

Yes. In a fresh container with an empty cache, pip install sshtunnel finished in 0.3s, leaving 8 packages and 22 MB on disk. pip-audit reported no known vulnerabilities.

What does sshtunnel need to run?

Python 3.x, and nothing compiled: it is pure Python. In our run import sshtunnel succeeded in 0.83s.

sshtunnel or paramiko: which should you use?

paramiko: Choose it when your application already owns Paramiko and can adapt its maintained forwarding example directly. sshtunnel 0.4.0 installed in 0.3 seconds and imported in 0.83 seconds in our August 22 sandbox, but its open-ended Paramiko dependency can now resolve to 5.0.0, which removed a key class its source references.

When should you not use sshtunnel?

This is a new project. Version 0.4.0 is from January 2021, while AsyncSSH and the system OpenSSH client have current forwarding paths.

API stability2/5`SSHTunnelForwarder`, `open_tunnel`, context management, `start()`, `stop()`, and local port properties have remained unchanged for years. The surrounding contract is unstable because the package reaches into Paramiko key classes without constraining an upper compatible version. A frozen wrapper API does not protect callers from a dependency API removal.
Docs3/5Read the Docs includes network diagrams, password and key examples, nested tunnels, multiple bind addresses, CLI switches, and parameter reference. It also retains `easy_install`, Python 2 era commands, DSS key language, and examples that bind `0.0.0.0`. The documentation does not put the current Paramiko 5 compatibility risk next to installation, where a new reader needs it.
Maintenance1/5PyPI's newest release is 0.4.0 from January 11, 2021. GitHub reports 1,295 stars, 81 open issues and pull requests, an unarchived repository, and a last push on August 27, 2025. One later push has not produced updated dependency bounds or a release compatible with Paramiko's removal of `DSSKey`, which is weak maintenance for SSH-facing code.
Ecosystem2/5The package can forward any TCP service and is common in older database connection examples. It builds on Paramiko and gives Python callers the chosen local port. That reach is now a liability as well: PyPI's Paramiko line has moved to 5.0.0, while sshtunnel 0.4.0 still assumes older key classes and offers no asyncio or OpenSSH-native integration.

Use it if

  • An existing pinned deployment already uses `SSHTunnelForwarder` and its Paramiko combination is covered by connection tests.
  • A short Python job needs an OS-assigned local port before creating its database or HTTP client.
  • A context manager is simpler for this process than supervising an external `ssh -L` command.
  • The code will bind only to localhost, verify the gateway host key, and close the forwarder on every path.
Skip it if

Setup reality

We installed sshtunnel 0.4.0 in a fresh Python 3.12 Bookworm sandbox on August 22, 2026. pip finished in 0.3 seconds, left 8 packages consuming 22 MB, and found 0 known vulnerabilities. The package has 5 direct dependencies, is pure Python, carries an MIT license, and declares no Python floor. import sshtunnel worked in 0.83 seconds. No py.typed marker was present.

That successful run is a dated resolver result, not proof that every unpinned install will keep working. PyPI now serves Paramiko 5.0.0, while sshtunnel 0.4.0 declares no upper bound and refers to the removed DSSKey class. Lock and test the exact pair used by an existing deployment. A new service should avoid inheriting this compatibility problem.

Configuration needs the gateway address, user, authentication material, and the remote address as resolved from the gateway. Bind ('127.0.0.1', 0) explicitly; port 0 selects an available local port after start(). Supply a trusted ssh_host_key instead of accepting any reachable gateway. Agent and key-directory discovery can choose credentials you did not intend, so disable them where deterministic identity matters.

Create the database or HTTP client inside the tunnel context and close it before the forwarder exits. SSH forwarding adds no database TLS, authentication, pooling, retries, or reconnection. Keepalives can expose a broken path but do not replay interrupted operations. Test shutdown under signals and failed connections because threads and sockets must disappear cleanly.

Patterns

Freeze a legacy dependency pair pin-legacy-environment

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

Use this only while maintaining an audited deployment. Test the resolved Paramiko patch and schedule replacement work.

Bind a random localhost port forward-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)

The assigned `local_bind_port` exists after startup. Explicit localhost binding prevents other hosts from reaching the forwarded service.

Check the gateway identity verify-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 sample key with one verified out of band. Host-key verification detects a gateway impersonation.

Turn off implicit key selection disable-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 home-directory discovery are enabled by default; disabling both makes the selected production key predictable.

Forward two internal services forward-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 lists correspond by position. Read the assigned ports from the plural property after startup.

Stop a manually managed forwarder start-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()

A context manager is safer. If lifecycle is manual, `finally` must close forwarding threads and sockets.

Ask SSH to send keepalives keep-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 reveal a dead transport. They do not reconnect the tunnel or retry the application's failed operation.

Point at one SSH config file use-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 complete OpenSSH parsing. Reports around `Include` and `ProxyJump` require a test with the exact configuration.

Run a localhost-only CLI forward use-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

Include `127.0.0.1` in `-L`; leaving out the host makes the documented default listen on all interfaces.

Capture tunnel diagnostics enable-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 output can expose users, internal hosts, ports, and key paths. Limit access and return logging to normal afterward.

Alternatives

PackageRegistryPick it when
paramikoPyPIChoose it when your application already owns Paramiko and can adapt its maintained forwarding example directly.
asyncsshPyPIChoose it for asyncio-native SSH with maintained local and remote forwarding APIs.
fabricPyPIChoose it when tunnels are part of a larger Python SSH command and deployment workflow.

More utils guides

lru-cache · ajv · type-fest · p-limit · find-up · js-yaml · the whole shelf →

How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.