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.
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.
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
- You are starting a new project: sshtunnel 0.4.0 was released in January 2021 and its declared Paramiko dependency has no upper bound even though current source references paramiko.DSSKey, which Paramiko 5.0.0 no longer exports
- You want an installation that works with current dependencies without manual pins: the open compatibility issue reports module 'paramiko' has no attribute 'DSSKey', and the published requirement still says only paramiko>=2.7.2
- You need OpenSSH configuration fidelity such as modern ProxyJump, Include handling, hardware-backed keys, certificates, or system policy; the issue tracker contains unresolved reports around ProxyJump and config Include behavior
- You cannot tolerate accidental network exposure: the CLI documentation says an omitted local host defaults to 0.0.0.0, so other machines may reach the forwarded service unless you bind 127.0.0.1 explicitly
- You need async integration, dynamic SOCKS forwarding, or a maintained SSH stack: the API uses forwarding threads and an old Paramiko integration rather than asyncio
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,<4Treat 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_portsRemote 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.comAlways 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
| Package | Registry | Pick it when |
|---|---|---|
| paramiko | PyPI | You already use Paramiko and can implement or adapt its maintained forwarding example without the stale wrapper |
| asyncssh | PyPI | You need maintained asyncio-native SSH connections and local or remote port forwarding |
| fabric | PyPI | Your real workflow combines SSH command execution, deployment tasks, and connection management rather than only one tunnel |