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()
返回樣本如下。