整理记录 SSH 的常见概念、认证方式与典型用法。

基本使用(1)

ssh 最基本的用途就是登录远程服务器

1
ssh user@hostname

其中:

  • user 是登录用户名
  • hostname 是主机名,它可以是域名,也可以是某个具体的 IP 地址或局域网内部的主机名。

可以缺省用户名,此时将使用本地用户名作为远程服务器的登录用户名。

1
ssh hostname

用户名也可以通过 -l 参数指定,这样用户名和主机名就不用写在一起了,在脚本中可能更方便

1
ssh -l username hostname

ssh 默认连接远程服务器的 22 端口。使用 -p 参数也可以指定其他端口;前提是远程 SSH 服务端确实监听在该端口上。

1
ssh -p 8821 foo.com

ssh 在连接到远程服务器后会进行验证:如果第一次通过 ssh 连接某一台服务器, 命令行会显示一段文字,表示不认识这台机器,提醒用户确认是否需要连接

1
2
3
The authenticity of host 'foo.com (192.168.121.111)' can't be established.
ECDSA key fingerprint is SHA256:Vybt22mVXuNuB5unE++yowF7lgA/9/2bLSiO3qmYWBY.
Are you sure you want to continue connecting (yes/no)?

这表示客户端尚未记录该主机的主机密钥,需要确认是否信任它。 如果 known_hosts 中已经存在该主机的记录,但当前服务端提供的主机密钥与记录不匹配,OpenSSH 通常会直接给出明显的安全警告,而不是进入这种首次确认流程。

“服务器指纹”是服务端主机公钥(host key)的哈希值,用于确认连接的是否仍是同一台主机。下面的命令可以查看公钥指纹:

1
2
ssh-keygen -l -f /etc/ssh/ssh_host_ecdsa_key.pub
256 SHA256:Vybt22mVXuNuB5unE++yowF7lgA/9/2bLSiO3qmYWBY root@server (ECDSA)

ssh 会将已接受过的主机公钥或其哈希标识记录在 ~/.ssh/known_hosts 文件中。 之后再次连接时,客户端会拿当前服务端提供的主机公钥与该文件中的记录进行比对:

  • 如果是首次见到的主机,通常会提示用户确认;
  • 如果公钥匹配,则继续连接;
  • 如果公钥不匹配,则会触发主机密钥变更警告,这通常意味着服务器重装、更换了主机密钥,或者存在中间人攻击风险。

我们可以使用下面的命令检查指定的主机名是否在 known_hosts 中,或者将指定主机名从 known_hosts 中移除

1
2
3
4
5
# find
ssh-keygen -F example.com

# remove
ssh-keygen -R example.com

在主机密钥校验通过后,ssh 会进入用户认证阶段。服务端可能允许多种认证方式,例如密码认证、公钥认证、键盘交互认证等;具体是否要求输入密码,取决于服务端配置以及客户端可用的认证材料。

有时我们只需要临时登录并执行一条简单命令,可以把命令直接加在后面,在登录之后会自动执行这条命令并输出到本地,然后自动退出 ssh 登录,例如

1
ssh user@hostname cat /etc/hosts

密钥

非对称加密算法

SSH 支持多种公钥算法,用于公钥认证、主机密钥以及签名验证:

  • RSA:历史较久且仍被广泛支持。现代环境通常使用 rsa-sha2-256 或 rsa-sha2-512 签名;基于 SHA-1 的旧签名算法 ssh-rsa 已被现代 OpenSSH 默认禁用。
  • ECDSA:基于椭圆曲线,常见于现代 OpenSSH 环境。
  • Ed25519:通常被认为是在现代环境中兼顾安全性、性能与密钥体积的优选方案之一。

除此之外,还有一些已经不建议在新环境中继续使用的旧选项,例如 SSH 协议版本 1 相关能力以及 DSA(ssh-dss)。

关于密钥的位数:

  • RSA:建议至少使用 2048 位;在较长生命周期的场景中,3072 位或 4096 位更常见。
  • ECDSA:只支持 256、384、521 三种曲线参数。
  • Ed25519:参数固定,不需要像 RSA 那样手动选择位数。

在新的个人或团队环境中,通常优先选择 Ed25519;如果需要兼容旧系统,再考虑 RSA。ECDSA 也可使用,但并不是“位数越长越值得默认选择”,应结合兼容性与现网要求决定。

密钥文件

密钥主要用于证明客户端身份。对于同一台客户端机器上的同一用户,通常会维护一组或多组密钥对。 用户必须妥善保管私钥,并将对应公钥部署到需要登录的远程服务器上,见下文。 密钥通常存放在 ~/.ssh 目录下,下面是这些文件的默认名称

  • id_rsa,id_rsa.pub:用于 SSH 协议版本 2 的 RSA 私钥和公钥。
  • id_ecdsa,id_ecdsa.pub:ECDSA 私钥和公钥。
  • id_ed25519,id_ed25519.pub:ED25519 私钥和公钥。
  • identity,identity.pub:历史遗留名称,对现代 OpenSSH 基本没有实际意义。

OpenSSH 通常要求私钥文件不能对其它用户可读或可写,否则可能拒绝使用。常见做法是将私钥设为 600,公钥设为 644。

私钥文件和公钥文件都是文本文件,私钥例如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
-----BEGIN OPENSSH PRIVATE KEY-----
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
yf/PzW634X+YF3AAAACWxpc2h1QFRYSgE=
-----END OPENSSH PRIVATE KEY-----

不同算法的安全性不能只通过“文件长度”直接比较。一般来说,Ed25519 和 ECDSA 的密钥材料更短,但这并不代表安全性更低。

公钥例如

1
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCmP1rbqoMBBO9rBNSfRYZyyRUlWeF1pldu3bW8MMfXf/oR78CEN4MT643ZV994qlukZJHjja3cE9X23mUley78DMUaSCIDF+KuBlZYNv1Iaok6MMbUMNfXXESms5bjAT76kpx35IGvbSyOnpgmXMeMcxGbRtgbe448mZCESh8/+mUciznqZiHjhC8oZWJOrvJyx/TYR/wfoNz1MxfafBf0HEUSShxU6HjfQHlt5+rA++mzjOtlTEcqxWIXKDvDy3iVn885kwpUQazQ8D2rJZBmycggRI4TGw6iZYyp5YZ9akXnW2qmT71GNxI6Xt0SUOQIlHHD/9U1yZhebAbkCaPzHbiG1XII78PVxOUcSyo7cp5fVmINwgjEuq0oCbwSdfGPISl32NKeBzGDSLz/qwb9YQXGZRv5FDWP7TZkO78r8+RGKDDWHP8F7XuE3sPrObkCTkMim1jxWdIdXBs/JbG/MJQaplseUKGTKlmeJ5x9SvsmlaE09i2ST7dIAnlrtK0= your_email@domain.com

公钥内容通常只有一行,比私钥长度小很多。公钥文件的开头会表明当前算法类型,密钥数据之后还可以包含一段可选注释。ssh-keygen 默认使用 user@host,但注释可以是任意文本,也可以省略;它不是密钥的一部分,可以直接修改而不影响密钥对。

密钥生成

OpenSSH 使用 ssh-keygen 生成密钥:

1
ssh-keygen

ssh-keygen 需要我们确认或修改几个信息:

  1. 保存的路径和文件名,使用默认的路径和文件名即可,还可以通过 -f 选项指定,见下文
  2. 私钥保护口令(passphrase)。如果设置了口令,客户端在使用私钥时通常需要先解锁该私钥;也可以借助 ssh-agent 缓存解锁状态。是否留空取决于你的安全要求

最后 ssh-keygen 会生成一对密钥对,将其存储到指定位置,然后显示公钥的指纹和一个基于公钥生成的图像, 用图像来判断比使用字符串形式的指纹对人类更加友好

1
2
3
4
5
6
7
8
9
10
11
12
13
14
The key fingerprint is:
SHA256:MaA3rGf/h3Jh5jVehAw6QerUG0TMOTJN/n93P2zwMXs xxx@yyy
The key's randomart image is:
+--[ED25519 256]--+
| .B=. |
| oo*B . |
| . *o*+ o . |
| = .o* o . |
| . + S.. . |
| o . +.o..o |
| .+ =.o= *|
| ..+ o. BE|
| o.. . +|
+----[SHA256]-----+

ssh-keygen 支持很多选项,下面介绍几个常用选项。

-t 选项可以指定密钥类型。现代 OpenSSH 中常直接显式指定,例如:

1
2
ssh-keygen -t rsa
ssh-keygen -t ed25519

对于位数可变的算法,-b 选项可以指定密钥的二进制位数

1
ssh-keygen -t rsa -b 4096

对于位数可变的算法,-b 必须使用该算法支持的合法参数。对于 Ed25519 这类固定参数算法,-b 不适用。

-C 选项可以为公钥文件指定任意注释,默认值通常是当前的 user@host

1
ssh-keygen -C "your_email@domain.com"

-f 参数可以指定生成的私钥文件(否则使用默认名称,并存放在 ~/.ssh/ 目录下)

1
ssh-keygen -f mykey

这个命令会在当前目录下生成私钥文件 mykey 和公钥文件 mykey.pub。

-N 参数可以指定私钥的保护密码(passphrase),可以直接留空,也可以交互式输入

1
ssh-keygen -N ''

可以使用下面的命令根据已有私钥导出对应公钥:

1
2
ssh-keygen -y -f key_file_name # 输出到标准输出流
ssh-keygen -y -f key_file_name > key_file_name.pub

例如,显式生成一把 RSA 密钥:

1
ssh-keygen -t rsa -C "your_email@domain.com" -f key_file_name -N ''

基本使用(2)

通常更推荐使用公钥认证而不是纯密码认证。大致流程如下:

  • 预备步骤,客户端通过 ssh-keygen 生成公钥和私钥,并将公钥部署到远程服务器。
  • 第一步,客户端向服务器发起 SSH 连接,并协商协议参数。
  • 第二步,客户端可以先提交不带签名的公钥探测请求,确认服务端是否接受该公钥;也可以跳过探测,直接提交带签名的认证请求。
  • 第三步,客户端使用私钥对包含会话标识在内的认证数据进行签名,并把公钥和签名发送给服务端。
  • 第四步,服务端确认该公钥已获授权并验证签名;验证通过则证明客户端持有对应私钥。

密钥对的生成在上面已经介绍,下面关注将公钥上传到服务器中。

用户公钥保存在服务器的 ~/.ssh/authorized_keys 文件中,把公钥添加到这个文件之中就是把公钥上传到服务器了。 authorized_keys 是纯文本文件,其中可以包含多个公钥,每个公钥占一行,把新的公钥追加进去即可。 如果 authorized_keys 文件不存在,也可以直接手动创建。

OpenSSH 会检查 ~/.ssh 目录和 authorized_keys 的权限。常见可用设置是:~/.ssh 为 700,authorized_keys 为 600;在部分环境中 644 也可能可用,但更稳妥的是收紧为仅账户本人可写。

用户可以先用密码登录远程服务器,再手动编辑该文件;也可以直接在本机执行下面的命令:

1
cat ~/.ssh/id_rsa.pub | ssh user@host "mkdir -p ~/.ssh && chmod 700 ~/.ssh && cat >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys"

更常用的做法是使用 ssh-copy-id:

1
ssh-copy-id -i ~/.ssh/id_ed25519.pub user@host

手动追加时,注意将文件替换为需要上传的公钥,并确保服务器上的 authorized_keys 没有格式错误且权限正确。

配置好公钥后,可以用 -i 指定私钥文件进行认证:

1
ssh -i /path/to/private_key user@hostname

如果省略 -i,ssh 客户端通常会尝试若干默认身份文件,例如:

  • ~/.ssh/id_rsa
  • ~/.ssh/id_ecdsa
  • ~/.ssh/id_ecdsa_sk
  • ~/.ssh/id_ed25519
  • ~/.ssh/id_ed25519_sk
  • ~/.ssh/id_dsa(旧算法,现代环境通常不再使用)

我们还可以使用 ~/.ssh/config 文件进行更详细的配置,见下文。

配置

客户端配置

ssh 客户端通常需要关注如下两个配置文件:

  • ~/.ssh/known_hosts:记录已接受的远程主机公钥标识。部分场景下还可能出现 known_hosts.old 之类的备份文件,但不应把它视为所有环境中的固定行为;
  • ~/.ssh/config:用户级配置文件,与之相对的是全局配置文件 /etc/ssh/ssh_config,用户级的优先级更高。

config 配置文件由一系列 Host 块组成,每个块包含一组针对特定主机或一组主机的配置选项。 当多个 Host 块都匹配某个目标时,同一配置项通常以“首次取得的值”为准,因此一般把更具体的规则放在前面、通用规则放在后面。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Host myserver1.example.com
User user1
Port 2200

Host myserver2
HostName myserver2.example.com
User user2
Port 2222

Host myserver3
HostName myserver3.example.com
User user3
Port 2200
IdentityFile ~/.ssh/id_rsa3

Host *.example.com
User commonuser
Port 2222

# 通配符配置通常放在最后
Host *
ServerAliveInterval 60

解释一下这里的选项:

  • Host <pattern>:匹配主机别名或目标模式,可以是具体主机名、别名,也可以包含通配符
  • HostName:实际要连接的主机名
  • User:登录用户名
  • Port:端口号(默认是 22)。
  • IdentityFile:用于身份认证的私钥文件路径;即使指定了该选项,客户端通常仍会依次尝试 agent 中的其他身份,配合 IdentitiesOnly yes 才能限制为配置的身份文件
  • ServerAliveInterval:客户端定期向服务端发送保活消息的间隔秒数
  • ServerAliveCountMax:连续多少次保活无响应后,客户端判定连接失效

服务端配置

ssh 服务端通常需要关注如下两个配置文件:

  • ~/.ssh/authorized_keys:记录允许通过公钥认证登录该账户的客户端公钥;
  • /etc/ssh/sshd_config:ssh 服务端(sshd 服务)的全局配置文件。

修改服务器上的 /etc/ssh/sshd_config 可以控制 sshd 的监听参数以及用户认证行为,例如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 设置 sshd 的监听端口(还需要同步调整防火墙等网络策略)
# 默认:22
Port 2222

# 禁用 password 认证
# OpenSSH 上游默认:yes
PasswordAuthentication no

# 如果还要禁用键盘交互认证,需要单独设置
KbdInteractiveAuthentication no

# 禁止 root 直接通过 SSH 远程登录(只允许先用普通用户进行 SSH 登陆,然后切换为 root 用户)
# OpenSSH 上游默认:`prohibit-password`,即禁止 root 使用密码和键盘交互认证远程登录
PermitRootLogin no

发行版可能覆盖上游默认值,实际配置可以使用 sshd -T 查看。

配置修改后应先使用 sshd -t 检查语法,并保留现有会话,再重新加载或重启 sshd,通过新连接验证配置。

修改 sshd 以及防火墙的配置都要非常小心,因为配置错误或者操作不当可能就把自己关在外面了,无法远程连接,只能去机房修复配置。

除了配置文件,ssh 服务端在 /etc/ssh/ 目录中还存储了所有的服务端密钥对,例如 /etc/ssh/ssh_host_ecdsa_key, 这些服务端密钥用于向客户端证明“自己就是这台主机”。如果系统重装、重新初始化 SSH 服务,或显式重建主机密钥,客户端侧保存的主机指纹就会发生不匹配,需要重新核验并更新记录。对于需要保持主机身份连续性的服务器,确实应提前备份这些主机密钥。

在自建 Git 服务器中,git 账户通常不应获得常规交互式 shell。常见做法是把该账户的登录 shell 设为 git-shell:

1
git:x:1003:1003:,,,:/home/git:/usr/bin/git-shell

如果直接禁止该账户通过 SSH 登录,也可能连带影响基于 SSH 的 Git 仓库访问,因此要根据部署方式区分“禁止交互式 shell”和“禁止 SSH 连接”这两件事。

文件权限小结

整理一下 Linux 下常见的 .ssh/ 权限建议:

  • ~/.ssh/ 目录:通常设为 700
  • 私钥:通常设为 600,400 也常见
  • 公钥:通常设为 644
  • authorized_keys:建议设为 600
  • config、known_hosts:通常设为 600 或 644,关键是不能让权限宽松到被 OpenSSH 视为不安全

进阶使用

端口常识

下面是不同端口号段的含义:

  • 0-1023:系统端口(System Ports),预留给常见服务和协议;
  • 1024-49151:用户端口(User Ports),可以注册给特定服务;
  • 49152-65535:动态或私有端口(Dynamic/Private Ports),通常用于临时连接。

知名端口例如:

  • HTTP:80
  • HTTPS:443
  • FTP:21
  • SSH:22
  • DNS:53

可以使用下面的命令查看当前是否有进程在某个端口上处于监听状态:

1
2
3
4
5
# Linux
ss -lntp 'sport = :22022'

# Windows
netstat -ano | findstr :22022

如果想停止该监听,应该定位对应的进程并终止它。对于 SSH 场景,更准确地说是停止 sshd 进程或调整其监听配置,而不是“关闭端口本身”。

监听地址同样重要:127.0.0.1:8080 通常只有本机能够连接,而 0.0.0.0:8080 会监听所有 IPv4 网络接口,其他主机也可能连接。

端口转发

ssh 除了登录服务器,还有一大用途:建立加密隧道,也就是端口转发(port forwarding)。 常见用途包括:

  • 通过 SSH 通道承载原本未加密的应用层流量;
  • 借助可达的 SSH 主机作为跳板,访问原本不可直接建立连接的目标服务。

注意:SSH 只加密客户端与 SSH 服务端之间的隧道。如果目标服务位于第三台主机上,SSH 服务端到目标服务之间的流量不会因为端口转发自动加密,仍应根据需要使用 HTTPS、TLS 等协议保护。

转发方向

三种转发都在已有的 SSH 连接中承载 TCP 流量。-L 和 -R 的区别是监听端口所在的一侧,-D 则在本地建立 SOCKS 代理。

本地转发 -L:入口位于 SSH 客户端,SSH 服务端负责连接目标服务。

flowchart LR
    APP["本地应用<br/>连接 127.0.0.1:13306"]
    CLIENT["SSH 客户端<br/>监听本地端口 13306"]
    SERVER["SSH 服务端<br/>db-server"]
    TARGET["目标服务<br/>127.0.0.1:3306"]

    APP --> CLIENT
    CLIENT ==>|SSH 隧道| SERVER
    SERVER --> TARGET

远程转发 -R:入口位于 SSH 服务端,SSH 客户端负责连接目标服务。

flowchart LR
    APP["远端应用<br/>连接 127.0.0.1:18080"]
    SERVER["SSH 服务端<br/>监听远程端口 18080"]
    CLIENT["SSH 客户端<br/>运行 ssh -R"]
    TARGET["客户端一侧的服务<br/>127.0.0.1:3000"]

    APP --> SERVER
    SERVER ==>|SSH 隧道| CLIENT
    CLIENT --> TARGET

动态转发 -D:入口是 SSH 客户端上的 SOCKS 代理,应用为每次连接指定不同的目标。

flowchart LR
    APP["浏览器或命令行工具"]
    CLIENT["SSH 客户端<br/>SOCKS5 127.0.0.1:1080"]
    SERVER["SSH 服务端<br/>proxy-host"]
    WEB1["目标一<br/>example.com:443"]
    WEB2["目标二<br/>内网服务:端口"]

    APP -->|SOCKS 请求| CLIENT
    CLIENT ==>|SSH 隧道| SERVER
    SERVER --> WEB1
    SERVER --> WEB2

命令中 localhost 的含义取决于由哪一侧发起目标连接:

  • -L 中的目标地址由 SSH 服务端一侧访问,因此其中的 localhost 指 SSH 服务端;
  • -R 中的目标地址由 SSH 客户端一侧访问,因此其中的 localhost 指运行 ssh 命令的机器;
  • -D 的目标地址由使用代理的应用逐次提供,不需要预先写死。

建立隧道时常用以下选项:

  • -N:不执行远程命令,仅建立转发;
  • -T:不分配伪终端;
  • -f:认证成功后转入后台,通常与 -N 配合;
  • -o ExitOnForwardFailure=yes:无法建立请求的监听端口时退出。

本地转发(Local port forwarding)

本地转发在 SSH 客户端监听端口,由 SSH 服务端连接目标服务。

本地转发的命令格式为:

1
ssh -L [bind_address:]local_port:target_host:target_port user@ssh_server

其中:

  • bind_address 和 local_port:SSH 客户端上的监听地址和端口;
  • target_host 和 target_port:由 ssh_server 访问的目标地址和端口;
  • ssh_server:承载隧道并代替客户端访问目标的 SSH 服务端。

target_host 只需能被 ssh_server 访问。若不需要让其他主机使用该转发,bind_address 应写成 127.0.0.1。

  1. 访问远程 MySQL,但数据库只监听在服务器本机的 3306 端口
1
ssh -NT -o ExitOnForwardFailure=yes -L 127.0.0.1:13306:127.0.0.1:3306 user@db-server

建立隧道后,本地数据库客户端可以直接连接:

1
mysql -h 127.0.0.1 -P 13306 -u root -p

这里两个回环地址属于不同机器:第一个是 SSH 客户端上的监听地址,第二个是 db-server 看到的目标地址。

  1. 访问远程机器上的 Web 管理界面,例如只允许本机访问的 http://localhost:8080
1
ssh -NT -L 127.0.0.1:8080:127.0.0.1:8080 user@web-server

此时在本地浏览器访问 http://127.0.0.1:8080,实际访问到的是远程机器上的 localhost:8080。

  1. 通过跳板机访问内网中的 SSH 主机
1
2
ssh -NT -L 127.0.0.1:22022:10.0.0.15:22 user@jump-host
ssh -p 22022 user@127.0.0.1

这个例子适合解释本地转发原理;如果目的只是登录 SSH 主机,使用后文介绍的 -J 更直接:

1
ssh -J jump-user@jump-host target-user@10.0.0.15

若要让其他主机连接本地转发端口,可以指定 0.0.0.0 或使用 -g,同时应检查本机防火墙和目标服务的认证,避免意外暴露内网服务。

远程转发(Remote port forwarding)

远程转发在 SSH 服务端监听端口,由 SSH 客户端连接目标服务。

远程转发的命令格式为:

1
ssh -R [bind_address:]remote_port:target_host:target_port user@ssh_server

其中:

  • bind_address 和 remote_port:SSH 服务端上的监听地址和端口;
  • target_host 和 target_port:由 SSH 客户端一侧访问的目标;
  • ssh_server:接受 SSH 连接并创建远程监听端口的主机。

例如,内网机器 B 可以连接公网服务器 A,也可以访问内网主机 C。在 B 上执行:

1
ssh -NT -R 127.0.0.1:22022:C-host:22 user@A-host

其中 A 是 SSH 服务端和远程监听端,B 是 SSH 客户端,C-host:22 由 B 负责访问。建立隧道后,在 A 上连接自己的 localhost:22022 即可到达 C:

1
ssh -p 22022 user@localhost

更常见的远程转发例子:

  1. 把本地开发中的 Web 服务临时暴露到远程服务器上

假设本地正在运行一个开发服务:

1
python3 -m http.server 3000 --bind 127.0.0.1

然后把本地 3000 端口转发到远程服务器的 18080 端口:

1
ssh -NT -o ExitOnForwardFailure=yes -R 127.0.0.1:18080:127.0.0.1:3000 user@remote-host

这样登录到 remote-host 后,就可以访问:

1
curl http://127.0.0.1:18080

-R 中最后的 127.0.0.1:3000 指运行 SSH 客户端的本机,而不是 remote-host。

  1. 让远程机器访问本地仅在回环地址监听的 SSH 服务
1
ssh -NT -R 127.0.0.1:22022:127.0.0.1:22 user@remote-host

之后在 remote-host 上可以连接:

1
ssh -p 22022 local-user@127.0.0.1

默认情况下,OpenSSH 服务端通常只允许远程转发监听回环地址。如果需要让远端其他主机连接,可以在服务端设置 GatewayPorts clientspecified,再请求监听 0.0.0.0,例如 -R 0.0.0.0:22022:10.0.0.15:22。这可能把内网服务暴露给外部网络,还必须同步检查防火墙和目标服务认证;没有明确需求时应继续使用 127.0.0.1。

动态转发(Dynamic port forwarding)

动态转发在本地建立 SOCKS 代理。目标地址不写在 SSH 命令中,而是由应用通过 SOCKS 协议逐次提供;目标连接从 SSH 服务端一侧发起。

动态转发的命令格式为

1
ssh -D [bind_address:]local_port user@tunnel-host

例如在本地创建一个只监听回环地址的 SOCKS 代理:

1
ssh -NT -D 127.0.0.1:1080 user@tunnel-host

这里 -N 表示不执行远程命令,-T 表示不分配伪终端;关闭该 SSH 进程后,转发也会随之停止。

  1. 给浏览器配置一个临时 SOCKS5 代理
1
ssh -NT -o ExitOnForwardFailure=yes -D 127.0.0.1:1080 user@proxy-host

然后在浏览器或系统代理设置中填写:

  • 协议:SOCKS5
  • 地址:127.0.0.1
  • 端口:1080

浏览器流量将通过 proxy-host 转发。

  1. 给命令行工具临时指定 SOCKS 代理
1
2
ssh -NT -D 127.0.0.1:1080 user@proxy-host
curl --socks5-hostname 127.0.0.1:1080 https://example.com

这里 --socks5-hostname 表示连同域名解析也交给远端代理处理,适合访问只有远端网络才能解析的域名。与之相比,--socks5 通常先在本地解析域名,再把 IP 地址交给代理。

动态转发主要代理支持 SOCKS 的 TCP 应用,它不是 VPN,也不会自动接管系统中的所有网络流量。

  1. 配合 ProxyJump 或跳板机建立动态转发
1
ssh -J user@jump-host -NT -D 127.0.0.1:1080 user@target-host

这个命令表示:先经过 jump-host 登录 target-host,再基于到 target-host 的 SSH 会话在本地开启一个 SOCKS 代理。SOCKS 流量最终从 target-host 一侧发出,而不是从 jump-host 发出。

ProxyCommand 与 -W

-W 用于把标准输入/输出直接转发到目标主机和端口,常用于自定义跳板链路。较老环境中也常见用 nc/netcat 实现类似效果。

例如,主机 A 无法直接连接 example.com,但可以经过主机 B。在 A 的 SSH 配置中写入:

1
2
3
4
5
6
7
8
Host B
HostName jump.example.com
User jump-user

Host example.com
User git
HostName example.com
ProxyCommand ssh -q -W %h:%p B

A 会经由 B 连接 example.com。A 需要分别具备登录 B 和目标主机的凭据,两次认证可以使用不同密钥;目标主机的私钥不需要复制到 B。

这种场景直接使用 ProxyJump 更简单。ProxyCommand 主要用于兼容旧版 OpenSSH,或经过 nc、HTTP CONNECT 代理等自定义链路。它与 agent forwarding 无关,不必为此启用 ForwardAgent。

使用 ProxyJump 跳板

-J(ProxyJump)用于声明跳板机,是现代 OpenSSH 中比手写 ProxyCommand 更直接的写法。

-J 选项可以简化经由跳板机建立 SSH 连接的写法,在本地主机 A 上通过 B 登录 C:

1
ssh -J userB@B userC@C

其中 B 是跳板机,C 是目标主机。

还可以使用多个跳板机,此时需要使用逗号分隔:

1
ssh -J user1@jump1,user2@jump2 target-user@target-host

跳板配置也可以写入 ~/.ssh/config:

1
2
3
4
5
6
7
8
9
10
11
12
Host jump
HostName jump.example.com
User jump-user
IdentityFile ~/.ssh/id_ed25519_jump
IdentitiesOnly yes

Host internal
HostName 10.0.0.15
User target-user
IdentityFile ~/.ssh/id_ed25519_internal
IdentitiesOnly yes
ProxyJump jump

ssh、scp 和 sftp 都可以直接使用该别名:

1
2
3
ssh internal
scp report.txt internal:/tmp/
sftp internal

端口转发也可以经过跳板机:

1
ssh -J userB@B -NT -L 127.0.0.1:8989:127.0.0.1:8888 userC@C

这里 -L 的目标 127.0.0.1:8888 属于最终的主机 C,而不是跳板机 B。