SSH Automation¶
Non-Interactive SSH¶
BatchMode¶
Disable all prompts for scripting:
Fails immediately if: - Password required - Host key verification needed - Any prompt required
StrictHostKeyChecking Options¶
# Prompt (default)
StrictHostKeyChecking ask
# Accept new, reject changed
StrictHostKeyChecking accept-new
# Skip verification (dangerous)
StrictHostKeyChecking no
Recommended for Scripts¶
ssh -o BatchMode=yes \
-o ConnectTimeout=10 \
-o StrictHostKeyChecking=accept-new \
user@host command
SSH Config for Automation¶
# ~/.ssh/config
Host auto-*
BatchMode yes
StrictHostKeyChecking accept-new
ConnectTimeout 10
ServerAliveInterval 60
IdentitiesOnly yes
Host auto-web
HostName web.example.com
User deploy
IdentityFile ~/.ssh/deploy_key
Host auto-db
HostName db.example.com
User backup
IdentityFile ~/.ssh/backup_key
Running Remote Commands¶
Single Command¶
Multiple Commands¶
Here Document¶
Script File¶
With Arguments¶
Exit Codes¶
SSH returns the remote command's exit code:
ssh user@host "grep pattern /var/log/syslog"
if [ $? -eq 0 ]; then
echo "Pattern found"
else
echo "Pattern not found"
fi
Common Exit Codes¶
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | General error |
| 255 | SSH error (connection failed) |
Handling Output¶
Capture Output¶
Separate stdout and stderr¶
Streaming¶
Parallel Execution¶
GNU Parallel¶
Background Jobs¶
for host in server1 server2 server3; do
ssh $host "apt update && apt upgrade -y" &
done
wait
echo "All updates complete"
xargs¶
Key-Based Automation¶
Dedicated Automation Key¶
# Generate key (no passphrase for automation)
ssh-keygen -t ed25519 -f ~/.ssh/automation_key -N ""
# Or with comment
ssh-keygen -t ed25519 -f ~/.ssh/automation_key -N "" -C "automation@hostname"
Restricted Authorized Keys¶
On the server, restrict what the key can do:
# ~/.ssh/authorized_keys
command="/usr/local/bin/backup.sh",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty ssh-ed25519 AAAAC3... automation-backup
restrict,command="/usr/local/bin/deploy.sh" ssh-ed25519 AAAAC3... automation-deploy
Restrict by Source IP¶
Scripting Patterns¶
Health Check¶
#!/bin/bash
HOSTS="server1 server2 server3"
for host in $HOSTS; do
if ssh -o BatchMode=yes -o ConnectTimeout=5 $host "true" 2>/dev/null; then
echo "$host: OK"
else
echo "$host: FAILED"
fi
done
Collect Information¶
#!/bin/bash
for host in server1 server2 server3; do
echo "=== $host ==="
ssh $host "hostname; uptime; df -h /"
done
Deploy Script¶
#!/bin/bash
set -e
SERVERS="web1 web2 web3"
DEPLOY_DIR="/var/www/app"
for server in $SERVERS; do
echo "Deploying to $server..."
rsync -avz --delete ./dist/ $server:$DEPLOY_DIR/
ssh $server "systemctl restart app"
done
echo "Deployment complete"
Parallel with Feedback¶
#!/bin/bash
SERVERS="server1 server2 server3"
for server in $SERVERS; do
(
echo "Starting $server..."
ssh $server "apt update && apt upgrade -y"
echo "Finished $server"
) &
done
wait
echo "All servers updated"
Error Handling¶
Trap Errors¶
#!/bin/bash
set -e
trap 'echo "Error on line $LINENO"' ERR
ssh server1 "command1"
ssh server2 "command2"
ssh server3 "command3"
Check Each Command¶
#!/bin/bash
ssh server1 "command1" || { echo "Failed on server1"; exit 1; }
ssh server2 "command2" || { echo "Failed on server2"; exit 1; }
Continue on Error¶
#!/bin/bash
for host in server1 server2 server3; do
if ! ssh $host "command"; then
echo "Warning: $host failed" >&2
fi
done
Ansible Alternative¶
For complex automation, consider Ansible:
# playbook.yml
- hosts: webservers
tasks:
- name: Update packages
apt:
update_cache: yes
upgrade: yes
- name: Restart service
service:
name: nginx
state: restarted
Cron Jobs¶
Simple Backup¶
# /etc/cron.d/backup
0 2 * * * backupuser ssh -i /home/backupuser/.ssh/backup_key -o BatchMode=yes backup@remote "pg_dump mydb" > /backups/db.sql
With Logging¶
#!/bin/bash
# /usr/local/bin/daily-backup.sh
LOG="/var/log/backup.log"
exec >> $LOG 2>&1
echo "=== Backup started $(date) ==="
ssh backup@server "tar czf - /data" > /backups/data-$(date +%Y%m%d).tar.gz
echo "=== Backup finished $(date) ==="
SSH in CI/CD¶
GitHub Actions¶
- name: Deploy
env:
SSH_PRIVATE_KEY: ${{ secrets.SSH_KEY }}
run: |
mkdir -p ~/.ssh
echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519
ssh-keyscan server.example.com >> ~/.ssh/known_hosts
rsync -avz ./dist/ deploy@server.example.com:/var/www/app/
GitLab CI¶
deploy:
script:
- eval $(ssh-agent -s)
- echo "$SSH_PRIVATE_KEY" | ssh-add -
- mkdir -p ~/.ssh
- ssh-keyscan server.example.com >> ~/.ssh/known_hosts
- rsync -avz ./dist/ deploy@server.example.com:/var/www/app/
Security Best Practices¶
- Use dedicated keys for automation
- Restrict keys with command= and from=
- No passphrase only when necessary, protect key file
- Minimal permissions for automation users
- Audit logs review automated access
- Rotate keys periodically
- Use secrets management in CI/CD