SSH (Secure Shell) is how the vast majority of server administration actually happens. Whether you’re logging into a single VPS or orchestrating deployments across a fleet of machines, the ability to connect, run commands, transfer files, and automate all of it from a Bash script is a core skill for anyone working with Linux infrastructure.
This guide covers everything from a first connection to scripting multi-server automation with SSH.
What SSH Actually Does
SSH creates an encrypted channel between your local machine and a remote host over a network, replacing older, insecure protocols like Telnet and rsh. Beyond just remote shell access, SSH also provides secure file transfer (via scp/sftp), port forwarding/tunneling, and the transport layer for tools like rsync and git over SSH.
Basic Connection
ssh username@remote_host
# Specify a non-default port
ssh -p 2222 username@remote_host
# Run a single command remotely without an interactive session
ssh username@remote_host "df -h"
# Connect using a specific identity file
ssh -i ~/.ssh/id_ed25519 username@remote_host
Setting Up SSH Key-Based Authentication
Password authentication is both less secure and harder to automate. Key-based authentication is the standard for both interactive and scripted use.
# Generate a new key pair (Ed25519 is the modern recommended algorithm)
ssh-keygen -t ed25519 -C "your_email@example.com"
# Copy your public key to a remote server
ssh-copy-id username@remote_host
# Or manually, if ssh-copy-id isn't available
cat ~/.ssh/id_ed25519.pub | ssh username@remote_host "mkdir -p ~/.ssh && cat >> ~/.ssh/authorized_keys"
How this works internally: ssh-keygen generates a private key (kept secret, never shared) and a public key (safe to distribute). ssh-copy-id appends your public key to the remote server’s ~/.ssh/authorized_keys file. When you connect, the server challenges your client to prove it holds the matching private key — all without the private key or a password ever crossing the network.
The SSH Config File
Rather than typing long connection strings repeatedly, define hosts in ~/.ssh/config:
Host myserver
HostName 203.0.113.10
User deploy
Port 2222
IdentityFile ~/.ssh/id_ed25519_deploy
Host jumpbox
HostName jump.example.com
User admin
Host internal-db
HostName 10.0.1.5
User admin
ProxyJump jumpbox
Now you can simply run:
ssh myserver
ssh internal-db # automatically tunnels through jumpbox
Running Commands and Scripts Remotely
# Single command
ssh user@host "uptime"
# Multiple commands
ssh user@host "cd /var/www && git pull && systemctl restart myapp"
# Run a local script on a remote machine without copying it first
ssh user@host 'bash -s' < local_script.sh
# Pass arguments to a remote script
ssh user@host 'bash -s' -- arg1 arg2 < local_script.sh
The bash -s pattern reads the script from standard input on the remote side, which means you never need to manually copy the script over first — useful for one-off automation tasks.
Transferring Files: scp, sftp, and rsync
# Copy a local file to a remote server
scp localfile.txt user@host:/remote/path/
# Copy a remote file to your local machine
scp user@host:/remote/path/file.txt ./
# Copy a whole directory recursively
scp -r local_dir/ user@host:/remote/path/
# Interactive file transfer session
sftp user@host
# rsync — efficient, incremental sync (preferred for repeated transfers)
rsync -avz --progress local_dir/ user@host:/remote/path/
rsync is generally preferred over scp for anything beyond a one-off transfer because it only sends the differences between source and destination, and supports resuming interrupted transfers.
Automating SSH in Bash Scripts
Looping Over Multiple Servers
#!/bin/bash
servers=("web1.example.com" "web2.example.com" "web3.example.com")
for server in "${servers[@]}"; do
echo "=== $server ==="
ssh -o ConnectTimeout=5 deploy@"$server" "systemctl status myapp --no-pager"
done
Deploying Code to Multiple Servers
#!/bin/bash
set -euo pipefail
servers=("web1.example.com" "web2.example.com")
APP_DIR="/var/www/myapp"
for server in "${servers[@]}"; do
echo "Deploying to $server..."
ssh deploy@"$server" "cd $APP_DIR && git pull origin main && npm install --production && sudo systemctl restart myapp"
echo "$server: deployment complete"
done
echo "All servers updated."
Checking Remote Command Exit Status
#!/bin/bash
if ssh -o BatchMode=yes -o ConnectTimeout=5 user@host "test -f /etc/myapp/config.yml"; then
echo "Config file exists"
else
echo "Config file missing or connection failed"
fi
BatchMode=yes disables interactive prompts (like password requests), which is essential for scripts — you want a script to fail fast with a clear error rather than hang waiting for input that will never come.
SSH with a Timeout to Avoid Hanging Scripts
timeout 10 ssh user@host "long_running_command" || echo "Command timed out or failed"
SSH Tunneling and Port Forwarding
# Local port forwarding — access a remote service as if it were local
ssh -L 8080:localhost:80 user@remote_host
# Remote port forwarding — expose a local service to the remote side
ssh -R 9000:localhost:3000 user@remote_host
# Dynamic forwarding (SOCKS proxy)
ssh -D 1080 user@remote_host
Local forwarding is commonly used to securely access a database or admin panel that’s only bound to localhost on the remote machine, without exposing it directly to the internet.
Real-World Automation Examples
Health Check Across a Fleet
#!/bin/bash
servers=("app1" "app2" "app3")
for s in "${servers[@]}"; do
status=$(ssh -o ConnectTimeout=5 "$s" "systemctl is-active myapp" 2>/dev/null)
if [[ "$status" != "active" ]]; then
echo "ALERT: $s reports status '$status'"
fi
done
Automated Backup Pull from a Remote Server
#!/bin/bash
REMOTE="backup@dbserver:/backups/"
LOCAL="/local/backup_archive/"
DATE=$(date +%Y%m%d)
rsync -avz "$REMOTE" "${LOCAL}${DATE}/"
echo "Backup synced for $DATE"
Best Practices
- Always use key-based authentication and disable password authentication on servers (
PasswordAuthentication noin/etc/ssh/sshd_config) once keys are set up. - Use
~/.ssh/configto keep scripts and commands readable, rather than repeating full connection strings everywhere. - Set
ConnectTimeoutandBatchMode=yesin scripted SSH calls so failures are fast and explicit instead of hanging indefinitely. - Prefer
rsyncoverscpfor anything beyond trivial one-off transfers. - Use
ProxyJump(or the olderProxyCommand) for bastion/jump-host architectures instead of manually chaining SSH sessions. - Test automation scripts against a single server before rolling out to an entire fleet.
Security Considerations
- Never embed passwords in scripts. Rely entirely on key-based auth, and protect private keys with a passphrase plus
ssh-agentfor convenience. - Restrict key permissions correctly — SSH will refuse to use a private key with overly permissive file permissions:
chmod 600 ~/.ssh/id_ed25519chmod 700 ~/.ssh - Use dedicated, limited-privilege service accounts for automated deployment tasks rather than SSHing in as root.
- Consider
authorized_keysrestrictions (likecommand=forcing a specific command, orno-port-forwarding) for automation keys that should only be able to perform one narrow task. - Rotate and audit SSH keys periodically, and remove access for keys tied to former employees or decommissioned systems immediately.
- Verify host keys rather than blindly accepting them —
StrictHostKeyChecking=nois convenient in disposable CI environments but risky for anything persistent, since it disables protection against man-in-the-middle attacks.
Optimization Tips
- Use SSH connection multiplexing to avoid the overhead of a fresh TCP/TLS-equivalent handshake for every command:
Host * ControlMaster auto ControlPath ~/.ssh/sockets/%r@%h-%p ControlPersist 600This lets multiple SSH commands to the same host reuse a single underlying connection, dramatically speeding up scripts that make several sequential SSH calls. - Run independent server operations in parallel using background jobs and
wait, rather than looping sequentially, when servers don’t depend on each other:for server in "${servers[@]}"; do ssh "$server" "some_check" &donewait - Use
rsync‘s--partialflag for large transfers over unreliable connections, allowing resumption instead of restarting from scratch.
Troubleshooting Common Issues
Problem: “Permission denied (publickey)” when connecting. Confirm the public key is actually present in the remote ~/.ssh/authorized_keys, that private key permissions are 600, and that the .ssh directory itself is 700. Overly open permissions cause SSH to silently ignore the key.
Problem: Script hangs indefinitely when run via cron. This is almost always a missing BatchMode=yes combined with a host key that hasn’t been accepted yet, causing SSH to wait for interactive confirmation that will never come in a non-interactive cron context.
Problem: Connection is slow to establish. Enable connection multiplexing (ControlMaster), and check whether UseDNS is enabled on the server side — reverse DNS lookups on connection can add noticeable delay.
Problem: “Host key verification failed” after a server was rebuilt. The server’s host key changed (expected after a reinstall). Remove the old entry with ssh-keygen -R hostname and reconnect to accept the new key.
Common Mistakes
- Using password authentication in scripts, which either requires insecure hardcoding or breaks automation entirely.
- Forgetting
ConnectTimeoutandBatchMode, causing scripts to hang on unreachable or misconfigured hosts. - Running deployment loops sequentially when parallel execution would be safe and much faster.
- Leaving
StrictHostKeyChecking=noenabled permanently instead of only in disposable/CI environments. - Granting automation keys full shell access when a restricted
command=key would be safer.
Frequently Asked Questions
What’s the difference between scp and rsync? scp performs a straightforward copy every time. rsync compares source and destination and transfers only the differences, making repeated syncs much faster and supporting resumable transfers.
How do I run an SSH command without being prompted for anything? Use -o BatchMode=yes along with key-based authentication — this ensures the connection fails immediately with an error rather than waiting for interactive input.
Can I use SSH to connect through a bastion/jump host automatically? Yes — configure ProxyJump in ~/.ssh/config (or use -J on the command line) to route the connection through an intermediate host transparently.
Is it safe to disable host key checking? Only in temporary, disposable, or fully trusted environments like ephemeral CI runners. In persistent production use, disabling it removes protection against man-in-the-middle attacks.
Summary
SSH is the backbone of remote Linux administration, and Bash is how you turn manual SSH sessions into reliable automation — deploying code across servers, pulling backups, running health checks, and tunneling into internal services, all securely and repeatably. The combination of key-based authentication, a well-organized ~/.ssh/config, and scripting patterns like BatchMode, timeouts, and connection multiplexing is what takes SSH from “a way to log in” to “a real automation platform.”
References
- OpenSSH Manual Pages: https://www.openssh.com/manual.html
- man7.org ssh_config(5): https://man7.org/linux/man-pages/man5/ssh_config.5.html
- man7.org ssh(1): https://man7.org/linux/man-pages/man1/ssh.1.html
- rsync Official Documentation: https://rsync.samba.org/documentation.html
