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
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_rsapath 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:
- Generating a new key pair (private + public) or loading an existing private key.
- Inspecting key properties like type, bit length, fingerprint, and comment.
- Writing the keys to files in the correct format, or passing them in memory to other tools.
- 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 withssh-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
0o600on private keys; otherwise, SSH clients on Unix will refuse to use them. Your Python script can do that withos.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-keygenviasubprocess, 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, tryload_ssh_private_key()for OpenSSH format orload_pem_private_key()for PEM. -
OSError: [Errno 13] Permission deniedwhen using the key withssh— Your private key file has too-open permissions. Fix withchmod 600 id_rsa(oros.chmodin Python). -
Key works locally but not on the server — The public key is not in the server's
~/.ssh/authorized_keys. Copy it withssh-copy-idor 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 toparamikowith 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; usecryptographyinstead. - Forgetting to set
0o600permissions on private key files, causing SSH clients to refuse the key. - Hardcoding the path to
~/.ssh/id_rsa— usePath.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
- Use
paramikoto load keys directly when you need to establish SSH connections in the same script. - Prefer Ed25519 keys over RSA for modern performance and security, but confirm server compatibility.
- Use
subprocesswithssh-keygenonly 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_keysacross hundreds of servers.
Key takeaways
- Use
cryptographyto generate, load, and verify SSH keys in pure Python. - Always encrypt private keys with a passphrase and set file permissions to
0o600on 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
paramikowhen you need to actually connect via SSH, but stick withcryptographyfor key-only operations. - Avoid shelling out to
ssh-keygen— it's brittle and hard to maintain.
Keep learning
Related tutorials, quizzes, and articles for this topic.
Discussion
Questions, corrections, and tips help everyone reading this page.
0 comments
Add a comment
No comments yet — start the thread.