pyguides

SSH Automation with Paramiko in Python

Paramiko is a pure-Python library that implements the SSH2 protocol. It lets you connect to SSH servers, execute commands, transfer files, and automate remote server management—all without needing external dependencies like OpenSSH.

Installing Paramiko

Install Paramiko with pip:

pip install paramiko

That is it. No system-level SSH libraries required.

Establishing an SSH Connection

The simplest way to connect to a remote server is with a username and password:

import paramiko

client = paramiko.SSHClient()
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())

client.connect(
    hostname="server.example.com",
    username="deploy",
    password="secret-password"
)

client.close()

The set_missing_host_key_policy() call tells Paramiko to automatically accept unknown host keys. For production use, you would want to load a known hosts file instead:

client.load_system_host_keys()
client.set_missing_host_key_policy(paramiko.RejectPolicy())

This rejects connections to servers whose keys are not already known, protecting against man-in-the-middle attacks. AutoAddPolicy is convenient for development and throwaway scripts, but in any environment where the server identity matters (CI/CD pipelines deploying to production, configuration management systems touching customer data), you should load known host keys and use RejectPolicy. Paramiko can also load keys from a specific known hosts file on disk rather than relying on the system default, which is useful in containerized or ephemeral environments where the standard SSH directory may not exist.

Connecting with SSH Keys

Password authentication is convenient but key-based authentication is more secure and works better for automation:

import paramiko

client = paramiko.SSHClient()
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())

# Connect using a private key file
client.connect(
    hostname="server.example.com",
    username="deploy",
    key_filename="/home/user/.ssh/id_ed25519"
)

client.close()

Paramiko supports RSA, ECDSA, and Ed25519 keys. You can also load keys directly:

private_key = ***************************************("/path/to/key")
client.connect(hostname="...", username="...", pkey=private_key)

Loading keys from memory rather than from a file path is helpful when keys are stored in a secrets manager or retrieved from an environment variable at runtime. The RSAKey.from_private_key_file method also accepts an optional password parameter for encrypted key files, which Paramiko will use to decrypt the key material before presenting it to the server. Ed25519 keys are newer and offer better security properties with smaller key sizes, but some older SSH servers may not accept them; falling back to RSA or ECDSA keys provides broader compatibility in heterogeneous server environments.

Executing Commands

Once connected, execute commands with exec_command():

import paramiko

client = paramiko.SSHClient()
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
client.connect(hostname="server.example.com", username="deploy", password="pass")

stdin, stdout, stderr = client.exec_command("uname -a")

# Read the output
output = stdout.read().decode()
error = stderr.read().decode()
exit_code = stdout.channel.recv_exit_status()

print(f"Output: {output}")
print(f"Exit code: {exit_code}")

client.close()

The exec_command() method returns three file-like objects: stdin, stdout, and stderr. The command runs in a non-interactive shell, so environment variables and shell features may not work as expected. If you need shell expansion or pipeline support, invoke a shell explicitly by passing the command as a string to bash -c rather than directly to exec_command. The exit code must be retrieved from the channel object because Paramiko does not attach it to the stdout stream; calling recv_exit_status() blocks until the remote command finishes, so call it only after you have finished reading the output streams to avoid deadlocks.

Capturing Real-Time Output

For long-running commands, you might want to see output as it happens:

import paramiko

client = paramiko.SSHClient()
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
client.connect(hostname="server.example.com", username="deploy", password="pass")

stdin, stdout, stderr = client.exec_command("for i in 1 2 3 4 5; do echo \"Count: $i\"; sleep 1; done")

# Read line by line as output arrives
for line in stdout:
    print(line.rstrip())

client.close()

This pattern is useful for deployment scripts, build processes, or any command that produces output over time. Iterating line by line keeps memory usage low regardless of how much data the remote command emits, and the rstrip call removes the trailing newline that each line already carries. For commands that produce binary output, you should read in bytes mode and decode selectively rather than iterating as text lines. Paramiko also supports setting a timeout on the channel so that a stuck remote process does not hang your automation script indefinitely.

Working with SFTP

Paramiko includes SFTP for secure file transfers:

import paramiko

client = paramiko.SSHClient()
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
client.connect(hostname="server.example.com", username="deploy", password="pass")

# Open an SFTP session
sftp = client.open_sftp()

# Download a file
sftp.get("/remote/file.txt", "local_file.txt")

# Upload a file
sftp.put("local_script.py", "/remote/script.py")

# List directory contents
for filename in sftp.listdir("/remote/path"):
    print(filename)

sftp.close()
client.close()

SFTP operations mirror Python file operations closely, making it intuitive for anyone familiar with built-in file handling. The get and put methods transfer entire files in a single call, but for large files you can open remote file handles and read or write in chunks to control memory usage and report transfer progress. The listdir method only returns filenames; it does not include file metadata like size or modification time, so you will need to call stat on individual entries if you need that information. SFTP sessions operate over the already-authenticated SSH transport, so no additional credentials are needed beyond the initial connection.

Managing Directories and Files

SFTP provides directory and file management methods:

import paramiko

client = paramiko.SSHClient()
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
client.connect(hostname="server.example.com", username="deploy", password="pass")

sftp = client.open_sftp()

# Create a directory
sftp.mkdir("/remote/new_directory")

# Remove a directory
sftp.rmdir("/remote/empty_directory")

# Delete a file
sftp.remove("/remote/file_to_delete")

# Rename a file
sftp.rename("/remote/old_name.txt", "/remote/new_name.txt")

# Get file attributes
stat = sftp.stat("/remote/file.txt")
print(f"Size: {stat.st_size}, Permissions: {oct(stat.st_mode)}")

sftp.close()
client.close()

These operations map directly to traditional FTP commands. The stat call returns an SFTPAttributes object that includes file size, permissions, modification time, and ownership information, the same metadata you would get from running ls -l on the remote server. One subtlety worth noting is that rmdir requires the directory to be empty, and trying to remove a non-empty directory will raise an IOError rather than performing a recursive deletion. If you need to delete a directory tree, you must walk its contents with listdir and remove files individually before removing each subdirectory.

Context Manager Pattern

Use context managers to ensure connections close properly, even if errors occur:

import paramiko
from contextlib import contextmanager

@contextmanager
def ssh_connection(hostname, username, password=None, key_filename=None):
    client = paramiko.SSHClient()
    client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
    
    try:
        client.connect(
            hostname=hostname,
            username=username,
            password=password,
            key_filename=key_filename
        )
        yield client
    finally:
        client.close()

# Usage
with ssh_connection("server.example.com", "deploy", password="pass") as client:
    stdin, stdout, stderr = client.exec_command("uptime")
    print(stdout.read().decode())

This pattern prevents connection leaks in long-running automation scripts.

Handling Multiple Servers

A common pattern is running the same command across multiple servers:

import paramiko
from concurrent.futures import ThreadPoolExecutor

def run_on_server(server_info, command):
    hostname, username, password = server_info
    
    client = paramiko.SSHClient()
    client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
    
    try:
        client.connect(hostname=hostname, username=username, password=password)
        stdin, stdout, stderr = client.exec_command(command)
        output = stdout.read().decode()
        return f"{hostname}: {output.strip()}"
    finally:
        client.close()

# Run command on multiple servers
servers = [
    ("server1.example.com", "deploy", "password1"),
    ("server2.example.com", "deploy", "password2"),
    ("server3.example.com", "deploy", "password3"),
]

with ThreadPoolExecutor(max_workers=5) as executor:
    results = list(executor.map(lambda s: run_on_server(s, "uptime"), servers))
    
for result in results:
    print(result)

The thread pool lets you run commands in parallel. Paramiko is thread-safe for concurrent connections to different servers, but you must create separate client instances for each connection; sharing a single SSHClient across threads leads to protocol errors and corrupted data streams. The max_workers parameter controls how many servers you contact simultaneously, which is useful for limiting load when running against dozens or hundreds of machines. For very large server fleets, consider batching into groups of twenty to fifty servers per pool to keep connection overhead reasonable.

SSH Tunneling (Port Forwarding)

Paramiko supports SSH tunneling for secure port forwarding:

import paramiko

client = paramiko.SSHClient()
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
client.connect(hostname="jump-server.example.com", username="deploy", password="pass")

# Forward local port 9000 to remote port 5432
transport = client.get_transport()
forwarder = transport.open_channel(
    "direct-tcpip",
    ("localhost", 5432),
    ("127.0.0.1", 9000)
)

forwarder.close()
client.close()

This is useful for accessing services behind a jump server or tunneling database connections securely.

Error Handling

Production-ready scripts need proper error handling:

import paramiko
from paramiko.ssh_exception import SSHException, AuthenticationException

def safe_ssh_command(hostname, username, password, command):
    client = paramiko.SSHClient()
    client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
    
    try:
        client.connect(hostname=hostname, username=username, password=password)
    except AuthenticationException:
        return f"Authentication failed for {hostname}"
    except SSHException as e:
        return f"SSH error for {hostname}: {e}"
    
    try:
        stdin, stdout, stderr = client.exec_command(command)
        exit_code = stdout.channel.recv_exit_status()
        
        if exit_code != 0:
            error = stderr.read().decode()
            return f"Command failed on {hostname}: {error}"
        
        return stdout.read().decode()
    except Exception as e:
        return f"Error executing command: {e}"
    finally:
        client.close()

This handles authentication failures, SSH errors, and command failures separately.

Security Best Practices

When using Paramiko in production:

  1. Never hardcode passwords — Use environment variables or a secrets manager
  2. Validate host keys — Use RejectPolicy with a known hosts file
  3. Use key-based auth — It is more secure than passwords
  4. Limit permissions — The SSH user should have only the needed permissions
  5. Log access — Track who connected and what commands ran

Next Steps

You now know how to connect to remote servers, execute commands, transfer files, and handle errors programmatically. Combined with what you learned about subprocess, you can build complete deployment and server management automation.

The next tutorial in this series covers AWS automation with Boto3, which pairs well with Paramiko for infrastructure that spans both servers and cloud services.

See Also