Paramiko guide

Paramiko for network engineers: SSH from Python, explained

Paramiko is a pure-Python implementation of the SSHv2 protocol, client and server, maintained by Jeff Forcier. This guide covers it for network devices: SSHClient, invoke_shell, host keys and the common fixes.

Paramiko at a glance

Paramiko is a pure-Python implementation of the SSHv2 protocol, providing both client and server functionality, in the words of paramiko.org. Several well-known tools, including Fabric and Netmiko, build on it.

Latest release
5.0.0, released 9 May 2026 (release page on PyPI)
Licence
LGPL-2.1 (licence text)
Python
3.9 or newer. PyPI lists Python 3.9 to 3.13
Maintainer
Jeff Forcier (bitprophet on GitHub)

Verified on against PyPI, paramiko.org including its changelog, docs.paramiko.org and the GitHub repository at tag 5.0.0. The code examples were checked against the 5.0.0 source. None was run against a network device.

What is Paramiko?

An SSH client negotiates algorithms, checks the server's host key, authenticates, and opens channels to carry commands, shells and file transfers. Paramiko does that inside your Python process, using the cryptography library, so there is no SSH binary to call.

Outside networking it runs commands on servers and transfers files over SFTP. Inside networking it is the layer under a great deal of Python tooling, because most network devices speak SSH. This guide is about that second use: routers, switches and firewalls, where the device is not a Linux host and a few things behave differently.

One piece of guidance from the project itself is worth stating early. Paramiko's own description on PyPI says that Fabric is what the maintainers recommend for common client use-cases such as running remote shell commands or transferring files, and that direct use of Paramiko is intended for people who need advanced or low-level primitives, or want to run an in-Python SSH server. Network automation is often exactly that kind of case, which is why so many network libraries sit on Paramiko rather than on a higher-level wrapper.

If you are choosing a tool, this page covers Paramiko itself. For the library that adds network-device behaviour on top of it, read the Netmiko guide. For running tasks across a whole inventory, see the Nornir guide.

Who maintains Paramiko?

Jeff Forcier (bitprophet on GitHub) maintains Paramiko. PyPI lists him as the author, paramiko.org carries his copyright, and he has the largest share of commits in the project's GitHub contributor list. paramiko.org also says that he keeps a roadmap on his personal site, and that professionally supported Paramiko is available through the Tidelift Subscription. He also maintains Fabric, the high-level library built on Paramiko.

The project is also the work of its contributors. The changelog credits patches and reports by name, release after release, and it runs back through the 1.x and 2.x lines. A library that has been maintained for that long is a real piece of infrastructure for the Python community.

Netmiko's GitHub description reads “Multi-vendor library to simplify Paramiko SSH connections to network devices”, and its PyPI metadata lists Paramiko as a dependency.

One more point from the project, quoted fairly because it affects network use. The Paramiko FAQ says the maintainers can only fully support standard OpenSSH implementations, such as those found on a typical Linux distribution, because volunteer development time and access to non-mainstream platforms are limited. Bug reports for nonstandard SSH implementations are typically closed, though the maintainers say they will consider patches for them. Network operating systems are often in that second group, so expect to test against your own platform and not to rely on the project to have done it. Read the FAQ for the full wording.

Install Paramiko

Paramiko 5.0.0 needs Python 3.9 or newer. Create a virtual environment and install it with pip. The last line prints the version you ended up with, which is worth checking when an environment also holds other libraries that depend on Paramiko.

Install Paramiko into a virtual environment
python3 -m venv .venv
source .venv/bin/activate
pip install paramiko
python -c "import paramiko; print(paramiko.__version__)"

Paramiko's direct dependencies on PyPI are cryptography, bcrypt, pynacl and invoke. The installing page explains that Cryptography is the big one, with its own dependencies, and that it provides the low-level encryption algorithms the SSH protocol needs. On most platforms pip installs a prebuilt wheel for it, and the installing page lists what a build from source needs where no wheel is available.

The same page says which release lines receive fixes: bug fixes are guaranteed for the last two or three releases, including the latest stable one. New users should install the latest stable release. If other packages in your environment declare their own Paramiko range, let pip resolve one that satisfies all of them. For example, Netmiko's PyPI metadata has a separate par4 extra that limits Paramiko to the 4.x line, and scrapli's paramiko extra asks for a version below 4.0.0. Those ranges are theirs to maintain, and they change between releases.

What changed in Paramiko 4 and 5

The changelog records two sets of removals that matter to anyone who still has older devices or older scripts.

  • 4.0.0 removed support for the DSA (also called DSS) key algorithm, which the changelog describes as badly outdated and insecure for a decade or more, and recently removed from OpenSSH as well. An ImportError naming DSSKey is a code problem, not a key problem: something is importing a class that Paramiko 4 and later no longer ship. The remedies are separate.
    • If your code or a dependency imports DSSKey, update that code or upgrade the dependency. Changing a device key does not fix an import error.
    • Where you control DSA keys, replace them with Ed25519, ECDSA or RSA. The changelog recommends Ed25519, or perhaps ECDSA.
    • If DSA compatibility with hosts you do not control is unavoidable, pinning a Paramiko release before 4.0, the 3.x line the changelog names, is the remaining option. It keeps an algorithm the changelog calls insecure and leaves you on an older line, so confine it to a separate environment and plan its retirement.
  • 5.0.0 removed signing and verifying with SHA-1 RSA signatures, key exchange using SHA-1 (diffie-hellman-group1-sha1, diffie-hellman-group14-sha1 and diffie-hellman-group-exchange-sha1), and GSSAPI support. It also raised the minimum modulus size for diffie-hellman-group-exchange-sha256 from 1024 to 2048. The changelog flags each as backwards incompatible for legacy systems that cannot use the SHA-2 alternatives. The older devices section below covers what that means in practice.

SSHClient basics

SSHClient is Paramiko's high-level client. You create one, call connect, and then run commands or open an SFTP session on the same connection. The client documentation is the reference for everything in this section.

connect checks the server's host key against the keys you have loaded, applies the missing-host-key policy if the host is unknown, and then authenticates. The documentation gives the order in which it tries credentials: the pkey or key_filename you pass, then any key from an SSH agent, then keys it finds in ~/.ssh/, and finally a password if you gave one. The sections on host keys and on older devices come back to the first two steps. Credentials in these examples come from the environment, never from the script.

Set credentials in the environment, never in the script
# Set these in your shell, or load them from a secret store.
# Never commit them to a repository.
export PARAMIKO_USER="netops"
export PARAMIKO_PASSWORD="replace-with-your-password"
export PARAMIKO_KEY_FILE="$HOME/.ssh/id_ed25519"

Key authentication

exec_command opens a new channel and runs one command on the server. It returns three file-like objects: stdin, stdout and stderr. Read the output, then ask the channel for the exit status. SSHClient works as a context manager, so the connection is closed when the block ends. The documentation warns that failing to close a client explicitly can lead to end-of-process hangs, which is why the closing step is not optional.

The timeout argument to exec_command sets the channel's I/O timeout, as the documentation puts it. It limits how long a read can block. It is not a deadline for the command, and recv_exit_status() waits until the command finishes or the channel closes, whatever timeout is set.

Connect with a key and run one command
import os

import paramiko

HOST = "192.0.2.10"

with paramiko.SSHClient() as client:
    # Trust only hosts already listed in ~/.ssh/known_hosts. The default
    # policy rejects any other host, which is the safe choice.
    client.load_system_host_keys()
    client.connect(
        HOST,
        username=os.environ["PARAMIKO_USER"],
        key_filename=os.environ["PARAMIKO_KEY_FILE"],
        timeout=10,
    )
    # timeout is a channel I/O timeout. It limits how long a read may block,
    # not how long the command may run.
    stdin, stdout, stderr = client.exec_command("uname -a", timeout=30)
    # This reads stdout only, which suits a short command. The helper in the
    # next example reads both streams for commands that may write more.
    output = stdout.read().decode()
    status = stdout.channel.recv_exit_status()

print("exit status:", status)
print("stdout:", output.strip())

Password authentication

Because the documented order tries agent and discovered keys before the password, a password login can offer several keys first. Passing allow_agent=False and look_for_keys=False makes the password the only credential offered, so nothing you did not intend is tried first.

Connect with a password only
import os

import paramiko

HOST = "192.0.2.10"

with paramiko.SSHClient() as client:
    client.load_system_host_keys()
    client.connect(
        HOST,
        username=os.environ["PARAMIKO_USER"],
        password=os.environ["PARAMIKO_PASSWORD"],
        # Use only the password: skip the SSH agent and any keys in ~/.ssh.
        allow_agent=False,
        look_for_keys=False,
        timeout=10,
    )
    print("connected:", client.get_transport().is_active())

Exit status, stdout and stderr

Each exec_command call is a separate channel on the same connection, so state such as the working directory or an exported variable does not carry from one call to the next. A small helper keeps the pattern in one place.

Reading all of stdout and then all of stderr can block. If the command fills the stderr window while your code waits on stdout, the command waits for window space and your code waits for stdout. The documentation for recv_exit_status warns about the same window-size hang for large output. This helper reads both streams as data arrives, using recv_ready and recv_stderr_ready, and enforces its own deadline, closing the channel if the command runs too long. When you do not need stderr separately, the simpler alternative is channel.set_combine_stderr(True), which merges stderr into the one stream you read.

A helper that reads both streams and returns the exit status, stdout and stderr
import os
import time

import paramiko

HOST = "192.0.2.10"


def run(client, command, deadline=30.0, poll=0.05):
    """Run one command and return (exit_status, stdout, stderr).

    Both streams are read as data arrives, so a full stderr window is not left
    unread while stdout is being read. The deadline covers the whole command:
    the channel is closed if it passes. A server that closes the channel
    without sending an exit status also ends at the deadline.
    """
    stdin, stdout, stderr = client.exec_command(command)
    channel = stdout.channel
    out, err = bytearray(), bytearray()
    end = time.monotonic() + deadline
    while True:
        moved = False
        if channel.recv_ready():
            out += channel.recv(65536)
            moved = True
        if channel.recv_stderr_ready():
            err += channel.recv_stderr(65536)
            moved = True
        if not moved:
            if channel.exit_status_ready():
                break
            if time.monotonic() > end:
                channel.close()
                raise TimeoutError(
                    f"{command!r} did not finish in {deadline:.0f} seconds."
                )
            time.sleep(poll)
    return (
        channel.recv_exit_status(),
        out.decode(errors="replace"),
        err.decode(errors="replace"),
    )


with paramiko.SSHClient() as client:
    client.load_system_host_keys()
    client.connect(
        HOST,
        username=os.environ["PARAMIKO_USER"],
        key_filename=os.environ["PARAMIKO_KEY_FILE"],
        timeout=10,
    )
    # Each exec_command opens a new channel on the same SSH connection.
    # State such as the working directory does not carry between them.
    for command in ("hostname", "ls /does-not-exist"):
        status, out, err = run(client, command)
        print(f"{command!r} exited {status}")
        print("  stdout:", out.strip())
        print("  stderr:", err.strip())

The key, helper and host key examples in this guide were run against an OpenSSH server on Linux. That says nothing about how a router or switch behaves, which is the subject of the next section.

Why network devices are different: invoke_shell

Examples written for Paramiko 5.0.0 and checked against its source. Run them against a lab device before production.

exec_command asks the server to run a single command, and that model comes from Unix servers. Many network devices present a command-line interface that is a program in its own right, with prompts, modes and paging, and whether a given platform accepts a command over an exec channel varies with the operating system and software release, so test your own platform. The reliable approach for a prompt-driven CLI is the one a person uses: open an interactive session, type a command, and read until the prompt comes back.

invoke_shell does that. Its documentation says it starts an interactive shell session, opening a new channel connected to a pseudo-terminal of the terminal type and size you request. From then on you are reading and writing a stream, not collecting the result of a command, and that changes what you have to handle.

Interactive channels, prompts and buffers

  • Data arrives in pieces. recv(nbytes) returns what is available, up to nbytes, and a single command's output can arrive across several calls, split anywhere, including in the middle of a line.
  • You decide when a command has finished. There is no per-command exit status on an interactive channel. The usual signal is that the prompt has reappeared, so you read until the last line of the output looks like one.
  • Do not guess with a sleep. Waiting a fixed time and then calling recv either truncates slow output or wastes time on fast output. Read in a loop with a deadline instead.
  • Reads block unless you tell them not to. With no timeout set, recv waits indefinitely. Setting one with settimeout makes it raise socket.timeout when nothing arrives, and the documentation says a zero-length result means the channel has closed. Both are signals your loop should handle. The timeout applies to writes as well as reads.
  • The device echoes what you type. The first line of the output is usually your own command, which you will want to drop.

Paging

A device that pages long output stops at a prompt such as --More-- and waits for a key. A script that reads until the next CLI prompt will wait indefinitely. The usual remedy is to turn paging off for the session before sending anything else. On Cisco IOS-style platforms the command is terminal length 0. Other platforms use different commands, so check your vendor's documentation, and do not assume one command works everywhere.

Sending several commands to a Cisco-style CLI

The example below connects, opens a shell, waits for the first prompt, turns paging off, and then sends two show commands. The helper reads until the last line matches a prompt pattern, raises TimeoutError if none appears before the deadline, and raises ConnectionError if the device closes the channel. It restores the channel's previous timeout when it returns.

Commands are sent with sendall, not send. send may transmit only part of the data and returns how much it sent. sendall keeps sending until all of it has gone or an error occurs, and raises socket.timeout if sending stalls for longer than the channel's timeout. The output is cleaned conservatively: the first line is removed only if it is exactly the command that was sent, and the last only if it matches the prompt pattern.

Send several commands to a Cisco-style CLI over an interactive shell
import os
import re
import socket
import time

import paramiko

HOST = "192.0.2.20"
# Matches prompts such as "edge1#", "edge1>" and "edge1(config)#".
PROMPT = re.compile(r"^[\w.\-]+(\([\w.\-/]+\))?[#>]\s*$")


def read_until_prompt(channel, timeout=30.0, poll=0.2):
    """Collect output until the last line looks like a CLI prompt."""
    output = ""
    deadline = time.monotonic() + timeout
    previous = channel.gettimeout()
    channel.settimeout(poll)  # recv() waits at most this long per call
    try:
        while time.monotonic() < deadline:
            try:
                chunk = channel.recv(65535)
            except socket.timeout:
                continue
            if not chunk:  # zero bytes means the channel has closed
                raise ConnectionError("The device closed the channel.")
            output += chunk.decode("utf-8", errors="replace")
            lines = output.splitlines()
            if lines and PROMPT.match(lines[-1].strip()):
                return output
    finally:
        channel.settimeout(previous)
    raise TimeoutError(f"No prompt seen within {timeout:.0f} seconds.")


def run_commands(channel, commands):
    """Send each command in turn and return {command: output}."""
    results = {}
    for command in commands:
        channel.sendall(command + "\n")
        lines = read_until_prompt(channel).splitlines()
        # Drop the echoed command only if the first line is exactly that.
        if lines and lines[0].strip() == command:
            lines = lines[1:]
        # Drop the trailing prompt only if the last line matches the pattern.
        if lines and PROMPT.match(lines[-1].strip()):
            lines = lines[:-1]
        results[command] = "\n".join(lines)
    return results


with paramiko.SSHClient() as client:
    client.load_system_host_keys()
    client.connect(
        HOST,
        username=os.environ["PARAMIKO_USER"],
        password=os.environ["PARAMIKO_PASSWORD"],
        allow_agent=False,
        look_for_keys=False,
        timeout=10,
    )
    channel = client.invoke_shell(width=200, height=50)
    read_until_prompt(channel)  # banner and the first prompt

    # Turn paging off first, or long output stops at a "--More--" prompt.
    # The command is Cisco IOS style. Other platforms use different ones.
    run_commands(channel, ["terminal length 0"])

    outputs = run_commands(channel, ["show version", "show ip interface brief"])
    for command, text in outputs.items():
        print(f"--- {command} ---")
        print(text)

    channel.sendall("exit\n")

Treat this as a starting point. The prompt pattern has to match your platform, and it will need to cope with whatever else your devices send: a banner that ends in something prompt-like, an enable or configuration mode that changes the prompt, a confirmation question that is not a prompt at all, or a password request. Every platform adds its own cases.

That is the honest reason a library like Netmiko exists. Prompt detection, mode handling, paging and the per-platform quirks above are exactly what Netmiko does for you, on top of Paramiko. If you find yourself extending the helper above to cover a second and third vendor, read the Netmiko guide before writing more of it.

Host keys and security

A host key is how an SSH server proves which device it is. Checking it is what stops a script from sending credentials and commands to whatever happens to answer on an address. Paramiko makes the check explicit, and how you configure it is the most important security decision in a Paramiko script.

The three policies

When connect meets a host with no key on record, it asks the client's missing-host-key policy what to do. The documentation names RejectPolicy as the default, and the 5.0.0 source confirms it: a new SSHClient starts with a RejectPolicy. The other two are opt-in.

Paramiko missing-host-key policies and what each does with an unknown host
PolicyUnknown hostWhat to know
RejectPolicyRaises SSHException with the message “Server '...' not found in known_hosts”, and the connection fails.The default. Nothing is trusted unless you loaded its key.
WarningPolicyIssues a Python warning that names the key type and fingerprint, then accepts the host.The key is accepted and is not added to the host keys, so the same warning returns on the next connection.
AutoAddPolicyAccepts the host, adds its key to the client's host keys, and saves them if a host keys file was loaded.The key is accepted without independently verifying the host's identity.

A policy decides only what happens for a host with no key on record. If a key is on record and the device presents a different one, connect raises BadHostKeyException whichever policy is set. That exception is the one to take seriously: it means the device was replaced or rekeyed, or something else is answering for it.

Is AutoAddPolicy safe?

It is convenient, and it is not for production. AutoAddPolicy accepts an unknown host's key without independently verifying its identity, which leaves the first connection open to impersonation. Once the key is recorded and loaded on later connections, a different key for that host raises BadHostKeyException. That protection only persists if the keys are saved (save_host_keys) and loaded again (load_host_keys). AutoAddPolicy saves them itself only when a file was loaded with load_host_keys, and a script that loads nothing starts with no record every time.

In a lab you control it is a reasonable way to read a key once, provided you compare the fingerprint through a trusted, independent channel before you trust it. The second example below does that: it prints the candidate key's SHA256 fingerprint, asks for confirmation, and only then adds the key to the known hosts file you use with RejectPolicy. Nothing is written to that file before the comparison.

Loading known hosts

load_system_host_keys() reads the user's known hosts file, as used by OpenSSH, and does not write back to it. load_host_keys(filename) loads a file that is checked after the system keys and that save_host_keys can write, which suits a file you maintain for an estate. One detail trips people up: for a port other than 22 the host name Paramiko looks up is [host]:port, matching the OpenSSH format, so an entry for a device on port 2222 must be written that way.

Verify host keys against a known_hosts file you maintain
import os

import paramiko

HOST = "192.0.2.10"

client = paramiko.SSHClient()
# A known_hosts file that you maintain, for example one per estate.
client.load_host_keys(os.environ["KNOWN_HOSTS_FILE"])
client.set_missing_host_key_policy(paramiko.RejectPolicy())  # the default

try:
    client.connect(
        HOST,
        username=os.environ["PARAMIKO_USER"],
        key_filename=os.environ["PARAMIKO_KEY_FILE"],
        timeout=10,
    )
except paramiko.BadHostKeyException as exc:
    # The device presented a different key from the one on record.
    print("Host key changed for", exc.hostname, "- do not continue.")
except paramiko.SSHException as exc:
    # With RejectPolicy, an unknown host raises SSHException:
    # "Server '...' not found in known_hosts".
    print("Connection refused by policy or protocol:", exc)
else:
    print("host key verified")
finally:
    client.close()
Lab only: read a candidate host key, compare it, then record it
import os

import paramiko

HOST = "192.0.2.30"  # a lab device you control
# The file you use with RejectPolicy. Nothing is written to it until the
# fingerprint has been compared.
KNOWN_HOSTS_FILE = os.environ["KNOWN_HOSTS_FILE"]

client = paramiko.SSHClient()
# LAB ONLY: accept the key so that it can be read. This does not verify who
# answered, and it writes nothing to disk because no file has been loaded.
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
try:
    client.connect(
        HOST,
        username=os.environ["PARAMIKO_USER"],
        key_filename=os.environ["PARAMIKO_KEY_FILE"],
        timeout=10,
    )
    key = client.get_transport().get_remote_server_key()
finally:
    client.close()

print("Candidate host key:", key.get_name(), key.fingerprint)
print("Compare this fingerprint through a trusted, independent channel,")
print("such as the device console or your vendor or asset record.")
if input("Type yes only if it matches: ").strip().lower() == "yes":
    known = paramiko.HostKeys()
    if os.path.exists(KNOWN_HOSTS_FILE):
        known.load(KNOWN_HOSTS_FILE)
    known.add(HOST, key.get_name(), key)
    known.save(KNOWN_HOSTS_FILE)
    print("Recorded. Connect from now on with RejectPolicy.")
else:
    print("Not recorded.")

Key types in the current version

Paramiko 5.0.0 provides RSA, ECDSA and Ed25519 key classes. DSA was removed in 4.0.0. In the 5.0.0 source the host key algorithms Paramiko prefers, in order, are ssh-ed25519, the three ecdsa-sha2-nistp curves, rsa-sha2-512 and rsa-sha2-256. The host keys documentation and the client documentation cover the rest.

Older devices and algorithms

Older network kit may offer only older algorithms, and a modern SSH client may refuse to negotiate them. Paramiko 5.0.0 removed SHA-1 key exchange and SHA-1 RSA signatures, so a device that offers nothing else fails during the handshake. The message is clear once you know to look for it: Incompatible ssh peer (no acceptable kex algorithm), or the matching message for host keys, ciphers or MACs. Every step in this section is a security trade-off, and the aim is to make it deliberately, with a plan to retire it.

Seeing what is on offer

Paramiko logs through the standard logging module, and at debug level it records the algorithms the two sides offered during negotiation, which shows the mismatch directly.

Turn on Paramiko debug logging to see the negotiation
import logging
import os

import paramiko

logging.basicConfig(level=logging.INFO)
# DEBUG logging shows what each side offered during negotiation.
logging.getLogger("paramiko").setLevel(logging.DEBUG)

with paramiko.SSHClient() as client:
    client.load_system_host_keys()
    client.connect(
        "192.0.2.10",
        username=os.environ["PARAMIKO_USER"],
        key_filename=os.environ["PARAMIKO_KEY_FILE"],
        timeout=10,
    )

disabled_algorithms

SSHClient.connect accepts disabled_algorithms, which it passes to Transport. It is a dictionary mapping an algorithm type to a list of identifiers to switch off for that connection. The keys match the last word of Paramiko's built-in lists: kex, ciphers, macs, keys (host key algorithms), pubkeys (user key algorithms) and compression. The values must exactly match members of that list. The Transport documentation's own example is disabling a key exchange for a server that implements it differently from Paramiko, and the source recommends this argument over monkeypatching.

Disable one algorithm with disabled_algorithms
import os

import paramiko

HOST = "192.0.2.40"

with paramiko.SSHClient() as client:
    client.load_system_host_keys()
    client.connect(
        HOST,
        username=os.environ["PARAMIKO_USER"],
        password=os.environ["PARAMIKO_PASSWORD"],
        allow_agent=False,
        look_for_keys=False,
        timeout=10,
        # Remove an algorithm from the lists Paramiko offers. This example
        # is the one in the Transport documentation: a server that
        # implements this key exchange differently from Paramiko.
        disabled_algorithms={"kex": ["diffie-hellman-group16-sha512"]},
    )
    print("negotiated with", client.get_transport().remote_version)

The important limit is that disabled_algorithms only removes things from what Paramiko offers. It cannot bring back an algorithm that a release no longer implements.

When the device needs what 5.0 removed

In preference order, the options are these. First, fix the device: a software update or a configuration change that enables a SHA-2 key exchange and SHA-2 RSA signatures, following your vendor's documentation, is the cleanest answer because it removes the problem. Second, if the device cannot be changed, an earlier release line still offers the old algorithms. The 4.0.0 source still lists the SHA-1 key exchanges and ssh-rsa, and 3.5.1 still has DSA. The changelog's own advice for DSA hosts you do not control is to keep using Paramiko 3.x.

Pin an earlier release line only when a device you cannot change needs it
# 5.0 removed SHA-1 key exchange. The 4.x line still offers it.
pip install "paramiko>=4,<5"

# 4.0 removed DSA keys. The 3.x line still has them.
pip install "paramiko>=3.5,<4"

If you take the second route, keep the older environment small and separate, record why it exists, and treat it as temporary. The installing page says bug fixes are guaranteed for the last two or three releases, so check where a pinned line stands before relying on it.

It is also worth knowing what remains in 5.0.0. Its source still lists AES-CBC and 3DES-CBC among its ciphers and HMAC-SHA1 and HMAC-MD5 among its MACs, so a device limited to those may negotiate, and a device limited to the removed algorithms will not. Read the preference lists for the version you run, because they change between releases.

SFTP and SCP, briefly

Paramiko includes an SFTP client: SSHClient.open_sftp() returns an SFTPClient on the same connection, documented in the SFTP reference, for moving files such as configurations and images where a device supports the SFTP subsystem. SCP is not part of Paramiko. A separate package, scp on PyPI, describes itself as an scp module for Paramiko and uses a Paramiko transport, and Netmiko's PyPI metadata lists it as a dependency.

AsyncSSH vs Paramiko

AsyncSSH is an alternative worth comparing with Paramiko, and the honest summary is that they are two good answers to different shapes of program. Both implement SSHv2 for Python, and neither is specific to network devices. The difference that matters first is the concurrency model.

Paramiko and AsyncSSH compared on concurrency model, API, Python version, licence, release, maintainer and algorithm control
AspectParamikoAsyncSSH
Concurrency modelA blocking API. Its Transport is a Python thread, so running many hosts at once means threads or processes that you manage.Built on the Python asyncio framework. PyPI lists multiple SSH connections in a single event loop.
API styleSSHClient with file-like stdin, stdout and stderr, and Channel objects for interactive sessions.Coroutines: await conn.run(...) returns a completed process, and create_process gives streams for longer sessions.
Python3.9 or newer.3.10 or newer.
LicenceLGPL-2.1, on PyPI.EPL-2.0 OR GPL-2.0-or-later, on PyPI.
Latest release5.0.0, 9 May 2026.2.24.0, 27 June 2026.
MaintainerJeff Forcier.Ron Frederick.
Client and serverBoth. paramiko.org describes it as client and server.Both. PyPI describes an asynchronous client and server.
File transferAn SFTP client. SCP is a separate package that uses a Paramiko transport.SFTP and SCP client and server functions, per PyPI.
Algorithm controldisabled_algorithms removes algorithms from what is offered. 5.0.0 removed SHA-1 key exchange and SHA-1 RSA signatures.Per-connection algorithm lists such as kex_algs, encryption_algs and mac_algs. The 2.24.0 source's supported key exchange list still includes some SHA-1 methods, and PyPI lists post-quantum ML-KEM and SNTRUP among its algorithms.
Network-device behaviourNone of its own: it is the SSH layer. Netmiko builds on it for prompts, modes and per-platform drivers.None of its own: it is the SSH layer. Scrapli lists it as one of its optional transports.

The AsyncSSH column restates its PyPI page and its 2.24.0 source, and its documentation is the reference. Both libraries move between releases, so check the current pages for your versions.

The same job in AsyncSSH

This example runs one command on two hosts at once with asyncio.gather. It is the AsyncSSH counterpart of the key authentication example earlier, with host key checking against a known hosts file you supply. Every name and keyword in it was checked against AsyncSSH 2.24.0.

Install AsyncSSH, a separate package
# AsyncSSH is a separate package and needs Python 3.10 or newer.
pip install asyncssh
The same job with AsyncSSH, across two hosts at once
import asyncio
import os

import asyncssh

HOSTS = ["192.0.2.10", "192.0.2.11"]


async def uname(host):
    async with asyncssh.connect(
        host,
        username=os.environ["SSH_USER"],
        client_keys=[os.environ["SSH_KEY_FILE"]],
        known_hosts=os.environ["KNOWN_HOSTS_FILE"],
    ) as conn:
        result = await conn.run("uname -a", check=False, timeout=30)
        return result.exit_status, result.stdout


async def main():
    results = await asyncio.gather(
        *(uname(host) for host in HOSTS), return_exceptions=True
    )
    for host, result in zip(HOSTS, results):
        if isinstance(result, Exception):
            print(host, "failed:", result)
        else:
            status, out = result
            print(host, status, out.strip())


asyncio.run(main())

Choosing between them

Paramiko tends to suit scripts and tools that are already synchronous, code that sits under libraries built on it, and teams that are happy to use threads for concurrency. AsyncSSH tends to suit programs that are already built on asyncio and want many sessions in one event loop. The licences differ too: LGPL-2.1 for Paramiko and EPL-2.0 OR GPL-2.0-or-later for AsyncSSH, as PyPI lists them. Ask your own legal adviser how either applies to the way you distribute software. This guide does not rank them.

Paramiko alternatives

Beyond AsyncSSH, the options sit at different layers. ssh2-python provides Python bindings for the libssh2 C library under LGPL-2.1; its latest PyPI release is 1.2.0.post1, dated 12 October 2025, and its repository is not archived, so check its recent activity against your needs. Higher up, Netmiko (MIT, version 4.8.0) adds network-device behaviour on top of Paramiko, Scrapli (MIT, version 2026.2.20) is a screen-scraping client for network devices with a pluggable transport system, and Fabric is the high-level library for running commands on servers that Paramiko's own description recommends for common client use-cases. They solve different problems, and this guide does not rank them.

For how Netmiko and Paramiko relate, what Netmiko adds and how to use it directly, read the Netmiko guide. For running tasks across a whole inventory, see the Nornir guide.

Troubleshooting common Paramiko problems

Most first problems fall into a handful of groups: authentication, timeouts, the SSH banner, algorithm mismatches, closed channels and reads that never return. Everything below is behaviour that the Paramiko 5.0.0 documentation or source confirms, and the messages quoted are the ones the source raises. The documentation and the FAQ cover more.

Handle the common connection failures separately
import os
import socket

import paramiko

HOST = "192.0.2.10"

client = paramiko.SSHClient()
client.load_system_host_keys()
try:
    client.connect(
        HOST,
        username=os.environ["PARAMIKO_USER"],
        key_filename=os.environ["PARAMIKO_KEY_FILE"],
        timeout=10,  # TCP connect and session negotiation
        banner_timeout=30,  # wait for the SSH banner
        auth_timeout=30,  # wait for the authentication reply
        channel_timeout=30,  # wait for the session channel to open
    )
except paramiko.BadHostKeyException:
    print("Host key does not match the one on record.")
except paramiko.AuthenticationException as exc:
    # Includes BadAuthenticationType, which lists allowed_types.
    print("Authentication failed:", exc)
except paramiko.SSHException as exc:
    # Banner, algorithm and policy errors, and "No authentication
    # methods available", all arrive here with a message to read.
    print("SSH error:", exc)
except paramiko.ssh_exception.NoValidConnectionsError:
    print("Connection refused or host unreachable.")
except socket.timeout:
    print("Timed out while connecting.")
finally:
    client.close()

The handlers are ordered from most to least specific on purpose. BadHostKeyException, AuthenticationException and the algorithm errors are all subclasses of SSHException, so a broad handler placed first would hide them. NoValidConnectionsError lives in paramiko.ssh_exception and is a socket.error.

Authentication failed

AuthenticationException with the message Authentication failed. means the server refused the credentials you offered. Check the username, the password or key, and remember the documented order: Paramiko offers your key, then agent keys, then keys in ~/.ssh/, then the password. If the server does not allow the method at all you get BadAuthenticationType, a subclass, whose message lists the allowed types, for example allowed types: ['publickey', 'keyboard-interactive']. Offer one of those instead. The Transport documentation describes auth_interactive for servers that ask questions.

No authentication methods available

SSHException: No authentication methods available is raised when connect had nothing to try: no password, no pkey or key_filename, and no agent or ~/.ssh/ key found, often because you turned both allow_agent and look_for_keys off without passing a credential. Pass a password or a key, or re-enable the searches.

Timeouts

There are four separate timeouts, and the right one depends on where the wait happens. timeout bounds the TCP connect and, in the 5.0.0 source, is also passed to the SSH session negotiation. banner_timeout (15 seconds by default), auth_timeout (30 seconds) and channel_timeout (an hour) cover the banner, the authentication reply and opening the channel. Reads on a channel have their own: after channel.settimeout(seconds), recv raises socket.timeout. Where a host is unreachable and silent, a short timeout ends the attempt with a timeout error. Where every address refuses or is unreachable, you get NoValidConnectionsError.

Banner errors

Before anything else, Paramiko reads the server's SSH identification line. The first line gets banner_timeout seconds, and the source then allows two seconds for each further line while it skips lines that do not start with SSH-. If the wait fails you see SSHException with Error reading SSH protocol banner and the underlying reason. If no SSH- line arrives, Paramiko raises an error that begins Indecipherable protocol version, and an identification line it cannot parse raises Invalid SSH banner.

Check the basics first: that the port is the SSH port, that an SSH service is listening on it, that the host is reachable, and what the server actually sends when you connect. A plain TCP client or ssh -v shows the first line, which should begin SSH-. Raise banner_timeout only when that evidence points to a slow banner. It does not help when something other than an SSH server is answering.

Incompatible algorithms

When the two sides share no algorithm, Paramiko raises IncompatiblePeer, a subclass of SSHException, with one of these messages: no acceptable kex algorithm, no acceptable host key, no acceptable ciphers, no acceptable macs or no acceptable compression. Turn on debug logging to see both sides' lists, then read the older devices section above. Paramiko also logs a failed negotiation, with a traceback, at error level from its transport thread, so you may see it on stderr as well as catching the exception.

Channel closed and refused requests

A closed channel does not by itself mean the device rejected your request. Normal completion, a session the device ended and a lost connection all close a channel. What differs is where you see it.

  • An explicit rejection surfaces from the call that made the request. exec_command is documented to raise SSHException if the server fails to execute the command, and invoke_shell behaves the same way. In the 5.0.0 source a failed request closes the channel, and the exception message is Channel closed.
  • A closure after the request succeeded does not raise from those calls. They return normally, recv returns zero bytes once the stream has closed, and recv_exit_status returns the status the server sent, or -1 if it sent none. Writing to a closed channel raises an error such as Socket is closed.

So the message alone does not tell you why. Look at which call raised, and whether the request had already succeeded. If a device rejects the exec request, an interactive shell is the next thing to try.

Reads that hang with invoke_shell

A read that never returns is almost always one of four things. The device is waiting at a pager (--More--), so turn paging off first. The prompt pattern never matches, because of a mode change, a confirmation question or a prompt style you did not anticipate. The command is still running and simply slow. Or recv has no timeout and nothing is arriving. Put a deadline on every read, and when it expires include the text received so far in the error, because the last few lines the device sent usually show what it is waiting for.

One more item from the FAQ: if a script hangs at shutdown, make sure you explicitly close your SSHClient. The FAQ says closing is not strictly necessary every time, but is almost always the right answer to that symptom.

Host key errors

Server '...' not found in known_hosts is the RejectPolicy doing its job on an unknown host, and BadHostKeyException means a key on record has changed. Both are covered under host keys above.

What it takes to run Paramiko scripts at enterprise scale

Paramiko is an SSH library, and it is deliberately scoped as one: it carries a secure session to a device and leaves everything above that to you. Running scripts across a whole estate means providing, integrating or maintaining several capabilities around it. Many organisations already have some of them, such as a source of truth, a scheduler, a secret store, an identity provider and central logging, so the work is often integration, not building from scratch.

The list below is a set of requirements to check, not a build plan. Which ones apply depends on your estate, your auditors and your team. This is engineering work that comes with building on a library, and none of it is a flaw in Paramiko.

  • Prompt handling and vendor quirks

    Paramiko gives you a channel, not a prompt. Teams provide prompt handling themselves or through libraries such as Netmiko, which already handles it for many platforms. Either way, paging, privilege and configuration modes, confirmation questions and banners have to keep working when an operating system release changes its output.

  • Algorithm and key compatibility across the estate

    A mixed estate has devices that negotiate different algorithms, and a Paramiko upgrade can remove one that an older device still needs. Somebody has to track which devices need which exceptions, and plan their retirement.

  • Host keys across hundreds of devices

    Strict host key checking needs a known hosts file that is correct as devices are added, replaced and rekeyed. Someone has to own that file, decide how a changed key is investigated, and avoid the shortcut of accepting everything.

  • Concurrency and rate limiting

    Paramiko's API is blocking, so concurrency usually means worker threads or processes, and an asyncio application must offload those calls. Choose concurrency limits and pacing that suit your devices and AAA servers.

  • Inventory

    Paramiko does not know which devices exist, their platforms or their addresses. At scale somebody has to keep that list correct, or point your scripts at the system that already holds the truth.

  • Scheduling

    Regular collection needs something to start it, retries for unreachable devices, and a way to see that a run finished. Cron, a CI system or an orchestrator may cover the scheduling, leaving the retries and the visibility to integrate.

  • Configuration versioning and diff

    Collecting a configuration is one step. The requirement is to keep each version with its device and time, compare versions sensibly while ignoring noise such as timestamps, and present the result in a way people will read. A Git repository, an existing archive or a dedicated tool can fill that role.

  • Compliance checks

    Checking configurations against policy means defining the rules, running them on each collection and reporting results by device and by rule. Where an auditor needs the result, it should be repeatable.

  • Role-based access

    Once more than one person uses the scripts, who may run which task against which devices needs an answer. Permissions you already have in a CI system or on a jump host may provide part of it.

  • Sign-in for people (SSO and RADIUS)

    If your automation is a service that people use, they will usually sign in through the identity provider or RADIUS server you already run. This is separate from how the scripts sign in to your network devices.

  • Credentials for devices, and rotation

    Reading device credentials from the environment is the right start. At scale the requirement is a secret store, accounts scoped per device or group, and a way to rotate credentials without breaking the scheduled jobs that use them.

  • Audit trail

    Auditors may ask who ran what, against which device, and with what result. Existing logging, CI job history or a SIEM may already capture some of it. The requirement is that the evidence exists and is kept for as long as you need it.

  • High availability

    If runs start from one host, that host becomes part of the operations process. Your existing platform for scheduled jobs may already be resilient, and the requirement is to make sure the scripts benefit from it.

  • Ownership of breakage

    Paramiko releases remove algorithms, and device software changes prompts and output. Somebody has to own noticing, fixing and testing against each new release on both sides, and deciding when a script or a dependency needs a replacement.

Using Paramiko alongside rConfig

“Alongside” means the two can coexist. rConfig can take on the configuration management responsibilities described above, and your own Python scripts can keep using Paramiko. This guide does not describe a native integration between them.

What rConfig can cover, by edition

Not every edition includes every capability. This is how the current edition comparison on the rConfig site divides the capabilities discussed in this guide.

rConfig capabilities by edition, as listed in the edition comparison
EditionAdds
rConfig Core (free, open source)Scheduled backups, Compare configuration versions, Single sign-on
Starter and abovePolicy checks and compliance status, Role-based access control, RADIUS authentication, LDAP and Active Directory, User audit log, Restore a previous configuration, Push configuration changes
Standard and aboveCheck compliance continuously, Evidence you can hand to an auditor
Enterprise / MSPDistributed collectors, High availability, Manage every rConfig instance centrally

What stays with Paramiko

rConfig does not claim to match Paramiko for scripted change or automation. Custom workflows, bespoke logic and one-off jobs are where your own scripts remain the right tool, and teams can keep them for those while rConfig handles collection, history and review. See how automation and configuration management differ and the rConfig solution overview.

When it makes sense to stop building

This is not an argument against Paramiko. It is excellent foundation software, and plenty of teams keep their scripts running alongside a platform for exactly the custom work. Whether to keep building the surrounding capabilities, adopt a platform for part of the work, or do both depends on your requirements, the engineering capacity you can give the work, what the surrounding capabilities cost to maintain, and the infrastructure you already have. If your source of truth, scheduler, secret store and logging already meet the requirements in the previous section, extending your scripts may be the best answer.

These questions can help you decide:

  • Does an auditor need evidence that your scripts do not produce today?
  • Are prompt handling and per-vendor fixes taking more of your time than the automation itself?
  • Do several teams need controlled access to the same collected configurations?
  • Is the work described in the previous section growing faster than the capacity you have to build and maintain it?

If the answers point that way, a platform that supplies some of those capabilities may be worth evaluating next to what you already run.

Evaluate rConfig Core alongside your existing scripts

rConfig V8 Core is free and open source, with no time limit and no device limit, so you can run it next to your Paramiko scripts and see which of the requirements above it covers for your estate. If you want to talk through the paid editions, you can book a demo.

Paramiko FAQ

What is Paramiko used for?

Paramiko is a pure-Python implementation of the SSHv2 protocol, providing both client and server functionality. It is used to run commands on remote hosts, hold interactive sessions, transfer files over SFTP and run an SSH server from Python. Network tools such as Netmiko build on it to reach devices over SSH. Paramiko's own description says Fabric is the maintainers' recommendation for common client use-cases, and that direct use of Paramiko is for people who need advanced or low-level primitives.

Is Paramiko still maintained?

Yes. Version 5.0.0 was released on 9 May 2026, following 4.0.0 in August 2025, and the GitHub repository shows activity after that release. Jeff Forcier maintains it with contributors. The installing page says bug fixes are guaranteed for the last two or three releases, including the latest stable one.

How do I install Paramiko?

Create a virtual environment and run pip install paramiko. Paramiko 5.0.0 needs Python 3.9 or newer. Its direct dependencies are cryptography, bcrypt, pynacl and invoke. To check the result, run python -c 'import paramiko; print(paramiko.__version__)'.

What is SSHClient in Paramiko?

SSHClient is Paramiko's high-level client class. You create one, load host keys, call connect to authenticate, and then use exec_command to run a command, invoke_shell to open an interactive session, or open_sftp for file transfer. Use it as a context manager, or call close(), so that the connection is always closed.

When should I use invoke_shell instead of exec_command?

Use exec_command when the server runs one command and returns its output and exit status, as a Linux server does. Use invoke_shell for a prompt-driven command line, where you send a command and read until the prompt returns. Many network devices need the second approach, but behaviour varies by platform and software release, so test yours. The Paramiko FAQ says the project fully supports only standard OpenSSH servers.

How do I send multiple commands to a Cisco device with Paramiko?

Open an interactive session with invoke_shell and read until the first prompt. On Cisco IOS-style platforms, turn paging off with terminal length 0. Then send each command followed by a newline and read until the prompt returns before you send the next one. Put a deadline on every read, and treat a zero-byte result from recv as the channel closing. The example in this guide shows the full pattern, and Netmiko does this prompt handling for you.

Is AutoAddPolicy safe?

Not for production. AutoAddPolicy accepts an unknown host's key without independently verifying its identity, which leaves the first connection open to impersonation. Once the key is recorded and loaded on later connections, a different key for that host raises BadHostKeyException, but that only persists if the keys are saved with save_host_keys and loaded again with load_host_keys. Paramiko's default is RejectPolicy, which refuses hosts it has no key for. Use AutoAddPolicy only in a lab you control, compare the fingerprint through a trusted, independent channel, then record the key in a known_hosts file and use RejectPolicy.

Should I use Paramiko or AsyncSSH?

It depends on the code around it. Paramiko has a blocking API and is the library that Netmiko and Fabric build on. AsyncSSH is built on asyncio and runs many connections in one event loop. Paramiko 5.0.0 needs Python 3.9 or newer and is LGPL-2.1. AsyncSSH 2.24.0 needs Python 3.10 or newer and is EPL-2.0 OR GPL-2.0-or-later. Neither is specific to network devices, so pick the one that matches the concurrency model of your code.

What are the alternatives to Paramiko?

AsyncSSH is the asyncio-based option. ssh2-python provides Python bindings for the libssh2 C library. Higher-level libraries solve different problems: Netmiko and Scrapli for network devices, and Fabric for running commands on servers. This guide does not rank them.

What licence is Paramiko under?

Paramiko is licensed under LGPL-2.1, as shown on PyPI and in the licence file in its repository. Ask your own legal adviser how it applies to the way you distribute your software.

Can I use Paramiko alongside rConfig?

Yes, they can run side by side. Teams keep their own Python scripts for custom workflows and scripted work, and can use rConfig for configuration management. rConfig Core is free and open source and covers scheduled backups, comparison between configuration versions and single sign-on. Compliance policy checks, role-based access control, RADIUS sign-in and a user audit log are in the paid editions from Starter, scheduled compliance checks and reports from Standard, and distributed collection and high availability in Enterprise and MSP. This guide does not describe a native integration between rConfig and Paramiko.

Further reading and credit

Paramiko

Related libraries

On rConfig

Thank you to Jeff Forcier, who maintains Paramiko, and to the contributors who have built and maintained it for the whole Python community. Paramiko is foundational software, and a great deal of network and server tooling stands on it.