SSH to network gear you can't reach directly — legacy crypto + jump-host support over Paramiko.
Project description
reachssh
SSH to network gear you can't reach directly.
A thin Paramiko wrapper built for the awkward realities of network-device SSH — the two problems plain Paramiko makes you solve yourself, solved once:
-
Legacy crypto. Old IOS/NX-OS/Junos/ASA gear negotiates ancient KEX, ciphers, and host-key types (DH group14/group1-SHA1, 3DES-CBC, ssh-rsa with SHA-1) that modern OpenSSH and recent Paramiko disable by default. reachssh registers every algorithm handler the installed Paramiko ships and runs a two-pass connect (force ssh-rsa/SHA-1 first, fall back to rsa-sha2 for hardened devices) so both crusty and modern gear just work.
-
Jump hosts / bastions. Paramiko reads
ProxyJumpfrom nowhere — not from~/.ssh/config, not from anywhere. reachssh opens adirect-tcpipchannel through the bastion and tunnels the device connection over it, with the channel re-opened per connect attempt so the legacy/modern retry still works through the jump.
Everything else it does is the network-CLI plumbing you'd otherwise rewrite
per tool: interactive invoke_shell sessions, prompt detection, vendor-
agnostic pagination disable, and deterministic command-output stripping so
config snapshots diff cleanly run-to-run.
reachssh depends only on Paramiko.
Install
pip install reachssh
Basic use
from reachssh import SSHClient, SSHClientConfig
cfg = SSHClientConfig(
host="172.16.1.2",
username="cisco",
password="cisco",
legacy_mode=True, # register + prefer legacy algorithms
)
with SSHClient(cfg) as c:
c.find_prompt()
c.disable_pagination()
print(c.execute_command("show ip interface brief"))
Through a jump host
A single bastion hop — build a JumpSpec and hand it to the config:
from reachssh import SSHClient, SSHClientConfig, JumpSpec, JumpHop
jump = JumpSpec(hops=[JumpHop(
name="thinkstation",
host="10.0.0.108",
username="speterman",
key_content=open("/home/me/.ssh/id_rsa").read(), # or password=...
)])
cfg = SSHClientConfig(
host="172.16.1.2", username="cisco", password="cisco",
legacy_mode=True, jump=jump,
)
with SSHClient(cfg) as c:
c.find_prompt()
print(c.execute_command("show version"))
Rule-based jump selection (many devices, different bastions)
Route-map / ACL ordering — rules are evaluated top-to-bottom, first match wins, so a special-case device rule sits above the broad site rule and wins by position:
from reachssh import build_proxy_resolver
proxy_config = {
"jump_hosts": {
"iad-bastion": {"host": "10.0.0.108", "port": 22, "credential": "lab-jump"},
},
"proxy_rules": [
{"match": {"devices": ["bastion-*"]}, "jump": "direct"}, # the bastion itself
{"match": {"site": "iad-lab"}, "jump": "iad-bastion"},
],
}
# reachssh never touches secrets at rest. YOU supply a resolver mapping a
# credential *name* to a tuple: (user, password) or (user, password, key_pem).
creds = {"lab-jump": ("speterman", None, open("/home/me/.ssh/id_rsa").read())}
resolver = build_proxy_resolver(proxy_config, credential_resolver=creds.get)
spec = resolver.resolve({"name": "wan-core-1", "site_slug": "iad-lab"})
# -> JumpSpec through iad-bastion; pass as SSHClientConfig(jump=spec)
The credential_resolver is the only place bastion secrets enter the library.
Pass a vault lookup, a dict .get, a lambda reading env vars — whatever you
already use.
Migrating a tool off its own SSH client
If you have a tool carrying its own copy of this client (this code originated
inside such tools), replace the vendored ssh/client.py import with:
from reachssh import SSHClient, SSHClientConfig
Tools that never had jump support gain it for free — populate
SSHClientConfig.jump and connections route through the bastion with no other
change.
Paramiko version note
reachssh registers whatever algorithm handlers the installed Paramiko ships
and silently skips ones that aren't present — it never crashes on a missing
handler. Consequence: on Paramiko 5.x, the kex_group1 module was removed,
so diffie-hellman-group1-sha1 cannot be offered. group14-sha1, 3des-cbc,
and ssh-rsa (SHA-1) are still available and cover the large majority of
legacy gear. If you must talk to a device that only speaks
diffie-hellman-group1-sha1, pin Paramiko ≤ 4.x, where that handler still
ships.
Emulation (optional)
For testing without live gear, enable_emulation("ip_lookup.json") transparently
redirects connections to a local mock-device server. Inert unless enabled.
License
MIT
Project details
Release history Release notifications | RSS feed
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
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file reachssh-0.1.0.tar.gz.
File metadata
- Download URL: reachssh-0.1.0.tar.gz
- Upload date:
- Size: 39.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.5
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
eb6bc00939226b806310a4c85daa10bb622575af83d2e1d4e52f6c032641ac6d
|
|
| MD5 |
2d4e4dc531f818094f04ba8e6d2840da
|
|
| BLAKE2b-256 |
1783e48384e1b812e3792307525fafea97920c3132b3a5e2d0fcfe5997aa50ca
|
File details
Details for the file reachssh-0.1.0-py3-none-any.whl.
File metadata
- Download URL: reachssh-0.1.0-py3-none-any.whl
- Upload date:
- Size: 38.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.5
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
490a4528220673ff576fc13cf6bb729d6049c07731c6da01b2d508fc1a2d7c67
|
|
| MD5 |
78c8d14b69333f042e8c8b84b6be2f4a
|
|
| BLAKE2b-256 |
eb4bb017f2af5f86fabd59d83762674d7c43cb60af8c4382f1fdc1a3afd2eb30
|