ssh 命令详解与参数实测(Ubuntu 22.04)

以 Ubuntu 22.04 OpenSSH 8.9p1 为实测环境,逐参数验证 ssh:远程命令执行与退出码透传、主机密钥确认策略、-p 端口、ed25519 密钥免密登录、-v 调试、-L 本地端口转发、-C 压缩字节级对照与 -o 连接参数,每项附真实回显与排障 FAQ。

Linux命令Linux系统UbuntuUbuntu 22.04命令行网络工具2026/10/16

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 / -YX11 转发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_keys 600,权限过松时 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 参数实测取值作用出现小节
StrictHostKeyCheckingask / no / accept-new主机密钥确认策略3.2
PubkeyAuthenticationno禁用公钥认证(做密码基线对照)3.4
IdentitiesOnlyyes只使用 -i 显式指定的私钥3.4
PasswordAuthenticationno禁用密码认证3.4
Compressionyes等价 -C3.8
ConnectTimeout3限制 TCP 建连超时秒数本节
ServerAliveInterval5每 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 节,此处按执行顺序归纳):

  1. 生成密钥对:ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519 -N "",得到私钥 id_ed25519 与公钥 id_ed25519.pub;
  2. 推送公钥:ssh-copy-id -i ~/.ssh/id_ed25519.pub tester@127.0.0.1(最后输一次密码),输出 Number of key(s) added: 1;
  3. 验证免密: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 输出一致,指纹比对正是防中间人的核心手段。

六、参考链接