认证密钥可以提高效率,如果使用得当。作为额外的优势,密码短语和私钥永远不会离开客户端[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
  1. 创建密钥对。以下示例在目录~/.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选项设置使用的位数。

自OpenSSH 6.5起,新增了一种使用bcrypt(3)密钥派生函数(KDF)来更好地保护静态密钥的新私钥格式。此新格式始终用于Ed25519密钥,未来某时将成为所有密钥的默认格式。但目前,可以通过在ssh-keygen(1)中使用-o选项来生成或保存其他类型的密钥。

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

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

  1. 将密钥放到正确的位置。仅将公钥传输到远程主机。
$ ssh-copy-id -i ~/.ssh/mykey_ed25519 fred@remotehost.example.org

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

然后,如果客户端上没有密钥,将公钥和私钥一起传输到客户端。通常,最好将公钥和私钥保存在~/.ssh/目录中,尽管在此步骤之后,公钥不再需要在客户端上,并且可以在需要时重新生成。

  1. 测试密钥

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

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

选项-i告诉ssh(1)尝试哪个私钥。仅在验证基于密钥的认证工作正常后,才关闭原始SSH会话。

一旦验证了基于密钥的认证正常工作,就可以使用ssh_config(5)客户端上创建永久快捷方式,下面将进一步解释。特别是,参见IdentityFileIdentitiesOnlyAddKeysToAgent配置指令,举三个例子。

基于密钥认证故障排除

如果服务器拒绝接受密钥并转而使用下一种认证方法(例如:“Server refused our key”),那么可能存在一些常见的错误,需要在服务器端检查。

最常见的错误之一是文件和目录的权限设置错误。authorized_keys文件必须归目标用户所有,并且不能是组可写的。密钥文件的目录也不能是组或全体用户可写的。

$ 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"

在服务器端,如果有多个公钥,标注每个密钥来自哪个客户端也非常重要。可以在authorized_keys文件的最后一列添加注释(如果尚未存在注释)。authorized_keys文件的格式可以在sshd(8)手册页中的“AUTHORIZED_KEYS FILE FORMAT”部分找到。如果密钥没有标注,它们可能很难匹配,视具体需求而定。

永久将密钥与服务器关联

密钥可以在运行时指定,但为了避免反复输入相同的路径,可以在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 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 指令决定允许哪些密钥类型用于认证。

使用认证代理的基于密钥的认证

当使用认证代理(如 ssh-agent(1))时,通常应该在会话开始时启动它,并用于启动登录会话或 X 会话,这样指向代理及其 UNIX 域套接字的环境变量会传递到随后的每个 shell 和进程。许多桌面发行版在登录或启动时会自动这样做。

启动代理涉及设置一对环境变量:

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

各种 SSH 和 SFTP 客户端会自动找到这些变量并使用它们联系代理并在需要时进行认证。然而,实际上,主要使用的是 SSH_AUTH_SOCK。如果是通过 ssh-agent(1) 启动 shell 或桌面会话,这些变量已经设置并可用。如果它们不可用,则需要手动设置每个 shell 或每个应用程序中的这些变量,或通过客户端配置文件中的 IdentityAgent 指令指向代理的套接字。

一旦代理可用,就需要加载相关的私钥,才能使用代理进行认证。加载到代理中的私钥可以多次使用。私钥通过 ssh-add(1) 加载到代理中:

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

密钥会在代理运行时一直保留,除非另有指定。可以使用 -t 选项为代理本身设置超时时间,或在实际加载密钥时使用 ssh-add(1) 设置超时。无论哪种方式,-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 则会一次性移除所有身份,而无需指定名称。

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

客户端配置指令 AddKeysToAgent 也可以用于根据需要将密钥加载到代理中。启用后,当第一次调用该密钥时,如果密钥尚未加载,它会自动加载到运行中的代理中。同样,IdentitiesOnly 指令可以确保在第一次尝试时提供相关密钥。无需每次运行客户端时都输入这些指令,可以将它们添加到 ~/.ssh/config 文件中,从而为指定的主机连接自动添加。

代理转发

代理转发是通过一个或多个中间主机的方式之一。然而,-J 选项(用于 ProxyJump)是一个更安全的选择。请参阅 "Passing Through a Gateway or Two" 部分了解更多内容。使用代理转发时,中间机器会在客户端与最终目的地之间转发认证请求和响应。这带来一定风险,但消除了在任何这些中间机器上使用密码或持有密钥的需求。

代理转发的主要优势是私钥本身不需要保存在任何远程机器上,从而避免了不必要的文件系统访问。[3] 另一个优势是,用户已认证的实际代理本身不会离开,从而减少了被分析的可能性。

使用代理的一个风险是,如果权限允许,代理可能会被重用来实现旁路认证。密钥不能以这种方式被复制,但在权限错误的情况下,认证仍然可能成功。需要注意的是,禁用代理转发不会提高安全性,除非用户也被禁止使用 shell 访问,因为他们可以随时安装自己的转发器。

通过在添加密钥到代理时使用 -c 选项,可以缓解代理转发的风险。这要求 SSH_ASKPASS 变量设置并可供代理进程使用,但会在每次远程系统使用密钥时在运行代理的主机上生成提示。因此,如果经过一个或多个中间主机,通常最好改为使用 SSH 客户端的 stdio 转发(通过 -W-J)。

客户端默认情况下禁用代理转发,如果需要使用,则必须显式启用。在 ssh_config(5) 中为特定服务器启用代理转发的配置如下:

Host gateway.example.org
    ForwardAgent yes

在服务器端,默认配置文件允许认证代理转发,因此只需在客户端启用即可,不需要在服务器端进行额外配置。不过,再次提醒,最好还是使用 ProxyJump

旧式的、更安全的 SSH 代理转发

通过一个或多个中间主机传递认证的最佳方式是使用 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 选项可以指向该路径,一旦启动了临时代理,以下示例使用特定代理的预定义套接字,每当连接到两个特定域时:

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) 中的 "TOKENS" 部分。

在这些配置设置下,身份验证代理必须在启动 SSH 客户端之前已经运行并指向指定的套接字。此外,套接字应该放在对其他账户不可访问的目录中。ssh-agent(1) 必须使用 -a 选项来命名套接字:

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

该代理配置可以手动启动,也可以通过脚本或服务管理器启动。然而,考虑到隐私和安全性,通常应避免使用代理转发。配置指令 ProxyJump 是最佳替代方案,在较旧的系统中,通过使用 ProxyCommandnetcat 进行主机穿越是更可取的。请参阅 "代理和跳跃主机" 部分,了解如何使用这些方法。

新样式的 SSH 代理目的地约束

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

  1. 没有限制的转发(不推荐)
  2. 仅限本地使用,这些密钥不会被转发
  3. 转发,但仅限于特定的远程主机
  4. 通过指定的路由转发到特定的远程主机

其目的是确保限制失败时是安全的,因此当路由中的一个或多个主机缺少所需的协议功能时,认证将被禁止。一旦密钥加载到代理中,目的地和路由不能修改,但可以加载多个到同一目的地的路由,且路由可以包含多个跳数。如果需要更改路由,则必须将密钥重新加载到代理中,并指定新的路由。

客户端的默认设置是将密钥仅保留在代理中以供本地使用。然而,可以通过在启动客户端时添加 -a 选项或在 ssh_config(5) 中将 ForwardAgent 指令设置为 "no" 来显式强制这一点。

为了加载密钥进行无限转发(尽管这不是最好的做法),只需像往常一样使用 ssh-add(1) 将它们添加到代理中。然后,在客户端使用 -A 选项或将 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 文件或通过 -H 选项指定的另一个文件来识别这些允许的主机。如果在加载密钥时没有使用 -H 选项,则将使用默认的已知主机文件:~/.ssh/known_hosts/etc/ssh/ssh_known_hosts~/.ssh/known_hosts2/etc/ssh/ssh_known_hosts2

对于密钥,必须谨慎地维护已知主机列表,可能需要借助 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 版本开始,OpenSSH 支持使用由硬件安全令牌(如 OnlyKey、Yubikey 或其他许多硬件令牌)支持的密钥,支持 FIDO2 协议。Universal 2nd Factor(U2F)身份验证通过 FIDO2 协议直接在 OpenSSH 中得到支持,无需第三方软件。目前有两种类型的硬件支持密钥,ECDSA-SK 和 Ed25519-SK,但只有最新的硬件令牌才支持后者。如果令牌的固件不支持 Ed25519-SK 格式,则在尝试使用该密钥类型时会出现以下错误消息:

Key enrollment failed: invalid format

如果支持,可以使用 ssh-keygen(1) 创建这两种密钥。步骤与创建普通密钥几乎相同,但令牌必须首先连接到系统。如果需要,必须输入令牌的 PIN 并触摸或以其他方式激活令牌。之后,密钥创建按常规进行。注意通过 -t 选项指定密钥类型。

如果支持,任何一种密钥类型都可以使用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

生成的私钥文件实际上不是密钥本身,而是一个“密钥句柄”,该句柄由硬件安全令牌使用,在需要时生成真正的私钥。因此,硬件支持的私钥文件在没有附带的硬件令牌时是无用的。这也意味着这些密钥文件不能在不同的硬件令牌之间移植,比如当有多个令牌备用时,即便是同一个账户,也必须为每个令牌生成不同的密钥对。

硬件安全令牌驻留私钥

可以将私钥存储在令牌内部,但目前不能直接从令牌内部使用,必须先将其保存为文件。同时,密钥只能在创建时使用-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

authorized_keys中插入的command="..."指令会覆盖所有其他内容,确保登录时只执行指定的脚本。如果需要将参数传递给脚本,可以查看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

然后,密钥通过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,以便限制访问指定的文件系统部分。例如,以下是ssh -v显示的一个rsync(1)的使用情况,注意“Sending command”行:

$ rsync -e 'ssh -v' fred@server.example.org:/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 AAAAC3NzaC1lZDI1NTE5AAAAIMm0rs4eY8djq

Bb3dIEgbQ8lmdlxb9IAEuX/qFCTxFgb


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

### 只读访问密钥
在某些情况下,需要防止账户更改自己的认证密钥。尽管这种情况可能更适合使用证书,但如果使用密钥,可以通过将密钥文件放在外部目录中,并仅授予用户读取权限来实现。然后,`AuthorizedKeysFile`指令指定`sshd(8)`查找密钥的位置,可以指向一个安全的位置,而不是默认位置。

一个好的替代位置可以是一个新的目录`/etc/ssh/authorized_keys`,其中存储选定账户的密钥文件。可以通过在`Match`指令下设置,使该更改仅适用于一组账户。大多数系统上,密钥的默认位置通常是`~/.ssh/authorized_keys`。

```bash
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指令并不是必须的。可以将设置放在服务器配置文件的主部分中,这样它们会适用于所有账户。

标记公钥为已撤销

密钥可以被撤销。已撤销的密钥可以存储在 /etc/ssh/revoked_keys 文件中,这是通过在 sshd_config(5) 文件中使用 RevokedKeys 指令指定的,这样 sshd(8) 就会防止尝试使用这些密钥进行登录。如果使用已撤销的密钥,客户端不会收到警告或错误信息,身份验证将简单地继续到下一个密钥或方法。

撤销的密钥文件应包含已撤销的公钥列表,每个密钥占一行,这些密钥不能再用于连接到服务器。密钥不能包含额外的内容(如登录选项),否则它会被忽略。如果在登录尝试过程中使用了撤销的密钥,服务器会简单地忽略它并继续使用下一个身份验证方法。日志中会记录此次尝试,包括密钥的指纹。有关日志的更多信息,请参阅相关部分。

RevokedKeys /etc/ssh/revoked_keys

RevokedKeys 配置指令在 sshd_config(5) 中默认没有设置。如果要使用它,必须显式设置。这是一个可能更适合通过使用证书来实现的情况,因为证书可以设置有效期,而密钥是永久有效的。

密钥撤销列表(KRL)

密钥撤销列表(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 中,损坏或破损的密钥不会被加载,如果尝试加载它们会产生错误信息。与常规的撤销密钥列表一样,公钥不能包含任何额外内容,否则在尝试加载到 KRL 或在 KRL 中搜索时会报错。

通过指纹验证主机密钥

上述示例涉及使用密钥对客户端进行身份验证。密钥的另一种用途是当服务器向客户端进行自我识别时,这通常发生在每次非复用会话的开始时。为了进行这种身份验证,客户端首先获取服务器的公钥,通常是在首次连接时或之前,这样客户端可以确保自己连接的是同一台服务器,而不是伪装者。默认情况下,客户端会将获取的主机密钥存储在 /etc/ssh/ssh_known_hosts(如果由系统管理员管理)或 ~/.ssh/known_hosts(如果由客户端自己的账户管理)中。文件的格式是每一行包含一个主机地址及其匹配的公钥。该文件的格式在 sshd(8) 的 "SSH_KNOWN_HOSTS FILE FORMAT" 部分有详细描述。

首次连接远程主机时,应该验证服务器的主机密钥,以确保客户端连接的是正确的机器而不是伪装者。通常通过比较服务器主机密钥的指纹来进行验证,而不是直接比较整个密钥本身。如果主机密钥不在已知主机列表中,客户端默认会显示主机密钥的指纹。

$ 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)?

该指纹可以通过其他方式(如邮寄、电子邮件、短信、快递等)与预先获得的指纹进行比对。上述例子表示密钥的指纹是一个基于 SHA256 的 Base64 编码校验和。这是默认格式。通过将客户端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)?

但新版本中的默认格式是 SHA256 Base64 编码,它的碰撞几率较低。

在 OpenSSH 6.7 及之前版本中,客户端会显示主机密钥的 MD5 十六进制校验和,而不是当前使用的 SHA256 Base64 编码校验和:

$ 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 艺术形式的可视化主机密钥。有关更多信息,请参见下文。

下载密钥

即使主机的密钥通常在首次连接时显示以供审查,它也可以随时按需通过 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+GBLamiiIaWbeGZMqeTXg15

R42yCC0 host.example.org (RSA) 256 SHA256:ZmS+IoHh31CmQZ4NJjv3z58Pfa0zMaOgxu8yAcpuwuw host.example.org (ED25519)


如果服务器在端口上提供了多个公钥类型,则 `ssh-keyscan(1)` 会获取它们。如果通过标准输入或文件提供了多个密钥,`ssh-keygen(1)` 将按顺序处理它们。在 OpenSSH 7.2 之前,手动指纹计算是两步过程,首先将密钥读取到文件中,然后计算其指纹。


手动生成哈希值或指纹

在支持的系统中,可以通过 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) 文件,部分原因是为了保持代码库的简单性。由于有许多配置选项(如 ProxyJump, ProxyCommand, Match, BindInterface, 和 CanonicalizeHostname),这些选项的实现会相当复杂。通过将 ssh-keyscan 封装成一个简短的 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) 进行两步操作。首先,从已知的私钥重新生成公钥,并用其生成指纹输出到标准输出。然后,生成未知公钥的指纹进行比较。在此示例中,私钥 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 校验和,两个指纹会被显示在一起,便于视觉比较。旧版本不支持从标准输入读取,因此那时需要一个中间文件。更旧的版本只会显示每个密钥的 MD5 校验和。无论如何,使用 shell 脚本进行自动化是很简单的,但超出了本书的讨论范围。

验证服务器密钥

当首次连接时,必须可靠地验证服务器的主机密钥。可能需要联系系统管理员,由他们提供该密钥以便提前了解并在第一次连接时进行验证。

服务器 RSA 密钥的指纹

以下是读取服务器 RSA 密钥并显示其 SHA256 Base64 指纹的示例:

$ ssh-keygen -lf /etc/ssh/ssh_host_rsa_key.pub     
3072 SHA256:hlPei3IXhkZmo+GBLamiiIaWbeGZMqeTXg15R42yCC0 root@server.example.net (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 root@server.example.net (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 root@server.example.net (RSA)

使用 ssh-keyscan(1) 获取密钥

也可以使用 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.

出现此警告的常见原因有三种:

  1. 服务器的密钥已被替换,通常是由于服务器操作系统重新安装,且没有备份旧密钥。
  2. 系统管理员废除了过时或受损的密钥。如果可以计划迁移,可以将新密钥添加到服务器,并使用 UpdateHostKeys 选项让客户端接受新密钥。
  3. 连接到了错误的机器,例如当远程系统由于动态地址分配而更改 IP 地址时。

在所有三种情况下,唯一需要做的就是联系系统管理员并验证密钥。询问是否最近重新安装了 OpenSSH 服务器,或者机器是否是从旧的备份恢复的。记住,在某些情况下,系统管理员可能是你自己。

被中间人攻击的风险

比较少见但需要排除的情况是连接的机器是中间人攻击的一部分。

获取可信的密钥指纹

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

$ ssh-keygen -R server.example.org

然后尝试登录,但首先比较密钥指纹,只有当指纹与通过其他方式获得的指纹匹配时,才继续。如果指纹匹配,可以继续登录过程,密钥将自动添加。如果指纹不匹配,立即停止并弄清楚你连接的是哪台机器。建议通过电话(而不是电脑电话)联系远程机器的系统管理员或网络管理员。

一个主机的多个密钥,多个主机使用一个密钥

多个主机名或 IP 地址可以通过模式匹配或简单地列出多个系统来共享相同的密钥。这可以在 /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 没有配置为使相同机器保持相同地址的情况下,或者在使用某些标准输入输出转发方法通过中间主机传递时非常有用。

known_hosts 中更新和轮换主机密钥

OpenSSH 从 6.8 版本开始支持协议扩展,可以从 known_hosts 文件中轮换掉弱公钥。这样,服务器可以通知客户端其所有的主机密钥,并在至少一个已知的受信任密钥存在的情况下更新 known_hosts 文件。这种方法仍然要求私钥可用,以便完成证明。在 ssh_config(5) 中,UpdateHostKeys 指令指定客户端是否应该接受服务器认证完成后提供的其他主机密钥,并将其添加到 known_hosts。服务器可以在删除过时密钥之前,提供同一类型的多个密钥一段时间,从而为密钥轮换提供自动化选项,也可以从较弱的算法升级到较强的算法。有关密钥管理标准的详细信息,请参阅 RFC 4819:Secure Shell 公钥子系统。

最后修改: 2025年01月19日 星期日 21:46