| |

Bash Scripting 29 ๐Ÿง Remote Automation โ€” Executing commands securely via SSH loops

โฑ๏ธ Estimated read: 14 min | |

A script that runs commands on one remote host is straightforward. A script that runs the same command on dozens of hosts, collects the output, and reports which ones succeeded and which failed is a different problem. The loop itself is trivial. The security and reliability concerns are not: how the script authenticates without prompting for a password, how it handles a host that is unreachable, how it distinguishes a failed remote command from a failed SSH connection, and how it avoids re-negotiating a new TCP connection for every command on the same host.

This chapter covers the SSH loop pattern: key-based authentication that works without a terminal, the BatchMode and ConnectTimeout options that make SSH fail instead of prompt, the for loop and while read constructs that iterate over a host list, the exit-status convention where 255 means connection failure and any other non-zero value means command failure, and the ControlMaster multiplexing that reuses a single connection across multiple commands to the same host. It also covers the safe handling of secrets, which should never be embedded in a script that is checked into a repository.

Key point: ssh exits with the remote command’s status, or with 255 if the connection itself failed. A loop that treats every non-zero status the same cannot distinguish “the command failed on the host” from “the host is unreachable.” Check for 255 separately.


Why SSH loops need these safeguards

The interactive prompt problem. By default, ssh prompts for a password if key authentication fails, and prompts for confirmation if the host key is unknown. In a script running from cron or a CI pipeline, there is no terminal to answer these prompts. The script hangs until it times out. The BatchMode=yes option disables all interactive prompting: if authentication requires input, ssh fails immediately instead of waiting . The ConnectTimeout option bounds how long connection establishment can take, so an unreachable host does not stall the loop .

The exit-status ambiguity problem. ssh has a single special exit code: 255 means the connection failed for any reason (DNS resolution, TCP connection, authentication, host key mismatch). Any other exit code is the exit status of the remote command . A loop that reports “host failed” for every non-zero status will misreport a command that returned 1 as an unreachable host. The script must check for 255 first, then interpret the remaining codes as command results.

The reconnection overhead problem. A loop that runs three commands on the same host via three separate ssh invocations performs three TCP handshakes, three authentication exchanges, and three host-key verifications. On a high-latency link, this overhead dominates the actual work. The ControlMaster feature of OpenSSH allows the first connection to become a master, and subsequent connections to reuse it through a Unix socket . The handshake happens once; the remaining commands are sent over the existing channel.

The secret handling problem. A script that connects to remote hosts needs a credential. If that credential is a password embedded in the script, it is visible to anyone who can read the script, and it is likely to be committed to version control. Key-based authentication with an agent or a dedicated key file is the standard solution. If a secret must be passed to a remote command, piping it over the SSH channel is safer than embedding it in the command line, because the command line is visible in the process list .


a. Key-based authentication and batch mode

The prerequisite for an unattended SSH loop is key-based authentication. The script’s user must have a private key, and the corresponding public key must be in the authorized_keys file of the target user on every host.

# Generate a key pair if one does not exist
ssh-keygen -t ed25519 -f ~/.ssh/automation_key -N ""

# Copy the public key to each target host
ssh-copy-id -i ~/.ssh/automation_key.pub user@host1
ssh-copy-id -i ~/.ssh/automation_key.pub user@host2

The -N "" flag creates the key without a passphrase, which is what allows the script to use it without an agent. A passphrase-protected key requires ssh-agent or keychain to unlock it, which adds a dependency the script must manage.

Once the key is in place, the script invokes ssh with options that make it safe for batch execution:

ssh -o BatchMode=yes -o ConnectTimeout=10 -o StrictHostKeyChecking=accept-new \
    -i ~/.ssh/automation_key user@host1 'uptime'

BatchMode=yes prevents any prompt. ConnectTimeout=10 fails after ten seconds if the host is unreachable. StrictHostKeyChecking=accept-new accepts a host key on first connection but rejects a changed key on subsequent connections, which is the right balance for automation .

b. The for loop and while read patterns

The simplest loop iterates over an array of hostnames:

#!/bin/bash
set -euo pipefail

hosts=("web01" "web02" "db01")
SSH_OPTS=(-o BatchMode=yes -o ConnectTimeout=10 -o StrictHostKeyChecking=accept-new)

for host in "${hosts[@]}"; do
    echo "=== $host ==="
    if ssh "${SSH_OPTS[@]}" "user@$host" 'hostname && uptime'; then
        echo "OK: $host"
    else
        status=$?
        if [[ $status -eq 255 ]]; then
            echo "CONNECTION FAILED: $host" >&2
        else
            echo "COMMAND FAILED: $host (status $status)" >&2
        fi
    fi
done

The if ssh ...; then construct captures the exit status. A zero status means both the connection and the command succeeded. A non-zero status requires the 255 check to distinguish the two failure modes .

For a host list stored in a file, the while read pattern is more appropriate:

while IFS= read -r host; do
    [[ -z "$host" || "$host" == \#* ]] && continue
    ssh "${SSH_OPTS[@]}" "user@$host" 'command' </dev/null
done < hosts.txt

The </dev/null redirect is important. Without it, ssh may consume the input that the while read loop is reading from, causing the loop to skip hosts . The IFS= read -r preserves leading and trailing whitespace in the hostname, and the comment check skips blank lines and comments.

c. Distinguishing connection failure from command failure

The 255 convention is the only reliable way to distinguish a transport failure from a command failure. A script that needs to report the actual remote exit status must capture it before any other command overwrites $?.

output=$(ssh "${SSH_OPTS[@]}" "user@$host" 'some_command' 2>&1)
status=$?

if [[ $status -eq 255 ]]; then
    echo "SSH connection to $host failed" >&2
elif [[ $status -ne 0 ]]; then
    echo "Command failed on $host with status $status" >&2
else
    echo "Command succeeded on $host: $output"
fi

If the remote command itself can exit with 255, the script cannot use this convention. In that case, the remote command must print its own exit status to stdout, and the script must parse it from the output . This is rare, but it is the only way to handle the collision.

d. Connection multiplexing with ControlMaster

For a loop that runs multiple commands against the same host, ControlMaster eliminates the repeated handshake. The first connection creates a master socket, and subsequent connections reuse it.

#!/bin/bash
set -euo pipefail

host="web01"
socket_dir=$(mktemp -d)
control_socket="$socket_dir/ssh-%C"

cleanup() {
    ssh -o ControlPath="$control_socket" -O exit "user@$host" 2>/dev/null || true
    rm -rf "$socket_dir"
}
trap cleanup EXIT

# Open the master connection in the background
ssh -o ControlMaster=yes -o ControlPath="$control_socket" \
    -o ControlPersist=60 -o BatchMode=yes \
    -fN "user@$host"

# Verify the master is alive
ssh -o ControlPath="$control_socket" -O check "user@$host"

# Commands reuse the master connection
ssh -o ControlPath="$control_socket" "user@$host" 'command1'
ssh -o ControlPath="$control_socket" "user@$host" 'command2'

The -fN flags start the master in the background (-f) without running a remote command (-N). The ControlPersist=60 keeps the master alive for 60 seconds after the last client disconnects. The trap cleanup EXIT ensures the master is terminated and the socket directory removed even if the script is interrupted .

The %C in the ControlPath is a hash of the connection parameters, which keeps the socket name short and avoids the Unix socket path length limit . The socket is placed in a mktemp -d directory rather than a predictable /tmp path, which prevents another user from hijacking the socket.

For a loop over multiple hosts, the pattern becomes a function that opens a master per host, runs the commands, and closes the master before moving to the next host.


Complete Example Session

# ============================================
# PART 1: KEY-BASED AUTHENTICATION
# ============================================
# Generate a dedicated key for automation
ssh-keygen -t ed25519 -f ~/.ssh/automation_key -N ""

# Copy the public key to each target
ssh-copy-id -i ~/.ssh/automation_key.pub user@web01
ssh-copy-id -i ~/.ssh/automation_key.pub user@web02
# ============================================
# PART 2: BATCH MODE OPTIONS
# ============================================
SSH_OPTS=(
    -o BatchMode=yes
    -o ConnectTimeout=10
    -o StrictHostKeyChecking=accept-new
    -i ~/.ssh/automation_key
)

# BatchMode prevents prompts.
# ConnectTimeout bounds the wait.
# ============================================
# PART 3: THE BASIC LOOP
# ============================================
hosts=("web01" "web02" "db01")

for host in "${hosts[@]}"; do
    echo "=== $host ==="
    ssh "${SSH_OPTS[@]}" "user@$host" 'hostname && uptime'
done
# ============================================
# PART 4: DISTINGUISHING FAILURE TYPES
# ============================================
for host in "${hosts[@]}"; do
    if ssh "${SSH_OPTS[@]}" "user@$host" 'true' 2>/dev/null; then
        echo "OK: $host"
    else
        status=$?
        if [[ $status -eq 255 ]]; then
            echo "CONNECTION FAILED: $host" >&2
        else
            echo "COMMAND FAILED: $host (status $status)" >&2
        fi
    fi
done
# ============================================
# PART 5: WHILE READ OVER A HOST FILE
# ============================================
while IFS= read -r host; do
    [[ -z "$host" || "$host" == \#* ]] && continue
    ssh "${SSH_OPTS[@]}" "user@$host" 'command' </dev/null
done < hosts.txt

# </dev/null prevents ssh from consuming the host list.
# ============================================
# PART 6: CAPTURING OUTPUT AND STATUS
# ============================================
output=$(ssh "${SSH_OPTS[@]}" "user@$host" 'some_command' 2>&1)
status=$?

if [[ $status -eq 255 ]]; then
    echo "Connection failed" >&2
elif [[ $status -ne 0 ]]; then
    echo "Command failed: $status" >&2
else
    echo "Success: $output"
fi
# ============================================
# PART 7: CONTROLMASTER SETUP
# ============================================
socket_dir=$(mktemp -d)
control_socket="$socket_dir/ssh-%C"

ssh -o ControlMaster=yes -o ControlPath="$control_socket" \
    -o ControlPersist=60 -o BatchMode=yes \
    -fN "user@web01"
# ============================================
# PART 8: REUSING THE MASTER
# ============================================
ssh -o ControlPath="$control_socket" "user@web01" 'command1'
ssh -o ControlPath="$control_socket" "user@web01" 'command2'

# Both reuse the master connection.
# No new handshake for command2.
# ============================================
# PART 9: CLEANUP WITH TRAP
# ============================================
cleanup() {
    ssh -o ControlPath="$control_socket" -O exit "user@web01" 2>/dev/null || true
    rm -rf "$socket_dir"
}
trap cleanup EXIT

# The master is terminated and the socket removed
# even if the script fails or is interrupted.
# ============================================
# PART 10: COMPLETE SCRIPT
# ============================================
#!/bin/bash
set -euo pipefail

SSH_OPTS=(-o BatchMode=yes -o ConnectTimeout=10 -o StrictHostKeyChecking=accept-new)
hosts=("web01" "web02")

for host in "${hosts[@]}"; do
    if ssh "${SSH_OPTS[@]}" "user@$host" 'uptime' </dev/null; then
        echo "OK: $host"
    else
        status=$?
        [[ $status -eq 255 ]] && echo "CONN FAIL: $host" >&2 || echo "CMD FAIL: $host ($status)" >&2
    fi
done

The ten parts covered key generation, batch options, the basic loop, failure distinction, while read, output capture, ControlMaster setup, reuse, cleanup, and the complete script.


Quick Reference

SSH options for automation

OptionPurpose
BatchMode=yesDisable all interactive prompts
ConnectTimeout=10Fail after 10 seconds if unreachable
StrictHostKeyChecking=accept-newAccept first key, reject changes
-i keyfileSpecify the identity file
ControlMaster=yesCreate a master connection
ControlPath=socketShared socket path
ControlPersist=60Keep master alive for 60 seconds
-fNBackground, no remote command

Exit status convention

StatusMeaning
0Remote command succeeded
1โ€“254Remote command failed with that status
255SSH connection failed (any reason)

Loop patterns

PatternUse case
for host in "${hosts[@]}"Inline host array
while IFS= read -r host; do ... done < fileHost file
ssh ... </dev/nullPrevent SSH from consuming stdin
if ssh ...; then ... else ... fiCapture status

ControlMaster lifecycle

StepCommand
Openssh -o ControlMaster=yes -fN host
Verifyssh -O check host
Reusessh -o ControlPath=socket host cmd
Closessh -O exit host

Best Practices

โœ… Do This:

# Use BatchMode to prevent hangs
ssh -o BatchMode=yes host command                         # โœ…
# Check for 255 separately
if [[ $? -eq 255 ]]; then echo "connection failed"; fi    # โœ…
# Redirect stdin from /dev/null in loops
ssh host command </dev/null                               # โœ…
# Use a trap to clean up ControlMaster sockets
trap 'ssh -O exit host; rm -rf "$socket_dir"' EXIT        # โœ…

โŒ Don’t Do This:

# Embed passwords in the script
sshpass -p 'secret' ssh host command                      # โŒ
# Treat every non-zero status as connection failure
if ! ssh host cmd; then echo "host down"; fi              # โŒ (command may have failed)
# Forget </dev/null in while read loops
while read host; do ssh host cmd; done < hosts.txt        # โŒ (skips hosts)
# Use a predictable ControlPath in /tmp
ControlPath=/tmp/ssh-%r@%h:%p                             # โŒ (hijack risk)

Common Pitfalls

PitfallWhy It HappensFix
Script hangs waiting for passwordBatchMode not setAdd -o BatchMode=yes
Host appears unreachable when command failed255 not checked separatelyCheck [[ $? -eq 255 ]]
Loop skips hostsssh consumed stdinAdd </dev/null
Host key prompt hangs scriptStrictHostKeyChecking not setUse accept-new
ControlMaster socket left behindNo cleanup trapAdd trap ... EXIT with -O exit
Permission denied on keyKey file permissions too openchmod 600 ~/.ssh/automation_key

Real-World Examples

1. Basic loop

for host in web01 web02; do ssh "$host" 'uptime'; done

2. Batch mode options

ssh -o BatchMode=yes -o ConnectTimeout=10 user@host 'command'

3. Status check

ssh host 'cmd'; status=$?; [[ $status -eq 255 ]] && echo "conn fail"

4. While read

while read -r host; do ssh "$host" 'cmd' </dev/null; done < hosts.txt

5. Output capture

output=$(ssh host 'cmd' 2>&1); status=$?

6. ControlMaster open

ssh -o ControlMaster=yes -o ControlPath=/tmp/sock -fN host

7. ControlMaster reuse

ssh -o ControlPath=/tmp/sock host 'cmd'

8. ControlMaster close

ssh -o ControlPath=/tmp/sock -O exit host

9. Key with no passphrase

ssh-keygen -t ed25519 -f ~/.ssh/auto -N ""

10. Trap cleanup

trap 'ssh -O exit host 2>/dev/null; rm -rf "$dir"' EXIT

Visual

The SSH loop flow

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  SSH LOOP                                                    โ”‚
โ”‚                                                              โ”‚
โ”‚  for host in hosts:                                          โ”‚
โ”‚       โ”‚                                                      โ”‚
โ”‚       โ–ผ                                                      โ”‚
โ”‚  ssh -o BatchMode=yes -o ConnectTimeout=10 host cmd         โ”‚
โ”‚       โ”‚                                                      โ”‚
โ”‚       โ”œโ”€โ”€ exit 0 โ”€โ”€โ–บ command succeeded                      โ”‚
โ”‚       โ”‚                                                      โ”‚
โ”‚       โ”œโ”€โ”€ exit 1-254 โ”€โ”€โ–บ command failed (remote status)     โ”‚
โ”‚       โ”‚                                                      โ”‚
โ”‚       โ””โ”€โ”€ exit 255 โ”€โ”€โ–บ connection failed                    โ”‚
โ”‚                                                              โ”‚
โ”‚  The three outcomes require different handling.              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

ControlMaster multiplexing

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  WITHOUT CONTROLMASTER                                       โ”‚
โ”‚                                                              โ”‚
โ”‚  ssh host cmd1 โ”€โ”€โ–บ TCP handshake โ”€โ”€โ–บ auth โ”€โ”€โ–บ cmd1          โ”‚
โ”‚  ssh host cmd2 โ”€โ”€โ–บ TCP handshake โ”€โ”€โ–บ auth โ”€โ”€โ–บ cmd2          โ”‚
โ”‚  ssh host cmd3 โ”€โ”€โ–บ TCP handshake โ”€โ”€โ–บ auth โ”€โ”€โ–บ cmd3          โ”‚
โ”‚                                                              โ”‚
โ”‚  3 handshakes, 3 auths.                                      โ”‚
โ”‚                                                              โ”‚
โ”‚  โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”‚
โ”‚                                                              โ”‚
โ”‚  WITH CONTROLMASTER                                          โ”‚
โ”‚                                                              โ”‚
โ”‚  ssh -fN host โ”€โ”€โ–บ TCP handshake โ”€โ”€โ–บ auth โ”€โ”€โ–บ master socket  โ”‚
โ”‚  ssh -o ControlPath=sock host cmd1 โ”€โ”€โ–บ reuse โ”€โ”€โ–บ cmd1       โ”‚
โ”‚  ssh -o ControlPath=sock host cmd2 โ”€โ”€โ–บ reuse โ”€โ”€โ–บ cmd2       โ”‚
โ”‚                                                              โ”‚
โ”‚  1 handshake, 1 auth.                                        โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Exit status decision

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  EXIT STATUS                                                 โ”‚
โ”‚                                                              โ”‚
โ”‚  status=$?                                                   โ”‚
โ”‚       โ”‚                                                      โ”‚
โ”‚       โ”œโ”€โ”€ 0 โ”€โ”€โ–บ success                                      โ”‚
โ”‚       โ”‚                                                      โ”‚
โ”‚       โ”œโ”€โ”€ 255 โ”€โ”€โ–บ connection failure                         โ”‚
โ”‚       โ”‚         (DNS, TCP, auth, host key)                  โ”‚
โ”‚       โ”‚                                                      โ”‚
โ”‚       โ””โ”€โ”€ other โ”€โ”€โ–บ remote command failure                   โ”‚
โ”‚                    (the command's own exit code)             โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

ControlMaster lifecycle

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  CONTROLMASTER LIFECYCLE                                     โ”‚
โ”‚                                                              โ”‚
โ”‚  mktemp -d โ”€โ”€โ–บ socket_dir                                   โ”‚
โ”‚       โ”‚                                                      โ”‚
โ”‚       โ–ผ                                                      โ”‚
โ”‚  ssh -fN -o ControlMaster=yes -o ControlPath=sock host      โ”‚
โ”‚       โ”‚                                                      โ”‚
โ”‚       โ–ผ                                                      โ”‚
โ”‚  Master running in background                                โ”‚
โ”‚       โ”‚                                                      โ”‚
โ”‚       โ”œโ”€โ”€ ssh -O check host โ”€โ”€โ–บ verify                      โ”‚
โ”‚       โ”‚                                                      โ”‚
โ”‚       โ”œโ”€โ”€ ssh -o ControlPath=sock host cmd โ”€โ”€โ–บ reuse        โ”‚
โ”‚       โ”‚                                                      โ”‚
โ”‚       โ””โ”€โ”€ trap EXIT โ”€โ”€โ–บ ssh -O exit host โ”€โ”€โ–บ terminate      โ”‚
โ”‚       โ”‚                                                      โ”‚
โ”‚       โ–ผ                                                      โ”‚
โ”‚  rm -rf socket_dir                                           โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Summary

ItemValue
Prompt preventionBatchMode=yes
Connection timeoutConnectTimeout=10
Host key policyStrictHostKeyChecking=accept-new
Connection failure code255
Command failure code1โ€“254 (remote command’s status)
Stdin protection</dev/null in loops
MultiplexingControlMaster + ControlPath
Master openssh -fN -o ControlMaster=yes
Master closessh -O exit
Cleanuptrap ... EXIT

Key takeaways:

  • BatchMode=yes is mandatory for unattended loops. Without it, ssh prompts for a password or host-key confirmation, and the script hangs until it times out. ConnectTimeout bounds the wait for an unreachable host .
  • Exit code 255 is the only connection-failure indicator. Any other non-zero status is the remote command’s own exit code. A loop that reports “host down” for a command that returned 1 is misdiagnosing the failure .
  • </dev/null prevents SSH from consuming the host list. In a while read loop, ssh may read from the same stdin that the loop uses, causing hosts to be skipped .
  • ControlMaster eliminates repeated handshakes. For multiple commands against the same host, the first connection becomes a master and subsequent connections reuse it. The socket must be cleaned up with a trap to prevent orphaned masters .
  • Keys should be dedicated and unprotected only when necessary. A passphrase-protected key requires ssh-agent. An unprotected key is simpler for automation but must be stored with restrictive permissions and not committed to a repository.
  • Secrets should not be embedded in the script. If a secret must be passed to a remote command, pipe it over the SSH channel rather than putting it on the command line, where it is visible in the process list .

Remember: An SSH loop is a reliability problem disguised as a simple for construct. The loop must authenticate without a terminal, handle unreachable hosts without hanging, distinguish connection failures from command failures, and avoid re-negotiating the connection for every command. BatchMode, ConnectTimeout, the 255 check, </dev/null, and ControlMaster are the five mechanisms that make the difference between a loop that works and a loop that fails silently or hangs indefinitely. Get the exit-status handling right first: a script that cannot tell why a host failed cannot report what happened.

Was this helpful?


Stop using slow, ad-bloated tool sites! ๐Ÿคฎ

๐Ÿ”Ž Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
โœ… Finance (Mortgage, Interest, Inflation)
โœ… Tech (Base64, JSON, Dev Suite, IP)
โœ… Health (BMI, BMR, TDEE)
โœ… Productivity (Timer, Workspace, QR)

โšก๏ธ Fast & Private
๐Ÿ”’ No data leaves your device
๐Ÿ’Ž 100% Free

๐Ÿ”— Use it now: https://tools.kandz.me
๐Ÿ”– Bookmark itโ€”youโ€™ll need it later!