MongoDB 7.0正式推出了可查詢加密(Queryable Encryption)功能,用于滿足更高數據庫安全性要求的使用場景。
背景信息
MongoDB的透明數據加密(TDE)和云盤加密功能,都屬于靜態數據加密(Encryption at Rest)方案。該方案可以解決以下問題:
數據保護:靜態數據加密可以保護磁盤上的數據不會被未經授權的訪問。即便攻擊者能夠物理地訪問存儲介質(如硬盤或SSD),未加密的數據也不會輕易地被泄露。
泄露預防:若存儲設備被盜或丟失,比如在一個數據中心發生安全事件或筆記本電腦遺失,加密能夠確保敏感數據不會落入不當之手。
合規性要求:多個行業標準和法規要求企業必須對敏感數據進行加密。敏感數據包含用戶的隱私數據、財務信息等信息,Encryption at Rest幫助企業達到法規要求。
MongoDB開通了TDE或云盤加密時,備份文件也會被加密。
采用Encryption at Rest方案時,數據被讀取到內存中處理時仍然是明文形式。因此,為全面保護數據,您還應該考慮實施其他安全措施,如網絡加密(SSL或TLS)、數據庫訪問控制、審計和監控等。對于阿里云內部運維人員訪問數據庫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 contention為8)、沖突可能導致寫入延遲變大。
元數據集合大于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驅動進行驗證。
安裝Node.js以及npm。
curl -fsSL https://rpm.nodesource.com/setup_lts.x | sudo bash - sudo yum install nodejs node -v npm -v
安裝MongoDB Node.js官方驅動。
mkdir node_quickstart cd node_quickstart npm init -y npm install mongodb@6.6
安裝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
安裝Node.js驅動依賴的mongodb-client-encryption包。
sudo yum groupinstall 'Development Tools' npm install mongodb-client-encryption
安裝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}
獲取自動加密的共享庫。
在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.js的REPL環境并在其中繼續后面的操作:
node -i -e "const MongoClient = require('mongodb').MongoClient; const ClientEncryption = require('mongodb').ClientEncryption;"
創建客戶主密鑰。
說明以下內容相當于是本地的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')
初始化變量。
// 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
:相當于keyVaultDatabaseName
和keyVaultCollectionName
變量。encryptedDatabaseName
:存儲加密數據的庫。encryptedCollectionName
:存儲加密數據的集合。
在密鑰庫集合上創建唯一索引。
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();
創建加密集合。
獲取客戶主密鑰并指定KMS提供商。
const localMasterKey = fs.readFileSync("./customer-master-key.txt"); kmsProviders = {local: {key: localMasterKey}};
創建數據加密的密鑰。
說明執行此步驟必須保證
uri
中使用的用戶具有encryption.__keyVault
和medicalRecords
庫的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"], });
指定需要加密的字段并配置剛創建的數據加密密鑰(DEK)。
const encryptedFieldsMap = { [`${encryptedDatabaseName}.${encryptedCollectionName}`]: { fields: [ { keyId: dek1, path: "patientId", bsonType: "int", queries: { queryType: "equality" }, }, { keyId: dek2, path: "medications", bsonType: "array", }, ], }, };
指定自動加密共享庫并創建MongoClient。
const extraOptions = {cryptSharedLibPath: "/root/lib/mongo_crypt_v1.so"}; const encClient = new MongoClient(uri, { autoEncryption: { keyVaultNamespace, kmsProviders, extraOptions, encryptedFieldsMap, }, }); await encClient.connect();
創建加密集合。
const newEncDB = encClient.db(encryptedDatabaseName); await newEncDB.dropDatabase(); await newEncDB.createCollection(encryptedCollectionName);
創建用于加密讀寫的客戶端MongoClient。
指定存儲數據加密密鑰的集合。
const eDB = "encryption"; const eKV = "__keyVault"; const keyVaultNamespace = `${eDB}.${eKV}`; const secretDB = "medicalRecords"; const secretCollection = "patients";
指定客戶主密鑰。
重要請勿在生產環境中使用本地密鑰文件。
const fs = require("fs"); const path = "./customer-master-key.txt"; const localMasterKey = fs.readFileSync(path); const kmsProviders = { local: { key: localMasterKey, }, };
獲取數據加密密鑰。
說明此處的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" });
指定自動加密共享庫并創建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();
創建ClientEncryption對象。
const encryption = new ClientEncryption(unencryptedClient, { keyVaultNamespace, kmsProviders, });
向加密集合中插入包含加密字段的文檔。
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, });
在加密集合上進行字段級查詢。
const findPayload = await encryption.encrypt(patientId, { algorithm: "Indexed", keyId: dek1._id, queryType: "equality", contentionFactor: 1, }); console.log(await encryptedColl.findOne({ patientId: findPayload }));
返回示例如下。
不使用帶加密選項的Client訪問不了加密字段。
直接使用剛才創建的未加密的客戶端
unencryptedClient
進行相同查詢。console.log(await unencryptedClient.db(secretDB).collection(secretCollection).findOne());
返回示例如下。
您也可以在外部直接用mongosh訪問,模擬沒有客戶端密鑰的情況下對數據庫進行訪問。
//另外開一個終端會話,使用mongosh 直連MongoDB URI mongosh ${MONGODB_URI} db.getSiblingDB("medicalRecords").patients.findOne()
返回示例如下。