跳转至

SSH 使用技巧

主要作者

@iBug@taoky

本文已完成

尽管 SSH 是一种开放协议,它的主流实现 OpenSSH 具有最丰富的功能,也是绝大多数 Linux 发行版采用的默认 SSH 方案,因此本教程主要介绍 OpenSSH 的使用。

客户端配置

SSH 客户端会按顺序处理以下配置,先出现的配置优先级更高:

  • 命令行参数
  • ~/.ssh/config
  • /etc/ssh/ssh_config

OpenSSH 的所有配置项都可以在 ssh_config(5) 中找到,这里介绍一些常用的配置。

对于经常登录的主机,可以在 ~/.ssh/config 中配置主机别名、用户名、端口等信息,以简化登录命令。

Host example
  Hostname example.com
  User sshuser
  Port 22

注意 SSH config 没有提供密码配置,因为将密码存储在明文文件中是不安全的做法,请使用密钥登录。

公钥认证

默认情况下,SSH 会寻找 ~/.ssh/id_* 作为私钥,其中 * 部分可以是 rsaecdsaed25519 等,也可以通过 -i 参数指定私钥文件。私钥的文件名加上 .pub 后缀就是公钥文件,暂时没有方法指定公钥文件的路径。如果要在配置文件中指定一个或多个私钥,可以使用 IdentityFile 选项,例如:

Host example
  IdentityFile ~/.ssh/id_ed25519
  #CertificateFile ~/.ssh/id_ed25519-cert.pub

一般来说,除非为了兼容一些非常古老(如 10 年前的)或非常简单的(如嵌入式)系统而不得不使用较短的 RSA 密钥对的时候,我们推荐使用 Ed25519 密钥对,或者 ECDSA 密钥对。这两种基于椭圆曲线的密码学算法比 RSA 更安全,而且性能也更好。如果不得不使用 RSA 的话,请尽可能使用 3072 位或更长的密钥长度。密钥长度可以在使用 ssh-keygen 生成密钥对时指定(-b),其中不同算法支持与推荐的长度也是不同的:

算法 支持长度 推荐长度 说明
RSA 1024-4096 3072 或以上 曾经的推荐长度是 2048 位,但 2020 年以后认为这个长度已不够安全
ECDSA 256 / 384 / 521 256 / 384 / 521 由于椭圆曲线参数选择的特殊性,只有这三种长度可选。注意最后一个选项是 521,不是 512
Ed25519 - - Ed25519 是基于 Edwards 曲线的算法,没有“长度”这种参数

另外,较早版本的 SSH 还有 DSA 算法(公钥以 ssh-dss 开头),它仅有 1024 位一种长度,安全性比 RSA 还差。因此 OpenSSH 7.0 开始默认不再生成或使用 DSA 密钥,OpenSSH 9.8 开始在编译期禁用 DSA 算法,并将在后续版本中完全删除 DSA 相关代码。

端口转发

SSH 配置 TCP 端口转发的格式为 [bind_address:]port:host:hostport,SSH 支持三种端口转发:

本地端口转发(Local port forwarding)

在本地上监听一个端口,将收到的数据转发到远程主机的指定端口。即将远程主机上某个服务的端口转发到本地,使本地的其他程序可以通过 SSH 访问到远程的服务。例如将远程主机的 80 端口转发到本地的 8080:

ssh -L 8080:localhost:80 example

也可以将远程主机所在网络的机器通过这种方法转发,假设需要访问的远程主机网络内部的机器名叫 internalserver

ssh -L 8080:internalserver:80 example

本地端口转发默认监听在 localhost。如果要监听其他地址,可以指定需要监听的地址,例如:

ssh -L 0.0.0.0:8080:localhost:80 example

虽然 SSH 客户端也有一个 GatewayPorts 选项,但它只影响没有指定监听地址的语法模式(即三段式 localport:remotehost:remoteport)。指定四段式语法后,GatewayPorts 选项不再起作用。

远程端口转发(Remote port forwarding)

在远程主机上监听一个端口,将收到的数据转发到本地的指定端口。即将本地某个服务的端口转发到远程主机上,使远程的其他程序可以通过 SSH 访问到本地的服务。例如将本地主机的 80 端口转发到远程主机的 8080 端口:

ssh -R 8080:localhost:80 example

上面命令表示在远程主机 example 上监听 8080 端口,将收到的数据转发到本地的 80 端口。

同样的,也可以将本地网络中的机器做转发,假设对应机器名为 myinternalserver

ssh -R 8080:myinternalserver:80 example

注意远程端口转发默认只能监听 localhost。如果要监听其他地址,需要在远程主机的 sshd_config 中设置 GatewayPorts yes。与另外两种端口转发不同,客户端无法覆盖服务端的 GatewayPorts 设定。

在 OpenSSH 7.6 版本之后的客户端,-R 也可以用来让远程主机利用本地作为 SOCKS5 代理(相当于下面的 -D 参数反过来),对应手册中的 -R [bind_address:]port 部分:

ssh -R 1080 example
# 指定远程主机上的监听地址
ssh -R 127.0.0.1:1080 example

动态端口转发(Dynamic port forwarding)

在本地监听一个端口用作 SOCKS5 代理,将收到的数据转发到远程主机,相当于利用了远程主机作为代理。例如:

ssh -D 1080 example

由于 SOCKS 代理是一个通用的代理协议,因此可以用于任何 TCP 连接,不仅仅是 HTTP。

与 LocalForward 类似,DynamicForward 也可以指定监听地址:

ssh -D 0.0.0.0:1080 example

同样地,GatewayPorts 只影响没有指定监听地址的语法模式(即只给出了一个端口)。指定监听地址后,GatewayPorts 选项不再起作用。

在配置文件中进行端口转发

以上三种端口转发都可以在配置文件中指定,例如:

Host example
  LocalForward 8080 localhost:80
  LocalForward 8081 localhost:8081
  RemoteForward 8080 localhost:80
  RemoteForward 8081 localhost:8081
  DynamicForward 1080
  DynamicForward 1081

-L-R-D 和配置文件中对应的选项都可以多次出现,指定多条转发规则,它们互相独立、不会覆盖,因此如果重复指定了同一个端口,就会出现冲突。

本地端口转发和远程端口转发的工作模式可以结合由 Ivan Velichko 绘制的图片来理解:

SSH local and remote port forwarding

代理与环境变量

由于各种各样的原因,目前各种应用程序对代理相关的环境变量(http_proxyhttps_proxyall_proxyno_proxy)的支持程度差异很大:

  • http_proxyhttps_proxy 分别用于连接到 HTTP 和 HTTPS 服务器时的代理设置。注意这和代理本身的协议无关,一部分原因是 HTTP 是明文协议,一些代理服务器可以修改 HTTP 请求的内容,或者做缓存操作,但是 HTTPS 就不行了。
  • 环境变量是大小写敏感的,因此对应用程序来说,设置 HTTP_PROXYhttp_proxy 是不一样的。几乎所有的程序都支持小写的代理环境变量,但是大写的代理环境变量支持情况就不一定了,因此推荐只设置小写的代理环境变量。
  • 不是所有程序都支持设置 SOCKS5 代理。curl 支持 SOCKS5 代理(socks5://socks5h://),其中带 h 的后者代表由代理(而不是本机)解析主机名。

更多信息可参考:We need to talk: Can we standardize NO_PROXY?

使用代理

SSH 支持自定义代理命令,从而可以通过代理服务器连接目标主机。 一个常见的用法是通过 SOCKS5 代理连接目标主机,可以借助 nc 命令实现1

Host example
  ProxyCommand nc -X 5 -x proxy.example.com:1080 %h %p

让服务器的 git 使用本机作为代理

在访问 SSH remote 时,git 可以读取 GIT_SSH_COMMAND 环境变量指定的 SSH 命令,例如 GIT_SSH_COMMAND="ssh -i .git/id_ed25519" git ... 就可以让 git 使用指定的路径的密钥。

而在访问 HTTP(S) remote 时,git 会使用 libcurl,因此会读取 http_proxyhttps_proxyall_proxy 环境变量指定的代理。也可以使用 http.proxy 这个 git config 选项来指定。

结合端口转发代理部分给出的命令,如果希望让 SSH 连接到的远程服务器上的 git 临时利用本机作为代理连接 SSH 和 HTTP(S) remote,那么应该如何操作?

跳板

SSH 支持通过跳板机连接目标主机,即先 SSH 登录 jump-host,再从 jump-host 登录目标主机。一些受限的网络环境常常采用这种方案,例如一个集群内只有跳板机暴露在公网上,而其他主机都在被隔离的内网中,只能通过跳板机访问。

ssh 命令的 -J 选项可以指定跳板机,例如:

ssh -J user@jumphost.example.com user@realhost.example.com

对应的配置文件语句是 ProxyJump user@jumphost.example.com

如果要给跳板机设置更多参数,如端口等,则必须使用配置文件:

Host jumphost
  HostName jumphost.example.com
  User jumphostuser
  Port 2333

Host realhost
  HostName realhost.example.com
  User realhostuser
  ProxyJump jumphost

跳板机需要支持 TCP 端口转发

SSH 跳板机和前文所述的“本地端口转发”采用相同的技术,因此跳板机需要允许 TCP 端口转发(默认开启)。

SSH Agent

SSH agent 是一个在后台运行的程序,用于持有私钥供 SSH 客户端在认证时使用。SSH agent 自身持有私钥,只对外提供签名操作。SSH agent 通常由桌面环境或 shell 自动启动。使用 SSH agent 通常有以下好处:

  • 私钥只需解密一次,之后 Agent 在后台保持密钥可用,无需反复输入密码。
  • 私钥只存放在本地主机,不需要复制到远程服务器上。

ssh-agent -s 命令可以启动一个 SSH agent,生成相关 Bourne shell 命令并输出到 stdout,例如:

$ ssh-agent -s
SSH_AUTH_SOCK=/tmp/ssh-uRRuiB8l0C76/agent.3173586; export SSH_AUTH_SOCK;
SSH_AGENT_PID=3173587; export SSH_AGENT_PID;
echo Agent pid 3173587;

其中 SSH_AUTH_SOCK 环境变量表示 SSH agent 监听的 Unix domain socket,可以用于控制所使用的 SSH agent。通常使用以下命令启动并在当前环境中使用该 SSH agent:

eval "$(ssh-agent -s)"

在配置文件中可以使用 IdentityAgent 选项来指定使用特定 Unix domain socket 与 SSH agent 通信,其优先级高于 SSH_AUTH_SOCK 环境变量,例如:

Host example
  IdentityAgent ~/example-agent.sock

指定 SSH agent 中特定密钥

默认情况下,SSH 客户端会依次尝试 SSH agent 中的所有密钥(如果有 SSH agent)、IdentityFile 指定的所有密钥(如果配置 IdentityFile)和默认密钥(如果没有配置 IdentityFile)。当 SSH agent 中管理的密钥较多时,多次错误尝试容易导致 Too many authentication failures 错误。可以使用 IdentityFileIdentitiesOnly 来使用特定密钥,从而避免相关错误。考虑到部分场景(例如使用密码管理器提供的 SSH agent)下,本地环境中不会存储私钥,可以在本地存储公钥并在 IdentityFile 选项中指定相应公钥来使用对应身份凭证。例如:

Host example
  IdentityFile ~/.ssh/id_ed25519
  # IdentityFile ~/.ssh/id_ed25519.pub # 本地不存储私钥时指定相应公钥
  IdentitiesOnly yes

可以通过 ssh-add 管理密钥:

ssh-add ~/.ssh/id_ed25519    # 将私钥加入 agent
ssh-add -l                   # 列出已加载的密钥指纹
ssh-add -L                   # 列出已加载的公钥
ssh-add -d ~/.ssh/id_ed25519 # 移除指定密钥
ssh-add -D                   # 移除所有密钥

-A 参数可以将本地的 SSH agent 转发到远程主机,使远程主机能使用你本地的 SSH 私钥向第三方认证。这对于在主力机上统一维护密钥、不在远程机器上留存私钥的场景非常适用。

ssh -A user@remote.example.com

也可以在配置文件中指定:

Host remote
  ForwardAgent yes

SSH agent 转发的机制

转发 SSH agent 会在远程主机上创建一个 Unix domain socket,并设置 SSH_AUTH_SOCK 环境变量指向该 socket。该 socket 上的通信通过 SSH 连接内的 auth-agent@openssh.com channel 发送给本地 SSH 客户端,再由本地 SSH 客户端与 SSH Agent 进行通信。

安全提示

从上述机制中不难看出,SSH agent 转发期间,远程主机上拥有足够权限(例如 root)的用户可以使用你转发的 SSH agent socket,并可以使用其间接使用你的 SSH agent。建议仅在信任的远程主机上开启 -AForwardAgent切勿Host * 块中全局启用。

高级功能:连接复用

SSH 协议允许在一条连接内运行多个 channel,其中每个 channel 可以是一个 shell session、端口转发、scp 命令等。OpenSSH 支持连接复用,即一个 SSH 进程在后台保持连接(称为 master 进程),其他客户端在连接同一个主机时可以复用这个连接,而不需要重新握手认证等,可以显著减少连接时间。这在频繁连接同一个主机时非常有用,尤其是当主机的延迟较大、常用操作所需的 RTT 较多时(例如从 GitHub 拉取仓库,或者前文所述的跳板机使用方式)。

启用连接复用需要在配置文件中同时指定 ControlMasterControlPathControlPersist 三个选项(它们的默认值都是禁用或者很不友好的值):

Host *
  ControlMaster auto
  ControlPath /tmp/sshcontrol-%C
  ControlPersist yes

其中 %C%l%h%p%r 的 hash,因此连接不同主机的 control socket 不会冲突。

连接复用的标识符

如果你尝试用相同的用户名和不同的公钥连接同一个目标(例如 git@github.com),由于没有新建连接的过程,你指定的公钥并不会生效。 解决此问题的方法是再单独指定另一个 ControlPath,或者设置 ControlPath=none 暂时禁用连接复用功能。

连接复用时的端口转发

当启用连接复用时,所有的端口转发等 SSH 连接特性都将由 master 进程管理,此时新的 ssh <host> -L <forwarding> 会将一个新的端口转发请求发送给 master 进程,并在当前 ssh 命令结束后持续转发。

例如:

ssh host # do something
ssh host -L <forwarding_1>
ssh host -R <forwarding_2>

在以上所有 ssh 命令都退出后,forwarding_1forwarding_2 将仍然在后台持续进行端口转发。

若要停止这些端口转发,可以使用 ~C 命令-O 指令(见下)使后台的 master 进程停止进行端口转发。

启用连接复用后,可以通过 ssh <host> -O <command> 的方式向后台监听新连接的 master 进程发送一些控制指令,例如:

  • ssh host -O exit 可以使 master 进程退出,后续的 ssh host 命令将重新建立连接。
  • ssh host -O cancel 可以在保持的后台连接中取消所有的端口转发。

文件传输

SFTP(Secure File Transfer Protocol)和 SCP(Secure Copy Protocol)都是基于 SSH 的另一种文件传输工具,它用于在本地和远程系统之间安全地复制文件。SCP 功能相对简单,主要提供文件的复制功能。SFTP 是一个独立的协议,建立在 SSH 之上,提供了一个交互式文件传输会话和更丰富的文件操作功能,包括对文件的浏览、编辑和管理。

Rsync

SCP 和 SFTP 能够提供的文件传输功能较为基础。如果你需要更多的功能,例如增量传输、断点续传、文件校验等,可以考虑使用 Rsync。Rsync 可以使用 SSH 作为传输层,因此可以替代 scp 命令。

详情可以参考本教程关于 Rsync 的章节

SCP

SCP 是基于 SSH (Secure Shell) 协议的文件传输工具,它允许用户在本地和远程主机之间安全地复制文件。SCP 使用 SSH 进行数据传输,提供同 SSH 相同级别的安全性,包括数据加密和用户认证。

SCP 命令的基本语法如下:

scp [选项] [源文件] [目标文件]

其中,源文件或目标文件的格式可以是本地路径,或者远程路径,如 用户名@主机名:文件路径

文件复制

从本地复制到远程服务器

scp /path/to/local/file username@remotehost:/path/to/remote/directory

或从远程服务器复制到本地

scp username@remotehost:/path/to/remote/file /path/to/local/directory

这个命令会提示你输入远程主机上用户的密码,除非你已经设置了 SSH 密钥认证。

Tip

你可以一次性传输多个文件或目录,将它们作为源路径的参数。例如:

scp file1.txt file2.txt username@remotehost:/path/to/remote/directory

或者经过本地流量中转,在两个远程主机之间复制文件

scp username1@remotehost1:/path/to/remote/file username2@remotehost2:/path/to/remote/directory

常用参数

复制目录

如果需要复制整个目录,需要使用 -r 选项,这表示递归复制:

scp -r /path/to/local/directory username@remotehost:/path/to/remote/directory
使用非标准端口

如果远程主机的 SSH 服务运行在非标准端口(22),则可以使用 -P 选项指定端口:

scp -P 2222 /path/to/local/file username@remotehost:/path/to/remote/directory

Tip

你也可以在 SSH 客户端配置文件中为 Host remotehost 指定 Port 2222,这样就不需要每次在命令行中指定端口了。

限制带宽

使用 -l 选项可以限制 SCP 使用的带宽,单位是 Kbit/s

scp -l 1024 /path/to/local/file username@remotehost:/path/to/remote/directory
保留文件属性

-p 选项可以保留原文件的修改时间和访问权限:

scp -p /path/to/local/file username@remotehost:/path/to/remote/directory
开启压缩

使用 -C 选项开启压缩,可以减少传输数据量并提升传输速度,特别对于文本文件效果显著。

scp -C /path/to/local/file username@remotehost:/path/to/remote/directory

Tip

此选项等价于 ssh-C 选项,即在 SSH 层面开启压缩,并非 SCP 协议层面的压缩。

你也可以在 SSH 客户端配置文件中为 Host remotehost 指定 Compression yes,这样就不需要每次在命令行中启用压缩了。

现代的 scp 命令已经默认使用 SFTP 协议

从 OpenSSH 9.0 开始,scp 命令已经默认使用 SFTP 协议进行文件传输,而不再使用旧的 SCP 协议。对于一些不支持 SFTP 的远程主机(如使用 dropbear 的嵌入式设备或上古版本的 SSH 服务端等),这可能会导致问题,例如:

/usr/libexec/sftp-server: No such file or directory

如果你需要使用旧的 SCP 协议,可以使用 -O 选项:

scp -O /path/to/local/file username@remotehost:/path/to/remote/directory

SFTP

SFTP 是一种安全的文件传输协议,它在 SSH 的基础上提供了一个扩展的功能集合,用于文件访问、文件传输和文件管理。与 SCP 相比,SFTP 提供了更丰富的操作文件和目录的功能,例如列出目录内容、删除文件、创建和删除目录等。由于 SFTP 在传输过程中使用 SSH 提供的加密通道,因此它能够保证数据的安全性和隐私性。

启动 SFTP 会话

要连接到远程服务器,可以使用以下命令:

sftp username@remotehost

如果远程服务器的 SSH 服务使用的不是默认端口(22),可以使用 -P 选项指定端口:

sftp -P 2233 username@remotehost

文件和目录操作

  • ls:列出远程目录的内容。
  • get remote-file [local-file]:下载文件。
  • put local-file [remote-file]:上传文件。
  • mkdir directory-name:创建远程目录。
  • rmdir directory-name:删除远程目录。
  • rm file-name:删除远程文件。
  • chmod mode file-name:改变远程文件的权限。
  • pwd:显示当前远程目录。
  • lpwd:显示当前本地目录。
  • cd directory-name:改变远程工作目录。
  • lcd directory-name:改变本地工作目录。

退出 SFTP 会话

输入 exitbye 来终止 SFTP 会话。

使用脚本进行自动化操作

通过创建一个包含 SFTP 命令的批处理文件,你可以 让SFTP 会话自动执行这些命令。例如,你可以创建一个文件 upload.txt,其中包含以下内容:

put file1.txt
put file2.jpg
put file3.pdf
quit
然后使用命令 sftp -b upload.txt username@remotehost 来自动上传文件。

服务端配置

服务端的配置与客户端有一些不同点:

  • sshd 服务端程序只有很少量的命令行参数,各种配置都在配置文件中完成。特别注意,sshd 的配置文件不是可选的:如果配置文件不存在或者包含错误,sshd 会拒绝启动。
  • sshd 仅有一个配置文件 /etc/ssh/sshd_config,它的配置项可以在 sshd_config(5) 中找到。

sshd 接受 SIGHUP 信号作为重新载入配置文件的方式。sshd -t 命令可以检查配置文件的语法是否正确,这也是大多数发行版提供的 ssh.service 中指定的 ExecStartPre= 命令和第一条 ExecReload= 命令,即在尝试启动和重新加载服务前先检查配置文件的语法。

systemd 与 sshd

在使用 systemd 的较新的系统下,服务端可能会有以下变化:

  • 默认使用 ssh.socket 而不是 ssh.service 对外提供服务。
  • systemd-ssh-generator 会在 Unix socket 和 vsock 上开启额外的端口。

其中的一些设置可能会以非预期的方式影响系统的安全性,阅读服务与日志管理的相关内容以了解更多相关信息。

authorized_keys 文件

~/.ssh/authorized_keys 文件是 SSH 服务端用于验证客户端公钥的文件,每行一个公钥,空行或者以 # 开头的行会被当作注释忽略。

authorized_keys 文件还允许为每个公钥指定一些选项,例如:

from="192.0.2.0/24,2001:db8::/32"

限制此公钥只能从指定的 IP 地址连接。

expiry-time="197001010800Z"

限制此公钥的有效时间,格式为 YYYYMMDDhhmm(服务器的本地时间),或者在其后添加一个大写字母 Z 表示 UTC 时间。适合用于添加临时用途的公钥,确保即使事后忘记删除了,它也不会超期生效。

command="/path/to/command"

限制此公钥只能用于执行指定的命令,且不能登录 shell。如果使用此公钥登录时提供了额外的命令(例如 ssh user@host some/other/command),提供的命令将会在 SSH_ORIGINAL_COMMAND 环境变量中传递给指定的命令。

指定命令的一个常用场景是为备份服务提供有限的访问,例如 command="/usr/bin/rrsync /path/to/backup",这样备份服务就只能使用 rsync 命令访问指定的目录。

如果你需要使用 command= 的话,你很可能也需要 restrict(见下)。

no-port-forwarding, no-X11-forwarding, no-agent-forwarding, no-pty, no-user-rc

禁止对应的功能。这些选项可以用于限制公钥的功能,例如禁止各种转发和使用终端等。

特别地,禁止 TCP 端口转发之后,该公钥也不能用于将本机作为跳板机登录其他机器(即 -J 参数或 ProxyJump 配置项)。

restrict

禁止所有可选功能,相当于同时使用上一条列出的(和没列出的,详情见 man page)所有选项。

通常与 command= 搭配使用,确保指定公钥只能做指定的事情。

如果需要在 restrict 的基础上单独开放某些功能,可以使用 port-forwarding 等(也就是去掉前面的 no-)。

完整的选项列表可以在 sshd(8)AUTHORIZED_KEYS FILE FORMAT 部分找到。

例:用于备份 LUG FTP 的公钥配置
restrict,from="192.0.2.2",command="/usr/bin/rrsync -ro /mnt/lugftp" ssh-rsa ...

SSH 证书

OpenSSH 支持使用证书作为客户端和服务端的身份验证方式,即在正确配置了证书的情况下,客户端无需预先记录服务器的公钥(即加入 known_hosts 文件)即可信任 SSH 服务端,而服务端也无需预先记录客户端的公钥(即写入对应用户的 authorized_keys 文件)即可确认登录者的身份。使用证书进行 SSH 身份验证有以下好处:

  • 对于服务器和客户端(或用户)数量较多的场景,通过签署证书的方式可以大大降低配置复杂度,且容易管理登录授权的分配情况。

    • 如果客户端被允许自行修改 authorized_keys 文件,则其仍然能够添加其他公钥用于登录已授权的服务端,绕过采用证书认证的管理目的。请根据实际情况决定是否需要禁止客户端自行修改 authorized_keys 文件。

      提示:SSH 服务端具有 AuthorizedKeysFile 设置项,其默认值为 .ssh/authorized_keys

  • 默认情况下,SSH 服务端会在日志中记录登录时所用的证书的“主体”与编号,即签发证书时 -I-z 参数的值,这使得管理员能够更方便地追踪客户端密钥对的使用情况。

  • 相比于 authorized_keys 文件,签发证书时能够更方便地指定一些与公钥相关的参数,如有效期(-V 参数)等。
  • 相比于使用私钥登录的场景,签发证书的过程可以离线进行,提升 CA 私钥的安全性。

使用证书认证方案时的注意事项:

  • SSH 证书没有 X.509 证书的链式结构,即 SSH 证书没有“中间 CA”的概念,CA 的身份由公钥唯一确定,因此所有的 CA 都是根 CA。一旦 CA 的私钥发生泄露,需要立刻在所有机器上删除对应的 CA 公钥配置。这对 SSH CA 的密钥管理提出了更高的要求。
  • SSH 证书没有自动更新 CRL 的机制,证书的撤销依赖于管理员自行维护“公钥吊销列表”(Key Revocation List,KRL)文件,因此如果已获得签发证书的对应私钥发生泄露,也需要维护 KRL 直到对应证书过期。

    特别地,如果发生泄露的证书未设置有效期,则其是无限期有效的,对应的已泄露的私钥也需要无限期地记录在所有信任此证书的服务器的 KRL 中,这可能会带来一定的管理负担。因此我们推荐为所有的客户端证书指定有效期,从不签发无限期有效的客户端证书。

总的来说,对于中小规模的社团和实验室服务器管理场景,(对管理员)使用 SSH 证书认证是个较为方便易用的方式。

与 X.509 证书类似,客户端通过证书信任服务端和服务端通过证书认证客户端是两件独立的事,在实际应用中也可以根据需求使用同一个 CA 或分别使用不同的 CA。以下分别以「服务端 CA」和「客户端 CA」指代为服务端/客户端签发证书的 CA。

创建 SSH CA

前文提到,SSH CA 的身份由公钥唯一确定,因此一对普通的 SSH 密钥对就是一个 SSH CA 所需的全部内容,不像 X.509 需要再进一步为根 CA 产生一个自签名证书。此处引用创建 SSH 密钥对的命令:

ssh-keygen -f my_ca [-t ed25519] [-C 'My CA'] [-N 'my-ca-p@ssw0rd']

注意到尽管 -t-C-N 参数都是可选的2,为了便于辨认 CA 和提高安全性,我们强烈建议采用先进的密码学算法(Ed25519)、指定可读的备注文字和为私钥设置密码。

生成密钥对后,请保管好 my_ca 私钥文件,然后即可将 my_ca.pub 公钥文件复制或公开传播了。

服务端证书

首先,客户端需要信任服务端 CA 签发的证书,方法是在 known_hosts 文件中加入服务端 CA 的公钥,并在公钥前添加选项 @cert-authority *,例如;

~/.ssh/known_hosts
@cert-authority * ssh-ed25519 AAAAC3N... My CA

对于实验室等公用机器的场景,也可以将服务端 CA 条目配置在 /etc/ssh/ssh_known_hosts 文件中,其会对所有用户生效,而无需再为每个用户单独配置。

配置服务端证书

签发服务端证书需要使用服务端 CA 私钥和对应服务端的公钥

ssh-keygen -s my_ca -I myserver.example.com -h -n myserver.example.com [-V validity] [-z serial] ssh_host_ed25519_key.pub

其中各参数的含义如下:

  • -s my_ca:指定 CA 私钥文件。
  • -I myserver.example.com:指定证书的“主体”(identity),即签发对象的一个可辨认的名字,类似 X.509 证书的 Common Name(CN)。常见的选项是使用服务器的 FQDN 作为 identity,便于辨认。
  • -h:表示签发的是服务端证书(host certificate),而非客户端证书(client certificate)。
  • -n myserver.example.com:指定证书的“有效主体名称”(principal names),类似 X.509 证书的 Subject Alternative Names(SAN)。客户端验证证书时会比较此参数与尝试连接的 Hostname,即 ssh 命令的主机参数,或客户端配置文件中的 HostName 选项。签发证书时可以指定多个名称,使用逗号分隔,如 -n example.com,192.0.2.1
    • -n 参数未指定,则默认为不验证主机名。我们不推荐省略 -n 参数,因为这会降低证书的安全性。
  • -V validity:指定证书的有效期(validity period)。Validity 的格式为 [from:]to,且时间格式支持多种格式,如绝对时间 YYYYMMDDhhmm 和相对时间 -n[unit]+n[unit] 等,详情见 ssh-keygen(1)-V 参数说明。
    • 对于服务端证书,常见的做法是指定 -n 但不指定 -V 以减少管理负担,因为服务端证书通常不会频繁更换。
  • -z serial:指定证书的编号(serial number),用于区分不同的证书。若未指定此参数,则默认为 0。
  • 最后的参数是需要签发证书的服务端公钥文件。

ssh-keygen 会签发出的证书文件写入 ssh_host_ed25519_key-cert.pub,即将输入文件名结尾的 .pub 替换为 -cert.pub。你需要将该文件传输到服务器上,并修改配置文件指示 sshd 使用证书文件:

/etc/ssh/sshd_config.d/ca.conf
HostKey /etc/ssh/ssh_host_ed25519_key
HostCertificate /etc/ssh/ssh_host_ed25519_key-cert.pub

在 reload 或重启 sshd 服务后,服务端就会在握手时发送证书给客户端。若客户端也正确配置了服务端 CA 信任,则无需将服务端的公钥预先加入 known_hosts 文件即可完成验证。

客户端证书

首先为服务端配置客户端 CA 信任,需要在服务端建立一个“信任 CA 列表”文件。其采用通常的 authorized_keys 格式,即每行一个公钥。我们建议使用一个约定俗成、易于辨认的路径 /etc/ssh/ssh_user_ca

/etc/ssh/ssh_user_ca
ssh-ed25519 AAAAC3N... My org CA

然后在 sshd_config 中指定该文件:

/etc/ssh/sshd_config.d/ca.conf
TrustedUserCAKeys /etc/ssh/ssh_user_ca

签发客户端证书

与服务端证书类似,签发客户端证书需要使用客户端 CA 私钥和对应客户端的公钥

ssh-keygen -s my_ca -I 'User 1' -n user1 [-V validity] [-z serial] [-O options] ~/.ssh/id_ed25519.pub

签发客户端证书时使用的命令行参数基本相同,仅有少许区别:

  • 不能使用 -h 参数,因为签发的是客户端证书。
  • -n 参数指定的是允许使用此证书登录的用户名(principal name)。
    • -n 参数未指定,则默认为允许以任意用户名登录。若为普通用户签发证书,请务必指定正确的用户名。
  • -O 参数可以指定一些额外的公钥选项,对应 authorized_keys 文件中的选项。详情见 ssh-keygen(1)CERTIFICATES 一节。

ssh-keygen 会签发出的证书文件写入 ~/.ssh/id_ed25519-cert.pub,即相同的文件名规律。

与服务端证书不同的是,ssh 客户端命令会根据此文件名规则自动寻找与私钥匹配的证书文件,因此无需修改客户端配置文件即可使用证书进行身份验证。若要指定使用其他证书文件,可以在配置文件中使用 CertificateFile 选项。例如:

~/.ssh/config
Host example
  CertificateFile ~/.ssh/example-cert.pub

在服务器的登录日志(/var/log/auth.logjournalctl -u ssh)中,使用证书登录时会记录类似如下的信息:

Accepted publickey for user1 from 192.0.2.1 port 1145 ssh2: ED25519-CERT SHA256:... ID User 1 (serial 1) CA RSA SHA256:...

其中 ID 后面的部分即为证书的“主体”(identity,签发时的 -I 参数值),(serial 1) 是证书的编号,CA RSA SHA256:... 是签发此证书的 CA 公钥的指纹。

查看 SSH 证书

还是使用 ssh-keygen 命令:

ssh-keygen -L -f <filename-cert.pub>

例如:

ssh-keygen -Lf ~/.ssh/id_ed25519-cert.pub
/home/ustclug/.ssh/id_ed25519-cert.pub:
        Type: ssh-ed25519-cert-v01@openssh.com user certificate
        Public key: ED25519-CERT SHA256:A2ZueGXJGzK3nnECPgD7rJTxMAY9wbhUDGq+cEjf5qA
        Signing CA: RSA SHA256:6evsncr34cyV1FzWyCozOg8pbHMeiSgAknUYJJlbuFs (using rsa-sha2-512)
        Key ID: "ustc@ustclug"
        Serial: 0
        Valid: from 2025-01-01T19:55:00 to 2025-02-01T20:00:00
        Principals:
                root
                ustc
        Critical Options: (none)
        Extensions:
                permit-X11-forwarding
                permit-agent-forwarding
                permit-port-forwarding
                permit-pty
                permit-user-rc

杂项

拆分配置文件

从 OpenSSH 7.3p1 开始,ssh_config 和 sshd_config 都支持 Include 选项,可以在主配置文件中 include 其他文件。与 C 的 #include 或 Nginx 的 include 不同,SSH config 里的 Include 等价于文本插入替换,并且 Include 可以出现在 HostMatch 块中,出现在这两个块中的 Include 会被视作条件包含。因此一个(不太常见的)坑是:

错误写法
Host example
  HostName example.com
  User user

Include ~/.ssh/global.conf

因为 SSH 读取配置文件时是不会看缩进的,因此上面示例中的 Include 仅对 Host example 生效。正确的写法是将其放在一个 Match all 块(或者 Host *)中:

正确写法
Host example
  HostName example.com
  User user

Match all
  Include ~/.ssh/global.conf

更加推荐的写法是将 Include 放在配置文件开头:

推荐写法

Include ~/.ssh/global.conf

Host example
  HostName example.com
  User user

一些坑点

在 OpenSSH 内部,同一个“代号”可能指代多种(有关联但)不同的细节。例如 ssh-rsa 至少有以下三种不同的含义:

  • RSA 密钥对或 RSA 公钥算法(id_rsaid_rsa.pub 文件)
  • 基于 RSA / SHA-1 的 SSH 证书的签名算法(CASignatureAlgorithms

    这种算法已经在 OpenSSH 8.2 中被淘汰,而 OpenSSH 7.2 起就已经支持替代算法 rsa-sha2-256rsa-sha2-512(采用 SHA-256 和 SHA-512 哈希算法),虽然直到 OpenSSH 8.2,使用 RSA CA 私钥签出来的证书才默认采用 rsa-sha2-512 算法。

    如果你正在使用一个 RSA CA,那么你需要将已有的证书使用 OpenSSH 8.2 以上的版本重新签名。

    如果需要临时兼容 ≤ 7.1 版本的 OpenSSH,可以在 ~/.ssh/configsshd_config 中指定 CASignatureAlgorithms +ssh-rsa,这样就可以使用旧版本的证书签名算法了。

  • 基于 RSA / SHA-1 的公钥签名算法套件(ssh-rsa)。与前面的证书不同,这种签名算法是用于用户登录时的公钥验证,不会保存在文件里,而是在 SSH 协议内部使用。

    与前一个采用 SHA-1 作为哈希算法的算法套件类似,OpenSSH 8.8 起也不再默认启用,且替代算法也分别叫做 rsa-sha2-256rsa-sha2-512。好消息是,你不需要重新签发任何证书,只要确保客户端和服务端的 OpenSSH 版本都不低于 7.2 就可以了。

    如果需要临时兼容 ≤ 7.1 版本的 OpenSSH,可以通过配置选项 HostkeyAlgorithmsPubkeyAcceptedAlgorithms 启用。这两个选项分别控制服务端和客户端的公钥签名算法套件,并且在两端都可以指定。

SSH 转义序列

SSH 连接由本地的 ssh 客户端发起,建立连接后,你在终端输入的内容默认都会发送到远端服务器。但有时候,我们需要让本地 ssh 客户端立即响应一些特殊操作,比如断开连接、挂起会话、配置端口转发等,这时就可以用“转义序列”来实现。

转义序列的用法是:在新的一行先按回车,然后输入 ~,后面的内容就会被本地 ssh 客户端识别为命令,而不会发给远端服务器。以下展示的转义序列均以 ~ 开头表示。

常用的 SSH 转义序列如下:

  • ~.:立刻断开当前 SSH 连接。在命令行输入回车后,直接输入 ~.,无需等待远端响应,适用于远端卡死或网络异常时强制断开。
  • ~^Z:挂起 SSH 客户端(发送 SIGTSTP),回到本地 shell,可以用 fg 恢复 SSH 会话。回车后输入 ~,再按下 Ctrl+Z
  • ~C:打开 SSH 客户端的命令行,可以用 -L -R -D 参数配置端口转发。-KL -KR -KD 参数关闭端口转发。

    Warning

    OpenSSH 9.2 新增了 EnableEscapeCommandline 设置项,且默认为 no。如果需要使用 ~C 命令行,需要先启用该设置项。OpenSSH ≤ 9.1 的版本默认允许 ~C

  • ~#:列出当前所有转发的连接(如端口转发、X11 转发等),便于排查端口转发问题。

  • ~?:显示所有可用的转义序列及其说明,遇到不确定的情况可以先用这个命令查看帮助。

如果开启了连接复用功能,那么一部分转义序列无法使用,例如 ~^Z~C 等。

注意事项

  • 转义序列必须在按下回车后立即输入(即当前行前面不能有其他字符),否则会被当作普通输入传给远端。
  • 在多层 SSH 跳板(多次 ssh 嵌套)时,每多一层,需要输入的 ~ 会翻倍。例如,~~. 指令会断开第二层的 SSH 连接,而 ~~~~. 指令会断开第三层的 SSH 连接。

  1. 需要使用 OpenBSD 版本的 nc 命令,如 apt install netcat-openbsd。 

  2. 事实上 -f 参数也是可选的,但为了避免 ssh-keygen 将其放置在 ~/.ssh 目录下,从而更容易与个人用于登录服务器的私钥混淆,此处显式指定了输出文件名。