简介
MongoDB,也被称为_Mongo_,是一个开源的文档数据库,用于许多现代网络应用。它被归类为NoSQL数据库,因为它不依赖于关系型数据库模型。相反,它使用类似JSON的文档,并具有动态模式。这意味着,与关系型数据库不同,MongoDB在向数据库添加数据之前不需要预定义模式。
当你使用多个分布式MongoDB实例时,比如在副本集或分片数据库架构的情况下,确保它们之间的通信安全是很重要的。做到这一点的一个方法是通过 密钥文件认证.这涉及到创建一个特殊的文件,该文件本质上是集群中每个成员的共享密码。
本教程概述了如何更新现有副本集以使用密钥文件验证。本指南涉及的程序还将确保副本集不会经历任何停机,因此副本集内的数据对任何需要访问它的客户或应用程序来说仍然可用。
前提条件
要完成本教程,你将需要。
- 三台服务器,分别运行Ubuntu 20.04。这三台服务器都应该有一个管理用的非root用户和一个配置了UFW的防火墙。要设置这些,请遵循我们的Ubuntu 20.04的初始服务器设置指南。
- 在你的每台Ubuntu服务器上安装MongoDB。按照我们的教程:如何在Ubuntu 20.04上安装MongoDB,确保在你的每台服务器上完成每一步。
- 你的三个MongoDB安装被配置为一个副本集。按照这个教程:如何在Ubuntu 20.04上配置MongoDB复制集来进行设置。
- 为每个服务器生成SSH密钥。此外,你应该确保每台服务器都有其他两台服务器的公钥添加到其
authorized_keys
文件中。这是为了确保每台机器可以通过SSH相互通信,这将使在步骤2中向每台机器分发密钥文件变得更加容易。要设置这些,请遵循我们的指南:如何在Ubuntu 20.04上设置SSH密钥。
请注意,为清晰起见,本指南将遵循先决条件副本集教程中的惯例,将三个服务器称为mongo0、mongo1和mongo2。它还假定你已经完成了该指南的第1步,并配置了每个服务器的hosts
文件,以便将以下主机名解析为特定服务器的IP地址。
主机名 | 解析为 |
---|---|
mongo0.replset.member | mongo0 |
mongo1.replset.member | mongo1 |
mongo2.replset.member | mongo2 |
在本指南中,有一些情况下,你必须在这些服务器中的一个上运行一个命令或更新一个文件。在这种情况下,本指南将默认在示例中使用mongo0,并将通过在蓝色背景中显示命令或文件变化来表示,就像这样。
任何必须在多个服务器上运行的命令或进行的文件修改都会有一个标准的灰色背景,像这样。
关于密钥文件认证
在MongoDB中,密钥文件认证依赖于盐化挑战响应认证机制(SCRAM),这是数据库系统的默认认证机制。SCRAM涉及MongoDB读取和验证用户提交的凭证与他们的用户名、密码和认证数据库的组合,所有这些都是由给定的MongoDB实例所知道。这与连接到数据库时提供密码的用户的认证机制相同。
在密钥文件认证中,密钥文件作为集群中每个成员的共享密码。一个密钥文件必须包含6到1024个字符。密钥文件只能包含base64集的字符,并注意MongoDB在读取密钥时要剥离空白字符。从Mongo的4.2版本开始,keyfiles使用YAML格式,允许你在一个keyfile中共享多个key。
警告。MongoDB的社区版本带有两种认证方法,可以帮助保持你的数据库安全,即_密钥文件认证_和_x.509认证_。对于采用复制的生产部署,MongoDB文档建议使用x.509认证,它将密钥文件描述为 “最低限度的安全形式”,”最适合于测试或开发环境”。
获得和配置x.509证书的过程有许多注意事项,必须根据具体情况作出决定,这意味着这个过程超出了DigitalOcean教程的范围。如果你打算在生产环境中使用副本集,我们强烈建议你查阅MongoDB官方关于x.509认证的文档。
如果你打算将你的副本集用于测试或开发,你可以按照本教程继续进行,为你的集群添加一层安全。
第1步 – 创建一个用户管理员
当你在MongoDB中启用认证时,它也将为副本集启用_基于角色的访问控制_。根据MongoDB的文档。
MongoDB使用基于角色的访问控制(RBAC)来管理对MongoDB系统的访问。一个用户被授予一个或多个角色,这些角色决定了该用户对数据库资源和操作的访问。
当访问控制在MongoDB实例上启用时,这意味着除非你作为一个有效的MongoDB用户进行认证,否则你将无法访问系统中的任何资源。即使如此,你也必须以具有适当权限的用户身份进行认证,以访问特定的资源。
如果你在启用密钥文件认证(以及由此产生的访问控制)之前没有为你的MongoDB系统创建一个用户,你将不会被锁定在副本集之外。你可以创建一个MongoDB用户,你可以用它来验证该组,如果有必要,可以通过Mongo的 localhost例外.这是MongoDB为启用了访问控制但缺乏用户的配置所做的一个特殊例外。这个例外只允许你连接到本地主机上的数据库,然后在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()
方法只与MongoDB4.2和更新的版本兼容。如果你使用的是旧版本的Mongo,那么你将不得不用明文输入密码,就像你输入用户名那样。
pwd: "password",
复制代码
请确保在这个字段后面也有一个逗号。
pwd: passwordPrompt(),
复制代码
然后输入一个roles
字段,后面是一个数组,详细说明你希望你的管理用户拥有的角色。在MongoDB中,_角色_定义了用户可以在他们可以访问的资源上执行哪些操作。你可以自己定义自定义的角色,但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
选项告诉shell,下面的参数是你想认证的用户名。-p
标志告诉它提示你输入一个密码,--authenticationDatabase
选项在用户认证数据库的名称之前。如果你输入了一个错误的密码,或者用户名和认证数据库不匹配,你将无法进行认证,你将不得不再次尝试连接。
另外,请注意,为了让你作为用户管理员在复制集中创建新的用户,你必须连接到复制集的主要成员。
添加另一个用户的过程与用户管理员的过程相同。下面的例子创建了一个具有clusterAdmin
角色的新用户,这意味着他们将能够执行一些与复制和分片有关的操作。在MongoDB的范围内,拥有这些权限的用户被称为_集群管理员_。
有一个专门的用户来执行这样的特定功能是一个很好的安全实践,因为它限制了你在系统上拥有的特权用户的数量。在本教程后面的部分启用密钥文件认证后,任何想要执行clusterAdmin
角色所允许的任何操作的客户端–例如任何rs.
方法,如rs.status()
或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
指令的下面,添加一个transitionToAuth
指令,其值为true
。当设置为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的外壳。为了说明问题,本指南将再次假设这是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
复制代码
找到安全部分,去掉pound符号,取消对security
header的注释。然后添加你在其他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内容库。