Jun
02
2021
--

Using the MongoDB Field Encryption Feature

MongoDB Field Encryption

MongoDB Field EncryptionOne of the main topics we have today is surely about security. On a daily routine, this can pass unnoticed, but sooner or later, we have to implement or work on some security guidelines. And today, we are going to discuss one of them, which is Field Encryption

Feature Introduction

Discussing the feature per se, it’s new in version 4.2+ releases only. And MongoDB provides two methods of Field Encryption, they are:

The automatic mode is available only on the Enterprise Edition and Atlas, while the manual method is supported on the Community Edition by the MongoDB drivers and mongo shell as well.

This article will use the Percona Server for MongoDB (PSMDB) running version 4.4 release with authentication enabled and the manual method in this article. As our objective here is to demonstrate the feature, we will use the mongo-shell to run all the operations.

However, for the application encrypting via driver is the most suggested approach. There is a detailed list of supported drivers for field-level encryption in the official documentation:

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

How To

   1 – To start using the feature, and illustration purpose, we will use the locally Managed Keyfile as our Key Management Service (KMS). 

It’s important to mention a local keyfile is quick, but lower security, thus not recommended in a production environment as it is stored alongside the database. For production, please consider using one of the following services:

As Key Management Service (KMS), MongoDB also supports:

  • Amazon Web Services KMS
  • Azure Key Vault
  • Google Cloud Platform KMS

Using locally Managed Keyfile, MongoDB requires specifying a file with base64-encoded 96-byte string with no line breaks[1]; Which can be created with the following example:

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

Please make sure to save the keyfile in a secure location, to avoid losing it. Otherwise, you would not be able to decrypt or read it later.

   2 – Once we have the key file, let’s open a mongo shell session with the database without connecting yet. Using the option –nodb; also use –shell to execute supplied code (in this case the –eval string value) without an automatic exit at the end; This step is necessary as we have to load the key into an object which will become a database connection property latter. In the following example, we are loading our key file into the database variable LOCAL_KEY:

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 – Next, load the document ClientSideFieldLevelEncryptionOptions using the proper client-side field-level encryption configuration:

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

   4 – After setting the variable; We can establish an FLE-enabled connection using the above variables via ClientSideFieldLevelEncryptionOptions on Mongo() constructor. This connection is also using normal username+password authentication as you can see in the mongodb://…/ URI string.

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

   5 – The next step will be to create the internal keyvault object per se; Until this moment, we were setting the variables and adjusting our client connections to proceed. It can be made as follow:

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

  5.1 – For extra illustration, as the above command does not generate any output; With a different session, we can check the newly created vault structure on the server: 

> show dbs
admin            0.000GB
config           0.000GB
encryption       0.000GB

> use encryption
switched to db encryption

> show collections
__dataKeys

   6 – With all set and vault structures deployed, let’s add a data encryption key to the database connection’s key vault. If successful, createKey() returns the UUID of the new data encryption key. This UUID which is a BSON Binary it’s our encryption key and will be used to encrypt the fields manually:

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

   7 – Next, let’s insert a document using the above key to encrypt; Please note we did the above operations in the same mongo-shell, and to encrypt, we are going to use the encrypt() method along with the parameters to hide the SSN: “123-45-6789”.

    7.1 To use the encrypt() function on a field it expects the following 3 arguments: 

  •   encryptionKeyId – This is our generated key.
  •   Value – In this example, we are going to hide the “123-45-6789” value from SSN.
  •   encryptionAlgorithm – And what algorithm are we going to use to encrypt the field, we can choose between Deterministic Encryption[2] or Randomized Encryption[3]

    7.2 – With all set, we can insert the document encrypting the field as follows:

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 })

Perfect! At this point, we were able to encrypt the field manually.

So, now you must be thinking –

How can I read that encrypted value?

Let demonstrate in the next section.

Reading an Encrypted Field

At this point, if we connect without passing the encryption configuration, we will not be able to read the information:

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."
}
exit

1 – To read, the client must load the ClientSideFieldLevelEncryptionOptions configuration into its session; By opening a connection, load the options into the variable, and use the Mongo() constructor to logging, similar as we did before, but at this time, without the requirement of key vault configuration:

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

(that’s why is important to store the key in a safe place, as its encoding will be used on the read-write encrypt routine)

2 – Into the session, and with the configuration done, we can run a find()  to return the plain document:

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."
}

Common Questions

When implementing a new feature, we also start thinking about scenarios we would like to understand before implementing it. Here are some questions that you might have after reading this article:

     1. Can a root user be able to read the fields?

    • No, what dictates if a user will be able to read encrypted fields is if the connection is loaded with the used encryption key; Without it, the user won’t be able to read, even if it has root privileges on the database.
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."
} 

    2. What happens if I lose the Key?

    • Deleting or losing the customer master key renders all data encryption keys encrypted with that as permanently unreadable, which in turn causes all values encrypted with those data encryption keys as permanently unreadable.

     3. How does it work in a ReplicaSet or Sharded Cluster?

    • The same behavior noted in the “Reading an Encrypted field” section happens for both configurations; Once inserted a document with an encrypted field, it is replicated throughout the nodes as it is; encrypted. And only a user with the correct key will be able to read it.
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. Are there any limitations to using the feature?

    • Yes, in the official manual here. It has a comprehensive description of the limitations; We suggest you review the manual as limitations exist on different configurations as Shard Key, Unique Indexes, Collation, Views, and so on. 

And if you have any further questions, please feel free to share them with us in the comment section below.

Conclusion

Even with the restriction to use the Automatic Client-Side Field Level Encryption, the manual method and the feature per se have shown us to be an exciting option, mainly because it strengthens the security by allowing encrypting sensitive fields natively from MongoDB instead of using third-party tools. It is helping to reduce possible breaches a third-party tool can create.

Additionally, if you are looking for a list of security measures to implement to protect the MongoDB installation, you can use the MongoDB Security Checklist, which is a good starting point if you are not following any standard security policy.

References:

Powered by WordPress | Theme: Aeros 2.0 by TheBuckmaker.com