安全地为多台主机共享管理员权限
问题:您有一个庞大的管理员团队,人员流动率很高。可能是承包商来来往往。可能由于地理位置、管理员级别甚至国籍(如某些美国政府合同),您有不同级别的访问权限限制。您需要为这些人提供对数十个(甚至数百个)主机的管理访问权限,但您无法管理所有主机上的所有帐户。
我在工作的大型企业中遇到了这个问题,我们的团队制定了一个解决方案,该方案:
-
不需要在团队成员入职或离职时更新多个主机上的帐户。
-
不需要删除或更换安全外壳 (SSH) 密钥。
-
不需要管理个人 SSH 密钥。
-
不需要分布式 sudoers 或其他特权访问管理工具(某些基于 Linux 的设备可能不支持这些工具)。
-
最重要的是,不需要共享密码或密钥密码短语。
它适用于任何理解 SSH 密钥信任关系的 UNIX 或 Linux 平台。我个人已经在六种不同的 Linux 发行版以及 Solaris、HP-UX、Mac OS X 和一些 BSD 变体上使用了它。
在我们的案例中,要管理的主机是几十个基于 Linux 的专用设备,这些设备不支持中央帐户管理工具或 sudo。它们旨在(在完全使用 shell 时)作为 root 帐户使用。
我们的环境(由于政府合同)还需要两层访问方案。团队中的美国公民可以 root 身份访问任何主机。非美国公民只能访问主机的一个子集。本文中描述的技术可以扩展到 N 层,而不会有任何实际问题,但我在本文中描述了 N == 2 的情况。
场景我假设您,读者,知道如何设置 SSH 信任关系,以便一台主机上的帐户可以直接登录到另一台主机上的帐户,而无需密码提示。(基本上,您只需创建密钥对并将公钥复制到远程主机的 ~/.ssh/authorized_keys 文件中。)如果您不知道如何执行此操作,请立即停止阅读并去学习。在 Web 上搜索“ssh trust setup”将产生数千个链接——或者,如果您是老派人士,ssh(1) 手册页的 AUTHENTICATION 部分也可以。另请参阅 ssh-copy-id(1),它可以大大简化密钥文件的分发。
Steve Friedl 的网站上有一个关于这些基础知识的优秀技术提示,以及一些关于 SSH 代理转发的材料,这是一个集中管理个人用户 SSH 身份验证的巧妙技巧。技术提示可在 http://www.unixwiz.net/techtips/ssh-agent-forwarding.html 获取。
我在下面描述了密钥缓存,因为它不是很常用,并且是本文描述的技术的核心。
为了说明,我将为参与者(分配角色的个人)、访问层级和“虚拟”帐户分配名称。
主机
-
darter — 中央管理主机的主机名,所有最终用户和实用程序帐户都在其上处于活动状态,所有密钥都存储在此处并进行缓存;此外,控制对实用程序帐户访问权限的 sudoers 文件也在此处。
-
n1、n2、... — 目标主机的主机名,将为所有团队成员授予访问权限(“n”代表“非特殊”)。
-
s1、s2、... — 目标主机的主机名,将仅为某些团队成员授予访问权限(“s”代表“特殊”)。
帐户(仅在 darter 上)
-
univ — 实用程序帐户的名称,其中保存所有目标主机(u1、u2、...)将信任的 SSH 密钥。
-
spec — 实用程序帐户的名称,其中保存仅特殊、受限访问主机(s1、s2、...)将信任的 SSH 密钥。
-
joe — 假设管理整个方案的人的名字是“Joe”,他的帐户是“joe”。Joe 是一位受信任的管理员,拥有“王国之钥”——他不能是受限用户。
-
andy、amy — 这些是允许登录所有主机的用户。
-
alice
-
ned、nora — 这些是仅允许登录“n”(非特殊)主机的用户;他们永远不应该被允许登录特殊主机 s1、s2、...
-
nancy
您将需要在 darter 上创建共享的、非特权的实用程序帐户,供不受限制和受限制的管理员使用。按照我们的约定,这些帐户将分别称为“univ”和“rstr”。实际上,不应有人直接登录 univ 和 rstr,事实上,这些帐户不应拥有自己的密码或受信任的密钥。所有对共享实用程序帐户的登录都应使用 su(1) 从 darter 上的现有个人帐户执行。
设置Joe 的第一个操作是登录到 darter 并“成为”univ 帐户
$ sudo su - univ
然后,在该共享实用程序帐户下,Joe 创建一个 .ssh 目录和一个 SSH 密钥对。此密钥将受到每个目标主机上 root 帐户的信任(因为它是“univ”-ersal 密钥)
$ mkdir .ssh # if not already present
$ ssh-keygen -t rsa -b 2048 -C "universal access
↪key gen YYYYMMDD" -f
.ssh/univ_key
Enter passphrase (empty for no passphrase):
非常重要:Joe 为此密钥分配了一个强密码短语。此密钥的密码短语不会被普遍共享。
(-C
之后的字段只是一个注释;此格式反映了我的个人偏好,但您当然可以自由开发自己的格式。)
这将在 .ssh 中生成两个文件:univ_key(私钥文件)和 univ_key.pub(公钥文件)。私钥文件已加密,并受到 Joe 上面为其分配的非常强的密码短语的保护。
Joe 退出 univ 帐户并登录 rstr。他执行相同的步骤,但创建名为 rstr_key 而不是 univ_key 的密钥对。他为私钥文件分配了一个强密码短语——它可以与分配给 univ 的密码短语相同,事实上,从简单性的角度来看,这可能是更可取的。
Joe 将 univ_key.pub 和 rstr_key.pub 复制到公共位置以方便使用。
对于每个授予所有人访问权限的主机(n1、n2、...),Joe 使用目标主机的 root 凭据将 univ_key.pub 和 rstr_key.pub(在单独的行上)复制到 root 帐户目录下的 .ssh/authorized_keys 文件中。
对于每个仅授予少数人访问权限的主机(s1、s2、...),Joe 使用目标主机的 root 凭据仅将 rstr_key.pub(在单行上)复制到 root 帐户目录下的 .ssh/authorized_keys 文件中。
因此,回顾一下,现在,当用户使用 su
“成为” univ 帐户时,他或她可以登录到任何主机,因为 univ_key.pub 存在于 n1、n2、... 和 s1、s2、... 的 authorized_keys 文件中。
但是,当用户使用 su
“成为” rstr 帐户时,他或她只能登录到 n1、n2、...,因为这些主机的 authorized_keys 文件包含 rstr_key.pub,但不包含 univ_key.pub。
当然,为了解锁这两种情况下的访问权限,用户将需要 Joe 创建密钥时使用的强密码短语。这似乎破坏了整个方案的目的,但有一个技巧可以绕过它。
技巧首先,让我们谈谈密钥缓存。任何使用受密码短语保护的密钥文件的 SSH 密钥用户都可以使用名为 ssh-agent 的程序缓存这些密钥。ssh-agent 在调用时不会直接接受密钥。它作为独立程序调用,没有任何参数(至少,此处对我们无用的参数)。
ssh-agent 的输出是一对环境变量/值对,外加一个 echo 命令,适合输入到 shell。如果您“直接”调用它,这些变量将不会成为环境的一部分。因此,ssh-agent 始终作为 shell 内置命令 eval
的参数调用
$ eval $(ssh-agent)
Agent pid 29013
(eval
的输出还包括一个 echo 语句,用于向您显示您刚刚创建的代理实例的 PID。)
一旦您运行了代理,并且您的 shell 知道如何与其通信(感谢环境变量),您可以使用命令 ssh-add
将密钥缓存到其中。如果您为 ssh-add
提供密钥文件,它将提示您输入密码短语。一旦您提供正确的密码短语,ssh-agent 将在内存中保存未加密的密钥。任何 SSH 调用都将在尝试身份验证之前与 ssh-agent 检查。如果内存中的密钥与远程主机上的公钥匹配,则建立信任,并且登录直接发生,无需输入密码或密码短语。
(顺便说一句:对于那些使用 Windows 终端程序 PuTTY 的人来说,该工具提供了一个名为 Pageant 的密钥缓存程序,它执行的功能大致相同。PuTTY 的等效于 ssh-keygen 的实用程序称为 PuTTYgen。)
您现在需要做的就是进行设置,以便 univ 和 rstr 帐户在每次登录时都进行自我设置,以利用 ssh-agent 的持久实例。通常,用户在登录时手动调用 ssh-agent,在该会话期间使用它,然后在退出之前使用 eval $(ssh-agent -k)
杀死它。与其手动管理它,不如在每个实用程序帐户的 .bash_profile 中编写一些代码来执行以下操作
-
首先,检查当前帐户是否有一个当前的 ssh-agent 实例。
-
如果没有,则调用 ssh-agent 并将环境变量捕获到 /tmp 中的一个特殊文件中。(它应该在 /tmp 中,因为 /tmp 的内容在系统重启之间被清除,这对于管理缓存的密钥非常重要。)
-
如果有,则在 /tmp 中找到保存环境变量的文件,并将其源到 shell 的环境中。(此外,处理代理正在运行但找不到 /tmp 文件的错误情况,方法是杀死 ssh-agent 并从头开始。)
以上所有内容都假设密钥已解锁并缓存。(我稍后会回到这个问题。)
以下是 univ 帐户的 .bash_profile 中的代码的样子
/usr/bin/pgrep -u univ 'ssh-agent' >/dev/null
RESULT=$?
if [[ $RESULT -eq 0 ]] # ssh-agent is running
then
if [[ -f /tmp/.env_ssh.univ ]] # bring env in to session
then
source /tmp/.env_ssh.univ
else # error condition
echo 'WARNING: univ ssh agent running, no environment
↪file found'
echo ' ssh-agent being killed and restarted ... '
/usr/bin/pkill -u univ 'ssh-agent' >/dev/null
RESULT=1 # due to kill, execute startup code below
fi
if [[ $RESULT -ne 0 ]] # ssh-agent not running, start
↪it from scratch
then
echo "WARNING: ssh-agent being started now;
↪ask Joe to cache key"
/usr/bin/ssh-agent > /tmp/.env_ssh.univ
/bin/chmod 600 /tmp/.env_ssh.univ
source /tmp/.env_ssh.univ
fi
当然,rstr 帐户的代码是相同的,只是将所有地方的 s/univ/rstr/ 替换为 s/rstr/univ/。
每当 darter(所有用户帐户和密钥所在的中央管理主机)重新启动时,Joe 都必须一次介入。Joe 将必须登录并成为 univ 并执行命令
$ ssh-add ~/.ssh/univ_key
然后输入密码短语。然后 Joe 登录到 rstr 帐户并针对 ~/.ssh/rstr_key 执行相同的命令。命令 ssh-add -l
按指纹和文件名列出缓存的密钥,因此如果对密钥是否已缓存有疑问,这就是找出答案的方法。如果需要,单个代理可以缓存多个密钥,但这在我的环境中并不常用。
一旦密钥被缓存,它们将保持缓存状态。(ssh-add -t <N>
可用于指定 N 秒的超时时间,但您不会希望将该选项用于此共享访问方案。)每次 darter 重新启动时都必须为每个帐户重建缓存,但由于 darter 是 Linux 主机,因此这种情况很少发生。在重新启动之间,ssh-agent 的单个实例(每个实用程序帐户一个)只是运行并将密钥保存在内存中。我上次输入我们的实用程序帐户密钥的密码短语是在 500 多天前——而且我可能会在几百天后才需要再次这样做。
最后一步是设置 sudoers 以管理对实用程序帐户的访问。您真的不必这样做。如果您愿意,您可以为 univ 和 rstr 设置(不同的)密码,然后让用户持有它们。当然,共享密码一开始就不是一个好主意。(这是整个方案的主要要点之一!)每次 univ 帐户的某个用户离开团队时,您都必须更改该密码,并将新密码(希望以安全且带外的方式)分发给所有剩余用户。
不,使用 sudoers 管理访问权限是一个更好的主意。本文不是要教您 sudoers 的极其怪异的非理性挫败感 (EBNF) 语法的来龙去脉——或任何方面。我只想给您作弊码。
回想一下,Andy、Amy、Alice 等都被允许访问所有主机。这些用户被允许使用 sudo 执行 su - univ
命令。Ned、Nora、Nancy 等仅被允许访问受限主机列表。他们只能使用 su - rstr
命令登录到 rstr 帐户。这些用户的 sudoers 条目可能如下所示
User_Alias UNIV_USERS=andy,amy,alice,arthur # trusted
User_Alias RSTR_USERS=ned,nora,nancy,nyarlathotep # not so much
# Note that there is no harm in putting andy, amy, etc. into
# RSTR_USERS as well. But it also accomplishes nothing.
Cmnd_Alias BECOME_UNIV = /bin/su - univ
Cmnd_Alias BECOME_RSTR = /bin/su - rstr
UNIV_USERS ALL= BECOME_UNIV
RSTR_USERS ALL= BECOME_RSTR
让我们回顾一下。每个主机 n1、n2、n3 等都在 authorized_keys 中同时拥有 univ 和 rstr 密钥文件。
每个主机 s1、s2、s3 等在 authorized_keys 中仅拥有 univ 密钥文件。
当 darter 重新启动时,Joe 登录到 univ 和 rstr 帐户,并使用私钥文件作为参数执行 ssh-add 命令。当提示时,他输入这些密钥的密码短语。
现在 Andy(例如)可以登录到 darter,执行
$ sudo su - univ
并使用他的密码进行身份验证。他现在可以 root 身份登录到 n1、n2、...、s1、s2、... 中的任何一个,而无需进一步身份验证。如果 Andy 需要检查例如 ntp 在 20 台主机上的功能,他可以执行一个循环
$ for H in n1 n2 n3 [...] n10 s1 s2 s3 [...] s10
> do
> ssh -q root@$H 'ntpdate -q timeserver.domain.tld'
> done
它将运行,无需进一步干预。
同样,nancy 可以登录到 darter,执行
$ sudo su - rstr
并登录到 n1、n2 等中的任何一个,执行类似的循环,等等。
优点和风险假设 Nora 离开团队。您只需编辑 sudoers 以将她从 RSTR_USERS 中删除,然后锁定或删除她的系统帐户。
“但是 Nora 因行为不端而被解雇了!如果她保留了密钥对的副本怎么办?”
这个方案的妙处在于,访问这两个密钥文件无关紧要。拥有公钥文件并不重要——如果您愿意,可以将公钥文件放在 Internet 上。它是公开的!
拥有私钥文件的加密副本也无关紧要。如果没有密码短语(只有 Joe 知道),该文件可能就像 /dev/urandom 的输出一样。Nora 从未访问过原始密钥文件——只有缓存代理访问过。
即使 Nora 保留了密钥文件的副本,她也无法使用它们进行访问。删除她对 darter 的访问权限即删除了她对每个目标主机的访问权限。
当然,UNIV_USERS 中的用户也是如此。
对此有两个注意事项,请确保您充分理解它们。
第一个注意事项:几乎不用说,任何具有 darter root 访问权限的人显然都可以随时成为 root 用户,然后 su - univ
。如果您授予某人对 darter 的 root 访问权限,您也就授予了该人对所有目标主机的完全访问权限。毕竟,这就是说目标主机“信任”darter 的含义。此外,即使不知道密钥密码短语的具有 root 访问权限的用户仍然可以使用一些适度复杂的黑魔法从内存中恢复原始密钥。(Linux 内存架构和代理的巧妙设计阻止非特权用户恢复他们自己的代理的内存内容以提取密钥。)
第二个注意事项:显然,任何持有密码短语的人都可以制作(并保留)私钥的未加密副本。在我们的示例中,只有 Joe 拥有该密码短语,但在实践中,您将需要两到三位受信任的管理员知道该密码短语,以便他们可以在 darter 重新启动后介入重新缓存密钥。
如果任何具有对您的中央管理主机(在本例中为 darter)的 root 访问权限的人或任何持有私钥密码短语的人离开团队,您将必须生成新的密钥对,并替换企业中每个目标主机上的 authorized_keys 的内容。(幸运的是,如果您小心谨慎,可以使用旧的信任关系来创建新的信任关系。)
因此,您将希望仅将密码短语委托给您团队中职位至少相当稳定的人员。本文中描述的技术可能不适用于没有稳定“核心”管理员的高流动率环境。
关于这一点还有一件事:您无需管理分层或任何类型的共享访问权限即可使此基本技巧有用。正如我在上面提到的,使用 SSH 密钥缓存代理的常用方法是在会话开始时调用它,缓存您的密钥,然后在结束会话之前杀死它。但是,通过将上面的代码包含在您自己的 .bash_profile 中,您可以在 /tmp 中创建自己的文件,检查它,如果存在则加载它,依此类推。这样,主机始终只有一个 ssh-agent 实例在运行,并且您的密钥将永久缓存在其中(或者至少直到下次重新启动)。
即使您不想那么持久地缓存您的密钥,您仍然可以使用单个 ssh-agent 并使用前面提到的超时 (-t) 选项缓存您的密钥;您仍然可以节省一个步骤。
请注意,但是,如果您这样做,则该主机上的任何 root 用户都将有权访问您在该计算机上信任您的帐户的任何帐户——所以caveat actor。(我仅在仅由我管理的个人计算机上使用此技巧。)
个人使用的技巧正在变得过时,因为 Mac OS X(通过 SSHKeyChain)和较新版本的 GNOME(通过 Keyring)在您第一次使用密钥身份验证设置 SSH 连接到主机时会自动知道,然后询问您的密码短语并将密钥缓存到您 GUI 登录会话的剩余时间内。考虑到默认超时和关于 root 用户访问未锁定密钥的警告的缺乏,我不确定这是否是纯粹的技术进步。(可以在两个实用程序中配置超时,但这需要用户了解该选项,并努力配置它。)
致谢我衷心感谢 David Scheidt 和 James Richmond 在本文准备过程中提供的技术审查和有益建议。