Automating SSH with Bash: Managing Multiple Servers in DevOps
Comprehensive guide to automating SSH with Bash for multi-server management: ssh-keygen, ssh-copy-id, ~/.ssh/config, scp, rsync, and parallel remote execution with & and wait.
In the previous post, we explored monitoring system resources and managing processes on a single server. However, in real-world DevOps, you rarely manage just one machine. As infrastructure scales to 10, 50, or even hundreds of servers, manually SSHing into each machine to run commands becomes a nightmare—slow, error-prone, and impossible to reproduce consistently.
Automating SSH with Bash solves this: write once, run across every server, with consistent results that can integrate into CI/CD pipelines or cron jobs. This article covers setting up SSH key-based authentication, configuring ~/.ssh/config for multi-server management, transferring files securely with scp and rsync, and includes a practical script for parallel remote execution across multiple servers.
Setting Up SSH Key-Based Authentication
SSH key-based authentication eliminates password prompts on every connection—a mandatory requirement for any automation script.
Generate an SSH Key Pair
1
2
3
4
5
# Generate RSA 4096-bit keyssh-keygen -t rsa -b 4096 -f ~/.ssh/id_rsa_deploy -C "deploy@automation"# Or use Ed25519 (faster, more secure)ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519_deploy -C "deploy@automation"
Note: If you use a key for automation, set an empty passphrase (press Enter when prompted) or use ssh-agent to manage passphrases. However, in CI/CD environments, use a passphrase-less key and protect it with filesystem permissions.
Copy the Key to Target Servers
1
2
3
4
5
6
# Standard method using ssh-copy-idssh-copy-id -i ~/.ssh/id_ed25519_deploy.pub user@server1
ssh-copy-id -i ~/.ssh/id_ed25519_deploy.pub user@server2
# Manual method if ssh-copy-id is unavailablecat ~/.ssh/id_ed25519_deploy.pub | ssh user@server1 "mkdir -p ~/.ssh && cat >> ~/.ssh/authorized_keys"
# ~/.ssh/config
# Staging servers
Host staging-web
HostName 192.168.1.10
User deploy
IdentityFile ~/.ssh/id_ed25519_deploy
Port 22
Host staging-db
HostName 192.168.1.11
User deploy
IdentityFile ~/.ssh/id_ed25519_deploy
Port 22
# Production servers
Host prod-web
HostName 10.0.0.10
User deploy
IdentityFile ~/.ssh/id_ed25519_deploy
Port 2222
Host prod-db
HostName 10.0.0.11
User deploy
IdentityFile ~/.ssh/id_ed25519_deploy
Port 2222
# Global defaults
Host *
ServerAliveInterval 60
ServerAliveCountMax 3
StrictHostKeyChecking accept-new
LogLevel ERROR
After configuration, access servers using short host names:
1
2
3
4
5
# Instead of: ssh [email protected]ssh staging-web "df -h /"# Or add a Bash aliasecho"alias prod-web='ssh prod-web'" >> ~/.bashrc
Key configuration explained:
ServerAliveInterval 60: Send keepalive every 60 seconds to prevent disconnection.
ServerAliveCountMax 3: Disconnect after 3 failed keepalive attempts.
StrictHostKeyChecking accept-new: Automatically accept new host keys without prompting.
LogLevel ERROR: Show only errors, reducing verbosity in scripts.
Remote Command Execution
Basic Commands
1
2
3
4
5
6
7
8
# Run a single command on a remote serverssh user@server1 "uptime"# Run multiple commands sequentiallyssh user@server1 "cd /opt/app && git pull && systemctl restart myapp"# Run commands with sudossh user@server1 "sudo systemctl status nginx"
Passing Environment Variables
1
2
3
4
5
6
7
8
9
10
11
# Pass variables into SSH sessionDEPLOY_ENV="staging"APP_VERSION="v2.1.0" ssh user@server1 "echo \$DEPLOY_ENV \$APP_VERSION"# Safer approach using heredocssh user@server1 << 'REMOTE_SCRIPT'
export DEPLOY_ENV="staging"
cd /opt/app
git pull origin main
npm install --production
systemctl restart myapp
REMOTE_SCRIPT
Handling Remote Execution Errors
1
2
3
4
5
6
7
# Check exit codeif ssh user@server1 "systemctl status nginx" 2>/dev/null;thenecho"Nginx is running on server1"elseecho"Nginx is down on server1 — attempting restart" ssh user@server1 "sudo systemctl restart nginx"fi
File Transfer Between Servers
scp — Secure Copy
1
2
3
4
5
6
7
8
9
10
11
# Copy file from local to remotescp /local/path/config.yml user@server1:/opt/app/config.yml
# Copy directory recursivelyscp -r /local/path/dist/ user@server1:/opt/app/dist/
# Copy from remote to localscp user@server1:/var/log/app.log /local/logs/
# Copy between two servers (via local intermediary)scp user@server1:/data/backup.sql user@server2:/data/restore.sql
rsync — Remote Sync
rsync is more powerful than scp because it supports incremental synchronization—only transferring changed files:
1
2
3
4
5
6
7
8
9
10
11
# Sync dist directory to serverrsync -avz --progress /local/path/dist/ user@server1:/opt/app/dist/
# rsync over SSH with custom portrsync -avz -e "ssh -p 2222" /local/path/ user@server1:/opt/app/
# Sync only files modified within the last dayrsync -avz --max-age=1 /local/path/logs/ user@server1:/backup/logs/
# Dry-run before executingrsync -avzn --delete /local/path/ user@server1:/opt/app/
--delete: Remove files at the destination that don’t exist in the source.
--progress: Display transfer progress.
Looping Through Multiple Servers
Basic Loop
1
2
3
4
5
6
SERVERS=("web-01""web-02""web-03")for server in "${SERVERS[@]}";doecho"=== $server ===" ssh "deploy@${server}""hostname && uptime && df -h / | tail -1"done
Reading Server List from a File
1
2
3
4
5
6
7
8
9
# servers.txt contains one server per line# web-01# web-02# db-01whileIFS=read -r server;do[[ -z "$server"||"$server"=~ ^# ]]&&continue ssh "deploy@${server}""hostname" 2>/dev/null &&echo"OK: $server"||echo"FAIL: $server"done < servers.txt
Error Handling in Loops
1
2
3
4
5
6
7
8
9
10
11
12
13
14
SERVERS=("web-01""web-02""web-03")FAILED=()for server in "${SERVERS[@]}";doif ! ssh "deploy@${server}""echo OK" 2>/dev/null;thenFAILED+=("$server")echo"[ERROR] Cannot connect to $server"fidoneif[[${#FAILED[@]} -gt 0]];thenecho"Failed servers: ${FAILED[*]}"exit1fi
Parallel Execution with & and wait
When managing multiple servers, sequential execution can be extremely slow. Bash supports parallel execution using & (background process) and wait (wait for all to complete):
Secure SSH keys: Set chmod 600 on private keys and chmod 644 on public keys. Never commit keys to a repository. In CI/CD, store keys as secrets and mount them as temporary files.
Host key verification: On the first connection, SSH prompts you to confirm the host key. In automated scripts, use StrictHostKeyChecking accept-new in ~/.ssh/config or the -o StrictHostKeyChecking=accept-new flag to auto-accept.
Concurrency limits: Running too many SSH sessions simultaneously can overload the network or source server. Always set MAX_PARALLEL appropriate for your infrastructure.
Connection timeouts: Use -o ConnectTimeout=10 and -o BatchMode=yes to prevent scripts from hanging indefinitely when a server is unresponsive.
Rollback strategy: Always have a rollback plan before bulk deployments. The example script above only deploys—in production, add a snapshot or backup step before making changes.
Conclusion
Automating SSH with Bash not only saves time but also ensures consistency when managing multiple servers. From setting up key-based authentication, configuring ~/.ssh/config for quick access, using scp/rsync for file transfers, to running commands in parallel with & and wait—all can be combined into a clean Bash script.
In the next post, we will explore combining Bash and Docker: writing wrapper scripts to manage containers, auto-restarting on crashes, and deploying stacks with docker compose.