第十一章:密钥分发

管理SSH最烦人的部分是分发和验证密钥。

无论你给用户带来多么可怕的教训,他们中的许多人都不会费心将服务器指纹与你提供的列表进行比较;相反,他们会点击“是的,接受密钥”。无论我们多么努力地教育他们,用户都会很快习惯于看起来可怕的警告,并学会忽略它们。帮助用户注意的最好方法是确保他们看不到警告,除非确实有问题。

同样,基于密钥的身份验证通常比基于密码的身份验证更安全。然而,许多用户不会费心将他们的授权密钥复制到服务器上。他们只会使用熟悉的密码。如果你想强制执行基于密钥的身份验证,你需要在自己的服务器上获取用户的authorized_keys。如果你管理数十或数百台服务器和/或用户,你将需要自动化在系统之间分发用户密钥更新。

虽然OpenSSH不包括自动密钥分发工具,但了解与密钥相关的功能可以大大简化你的自动化过程。我们将从主机密钥开始,然后讨论用户密钥。

known_hosts详细信息

OpenSSH和PuTTY的主机密钥分发都已known_hosts开头。如果你要分发主机公钥,你需要确保这些记录是原始的(pristine)。这意味着你需要完全理解known_hosts文件。

known_hosts中的每一行代表一个主机的一个公钥,已空格分隔。如果一个主机支持三种不同的公钥算法,并且你已使用所有三个密钥连接到此主机,则该主机在known_hosts中将有三个条目。每个条目还提供服务器的主机名或IP地址以及用于密钥的算法。但每个条目也可以包含其他几个字段。

标记

known_hosts文件支持特殊标记(markers)、@cert-authority和@revoked。这些标记必须排在第一行。

以@cert-authority开头的known_hosts条目表示主机密钥用于SSH证书办法机构。SSH证书颁发机构与TLS CA不同。第十四章将讨论SSH CA。

如果入侵者闯入SSH服务器并复制服务器私钥,则该密钥将不再可信。精明的入侵者可能会使用该密钥试图欺骗服务器。通过在known_hosts中用@revoked标记密钥,你可以告诉ssh不要接受此密钥并生成可怕的警告。

请注意,没有“无论如何接受此密钥”选项。被撤销的密钥完全不可信。将密钥保留在known_hosts中,但将其标记为已撤销,可以向用户发出明确的警告,表明他们遇到了一个受损的系统。

标记必须位于行首,主机名之前。

主机名

主机名是SSH识别SSH服务器的方式。如果你使用短主机名连接到服务器,ssh会记录它用来联系服务器的完整主机名。这意味着,如果我键入ssh wrath,ssh会将主机名记录为wrath.mwi.io,因为这是我的系统解析器为ssh提供的名称。该机器可能有其他主机名或别名,也可能通过其IP地址知道。一个真正权威的known_hosts文件必须包含每个名称的密钥。

好消息是,你不必为这些不同的名称包含多个几乎重复的行,known_hosts文件在一个条目中接受多个主机名,只要它们用逗号隔开即可:

一些系统管理员更改了他们的SSH服务器运行的TCP端口。这对安全性来说不是很有用,但有助于减缓更原始的蠕虫并减少日志聊天。这些主机名出现在known_hosts的括号中,后面是冒号和端口号:

第五章介绍了通过散列来模糊主机名,防止偶然的入侵者从known_hosts中提取服务器信息。在一行中列出多个主机名简化了known_hosts的几种管理,但与哈希主机名冲突。如果要对主机名进行哈希运算,则必须在单独的行中列出每个主机名。一个名为avarice.mwi.io、mail.mwi.il和192.51.100.12的主机需要三个known_hosts条目,每个条目都将被单独散列。如果你要在分发known_hosts条目之前对其进行哈希处理,我建议你以明文形式维护你的主文件。

理论上,接受多个IP地址连接的主机需要为每个地址都有一个known_hosts条目。如果你通常无法连接到所有这些地址,那么就不用费心了。我有一个有几十个IP地址的服务器,但我只通过SSH连接到其中一个地址,所以服务器只有一个known_hosts IP条目。如果你有这样的服务器,将sshd锁定为只监听一个地址可能会简化管理。

密钥类型

密钥类型是用于生成此主机密钥的算法。现代known_hosts可以包含六种不同的密钥类型:

空间中出现的任何其他东西都很奇怪,需要调查。

密钥

公钥是一个长的乱字母数字字符串。它通常以一系列大写字母A开头,通常(但并非总是)以等号(=)结尾。密钥填充了行的大部分。

解释

解释(comment)是自由格式的文本。你可以根据需要使用解释。在自动维护的known_hosts文件中,它通常是空白的,但你会发现它在集中管理中很有用。

创建 known_hosts

生成known_hosts的简单方法是使用ssh-keyscan命令:

这为你提供了一个known_hosts文件作为开始。现在,你需要根据第四章中生成的指纹验证这些密钥。

我鼓励你自动收集known_hosts条目。你如何做到这一点完全取决于你所在组织的首选工具。理想情况下,你应该在机器首次部署时运行ssh-keyscan,在任何入侵者有机会将其丢弃之前,并立即更新你的known_hosts。

如果你想简化known_hosts,你可以减少SSH服务器提供的密钥数量。你可以声明网络中的所有主机只提供ED25519密钥,从而消除所有其他密钥类型的所有known_hosts条目。sshd_config的HostKeyAlgorithms关键字允许你设置sshd用于主机标识的算法:

你将使用的确切方法完全取决于你熟悉的工具和你已经到位的自动化。如果可能,重新调整现有工具的用途。

如果你编写了一个好的工具来收集、验证和构建known_hosts文件,请将其公开。

撤销主机密钥

如果你有理由怀疑服务器的密钥已被泄露,请撤销(revoke)它。在known_hosts中查找服务器的所有主机密钥条目。在它们前面添加字符串@revoked。为服务器生成新的主机密钥并重新启动sshd,然后将新主机密钥添加到known_hosts中。现在,你可以将更新的known_hosts分发给你的客户端,在(不太可能的)用户尝试使用吊销的密钥的情况下,用户将收到警告。

撤销密钥的有效性完全取决于想客户端分发known_hosts。

分发主机密钥

每当添加、移动或更改SSH服务器的主机密钥时,用户都会看到有关主机密钥的警告。分发known_hosts的全部目的是让用户看不到不必要的警告。保持领先于你的用户。在部署或删除服务器时,或者必须为服务器提供新密钥时,随时更新你的known_hosts,如果你延迟更新known_hosts,用户将学会忽略警告。

维护集中式known_hosts文件最糟糕的部分式将文件复制到所有服务器和工作站。像Ansible、Puppet等集中式系统可以帮助你管理。Active Directory可以很好地将主机密钥分发到Windows系统。如果你从未使用过自动化,推荐Ansible。一旦你对现有系统有了完整的了解,更新该文件并将其推送到所有系统只需要一两分钟,这将为你的用户和支持团队节省数小时的劳动力。

分发known_hosts

所有OpenSSH客户端都会检查/etc/ssh/ssh_known_hosts中的主机密钥。将你的known_hosts复制到每个服务器和工作站上的此位置。下次有人在这些机器上使用ssh时,正确的密钥已经到位。

除了系统的/etc/ssh/ssh_known_hosts之外,OpenSSH还会检查每个用户的个人known_hosts文件中的主机密钥。客户端将使用与服务器提供的密钥匹配的任何条目。当你首次部署集中式known_hosts时,每个用户可能都有一个现有的个人known_hosts。你不希望用户个人缓存中的任何过时或无效条目干扰以后的密钥更改或撤销。不要只是删除每个人的known_hosts;它们可能包含你无法控制的服务器的已验证主机密钥。相反,在第一次部署时,将每个用户的个人known_hosts移动到known_hosts.personal这样的设置。

一定要告诉你的用户发生了什么。最好提前告知。

一旦你有了一个维护known_hosts的系统,你就会发现SSH中自动化的其他用途。请记住,/etc/ssh/ssh_config为ssh设置了系统范围的默认值。如果你有需要特殊设置的组织标准,你可以在全局配置中输入它们,为用户节省编辑自己的配置或记住命令行参数的工作。如果你的组织在非标准端口上运行SSH,在/etc/SSH/SSH_config中设置Port关键字实际上可能会给你的用户带来好运。个人配置文件会覆盖系统范围的内置,因此如果用户真的有意这么做,条目仍然可以自食其果。

分发 PuTTY 主机密钥

PuTTY将其主机密钥保存到Windows注册表中。复制密钥并不像将文件移动到所有工作站那么容易,但可以简化。PuTTY团队有一个Python脚本,可以将known_hosts转换为PuTTY的注册表项hk2reg.py。在正常的PuTTY安装中找不到hk2reg.py,但它包含在源代码中。你可以从PuTTY网站下载PuTTY源代码。

运行hk2reg.py并给它一个参数,你的原始known_hosts:

通过Active Directory、登录脚本或让用户双击在客户端上安装此注册表文件。

请记住,PuTTY将密钥存储在每个用户的注册表中。没有系统范围的PuTTY注册表树。密钥分发是按用户的,不是按机器的。

如果你正在为各种平台维护known_hosts,我建议你采用以下分发主机密钥的工作流程:从收集主机密钥开始、为你的OpenSSH客户端创建一个known_hosts文件、触发脚本将新的known_hosts自动分发到每个OpenSSH系统、运行时使用kh2reg.py创建Windows注册表、最后将新的注册表文件排队以便通过ActiveDirectory分发。下次人们登录时,他们应该拥有所有新密钥。

DNS中的主机密钥

OpenSSH支持在域名系统(Domain Name System——DNS)中检查主机密钥指纹。(PuTTY没有)。这消除了将文件推送到服务器的可能性,但传统的DNS服务并不安全。如果你想通过DNS安全地分发服务器公钥指纹,你绝对必须拥有DNS安全扩展(DNS Security Extensions——DNSSEC)。如果你还没有DNSSEC,请立即进行配置,然后返回此处。参阅 《DNSSEC精通》(DNSSEC Mastery)。

本书不会介绍DNS的基础知识。如果你正在考虑通过DNS分发密钥指纹,你需要知道什么是区域文件,为什么RR很重要,为什么要更新序列号。

SSHFP 记录

SSH指纹(SSHFP——fingerprint)记录提供了主机的SSH指纹。该记录看起来像这样:

与任何标准DNS记录一样,第一个字段给出主机名,第二个字段表示这是一条互联网记录,第三个字段则表示这是SSH指纹记录。第四个字段表示SSH特定的详细信息,该字段给出了算法类型。你不必记住哪个数字映射到哪个算法,但这里的1表示这是一个RSA指纹。第五个字段是用于生成此指纹的消息摘要算法。1表示SHA-1,2表示SHA-256。最后,第六个字段是实际的密钥指纹。

服务器提供的每个公钥都需要两个SSHFP记录:一个用于SHA-1、一个用于SHA256。

创建SSHFP 记录

不要试图手动创建SSHFP记录。ssh-keygen程序可以使用-r标志读取本地服务器上的密钥文件并生成记录。将主机名作为参数:

将这些记录加载到你的DNS服务器。

你还可以将服务器的公钥文件复制到中央主机,并告诉ssh-keygen使用待有-f标志的文件:

你必须为每个密钥文件单独运行此命令,但如果你有一个中央自动化服务器,这种方法有很多值得推荐的地方。记住,公钥会显示给任何可以连接到服务器SSH端口的人。将公钥文件复制到安全服务器通常不会带来安全风险。

一些免费的DNS提供商,如Hurricane Electric,支持SSHFP记录。

配置客户端

OpenSSH客户端默认情况下可能会使用SSHFP记录,具体取决于操作系统分发者编译它的方式。使用VerifyHostKeyDNS关键字明确定义ssh应该做什么。如果设置为yes,客户端完全信任SSHFP记录提供的密钥。如果设置为ask,ssh将显示指纹并询问用户该做什么。

这将处理主机密钥。现在我们来谈谈用户身份验证密钥。

分发authorized_keys

一个只有少数服务器的系统管理员可以很容易地维护自己的authorized_keys文件。最多需要七八台服务器,在任何地方复制authorized_keys都会变得相当乏味。拥有一个完整的系统管理团队,并希望在数百台服务器上禁止密码身份验证?你真的必须考虑自动化authorized_keys复制的方法。你可以让自动化系统在所有系统上复制身份验证密钥,也可以让sshd在每次登录尝试时向网络查询用户的authorized_keys。两者都有自己的位置。

复制密钥文件

让用户维护自己的密钥文件可能会导致操作问题。用户有一种不可思议的能力来破坏他们的文件,尤其是当他们认为自己知道自己在做什么的时候。

通过使用集中式系统部署authorized_keys,你有机会在用户遇到麻烦之前执行一些基本的完整性测试。你不需要复杂的密钥文件解析和验证系统,但可以说”你意识到你的密钥条目中间有换行符吗?“这样可以减少所有相关人员的烦恼。此外,如果用户的工作站被黑客入侵,入侵者将其引导到服务器访问中,入侵者可以将自己的密钥添加到用户的authorized_keys中,并将其复制到known_hosts中的所有服务器。集中密钥管理并消除用户在不通过自动化系统的情况下上传新密钥文件的能力可能是可取的。

你真的不希望你的自动化系统在每个用户的主目录中乱跑。相反,请利用AuthorizedKeysFile sshd_config关键字。这使你可以将用户的authorized_keys文件放在任何你想要的地方。将其与%u令牌结合使用,使root拥有所有用户密钥:

请记住,%u令牌代表用户名。使用此AuthorizedKeysFile设置,用户mwl的身份验证密钥将位于/etc/ssh/mwl中,而用户djm的密钥将位于/etc/ssh/djm中。用户主目录外的密钥文件看起来与任何其他authorized_keys完全相同,但它们必须由root拥有。即使我们假设入侵者入侵了一个账户,他们也无法在没有特权升级的情况下编辑密钥。

使用操作系统支持的任何功能来保护这些文件。在UFS文件系统上,不可变标志可能适合你的环境。或NFSv4 ACL。如果拒绝让你更改文件让你感到恼火,请考虑将其用于保护授权密钥文件。

在网络中查询密钥

如果你由LDAP等集中式身份验证系统,则可以在该系统中存储用户身份验证密钥。OpenSSH可以使用AuthorizedKeysCommand和AuthorizedKeysCommandUser关键字查询该信息源。

每当你看到基于网络的身份验证时,人们的大脑就会跳到LDAP。LDAP专门用于这种目录查找——它几乎是一个针对读取进行了优化的数据库。

本书不会详细介绍LDAP,因为LDAP目录在供应商之间差异很大。但无论使用哪种方式,你都需要将SSH密钥模式加载到你的目录中。与LDAP管理员交谈,看看他们能提供什么。所需的确切模式因目录排列而异,但通常涉及将sshPublicKeys条目附加到用户的账户。基于商业LDAP产品构建的大型企业的LDAP管理员通常不愿意扩展核心目录条目,因为这限制了他们获得供应商支持的能力。根据我的经验,解决这个问题比密钥分发的任何其他部分都需要付出更多的努力。

加载模块后,你需要一个脚本从目录中获取authorized_keys。脚本的类型与人们使用的身份验证系统的类型完全不同。针对Active Directory进行身份验证的脚本将与针对自制OpenLDAP目录进行身份验证完全不同。CentOS附带了一个脚本,用于对其LDAP服务器ssh-ldap-helper进行身份验证。人们已经为各种目录服务解决了这个问题,并提供了他们的脚本,所以在接下来的十年里调整自己的脚本之前,一定要寻找现有的解决方案。

AuthorizedKeysCommandUser关键字定义了将在AuthorizedKeysCommand中运行脚本的账户。如果不设置AuthrizedKeysCommandUser,sshd将不会运行该脚本。所有获取用户授权密钥的尝试都将失败。我建议创建一个除了运行此脚本之外没有特权的用户。孤立的无特权用户是一种廉价得离谱得安全解决方案,使用频率不够高。

仅仅因为LDAP得到了所有得关注,不要因为认为LDAP是一种要求而限制自己。如果你有它,这很方便,是的,但你可以使用任何对你的环境有意义的服务。如果你的组织有一条规则,要求所有应用程序都必须通过ODBC或HTTPS上的Wordpress XMLRPC进行互操作,请利用你现有的专业知识,编写一个以这种方式获取密钥的脚本。AuthorizedKeysCommand是一个脚本。你是系统管理员,这是你的东西。

无论你是在谈论用户身份验证密钥还是主机公钥,自动化和密钥分发都是至关重要的。现在你可以让自动化管理SSH了,让我们看看SSH如何管理自动化。