简介:一份面向Linux运维工程师与开发者的实用技术文档,聚焦如何借助SSH协议在远程Linux系统上安全高效地执行命令,解决日常运维中频繁登录服务器、批量执行指令的痛点。资源为单个PDF文件,体积仅74KB,内容紧凑,便于随时查阅。文档从SSH工作原理入手,系统讲解基础连接语法、运行单条与多条命令、使用sudo提升权限、通过配置文件简化连接、配置SSH密钥免密登录等核心方法,并配有df、uptime、fdisk、systemctl及非标准端口连接等真实命令示例,直观展示输出效果和常见场景。无论临时查看磁盘状态、远程重启服务,还是管理多台主机,都能快速上手。内容按实战步骤组织,每类操作均给出可套用的命令行格式与实际输出,便于对照练习并掌握权限配置要点。已有580人学习下载,适合希望提升远程操作效率的初学者及需要快速查阅SSH用法的运维人员。
1. SSH 远程执行命令:从一次深夜救火说起
深夜接到电话,线上二十多台机器要统一改一个 Nginx 参数。你不可能一台台登进去改,哪怕每台只用两分钟,全部跑完也天亮。这时候你会想到让 SSH 远程执行命令上场:本地敲一条命令,把要做的操作发给远程 Linux,输出直接拉回本地终端。SSH 全称 Secure Shell,核心价值不只是“登录”,而是把命令、脚本、状态检查都做成可编程的通道。下面从最小的一条远程命令,到带 sudo、带后台任务的完整方案,以及我在实际运维里踩过的转义、密钥和断连的坑。适合每天要和大量 Linux 服务器打交道的运维、SRE 和用 vscode 连远程环境的开发者。先从最小闭环开始。
2. 在远程 Linux 上跑命令的最小闭环:单条命令到批量循环
理解 SSH 远程执行命令,第一步要接受一个反直觉的事实:ssh user@host 'command'并不是“登录进去再敲命令”,而是 SSH 客户端把后面的字符串原样交给远端用户的登录 shell 去执行,执行完连接就关闭。这个机制决定了后续所有写法和坑。
2.1 单条命令:外层引号决定谁先解释
最基础的用法是直接带一条命令,我一般会先加超时和免交互参数,避免在脚本里卡死:
ssh -o ConnectTimeout=10 -o BatchMode=yes deploy@192.168.1.20 'df -h /'拆开看这条命令:-o ConnectTimeout=10限制 TCP 握手 10 秒超时;-o BatchMode=yes表示不允许交互输密码,密钥认证失败就直接报错退出,这在高危的自动化脚本里非常有用,不会让整个 CI 挂在那里等人输入。最后'df -h /'用单引号包住,意思是这段内容本地 shell 不做任何展开,原样传给远端。
为什么强调单引号优先?因为双引号外层的命令会被本地 shell 先处理一遍,最常见的翻车现场是$PATH、$HOME这类变量被本地展开后才传到远端。后面 3.1 会细说,这里先记住一条经验:拿不准的时候,最外层一律用单引号。
多段命令可以用布尔符串起来,注意它们是在远端被 bash 解释的:
ssh root@10.0.0.5 "df -h /boot && echo 'boot size ok' || echo 'check failed'"这里我用了双引号,因为里面'boot size ok'需要单引号保护空格。逻辑是:外层双引号把整段传给远端,远端 bash 执行df -h /boot,成功后打印提示,失败走||分支。返回值取最后一条命令的状态码,这也是后面批量巡检判断成功失败的依据。
2.2 批量跑通多台机器:循环与并发控制
单台跑通以后,批量就是加一个循环。常见做法是把服务器列表放进文件,逐台执行同样的检查:
for ip in $(cat servers.txt); do echo "== $ip ==" ssh -o ConnectTimeout=5 -o BatchMode=yes root@$ip 'uptime && free -m | head -3' done这个循环里$(cat servers.txt)和$ip是在本地展开的,所以这里不能用单引号包整条命令,需要让本地先遍历 IP,再把固定的远端命令单引号包住。&&和管道|都在远端生效。逐台跑的缺点是慢,二十台机器每台等 2 秒就是 40 秒,可以接受;到两百台就要上并发。
并发执行我一般用 xargs。-P参数控制同时跑几个 SSH 进程,别一把梭全开,容易把本地 sshd 的连接数或者网络带宽打满:
xargs -P 5 -I{} ssh -o ConnectTimeout=5 -o BatchMode=yes root@{} 'hostname && uname -r' < servers.txt-I{}表示把 servers.txt 里每一行替换到{}位置,-P 5最多同时五个 SSH 会话。需要说明的是,并发输出的顺序会和文件顺序不一致,这是正常的;如果要求严格有序,就回到 for 循环。
这套命令最实用的场景之一是远程安全更新:先跑apt-get update -y或yum check-update做 dry-run,确认变更范围后再真正升级。我一般会先只跑前几台验证结果,确认没问题再放开全部机器,避免“一条命令带走整个机房”的事故。
2.3 用 bash -s 装载本地脚本:让复杂操作可以复用
命令一长,写在引号里就变成灾难。多台机器要执行同一套部署流程时,常见的做法是把脚本放在本地,通过 stdin 送过去执行:
ssh deploy@10.0.0.8 'bash -s' < ./deploy.sh原理是 SSH 会把本地文件内容作为标准输入传给远端的bash -s,远端 bash 逐行执行脚本,stdout 和 stderr 流回本地终端。这里的-s就是“从 stdin 读脚本”的意思。脚本的退出码按最后一条命令算,SSH 客户端会把这个状态码原样返回,本地能直接拿到$?。
带参数时注意分隔符:
ssh deploy@10.0.0.8 'bash -s --' < ./deploy.sh -f prod--之后的内容作为位置参数传给脚本,等价于在远端执行bash -s -- -f prod。脚本里用$1、$2读取。
踩坑提醒:本地脚本如果是 Windows 编辑保存的,行尾是 CRLF,远端 bash 会报$'\r': command not found之类的错误。传上去之前先确认行尾是 LF,或者干脆在脚本第一行加sed -i 's/\r$//'之类的前置处理。另外一个常见误用是ssh host < script.sh,这样也能跑,但远端的默认 shell 不一定是 bash,语法差异会埋雷,显式写bash -s更稳妥。
3. 把远程命令写得像本地一样顺手:引号、sudo 与重定向的四个边界
很多人用了一段时间 SSH 远程执行命令后,开始觉得“这不就是个远程 shell 吗”,于是把本地那套变量、管道、重定向习惯直接搬过去,结果翻车。本质原因是:命令字符串要过两层 shell——本地 shell 先解析一遍,远端 shell 再解析一遍。本章把四个最容易出界的点讲透。
3.1 单引号、双引号与反引号:谁的优先级更高
看下面这个对比,输出会完全不同:
pwd_dir=$(pwd) ssh user@host "echo 本地目录: $pwd_dir" ssh user@host 'echo 远端目录: $(pwd)'第一行pwd_dir=$(pwd)在本地执行,拿到本地当前目录。第二行用双引号,本地 shell 会先展开$pwd_dir,所以远端收到的是echo 本地目录: /home/me,打印的是本地路径。第三行用单引号,$(pwd)原样传到远端,由远端 bash 展开,打印的是远程目录。
这就是第一道边界:外层双引号 → 本地先展开;外层单引号 → 全部交给远端。反引号做命令替换同理。最保险的写法是外层单引号、内层双引号,例如:
ssh root@10.0.0.5 'echo "$HOME" && ls -l "/var/log/$(hostname).log"'这里外层的单引号让整个字符串原样传输,远端的双引号负责保护含空格的路径,$HOME和$(hostname)都由远端 bash 解释。本地要想插入自己的变量,简单方案是先用本地变量拼好字符串再传,或者用环境变量通过SendEnv机制送过去(需要服务端AcceptEnv配合,不展开)。
3.2 sudo 的三种姿势与 tty 陷阱
远程执行时遇到需要 root 权限的命令,第一个坑就出现了:直接ssh user@host 'sudo systemctl restart nginx'在很多系统上会报sudo: no tty present。原因是 sudo 默认要读密码,而 SSH 非交互执行时没有分配伪终端。解法是加-t:
ssh -t ops@10.0.0.9 'sudo systemctl restart nginx'-t强制分配一个 tty,sudo 就能弹密码提示了。代价是输出里会掺入终端控制字符,做日志解析时要清洗。如果用户已经配了 NOPASSWD sudo,就不需要-t,命令会更干净。
第二种姿势是sudo -S从 stdin 读密码:
echo "mypass" | ssh ops@host 'sudo -S apt-get update -y'但这会带来两个副作用:密码明文出现在本地 bash history 和远端进程列表里,非常不建议在共享机器上用。更干净的是第三种:在 sudoers.d 里给指定命令放行 NOPASSWD。我一般会单独建文件,不直接改/etc/sudoers:
cat > /tmp/ops-sudo << 'EOF' ops ALL=(ALL) NOPASSWD: /usr/bin/systemctl, /usr/bin/apt-get EOF scp /tmp/ops-sudo ops@host:/tmp/ ssh ops@host 'sudo install -m 440 /tmp/ops-sudo /etc/sudoers.d/ops-sudo'这样只放行特定命令,远程执行时不用密码,又不至于把整个 sudo 权限裸奔出去。
3.3 管道和重定向:到底在哪一端执行
第三个边界是管道符和重定向的归属。命令字符串里的>、>>、|,如果被单引号包住,就在远端执行;如果写在引号外面,就在本地执行。四条命令对比一下就清楚了:
ssh root@10.0.0.5 'echo remote > /tmp/r.txt' # 远端写文件 ssh root@10.0.0.5 'cat /tmp/r.txt' > local.txt # 本地写文件 ssh root@10.0.0.5 'cat /tmp/r.txt' | grep pattern # grep 在本地跑 ssh root@10.0.0.5 'cat /tmp/r.txt | grep pattern' # grep 在远端跑第二个场景很实用:把远程命令结果拉回本地存档。第三个场景要警惕——管道符在本地,意味着要传回全量内容,网络开销大;如果只想要过滤后的结果,不如把 grep 放进引号内,让远端过滤完再传,省带宽也省本地 CPU。判断标准很简单:引号内归远端,引号外归本地。
3.4 heredoc 传多行:多行脚本不再难读
命令超过五行时,字符串拼接会让代码像一团乱麻。我个人习惯用 heredoc 配合bash -s,可读性和可维护性都好很多:
ssh root@10.0.0.5 'bash -s' << 'EOF' set -e cd /opt/deploy ./run.sh --env prod tail -20 /var/log/run.log EOF这里的<< 'EOF'是关键:EOF 加了引号,表示 heredoc 里的所有内容不做本地变量展开,$PATH、$(date)这类都由远端 bash 处理。如果去掉引号,本地 shell 会先替换一遍,结果往往是变量变成空值,查半天查不出原因。set -e让脚本在第一条失败命令处停止,避免带着错误继续往下跑,这对远程批量操作特别重要。
需要传递本地变量时,可以在 heredoc 中只保留需要本地的部分,或者更稳妥的做法是在脚本里用环境变量名,然后用ssh user@host 'export APP_ENV=prod; bash -s'的方式在远端设置。
4. 免密登录与密钥管理:批量执行前必须先解决的三件事
批量执行命令时最难受的不是命令写错,而是每台机器都要输密码,输完密码还要等 known_hosts 确认。这一章把密钥认证的三件事一次讲清楚:生成安装密钥、修正好权限与 known_hosts、用 ssh_config 把连接参数固化下来。
4.1 生成密钥与安装公钥:ssh-copy-id 一行搞定
现代系统推荐用 Ed25519 密钥,短、快、安全性足够。生成命令:
ssh-keygen -t ed25519 -C "ops-laptop-2024" -f ~/.ssh/id_ed25519-C是注释,通常写用途或机器名,方便日后管理多把钥匙;-f指定私钥文件路径,不指定就交互式提问。老一点的环境如果只支持 RSA,生成时加-t rsa -b 4096,但新机器优先 ed25519。生成的id_ed25519.pub是公钥,id_ed25519是私钥,私钥永远不要离开本机。
安装公钥到远端,最省事的是 ssh-copy-id:
ssh-copy-id -i ~/.ssh/id_ed25519.pub ops@10.0.0.20它会自动把公钥追加到远端~/.ssh/authorized_keys,并修正目录权限。没有这个工具时,手工三步是:把公钥内容追加到 authorized_keys,chmod 700 ~/.ssh,chmod 600 ~/.ssh/authorized_keys。注意追加用cat >>而不是>,否则会覆盖掉这台机器上已有的其他公钥。
4.2 权限与 known_hosts:信任链上的两个细节
密钥认证配好后连不上,十有八九是权限问题。远端 SSH 服务对密钥相关文件权限极其敏感,过宽直接忽略。需要遵守的权限如下:
| 对象 | 建议权限 | 说明 |
|---|---|---|
| 用户家目录 | 700 | 组和其他用户不可进入 |
| ~/.ssh 目录 | 700 | 存放密钥的目录 |
| 私钥 id_ed25519 | 600 | 私钥必须仅本人可读写 |
| authorized_keys | 600 | 公钥列表 |
| known_hosts | 644 | 记录已信任主机公钥 |
用一条命令批量修正远端权限:
ssh ops@host 'chmod 700 ~/.ssh && chmod 600 ~/.ssh/authorized_keys && chmod 600 ~/.ssh/id_*'接下来是 known_hosts。当你第一次连接一台机器,SSH 会提示确认主机指纹,这就是要写入 known_hosts。在脚本里不能交互确认,我一般用-o StrictHostKeyChecking=accept-new,自动接受新主机但不接受已存在主机的变化。批量场景下可以预先用 ssh-keyscan 填充指纹:
ssh-keyscan -t ed25519 10.0.0.20 >> ~/.ssh/known_hostsssh-keyscan从目标机器抓取公钥写入本地信任库,后续连接不再询问。值得警惕的是:如果某台机器的 host key 突然变了,SSH 会报REMOTE HOST IDENTIFICATION CHANGED,这可能是中间人攻击,也可能是对方重装了系统。确认是重装后,用ssh-keygen -R 10.0.0.20删除旧记录再连。
4.3 ssh_config 与跳板机:别名化之后命令变短一半
需要管理的机器多了以后,每次都敲ssh -p 22022 -i ~/.ssh/id_ed25519_internal ops@10.0.0.30很痛苦。常见做法是把连接参数写进~/.ssh/config:
Host jump HostName 203.0.113.10 User ops IdentityFile ~/.ssh/id_ed25519_jump Host prod-* ProxyJump jump User admin IdentityFile ~/.ssh/id_ed25519_internal StrictHostKeyChecking accept-new配置里的语法不用=号,用空格分隔。ProxyJump jump表示所有匹配prod-*的主机都通过 jump 这台跳板机中转,SSH 会自动在跳板机上建立转发通道。配好之后,执行远程命令就从一大串变成ssh prod-01 'uptime',后端会自动用 admin 用户、指定私钥、走跳板连接。
重点提醒:IdentityFile指定多把私钥时,SSH 会逐个尝试,有时会先试错一把才连上,速度略慢但结果没问题。严格要指定某一把,可以用IdentitiesOnly yes,避免客户端把 ssh-agent 里的所有密钥都试一遍。
4.4 批量执行前的自检清单
批量命令跑之前,我习惯先做三件自检,避免跑到一半才发现一半机器连不上:
ssh-add -l第一件,确认密钥是否已加载到 agent。如果私钥有 passphrase 且没加进 ssh-agent,脚本批量执行时每次都要输入口令。ssh-add -l列出当前 agent 里的公钥指纹,为空就执行ssh-add ~/.ssh/id_ed25519加载。
ssh -v root@10.0.0.8 'true'第二件,用-v跑一次最小的连通性测试。-v会打印认证过程日志,能看到尝试了哪些密钥、最终用了哪个、known_hosts 是否通过。看到Authenticated字样就说明链路没问题。测试命令true无害,只返回成功。
第三件,做批量自检并收集失败列表:
fail_list=() for ip in $(cat servers.txt); do if ! ssh -q -o BatchMode=yes -o ConnectTimeout=5 root@$ip 'true'; then fail_list+=("$ip") fi done echo "失败: ${fail_list[@]}"-q抑制警告输出,BatchMode=yes保证不会卡在密码输入。失败的主机收集到一个数组里,最后统一打印,这样批量执行的真实失败面一目了然。
5. 远程执行命令常见问题排查:认证失败、断连与引号翻车
远程执行命令的排错和其他 Linux 问题不太一样:同样的报错,可能来自客户端配置、服务端 sshd、防火墙,或者纯粹是两层 shell 的引号问题。下面几条都是真实踩过的坑,按现象、原因、解决三件事写。
5.1 报错 Permission denied (publickey):密钥没生效,不是密码问题
现象:ssh user@host 'uptime'直接返回Permission denied (publickey),换密码登录也一样被拒。用-v看日志,发现客户端一直尝试密钥,但没有一个被服务端接受。
原因分两种:一种是公钥根本没进 authorized_keys,比如 ssh-copy-id 时用了>覆盖了旧内容;另一种是私钥在 agent 里加载错了,特别是同时有多把钥匙时容易串。git 连 SSH 时出现ssh认证失败 git也常是同一根因——Git 走的 SSH 用的密钥不是仓库管理员预期的那把。
解决:先用ssh-keyscan -t ed25519 user@host 2>/dev/null确认服务端在监听,再手工核对 authorized_keys 里是否有本机公钥。多密钥场景给每个主机在 ssh_config 里写死IdentityFile,并加IdentitiesOnly yes限定只能试这把,而不是把 agent 里的所有钥匙全试一遍。
5.2 新装的 Ubuntu/CentOS 连不上:ssh 服务和防火墙谁挡了路
现象:新装一台 Ubuntu,ssh user@ip卡在Connection timed out;CentOS 8 上执行了systemctl enable sshd后还是Connection refused。
原因要分两端排查。Ubuntu 默认镜像经常没装 openssh-server,systemctl status sshd会报服务不存在,先apt install openssh-server再systemctl start sshd。CentOS 8 上服务起来了但连不上,优先怀疑防火墙,firewall-cmd --list-all看 22 端口是否放行,没有就firewall-cmd --add-service=ssh --permanent && firewall-cmd --reload。
解决链路固定为三步:先看端口监听ss -tlnp | grep :22,确认 sshd 在跑且在 0.0.0.0 上监听;再看防火墙规则;最后看 SELinux,getenforce如果返回 Enforcing,用ausearch -m AVC -ts recent查是否有 sshd 相关拦截。前两步解决九成问题,SELinux 是最后一个大坑。
5.3 连接卡死:命令没执行完,还是 SSH 在等什么
现象:执行一条简单的ssh host 'uptime',敲下去十几秒都没反应,偶尔最终超时。
原因大多不是网络,而是 SSH 服务端的额外处理:DNS 反向解析(UseDNS)、GSSAPI 认证尝试、以及连接建立后客户端没有发送保活心跳。前两个发生在认证阶段,第三个发生在连接空闲时。
解决:服务端在/etc/ssh/sshd_config里设UseDNS no和GSSAPIAuthentication no,客户端在~/.ssh/config里加:
Host * ServerAliveInterval 30 ServerAliveCountMax 3ServerAliveInterval 30每 30 秒发一个保活包,ServerAliveCountMax 3连续三个没回应才判定断开,防止 SSH 会话因为网络设备空闲超时被掐断。改完服务端记得systemctl restart sshd。
5.4 引号转义翻车:本地变量被远端提前展开
现象:明明在远端执行echo $HOME,结果打印的是本机家目录路径;脚本里写rm -rf /tmp/$(date +%s),远端报找不到目录。
原因前面 3.1 提过:外层用了双引号,本地 shell 先把$HOME、$(date)展开完了,远端收到的是展开后的实际值。
解决:最外层一律单引号;需要本地变量参与时,把命令拆成先用 bash 拼接再传,例如cmd="echo $LOCAL_VAR"; ssh host \"$cmd\"。还要警惕反斜杠转义:想把远端命令里的$传给远端,单引号内直接写$HOME即可,不要在前面加反斜杠,加了反而把$传给远端 shell 后变成字面量。
5.5 麒麟/国产 Linux 只能往外连不能被别人连
现象:机器上执行ssh user@其他机器正常,但别的机器ssh root@本机就是Connection refused或超时。这也是国产 Linux 环境里被问得较多的一个问题。
原因要从三个角度查:sshd 是否真的在监听、监听地址是否是 127.0.0.1、以及系统层面有没有额外的安全策略。麒麟这类系统有些版本默认 sshd 只监听本机回环地址,或者安全组件拦截了外部入站连接。
解决:先ss -tlnp | grep :22看监听地址。如果显示127.0.0.1:22,编辑/etc/ssh/sshd_config,确认ListenAddress 0.0.0.0并重启 sshd。如果监听正常但外部仍连不上,依次检查 iptables 和系统自带的安全策略工具,执行iptables -L -n看是否有 DROP 规则,有就放行 22 端口。实在找不到方向时,在服务端开启journalctl -u sshd -f实时看日志,看连接请求是否到达 sshd,以此判断是被防火墙拦在门外,还是 sshd 拒绝了认证。
6. 让远程命令真正“跑完”:tmux 挂后台与结果回收
远程执行命令有个让人头疼的问题:一条长任务,比如apt upgrade或大数据导入,SSH 连接一断,远端 shell 收到 SIGHUP,任务跟着就没了。常见的做法是 nohup 加重定向,但 nohup 的 PID 不好记,任务跑到一半想看进度还得猜日志路径。我现在的习惯是:凡是超过五分钟的远程任务,一律用 tmux 或 systemd-run 托管。
先看 tmux 方案。连接后创建名为 patch 的会话,detach 回到本地,任务照常跑:
ssh -t ops@10.0.0.8 'tmux new -s patch -d' ssh -t ops@10.0.0.8 'tmux send-keys -t patch "sudo apt upgrade -y 2>&1 | tee /tmp/patch.log" Enter' ssh -t ops@10.0.0.8 'tmux attach -t patch'第一条tmux new -s patch -d创建独立会话并立即 detach;第二条send-keys把真正要执行的命令“敲”进那个会话,Enter是按下回车;第三条随时 attach 回去看实时输出。这样即使本地断网,远端 tmux 会话和里面的任务都还在。
另一个更“系统化”的方案是 systemd-run,把任务交给 systemd 管理,状态可查询、日志可 journalctl 拉取:
ssh ops@10.0.0.8 'systemd-run --unit=patch-task --collect bash -c "sudo apt upgrade -y > /tmp/patch.log 2>&1"'--unit=patch-task给任务命名,--collect让任务结束后自动清理单元。验证进度用systemctl status patch-task,看完整输出用journalctl -u patch-task。
任务跑完后,结果回收我一般用 scp 拉日志,--partial保证断点续传不重头再来:
scp -o ConnectTimeout=10 ops@10.0.0.8:/tmp/patch.log ./logs/这里加上本地时间戳再归档,避免多台机器的日志互相覆盖:
ts=$(date +%Y%m%d_%H%M) scp ops@10.0.0.8:/tmp/patch.log ./logs/10.0.0.8_$ts.log我早期用 nohup 跑批量任务,PID 丢了任务也找不到,后来把所有在线运维任务统一改成 tmux 或 systemd-run 托管,状态可查、日志可回收,再也没出现过“任务到底跑完没有”的尴尬。如果你也经常被远程长任务折磨,先别急着写 nohup,给 tmux 五分钟,它会成为你远程执行命令最顺手的一条路。希望帮到你。
本文还有配套的精品资源,点击获取