ssh 命令详解与参数实测(Ubuntu 22.04)
以 Ubuntu 22.04 OpenSSH 8.9p1 为实测环境,逐参数验证 ssh:远程命令执行与退出码透传、主机密钥确认策略、-p 端口、ed25519 密钥免密登录、-v 调试、-L 本地端口转发、-C 压缩字节级对照与 -o 连接参数,每项附真实回显与排障 FAQ。
ssh 命令详解与参数实测(Ubuntu 22.04)
目标:全面掌握 ssh 的每一个常用参数——远程登录与命令执行、主机密钥确认策略、端口指定、密钥免密登录、调试排障、本地端口转发、压缩传输与连接参数控制,全部命令与回显已在 Ubuntu 22.04(OpenSSH 8.9p1)实测通过。
一、环境信息
本节列出实测环境的完整信息,文中所有命令与回显均来自该环境,读者在同版本系统上可复现同样的结果。
- 操作系统:Ubuntu 22.04.5 LTS(jammy),架构 x86_64,内核
6.8.0-138-generic - 客户端与服务端:openssh-client / openssh-server 均已安装,版本
OpenSSH_8.9p1 Ubuntu-3ubuntu0.18,加密库OpenSSL 3.0.2 - sshd 监听:默认实例监听
22端口;为实测-p参数,另启动了一个监听2222端口的测试实例 - 测试账号:普通用户
tester(密码tester123)、tester1(密码tester1pass)——两个账号均为公开演示环境专用的测试凭据 - 密码自动化工具:
sshpass 1.09(apt 安装),用于在非交互脚本中自动填充密码 - 相关路径:
/root/.ssh/(客户端私钥与 known_hosts)、/root/demo/ssh/(演示数据目录) - 网络方式:SSH 目标全部使用本机回环地址
127.0.0.1自连自收,客户端与服务端同机,全程不依赖公网
二、命令概览
2.1 语法与测试数据
ssh(Secure Shell)是 OpenSSH 套件的远程登录客户端,通过对传输数据全程加密的方式,在不可信网络上提供安全的远程Shell、远程命令执行与端口转发能力。其基本语法为:
ssh [选项] [user@]hostname [command [argument ...]]
两个最常用的形态:
ssh user@host:不带 command,进入交互式登录 Shell;ssh user@host "command":带 command,在远端执行该命令后立即退出,本地退出码等于远端命令退出码。
⚠️ 注意:ssh 的端口只能用
-p选项指定,没有host:端口这种冒号写法(那是 scp 的远程路径语法)。这也是 ssh 与 scp 端口参数大小写易混淆的根源(scp 用大写-P,见 3.3 节)。
本文的测试数据约定:
- 目标主机:
127.0.0.1(回环自连),默认端口 22,测试实例端口 2222; - 登录账号:
tester,密码tester123,用sshpass -p tester123前缀实现非交互登录(仅演示环境如此使用,生产环境推荐密钥认证,见 3.4 节); - 演示目录:
/root/demo/ssh/,每个实验块自带数据准备与清理,可独立复现。
2.2 usage 用法总览
不带任何参数直接执行 ssh,会打印完整的 usage 用法说明(逐行照抄如下):
$ ssh
usage: ssh [-46AaCfGgKkMNnqsTtVvXxYy] [-B bind_interface]
[-b bind_address] [-c cipher_spec] [-D [bind_address:]port]
[-E log_file] [-e escape_char] [-F configfile] [-I pkcs11]
[-i identity_file] [-J [user@]host[:port]] [-L address]
[-l login_name] [-m mac_spec] [-O ctl_cmd] [-o option] [-p port]
[-Q query_option] [-R address] [-S ctl_path] [-W host:port]
[-w local_tun[:remote_tun]] destination [command [argument ...]]
从中可以看到 ssh 的参数全集,本文逐项实测其中最常用的一批,对应关系如下:
| 参数 | 作用 | 实测小节 |
|---|---|---|
| (无参数) | 远程登录 / 远程命令执行 | 3.1 |
-o StrictHostKeyChecking=... | 首次连接主机密钥确认策略 | 3.2 |
-p | 指定连接端口 | 3.3 |
-i | 指定私钥文件 | 3.4 |
-v | 调试输出 | 3.5 |
-N | 不执行远程命令 | 3.6 |
-L | 本地端口转发 | 3.7 |
-C | 压缩传输 | 3.8 |
-o | 通用连接参数 | 3.9 |
-G | 打印最终生效配置 | 3.10 |
-X / -Y | X11 转发 | 3.11 |
💡 说明:usage 中还有
-R(远程端口转发)、-D(动态 SOCKS 转发)、-J(跳板机 ProxyJump)、-F(指定配置文件)、-W(标准输入输出转发)等参数,其中端口转发三类在 3.7 节的拓展说明中一并对比。
2.3 版本信息
ssh -V
OpenSSH_8.9p1 Ubuntu-3ubuntu0.18, OpenSSL 3.0.2 15 Mar 2022
💡 说明:
ssh -V的输出打印在标准错误上(用ssh -V 2>&1才能重定向捕获),打印后直接退出。输出同时包含客户端版本(OpenSSH_8.9p1 及 Ubuntu 打包后缀)与链接的加密库版本(OpenSSL 3.0.2),排障时先报这两个版本号。scp 与 ssh 同属 OpenSSH 套件,版本一致,无需单独查询。
三、参数逐项实测
3.1 基础形态:远程登录与命令执行
先验证 ssh 最基础的两个形态:登录后执行单条命令、执行多条命令。本节同时确认「远端命令退出码会透传给本地」这一脚本化运维的关键语义。
sshpass -p tester123 ssh tester@127.0.0.1 "echo hi"
hi
sshpass -p tester123 ssh tester@127.0.0.1 "whoami; hostname; uname -r"
tester
f1034b9326b2
6.8.0-138-generic
sshpass -p tester123 ssh tester@127.0.0.1 "date +%F\ %T; uptime"
2026-10-15 19:55:52
19:55:52 up 1:53, 2 users, load average: 0.56, 0.45, 0.29
sshpass -p tester123 ssh tester@127.0.0.1 "exit 7"; echo rc=$?
rc=7
验证结论:
- 引号内的多个命令原样交给远端 Shell 执行,分号分隔的多条命令依次输出;
exit 7让远端 Shell 以 7 退出,本地rc=7证明 ssh 把远端命令退出码原样透传,脚本中可直接用$?判断远端执行成败;- 不带 command 时 ssh 进入交互式会话,退出码同样是远端会话的退出码。
💡 说明:
sshpass -p tester123是演示环境的自动化辅助手段(把密码喂给 ssh 的密码提示)。生产环境不建议在命令行明文传密码(会留在 Shell 历史与进程列表中),推荐 3.4 节的密钥认证。
3.2 首次连接的主机密钥确认(StrictHostKeyChecking)
ssh 客户端第一次连接一台主机时,会把服务端的主机公钥指纹展示给用户确认,确认后写入 ~/.ssh/known_hosts;之后每次连接都核对服务端出示的密钥,防止「中间人」冒充服务器。首次连接的行为由 StrictHostKeyChecking 策略控制。先用 rm 清空 known_hosts 模拟全新客户端:
rm -f /root/.ssh/known_hosts /root/.ssh/known_hosts.old; echo known_hosts-reset
known_hosts-reset
策略一:ask(默认值)。用 script 命令分配伪终端模拟真实交互场景,输入 no 拒绝:
printf "no\n" | script -qec "ssh -o StrictHostKeyChecking=ask tester@127.0.0.1 echo x" /dev/null; echo rc=$?
no
The authenticity of host '127.0.0.1 (127.0.0.1)' can't be established.
ED25519 key fingerprint is SHA256:qLHC9Vkif/PjQXqKSm8tKVKDcH92W3pBKF/N5QuU+3I.
This key is not known by any other names
Are you sure you want to continue connecting (yes/no/[fingerprint])? Host key verification failed.
rc=255
策略二:no,不询问直接信任并写入 known_hosts(仅供自动化场景,有中间人风险):
sshpass -p tester123 ssh -o StrictHostKeyChecking=no tester@127.0.0.1 "echo no-mode-ok"; echo rc=$?
Warning: Permanently added '127.0.0.1' (ED25519) to the list of known hosts.
no-mode-ok
rc=0
策略三:accept-new,首次自动接受新主机并记录,之后已记录主机的密钥变化仍会拒绝(推荐折中方案)。再次清空 known_hosts 后实测:
rm -f /root/.ssh/known_hosts /root/.ssh/known_hosts.old; echo reset-again
reset-again
sshpass -p tester123 ssh -o StrictHostKeyChecking=accept-new tester@127.0.0.1 "echo accept-new-ok"; echo rc=$?
Warning: Permanently added '127.0.0.1' (ED25519) to the list of known hosts.
accept-new-ok
rc=0
known_hosts 中已有记录后,第三次连接不再出现任何提示,直接登录成功:
sshpass -p tester123 ssh tester@127.0.0.1 "echo third-connect-no-warning"; echo rc=$?
third-connect-no-warning
rc=0
💡 技术拓展:主机密钥确认策略不止本文实测的三种,全量对比如下——
策略 行为 适用场景 yes只信任 known_hosts 已有密钥,新主机直接拒绝 安全要求最高的生产环境 ask(默认)新主机交互询问,用户确认后记录 人工日常使用 accept-new新主机自动记录,已记录密钥变化仍拒绝 自动化与安全的折中(本文推荐) no任何密钥都自动信任并记录,密钥变化也照连 一次性测试环境(有中间人风险) 本文实测采用
accept-new作为自动化推荐值;交互场景保持默认ask即可。
💡 说明:服务端实际出示的主机密钥可以在
-v调试输出中核对——3.5 节日志里的Server host key: ssh-ed25519 SHA256:qLHC9Vkif/PjQXqKSm8tKVKDcH92W3pBKF/N5QuU+3I与本节ask模式展示的指纹完全一致,这正是指纹比对的用途。删除某台主机的旧记录用ssh-keygen -R <主机名>,不要手工编辑 known_hosts(记录可能是哈希形态)。
3.3 -p:连接指定端口
sshd 服务端默认监听 22 端口,但生产中出于规避扫描或一机多实例的需要,经常改用其他端口,客户端就要用 -p 指定。先在服务器上再启动一个监听 2222 的 sshd 实例(-p 2222 让该实例只监听 2222),并用 ss 确认两个端口都在监听:
ss -tln | grep -q ":2222 " || /usr/sbin/sshd -p 2222; sleep 1; ss -tlnp 2>/dev/null | grep sshd
LISTEN 0 128 0.0.0.0:22 0.0.0.0:* users:(("sshd",pid=2423,fd=3))
LISTEN 0 128 0.0.0.0:2222 0.0.0.0:* users:(("sshd",pid=2914,fd=3))
LISTEN 0 128 [::]:22 [::]:* users:(("sshd",pid=2423,fd=4))
LISTEN 0 128 [::]:2222 [::]:* users:(("sshd",pid=2914,fd=4))
用 -p 2222 连接该实例(这是另一套监听套接字,主机密钥记录形式为 [127.0.0.1]:2222,与 22 端口的记录互相独立):
sshpass -p tester123 ssh -p 2222 -o StrictHostKeyChecking=accept-new tester@127.0.0.1 "echo port2222-ok"; echo rc=$?
port2222-ok
rc=0
连一个没有服务监听的端口,看典型报错形态:
sshpass -p tester123 ssh -p 2221 tester@127.0.0.1 "echo x" 2>&1; echo rc=$?
ssh: connect to host 127.0.0.1 port 2221: Connection refused
rc=255
⚠️ 注意:
Connection refused说明「主机可达但端口没人监听」,排查方向是服务端ss -tln核对端口;若是Connection timed out则是网络层不通或防火墙丢弃,两者要区分。ssh 自身错误(连不上、认证失败、主机密钥被拒)退出码统一为 255,与远端命令的正常退出码区分开。
💡 说明:ssh 的端口参数是小写
-p;而 scp 是大写-P(因为 scp 的小写-p被「保留文件时间戳」占用了),这是本组工具链里最经典的易错点。让 sshd 固定监听非默认端口的持久做法是在服务端/etc/ssh/sshd_config中设置Port 2222后重启 sshd 服务。
3.4 -i:指定私钥文件(ed25519 密钥免密登录全流程)
密码认证之外,ssh 更推荐公钥认证:客户端持私钥、服务端 ~/.ssh/authorized_keys 里放对应公钥,登录全程不传密码。本节从零实测:生成 ed25519 密钥对 → ssh-copy-id 推送公钥 → -i 免密登录,并先留一个纯密码认证的基线做对照。
基线:显式禁用公钥认证,确认当前只能靠密码登录:
sshpass -p tester123 ssh -o PubkeyAuthentication=no tester@127.0.0.1 "echo password-auth-baseline"; echo rc=$?
password-auth-baseline
rc=0
第一步,生成 ed25519 密钥对(-f 指定私钥文件,-N "" 表示私钥不设口令,-C 加注释):
ssh-keygen -t ed25519 -f /root/.ssh/demo_ed25519 -N "" -C "demo@ubuntu2204"
Generating public/private ed25519 key pair.
Your identification has been saved in /root/.ssh/demo_ed25519
Your public key has been saved in /root/.ssh/demo_ed25519.pub
The key fingerprint is:
SHA256:Z+H6wk1AlkVaOGq4J2vTDBdrFZZoFZWleQc/0oYmf9c demo@ubuntu2204
The key's randomart image is:
+--[ED25519 256]--+
| ooO=oo |
| o Xo.o = |
| o =.o= = * |
| . + o. * + ..|
| o +S.+ . . E|
| + = +. . . |
| X ..o |
| + o o.. |
| . . .. |
+----[SHA256]-----+
第二步,ssh-copy-id 把公钥追加到服务端账号的 authorized_keys(该命令本质上就是一条把公钥写进远端文件的 ssh 命令,此处仍需密码):
sshpass -p tester123 ssh-copy-id -i /root/.ssh/demo_ed25519.pub tester@127.0.0.1
/usr/bin/ssh-copy-id: INFO: Source of key(s) to be installed: "/root/.ssh/demo_ed25519.pub"
/usr/bin/ssh-copy-id: INFO: attempting to log in with the new key(s), to filter out any that are already installed
/usr/bin/ssh-copy-id: INFO: 1 key(s) remain to be installed -- if you are prompted now it is to install the new keys
Number of key(s) added: 1
Now try logging into the machine, with: "ssh 'tester@127.0.0.1'"
and check to make sure that only the key(s) you wanted were added.
第三步,用 -i 指定私钥登录——注意命令里没有 sshpass、没有输密码,直接成功:
ssh -i /root/.ssh/demo_ed25519 tester@127.0.0.1 "echo key-auth-ok; whoami"; echo rc=$?
key-auth-ok
tester
rc=0
第四步,强制「只允许公钥」的组合参数:IdentitiesOnly=yes 限定只用 -i 指定的这把钥匙,PasswordAuthentication=no 彻底禁掉密码回退——这是验证密钥配置真正生效的标准做法:
ssh -i /root/.ssh/demo_ed25519 -o IdentitiesOnly=yes -o PasswordAuthentication=no tester@127.0.0.1 "echo pubkey-only-ok"; echo rc=$?
pubkey-only-ok
rc=0
常见误用:把 .pub 公钥文件当成私钥传给 -i,看报错形态(完整保留):
ssh -i /root/.ssh/demo_ed25519.pub tester@127.0.0.1 "echo x" 2>&1
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@ WARNING: UNPROTECTED PRIVATE KEY FILE! @
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
Permissions 0644 for '/root/.ssh/demo_ed25519.pub' are too open.
It is required that your private key files are NOT accessible by others.
This private key will be ignored.
Load key "/root/.ssh/demo_ed25519.pub": bad permissions
Permission denied, please try again.
Permission denied, please try again.
tester@127.0.0.1: Permission denied (publickey,password).
实验完毕,把公钥从服务端移除、删除密钥文件,恢复环境:
sshpass -p tester123 ssh tester@127.0.0.1 "grep -v demo_ed25519 ~/.ssh/authorized_keys > /tmp/ak && mv /tmp/ak ~/.ssh/authorized_keys; echo key-removed"
key-removed
rm -f /root/.ssh/demo_ed25519 /root/.ssh/demo_ed25519.pub; echo keyfiles-cleaned
keyfiles-cleaned
💡 技术拓展:ssh 支持的密钥算法不止本文用的 Ed25519,主流对比如下——
算法 特点 适用场景 Ed25519 密钥短、签名快、安全性高,OpenSSH 6.5+ 支持 新部署首选(本文实测采用) RSA 兼容性最好,3072 位起才够安全,OpenSSH 8.8 后默认禁用 ssh-rsa(SHA-1)签名 老旧设备互联 ECDSA(nistp256/384/521) 密钥短、速度与 Ed25519 相当 兼容不支持 Ed25519 的较新设备 sk-ed25519 / sk-ecdsa 绑定 FIDO2 硬件安全钥匙,需触摸确认 高安全要求的人工登录 本文选择 Ed25519:一条
ssh-keygen -t ed25519即可,密钥文件最小、无算法参数纠结。
💡 说明:服务端公钥清单文件是账号家目录下的
~/.ssh/authorized_keys,一行一把公钥。权限要求严格:家目录不能对组/其他人可写,.ssh目录 700,authorized_keys600,权限过松时 sshd 会静默忽略公钥——这是密钥配好后仍要密码的最常见原因。
3.5 -v:调试输出逐段解读
-v 是 ssh 排障的第一工具,把连接全过程逐阶段打印出来。下面是一次完整登录的 -v 输出(逐行照抄,共约 90 行),随后按阶段解读:
sshpass -p tester123 ssh -v tester@127.0.0.1 "echo verbose-done" 2>&1
OpenSSH_8.9p1 Ubuntu-3ubuntu0.18, OpenSSL 3.0.2 15 Mar 2022
debug1: Reading configuration data /etc/ssh/ssh_config
debug1: /etc/ssh/ssh_config line 19: include /etc/ssh/ssh_config.d/*.conf matched no files
debug1: /etc/ssh/ssh_config line 21: Applying options for *
debug1: Connecting to 127.0.0.1 [127.0.0.1] port 22.
debug1: Connection established.
debug1: identity file /root/.ssh/id_rsa type -1
debug1: identity file /root/.ssh/id_rsa-cert type -1
debug1: identity file /root/.ssh/id_ecdsa type -1
debug1: identity file /root/.ssh/id_ecdsa-cert type -1
debug1: identity file /root/.ssh/id_ecdsa_sk type -1
debug1: identity file /root/.ssh/id_ecdsa_sk-cert type -1
debug1: identity file /root/.ssh/id_ed25519 type -1
debug1: identity file /root/.ssh/id_ed25519-cert type -1
debug1: identity file /root/.ssh/id_ed25519_sk type -1
debug1: identity file /root/.ssh/id_ed25519_sk-cert type -1
debug1: identity file /root/.ssh/id_xmss type -1
debug1: identity file /root/.ssh/id_xmss-cert type -1
debug1: identity file /root/.ssh/id_dsa type -1
debug1: identity file /root/.ssh/id_dsa-cert type -1
debug1: Local version string SSH-2.0-OpenSSH_8.9p1 Ubuntu-3ubuntu0.18
debug1: Remote protocol version 2.0, remote software version OpenSSH_8.9p1 Ubuntu-3ubuntu0.18
debug1: compat_banner: match: OpenSSH_8.9p1 Ubuntu-3ubuntu0.18 pat OpenSSH* compat 0x04000000
debug1: Authenticating to 127.0.0.1:22 as 'tester'
debug1: load_hostkeys: fopen /root/.ssh/known_hosts2: No such file or directory
debug1: load_hostkeys: fopen /etc/ssh/ssh_known_hosts: No such file or directory
debug1: load_hostkeys: fopen /etc/ssh/ssh_known_hosts2: No such file or directory
debug1: SSH2_MSG_KEXINIT sent
debug1: SSH2_MSG_KEXINIT received
debug1: kex: algorithm: curve25519-sha256
debug1: kex: host key algorithm: ssh-ed25519
debug1: kex: server->client cipher: chacha20-poly1305@openssh.com MAC: <implicit> compression: none
debug1: kex: client->server cipher: chacha20-poly1305@openssh.com MAC: <implicit> compression: none
debug1: expecting SSH2_MSG_KEX_ECDH_REPLY
debug1: SSH2_MSG_KEX_ECDH_REPLY received
debug1: Server host key: ssh-ed25519 SHA256:qLHC9Vkif/PjQXqKSm8tKVKDcH92W3pBKF/N5QuU+3I
debug1: load_hostkeys: fopen /root/.ssh/known_hosts2: No such file or directory
debug1: load_hostkeys: fopen /etc/ssh/ssh_known_hosts: No such file or directory
debug1: load_hostkeys: fopen /etc/ssh/ssh_known_hosts2: No such file or directory
debug1: Host '127.0.0.1' is known and matches the ED25519 host key.
debug1: Found key in /root/.ssh/known_hosts:1
debug1: ssh_packet_send2_wrapped: resetting send seqnr 3
debug1: rekey out after 134217728 blocks
debug1: SSH2_MSG_NEWKEYS sent
debug1: expecting SSH2_MSG_NEWKEYS
debug1: ssh_packet_read_poll2: resetting read seqnr 3
debug1: SSH2_MSG_NEWKEYS received
debug1: rekey in after 134217728 blocks
debug1: Will attempt key: /root/.ssh/id_rsa
debug1: Will attempt key: /root/.ssh/id_ecdsa
debug1: Will attempt key: /root/.ssh/id_ecdsa_sk
debug1: Will attempt key: /root/.ssh/id_ed25519
debug1: Will attempt key: /root/.ssh/id_ed25519_sk
debug1: Will attempt key: /root/.ssh/id_xmss
debug1: Will attempt key: /root/.ssh/id_dsa
debug1: SSH2_MSG_EXT_INFO received
debug1: kex_input_ext_info: server-sig-algs=<ssh-ed25519,sk-ssh-ed25519@openssh.com,ssh-rsa,rsa-sha2-256,rsa-sha2-512,ssh-dss,ecdsa-sha2-nistp256,ecdsa-sha2-nistp384,ecdsa-sha2-nistp521,sk-ecdsa-sha2-nistp256@openssh.com,webauthn-sk-ecdsa-sha2-nistp256@openssh.com>
debug1: kex_input_ext_info: publickey-hostbound@openssh.com=<0>
debug1: SSH2_MSG_SERVICE_ACCEPT received
debug1: Authentications that can continue: publickey,password
debug1: Next authentication method: publickey
debug1: Trying private key: /root/.ssh/id_rsa
debug1: Trying private key: /root/.ssh/id_ecdsa
debug1: Trying private key: /root/.ssh/id_ecdsa_sk
debug1: Trying private key: /root/.ssh/id_ed25519
debug1: Trying private key: /root/.ssh/id_ed25519_sk
debug1: Trying private key: /root/.ssh/id_xmss
debug1: Trying private key: /root/.ssh/id_dsa
debug1: Next authentication method: password
Authenticated to 127.0.0.1 ([127.0.0.1]:22) using "password".
debug1: channel 0: new [client-session]
debug1: Requesting no-more-sessions@openssh.com
debug1: Entering interactive session.
debug1: pledge: filesystem
debug1: client_input_global_request: rtype hostkeys-00@openssh.com want_reply 0
debug1: client_input_hostkeys: searching /root/.ssh/known_hosts for 127.0.0.1 / (none)
debug1: client_input_hostkeys: searching /root/.ssh/known_hosts2 for 127.0.0.1 / (none)
debug1: client_input_hostkeys: hostkeys file /root/.ssh/known_hosts2 does not exist
debug1: client_input_hostkeys: no new or deprecated keys from server
debug1: Sending environment.
debug1: Sending command: echo verbose-done
verbose-done
debug1: client_input_channel_req: channel 0 rtype exit-status reply 0
debug1: channel 0: free: client-session, nchannels 1
Transferred: sent 2020, received 2488 bytes, in 0.1 seconds
Bytes per second: sent 37477.4, received 46160.3
debug1: Exit status 0
按阶段解读这份日志:
| 阶段 | 关键行 | 含义 |
|---|---|---|
| 读配置 | Reading configuration data /etc/ssh/ssh_config | 客户端加载了哪些配置文件与匹配块 |
| TCP 连接 | Connecting to ... port 22 → Connection established | 三次握手完成;若卡在这一行之前是网络/防火墙问题 |
| 身份文件探测 | identity file ... type -1 | 逐一探测默认私钥是否存在,type -1 表示文件不存在(正常现象,不是错误) |
| 协议横幅 | Local/Remote version string | 双方交换 SSH-2.0 协议版本与软件版本 |
| 密钥交换(KEX) | kex: algorithm: curve25519-sha256、cipher: chacha20-poly1305@openssh.com | 协商出会话加密套件;Server host key 行给出服务端主机密钥指纹 |
| 主机核对 | Host '127.0.0.1' is known and matches ... | 与 known_hosts 比对通过 |
| 认证 | Authentications that can continue: publickey,password → Next authentication method | 服务端允许的认证方式列表,客户端按 publickey→password 顺序尝试;Authenticated ... using "password" 是认证成功标志 |
| 会话 | channel 0: new [client-session] → Sending command | 建立会话通道、发送命令、回传输出 |
| 收尾 | Transferred: sent/received、Exit status 0 | 本次会话传输字节数与远端退出码 |
💡 说明:排障口诀——问题在「连接」段找网络,在「KEX/主机核对」段找 known_hosts,在「认证」段找密钥与密码,
-vv、-vvv可输出更细的报文级日志。日志中Will attempt key列出的默认私钥全部不存在,因此回退到password方式——这也解释了 3.4 节为什么需要先ssh-copy-id。
3.6 -N:不执行远程命令
-N 告诉 ssh「登录后什么都不做」,不打开 Shell、不执行命令,会话只用于端口转发等附加通道。单独使用时它只会挂着——用 timeout 3 包住实测(3 秒后被 timeout 杀掉):
timeout 3 sshpass -p tester123 ssh -N tester@127.0.0.1; echo rc=$?
rc=124
rc=124 是 timeout 杀掉超时进程的标准退出码,证明这 3 秒里 ssh 确实一直挂起、没有执行任何命令。-N 的真正价值是配合 -L 做纯转发隧道,见下一节。
💡 说明:
-N常与-f(认证后转入后台)连用:ssh -N -f -L ...可以让隧道在后台常驻,不占用终端也不产生远端 Shell。
3.7 -L:本地端口转发
-L 在本地开一个监听端口,把经过它的流量通过 SSH 加密隧道转发到「从服务端视角可达」的目标地址,语法 -L [本地地址:]本地端口:目标地址:目标端口。实测场景:服务端本机 8000 端口跑着一个只绑定 127.0.0.1 的 HTTP 服务,从「客户端视角」通过隧道 12345 端口访问它。
第一步,准备演示数据并启动本地 HTTP 服务(绑定 127.0.0.1:8000),先直连验证服务正常:
mkdir -p /root/demo/ssh/www && printf "<h1>hello via ssh tunnel</h1>\n" > /root/demo/ssh/www/index.html && cat /root/demo/ssh/www/index.html
<h1>hello via ssh tunnel</h1>
cd /root/demo/ssh/www && nohup python3 -m http.server 8000 --bind 127.0.0.1 >/root/demo/ssh/http.log 2>&1 & sleep 1; curl -s http://127.0.0.1:8000/
<h1>hello via ssh tunnel</h1>
第二步,建立隧道:-N -L 12345:127.0.0.1:8000 组合,含义是「不执行命令,把本地 12345 端口转发到(从服务端看的)127.0.0.1:8000」。用 timeout 15 包裹防止演示进程残留,后台启动:
timeout 15 sshpass -p tester123 ssh -N -L 12345:127.0.0.1:8000 tester@127.0.0.1 >/root/demo/ssh/tunnel.log 2>&1 & sleep 2; echo tunnel-up
tunnel-up
第三步,访问本地 12345 端口验证流量真的走了隧道(返回内容与直连 8000 一致):
curl -s http://127.0.0.1:12345/; echo rc=$?
<h1>hello via ssh tunnel</h1>
rc=0
curl -sI http://127.0.0.1:12345/ | head -4
HTTP/1.0 200 OK
Server: SimpleHTTP/0.6 Python/3.10.12
Date: Thu, 15 Oct 2026 19:56:01 GMT
Content-type: text/html
第四步,清理隧道与服务进程,确认端口释放(输出中的 Terminated 是 Shell 报告后台任务被 pkill 终止,属预期现象):
pkill -f "http.server 8000"; pkill -f "ssh -N -L 12345"; sleep 1; ss -tln 2>/dev/null | grep -E ":8000 |:12345 " || echo ports-released
Terminated
ports-released
💡 技术拓展:SSH 端口转发共三类,方向和用途不同——
类型 语法 方向 典型用途 本地转发 -L-L 12345:host:port本地端口 → 经服务端 → 目标 加密访问内网 Web/数据库(本文实测) 远程转发 -R-R 12345:host:port服务端端口 → 经客户端 → 目标 把内网机器的服务暴露给外部 动态转发 -D-D 1080本地 SOCKS 代理,目标由应用指定 临时加密代理上网 本文实测
-L;三类都建议搭配-N(不执行命令)与-f(后台运行),并在脚本中加-o ExitOnForwardFailure=yes,转发建立失败时立即退出而不是空挂。
3.8 -C:压缩传输
-C 请求对 SSH 会话数据做 zlib 压缩(等价 -o Compression=yes),对慢速广域网上的高可压缩数据(文本、日志、代码)能明显省流量。实测对照:构造 300000 字节的高重复度文本,分别在开/关压缩下把同一文件经 SSH 传回本地,用 -v 输出的传输字节数做对比。
准备测试文件并复制一份到 /tmp(保证远程账号可读):
mkdir -p /root/demo/ssh && yes "the quick brown fox jumps over the lazy dog" | head -c 300000 > /root/demo/ssh/sample.txt && cp /root/demo/ssh/sample.txt /tmp/sample_ssh.txt && chmod 644 /tmp/sample_ssh.txt && wc -c /tmp/sample_ssh.txt
300000 /tmp/sample_ssh.txt
不压缩(基线):确认内容完整传回 300000 字节,SSH 层接收 302652 字节:
sshpass -p tester123 ssh -v tester@127.0.0.1 "cat /tmp/sample_ssh.txt" >/tmp/out-noc.bin 2>/tmp/v-noc.log; echo rc=$?; wc -c /tmp/out-noc.bin; grep -E "Transferred|Bytes per second" /tmp/v-noc.log
rc=0
300000 /tmp/out-noc.bin
Transferred: sent 2048, received 302652 bytes, in 0.1 seconds
Bytes per second: sent 37248.6, received 5504571.8
开启 -C:内容同样完整(300000 字节一字不差),SSH 层接收降到 173684 字节:
sshpass -p tester123 ssh -C -v tester@127.0.0.1 "cat /tmp/sample_ssh.txt" >/tmp/out-c.bin 2>/tmp/v-c.log; echo rc=$?; wc -c /tmp/out-c.bin; grep -E "Transferred|Bytes per second" /tmp/v-c.log
rc=0
300000 /tmp/out-c.bin
Transferred: sent 2100, received 173684 bytes, in 0.1 seconds
Bytes per second: sent 37475.4, received 3099469.4
从调试日志确认压缩确实协商生效(compression: zlib@openssh.com):
grep -E "kex: server->client cipher" /tmp/v-c.log
debug1: kex: server->client cipher: chacha20-poly1305@openssh.com MAC: <implicit> compression: zlib@openssh.com
清理演示数据:
rm -f /tmp/sample_ssh.txt /tmp/out-noc.bin /tmp/out-c.bin /tmp/v-noc.log /tmp/v-c.log /root/demo/ssh/sample.txt; echo compression-demo-cleaned
compression-demo-cleaned
💡 说明:本例 302652 → 173684 字节,流量省约 42.6%(数据是刻意构造的高重复文本,实际压缩率取决于内容)。对已经压缩过的数据(jpg、mp4、zip、tar.gz)再开
-C几乎无收益,反而消耗两侧 CPU;千兆内网一般不开,跨公网传日志/文本时值得开。
3.9 -o:通用连接参数(ConnectTimeout 实测)
-o 可以在命令行直接给任意一条 ssh_config 配置项赋值,优先级高于配置文件,常用于脚本中「一次性」调整行为。最常用的是 ConnectTimeout——默认情况下连接一个不通的地址要等系统级 TCP 超时(可达 2 分钟),显式设短才能让脚本快速失败。向一个不可路由的地址发起连接实测:
sshpass -p tester123 ssh -o ConnectTimeout=3 -o StrictHostKeyChecking=no tester@10.255.255.1 "echo x" 2>&1; echo rc=$?
ssh: connect to host 10.255.255.1 port 22: Connection timed out
rc=255
3 秒即超时退出,退出码 255(ssh 自身错误)。本文已实测过的其余常用 -o 参数汇总:
-o 参数 | 实测取值 | 作用 | 出现小节 |
|---|---|---|---|
StrictHostKeyChecking | ask / no / accept-new | 主机密钥确认策略 | 3.2 |
PubkeyAuthentication | no | 禁用公钥认证(做密码基线对照) | 3.4 |
IdentitiesOnly | yes | 只使用 -i 显式指定的私钥 | 3.4 |
PasswordAuthentication | no | 禁用密码认证 | 3.4 |
Compression | yes | 等价 -C | 3.8 |
ConnectTimeout | 3 | 限制 TCP 建连超时秒数 | 本节 |
ServerAliveInterval | 5 | 每 5 秒发心跳保活、探测死链 | 4.1 |
这些参数都可以组合使用,实测一组「加固型」连接参数:
sshpass -p tester123 ssh -o ConnectTimeout=3 -o ServerAliveInterval=5 -o StrictHostKeyChecking=accept-new -o IdentitiesOnly=no tester@127.0.0.1 "echo combo-ok; date +%F\ %T"; echo rc=$?
combo-ok
2026-10-15 19:58:29
rc=0
💡 说明:
-o的键名与/etc/ssh/ssh_config中的指令完全同名,等价于「命令行版的配置文件」。脚本里还有两个高频值:BatchMode=yes(禁止任何交互提示,无法认证时立即失败)与UserKnownHostsFile=/dev/null(忽略 known_hosts,仅测试用)。
3.10 -G:打印最终生效配置
-G 不发起连接,把「目标主机 + 命令行参数 + 配置文件」合并计算后的最终生效值逐行打印,用于确认配置是否按预期生效:
ssh -G tester@127.0.0.1 2>/dev/null | grep -E "^(user|hostname|port|passwordauthentication|pubkeyauthentication|compression) "
user tester
hostname 127.0.0.1
port 22
compression no
passwordauthentication yes
pubkeyauthentication true
💡 说明:从输出可核对默认值——端口 22、压缩关闭、密码与公钥认证都开启。调试「配置文件写了却不生效」的问题时,先
-G看最终值,再回查是哪一层配置覆盖了它。
3.11 -X / -Y:X11 转发(说明)
-X 开启 X11 转发,把远端图形程序的窗口通过 SSH 隧道投到本地 X 服务上显示,-Y 是取消安全扩展限制的可信转发(兼容性更好)。使用前提:本地运行着 X Server,远端装有 xauth。本文实测环境是纯命令行服务器、没有 X 显示系统,因此仅给出标准用法形态供参考:
ssh -X user@host # 开启 X11 转发后,直接运行远端图形程序(如 xclock)
ssh -Y user@host # 可信 X11 转发,绕过部分扩展限制
⚠️ 注意:X11 转发依赖本地图形环境,纯服务器之间无法演示;若远端缺
xauth,连接时会报Warning: No xauth data且转发静默失效。
3.12 scp / sftp 与 ssh 的关系
ssh 协议之上承载着两套文件传输工具:scp(老式的 rcp 风格复制)与 sftp(SSH 文件传输协议子系统)。sftp 与 ssh 共用同一加密传输层与认证体系,能 ssh 登录的账号就能 sftp 连接,实测:
echo "ls -l /etc/hostname" | sshpass -p tester123 sftp tester@127.0.0.1 2>&1
Connected to 127.0.0.1.
sftp> ls -l /etc/hostname
-rw-r--r-- ? 0 0 13 Oct 15 18:05 /etc/hostname
💡 说明:OpenSSH 8.9 的
scp默认仍走传统 SCP/RCP 协议(OpenSSH 9.0 起改为默认 SFTP),sftp则始终是 SFTP 子系统。二者与 ssh 的关系是「同一传输层、不同应用协议」——认证、主机密钥、端口参数全部共享(所以ssh -p 2222能连的端口,scp -P 2222也能用)。逐参数实测见本系列《scp 命令详解与参数实测(Ubuntu 22.04)》。
四、实战示例
4.1 场景一:脚本化巡检(命令执行 + 退出码判断)
脚本巡检的三要素:非交互认证(sshpass 或密钥)、采集多台指标(多命令拼接)、用退出码判断成败。单台巡检的完整形态:
sshpass -p tester123 ssh tester@127.0.0.1 "whoami; hostname; uname -r"
tester
f1034b9326b2
6.8.0-138-generic
sshpass -p tester123 ssh tester@127.0.0.1 "date +%F\ %T; uptime"
2026-10-15 19:55:52
19:55:52 up 1:53, 2 users, load average: 0.56, 0.45, 0.29
退出码透传让脚本可以直接判断远端成败——远端命令 exit 7,本地 $? 即为 7:
sshpass -p tester123 ssh tester@127.0.0.1 "exit 7"; echo rc=$?
rc=7
💡 说明:巡检脚本建议固定加
-o ConnectTimeout=3 -o BatchMode=yes(见 3.9 节),坏节点秒级跳过而不是卡死整个批次。
4.2 场景二:从零配置密钥免密(可复用流程)
一次配置、永久免密的标准三步流程(完整回显见 3.4 节,此处按执行顺序归纳):
- 生成密钥对:
ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519 -N "",得到私钥id_ed25519与公钥id_ed25519.pub; - 推送公钥:
ssh-copy-id -i ~/.ssh/id_ed25519.pub tester@127.0.0.1(最后输一次密码),输出Number of key(s) added: 1; - 验证免密:
ssh -i ~/.ssh/id_ed25519 tester@127.0.0.1 "echo key-auth-ok",实测输出key-auth-ok、rc=0,全程无密码提示。
验证密钥真正生效的加固组合(禁掉密码回退)实测输出为 pubkey-only-ok:
ssh -i /root/.ssh/demo_ed25519 -o IdentitiesOnly=yes -o PasswordAuthentication=no tester@127.0.0.1 "echo pubkey-only-ok"; echo rc=$?
pubkey-only-ok
rc=0
4.3 场景三:穿透访问只监听本机的服务
只绑定 127.0.0.1 的服务(数据库、管理后台等)从外部无法直接访问,用 -N -L 隧道穿透。实测链路:服务监听 127.0.0.1:8000 → 隧道本地端口 12345 → curl 127.0.0.1:12345 拿到与直连一致的内容(完整流程与清理见 3.7 节):
curl -s http://127.0.0.1:12345/; echo rc=$?
<h1>hello via ssh tunnel</h1>
rc=0
curl -sI http://127.0.0.1:12345/ | head -4
HTTP/1.0 200 OK
Server: SimpleHTTP/0.6 Python/3.10.12
Date: Thu, 15 Oct 2026 19:56:01 GMT
Content-type: text/html
4.4 场景四:加固型连接参数组合
把 3.9 节的参数组合成脚本模板:快速超时 + 心跳保活 + 主机密钥自动记录,实测通过:
sshpass -p tester123 ssh -o ConnectTimeout=3 -o ServerAliveInterval=5 -o StrictHostKeyChecking=accept-new -o IdentitiesOnly=no tester@127.0.0.1 "echo combo-ok; date +%F\ %T"; echo rc=$?
combo-ok
2026-10-15 19:58:29
rc=0
反面验证:BatchMode=yes 禁止一切交互提示,在没有可用密钥的裸客户端上会立即失败而不是挂起等输密码——适合放进 CI 流水线快速暴露配置问题:
ssh -o BatchMode=yes -o ConnectTimeout=3 tester@127.0.0.1 "echo x" 2>&1; echo rc=$?
tester@127.0.0.1: Permission denied (publickey,password).
rc=255
五、注意事项与常见问题
Q1:提示 Permission denied (publickey,password) 怎么排查?
先用 ssh -v 看认证段(3.5 节日志):Authentications that can continue 列出服务端允许的方式;Trying private key 显示尝试了哪些私钥。常见原因依次是:密码错、私钥没被服务端信任(authorized_keys 没加对)、~/.ssh 或 authorized_keys 权限过松被 sshd 拒绝。权限基准:家目录不给组/其他用户写权限、.ssh 700、authorized_keys 600。
Q2:私钥文件权限不对是什么报错?
实测把公钥文件误传给 -i 时出现完整告警(见 3.4 节):WARNING: UNPROTECTED PRIVATE KEY FILE! + Permissions 0644 ... are too open + This private key will be ignored.——ssh 客户端要求私钥不能被其他用户读取,chmod 600 私钥 即可修复。
Q3:服务器重装系统后连接被拒,报 REMOTE HOST IDENTIFICATION HAS CHANGED! 怎么办?
服务端主机密钥变了,与 known_hosts 旧记录不一致,ssh 主动中断以防中间人。确认是服务器重装(而非劫持)后,执行 ssh-keygen -R 主机名 删除旧记录再连。不要按提示手工改 known_hosts——条目可能是哈希形态。
Q4:sshpass 的退出码 5 和 ssh 的 255 有什么区别?
实测错密码时输出 Permission denied, please try again. 且 rc=5——这个 5 是 sshpass 的错误码(密码不正确);ssh 自身的认证失败退出码是 255。脚本判断时要区分:rc=5 是凭据错,rc=255 是连接/认证/主机密钥等 ssh 层错误,远端命令的正常退出码则原样透传(如 3.1 节的 rc=7)。
Q5:端口参数到底用 -p 还是 -P?
ssh 用小写 -p(3.3 节实测 2222 端口);scp 用大写 -P;rsync 走 ssh 时写作 -e "ssh -p 2222"。三者不一致是历史遗留,记住「ssh 独小写」即可。此外 ssh 不支持 host:port 冒号写法。
Q6:连接超时怎么控制?
TCP 层默认超时太长(可能两分钟),脚本必须显式 -o ConnectTimeout=3。实测向不可路由地址连接,3 秒报 Connection timed out、rc=255(见 3.9 节)。区分两种失败:Connection refused 是端口没监听(服务问题),Connection timed out 是网络不通或被防火墙丢弃。
Q7:ssh -N 挂着不动是坏了吗?
不是。-N 的语义就是「不执行命令、只保持会话」,实测单独运行 3 秒后被 timeout 杀掉(rc=124,见 3.6 节)。它就是为端口转发设计的,日常与 -L/-R/-D、-f 连用。
Q8:首次连接的指纹确认能否跳过?
分场景选择(对比见 3.2 节拓展表):自动化脚本推荐 StrictHostKeyChecking=accept-new(新主机自动记录、密钥漂移仍拦截);一次性测试可用 no;生产交互保持默认 ask,并人工核对指纹——本环境实测指纹 SHA256:qLHC9Vkif/... 与服务端 -v 输出一致,指纹比对正是防中间人的核心手段。
六、参考链接
- ssh 手册页(man7.org):https://man7.org/linux/man-pages/man1/ssh.1.html
- OpenSSH 官网与发布说明:https://www.openssh.com/
- OpenSSH 8.9 发布说明:https://www.openssh.com/releasenotes/8.9
- Ubuntu 22.04(jammy)ssh 手册页:https://manpages.ubuntu.com/manpages/jammy/en/man1/ssh.1.html
- Ubuntu 22.04(jammy)ssh-copy-id 手册页:https://manpages.ubuntu.com/manpages/jammy/en/man1/ssh-copy-id.1.html
- ssh_config 配置项手册:https://man7.org/linux/man-pages/man5/ssh_config.5.html