基于主机的认证

基于主机的认证允许主机代表该主机上的所有或部分用户进行认证。这些账户可以是系统上的所有账户,也可以是通过 Match 指令指定的子集。这种认证方式对于管理计算集群和其他相对同质化的机器池非常有用。

总的来说,服务器上必须准备三个文件用于基于主机的认证。客户端只需修改两个文件,但主机本身必须分配 SSH 主机密钥。接下来的步骤设置了一个系统到另一个系统的单向基于主机的认证。对于双向认证,请按照相同的步骤进行,但交换系统角色。

客户端配置基于主机的认证

客户端或源主机上,必须配置两个文件,此外至少需要存在一个主机密钥:

  • /etc/ssh/ssh_known_hosts - 用于已知主机的公钥的全局文件
  • /etc/ssh/ssh_config - 允许客户端请求基于主机的认证

然后,如果客户端的主机密钥还不存在,必须创建它们:

  • /etc/ssh/ssh_host_ecdsa_key
  • /etc/ssh/ssh_host_ed25519_key
  • /etc/ssh/ssh_host_rsa_key

这三步需要在每个将连接到指定主机的客户端系统上完成。设置完成后,登录到一个系统的账户将能够连接到另一个系统,而无需进一步的交互式认证。

需要注意的是,在某些环境中,基于主机的认证可能不足以防止未经授权的访问,因为它通常是基于主机进行的。

1. 将服务器的公钥添加到客户端

远程服务器的公钥必须存储在客户端系统的全局配置文件 /etc/ssh/ssh_known_hosts 中。一种获取服务器公钥的方式是使用 ssh-keyscan(1) 获取并保存它们:

$ ssh-keyscan server.example.com | tee -a /etc/ssh/ssh_known_hosts

在尝试任何操作之前,如果 /etc/ssh/ssh_known_hosts 文件存在,请确保先备份它。另一种方法是将服务器的公钥添加到相关账户的 ~/.ssh/known_hosts 文件中,但这种方式会更麻烦。

无论通过何种方式获取和验证,列在文件中的公钥显然必须与服务器上的私有主机密钥相对应。可以使用 RSA、ECDSA 或 Ed25519 三种类型中的任意一种,DSA 不再使用。验证公钥时,可以参考如何通过指纹验证主机密钥部分的方法。

2. 客户端全局配置

必须进行两个更改。首先,客户端配置必须请求基于主机的认证,以便连接到指定的系统。其次,客户端配置文件必须设置为启用 ssh-keysign(8)。它是一个帮助程序,用于访问本地主机密钥并生成基于主机的认证所需的数字签名。两个更改都可以在 /etc/ssh/ssh_config 文件中全局完成。

以下是客户端 ssh_config 配置的一个示例,它尝试对所有机器进行基于主机的认证。在 ssh_config(5) 中可以使用 Host 指令来进一步限制对特定服务器或服务器组的访问。

Host *.pool.example.org
	HostbasedAuthentication yes
	EnableSSHKeysign yes

在某些发行版中,例如 Red Hat Enterprise Linux 8,EnableSSHKeysign 指令可能需要放置在通用部分中才能生效。在这种情况下,做如下修改:

Host *.pool.example.org
	HostbasedAuthentication yes

Host *
	EnableSSHKeysign yes

可以根据需要添加其他配置指令。例如,以下是对同一组池应用的两个附加设置:

Host *.pool.example.org
	HostbasedAuthentication yes
	EnableSSHKeysign yes
	ServerAliveCountMax 3
	ServerAliveInterval 60

如果客户端主机的主目录是与其他机器共享的,例如通过 NFS 或 AFS,则可能需要考虑 NoHostAuthenticationForLocalhost 指令。

顺便提一下,ssh-keysign(8) 程序本身必须是 SUID root。但是,通常在安装时已经设置了 SUID,因此无需再次修改。

3. 设置客户端系统的主机密钥

程序 ssh-keysign(8) 需要读取客户端系统的私有主机密钥,这些密钥位于 /etc/ssh/ 目录下。安装 OpenSSH 服务器时会创建这些密钥,但如果客户端系统上没有安装 OpenSSH 服务器,也不需要安装。只要存在这些密钥即可。如果客户端系统的私有主机密钥不存在,则必须使用 ssh-keygen(1) 手动添加它们,才能启用基于主机的认证。

$ ssh-keygen -A

默认路径为 /etc/ssh/,当使用 -A 选项时。

服务器端配置基于主机的认证

服务器或目标主机上必须修改三个文件,以启用和允许基于主机的认证:

  • /etc/shosts.equiv - 与旧的 rhosts.equiv 文件使用相同的语法
  • /etc/ssh/ssh_known_hosts - 存储来自客户端的主机公钥
  • /etc/ssh/sshd_config - 启用基于主机的认证

shosts.equiv 文件的位置可能会因操作系统和发行版的不同而有所不同。

1. 在服务器上注册允许的客户端系统

shosts.equiv 文件必须包含一个允许尝试基于主机认证的客户端系统列表。该文件可以包含主机名、IP 地址或网络组。最好将此文件保持简单,仅列出主机名或 IP 地址。无论如何,它只是提供了初步的过滤。要进行更精细的配置,请使用 sshd_config(5) 来设置或撤销特定用户和组的访问权限。

需要注意的是,如果使用 shosts.equiv 文件,则客户端和服务器上的用户名必须匹配。例如,要连接到 bob@server1.example.org客户端上的用户名也必须是 bob。如果需要让用户 alice 连接到 bob@server1.example.org,则必须在 bob.shosts 文件中指定,而不是在全局的 shosts.equiv 文件中。

client1.example.org
192.0.2.102
client8.example.org -bull
@statcluster

在 OpenBSD 中,该文件位于 /etc/ 目录下,这是本书参考系统的位置。在其他系统中,该文件可能位于 /etc/ssh/ 目录下。无论如何,shosts.equiv 文件会识别哪些地址被允许尝试认证。要确保正确的位置,请查看实际系统上的 sshd(1) 手册页。

有关 .shosts.equiv(以及 .shosts)的更多细节,可以参考手册页 hosts.equiv(5),但需要注意的是,它的使用已经不推荐,许多系统可能根本没有该手册页。

旧时遗留文件

两个遗留文件可能存在于非常旧的系统中,如果它们在 shosts.equiv 中被引用,可以选择性地修改它们。/etc/netgroup/etc/netgroup.db 文件分别包含网络组的默认列表和网络组的数据库。

  • /etc/netgroup - 默认的网络组列表
  • /etc/netgroup.db - 从 netgroup 构建的网络组数据库

然而,这些文件大多数是旧版 rhosts 的遗留物,应避免使用。

另一个遗留做法是在每个账户的主目录中使用 .shosts 文件。这相当于 shosts.equiv 的本地版本。这样,每个用户可以有一个本地的 .shosts 文件,包含允许尝试基于主机认证的受信任远程主机或用户-主机对。

.shosts 文件的权限不得对任何组或其他用户可写。权限设置为 0644 即可。.shosts 文件的使用和格式与 .rhosts 相同,但允许基于主机的认证,而不允许使用不安全的旧工具 rloginrsh 进行登录。每一行包含一个主机,第一列是必须的,包含允许尝试基于主机认证的主机名或地址。

然而,使用全局的 .shosts.equiv 文件比在每个主目录中使用 .shosts 文件更为可取。

2. 将客户端的公钥添加到服务器

在服务器的 shosts.equiv 文件中列出的客户端系统,必须将它们的公钥添加到服务器的 /etc/ssh/ssh_known_hosts 文件中,以便被认可。每行需要三个数据字段:第一个是主机名或 IP 地址,或它们的逗号分隔列表,必须与 shosts.equiv 中的内容相匹配;第二个是密钥类型,可以是 ssh-rsa(RSA 密钥)、ssh-ed25519(Ed25519 密钥)或 ecdsa-sha2-nistp256(ECDSA 密钥);第三个字段是公钥本身。最后,可以选择性地添加关于密钥的注释。

desktop,192.0.2.102 ssh-rsa AAAAB3NzaC1yc2EAAAABIw ... qqU24CcgzmM=

客户端的步骤类似,获取客户端的公钥并将其传输到服务器的方法有很多种。可以使用 sftp(1) 进行复制,从 ~/.ssh/known_hosts 中复制,或者使用 ssh-keyscan(1) 获取。如果客户端系统同时运行着 SSH 服务器,则后两种方法有效。

$ ssh-keyscan -t rsa client.example.org | tee -a /etc/ssh/ssh_known_hosts

3. 服务器的全局配置

服务器上必须修改的第三个文件是 sshd_config(5)。必须通过设置 HostbasedAuthentication 指令来启用基于主机的认证,可以针对所有用户、某些用户或特定组进行设置。

HostbasedAuthentication yes

基于主机的认证可以限制为特定用户或组。以下是一个例子,展示如何允许 'cluster2' 组中的任何用户,让主机代表他们进行认证:

Match Group cluster2
        HostbasedAuthentication yes

可以使用 HostbasedAcceptedAlgorithms 指令来允许特定类型的主机密钥,之前该指令被称为 HostbasedAcceptedKeyTypes,可以列出接受的密钥算法的逗号分隔列表。所有要允许的密钥算法必须列出,没有列出的密钥算法将不被允许。可以在白名单中使用模式。以下示例允许 Ed25519 和 ECDSA 密钥,但不允许 RSA 和 DSA 密钥。

HostbasedAuthentication yes
HostbasedAcceptedAlgorithms ssh-ed25519*,ecdsa-sha2*

主机名和其他 DNS 问题

如果可能,最好为客户端配置完整的 DNS 条目,包括允许反向查找。如果客户端机器没有列出在 DNS 中,服务器可能无法识别它。在这种情况下,可能需要告诉 sshd(8) 不要为连接主机执行 DNS 反向查找。这在一些没有注册主机名的局域网中可能会遇到问题。以下是 /etc/ssh/sshd_config 中的一个示例,用于解决客户端缺少 DNS 记录的情况,使用 HostbasedUsesNameFromPacketOnly 指令:

HostbasedAuthentication yes
HostbasedUsesNameFromPacketOnly yes

除非常规方法失败,否则不要添加此指令。否则,它可能会干扰并阻止认证。

主机身份识别问题

有时,尝试连接的主机在目标主机上被识别为与预期不同的名称,这也会阻止认证。因此,请确保配置文件与主机实际使用的名称匹配。

调试

配置应该相当简单,服务器上只需要修改三个文件,客户端修改两个文件。如果遇到困难,请准备好以调试级别 1(-d)到 3(-ddd)运行 sshd(8),并以调试级别 3(-vvv)运行 ssh(1),多运行几次,看看你错过了什么。错误需要按正确的顺序清理,所以一次解决一个问题。

如果服务器显示消息 "debug3: auth_rhosts2_raw: no hosts access file exists",则可能是 shosts.equiv 文件的位置错误或缺失,并且在该账户中没有备用的 ~/.shosts 文件。

如果服务器在 known_hosts 中找到了客户端的公钥,但仍无法找到密钥,并且客户端的主机名不在常规 DNS 中,那么可能需要添加指令 HostbasedUsesNameFromPacketOnly。该指令使用客户端提供的名称,而不是执行 DNS 查找。

以下是一个成功的基于主机的认证的示例,用户 fred 从 IP 为 192.0.2.102(也称为 desktop1)的主机进行连接,使用的是 Ed25519 密钥。服务器首先尝试查找 ECDSA 密钥,但未找到。

# /usr/sbin/sshd -ddd
debug2: load_server_config: filename /etc/ssh/sshd_config
...
debug3: /etc/ssh/sshd_config:111 setting HostbasedAuthentication yes
debug3: /etc/ssh/sshd_config:112 setting HostbasedUsesNameFromPacketOnly yes
...
debug1: sshd version OpenSSH_6.8, LibreSSL 2.1
...
debug1: userauth-request for user fred service ssh-connection method hostbased [preauth]
debug1: attempt 1 failures 0 [preauth]
debug2: input_userauth_request: try method hostbased [preauth]
debug1: userauth_hostbased: cuser fred chost desktop1. pkalg ecdsa-sha2-nistp256 slen 100 [preauth]
...
debug3: mm_answer_keyallowed: key_from_blob: 0x76eede00
debug2: hostbased_key_allowed: chost desktop1. resolvedname 192.0.2.102 ipaddr 192.0.2.102
debug2: stripping trailing dot from chost desktop1.
debug2: auth_rhosts2: clientuser fred hostname desktop1 ipaddr desktop1
debug1: temporarily_use_uid: 1000/1000 (e=0/0)
debug1: restore_uid: 0/0
debug1: fd 4 clearing O_NONBLOCK
debug2: hostbased_key_allowed: access allowed by auth_rhosts2
debug3: hostkeys_foreach: reading file "/etc/ssh/ssh_known_hosts"
debug3: record_hostkey: found key type ED25519 in file /etc/ssh/ssh_known_hosts:1
debug3: load_hostkeys: loaded 1 keys from desktop1
debug1: temporarily_use_uid: 1000/1000 (e=0/0)
debug3: hostkeys_foreach: reading file "/home/fred/.ssh/known_hosts"
debug1: restore_uid: 0/0
debug1: check_key_in_hostfiles: key for host desktop1 not found
Failed hostbased for fred from 192.0.2.102 port 10827 ssh2: ECDSA SHA256:CEXGTmrVgeY1qEiwFe2Yy3XqrWdjm98jKmX0LK5mlQg, client user "fred", client host "desktop1"
debug3: mm_answer_keyallowed: key 0x76eede00 is not allowed
debug3: mm_request_send entering: type 23
debug2: userauth_hostbased: authenticated 0 [preauth]
debug3: userauth_finish: failure partial=0 next methods="publickey,password,keyboard-interactive,hostbased" [preauth]
debug1: userauth-request for user fred service ssh-connection method hostbased [preauth]
debug1: attempt 2 failures 1 [preauth]
debug2: input_userauth_request: try method hostbased [preauth]
debug1: userauth_hostbased: cuser fred chost desktop1. pkalg ssh-ed25519 slen 83 [preauth]
debug3: mm_key_allowed entering [preauth]
debug3: mm_request_send entering: type 22 [preauth]
debug3: mm_key_allowed: waiting for MONITOR_ANS_KEYALLOWED [preauth]
debug3: mm_request_receive_expect entering: type 23 [preauth]
debug3: mm_request_receive entering [preauth]
debug3: mm_request_receive entering
debug3: monitor_read: checking request 22
debug3: mm_answer_keyallowed entering
debug3: mm_answer_keyallowed: key_from_blob: 0x7e499180
debug2: hostbased_key_allowed: chost desktop1. resolvedname 192.0.2.102 ipaddr 192.0.2.102
debug2: stripping trailing dot from chost desktop1.
debug2: auth_rhosts2: clientuser fred hostname desktop1 ipaddr desktop1
debug1: temporarily_use_uid: 1000/1000 (e=0/0)
debug1: restore_uid: 0/0
debug1: fd 4 clearing O_NONBLOCK
debug2: hostbased_key_allowed: access allowed by auth_rhosts2
debug3: hostkeys_foreach: reading file "/etc/ssh/ssh_known_hosts"
debug3: record_hostkey: found key type ED25519 in file /etc/ssh/ssh_known_hosts:1
debug3: load_hostkeys: loaded 1 keys from desktop1
debug1: temporarily_use_uid: 1000/1000 (e=0/0)
debug3: hostkeys_foreach: reading file "/home/fred/.ssh/known_hosts"
debug1: restore_uid: 0/0
debug1: check_key_in_hostfiles: key for desktop1 found at /etc/ssh/ssh_known_hosts:1
Accepted ED25519 public key SHA256:BDBRg/JZ36+PKYSQTJDsWNW9rAfmUQCgWcY7desk/+Q from fred@desktop1
debug3: mm_answer_keyallowed: key 0x7e499180 is allowed
debug3: mm_request_send entering: type 23
debug3: mm_key_verify entering [preauth]
debug3: mm_request_send entering: type 24 [preauth]
debug3: mm_key_verify: waiting for MONITOR_ANS_KEYVERIFY [preauth]
debug3: mm_request_receive_expect entering: type 25 [preauth]
debug3: mm_request_receive entering [preauth]
debug3: mm_request_receive entering
debug3: monitor_read: checking request 24
debug3: mm_answer_keyverify: key 0x7e49a700 signature verified
debug3: mm_request_send entering: type 25
Accepted hostbased for fred from 192.0.2.102 port 10827 ssh2: ED25519 SHA256:BDBRg/JZ36+PKYSQTJDsWNW9rAfmUQCgWcY7desk/+Q, client user "fred", client host "desktop1"
debug1: monitor_child_preauth: fred has been authenticated by privileged process
...

请注意任何警告或错误消息,并仔细阅读它们。如果输出过多,记得使用 -e 选项将调试输出保存到一个单独的文件中,然后再阅读该文件。

由于此方法依赖于客户端和服务器,因此有时运行客户端时增加详细程度也有帮助。

$ ssh -v fred@server.example.org
$ ssh -vv fred@server.example.org
$ ssh -vvv fred@server.example.org

如果客户端的输出过多无法处理,记得使用 -E 选项,将调试日志重定向到一个文件,然后在方便的时候阅读该文件。

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