ssh —— 远程登录与远程执行
一句话定义
ssh 是 OpenSSH 客户端:在本机和远端之间建立加密通道,让你可以登录远端 shell、远程执行命令、转发端口、传文件(间接,通过 scp/sftp/rsync)。
典型场景
训练营里 ssh 是出现频次最高的命令(出场 70+ 次)。三种主用法:
- 登录式:
ssh m1进入交互 shell,慢慢敲命令 - 单次执行:
ssh m1 'kubectl get nodes'拿一次结果就走 - 批量脚本:
ssh m1 'bash -s' <<EOF ... EOF—— 把一整段脚本喂给远端 bash 执行(Day0 装机脚本就是这种)
基础用法
1. 登录式
ssh user@host # 进入交互 shell
ssh m1 # 用 ~/.ssh/config 里的 alias
ssh -p 26292 root@103.47.83.39 # 显式指定非默认端口
退出:exit 或 Ctrl-D 或 ~.(连字符断开都没反应时的"紧急断开")。
2. 单次执行
ssh m1 'kubectl get nodes'
ssh m1 hostname
ssh m1 'ls /var/log | head'
记得加引号。不加的话本地 shell 会先把命令解析一遍:
ssh m1 ls $HOME # ❌ $HOME 是本地的,不是远端的
ssh m1 'ls $HOME' # ✅ 单引号阻止本地展开,远端 shell 才解析 $HOME
3. heredoc 批量脚本(重点)
ssh m1 'bash -s' <<'EOF'
echo "Hello AI-Infra"
hostname
kubectl get nodes
EOF
这条命令为什么能跑通?拆开看:
bash -s:-s让 bash 从 stdin 读脚本(而不是从命令行参数或文件)<<'EOF' ... EOF:本地 shell 的 heredoc,把中间这段文本作为本进程(ssh)的 stdin- ssh 自动转发 stdin:ssh 把本地 stdin 通过加密通道塞给远端
bash -s的 stdin - 远端 bash 收到这段脚本就执行
所以这条命令的本质是:本地把一段脚本通过 ssh 管道喂给远端 bash 解释器。
关键细节:'EOF' vs EOF
NAME="local-machine"
# 引号包围 EOF —— 本地不展开
ssh m1 'bash -s' <<'EOF'
echo $NAME # 远端展开 → 远端 $NAME(多半为空)
hostname # 远端 hostname
EOF
# 不加引号 —— 本地先展开
ssh m1 'bash -s' <<EOF
echo $NAME # 本地展开 → 字面输出 "local-machine"
hostname # 还没到远端就被本地 shell 看见,没影响
EOF
绝大多数情况都该加单引号:
- 加引号:脚本原文发到远端,由远端解释。预期最一致。
- 不加引号:本地 shell 先 eval 一遍
$var和$(...),可能误把变量替换成空、误把$(date)替换成本地时间。
远程脚本最常见的坑:忘记加
'EOF'的引号 →$(...)在本地被求值为空 → 远端跑了一段空脚本却没报错。
核心参数
| 参数 | 作用 | 备注 |
|---|---|---|
-p <port> | 指定端口 | 比 config 里的 Port 优先 |
-l <user> | 指定用户 | 等价于 user@host 写法 |
-i <keyfile> | 指定私钥 | 一次性用某把 key |
-o KEY=VALUE | 临时覆盖一个配置项 | -o StrictHostKeyChecking=no |
-v / -vv / -vvv | 日志级别 | 排错神器 |
-N | 不执行远端命令,只建隧道 | 配合 -L -R -D |
-f | 进入后台 | 常与 -N 配合做长期端口转发 |
-T | 不分配伪终端 | 跑非交互脚本时建议加 |
-t | 强制分配伪终端 | 远端要跑 vim/htop 时必须 |
-L L:H:R | 本地端口转发 | 见下方 |
-R R:H:L | 远程端口转发 | |
-D <port> | 动态转发(SOCKS5) | |
-J <jumphost> | 跳板机一步到位 | 等价于 ProxyJump |
-G | 不连接,打印生效配置 | 见 ssh-config.md |
-q | 静默模式 | 抑制 warning |
-o 的常见组合
ssh -o StrictHostKeyChecking=accept-new \
-o UserKnownHostsFile=/dev/null \
-o BatchMode=yes \
-o ConnectTimeout=5 \
root@$ip 'echo OK'
| 选项 | 用途 |
|---|---|
StrictHostKeyChecking=accept-new | 首次自动信任、之后严格(写脚本的最佳默认值) |
UserKnownHostsFile=/dev/null | 临时实例,不污染主机的 known_hosts |
BatchMode=yes | 禁用一切交互(密码框、确认框),失败就失败 —— 脚本里必加 |
ConnectTimeout=5 | 默认是 TCP 重试好几分钟,脚本里要短超时 |
进阶用法
端口转发:把远端服务"拉到本地"
K8s dashboard、Grafana、Argo CD 这些通常只监听 cluster 内部。要在本地浏览器看,最简单的办法是 ssh 端口转发:
# 远端 kubectl proxy 监听 m1:8001,本地访问 localhost:8001 就能看到
ssh -N -L 8001:localhost:8001 m1
# 后台跑,不占终端
ssh -f -N -L 8001:localhost:8001 m1
-L L:H:R 的意思:本地 L 端口 → 通过 ssh 加密隧道 → 远端从远端视角访问 H:R。
H 是 远端视角下的地址。localhost 指远端自己。如果想转发到远端能访问、但你不能直接访问的内网机器:
ssh -L 5432:db-internal.local:5432 bastion
# 本地 localhost:5432 → bastion → 内网 db-internal.local:5432
远程端口转发:把本机服务推给远端用
# 让远端 m1 通过 localhost:3000 访问到你本机的开发服
ssh -R 3000:localhost:3000 m1
调试 webhook 类场景常用(让公网机器能回调你本地的服务)。
动态转发:把 ssh 当 SOCKS5 代理
ssh -D 1080 -N bastion
本地 localhost:1080 就是 SOCKS5 代理。配合浏览器 SOCKS 设置,访问公司内网网站。
跳板机一步到位
ssh -J bastion dev-01 # 命令行写法
等价于 config 里的 ProxyJump bastion。多级跳:-J b1,b2。
-t 远端跑交互工具
ssh m1 'sudo vim /etc/hosts' # ❌ vim 屏幕花掉
ssh -t m1 'sudo vim /etc/hosts' # ✅ 强制分配伪终端
ssh 默认在"非交互命令"模式下不分配 PTY;vim/htop/sudo 提示密码这些需要 PTY 才能工作。-t 强制分配。
-T 跑非交互脚本
ssh -T m1 < my-big-script.sh
与 -t 反向 —— 明确告诉 ssh 不要 PTY。能让 stderr/stdout 行为更可预测,写脚本时建议加。
真实场景:批量并行执行
训练营 5 台机器、阿里云 10 台机器,要每台都跑同一段配置。三种做法对比:
# 1. 串行 for 循环 —— 简单粗暴,5 台机器 5×30s = 2.5 分钟
for h in m1 m2 m3 m4 m5; do
ssh "$h" 'apt-get update -qq && apt-get install -y curl jq'
done
# 2. 后台并行 —— 5 台机器 ~30s 全部完成
for h in m1 m2 m3 m4 m5; do
ssh "$h" 'apt-get update -qq && apt-get install -y curl jq' &
done
wait # 等所有后台 ssh 跑完
# 3. parallel-ssh / ansible —— 真正的生产做法
# Ansible 一行: ansible all -i hosts -m apt -a "name=curl,jq state=present"
并行版的注意点:& 让 shell 把每个 ssh 放后台、立即继续;最后 wait 同步。但输出会混在一起(多个 ssh 同时写 stdout),需要的话用 &> /tmp/log-$h.log 各自重定向到文件。
调试:连不上怎么办
第 1 步:用 -v 看握手
ssh -v m1
-v 一级日志通常够用。看几个关键行:
debug1: Reading configuration data /home/u/.ssh/config
debug1: /home/u/.ssh/config line 3: Applying options for m1
debug1: Connecting to 10.0.24.28 [10.0.24.28] port 22.
debug1: Connection established.
debug1: Offering public key: /home/u/.ssh/id_rsa RSA SHA256:...
debug1: Authentications that can continue: publickey,password
debug1: Server accepted key: /home/u/.ssh/id_rsa
debug1: Authenticated to 10.0.24.28
常见信号:
| 看到 | 含义 | 怎么修 |
|---|---|---|
Connection timed out | TCP 都没通 | 检查 IP、端口、防火墙;用 nc -zv host port 验证 |
Connection refused | 端口 reachable 但远端没有 sshd 监听 | 检查 sshd 服务、确认端口号 |
Permission denied (publickey) | TCP 通了、ssh 通了、但 key 不被认 | 见下面 |
Authentications that can continue: password | 远端拒绝 pubkey 认证 | 见 sshd.md |
第 2 步:Permission denied (publickey) 的排查链
- 你用的是哪把 key?
ssh -v m1看Offering public key行 - 这把 key 的公钥在远端
~/.ssh/authorized_keys里吗?ssh m1 'cat ~/.ssh/authorized_keys' # 还能登录的话直接看 - 权限对吗?
ssh m1 'ls -ld ~/.ssh ~/.ssh/authorized_keys' # ~/.ssh 必须 700 # ~/.ssh/authorized_keys 必须 600 或 644 - 远端 sshd 允许 pubkey 认证吗?
ssh m1 'sshd -T | grep -i pubkey' # pubkeyauthentication yes ← 应该是 yes - 远端有没有 SELinux/AppArmor 阻拦?(CentOS 系常见)
ssh m1 'getenforce' # SELinux 状态
第 3 步:用 ssh -G 看配置生效
参数行为对不上配置预期时:
ssh -G m1 | grep -E 'hostname|user|port|identityfile'
不连接就能看到最终用什么 IP、什么 user、哪把 key。详细见 ssh-config.md。
常见踩坑
坑 1:Host key verification failed
远端 IP 变了(云厂商重新分配、机器重装系统),本地 known_hosts 里记的旧指纹对不上。
# 删除这个 host 的旧指纹(用 IP 或别名都行)
ssh-keygen -R 10.0.24.28
ssh-keygen -R m1
或者临时跳过校验(仅限一次性机器):
ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null root@$ip
坑 2:长时间空闲就掉线(NAT/防火墙超时)
中间 NAT、运营商防火墙会定期清空闲的 TCP 连接。解决方法:
# ~/.ssh/config
Host *
ServerAliveInterval 30 # 每 30s 发一次保活包
ServerAliveCountMax 3 # 连续 3 次失败才断开
坑 3:heredoc 远程脚本里的引号被错误展开
ssh m1 'bash -s' <<EOF # 注意:EOF 没引号
echo "now: $(date)" # ❌ $(date) 在本地执行,远端打印的是本地时间
EOF
ssh m1 'bash -s' <<'EOF' # ✅ 加引号
echo "now: $(date)" # 远端执行 date,打印远端时间
EOF
默认习惯写 <<'EOF',需要本地变量时再有意识地去掉引号。
坑 4:脚本里 ssh 卡在密码提示
写在 cron 里的脚本,一旦 key 认证失败就会卡死等密码,没人能输。
ssh -o BatchMode=yes m1 'command' # 禁掉所有交互、失败就立即退出
坑 5:ssh 进程不退出(端口转发挂着)
ssh -L 8001:localhost:8001 m1 跑完想要的事之后没退出。如果是 -f 后台模式,ssh 进程留在系统里:
ps aux | grep ssh # 找到 PID
kill <pid>
# 或者按转发端口找:
lsof -i :8001
坑 6:Pseudo-terminal will not be allocated
ssh m1 'sudo vim /etc/hosts'
# Pseudo-terminal will not be allocated because stdin is not a terminal.
加 -t:ssh -t m1 'sudo vim /etc/hosts'。
关联命令
- ssh-config ——
~/.ssh/config把这些 ssh 参数压缩成 alias - ssh-keygen —— 生成本地密钥对
- sshpass —— 首次推 key 的密码自动化
- sshd —— 服务端配置(很多"连不上"问题在远端)
- scp / rsync —— 远程拷贝(共用 ssh 配置)