如何在Ubuntu20.04上为MongoDB副本集配置密钥文件身份验证
介绍
MongoDB,也称为 Mongo,是一个开源文档数据库,用于许多现代 Web 应用程序。 它被归类为 NoSQL 数据库,因为它不依赖关系数据库模型。 相反,它使用具有动态模式的类似 JSON 的文档。 这意味着,与 关系数据库 不同,MongoDB 在将数据添加到数据库之前不需要预定义模式。
当您使用多个分布式 MongoDB 实例时,例如在 副本集 或 分片数据库架构 的情况下,确保它们之间的通信安全非常重要。 一种方法是通过 密钥文件身份验证 。 这涉及创建一个特殊文件,该文件基本上用作集群中每个成员的共享密码。
本教程概述了如何更新现有副本集以使用密钥文件身份验证。 本指南中涉及的过程还将确保副本集不会经历任何停机时间,因此副本集中的数据将可供任何需要访问它的客户端或应用程序使用。
先决条件
要完成本教程,您需要:
- 三台服务器,每台运行 Ubuntu 20.04。 所有这三个服务器都应该有一个管理非 root 用户和一个配置了 UFW 的防火墙。 要进行设置,请按照我们的 Ubuntu 20.04 初始服务器设置指南进行操作。
- MongoDB 安装在您的每台 Ubuntu 服务器上。 按照我们关于 如何在 Ubuntu 20.04 上安装 MongoDB 的教程进行操作,确保在每台服务器上完成每个步骤。
- 所有三个 MongoDB 安装都配置为副本集。 按照 How To Configure a MongoDB Replica Set on Ubuntu 20.04 上的教程进行设置。
- 为每个服务器生成的 SSH 密钥。 此外,您应该确保每台服务器都将其他两台服务器的公钥添加到其
authorized_keys
文件中。 这是为了确保每台机器可以通过 SSH 相互通信,这将更容易在步骤 2 中将密钥文件分发给它们中的每台机器。 要设置这些,请按照我们关于 如何在 Ubuntu 20.04 上设置 SSH 密钥的指南进行操作。
请注意,为清楚起见,本指南将遵循先决条件副本集教程中建立的约定,并将三个服务器称为 mongo0、mongo1 和 mongo2[X205X ]。 它还将假定您已完成该指南的 步骤 1 并配置了每个服务器的 hosts
文件,以便以下主机名将解析为给定服务器的 IP 地址:
主机名 | 决议为 |
---|---|
mongo0.replset.member | mongo0 |
mongo1.replset.member | mongo1 |
mongo2.replset.member | mongo2 |
在本指南中的一些实例中,您必须仅在其中一个服务器上运行命令或更新文件。 在这种情况下,本指南将默认在示例中使用 mongo0,并通过在蓝色背景中显示命令或文件更改来表示这一点,如下所示:
任何必须在 multiple 服务器上运行的命令或必须进行的文件更改都将具有标准的灰色背景,如下所示:
关于密钥文件身份验证
在 MongoDB 中,keyfile 身份验证依赖于 Salted Challenge Response Authentication Mechanism (SCRAM),这是数据库系统的默认身份验证机制。 SCRAM 涉及 MongoDB 根据用户名、密码和身份验证数据库的组合读取和验证用户提供的凭据,所有这些都为给定的 MongoDB 实例所知。 这与用于对连接数据库时提供密码的用户进行身份验证的机制相同。
在密钥文件身份验证中,密钥文件充当集群中每个成员的共享密码。 密钥文件必须包含 6 到 1024 个字符。 密钥文件只能包含 base64 集中的字符,请注意 MongoDB 在读取密钥时会去除空白字符。 从 Mongo 4.2 版本开始,密钥文件使用 YAML 格式,允许您在单个密钥文件中共享多个密钥。
警告: MongoDB 的社区版提供了两种身份验证方法,可以帮助确保您的数据库安全, 密钥文件身份验证 和 x.509 身份验证。 对于使用复制的生产部署, MongoDB 文档建议 使用 x.509 身份验证,并将密钥文件描述为“最适合测试或开发环境的最低安全形式”。
获取和配置 x.509 证书的过程伴随着一些必须根据具体情况做出的警告和决定,这意味着此过程超出了 DigitalOcean 教程的范围。 如果您计划在生产环境中使用副本集,我们 强烈建议 您查看 有关 x.509 身份验证的 官方 MongoDB 文档 。
如果您计划使用您的副本集进行测试或开发,您可以继续按照本教程为您的集群添加一层安全性。
第 1 步 — 创建用户管理员
当您在 MongoDB 中启用身份验证时,它还将为副本集启用 基于角色的访问控制 。 根据 MongoDB 文档:
MongoDB 使用基于角色的访问控制 (RBAC) 来管理对 MongoDB 系统的访问。 用户被授予一个或多个角色,这些角色决定了用户对数据库资源和操作的访问权限。
在 MongoDB 实例上启用访问控制后,这意味着您将无法访问系统上的任何资源,除非您已作为有效的 MongoDB 用户进行身份验证。 即使这样,您也必须以具有访问给定资源的适当权限的用户身份进行身份验证。
如果您在启用密钥文件身份验证(因此,访问控制)之前没有为您的 MongoDB 系统创建用户,您将不会被锁定在您的副本集之外。 您可以创建一个 MongoDB 用户,您可以使用该用户对集合进行身份验证,并在必要时通过 Mongo 的 localhost 异常 创建其他用户。 这是 MongoDB 为启用访问控制但缺少用户的配置创建的一个特殊例外。 此异常仅允许您连接到 localhost 上的数据库,然后在 admin
数据库中创建用户。
但是,在启用身份验证后依赖 localhost 异常创建 MongoDB 用户意味着您的副本集将经历一段停机时间,因为在您创建用户之前,副本将无法验证其连接。 此步骤概述了如何在 启用身份验证之前创建用户 ,以确保您的副本集保持可用。 该用户将有权在数据库上创建其他用户,从而使您可以自由地创建具有他们将来需要的任何权限的其他用户。 在 MongoDB 中,具有此类权限的用户称为 用户管理员 。
首先,连接到副本集的 主要成员 。 如果您不确定哪个成员是主要成员,您可以运行 rs.status()
方法来识别它。
在副本集中托管 MongoDB 实例的任何 Ubuntu 服务器的 bash 提示符下运行以下 mongo
命令。 此命令的 --eval
选项指示 mongo
操作不打开您自己运行 mongo
时出现的 shell 界面环境,而是运行命令或方法,包装在单个引号,在 --eval
参数之后:
mongo --eval 'rs.status()'
rs.status()
返回很多信息,但输出的相关部分是 "members" :
数组。 在 MongoDB 的上下文中,数组是保存在一对方括号([
和 ]
)之间的文档集合。
在 "members":
数组中,您会找到许多文档,每个文档都包含有关副本集中一个成员的信息。 在每个成员文档中,找到 "stateStr"
字段。 "stateStr"
值为 "PRIMARY"
的成员是副本集的主要成员。 以下示例显示了 mongo0 为主的情况:
Output. . . "members" : [ { "_id" : 0, "name" : "mongo0.replset.member:27017", "health" : 1, "state" : 1, "stateStr" : "PRIMARY", . . . }, . . .
一旦您知道哪些副本集成员是主要成员,就可以通过 SSH 连接到托管该实例的服务器。 出于演示目的,本指南将继续使用 mongo0 为主的示例:
ssh sammy@mongo0_ip_address
登录服务器后,打开mongo
shell环境连接MongoDB:
mongo
在 MongoDB 中创建用户时,您必须在将用作其 身份验证数据库 的特定数据库中创建它们。 用户名及其身份验证数据库的组合用作该用户的唯一标识符。
某些管理操作仅适用于身份验证数据库为 admin
数据库(每个 MongoDB 安装中包含的特殊特权数据库)的用户,包括创建新用户的能力。 因为此步骤的目标是创建一个可以在副本集中创建其他用户的用户管理员,所以连接到 admin
数据库,以便您可以授予该用户适当的权限:
use admin
Outputswitched to db admin
MongoDB 安装了 一些基于 JavaScript 的 shell 方法 ,您可以使用它们来管理数据库。 其中之一是 db.createUser
方法,用于在运行该方法的数据库中创建新用户。
启动 db.createUser
方法:
db.createUser(
注意:在输入右括号之前,Mongo 不会将 db.createUser
方法注册为完整的。 在您这样做之前,提示将从大于号 (>
) 变为省略号 (...
)。
此方法要求您指定用户的用户名和密码,以及您希望用户拥有的任何角色。 回想一下,MongoDB 将其数据存储在类似 JSON 的文档中; 当您创建一个新用户时,您所做的就是创建一个文档来保存适当的用户数据作为单独的字段。
与 JSON 中的对象一样,MongoDB 中的文档以大括号({
和 }
)开始和结束。 输入一个左大括号以开始用户文档:
{
接下来,输入 user:
字段,将所需的用户名作为双引号中的值,后跟逗号。 以下示例指定用户名 UserAdminSammy,但您可以输入任何您喜欢的用户名:
user: "UserAdminSammy",
接下来,输入一个 pwd
字段,其值为 passwordPrompt()
方法。 当您执行 db.createUser
方法时,passwordPrompt()
方法将提示您输入密码。 这比替代方法更安全,即像输入用户名一样以明文形式输入密码。
注意:passwordPrompt()
方法只兼容MongoDB版本4.2及更新的版本。 如果您使用的是旧版本的 Mongo,则必须以明文形式写出密码,类似于写出用户名的方式:
pwd: "password",
请务必在此字段后面加上逗号:
pwd: passwordPrompt(),
然后输入一个 roles
字段,后跟一个数组,详细说明您希望管理用户拥有的角色。 在 MongoDB 中,roles 定义了用户可以对他们有权访问的资源执行哪些操作。 您可以自己定义自定义角色,但 Mongo 还附带了许多授予常用权限的内置角色。
因为您正在创建用户管理员,所以您至少应该在 admin
数据库上授予他们内置的 userAdminAnyDatabase
角色。 这将允许用户管理员创建和修改新用户和角色。 因为管理用户在 admin
数据库中具有此角色,这也将授予它 对整个集群 的超级用户访问权限:
roles: [ { role: "userAdminAnyDatabase", db: "admin" } ]
之后,输入一个右大括号来表示文档的结束:
}
然后输入右括号关闭并执行db.createUser
方法:
)
总之,你的 db.createUser
方法应该是这样的:
> db.createUser( ... { ... user: "UserAdminSammy", ... pwd: passwordPrompt(), ... roles: [ { role: "userAdminAnyDatabase", db: "admin" } ] ... } ... )
如果每一行的语法正确,该方法将正确执行,并提示您输入密码:
OutputEnter password:
输入您选择的强密码。 然后,您将收到用户已添加的确认信息:
OutputSuccessfully added user: { "user" : "UserAdminSammy", "roles" : [ { "role" : "userAdminAnyDatabase", "db" : "admin" }, "readWriteAnyDatabase" ] }
这样,您就添加了一个 MongoDB 用户配置文件,您可以使用它来管理系统上的其他用户和角色。 您可以通过创建另一个用户来测试这一点,如本步骤的其余部分所述。
首先以您刚刚创建的用户管理员身份进行身份验证:
db.auth( "UserAdminSammy", passwordPrompt() )
如果验证成功,db.auth()
将返回 1
:
Output1
注意:以后,如果你想在连接集群时以用户管理员身份进行身份验证,可以直接在服务器提示符下使用如下命令进行验证:
mongo -u "UserAdminSammy" -p --authenticationDatabase "admin"
在此命令中,-u
选项告诉外壳程序以下参数是您要进行身份验证的用户名。 -p
标志告诉它提示您输入密码,并且 --authenticationDatabase
选项位于用户身份验证数据库的名称之前。 如果您输入了错误的密码或用户名和身份验证数据库不匹配,您将无法进行身份验证,您将不得不再次尝试连接。
另外,请注意,为了让您以用户管理员身份在副本集中创建新用户,您必须连接到该集的主要成员。
添加其他用户的步骤与添加用户管理员的步骤相同。 以下示例创建一个具有 clusterAdmin
角色的新用户,这意味着他们将能够执行许多与复制和分片相关的操作。 在 MongoDB 的上下文中,具有这些权限的用户称为 集群管理员 。
拥有一个专门的用户来执行这样的特定功能是一种很好的安全实践,因为它限制了您在系统上拥有的特权用户的数量。 在本教程后面启用密钥文件身份验证后,任何想要执行 clusterAdmin
角色允许的任何操作的客户端 - 例如任何 rs.
方法,例如 [X198X ] 或 rs.conf()
— 必须首先以集群管理员身份进行身份验证。
也就是说,您可以为该用户提供您想要的任何角色,并同样为他们提供不同的名称和身份验证数据库。 但是,如果您希望新用户充当集群管理员,那么您 必须 授予他们 admin
数据库中的 clusterAdmin
角色。
除了创建用户作为集群管理员之外,以下方法将用户命名为 ClusterAdminSammy 并使用 passwordPrompt()
方法提示您输入密码:
db.createUser( { user: "ClusterAdminSammy", pwd: passwordPrompt(), roles: [ { role: "clusterAdmin", db: "admin" } ] } )
同样,如果您使用的是 4.2 之前的 MongoDB 版本,那么您将不得不以明文形式写出您的密码,而不是使用 passwordPrompt()
方法。
如果每一行的语法正确,该方法将正确执行,并提示您输入密码:
OutputEnter password:
输入您选择的强密码。 然后,您将收到用户已添加的确认信息:
OutputSuccessfully added user: { "user" : "ClusterAdminSammy", "roles" : [ { "role" : "clusterAdmin", "db" : "admin" } ] }
此输出确认您的用户管理员能够创建新用户并授予他们角色。 您现在可以关闭 MongoDB shell:
exit
或者,您可以按 CTRL + C
关闭 shell。
此时,如果您有任何客户端或应用程序连接到您的 MongoDB 集群,那么现在是创建一个或多个具有适当角色的专用用户的好时机,他们可以使用这些用户对数据库进行身份验证。 否则,请继续阅读以了解如何生成密钥文件,将其分发给副本集的成员,然后将每个配置为要求副本集成员使用密钥文件进行身份验证。
第 2 步 — 创建和分发身份验证密钥文件
在创建密钥文件之前,在您将存储密钥文件的每个服务器上创建一个目录以使事情井井有条会很有帮助。 运行以下命令,在您的三台服务器 中的每台服务器上的管理 Ubuntu 用户的主目录 中创建一个名为 mongo-security
的目录:
mkdir ~/mongo-security
然后在您的 一个服务器 上生成一个密钥文件。 您可以在任何一台服务器上执行此操作,但出于说明目的,本指南将在 mongo0 上生成密钥文件。
导航到您刚刚创建的 mongo-security
目录:
cd ~/mongo-security/
在该目录中,使用以下 openssl
命令创建一个密钥文件:
openssl rand -base64 768 > keyfile.txt
记下这个命令的参数:
rand
:指示 OpenSSL 生成伪随机字节数据-base64
:指定命令应该使用base64编码将伪随机数据表示为可打印文本。 这很重要,因为如前所述,MongoDB 密钥文件只能包含 base64 集中的字符768
:命令应该生成的字节数。 在 base64 编码中,三个二进制字节的数据表示为四个字符。 因为 MongoDB 密钥文件最多可以有 1024 个字符,所以 768 是您可以为有效密钥文件生成的最大字节数
此命令的 768
参数后面是一个大于号 (>
)。 这会将命令的输出重定向到一个名为 keyfile.txt
的新文件中,该文件将用作您的密钥文件。 如果您愿意,可以随意将密钥文件命名为 keyfile.txt
以外的名称,但请确保在以后的命令中出现时更改文件名。
接下来,修改密钥文件的权限,以便只有所有者具有读取权限:
chmod 400 keyfile.txt
在此之后,将密钥文件分发到在您的副本集中托管 MongoDB 实例的另外两台服务器。 假设您遵循 如何设置 SSH 密钥 的先决条件指南,您可以使用 scp
命令执行此操作:
scp keyfile.txt sammy@mongo1.replset.member:/home/sammy/mongo-security scp keyfile.txt sammy@mongo2.replset.member:/home/sammy/mongo-security
请注意,这些命令中的每一个都将密钥文件直接复制到您之前在 mongo1 和 mongo2 上创建的 ~/mongo-security/
目录中。 请务必将 sammy
更改为您在每台服务器上创建的管理 Ubuntu 用户配置文件的名称。
接下来,将文件的所有者更改为 mongodb 用户配置文件。 这是安装 MongoDB 时创建的特殊用户,用于运行 mongod
服务。 此用户必须有权访问密钥文件,以便 MongoDB 使用它进行身份验证。
在 每个服务器 上运行以下命令,将密钥文件的所有者更改为 mongodb 用户帐户:
sudo chown mongodb:mongodb ~/mongo-security/keyfile.txt
在每台服务器上更改密钥文件的所有者后,您就可以重新配置每个 MongoDB 实例以强制执行密钥文件身份验证。
第 3 步 — 启用密钥文件身份验证
现在您已经生成了一个密钥文件并将其分发到副本集中的每个服务器,您可以更新每个服务器上的 MongoDB 配置文件以强制执行密钥文件身份验证。
为了避免在将副本集的成员配置为需要身份验证时出现任何停机,此步骤涉及首先重新配置集的辅助成员。 然后,您将指示您的主要成员下台并成为次要成员。 这将导致次要成员举行选举以选择新的主要成员,从而使您的集群可供任何需要访问它的客户端或应用程序使用。 然后,您将重新配置以前的主节点以启用身份验证。
在托管副本集的次要成员的每台服务器上,使用您喜欢的文本编辑器打开 MongoDB 的配置文件:
sudo nano /etc/mongod.conf
在文件中,找到 security
部分。 默认情况下它看起来像这样:
/etc/mongod.conf
. . . #security: . . .
通过删除井号 (#
) 取消注释此行。 然后,在下一行,添加一个 keyFile:
指令,后跟您在上一步中创建的密钥文件的完整路径:
/etc/mongod.conf
. . . security: keyFile: /home/sammy/mongo-security/keyfile.txt . . .
请注意,此新行的开头有两个空格。 这些是正确读取配置文件所必需的。 当您在自己的配置文件中输入此行时,请确保您提供的路径反映了每个服务器上密钥文件的实际路径。
在 keyFile
指令下方,添加一个值为 true
的 transitionToAuth
指令。 当设置为 true
时,此配置选项允许 MongoDB 实例接受经过身份验证和未经过身份验证的连接。 这在重新配置副本集以强制执行身份验证时很有用,因为它将确保您的数据在您重新启动集合的每个成员时保持可用:
/etc/mongod.conf
. . . security: keyFile: /home/sammy/mongo-security/keyfile.txt transitionToAuth: true . . .
同样,请确保在 transitionToAuth
指令之前包含两个空格。
进行这些更改后,保存并关闭文件。 如果您使用 nano
编辑它,您可以按 CTRL + X
、Y
,然后按 ENTER
进行编辑。
然后在 两个辅助实例的服务器 上重新启动 mongod
服务,以立即使这些更改生效:
sudo systemctl restart mongod
这样,您就为副本集的次要成员配置了密钥文件身份验证。 此时,经过身份验证和未经过身份验证的用户都可以不受限制地访问这些成员。
接下来,您将对主要成员重复此过程。 但是,在这样做之前,您必须让该成员下台,使其不再是主要成员。 为此,请在托管主要成员的服务器上打开 MongoDB shell。 出于说明目的,本指南将再次假设这是 mongo0:
mongo
在提示符下,运行 rs.stepDown()
方法。 这将指示主要成员成为次要成员,并将导致当前的次要成员进行选举以确定哪个将充当新的主要成员:
rs.stepDown()
如果该方法在输出中返回 "ok" : 1
,则表示主成员成功降级为辅助成员:
Output{ "ok" : 1, "$clusterTime" : { "clusterTime" : Timestamp(1614795467, 1), "signature" : { "hash" : BinData(0,"AAAAAAAAAAAAAAAAAAAAAAAAAAA="), "keyId" : NumberLong(0) } }, "operationTime" : Timestamp(1614795467, 1) }
降级后,您可以关闭 Mongo shell:
exit
接下来,打开此服务器上的 MongoDB 配置文件:
sudo nano /etc/mongod.conf
找到安全部分并通过删除井号取消注释 security
标头。 然后添加您添加到其他 MongoDB 实例的相同 keyFile
和 transitionToAuth
指令。 进行这些更改后,security
部分将如下所示:
/etc/mongod.conf
. . . security: keyFile: /home/sammy/mongo-security/keyfile.txt transitionToAuth: true . . .
同样,确保 keyFile
指令之后的文件路径反映了密钥文件在此服务器上的实际位置。
完成后,保存并关闭文件。 然后重启mongod
进程:
sudo systemctl restart mongod
之后,您的所有 MongoDB 实例都能够接受经过身份验证和未经身份验证的连接。 在本指南的最后一步中,您将配置您的实例以要求用户在执行特权操作之前进行身份验证。
第 4 步 — 在没有 transitionToAuth
的情况下重新启动每个成员以强制进行身份验证
此时,您的每个 MongoDB 实例都配置了 transitionToAuth
设置为 true
。 这意味着即使您已启用每台服务器使用您创建的密钥文件在内部对连接进行身份验证,它们仍然能够接受未经身份验证的连接。
要更改此设置并要求每个成员强制执行身份验证,请在每个服务器 上重新打开 mongod.conf
文件 :
sudo nano /etc/mongod.conf
找到 security
部分并禁用 transitionToAuth
指令。 您可以通过在该行前面加上一个井号来注释该行来做到这一点:
/etc/mongod.conf
. . . security: keyFile: /home/sammy/mongo-security/keyfile.txt #transitionToAuth: true . . .
在每个实例的配置文件中禁用 transitionToAuth
指令后,保存并关闭每个文件。
然后,在每台服务器上重启mongod
服务:
sudo systemctl restart mongod
之后,副本集中的每个 MongoDB 实例都需要您进行身份验证才能执行特权操作。
要对此进行测试,请尝试运行一个 MongoDB 方法,该方法在由具有适当权限的经过身份验证的用户调用时起作用。 尝试从任何 Ubuntu 服务器的提示符中运行以下命令:
mongo --eval 'rs.status()'
即使您在步骤 1 中成功运行此方法,rs.status()
方法现在只能由已被授予 clusterAdmin
或 clusterManager
角色的用户运行,因为您已经启用密钥文件身份验证。 无论您是在托管主要成员还是次要成员之一的服务器上运行此命令,它都不会工作,因为您尚未进行身份验证:
Output. . . MongoDB server version: 4.4.4 { "operationTime" : Timestamp(1616184183, 1), "ok" : 0, "errmsg" : "command replSetGetStatus requires authentication", "code" : 13, "codeName" : "Unauthorized", "$clusterTime" : { "clusterTime" : Timestamp(1616184183, 1), "signature" : { "hash" : BinData(0,"huJUmB/lrrxpx9YfnONM4mayJwo="), "keyId" : NumberLong("6941116945081040899") } } }
回想一下,在启用访问控制之后,所有集群管理方法(包括 rs.
方法,如 rs.status()
)只有在被授予适当集群管理角色的经过身份验证的用户调用时才有效。 如果您已创建集群管理员(如步骤 1 中所述)并以该用户身份进行身份验证,则此方法将按预期工作:
mongo -u "ClusterAdminSammy" -p --authenticationDatabase "admin" --eval 'rs.status()'
在提示时输入用户密码后,您将看到 rs.status()
方法的输出:
Output. . . MongoDB server version: 4.4.4 { "set" : "shard2", "date" : ISODate("2021-03-19T20:21:45.528Z"), "myState" : 2, "term" : NumberLong(4), "syncSourceHost" : "mongo1.replset.member:27017", "syncSourceId" : 2, "heartbeatIntervalMillis" : NumberLong(2000), "majorityVoteCount" : 2, . . .
这确认副本集正在执行身份验证,并且您能够成功进行身份验证。
结论
通过完成本教程,您使用 OpenSSL 创建了一个密钥文件,然后配置了一个 MongoDB 副本集以要求其成员将其用于内部身份验证。 您还创建了一个用户管理员,它将允许您在将来管理用户和角色。 在所有这一切中,您的副本集不会经历任何停机时间,您的数据将仍然可供您的客户端和应用程序使用。
如果您想了解有关 MongoDB 的更多信息,我们鼓励您查看我们的 整个 MongoDB 内容库 。