SSH Key Management in Bash
Generate, rotate, and distribute SSH keys with bash scripts
Overview
SSH keys are the standard way to authenticate to remote servers. Managing them manually across multiple machines is error-prone. This approach handles generating ed25519 keys, distributing them to servers, rotating compromised keys, and disabling password authentication.
When to Use
- You need to generate SSH keys for new team members
- You want to distribute your public key to multiple servers at once
- You need to rotate keys after a security incident
- You are hardening server security by disabling password auth
Solution
Generate an ed25519 key
#!/bin/bash
KEY_NAME="${1:-default}"
KEY_PATH="$HOME/.ssh/${KEY_NAME}"
if [ -f "$KEY_PATH" ]; then
echo "Key already exists: $KEY_PATH"
exit 1
fi
ssh-keygen -t ed25519 -C "$(whoami)@$(hostname)-${KEY_NAME}" -f "$KEY_PATH" -N ""
echo "Generated: $KEY_PATH"
echo "Public key: ${KEY_PATH}.pub"
Distribute public key to multiple servers
#!/bin/bash
PUBLIC_KEY="$HOME/.ssh/id_ed25519.pub"
SERVERS=(
"user@server1.example.com"
"user@server2.example.com"
"user@server3.example.com"
)
for server in "${SERVERS[@]}"; do
echo "Distributing key to $server..."
ssh-copy-id -i "$PUBLIC_KEY" "$server"
done
echo "Key distributed to ${#SERVERS[@]} servers"
Distribute key without ssh-copy-id
#!/bin/bash
PUBLIC_KEY=$(cat "$HOME/.ssh/id_ed25519.pub")
SERVERS_FILE="servers.txt"
while IFS= read -r server; do
[ -z "$server" ] && continue
echo "Adding key to $server..."
ssh "$server" "mkdir -p ~/.ssh && chmod 700 ~/.ssh && echo '$PUBLIC_KEY' >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys"
done < "$SERVERS_FILE"
Rotate compromised keys
#!/bin/bash
OLD_KEY="$1"
SERVERS_FILE="servers.txt"
if [ -z "$OLD_KEY" ]; then
echo "Usage: $0 <old-public-key-string>"
exit 1
fi
# Remove old key from all servers
while IFS= read -r server; do
[ -z "$server" ] && continue
echo "Removing old key from $server..."
ssh "$server" "sed -i '/${OLD_KEY}/d' ~/.ssh/authorized_keys"
done < "$SERVERS_FILE"
# Generate new key
ssh-keygen -t ed25519 -C "$(whoami)@$(hostname)-rotated" -f "$HOME/.ssh/id_ed25519_rotated" -N ""
echo "Old key removed. New key: $HOME/.ssh/id_ed25519_rotated"
echo "Distribute the new key with the distribute script."
Disable password authentication
#!/bin/bash
SSHD_CONFIG="/etc/ssh/sshd_config"
# Backup original config
cp "$SSHD_CONFIG" "${SSHD_CONFIG}.bak"
# Disable password auth
sed -i 's/^#*PasswordAuthentication.*/PasswordAuthentication no/' "$SSHD_CONFIG"
sed -i 's/^#*ChallengeResponseAuthentication.*/ChallengeResponseAuthentication no/' "$SSHD_CONFIG"
sed -i 's/^#*PubkeyAuthentication.*/PubkeyAuthentication yes/' "$SSHD_CONFIG"
# Restart SSH
systemctl restart sshd
echo "Password authentication disabled. Key-based auth only."
Audit SSH keys on a server
#!/bin/bash
echo "=== SSH Key Audit ==="
echo "Date: $(date)"
echo ""
for user_home in /home/* /root; do
user=$(basename "$user_home")
auth_file="${user_home}/.ssh/authorized_keys"
if [ -f "$auth_file" ]; then
key_count=$(wc -l < "$auth_file")
echo "User: $user ($key_count keys)"
while IFS= read -r key; do
[ -z "$key" ] && continue
key_type=$(echo "$key" | awk '{print $1}')
key_comment=$(echo "$key" | awk '{print $3}')
key_fingerprint=$(echo "$key" | ssh-keygen -lf - 2>/dev/null | awk '{print $1}')
echo " - $key_type | $key_fingerprint | $key_comment"
done < "$auth_file"
echo ""
fi
done
List all SSH keys on local machine
#!/bin/bash
echo "=== Local SSH Keys ==="
for key in ~/.ssh/id_* ~/.ssh/*.pub; do
[ -f "$key" ] || continue
if [[ "$key" == *.pub ]]; then
fingerprint=$(ssh-keygen -lf "$key" 2>/dev/null)
echo "Public: $key"
echo " $fingerprint"
fi
done
Explanation
Ed25519 keys are smaller, faster, and more secure than RSA. A 256-bit ed25519 key provides equivalent security to a 3072-bit RSA key. Always prefer ed25519 for new keys.
ssh-copy-id appends your public key to ~/.ssh/authorized_keys on the remote server. It handles permission setup (700 for ~/.ssh, 600 for authorized_keys). If ssh-copy-id is not available, you can do the same with a manual ssh command.
Key rotation removes the old public key from authorized_keys files across all servers. The sed -i command deletes any line containing the old key string. After removal, generate a new key and distribute it.
Disabling password authentication is the single most effective SSH hardening step. It eliminates brute-force password attacks entirely.
Variants
| Approach | Key Type | Use When |
|---|---|---|
| ed25519 | Ed25519 | New keys, modern servers |
| RSA 4096 | RSA | Legacy servers without ed25519 support |
| ECDSA | ECDSA | Specific compliance requirements |
| Hardware key (FIDO2) | Ed25519-SK | High-security environments with YubiKey |
Guidelines
- Use ed25519 for all new keys. It is faster and more secure than RSA.
- Set a passphrase on private keys. Use
ssh-agentto avoid typing it repeatedly. - Use
~/.ssh/configto manage multiple keys and server aliases. - Rotate keys after team member departures or suspected compromises.
- Audit
authorized_keysfiles quarterly. Remove keys for users who no longer need access.
Common Mistakes
- Using RSA keys shorter than 3072 bits. RSA 2048 is no longer recommended for long-term use.
- Leaving private keys without a passphrase. Anyone with file access can use the key.
- Not removing old keys from
authorized_keysafter rotation. The compromised key remains valid. - Disabling password auth before verifying key-based login works. You lock yourself out.
- Sharing private keys between users. Each user should have their own key pair.
Advanced Solutions
SSH config with jump host (bastion)
Connect to internal servers through a bastion host without exposing them to the internet. The ProxyJump directive chains SSH connections:
# Bastion host (reachable from internet)
Host bastion
HostName bastion.example.com
User deploy
IdentityFile ~/.ssh/id_ed25519_bastion
Port 2222
# Internal servers (only reachable via bastion)
Host *.internal.example.com
User deploy
IdentityFile ~/.ssh/id_ed25519_internal
ProxyJump bastion
# Disable agent forwarding to bastion for security
ForwardAgent no
# Database server (two-hop: bastion -> jump -> db)
Host db-prod.internal.example.com
User dbadmin
IdentityFile ~/.ssh/id_ed25519_db
ProxyJump jump-vm.internal.example.com
FIDO2 hardware-backed SSH keys (YubiKey)
Generate SSH keys that require physical hardware presence. The private key never leaves the YubiKey:
#!/bin/bash
# Generate a FIDO2-backed ed25519 key
# Requires OpenSSH 8.2+ and a FIDO2-compatible device (YubiKey 5 Series)
ssh-keygen -t ed25519-sk -C "$(whoami)@$(hostname)-fido2" -f "$HOME/.ssh/id_ed25519_sk"
# With a passphrase and requiring touch for every use
ssh-keygen -t ed25519-sk -O resident -O verify-required \
-C "$(whoami)@$(hostname)-fido2-strict" \
-f "$HOME/.ssh/id_ed25519_sk_strict"
echo "FIDO2 key generated. Touch YubiKey when prompted during use."
# Resident key can be recovered on a new machine
# ssh-keygen -K # downloads resident keys from FIDO2 device
SSH Certificate Authority (CA) for key signing
Instead of distributing public keys to every server, set up an SSH CA. Servers trust the CA, and users get signed certificates with expiration:
#!/bin/bash
# === On the CA host ===
# Generate CA key pair (do this once, keep the private key secure)
ssh-keygen -t ed25519 -f /etc/ssh/ca_key -N "" -C "SSH CA"
# === On each server (trust the CA) ===
# Add CA public key to trusted user CA
echo "TrustedUserCAKeys /etc/ssh/ca_key.pub" >> /etc/ssh/sshd_config
systemctl restart sshd
# === Sign a user's public key (on CA host) ===
# Sign with validity period and principal restrictions
ssh-keygen -s /etc/ssh/ca_key \
-I "user-$(date +%Y%m%d)" \
-n deploy,admin \
-V +1d \
~/.ssh/id_ed25519.pub
# This generates ~/.ssh/id_ed25519-cert.pub
# The user connects with their key + certificate:
# ssh -i ~/.ssh/id_ed25519 user@server
# The certificate is valid for 1 day and only for principals 'deploy' and 'admin'
Bulk key rotation across server fleet
Rotate keys across hundreds of servers using a parallel SSH tool and a rotation manifest:
#!/bin/bash
# rotation-manifest.txt format: server old_key_fingerprint new_key_path
MANIFEST="rotation-manifest.txt"
KEYS_DIR="$HOME/.ssh"
while IFS=$'\t' read -r server old_fp new_key; do
[ -z "$server" ] && continue
echo "Rotating key on $server..."
# Remove old key by fingerprint
ssh "$server" "ssh-keygen -R -f ~/.ssh/authorized_keys -F '$old_fp'" 2>/dev/null
# Add new key
NEW_PUB=$(cat "${KEYS_DIR}/${new_key}.pub")
ssh "$server" "echo '$NEW_PUB' >> ~/.ssh/authorized_keys"
echo " Done: $server"
done < "$MANIFEST"
echo "Rotation complete for $(wc -l < "$MANIFEST") servers" Frequently Asked Questions
How do I use a specific key for a specific server?
Use ~/.ssh/config:
Host server1.example.com
IdentityFile ~/.ssh/id_ed25519_server1
User deploy
Host *.internal
IdentityFile ~/.ssh/id_ed25519_internal
User admin
How do I set up SSH agent forwarding safely?
Add the way to the agent:
eval "$(ssh-agent -s)"
ssh-add ~/.ssh/id_ed25519
Then connect with -A to forward the agent. Only use agent forwarding with trusted servers. A compromised server can use your forwarded agent to connect to other servers as you.
How do I enforce key-based auth without locking myself out?
Test key login in a second terminal before disabling passwords:
# Terminal 1: keep this open
ssh user@server
# Terminal 2: test key login
ssh -o PasswordAuthentication=no user@server
# If that works, disable passwords in Terminal 1
What is the difference between authorized_keys and known_hosts?
authorized_keys lists public keys that can log into this machine. known_hosts lists servers this machine has connected to. They serve opposite purposes: one controls inbound access, the other validates outbound connections.
Related Resources
SSH Key Management
Generate, rotate, and distribute SSH keys securely with Bash scripts for team and server access.
DocSSL Certificate Management Template
A template for tracking TLS/SSL certificate inventory, renewals, deployments, and expiration risks across domains and services.
DocAPI Security Review Template
A checklist template for reviewing API authentication, rate limiting, and OWASP compliance.
GuideAPI Security Checklist — Authentication to Encryption
A thorough security checklist for APIs: authentication, authorization, input validation, rate limiting, encryption, logging, and deployment hardening.
GuideSecurity Best Practices Guide
A thorough guide to application security: authentication, authorization, input validation, secrets management, and common vulnerability prevention.
RecipeConfigure iptables Firewall Rules with Bash
Set up basic firewall rules with iptables and bash scripts