告别手动输密码: 嵌入式 SSH/SCP 自动化方案

5 阅读4分钟

原文链接: Linux Shell: 使用 Expect 自动化 SCP 和 SSH 连接的 Shell 脚本详解

1. 问题场景

嵌入式 Linux 开发的典型工作流:

宿主机 (x86 Ubuntu)                  目标板 (ARM Linux)
┌──────────────────┐    SSH/SCP     ┌──────────────────┐
│  交叉编译         │ ──────────→   │  /opt/app/        │
│  cmake --build   │               │  运行、调试        │
│  单元测试         │ ←────────── │  日志、core dump   │
└──────────────────┘    SCP/rsync   └──────────────────┘

每次编译后需要将二进制文件 SCP 到目标板、SSH 登录重启服务、拉取日志回宿主机分析。手动输入密码的重复操作在日均数十次的开发迭代中严重影响效率。

2. 方案一: Expect 自动化

Expect 是基于 Tcl 的交互自动化工具,通过模式匹配和自动应答实现非交互式操作。

2.1 SSH 自动登录

#!/bin/bash
# ssh_auto.sh
export TERM=xterm-256color
ip='192.168.1.10'
password='your_password'

ssh-keygen -f "$HOME/.ssh/known_hosts" -R "${ip}" 2>/dev/null

expect -c '
  set timeout 10
  set password "'"$password"'"
  spawn ssh -o StrictHostKeyChecking=no root@'"$ip"'
  expect {
    "*yes/no*" { send "yes\r"; exp_continue }
    "*password:*" { send "$password\r"; exp_continue }
    eof
  }
  interact
'

2.2 SCP 上传/下载

# 上传
expect -c '
  set timeout 30
  set password "'"$password"'"
  spawn scp -o StrictHostKeyChecking=no '"$file"' root@'"$ip"':'"$dest"'
  expect {
    "*password:*" { send "$password\r"; exp_continue }
    eof
  }
'

# 下载
expect -c '
  spawn scp -o StrictHostKeyChecking=no root@'"$ip"':'"$remote_file"' '"$dest"'
  expect {
    "*password:*" { send "$password\r"; exp_continue }
    eof
  }
'

2.3 局限性

问题说明
密码明文硬编码在脚本中,任何有读权限的用户都能看到
进程可见ps aux 可能显示命令行中的密码
多层转义Bash + Tcl 双层引号处理,密码含特殊字符时极易出错
无断点续传SCP 传输中断后必须重新开始
绕过主机验证StrictHostKeyChecking=no 禁用了中间人攻击防护

Expect 适合临时使用或密码无法更改的遗留系统。

3. 方案二: sshpass

sshpass 是专为 SSH 密码自动化设计的工具,比 Expect 更简洁:

# 安装
sudo apt install sshpass

# SSH 登录
sshpass -p 'your_password' ssh root@192.168.1.10

# SCP 上传/下载
sshpass -p 'your_password' scp ./app root@192.168.1.10:/opt/
sshpass -p 'your_password' scp root@192.168.1.10:/var/log/app.log ./

3.1 密码传递的安全层级

方式命令安全性风险
-p 命令行sshpass -p 'pwd' ssh ...最低ps 和 shell history 可见
-f 文件sshpass -f /path/to/pwfile ssh ...中等文件权限需设为 0400
-e 环境变量SSHPASS=pwd sshpass -e ssh ...较高用完后应 unset SSHPASS

推荐文件方式:

echo 'your_password' > ~/.ssh/.board_pwd
chmod 0400 ~/.ssh/.board_pwd
sshpass -f ~/.ssh/.board_pwd ssh root@192.168.1.10

3.2 sshpass vs Expect

维度sshpassExpect
SSH/SCP 专用通用交互自动化
代码量一行命令10+ 行脚本
特殊字符处理无需额外转义Bash + Tcl 双层转义
复杂交互 (sudo, 菜单)不支持支持

仅自动化 SSH/SCP 密码输入时,sshpass 比 Expect 更简洁。

4. 方案三: SSH 密钥认证 (推荐)

SSH 公钥认证是自动化登录的工业标准方案。无密码传输、无明文存储、抗暴力破解。

4.1 密钥生成与部署

# 1. 生成 Ed25519 密钥对
ssh-keygen -t ed25519 -C "dev@workstation" -f ~/.ssh/id_ed25519_board

# 2. 部署公钥到目标板
ssh-copy-id -i ~/.ssh/id_ed25519_board.pub root@192.168.1.10

# 3. 验证免密登录
ssh -i ~/.ssh/id_ed25519_board root@192.168.1.10

4.2 权限要求

路径权限说明
~/.ssh/700仅所有者可读写执行
~/.ssh/authorized_keys600仅所有者可读写
~/.ssh/id_ed25519 (私钥)600仅所有者可读写

4.3 ssh-agent 管理密钥

如果私钥设置了 passphrase,使用 ssh-agent 在会话期间缓存解密后的私钥:

eval "$(ssh-agent -s)"
ssh-add ~/.ssh/id_ed25519_board
# 后续所有 SSH/SCP 操作自动使用缓存的密钥

4.4 安全性对比

维度Expect / sshpassSSH 密钥
密码/密钥传输每次通过网络传输密码仅传输公钥签名,私钥不离开宿主机
暴力破解可被穷举Ed25519: 2^128 安全级别
中间人攻击StrictHostKeyChecking=no 禁用防护主机密钥验证 + 密钥签名双重保护
明文存储脚本/文件中存在明文密码私钥可加密 (passphrase)
凭证泄露影响密码泄露 = 完全控制公钥泄露无影响,私钥泄露可立即吊销

5. SSH Config: 统一管理多台设备

~/.ssh/config 中配置别名,避免记忆 IP 和参数:

# ~/.ssh/config

Host board
    HostName 192.168.1.10
    User root
    IdentityFile ~/.ssh/id_ed25519_board
    IdentitiesOnly yes
    StrictHostKeyChecking accept-new

Host lab-gateway
    HostName 10.0.0.1
    User engineer
    IdentityFile ~/.ssh/id_ed25519_work

Host lab-board
    HostName 192.168.100.10
    User root
    ProxyJump lab-gateway
    IdentityFile ~/.ssh/id_ed25519_board

Host *
    AddKeysToAgent yes
    ServerAliveInterval 60
    ServerAliveCountMax 3
    ConnectTimeout 10

配置后的使用:

ssh board                          # 自动查找 IP、用户名、密钥
scp ./app board:/opt/              # SCP 上传
ssh lab-board                      # 通过跳板机登录实验室目标板
scp ./firmware.bin lab-board:/tmp/ # 通过跳板机 SCP

5.1 关键配置项

配置项说明
IdentitiesOnly yes仅使用指定的密钥文件,不尝试 agent 中的其他密钥
StrictHostKeyChecking accept-new首次连接自动接受并保存主机密钥,后续连接验证
ServerAliveInterval 60每 60 秒发送心跳,防止 NAT 超时断连
AddKeysToAgent yes首次使用密钥时自动添加到 ssh-agent

5.2 ProxyJump 跳板机

ProxyJump 实现端到端加密的跳板连接,私钥始终留在宿主机上:

# 命令行方式
ssh -J engineer@10.0.0.1 root@192.168.100.10
scp -J engineer@10.0.0.1 ./app root@192.168.100.10:/opt/

# 多级跳板
ssh -J bastion1,bastion2 root@target
方式私钥位置安全风险
ProxyJump始终在宿主机跳板机无法窃取私钥
ForwardAgent yes转发到跳板机跳板机 root 用户可劫持 agent

推荐使用 ProxyJump。

6. rsync: 增量同步

rsync 使用 delta 算法,仅传输变化的部分。

6.1 基本用法

# 同步目录到目标板 (增量传输)
rsync -avz --progress ./build/output/ board:/opt/app/

# 从目标板拉取日志
rsync -avz board:/var/log/app/ ./logs/

# 通过跳板机同步
rsync -avz ./build/output/ lab-board:/opt/app/

6.2 常用选项

选项说明
-a归档模式: 递归、保留权限/时间戳/符号链接
-v显示传输过程
-z传输时压缩
--progress显示进度
--delete删除目标端多余的文件 (镜像同步)
--exclude '*.o'排除中间文件

6.3 性能对比

场景SCPrsync
首次传输 10 MB 二进制1.5 s1.8 s (校验开销)
修改 1 KB 后重新传输1.5 s (全量)0.3 s (增量)
传输中断后恢复从头开始--partial 断点续传

嵌入式迭代开发中,rsync 的增量传输可以将部署时间缩短 80% 以上。

7. 实用脚本: 一键部署

#!/bin/bash
# deploy.sh -- 编译并部署到目标板
# 用法: ./deploy.sh [board|lab-board]

set -euo pipefail

TARGET=${1:-board}
BUILD_DIR="./build"
REMOTE_DIR="/opt/app"
BINARY="my_app"

echo "=== Building ==="
cmake --build "$BUILD_DIR" --target "$BINARY" -j"$(nproc)"

echo "=== Deploying to $TARGET ==="
rsync -avz --progress \
    --exclude '*.o' \
    --exclude 'CMakeFiles' \
    "$BUILD_DIR/$BINARY" \
    "$TARGET:$REMOTE_DIR/"

echo "=== Restarting service ==="
ssh "$TARGET" "systemctl restart my_app || $REMOTE_DIR/$BINARY &"

echo "=== Done ==="

这个脚本不包含任何密码、IP 地址或密钥路径,所有连接参数由 ~/.ssh/config 管理。

8. 方案选择决策树

需要自动化 SSH/SCP?
│
├── 能否部署密钥到目标板?
│   ├── 是 → SSH 密钥认证 + SSH Config + rsync (推荐)
│   └── 否 (权限/策略限制)
│       ├── 能否安装 sshpass?
│       │   ├── 是 → sshpass -f (文件方式)
│       │   └── 否 → Expect 脚本 (最后手段)
│       └── 需要复杂交互 (sudo/菜单)?
│           └── 是 → Expect 脚本
│
├── 需要通过跳板机?
│   └── SSH Config + ProxyJump
│
├── 频繁传输文件?
│   ├── 目标板有 rsync → rsync -avz
│   └── 目标板无 rsync → scp
│
└── 安全性要求高?
    └── SSH 密钥 + StrictHostKeyChecking + 禁用 PasswordAuthentication

9. 安全加固

生产环境建议:禁用目标板密码登录 (/etc/ssh/sshd_config: PasswordAuthentication no)、使用 Ed25519 密钥、启用主机密钥验证 (StrictHostKeyChecking accept-new)、为私钥设置 passphrase。