How to Use Remote Servers with SSH in Bash

How to Use Remote Servers with SSH in Bash

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 no in /etc/ssh/sshd_config) once keys are set up.
  • Use ~/.ssh/config to keep scripts and commands readable, rather than repeating full connection strings everywhere.
  • Set ConnectTimeout and BatchMode=yes in scripted SSH calls so failures are fast and explicit instead of hanging indefinitely.
  • Prefer rsync over scp for anything beyond trivial one-off transfers.
  • Use ProxyJump (or the older ProxyCommand) 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-agent for 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_keys restrictions (like command= forcing a specific command, or no-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=no is 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 600 This 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 --partial flag 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

  1. Using password authentication in scripts, which either requires insecure hardcoding or breaks automation entirely.
  2. Forgetting ConnectTimeout and BatchMode, causing scripts to hang on unreachable or misconfigured hosts.
  3. Running deployment loops sequentially when parallel execution would be safe and much faster.
  4. Leaving StrictHostKeyChecking=no enabled permanently instead of only in disposable/CI environments.
  5. 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
Total
2
Shares

Leave a Reply

Previous Post
How to Schedule Tasks with Cron in Bash

How to Schedule Tasks with Cron in Bash

Next Post
How to Manage Users and Permissions in Bash

How to Manage Users and Permissions in Bash

Related Posts