MongoDB 7.0可查询加密

MongoDB 7.0正式推出了可查询加密(Queryable Encryption)功能,用于满足更高数据库安全性要求的使用场景。

背景信息

MongoDB的透明数据加密(TDE)和云盘加密功能,都属于静态数据加密(Encryption at Rest)方案。该方案可以解决以下问题:

  • 数据保护:静态数据加密可以保护磁盘上的数据不会被未经授权的访问。即便攻击者能够物理地访问存储介质(如硬盘或SSD),未加密的数据也不会轻易地被泄露。

  • 泄露预防:若存储设备被盗或丢失,比如在一个数据中心发生安全事件或笔记本电脑遗失,加密能够确保敏感数据不会落入不当之手。

  • 合规性要求:多个行业标准和法规要求企业必须对敏感数据进行加密。敏感数据包含用户的隐私数据、财务信息等信息,Encryption at Rest帮助企业达到法规要求。

说明

MongoDB开通了TDE或云盘加密时,备份文件也会被加密。

采用Encryption at Rest方案时,数据被读取到内存中处理时仍然是明文形式。因此,为全面保护数据,您还应该考虑实施其他安全措施,如网络加密(SSLTLS)、数据库访问控制、审计和监控等。对于阿里云内部运维人员访问数据库PAAS服务背后的ECS,阿里云内部提供客户授权以及强制审计来避免产生安全风险。

如果您对数据库安全性有更高要求,还需要额外的加密手段,可以使用MongoDB 7.0版本正式发布的可查询加密(Queryable Encryption)功能。

功能简介

可查询加密功能于MongoDB 6.0开始推出的Preview版本,于MongoDB 7.0正式发布。

可查询加密只允许在客户端查看解密后的敏感数据。在查询到达服务器端时,会包含从KMS获取的加密密钥,然后在服务器端以密文进行查询并返回,最后在客户端利用密钥解密后以明文呈现。

可查询加密的特点如下:

  • 从客户端加密敏感数据,只有客户端拥有加密密钥。

  • 数据在整个生命周期(传输、存储、使用、审计和备份)中都是加密的。

  • 客户端可以直接对加密数据进行丰富的查询(包括等值匹配、范围、前后缀或子字符串等查询类型)。

  • 强大的数据隐私保护能力,只有能访问服务端的应用程序和加密密钥的授权用户才能看到明文数据。

  • 更轻量化的应用程序开发,涉及敏感数据的开发者无需考虑过多安全合规等问题,数据库会直接提供综合加密解决方案。

  • 降低敏感数据上云的安全顾虑。

MongoDB社区版和企业版(Atlas)目前开放的能力稍有差异,社区版不支持自动加密。

驱动版本以及加密库版本的要求,请参见MongoDB官方文档

使用限制

  • 加密集合上诊断命令的输出结果和查询日志将被额外编辑或隐藏,不利于问题分析:

    • 针对加密集合的一些命令(aggregate/count/find/insert/update/delete等)会在慢日志和profiler中被忽略。

    • 诊断命令(collStats/currentOp/top/$planCacheStats)的结果会被额外编辑并隐藏部分字段。

  • 加密字段的竞争(default contention8)、冲突可能导致写入延迟变大。

  • 元数据集合大于1 GB时需要手动Compaction

  • encryptedFieldsMap对象不可更改(包括里面的查询类型字段等)。

  • 仅支持副本集和分片集群实例,不支持单节点实例。

  • 不支持在从节点上读取开启了可查询加密的数据。

  • 不支持多文档更新操作(updateMany/bulkWrite),限制了findAndModify的参数。

  • 不支持upsert语义(触发upsert时,加密字段并不会被插入)。

  • 无法在一个集合上同时启用可查询加密与CSFLE(客户端字段级加密),也无法直接将启用CSFLE的集合或者未加密集合直接转换为可查询加密。

  • 仅支持新建空集合来使用可查询加密,不支持已存在集合使用可查询加密。

  • 无法重命名包含加密字段的集合;也无法通过$rename重命名加密字段。

  • 创建加密集合时如果指定jsonSchema,则不能包含encrypt关键字。

  • 不支持视图、时序集合、capped集合。

  • 不支持TTL索引或唯一索引。

  • 无法关闭jsonSchema校验。

  • 需要使用配置了可查询加密的MongoClient来删除集合,否则会有元数据残留。

  • 可查询加密不支持Collation,Collation会阻止针对加密字段的正常排序行为。

  • _id字段不能被指定为加密字段。

  • 可查询加密仅支持有限的命令和操作符,更多介绍,请参见官方文档

准备工作

本文以ECS作为验证客户端,如果您的测试环境已经包含相关依赖则可以跳过相应的步骤。mongosh仅支持自动加密,而社区版MongoDB仅支持显式加密,不支持自动加密。因此本文将使用Node.js驱动进行验证

  1. 安装Node.js以及npm。

    curl -fsSL https://rpm.nodesource.com/setup_lts.x | sudo bash -
    sudo yum install nodejs
    
    node -v
    npm -v
  2. 安装MongoDB Node.js官方驱动。

    mkdir node_quickstart
    cd node_quickstart
    npm init -y
    npm install mongodb@6.6
  3. 安装libmongocrypt库。

    vi /etc/yum.repos.d/libmongocrypt.repo
    // 文件中填充以下内容
    [libmongocrypt]
    name=libmongocrypt repository
    baseurl=https://libmongocrypt.s3.amazonaws.com/yum/redhat/8/libmongocrypt/1.8/x86_64
    gpgcheck=1
    enabled=1
    gpgkey=https://pgp.mongodb.com/libmongocrypt.asc
    
    // install
    sudo yum install -y libmongocrypt
  4. 安装Node.js驱动依赖的mongodb-client-encryption包。

    sudo yum groupinstall 'Development Tools'
    npm install mongodb-client-encryption
  5. 安装mongosh并设置MONGODB_URI环境变量。

    wget https://repo.mongodb.org/yum/redhat/8/mongodb-org/7.0/x86_64/RPMS/mongodb-mongosh-2.2.5.x86_64.rpm
    yum install -y ./mongodb-mongosh-2.2.5.x86_64.rpm
    
    export MONGODB_URI="mongodb://root:xxxxxx@dds-2zef23cef14b4f142.mongodb.pre.rds.aliyuncs.com:3717,dds-2zef23cef14b4f141.mongodb.pre.rds.aliyuncs.com:3717/admin?replicaSet=mgset-855706"
    // 测试连通性
    mongosh ${MONGODB_URI}
  6. 获取自动加密的共享库。

    Download Center中选择与您的机器和发行版对应的客户端(package选择crypt_shared)。

    //本地目录解压得到lib/mongo_crypt_v1.so
    tar -xzvf mongo_crypt_shared_v1-linux-x86_64-enterprise-rhel80-7.0.9.tgz

操作步骤

说明

社区版MongoDB不支持自动加密,因此,本文内容为显式加密的流程。

进入Node.jsREPL环境并在其中继续后面的操作:

node -i -e "const MongoClient = require('mongodb').MongoClient; const ClientEncryption = require('mongodb').ClientEncryption;"
  1. 创建客户主密钥。

    说明

    以下内容相当于是本地的KMS提供商,生产环境不建议这样配置。

    创建一个96字节的CMK,存储到本地文件系统的customer-master-key.txt中。

    const fs = require("fs");
    const crypto = require("crypto");
    try {
      fs.writeFileSync("customer-master-key.txt", crypto.randomBytes(96));
    } catch (err) {
      console.error(err);
    }

    示例里直接用Node.js的调用随机字符串生成,您也可以在Shell中利用/dev/urandom来生成这个96字节的CMK。

    echo $(head -c 96 /dev/urandom | base64 | tr -d '\n')
  2. 初始化变量。

    // KMS provider name should be one of the following: "aws", "gcp", "azure", "kmip" or "local"
    const kmsProviderName = "local";
    const uri = process.env.MONGODB_URI;
    const keyVaultDatabaseName = "encryption";
    const keyVaultCollectionName = "__keyVault";
    const keyVaultNamespace = "encryption.__keyVault";
    const encryptedDatabaseName = "medicalRecords";
    const encryptedCollectionName = "patients";

    上述变量的说明如下。

    • kmsProviderName:KMS提供商,本案例中使用local(本地)。

    • uri:MongoDB的连接串,可设置在MONGODB_URI环境变量中或者直接提供字符串。

    • keyVaultDatabaseName:存储数据加密密钥(DEKs)的库。

    • keyVaultCollectionName:存储数据加密密钥(DEKs)的集合,需要与常规集合区分开。

    • keyVaultNamespace:相当于 keyVaultDatabaseNamekeyVaultCollectionName变量。

    • encryptedDatabaseName:存储加密数据的库。

    • encryptedCollectionName:存储加密数据的集合。

  3. 在密钥库集合上创建唯一索引。

    const keyVaultClient = new MongoClient(uri);
    await keyVaultClient.connect();
    const keyVaultDB = keyVaultClient.db(keyVaultDatabaseName);
    // 先dropDatabase以避免有残留
    await keyVaultDB.dropDatabase();
    const keyVaultColl = keyVaultDB.collection(keyVaultCollectionName);
    await keyVaultColl.createIndex(
      { keyAltNames: 1 },
      {
        unique: true,
        partialFilterExpression: { keyAltNames: { $exists: true } },
      }
    );
    // double check
    await keyVaultColl.indexes();
  4. 创建加密集合。

    1. 获取客户主密钥并指定KMS提供商。

      const localMasterKey = fs.readFileSync("./customer-master-key.txt");
      kmsProviders = {local: {key: localMasterKey}};
    2. 创建数据加密的密钥。

      说明

      执行此步骤必须保证uri中使用的用户具有encryption.__keyVaultmedicalRecords库的dbAdmin权限。

      const clientEnc = new ClientEncryption(keyVaultClient, {
        keyVaultNamespace: keyVaultNamespace,
        kmsProviders: kmsProviders,
      });
      const dek1 = await clientEnc.createDataKey(kmsProviderName, {
        keyAltNames: ["dataKey1"],
      });
      const dek2 = await clientEnc.createDataKey(kmsProviderName, {
        keyAltNames: ["dataKey2"],
      });
    3. 指定需要加密的字段并配置刚创建的数据加密密钥(DEK)。

      const encryptedFieldsMap = {
        [`${encryptedDatabaseName}.${encryptedCollectionName}`]: {
          fields: [
            {
              keyId: dek1,
              path: "patientId",
              bsonType: "int",
              queries: { queryType: "equality" },
            },
            {
              keyId: dek2,
              path: "medications",
              bsonType: "array",
            },
          ],
        },
      };
    4. 指定自动加密共享库并创建MongoClient。

      const extraOptions = {cryptSharedLibPath: "/root/lib/mongo_crypt_v1.so"};
      const encClient = new MongoClient(uri, {
        autoEncryption: {
          keyVaultNamespace,
          kmsProviders,
          extraOptions,
          encryptedFieldsMap,
        },
      });
      await encClient.connect();
    5. 创建加密集合。

      const newEncDB = encClient.db(encryptedDatabaseName);
      await newEncDB.dropDatabase();
      await newEncDB.createCollection(encryptedCollectionName);
  5. 创建用于加密读写的客户端MongoClient。

    1. 指定存储数据加密密钥的集合。

      const eDB = "encryption";
      const eKV = "__keyVault";
      const keyVaultNamespace = `${eDB}.${eKV}`;
      const secretDB = "medicalRecords";
      const secretCollection = "patients";
    2. 指定客户主密钥。

      重要

      请勿在生产环境中使用本地密钥文件。

      const fs = require("fs");
      const path = "./customer-master-key.txt";
      const localMasterKey = fs.readFileSync(path);
      const kmsProviders = {
        local: {
          key: localMasterKey,
        },
      };
    3. 获取数据加密密钥。

      说明

      此处的DEK名称需要与步骤四的第二步创建的DEK名称一致。

      const uri = process.env.MONGODB_URI;;
      const unencryptedClient = new MongoClient(uri);
      await unencryptedClient.connect();
      const keyVaultClient = unencryptedClient.db(eDB).collection(eKV);
      const dek1 = await keyVaultClient.findOne({ keyAltNames: "dataKey1" });
      const dek2 = await keyVaultClient.findOne({ keyAltNames: "dataKey2" });
    4. 指定自动加密共享库并创建MongoClient。

      const extraOptions = {
        cryptSharedLibPath: "/root/lib/mongo_crypt_v1.so",
      };
      const encryptedClient = new MongoClient(uri, {
        autoEncryption: {
          kmsProviders: kmsProviders,
          keyVaultNamespace: keyVaultNamespace,
          bypassQueryAnalysis: true,
          keyVaultClient: unencryptedClient,
          extraOptions: extraOptions,
        },
      });
      await encryptedClient.connect();
    5. 创建ClientEncryption对象。

      const encryption = new ClientEncryption(unencryptedClient, {
        keyVaultNamespace,
        kmsProviders,
      });
  6. 向加密集合中插入包含加密字段的文档。

    const patientId = 12345678;
    const medications = ["Atorvastatin", "Levothyroxine"];
    const indexedInsertPayload = await encryption.encrypt(patientId, {
      algorithm: "Indexed",
      keyId: dek1._id,
      contentionFactor: 1,
    });
    const unindexedInsertPayload = await encryption.encrypt(medications, {
      algorithm: "Unindexed",
      keyId: dek2._id,
    });
    const encryptedColl = encryptedClient.db(secretDB).collection(secretCollection);
    await encryptedColl.insertOne({
      firstName: "Jon",
      patientId: indexedInsertPayload,
      medications: unindexedInsertPayload,
    });
  7. 在加密集合上进行字段级查询。

    const findPayload = await encryption.encrypt(patientId, {
      algorithm: "Indexed",
      keyId: dek1._id,
      queryType: "equality",
      contentionFactor: 1,
    });
    
    console.log(await encryptedColl.findOne({ patientId: findPayload }));

    返回示例如下。image

  8. 不使用带加密选项的Client访问不了加密字段。

    直接使用刚才创建的未加密的客户端unencryptedClient进行相同查询。

    console.log(await unencryptedClient.db(secretDB).collection(secretCollection).findOne());

    返回示例如下。image

    您也可以在外部直接用mongosh访问,模拟没有客户端密钥的情况下对数据库进行访问。

    //另外开一个终端会话,使用mongosh 直连MongoDB URI
    mongosh ${MONGODB_URI}
    
    db.getSiblingDB("medicalRecords").patients.findOne()

    返回示例如下。image

相关文档