跳转到内容

OpenSSH/Cookbook/公钥认证

100% developed
来自 Wikibooks,开放世界中的开放书籍

 

如果操作得当,认证密钥可以提高效率。额外的好处是,密码短语和私钥永远不会离开客户端[1]。对于面向外部的系统,通常建议使用基于密钥的认证,以便可以关闭密码认证。


基于密钥的认证

[编辑 | 编辑源代码]

OpenSSH 可以使用公钥密码术进行认证。在公钥密码术中,加密和解密是不对称的。密钥成对使用,公钥用于加密,私钥用于解密。ssh-keygen(1) 实用程序可以生成 RSA、Ed25519、ECDSA、Ed25519-SK 或 ECDSA-SK 密钥用于认证。虽然仍然可以生成 DSA 密钥,并且大小固定为 1024 位,但不再推荐使用,应该避免。RSA 密钥允许的大小从 1024 位起。当前的默认大小为 3072 位。然而,在 2048 位之后,增长的安全性收益有限,因此椭圆曲线算法更受欢迎。ECDSA 密钥可以是 256、384 或 521 位。Ed25519、Ed25519-SK 和 ECDSA-SK 密钥的长度固定为 256 位。较短的密钥速度更快,但安全性较低。较长的密钥操作速度更慢,但提供了更好的保护,当然也有上限。

可以为密钥命名以帮助记忆它们的用途。由于密钥文件可以随意命名,因此可以创建多个密钥,每个密钥用于不同的服务或任务。公钥末尾的注释字段也可以帮助对密钥进行排序,特别是当您拥有许多密钥或不经常使用它们时。

基于密钥的认证过程使用这些密钥进行几次交换,使用密钥对一些简短的消息进行加密和解密。在开始时,客户端公钥的副本存储在服务器上,客户端私钥存储在客户端,两者都保持在各自的位置。私钥永远不会离开客户端。当客户端第一次联系服务器时,服务器会使用客户端的公钥加密一个随机数,并将这个加密的随机数作为挑战返回给客户端。客户端使用匹配的私钥解密消息并提取随机数来响应挑战。然后,客户端对会话 ID 和来自挑战的随机数进行 MD5 哈希运算,并将该哈希值返回给服务器。然后,服务器对会话 ID 和随机数进行自己的哈希运算,并将结果与客户端返回的哈希值进行比较。如果匹配,则允许登录。如果匹配失败,则尝试服务器上注册为属于同一帐户的下一个公钥,直到找到匹配项,或所有密钥都尝试过,或达到最大失败次数。[2]

当客户端使用代理来管理认证时,过程类似。不同之处在于 ssh(1) 将挑战传递给代理,代理计算响应并将响应传递回 ssh(1),然后 ssh(1) 将代理的响应传递回服务器。

公钥认证基础

[编辑 | 编辑源代码]

公钥认证需要一对匹配的密钥,可以使用 ssh-keygen(1) 生成密钥对。在这对密钥中,公钥必须正确存储在远程主机上。在服务器上,公钥被添加到该远程用户帐户的指定 **authorized_keys** 文件中。私钥安全地存储在客户端。密钥准备好后,可以反复使用。

基于密钥的认证准备工作分为四个步骤:

1)准备密钥存放的目录。如果远程机器上不存在 **authorized_keys** 文件或 **.ssh** 目录,或者客户端机器上不存在 **.ssh** 目录,则创建它们并设置正确的权限。在客户端,只需要一个目录,但它应该只能由其所有者写入。

$ mkdir ~/.ssh/
$ chmod 0700 ~/.ssh/

在远程机器上,需要 **.ssh** 目录,以及一个用于存储公钥的特殊文件,默认名为 **authorized_keys**。

$ mkdir ~/.ssh/
$ touch ~/.ssh/authorized_keys
$ chmod 0700 ~/.ssh/
$ chmod 0600 ~/.ssh/authorized_keys

2)创建一个密钥对。下面的示例在 **~/.ssh** 目录中创建了一个 Ed25519 密钥对。**-t** 选项指定密钥类型,**-f** 选项指定密钥文件的名称。最好为密钥文件起有描述性的名称,特别是当管理大量密钥时。在下例中,公钥将命名为 **mykey_ed25519.pub**,私钥将命名为 **mykey_ed25519**。确保输入一个可靠的密码短语,使用 128 位 AES 加密私钥。

$ ssh-keygen -t ed25519 -f ~/.ssh/mykey_ed25519

Ed25519、Ed25519-SK 和 ECDSA-SK 密钥的长度是固定的。对于 RSA 和 ECDSA 密钥,**-b** 选项设置使用的位数。

从版本 6.5 开始,可以使用一种新的私钥格式,该格式使用 bcrypt(3) 密钥派生函数 (KDF) 来更好地保护静止状态下的密钥。这种新格式始终用于 Ed25519 密钥,并且将来会成为所有密钥的默认格式。但目前,可以通过 ssh-keygen(1) 中的 **-o** 选项在生成或保存其他类型的现有密钥时请求使用它。

$ ssh-keygen -o -b 4096 -t rsa -f ~/.ssh/mykey_rsa

有关这种新格式的详细信息可以在源代码中的 **PROTOCOL.key** 文件中找到。

3)将密钥放到正确的位置。仅将公钥传输到远程机器。

$ ssh-copy-id -i ~/.ssh/mykey_ed25519 [email protected]

如果 ssh-copy-id(1) 不可使用,则可以使用任何不换行长行的编辑器。例如,nano(1) 可以使用 -w 选项启动,以防止长行换行。或者另一种永久设置的方法是编辑 nanorc(5)。但是,当编辑 authorized_keys 文件以添加密钥时,密钥本身必须完整且不间断地位于文件中的同一行上。

然后,如果它们尚未在客户端上,则将公钥和私钥都传输到那里。通常最好将公钥和私钥放在目录 ~/.ssh/ 中,尽管在此步骤之后,客户端不再需要公钥,并且可以在需要时重新生成。

4) 测试密钥

在仍然登录的情况下,使用客户端在新窗口中启动另一个 SSH 会话,并尝试使用私钥从客户端对远程机器进行身份验证。

$ ssh -i ~/.ssh/mykey_ed25519 -l fred remotehost.example.org

选项 -i 告诉 ssh(1) 要尝试哪个私钥。只有在验证基于密钥的身份验证有效后,才能关闭原始 SSH 会话。

一旦验证基于密钥的身份验证有效,就可以使用 ssh_config(5) 在客户端上创建永久快捷方式,这将在下面进一步解释。特别地,请查看 IdentityFileIdentitiesOnlyAddKeysToAgent 配置指令,仅举三例。

基于密钥的身份验证故障排除

如果服务器拒绝接受密钥并切换到下一个身份验证方法(例如:“服务器拒绝了我们的密钥”),则服务器端可能存在几个可能错误。

最常见的错误之一是文件和目录权限错误。授权密钥文件必须由相关用户拥有,并且不能由组写入。密钥文件的目录也不能由组或世界写入。

$ chmod u=rwx,g=rx,o= ~/.ssh
$ chmod u=rw,g=,o=  ~/.ssh/authorized_keys

另一个可能发生的错误是,如果远程主机上 authorized_keys 文件中的密钥被换行符破坏或中间有其他空格。可以通过连接行并删除空格或更仔细地重新复制密钥来解决此问题。

而且,虽然应该不言而喻,但密钥对的两半需要匹配。服务器上的公钥需要与客户端持有的私钥匹配。如果公钥丢失,则可以使用 -y 选项生成一个新的公钥,但不能反过来。如果私钥丢失,则应删除公钥,因为它不再有任何用处。如果一个帐户使用多个密钥,则添加注释可能是一个好主意。在客户端,了解密钥适用于哪个服务器可能是一个好主意,可以通过文件名本身或注释字段来实现。可以使用 -C 选项添加注释。

$ ssh-keygen -t ed25519 -f ~/.ssh/mykey_ed25519 -C "web server mirror"

在服务器上,如果一个帐户中存在多个公钥,则标记它们来自哪个客户端可能很重要。如果注释不存在,则可以在服务器上授权密钥文件的最后一列中添加注释。再次,授权密钥文件的格式在 sshd(8) 的手册页中“AUTHORIZED_KEYS 文件格式”部分给出。如果密钥没有标记,则可能难以匹配,这可能是您想要或不想要的。

将密钥永久与服务器关联

[编辑 | 编辑源代码]

可以在运行时指定密钥,但为了节省一遍又一遍地重新输入相同的路径,ssh_config(5) 中的 Host 指令可以将特定设置应用于目标主机。在这种情况下,通过更改 ~/.ssh/config,可以将特定密钥分配给在每次连接到该特定主机时自动尝试。将以下几行添加到 ~/.ssh/config 后,只需键入 ssh web1 即可使用该服务器的密钥进行连接。

Host web1
	Hostname 198.51.100.32
	IdentitiesOnly yes
	IdentityFile /home/fred/.ssh/web_key_ed25519

下面的 ~/.ssh/config 使用不同的密钥 serverserver.example.org,无论它们是否解析为同一台机器。这是可能的,因为传递给 ssh(1) 的主机名参数在匹配之前不会转换为规范主机名。

Host server
	IdentitiesOnly yes
	IdentityFile /home/fred/.ssh/key_a_rsa

Host server.example.org
	IdentitiesOnly yes
	IdentityFile /home/fred/.ssh/key_b_rsa

在此示例中,首先尝试较短的名称,但当然可以改为使用不太模棱两可的快捷方式。配置文件按照首次匹配的方式解析。因此,最具体的规则放在开头,最通用的规则放在结尾。

加密主目录

[编辑 | 编辑源代码]

使用加密主目录时,密钥必须存储在未加密的目录中。这意味着存储在实际主目录之外的某个地方,这意味着 sshd(8) 需要进行适当的配置才能在该特殊位置找到密钥。

以下是一种解决访问问题的方法。每个用户在 /etc/ssh/keys/ 下都有一个子目录,他们可以使用该子目录存储其 authorized_keys 文件。这在服务器的配置文件 /etc/ssh/sshd_config 中设置

AuthorizedKeysFile      /etc/ssh/keys/%u/authorized_keys

为密钥设置特殊位置为密钥的管理方式提供了更多可能性,并且如果它们用空格分隔,则可以指定多个密钥文件位置。用户不必对 authorized_keys 文件拥有写入权限。只需读取权限即可登录。但是,如果用户被允许添加、删除或更改其密钥,则他们需要对文件具有写入权限才能执行此操作。

加密主目录的一个症状是,基于密钥的身份验证仅在您已经登录到同一个帐户时有效,但在尝试首次连接并首次登录时失败。

有时还需要在身份验证后立即从 /etc/ssh/sshrc 添加脚本或调用程序以解密主目录。

无密码登录

[编辑 | 编辑源代码]

允许无密码登录的一种方法是按照上述步骤操作,但在创建密钥时,在提示输入密码时,只需不输入密码即可。请注意,使用没有密码的密钥非常危险,因此密钥文件应该受到很好的保护并进行跟踪。这包括它们只能用作下面描述的单用途密钥。及时的密钥轮换变得尤为重要。一般来说,最好不要创建没有密码的密钥。更好的解决方案是拥有密码并结合使用身份验证代理和单用途密钥。如今,大多数桌面环境都会自动启动 SSH 代理。如果它存在,它将在 SSH_AUTH_SOCK 环境变量中可见。在具有代理的帐户中,ssh-add(1) 可以将私钥加载到可用的代理中。

$ ssh-add ~/.ssh/mykey_ed25519

此后,客户端将在适当的时候自动检查代理中的密钥。如果代理中存在多个密钥,则有必要设置 IdentitiesOnly。有关此设置,请参见上面有关使用 ~/.ssh/config 的部分。请参见 [OpenSSH/Cookbook/Public_Key_Authentication#Key-based_Authentication_Using_an_Agent 使用代理的基于密钥的身份验证]。

同时要求密钥和密码

[编辑 | 编辑源代码]

虽然用户应该为他们的密钥设置强密码,但无法强制或验证这一点。实际上,由于私钥及其密码从未离开客户端机器,服务器无法对此有任何影响。相反,可以同时要求密钥和密码。从 OpenSSH 6.2 开始,服务器可以使用 AuthenticationMethods 指令要求使用多个身份验证方法进行登录。

AuthenticationMethods publickey,password

此示例来自 http://man.openbsd.org/sshd_config.5 sshd_config(5)] 要求用户首先使用密钥进行身份验证,只有在密钥成功后才会查询密码。因此,使用该配置,如果不先使用有效的密钥进行身份验证,就无法访问系统密码提示符。更改参数的顺序会更改身份验证方法的顺序。

要求两个或多个密钥

[编辑 | 编辑源代码]

从 OpenSSH 6.8 开始,服务器现在会记住哪些公钥已被用于身份验证,并拒绝接受以前使用的密钥。这允许设置要求用户使用两个不同的公钥进行身份验证,例如一个在文件系统中,另一个在硬件令牌中。

AuthenticationMethods publickey,publickey

AuthenticationMethods 指令(无论是用于密钥还是密码),也可以在服务器上的 Match 指令下设置,以仅应用于某些组或情况。

要求特定密钥类型进行身份验证

[编辑 | 编辑源代码]

同样从 OpenSSH 6.8 开始,PubkeyAcceptedKeyTypes 指令(后来更改为 PubkeyAcceptedAlgorithms)可以指定哪些密钥算法被接受进行身份验证。不在逗号分隔的模式列表中的那些密钥算法是不允许的。

PubkeyAcceptedAlgorithms ssh-ed25519*,ssh-rsa*,ecdsa-sha2*,sk-ssh-ed25519*,sk-ecdsa-sha2*

列表中可以是实际的密钥类型或模式。模式列表中不允许空格。可以使用客户端的 **-Q** 选项找到支持身份验证的密钥类型的完整列表。以下两行等效。

$ ssh -Q key-sig | sort

$ ssh -Q PubkeyAcceptedAlgorithms | sort

对于基于主机的身份验证,是 **HostbasedAcceptedAlgorithms** 指令决定允许用于身份验证的密钥类型。

使用代理的基于密钥的身份验证

[edit | edit source]

当使用身份验证代理,例如 ssh-agent(1) 时,通常应该在会话开始时启动它,并使用它来启动登录会话或 X 会话,以便指向代理及其 UNIX 域套接字的环境变量被传递到每个后续 shell 和进程。许多桌面发行版在登录或启动时自动执行此操作。

启动代理需要设置一对环境变量

  • SSH_AGENT_PID : 代理的进程 ID
  • SSH_AUTH_SOCK : UNIX 域套接字的文件名和完整路径

各种 SSH 和 SFTP 客户端会自动找到这些变量,并在需要身份验证时使用它们来联系代理并尝试。但是,主要使用的是 SSH_AUTH_SOCK。如果 shell 或桌面会话是使用 ssh-agent(1) 启动的,那么这些变量已经设置并可用。如果它们不可用,则需要在每个 shell 中手动设置这些变量,或者在每个应用程序中设置这些变量以使用代理,或者使用客户端配置文件中的 **IdentityAgent** 指令指向代理的套接字。

代理可用后,需要加载相应的私钥才能使用代理。私钥一旦进入代理,就可以多次使用。私钥使用 ssh-add(1) 加载到代理中。

$ ssh-add /home/fred/.ssh/mykey_ed25519
Identity added: /home/fred/.ssh/mykey_ed25519 (/home/fred/.ssh/mykey_ed25519)

密钥会一直保留在代理中,直到代理停止运行,除非另有说明。可以在启动代理本身时或使用 ssh-add(1) 实际加载密钥时使用 **-t** 选项设置超时。在这两种情况下,**-t** 选项都会设置一个超时间隔,超过此间隔,密钥将从代理中清除。

$ ssh-add -t 1h30m /home/fred/.ssh/mykey_ed25519
Identity added: /home/fred/.ssh/mykey_ed25519 (/home/fred/.ssh/mykey_ed25519)
Lifetime set to 5400 seconds

**-l** 选项将列出代理中所有身份的指纹。

$ ssh-add -l                                    
256 SHA256:77mfUupj364g1WQ+O8NM1ELj0G1QRx/pHtvzvDvDlOk mykey for task x (ED25519)
3072 SHA256:7unq90B/XjrRbucm/fqTOJu0I1vPygVkN9FgzsJdXbk myotherkey rsa for task y (RSA)

也可以使用 **-d** 从代理中删除单个身份,如果通过文件名识别,它将一次删除一个身份,但只有在给出文件名并且没有给出要删除的私钥的文件名时,**-d** 才会静默失败。使用 **-D** 而不是 **-d** 将一次删除所有身份,而无需指定任何身份的名称。

默认情况下,ssh-add(1) 使用通过环境变量 **SSH_AUTH_SOCK** 中命名的套接字连接的代理(如果已设置)。目前,这是它唯一的选项。但是,对于 ssh(1),除了使用环境变量,还可以使用客户端配置文件指令 **IdentityAgent**,它告诉 SSH 客户端使用哪个套接字与代理通信。如果环境变量和配置文件指令同时可用,则 **IdentityAgent** 中的值优先于环境变量中的值。**IdentityAgent** 也可以设置为 _none_ 以防止连接尝试使用任何代理。

客户端配置文件指令 **AddKeysToAgent** 在将密钥添加到代理时也很有用。启用此指令后,如果密钥尚未加载,它将在第一次调用密钥时自动将密钥加载到正在运行的代理中。同样,**IdentitiesOnly** 指令可以确保在第一次尝试时提供相关密钥。与其在每次运行客户端时都输入这些指令,不如将它们添加到 **~/.ssh/config** 中,从而为指定的宿主连接自动添加它们。

代理转发

[edit | edit source]

代理转发是通过一个或多个中间主机的一种方式。但是,**-J** 选项用于 **ProxyJump** 将是一个更安全的选项。请参阅跳转主机部分中的 通过网关或两个网关,了解有关该选项的信息。使用代理转发,中间机器会在客户端和最终目的地之间来回转发挑战和响应。这存在一些风险,但也无需在这些中间机器上使用密码或保存密钥。

代理转发的主要优势在于,私钥本身不需要在任何远程机器上,从而阻止了对私钥的不必要的访问。[3] 另一个优势是,用户已通过身份验证的实际代理不会移动到任何地方,因此不易被分析。

代理的一个风险是,如果权限允许,它们可以被重复使用以进行尾随登录。密钥不能以这种方式复制,但在权限不正确时,身份验证是可能的。请注意,禁用代理转发不会提高安全性,除非用户也被拒绝 shell 访问权限,因为他们始终可以安装自己的转发器。

可以通过在将密钥添加到代理时添加 **-c** 选项来确认密钥的每次使用,从而减轻代理转发的风险。这需要 SSH_ASKPASS 变量设置并可用于代理进程,但将在每次远程系统使用密钥时在运行代理的主机上生成提示。因此,如果要通过一个或多个中间主机,通常最好让 SSH 客户端使用带有 **-W** 或 **-J** 的标准输入转发。

在客户端方面,代理转发默认情况下是禁用的,因此如果要使用它,必须显式启用它。将以下行添加到 ssh_config(5) 中,以启用特定服务器的代理转发

        Host gateway.example.org
        ForwardAgent yes

在服务器端,默认配置文件允许身份验证代理转发,因此要使用它,只需要在客户端端配置即可。但是,再次强调,最好考虑使用 **ProxyJump** 替代方案。

旧样式,相对安全的 SSH 代理转发
[edit | edit source]

通过一个或多个中间主机的最佳方法是使用 **ProxyJump** 选项而不是身份验证代理转发,从而避免暴露任何私钥。如果必须使用身份验证代理转发,那么为了遵循最小权限原则,建议转发包含最少必要密钥数量的代理。有几种方法可以解决这个问题。

在 8.8 版及更早版本中,一种部分解决方案是创建一个一次性的、临时的代理来保存当前任务所需的单个密钥或多个密钥。另一种部分解决方案是,在操作系统级别设置一个用户可访问的服务,然后使用 ssh_config 完成其余操作。

可以通过创建特殊的 shell 别名或函数来启动一个单次使用的代理,从而自动启动每个会话独有的临时代理。函数或别名都可以被编写为要求对每个请求的签名进行确认。以下示例是一个别名,它基于 Vincent Bernat[4] 在 SSH 代理转发方面的更新博客文章。

$ alias assh="ssh-agent ssh -o AddKeysToAgent=confirm -o ForwardAgent=yes"

请注意,使用的是 ssh-agent(1)。当调用该别名时,SSH 客户端将使用一个唯一的、临时的支持密钥代理启动。别名设置了一个新的代理,包括设置两个环境变量,然后在调用客户端时设置了两个客户端选项。此安排仍然检查 ssh_config(5) 以获取其他选项和设置。SSH 会话结束后,启动它的代理也会结束并消失,从而自动清理自身。

另一种方法是依赖客户端的配置文件来设置一些设置。这些方法主要依赖于 ssh_config(5),但仍然需要独立的方法来启动临时代理,因为 OpenSSH 客户端在读取配置文件之前就已经运行了,因此不受配置文件引起的任何环境变量更改的影响,而代理信息正是通过环境变量传递的。但是,当与身份验证代理通信的 UNIX 域套接字的路径在事先决定时,**IdentityAgent** 选项可以指向它,一旦一次性代理[5] 实际上启动。以下示例在连接到两个特定域中的任何一个时,使用特定代理的预定义套接字

Host *.wikimedia.org *.wmflabs.org
        User fred
        IdentitiesOnly yes
        IdentityFile %d/.ssh/id_cloud_01
        IdentityAgent /run/user/%i/ssh-cloud-01.socket
        ForwardAgent yes
        AddKeysToAgent yes

**%d** 代表主目录的路径,**%i** 代表当前帐户的用户 ID(UID)。在某些情况下,**%i** 标记在设置配置文件中的 **IdentityAgent** 选项时也很有用。再次强调,在转发代理时,请谨慎使用转发代理中的密钥。请参阅 ssh_config(5) 中的“标记”部分,了解有关此类缩写的更多信息。

使用这些配置设置,身份验证代理必须在启动 SSH 客户端之前已启动并运行,并指向指定的套接字,以便该配置能够正常工作。此外,它应该将套接字放置在任何其他帐户都无法访问的目录中。 ssh-agent(1) 必须使用 **-a** 选项来命名套接字

$ ssh-agent -a /run/user/${UID}/ssh-cloud-01.socket

该代理配置可以手动启动,也可以通过脚本或服务管理器启动。但是,为了保护隐私和安全性,应避免代理转发。**ProxyJump** 配置指令是最佳替代方案,在较旧的系统上,使用 netcat 的 **ProxyCommand** 进行主机遍历是更优选择。再次强调,请参阅有关 代理和跳转主机 的部分,了解这些方法的使用方式。

新样式的 SSH 代理目标约束
[编辑 | 编辑源代码]

从 8.9 版本开始,ssh-agent(1) 将允许代理限制其用于身份验证的主机,如由 ssh-add(1) 使用 -h 选项指定。这些约束是通过两个代理协议扩展和对公钥身份验证协议的修改添加的。此功能可能会演变,但目前的结果是,帐户身份验证的密钥可以通过四种方式加载到代理中

  • 转发没有限制(不建议)
  • 仅限本地使用,这些不会被转发
  • 转发,但仅转发到特定远程主机
  • 通过指定路由转发到特定远程主机

目的是安全地失败限制,以防止在路由中的一个或多个主机缺少必要的协议功能时允许身份验证。加载密钥后,目的地和路由无法修改,但可以加载到同一目的地的多个路由,并且路由可以是任意数量的跳跃。如果需要更改路由,则必须使用新路由将密钥重新加载到代理中。

客户端的一般默认设置是仅将密钥保留在代理中以供本地使用。但是,可以通过在启动客户端时添加 -a 选项,或在 ssh_config(5) 的相关配置块中将 ForwardAgent 指令设置为 'no' 来明确地强制执行该设置。

为了加载用于无限制转发的密钥(这不是最好的主意),只需使用 ssh-add(1) 像往常一样添加它们。然后在客户端使用 -A 选项,或在 ssh_config(5) 的相关配置块中将 ForwardAgent 指令设置为 'yes'。

为了限制密钥仅连接到特定的远程主机,或将密钥加载到通过一个或多个中间主机进行转发的特定远程主机,请在将密钥加载到代理时使用 -h 选项。这里,一个密钥可能仅用于连接到特定目的地

$ ssh-agent -h server.example.org server.key.ed25519

如果传递了中间系统,最好的方法是使用 ProxyJump,即 SSH 客户端的 -J 选项。如果必须允许代理转发,那么最严格的方法是限制哪些系统可以使用密钥,再次使用 -h 选项。

$ ssh-agent -h middle.example.org -h "middle.example.org>server.example.org" server.key.ed25519

可以包含多个步骤,甚至多个路由。它们只需要明确枚举,尽管对于目标主机以及特定名称仍然可以使用模式。链中的每个主机都必须支持这些协议扩展,以使连接完成。

任何指定用于转发的密钥都无法用于对除那些已明确标识用于转发的主机以外的任何其他主机进行身份验证。这些允许的主机通过 known_hosts 文件或在使用 ssh-add(1) 加载密钥时由 -H 选项指定的另一个文件中的主机密钥或主机证书来标识。如果在将密钥加载到代理时未使用 -H,则将使用默认的已知主机文件:~/.ssh/known_hosts/etc/ssh/ssh_known_hosts~/.ssh/known_hosts2/etc/ssh/ssh_known_hosts2

在密钥的情况下,必须认真维护 known_hosts 列表[6],可能需要借助 UpdateHostkeysCanonicalizeHostname 客户端配置指令。使用证书需要代理只需要知道证书颁发机构 (CA) 即可。

再次,请参阅跳跃主机部分的 通过网关或两个网关,了解一种无需转发 SSH 代理即可通过一个或多个中间机器的方法。

检查代理以获取特定密钥

[编辑 | 编辑源代码]

ssh_add(1) 实用程序的 -T 选项可以通过查找匹配的公钥来测试特定私钥是否在代理中可用。这在 shell 脚本中很有用。

#!/bin/sh

key=/home/fred/.ssh/some.key.ed25519.pub

if ssh-add -T ${key}; then
        echo "Key ${key} Found"
else
        echo "Key ${key} missing"
fi

或者,也可以在脚本或交互式 shell 会话中以另一种语法完成,

$ key=/home/fred/.ssh/some.key.ed25519.pub
$ ssh-add -T ${key} && echo "Key found" || echo "Key missing"

但是,如果想要将密钥添加到代理中,则 AddKeysToAgent 客户端配置选项可以确保在任何给定登录会话期间首次使用时将特定密钥添加到 SSH 代理。这可以使用 -o AddKeysToAgent=yes 作为运行时参数来完成,或者通过适当地修改 ssh_config(5) 来完成

Host www
        HostName www.example.com
        IdentityFile %d/.ssh/www.ed25519
        IdentitiesOnly yes
        AddKeysToAgent yes

在配置文件中使用这些选项后,第一次运行 ssh www 时,指定的密钥将被添加到代理中,并保持可用。

使用硬件安全令牌的基于密钥的身份验证

[编辑 | 编辑源代码]

虽然独立密钥已经存在很长时间了,但从 8.2 版本开始,就可以使用由硬件安全令牌(如 OnlyKey、Yubikey 或许多其他令牌)支持的密钥,尽管使用了 FIDO2 协议。通用第二因素 (U2F) 身份验证通过 FIDO2 直接在 OpenSSH 中得到支持,不需要第三方软件。目前有两种类型的硬件支持密钥,ECDSA-SK 和 Ed25519-SK,但只有最新的硬件令牌支持后者。如果令牌的固件不支持 Ed25519-SK 密钥格式,则在尝试使用该密钥类型时会显示以下错误消息

Key enrollment failed: invalid format

如果支持,可以使用 ssh-keygen(1) 创建任一密钥类型。步骤与创建普通密钥几乎相同,但令牌必须首先对系统可用(已插入)。然后,如果需要,必须输入令牌的 PIN 并触摸或以其他方式激活令牌。之后,密钥创建过程将像往常一样进行。注意由 -t 选项指定的密钥类型。

$ ssh-keygen -t ed25519-sk -f /home/fred/.ssh/server.ed25519-sk -C "web server for fred"
Generating public/private ed25519-sk key pair.
You may need to touch your authenticator to authorize key generation.
Enter passphrase (empty for no passphrase): 
Enter same passphrase again: 
Your identification has been saved in /home/fred/.ssh/server.ed25519-sk
Your public key has been saved in /home/fred/.ssh/server.ed25519-sk.pub
The key fingerprint is:
SHA256:41wVVDnKJ9gKr2Sj4CFuYMhcNvYebZ6zq0PWyP4rRDo web server
The key's randomart image is:
+[ED25519-SK 256]-+
|           .o... |
|             .o  |
|           +.. . |
|   = .  . ..= .  |
|+ + * + So.. o   |
|o+.EoO *+oo      |
|.o oBo+++o       |
|  o .=.+.        |
| .   .===        |
+----[SHA256]-----+

创建后,公钥和私钥文件将像处理其他类型的密钥一样处理。但在进行身份验证时,必须在需要时提供硬件令牌并激活它。

$ ssh -i /home/fred/.ssh/server.ed25519-sk server.example.org
Enter passphrase for key '/home/fred/.ssh/server.ed25519-sk': 
Confirm user presence for key ED25519-SK SHA256:41wVVDnKJ9gKr2Sj4CFuYMhcNvYebZ6zq0PWyP4rRDo

生成的私钥文件实际上不是密钥本身,而是“密钥句柄”,它用于硬件安全令牌在实际使用时按需派生真实私钥[7]。因此,没有随附的硬件令牌,硬件支持的私钥文件是无用的。这也意味着这些密钥文件无法跨硬件令牌移植,例如当拥有多个备用或备份令牌时,即使由同一帐户使用。因此,当使用多个硬件令牌时,必须为每个令牌生成不同的密钥对。

硬件安全令牌驻留私钥

[编辑 | 编辑源代码]

可以在令牌本身内存储私钥,但目前无法直接从令牌内部使用,必须先将其保存为文件。此外,密钥只能在创建时使用 ssh-keygen(1) 中的 -O resident 选项加载到 FIDO 身份验证器中。否则,该过程与上述过程相同。

$ ssh-keygen -O resident -t ed25519-sk -f /home/fred/.ssh/server.ed25519-sk -C "web server for fred"
. . .

需要时,可以使用 -K 选项从 FIDO2 硬件令牌中提取驻留密钥并将其保存到文件中。在此阶段,可以为文件添加密码短语,但令牌本身不保留密码短语,只有可选的 PIN 保护令牌中的密钥。

$ ssh-keygen -K
Enter PIN for authenticator: 
Enter passphrase (empty for no passphrase): 
Enter same passphrase again: 
Saved ED25519-SK key to id_ed25519_sk_rk

$ mv -i id_ed25519_sk_rk /home/fred/.ssh/server.ed25519-sk

由于输出文件名是固定的,任何预先存在的具有该名称的文件都可能被覆盖,但首先会有警告。但是,不建议将密钥保存在硬件令牌中,因为与单独保存相比,这样做可以提供更多保护。

单用途密钥

[编辑 | 编辑源代码]

定制的单用途密钥可以消除许多管理活动的远程 root 登录。需要一个精心定制的 sudoers 以及一个非特权帐户。如果操作正确,它将提供足够的访问权限来完成工作,遵循最小特权的安全原则。

单用途密钥伴随着在 sshd_config(5) 中使用 ForceCommand 指令或在 authorized_keys 文件中使用 command="..." 指令。该方法是生成一个新的密钥对,将公钥传输到远程系统上的 authorized-keys,然后在该行中将相应的命令或脚本预先添加到该行。该行包含密钥。

$ grep '^command' ~/.ssh/authorized_keys
command="/usr/local/bin/somescript.sh" ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEcgdzDvSebOEjuegEx4W1I/aA7MM3owHfMr9yg2WH8H

在其中插入的 command="..." 指令会覆盖其他所有内容,并确保在仅使用该密钥登录时,仅运行脚本 /usr/local/bin/somescript.sh。如果需要将参数传递给脚本,请查看 SSH_ORIGINAL_COMMAND 环境变量的内容,并在 case 语句中使用它。永远不要信任该变量的内容,也不要直接使用该变量的内容,始终间接使用。

单用途密钥对于仅允许隧道且不执行其他操作很有用。以下密钥只会回显一些文本然后退出,除非与 -N 选项一起非交互式地使用。

$ grep '^command' ~/.ssh/authorized_keys
command="/bin/echo do-not-send-commands" ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBzTIWCaILN3tHx5WW+PMVDc7DfPM9xYNY61JgFmBGrA

无论用户在使用该密钥登录时尝试什么,会话只会回显给定的文本然后退出。使用 -N 选项会禁用运行远程程序,允许连接保持打开状态,从而允许隧道。

$ ssh -L 3306:localhost:3306 \
	-i ~/.ssh/tunnel_ed25519 \
	-N \
	-l fred \
	server.example.com

这会创建隧道并保持连接,尽管密钥配置会关闭交互式会话。另请参阅 ssh(1)-n-f 选项。

单用途密钥以避免远程 root 访问

[编辑 | 编辑源代码]

最简单的方法是编写一个简短的 shell 脚本,将其放在/usr/local/bin/目录下,然后配置sudoers,允许没有权限的帐户只运行该脚本,并且只运行该脚本。

%wheel  ALL=(root:root) NOPASSWD: /usr/sbin/service httpd stop
%wheel  ALL=(root:root) NOPASSWD: /usr/sbin/service httpd start

然后,密钥使用authorized_keys中的command="..."调用脚本。例如,一个密钥启动Web服务器,另一个密钥停止Web服务器。

$ grep '^command' ~/.ssh/authorized_keys
command="/usr/bin/sudo /usr/sbin/service httpd stop" ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEcgdzDvSebOEjuegEx4W1I/aA7MM3owHfMr9yg2WH8H
command="/usr/bin/sudo /usr/sbin/service httpd start" ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMidyqZ6OCvbWqA8Zn+FjhpYE6NoWSxVjFnFUk6MrNZ4

对于像rsync(1)tar(1)mysqldump(1)等复杂的程序,在构建单用途密钥时,需要采用更高级的方法。对于这些程序,-v选项可以显示传递给服务器的具体内容,以便正确设置sudoers。这样,它们可以被限制为仅访问文件系统中指定的区域。例如,下面是rsync(1)的一个特定使用情况,请注意“发送命令”行。

$ rsync -e 'ssh -v' [email protected]:/etc/ ./backup/etc/
. . .
debug1: Sending command: rsync --server --sender -e.LsfxC . /etc/
. . .

然后,可以将该输出添加到sudoers中,以便密钥只能执行该功能。

%backup ALL=(root:root) NOPASSWD: /usr/bin/rsync --server --sender -e.LsfxC . /etc/

然后,将所有内容整合在一起,帐户“backup”需要一个密钥。

$ grep '^command' ~/.ssh/authorized_keys
command="/usr/bin/rsync --server --sender -e.LsfxC . /etc/" ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMm0rs4eY8djqBb3dIEgbQ8lmdlxb9IAEuX/qFCTxFgb

许多程序都有---dry-run或等效选项。在确定正确的设置时,请记住使用它。

对密钥的只读访问

[edit | edit source]

在某些情况下,需要阻止帐户更改自己的身份验证密钥。但是,这种情况更适合使用证书。但是,如果使用密钥,则可以通过将密钥文件放在用户具有只读访问权限的外部目录(包括目录和密钥文件本身)来实现。然后,AuthorizedKeysFile指令指定sshd(8)查找密钥的位置,并且可以指向密钥的安全位置,而不是默认位置。

一个好的备用位置可能是新的目录/etc/ssh/authorized_keys,该目录可以存储选定帐户的密钥文件。可以通过在Match指令下添加设置来使更改仅应用于一组帐户。大多数系统上密钥的默认位置通常是~/.ssh/authorized_keys

Match Group sftpusers
	AuthorizedKeysFile /etc/ssh/authorized_keys/%u

然后,那里的权限将允许读取密钥,但不能写入密钥。

$ ls -dhln /etc/ssh/
drwxr-x--x 3 0 0 4.0K Mar 30 22:16 /etc/ssh/authorized_keys/

$ ls -dhln /etc/ssh/*.pub
-rw-r--r-- 1 0 0 173 Mar 23 13:34 /etc/ssh/fred
-rw-r--r-- 1 0 0  93 Mar 23 13:34 /etc/ssh/user1
-rw-r--r-- 1 0 0 565 Mar 23 13:34 /etc/ssh/user2
. . .

密钥甚至可以位于子目录中,但权限和所有权方面的限制相同。

对于chroot SFTP,方法相同,以确保帐户无法访问密钥文件。

Match Group sftpusers
	ChrootDirectory /home
	ForceCommand internal-sftp -d %u
	AuthorizedKeysFile /etc/ssh/authorized_keys/%u

当然,Match指令不是必需的。可以通过在服务器配置文件的主体部分添加指令,使设置应用于所有帐户。

将公钥标记为已撤销

[edit | edit source]

可以撤销密钥。已撤销的密钥可以存储在/etc/ssh/revoked_keys中,该文件在sshd_config(5)中使用RevokedKeys指令指定,以便sshd(8)阻止尝试使用这些密钥登录。如果尝试使用已撤销的密钥,客户端侧不会出现警告或错误。身份验证将简单地继续执行下一个密钥或方法。

已撤销的密钥文件应包含已撤销的公钥列表,每行一个,这些密钥不再能够用于连接到服务器。密钥不能包含任何额外的内容,例如登录选项,否则它将被忽略。如果在登录尝试期间尝试使用已撤销的密钥之一,服务器将简单地忽略它,并继续执行下一个身份验证方法。将记录尝试的日志条目,包括密钥的指纹。有关更多信息,请参阅有关日志记录的部分。

RevokedKeys /etc/ssh/revoked_keys

默认情况下,sshd_config(5)中没有设置RevokedKeys配置指令。如果要使用它,必须显式设置它。这是另一个情况,可能更适合通过使用证书来满足,因为可以为证书设置任何秒、分钟、小时、天或周的组合的有效期间隔,而密钥的有效期无限。

密钥撤销列表

[edit | edit source]

密钥撤销列表 (KRL) 是表示已撤销的密钥和证书的紧凑的二进制形式。要使用 KRL,服务器的配置文件必须使用RevokedKeys指令指向一个有效的列表。KRL 本身是使用ssh-keygen(1)生成的,可以从头开始创建或就地编辑。这里创建了一个新的 KRL,并填充了一个公钥。

$ ssh-keygen -kf  /etc/ssh/revoked_keys -z 1 ~/.ssh/old_key_rsa.pub

这里通过添加-u选项更新了一个现有的 KRL。

$ ssh-keygen -ukf /etc/ssh/revoked_keys -z 2 ~/.ssh/old_key_dsa.pub

一旦 KRL 就位,就可以测试特定的密钥或证书是否在撤销列表中。

$ ssh-keygen -Qf  /etc/ssh/revoked_keys ~/.ssh/old_key_rsa.pub

只有公钥和证书才会加载到 KRL 中。损坏或损坏的密钥不会被加载,如果尝试加载,将产生错误消息。与常规RevokedKeys列表一样,用于 KRL 的公钥不能包含任何额外的内容,例如登录选项,否则在尝试将其加载到 KRL 或在 KRL 中搜索它时,将产生错误。

通过指纹验证主机密钥

[edit | edit source]

上面的示例都是关于使用密钥对客户端进行身份验证到服务器。密钥使用的另一种情况是当服务器识别自己到客户端时,这会在每个非多路复用会话的开始时自动发生。为了进行这种识别,客户端会从服务器获取公钥,通常是在第一次联系之前或之前,之后客户端可以使用该公钥来确保它再次连接到同一个服务器,而不是冒名顶替者。在客户端上存储这些获取的主机密钥的默认位置是/etc/ssh/ssh_known_hosts(如果由系统管理员管理),或者~/.ssh/known_hosts(如果由客户端自己的帐户管理)。内容的格式是包含主机地址及其匹配公钥的一行。该文件在sshd(8)手册页的“SSH_KNOWN_HOSTS 文件格式”部分中有详细说明。

第一次连接到远程主机时,应该验证服务器的主机密钥,以确保客户端连接到正确的机器,而不是冒名顶替者或其他任何东西。通常,这种验证是通过比较服务器主机密钥的指纹来完成的,而不是尝试比较密钥本身。默认情况下,如果密钥在known_hosts注册表中没有找到,客户端将显示指纹。

$ ssh -l fred server.example.org
The authenticity of host 'server.example.org (192.0.32.10)' can't be established.
ECDSA key fingerprint is SHA256:LPFiMYrrCYQVsVUPzjOHv+ZjyxCHlVYJMBVFerVCP7k.
Are you sure you want to continue connecting (yes/no)?

这可以与通过带外接收到的指纹进行比较,例如通过邮件、电子邮件、短信、快递等。具体来说,该示例将密钥的指纹表示为 base64 编码的 SHA256 校验和。这是默认样式。指纹也可以通过将客户端的FingerprintHash配置指令作为运行时参数传递或在ssh_config(5)中设置它来以十六进制显示为 MD5 哈希。

$ ssh -o FingerprintHash=md5 host.example.org
The authenticity of host 'host.example.org (192.0.32.203)' can't be established.
RSA key fingerprint is MD5:10:4a:ec:d2:f1:38:f7:ea:0a:a0:0f:17:57:ea:a6:16.
Are you sure you want to continue connecting (yes/no)?

但是,新版本的默认值是 base64 中的 SHA256,它发生冲突的可能性更低。

在 OpenSSH 6.7 及更早版本中,客户端将指纹显示为十六进制 MD5 校验和,而不是当前使用的 base64 编码的 SHA256 校验和。

$ ssh -l fred server.example.org
The authenticity of host 'server.example.org (192.0.32.10)' can't be established. 
RSA key fingerprint is 4a:11:ef:d3:f2:48:f8:ea:1a:a2:0d:17:57:ea:a6:16. 
Are you sure you want to continue connecting (yes/no)?

另一种比较密钥的方法是使用 ASCII 艺术视觉主机密钥。请参阅下面有关此内容的进一步介绍。

下载密钥

[edit | edit source]

即使通常会显示主机密钥以供 SSH 客户端在第一次尝试连接时进行审查,也可以随时使用ssh-keyscan(1)按需获取它。

$ ssh-keyscan host.example.org  
# host.example.org SSH-2.0-OpenSSH_8.2
host.example.org ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBLC2PpBnFrbXh2YoK030Y5JdglqCWfozNiSMjsbWQt1QS09TcINqWK1aLOsNLByBE2WBymtLJEppiUVOFFPze+I=
# host.example.org SSH-2.0-OpenSSH_8.2
host.example.org ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC9iViojCZkcpdLju7/3+OaxKs/11TAU4SuvIPTvVYvQO32o4KOdw54fQmd8f4qUWU59EUks9VQNdqf1uT1LXZN+3zXU51mCwzMzIsJuEH0nXECtUrlpEOMlhqYh5UVkOvm0pqx1jbBV0QaTyDBOhvZsNmzp2o8ZKRSLCt9kMsEgzJmexM0Ho7v3/zHeHSD7elP7TKOJOATwqi4f6R5nNWaR6v/oNdGDtFYJnQfKUn2pdD30VtOKgUl2Wz9xDNMKrIkiM8Vsg8ly35WEuFQ1xLKjVlWSS6Frl5wLqmU1oIgowwWv+3kJS2/CRlopECy726oBgKzNoYfDOBAAbahSK8R
# host.example.org SSH-2.0-OpenSSH_8.2
host.example.org ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDDOmBOknpyJ61Qnaeq2s+pHOH6rdMn09iREz2A/yO2m

获取密钥后,可以使用ssh-keygen(1)显示其指纹。这可以通过管道直接完成。

$ ssh-keyscan host.example.org | ssh-keygen -lf - 
# host.example.org SSH-2.0-OpenSSH_8.2
# host.example.org SSH-2.0-OpenSSH_8.2
# host.example.org SSH-2.0-OpenSSH_8.2
256 SHA256:sxh5i6KjXZd8c34mVTBfWk6/q5cC6BzR6Qxep5nBMVo host.example.org (ECDSA)
3072 SHA256:hlPei3IXhkZmo+GBLamiiIaWbeGZMqeTXg15R42yCC0 host.example.org (RSA)
256 SHA256:ZmS+IoHh31CmQZ4NJjv3z58Pfa0zMaOgxu8yAcpuwuw host.example.org (ED25519)

如果服务器在轮询的端口上提供多个公钥类型,那么ssh-keyscan(1)将获取每个公钥类型。如果通过stdin或文件提供了多个密钥,那么ssh-keygen(1)将按顺序处理它们。在 OpenSSH 7.2 之前,手动指纹是一个两步过程,密钥被读取到一个文件,然后处理以生成其指纹。

$ ssh-keyscan -t ed25519 host.example.org > key.pub
# host.example.org SSH-2.0-OpenSSH_6.8
$ ssh-keygen -lf key.pub                       
256 SHA256:ZmS+IoHh31CmQZ4NJjv3z58Pfa0zMaOgxu8yAcpuwuw host.example.org (ED25519)

请注意,ssh-keyscan(1)的某些输出发送到stderr而不是stdout

可以使用awk(1)sed(1)xxd(1)(在找到这些命令的系统上)手动生成哈希或指纹。

$ awk '{print $2}' key.pub | base64 -d | md5sum -b | sed 's/../&:/g; s/: .*$//'
$ awk '{print $2}' key.pub | base64 -d | sha256sum -b | sed 's/ .*$//' | xxd -r -p | base64

如果主机名以纯文本形式存储而不是以哈希形式存储,则可以从文件中找到所有与known_hosts中存在的密钥新或不同的主机。

$ ssh-keyscan -t rsa,ecdsa -f ssh_hosts | \
  sort -u - ~/.ssh/known_hosts | \
  diff ~/.ssh/known_hosts -

将 ssh-keyscan(1) 与 ssh_config(5) 一起使用

[编辑 | 编辑源代码]

实用程序 ssh-keyscan(1) 不会解析 ssh_config(5)。这部分是为了保持代码库简单。有很多配置选项难以实现,包括但不限于 ProxyJumpProxyCommandMatchBindInterfaceCanonicalizeHostname[8] 。可以通过将实用程序包装在一个简短的 shell 函数中来完成通过客户端配置文件解析主机名。

my-ssh-keyscan() {
	for host in "$@" ; do
		ssh-keyscan $(ssh -G "$host" | awk '/^hostname/ {print $2}')
	done
}

该 shell 函数使用 ssh(1)-G 选项通过 ssh_config(5) 解析每个主机名,然后检查结果主机名的 SSH 密钥。

ASCII 艺术可视化主机密钥

[编辑 | 编辑源代码]

可以显示密钥的 ASCII 艺术表示形式以及 SHA256 base64 指纹。

$ ssh-keygen -lvf key 
256 SHA256:BClQBFAGuz55+tgHM1aazI8FUo8eJiwmMcqg2U3UgWU www.example.org (ED25519)
+--[ED25519 256]--+
|o+=*++Eo         |
|+o .+.o.         |
|B=.oo.  .        |
|*B.=.o .         |
|= B *   S        |
|. .@ .           |
| +..B            |
|  *. o           |
| o.o.            |
+----[SHA256]-----+

在 OpenSSH 6.7 及更早版本中,指纹采用 MD5 十六进制形式。

$ ssh-keygen -lvf key 
2048 37:af:05:99:e7:fb:86:6c:98:ee:14:a6:30:06:bc:f0 www.example.net (RSA) 
+--[ RSA 2048]----+ 
|          o      | 
|         o .     | 
|        o o      | 
|       o +       | 
|   .  . S        | 
|    E ..         | 
|  .o.* ..        | 
|  .*=.+o         | 
|  ..==+.         | 
+-----------------+

有关验证 SSH 密钥的更多信息

[编辑 | 编辑源代码]

可以通过比较 base64 编码的 SHA256 指纹,验证客户端或服务器上的密钥是否与已知良好密钥匹配。

验证流浪客户端密钥

[编辑 | 编辑源代码]

有时需要比较两个不确定的密钥文件以检查它们是否属于同一个密钥对。但是,公钥或多或少是可丢弃的。因此,在这种情况下,在客户端机器上简单的方法是只需重命名或擦除旧的、有问题的公钥,并用从现有私钥生成的新的公钥替换它。

$ ssh-keygen -y -f ~/.ssh/my_key_rsa

但是,如果必须真正比较这两个部分,则需要使用 ssh-keygen(1) 分两步完成。首先,从已知的私钥重新生成一个新的公钥,并将其用于生成到 stdout 的指纹。接下来,生成未知公钥的指纹以进行比较。在此示例中,比较了私钥 my_key_a_rsa 和公钥 my_key_b_rsa.pub

$ ssh-keygen -y -f my_key_a_rsa | ssh-keygen -l -f -
$ ssh-keygen -l -f my_key_b_rsa.pub

结果是每个密钥的 base64 编码 SHA256 校验和,其中一个指纹显示在另一个指纹下方,便于视觉比较。较旧的版本不支持从 stdin 读取,因此需要一个中间文件。即使是更旧的版本也只能显示每个密钥的 MD5 校验和。无论哪种方式,使用 shell 脚本进行自动化都足够简单,但这超出了本书的范围。

验证服务器密钥

[编辑 | 编辑源代码]

首次连接时,必须对服务器的主机密钥进行可靠验证。可能需要联系系统管理员,他们可以提供密钥以进行带外验证,以便预先知道指纹并准备好验证第一个连接。

以下是一个读取服务器的 RSA 密钥并将其指纹显示为 SHA256 base64 的示例。

$ ssh-keygen -lf /etc/ssh/ssh_host_rsa_key.pub     
3072 SHA256:hlPei3IXhkZmo+GBLamiiIaWbeGZMqeTXg15R42yCC0 [email protected] (RSA)

以下是读取相应的 ECDSA 密钥,但显示为 MD5 十六进制哈希。

$ ssh-keygen -E md5 -lf /etc/ssh/ssh_host_ecdsa_key.pub
256 MD5:ed:d2:34:b4:93:fd:0e:eb:08:ee:b3:c4:b3:4f:28:e4 [email protected] (ECDSA)

在 6.8 之前,指纹以 MD5 十六进制哈希表示。

$ ssh-keygen -lf /etc/ssh/ssh_host_rsa_key.pub 
2048 MD5:e4:a0:f4:19:46:d7:a4:cc:be:ea:9b:65:a7:62:db:2c [email protected] (RSA)

也可以使用 ssh-keyscan(1) 从活动的 SSH 服务器获取密钥。但是,仍然需要对指纹进行带外验证。

警告:远程主机标识已更改!

[编辑 | 编辑源代码]

如果服务器的密钥与客户端在系统或本地帐户的 authorized_keys 文件中记录的密钥不匹配,则客户端将发出警告以及可疑密钥的指纹。

@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@    WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!     @
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
IT IS POSSIBLE THAT SOMEONE IS DOING SOMETHING NASTY!
Someone could be eavesdropping on you right now (man-in-the-middle attack)!
It is also possible that a host key has just been changed.
The fingerprint for the RSA key sent by the remote host is
SHA256:GkoIDP/d0I6KA9IQyOB9iqL+Rzpxx9LhlSJPCEfjVQ4.
Please contact your system administrator.
Add correct host key in /home/fred/.ssh/known_hosts to get rid of this message.
Offending RSA key in /home/fred/.ssh/known_hosts:19
  remove with:
  ssh-keygen -f "/home/fred/.ssh/known_hosts" -R "server.example.com"
RSA host key for server.example.com has changed and you have requested strict checking.
Host key verification failed.

警告的三个常见原因。

一个原因是服务器的密钥已被替换,通常是因为服务器的操作系统已重新安装,而没有备份旧的密钥。另一个原因可能是当系统管理员已淘汰过时或已泄露的密钥时。但是,可以更好地规划这一点,如果有时间规划迁移,则可以将新的密钥添加到服务器并让客户端使用 UpdateHostKeys 选项,以便如果旧的密钥匹配,则接受新的密钥。第三种情况是当连接到错误的机器时,例如当远程系统因动态地址分配而更改 IP 地址时。

在所有三种情况下,密钥已更改,只有一件事要做:联系系统管理员并验证密钥。询问 OpenSSH-server 是否最近重新安装,或者机器是否从旧备份中恢复?请记住,在某些情况下,系统管理员可能是你自己。

这种情况比较罕见,但足够严重,必须排除,即错误的机器是中间人攻击的一部分。

在所有四种情况下,都可以通过任何可以验证消息完整性和来源的方法获取真实的密钥指纹,例如通过 PGP 签名的电子邮件。如果可以进行物理访问,则使用控制台获取正确的指纹。获得真实的密钥指纹后,返回到收到错误的客户端机器,并从 ~/.ssh/known_hosts 中删除旧的密钥。

$ ssh-keygen -R server.example.org

然后尝试登录,但首先比较密钥指纹,并且仅当密钥指纹与带外接收的指纹匹配时才继续。如果密钥指纹匹配,则继续登录过程,密钥将自动添加。如果密钥指纹不匹配,请立即停止并找出连接到的内容。最好给远程机器的系统管理员或网络管理员打电话(真正的电话,而不是电脑电话)。

known_hosts 中的主机有多个密钥,密钥有多个主机

[编辑 | 编辑源代码]

多个主机名或 IP 地址可以使用 known_hosts 文件中的同一个密钥,方法是使用模式匹配或简单地列出同一个密钥的多个系统。这可以在 /etc/ssh/ssh_known_hosts 中的全局密钥列表以及每个帐户的 ~/.ssh/known_hosts 文件中每个帐户的本地、特定于帐户的密钥列表中完成。实验室、计算集群和类似的机器池可以使用这种方式的密钥。以下是三个特定主机共享的密钥,通过名称标识。

server1,server2,server3 ssh-rsa AAAAB097y0yiblo97gvl...jhvlhjgluibp7y807t08mmniKjug...==

或者可以使用通配符在 /etc/ssh/ssh_known_hosts~/.ssh/known_hosts 中指定一个范围,在一定程度上可以做到这一点。

172.19.40.* ssh-rsa AAAAB097y0yiblo97gvl...jhvlhjgluibp7y807t08mmniKjug...==

相反,对于同一个地址的多个密钥,需要在 /etc/ssh/ssh_known_hosts~/.ssh/known_hosts 中为每个密钥创建多个条目。

server1 ssh-rsa AAAAB097y0yiblo97gvljh...vlhjgluibp7y807t08mmniKjug...==
server1 ssh-rsa AAAAB0liuouibl kuhlhlu...qerf1dcw16twc61c6cw1ryer4t...==
server1 ssh-rsa AAAAB568ijh68uhg63wedx...aq14rdfcvbhu865rfgbvcfrt65...==

因此,为了让一组服务器共享一组密钥,必须手动将每个服务器-密钥组合添加到 known_hosts 文件中。

server1 ssh-rsa AAAAB097y0yiblo97gvljh...07t8mmniKjug...==
server1 ssh-rsa AAAAB0liuouibl kuhlhlu...qerfw1ryer4t...==
server1 ssh-rsa AAAAB568ijh68uhg63wedx...aq14rvcfrt65...==

server2 ssh-rsa AAAAB097y0yiblo97gvljh...07t8mmniKjug...==
server2 ssh-rsa AAAAB0liuouibl kuhlhlu...qerfw1ryer4t...==
server2 ssh-rsa AAAAB568ijh68uhg63wedx...aq14rvcfrt65...==

虽然升级到证书可能是一种更合适的方法,而不是手动更新大量密钥。

处理动态(漫游)IP 地址的另一种方法

[编辑 | 编辑源代码]

可以使用 HostKeyAlias 手动指向正确的密钥,无论是作为 ssh_config(5) 的一部分还是作为运行时参数。以下使用机器 Foobar 的密钥连接到主机 192.168.11.15。

$ ssh -o StrictHostKeyChecking=accept-new \
      -o HostKeyAlias=foobar   \
      192.168.11.15

当 DHCP 未配置为尝试随着时间的推移为同一台机器保留相同的地址或使用某些 stdio 转发方法通过中间主机传递时,这很有用。

known_hosts 中的主机密钥更新和轮换

[编辑 | 编辑源代码]

从 OpenSSH 6.8[9] 及更高版本开始,就包含了一个用于将弱公开密钥从 known_hosts 中移除的协议扩展。有了它,服务器能够向客户端通告所有其主机密钥,并在至少有一个已知的可信密钥的情况下,使用新密钥更新 known_hosts。此方法仍然要求服务器提供私钥[10],以便完成证明。在 ssh_config(5) 中,指令 UpdateHostKeys 指定了在身份验证完成并将其添加到 known_hosts 之后,客户端是否应该接受来自服务器的额外主机密钥更新。服务器可以在一段时间内为同一类型的密钥提供多个密钥,然后从提供的密钥中删除已过时的密钥,从而允许自动旋转密钥,以及从较弱的算法升级到更强的算法。另见 RFC 4819:安全外壳公开密钥子系统 关于密钥管理标准。


参考资料

[edit | edit source]
  1. "安全外壳(SSH)身份验证协议". IETF. 2006. Retrieved 2015-05-06.
  2. Steve Friedl (2006-02-22). "SSH 代理转发图解指南". Unixwiz.net. Retrieved 2013-04-27.
  3. Daniel Robbins (2002-02-01). "常见线程:OpenSSH 密钥管理,第 3 部分". IBM. Retrieved 2013-04-27.
  4. Vincent Bernat (2020-04-05). "更安全的 SSH 代理转发". Retrieved 2020-10-04.
  5. "管理多个 SSH 代理". Wikimedia. Retrieved 2020-04-07.
  6. Damien Miller (2021-12-16). "SSH 代理限制". OpenSSH. Retrieved 2022-03-06.
  7. "OpenSSH U2F/FIDO 支持在基础中". OpenBSD-Tech 邮件列表. 2019-11-14. Retrieved 2021-03-24.
  8. Miller, Damien (2023-03-01). "为什么 ssh-keyscan 不使用 .ssh/config?". mindrot.org. https://lists.mindrot.org/pipermail/openssh-unix-dev/2023-March/040605.html. Retrieved 2023-03-01. 
  9. Damien Miller (2015-02-01). "OpenSSH 6.8+ 中的密钥轮换". DJM 的个人博客. Retrieved 2016-03-05.
  10. Damien Miller (2015-02-17). "主机密钥轮换,再谈". DJM 的个人博客. Retrieved 2016-03-05.

 

华夏公益教科书