[译文] 使用 MongoDB 字段加密功能

Jean da Silva 2021-08-18
1132

我们今天的主要话题之一肯定是关于安全性。在日常生活中,这可能会被忽视,但迟早,我们必须实施或制定一些安全准则。而今天,我们将讨论其中之一,即字段加密。

讨论功能本身,它只是4.2+版本中的新功能。而MongoDB提供了两种字段加密方式,分别是:

  • 自动客户端字段级加密
  • 显式(手动)客户端字段级加密
  • 在自动模式仅适用于企业版和Atlas,W往往微不足道手动通过支持基于社区版方法MongoDB的驱动程序和蒙戈外壳为好。

    本文将使用 Percona Server for MongoDB ( PSMDB ) 运行4.4版并启用身份验证和本文中的手动方法。由于我们在这里的目的是演示该功能,因此我们将使用mongo-shell来运行所有操作。

    但是,对于通过驱动程序加密的应用程序是最推荐的方法。官方文档中有详细的字段级加密支持驱动列表:

    https://docs.mongodb.com/manual/core/security-client-side-encryption/#field-level-encryption-drivers

    1 –为了开始使用该功能和说明目的,我们将使用 本地托管密钥文件 作为我们的密钥管理服务 (KMS)。

    重要的是要提到本地密钥文件速度快,但安全性较低,因此不建议在生产环境中使用,因为它与数据库一起存储。对于生产,请考虑使用以下服务之一:

    作为密钥管理服务 (KMS),MongoDB 还支持:

  • 亚马逊网络服务 KMS
  • Azure 密钥保管库
  • 谷歌云平台 KMS
  • 使用本地 Managed Keyfile ,MongoDB 需要指定 一个带有base64 编码的 96 字节字符串且没有换行符的文件[1];可以使用以下示例创建:

    shell#> openssl rand -hex 50 | head -c 96 | base64 | tr -d '\n' > /localkeys/client.key shell#> chmod 600 /localkeys/client.key shell#> chown mongod:mongod /localkeys/client.key

    请确保将密钥文件保存在安全位置,以免丢失。否则,您以后将无法解密或阅读它

    2 –一旦我们有了密钥文件,让我们在不连接的情况下打开一个与数据库的 mongo shell 会话。使用选项–nodb;还可以使用–shell来执行提供的代码(在这种情况下是 –eval 字符串值),最后不会自动退出;这一步是必要的,因为我们必须将密钥加载到一个对象中,该对象将成为数据库连接属性。在以下示例中,我们将密钥文件加载到数据库变量LOCAL_KEY 中:

    JavaScript shell#> mongo --shell --nodb --eval "var LOCAL_KEY = cat('/localkeys/client.key')" Percona Server for MongoDB shell version v4.4.3-5 type "help" for help mongo> LOCAL_KEY ODEyMTY2YmNmNDA4YWZlZWVhNTFmOTUyODk4YTJjODc1ODk0NTZiN2EzYWQwZDdjNmM4MDQ5ODUzYzRkMjlhNGZlM2UyZDVmMTNjZWQ1YjAyNjAwNzZmMmQ1ZjVkMzdi shell#> mongo --shell --nodb --eval "var LOCAL_KEY = cat('/localkeys/client.key')" Percona Server for MongoDB shell version v4.4.3-5 type "help" for help mongo> LOCAL_KEY ODEyMTY2YmNmNDA4YWZlZWVhNTFmOTUyODk4YTJjODc1ODk0NTZiN2EzYWQwZDdjNmM4MDQ5ODUzYzRkMjlhNGZlM2UyZDVmMTNjZWQ1YjAyNjAwNzZmMmQ1ZjVkMzdi

    3 –接下来,使用正确的客户端字段级加密配置加载文档ClientSideFieldLevelEncryptionOptions

    mongo> var ClientSideFieldLevelEncryptionOptions = { "keyVaultNamespace" : "encryption.__dataKeys", "kmsProviders" : { "local" : { "key" : BinData(0, LOCAL_KEY)

    4 –设置变量后;我们可以建立使用上述变量通过启用FLE连接ClientSideFieldLevelEncryptionOptions上Mongo() 构造函数。正如您在 mongodb://…/ URI 字符串中看到的那样,此连接也使用普通的用户名+密码身份验证。

    JavaScript mongo> csfleDatabaseConnection = Mongo("mongodb://dba:secret@localhost:27017/?authSource=admin", ClientSideFieldLevelEncryptionOptions) connection to localhost:27017 mongo> csfleDatabaseConnection = Mongo("mongodb://dba:secret@localhost:27017/?authSource=admin", ClientSideFieldLevelEncryptionOptions) connection to localhost:27017

    5 –下一步将是创建内部密钥库对象本身;直到这一刻,我们正在设置变量并调整我们的客户端连接以继续。它可以如下制作:

    mongo> keyVault = csfleDatabaseConnection.getKeyVault(); "mongo" : connection to localhost:27017, "keyColl" : encryption.__dataKeys

    5.1 – 额外说明,因为上述命令不会产生任何输出;使用不同的会话,我们可以检查服务器上新创建的保险库结构:

    > show dbs admin 0.000GB config 0.000GB encryption 0.000GB > use encryption switched to db encryption > show collections __dataKeys

    6 –部署所有集合和保管库结构后,让我们将数据加密密钥添加到数据库连接的密钥保管库。如果成功,createKey()返回新数据加密密钥的UUID。这个UUID是一个BSON 二进制文件,它是我们的加密密钥,将用于手动加密字段:

    mongo> keyVault.createKey( "local", /*Local-type key*/ "", /*Customer master key, used with external KMSes*/ [ "myFirstCSFLEDataKey" ] UUID("5bd46d64-3fe8-4e31-a800-219eaa1b6a85")

    7 –接下来,让我们插入一个使用上述密钥加密的文档;请注意,我们在同一个 mongo-shell 中进行了上述操作,为了加密,我们将使用encrypt()方法和参数来隐藏SSN:“123-45-6789” 。

    **7.1** –要在字段上使用encrypt()函数,它需要以下 3 个参数: 
    
  • encryptionKeyId – 这是我们生成的密钥。
  • ——在本例中,我们将向SSN隐藏“123-45-6789”值。
  • 加密算法 – 我们将使用什么算法来加密字段,我们可以在确定性加密[2] 或随机加密[3]之间进行选择
    7.2 –设置好后,我们可以插入加密字段的文档如下:
  • mongo> clientEncryption = csfleDatabaseConnection.getClientEncryption(); mongo> var csfleDB = csfleDatabaseConnection.getDB("percona"); mongo> csfleDB.getCollection("newcollection").insert({ "_id": 1, "medRecNum": 1, "firstName": "Jose", "lastName": "Pereira", "ssn": clientEncryption.encrypt(UUID("47130fb5-987c-4af0-9e83-5eaf672d608b"), "123-45-6789","AEAD_AES_256_CBC_HMAC_SHA_512-Random"), "comment": "Jose Pereira's SSN encrypted."}); WriteResult({ "nInserted" : 1 })

    此时,我们能够手动加密该字段。

    所以,现在你一定在想——

    “我怎样才能读取那个加密值?”

    让我们在下一节中演示。

    读取加密字段

    此时,如果我们不通过加密配置就连接,将无法读取信息:

    shell#> mongo "mongodb://dba:secret@localhost:27017/?authSource=admin" mongo> use percona switched to db percona mongo-shell-2> show collections newcollection mongo-shell-2> db.newcollection.find().pretty() "_id" : 1, "medRecNum" : 1, "firstName" : "Jose", "lastName" : "Pereira", "ssn" : BinData(6,"AkcTD7WYfErwnoNer2ctYIsCVXS2nJYpSEgYFlp8ORmZ1i9PO/RGELdm+XxZyN6+ls+KLeDu1LQFtIIJs1Bwy5AMnaA3Lf4qAfm0Nmov6Iwuqer67HV2nIQk6dIa98QFLXs="), "comment" : "Jose Pereira's SSN encrypted."

    1 –要读取,客户端必须将ClientSideFieldLevelEncryptionOptions配置加载到其会话中;通过打开连接,将选项加载到变量中,并使用Mongo()构造函数进行日志记录,与我们之前所做的类似,但此时不需要配置密钥库:

    shell># mongo --shell --nodb --eval "var LOCAL_KEY = cat('/localkeys/client.key')" Percona Server for MongoDB shell version v4.4.3-5 type "help" for help mongo> var ClientSideFieldLevelEncryptionOptions = { "keyVaultNamespace" : "encryption.__dataKeys", "kmsProviders" : { "local" : { "key" : BinData(0, LOCAL_KEY) mongo> csfleDatabaseConnection = Mongo("mongodb://dba:secret@localhost:27017/?authSource=admin", ClientSideFieldLevelEncryptionOptions) connection to localhost:27017

    (这就是为什么将密钥存储在安全的地方很重要,因为它的编码将用于读写加密例程)

    2 –进入会话,配置完成后,我们可以运行find() 来返回纯文档:

    mongo> percona = csfleDatabaseConnection.getDB("percona") percona mongo> newcollection = percona.getCollection("newcollection") percona.newcollection mongo> percona.newcollection.find().pretty() "_id" : 1, "medRecNum" : 1, "firstName" : "Jose", "lastName" : "Pereira", "ssn" : "123-45-6789", "comment" : "Jose Pereira's SSN encrypted."

    在实现新功能时,我们也会在实现之前开始考虑我们想要了解的场景。以下是您在阅读本文后可能会遇到的一些问题:

    1.一个可以根用户能够读取该领域?

    不,决定用户是否能够读取加密字段的是连接是否加载了使用的加密密钥;没有它,用户将无法读取,即使它具有数据库的 root 权限。

    mongo localhost:4420/admin -uroot -psekret --eval "db.getSiblingDB('percona').newcollection.findOne()" --quiet "_id" : 1, "medRecNum" : 1, "firstName" : "Jose", "lastName" : "Pereira", "ssn" : BinData(6,"ArIjMQ7O9Uwanyv31U9RulQCZPt7IxoZpu6mu9ekXMRcsaMKZgkJypzwNkuY+HEOMRn3eU6BMTkM71Gm5KDqi4ERTP8ExEfRMHwuDNrDmGmb1q0QA+W7CL4iMOL6oSX79uc="), "comment" : "Jose Pereira's SSN encrypted." }<span style="font-weight: 400;"> </span>

    2.钥匙丢了怎么办?

    删除 或丢失客户主密钥会使所有使用该密钥加密的数据加密密钥永久不可读,进而导致使用这些数据加密密钥加密的所有值永久不可读。

    3.它在 ReplicaSet 或 Sharded Cluster 中是如何工作的?

    两种配置都会发生“读取加密字段”部分中提到的相同行为;一旦插入带有加密字段的文档,它就会原样复制到整个节点;加密。并且只有拥有正确密钥的用户才能读取它。

    replset:SECONDARY> use percona switched to db percona replset:SECONDARY> rs.secondaryOk() replset:SECONDARY> db.getSiblingDB('percona').newcollection.findOne() "_id" : 1, "medRecNum" : 1, "firstName" : "Jose", "lastName" : "Pereira", "ssn" : BinData(6,"ArIjMQ7O9Uwanyv31U9RulQCZPt7IxoZpu6mu9ekXMRcsaMKZgkJypzwNkuY+HEOMRn3eU6BMTkM71Gm5KDqi4ERTP8ExEfRMHwuDNrDmGmb1q0QA+W7CL4iMOL6oSX79uc="), "comment" : "Jose Pereira's SSN encrypted."

    4.使用该功能有什么限制吗?

    是的,在这里的官方手册中。它对限制进行了全面的描述;我们建议您查看手册,因为不同的配置(如 Shard Key、Unique Indexes、Collat​​ion、Views 等)存在限制。

    即使限制使用自动客户端字段级加密,手动方法和功能本身已经向我们展示了一个令人兴奋的选择,主要是因为它通过允许从 MongoDB 本地加密敏感字段而不是使用第三方来增强安全性-派对工具。它有助于减少第三方工具可能造成的违规行为。

    此外,如果您正在寻找要实施以保护 MongoDB 安装的安全措施列表,您可以使用MongoDB 安全检查表,如果您不遵循任何标准安全策略,这是一个很好的起点。

    [1] – https://docs.mongodb.com/manual/tutorial/manage-client-side-encryption-data-keys/#create-the-database-connection
    [2] – https://docs.mongodb.com/manual/core/security-client-side-encryption/#deterministic-encryption
    [3] – https://docs.mongodb.com/manual/core/security-client-side-encryption/#randomized-encryption
    https://docs.mongodb.com/manual/tutorial/manage-client-side-encryption-data-keys/#create-a-data-encryption-key