Manage SSH Keys from Python

Learn to manage SSH keys from Python in this hands-on DevOps tutorial — generate, load, and verify keys, automate key handling, and avoid common pitfalls.

Focus: manage ssh keys from python

Sponsored

You've automated deployments, spun up cloud instances, and scripted your way around servers — but somewhere in that pipeline, a hardcoded path to ~/.ssh/id_rsa or a fragile subprocess.run(["ssh-keygen", ...]) is waiting to break your Monday. Managing SSH keys from Python isn't just about generating a key pair; it's about making your automation portable, secure, and predictable. This lesson shows you how to generate, load, inspect, and securely handle SSH keys directly in your Python code — no more shelling out to ssh-keygen and parsing error strings.

The problem this lesson solves

DevOps automation inevitably needs SSH keys: to connect to a fleet of servers, to sign commits, or to provision cloud instances. The moment you try to script key management with ssh-keygen and sed, you hit a wall of brittle parsing, platform-specific paths, and security slips like accidentally logging a private key.

Here's the pain you've likely felt:

  • Inconsistent key formats — OpenSSH vs. PEM vs. PKCS#8, and which one does the remote server actually want?
  • Manual key lifecycle — someone has to remember to rotate keys or revoke old ones.
  • Embedded secrets in scripts — private keys hardcoded in config files or passed as environment variables that end up in logs.
  • Portability — your ~/.ssh/id_rsa path works on your Mac, but the CI runner is a container with no home directory.

By the end of this lesson, you'll be able to manage SSH keys from Python — generate, load, inspect, and validate them — and integrate that into your automation scripts with confidence.

Core concept / mental model

Think of an SSH key pair as a digital lock and key: the private key is your physical key (never share it), and the public key is a lock that anyone can install on a server. When you connect, the server checks that your private key can unlock the public key it holds. The same pair can be reused across many servers — you just install the public key on each one.

In Python, you manage SSH keys by:

  1. Generating a new key pair (private + public) or loading an existing private key.
  2. Inspecting key properties like type, bit length, fingerprint, and comment.
  3. Writing the keys to files in the correct format, or passing them in memory to other tools.
  4. Verifying that a public key matches a private key, and that permissions on key files are correct.

Pro tip: The private key is a secret — treat it like a password. Never print it, never commit it, and never pass it over an unsecured channel. If in doubt, load the key and use it in memory, but don't write it to a log.

The most common Python library for this is cryptography, but you also have paramiko if you need SSH connections. For pure key management, cryptography is the standard choice.

How it works step by step

Let's trace the lifecycle of an SSH key in Python, from generation to validation.

Step 1: Install the library

You'll need cryptography (and optionally paramiko for SSH connections). Install with pip:

pip install cryptography

Step 2: Generate a key pair

The cryptography library's rsa module gives you a clean API to generate an RSA key. You can also choose Ed25519 for modern, faster, and more secure keys.

from cryptography.hazmat.primitives.asymmetric import rsa, ed25519
from cryptography.hazmat.primitives import serialization

# Generate an RSA private key (2048-bit is the minimum recommended)
rsa_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)

# Or generate an Ed25519 private key (modern, secure)
ed_key = ed25519.Ed25519PrivateKey.generate()

Step 3: Serialize the keys to files

You need to write the private key in PEM format (with encryption if you want) and the public key in OpenSSH format (what you put on servers).

# Serialize the private key to PEM, optionally with a passphrase
private_bytes = rsa_key.private_bytes(
    encoding=serialization.Encoding.PEM,
    format=serialization.PrivateFormat.TraditionalOpenSSL,
    encryption_algorithm=serialization.BestAvailableEncryption(b"my-secret-passphrase")
    # or serialization.NoEncryption()
)

with open("id_rsa", "wb") as f:
    f.write(private_bytes)

# Serialize the public key in OpenSSH format
public_bytes = rsa_key.public_key().public_bytes(
    encoding=serialization.Encoding.OpenSSH,
    format=serialization.PublicFormat.OpenSSH
)

with open("id_rsa.pub", "wb") as f:
    f.write(public_bytes)

Step 4: Load an existing private key

To use an existing key, load it from a file and decrypt it if needed.

from cryptography.hazmat.primitives import serialization

with open("id_rsa", "rb") as f:
    key_data = f.read()

private_key = serialization.load_pem_private_key(
    key_data,
    password=b"my-secret-passphrase"  # or None if no passphrase
)

Step 5: Verify key pair match

A common need is to check that a given private key corresponds to a public key file (e.g., before you push to a server). You can extract the public key from the private one and compare it with the .pub file.

public_key_from_private = private_key.public_key()
public_bytes = public_key_from_private.public_bytes(
    encoding=serialization.Encoding.OpenSSH,
    format=serialization.PublicFormat.OpenSSH
)

with open("id_rsa.pub", "rb") as f:
    existing_pub = f.read().strip()

if public_bytes.strip() == existing_pub:
    print("Keys match!")
else:
    print("Keys do NOT match!")

Step 6: Rotate and revoke

Key rotation means generating a new pair and distributing the new public key. You can automate that generation and even write a script to add the new public key to authorized_keys on remote servers (but that's a separate step).

Hands-on walkthrough

Let's put it together in a complete script that generates a key pair, writes it to a specified directory, and verifies the files are correct.

#!/usr/bin/env python3
"""Generate an SSH key pair with Python and verify it."""

import os
from pathlib import Path
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.primitives import serialization


def generate_ssh_key_pair(output_dir: str, key_name: str = "id_rsa", passphrase: bytes = b""):
    """Generate RSA key pair and write to output_dir."""
    # Create directory if needed
    Path(output_dir).mkdir(parents=True, exist_ok=True)

    # Generate private key
    key = rsa.generate_private_key(public_exponent=65537, key_size=2048)

    # Serialize private key
    encryption = serialization.BestAvailableEncryption(passphrase) if passphrase else serialization.NoEncryption()
    private_bytes = key.private_bytes(
        encoding=serialization.Encoding.PEM,
        format=serialization.PrivateFormat.TraditionalOpenSSL,
        encryption_algorithm=encryption
    )

    # Serialize public key
    public_bytes = key.public_key().public_bytes(
        encoding=serialization.Encoding.OpenSSH,
        format=serialization.PublicFormat.OpenSSH
    )

    # Write files
    priv_path = Path(output_dir) / key_name
    pub_path = Path(output_dir) / f"{key_name}.pub"

    with open(priv_path, "wb") as f:
        f.write(private_bytes)
    with open(pub_path, "wb") as f:
        f.write(public_bytes)

    # Set restrictive permissions on private key
    os.chmod(priv_path, 0o600)

    print(f"Private key written to {priv_path}")
    print(f"Public key written to {pub_path}")

    return priv_path, pub_path


if __name__ == "__main__":
    generate_ssh_key_pair("./my_keys", "deploy_key", b"s3cret")

Expected output (file contents):

  • my_keys/deploy_key — a PEM-encoded RSA private key with a passphrase.
  • my_keys/deploy_key.pub — a line starting with ssh-rsa AAAAB3Nza....

Now, let's load that key back and verify the public key matches:

from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import rsa
import os

# Load private key (assuming you know the passphrase)
with open("my_keys/deploy_key", "rb") as f:
    priv = serialization.load_pem_private_key(f.read(), password=b"s3cret")

# Derive public key and compare with the .pub file
pub_bytes = priv.public_key().public_bytes(
    encoding=serialization.Encoding.OpenSSH,
    format=serialization.PublicFormat.OpenSSH
)

with open("my_keys/deploy_key.pub", "rb") as f:
    stored_pub = f.read().strip()

assert pub_bytes.strip() == stored_pub, "Public key mismatch!"
print("Key pair verified successfully.")

If you want to connect to a server using your Python-managed key (e.g., to test it), you can use paramiko:

import paramiko

ssh = paramiko.SSHClient()
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
ssh.connect(
    hostname="192.168.1.100",
    username="deploy",
    key_filename="my_keys/deploy_key",
    passphrase="s3cret"
)  # Note: passphrase may be needed if key is encrypted
stdin, stdout, stderr = ssh.exec_command("uptime")
print(stdout.read().decode())
ssh.close()

Pro tip: Always set file permissions 0o600 on private keys; otherwise, SSH clients on Unix will refuse to use them. Your Python script can do that with os.chmod() right after writing the file.

Compare options / when to choose what

There are two main libraries and several key types. Here's a quick comparison:

Library / Key Type Best for Pros Cons
cryptography (RSA) Classic key management, wide compatibility Standard, well-documented Slower key generation, larger key size
cryptography (Ed25519) Modern security, speed, minimalist keys Fast, secure, small Some legacy servers may not support it
paramiko SSH connections and remote automation Built-in key handling + SFTP Heavier, not needed for key management alone
subprocess + ssh-keygen Quick and dirty, but not recommended No extra dependency Brittle parsing, platform-dependent, security risks

When to choose what?

  • If you're only managing keys (generating, loading, verifying) — use cryptography. It's clean and pure Python.
  • If you need to actually connect via SSH in the same script — use paramiko. It can load keys directly and handle authentication.
  • If you're in a constrained environment and can't install dependencies — fall back to ssh-keygen via subprocess, but wrap it carefully with exception handling and parse the output with regex.

Troubleshooting & edge cases

  • ValueError: Could not deserialize key data — You're trying to load a key that is not in PEM format, or you provided the wrong password. Check the file start: it should begin with -----BEGIN ... PRIVATE KEY-----. If not, try load_ssh_private_key() for OpenSSH format or load_pem_private_key() for PEM.

  • OSError: [Errno 13] Permission denied when using the key with ssh — Your private key file has too-open permissions. Fix with chmod 600 id_rsa (or os.chmod in Python).

  • Key works locally but not on the server — The public key is not in the server's ~/.ssh/authorized_keys. Copy it with ssh-copy-id or manually append.

  • Ed25519 keys fail on older servers — Make sure the server's SSH version supports Ed25519 (usually OpenSSH 6.5+). If not, use RSA.

  • Passphrase prompt during automation — If you use an encrypted key, your automation will hang waiting for input. Either use an ssh-agent with ssh-add, or load the key in Python and pass it to paramiko with the passphrase programmatically.

What you learned & what's next

You now know the core idea behind managing SSH keys from Python: using the cryptography library to generate, serialize, load, and verify key pairs — a fundamental skill for any DevOps automator. You practiced generating a key pair, writing it to disk with proper permissions, loading it back, and verifying the public match. You also learned how to compare libraries (cryptography vs. paramiko) and troubleshoot common issues like format mismatches and permissions.

Next in the track, you'll likely move on to using these keys to automate remote commands — for example, running scripts on a fleet of servers via paramiko or fabric. That's the natural evolution: from key management to key-driven automation.

Keep this lesson in your toolkit — every time you bootstrap a new server or CI pipeline, you'll reach for this pattern.

Practice recap

Mini-exercise: Write a Python script that loads an existing private key (e.g., ~/.ssh/id_rsa), prints the key type and fingerprint, and then checks if the derived public key matches the id_rsa.pub file. Modify the script to accept a passphrase from the environment variable SSH_KEY_PASSPHRASE without printing it. This builds your confidence in handling secrets safely.

Common mistakes

  • Using subprocess.run(["ssh-keygen", ...]) and parsing stdout — brittle and platform-dependent; use cryptography instead.
  • Forgetting to set 0o600 permissions on private key files, causing SSH clients to refuse the key.
  • Hardcoding the path to ~/.ssh/id_rsa — use Path.home() or environment variables for portability.
  • Passing an encrypted private key to paramiko.connect() without the passphrase argument, causing a hang or auth failure.

Variations

  1. Use paramiko to load keys directly when you need to establish SSH connections in the same script.
  2. Prefer Ed25519 keys over RSA for modern performance and security, but confirm server compatibility.
  3. Use subprocess with ssh-keygen only when you can't add Python dependencies, and wrap it with strict error handling.

Real-world use cases

  • Automating the creation of a new deploy key for each application environment and storing the public key in a central config.
  • A CI pipeline that generates a key pair on the fly to authenticate to a production server and securely passes the private key via a secret store.
  • A Python-based infrastructure tool that validates existing SSH key pairs before rolling out updates to authorized_keys across hundreds of servers.

Key takeaways

  • Use cryptography to generate, load, and verify SSH keys in pure Python.
  • Always encrypt private keys with a passphrase and set file permissions to 0o600 on Unix systems.
  • Public keys are stored in OpenSSH format (ssh-rsa ...), while private keys are usually PEM-encoded.
  • Verify that a public key matches a private key before deployment to avoid connectivity surprises.
  • Choose paramiko when you need to actually connect via SSH, but stick with cryptography for key-only operations.
  • Avoid shelling out to ssh-keygen — it's brittle and hard to maintain.

Sponsored

Sponsored

Discussion

Questions, corrections, and tips help everyone reading this page.

0 comments

Add a comment

Shown publicly with your comment.

Be constructive · max 4,000 characters

No comments yet — start the thread.

Related tutorials, quizzes, and articles for this topic.