Many years ago I took a semester off from college and worked as a software developer intern for a consulting company. To protect the innocent I won't reveal the names and you can't find any information about this time on my LinkedIn profile so don't bother checking. The reason I mentioned this point in my life is my job at that consulting company was working with a major manufacturer on their financial reporting system. My job you ask? To write the security component. Yes, I as the intern was given the responsibility of writing the component to handle the authentication and authorization of the entire application. After all, security is an afterthought of the software development process.
Fast forward a few decades and look at how times have changed! I don't believe that consulting company is around anymore and I hope that the application they wrote has been upgraded a few times since my work on it. Today threats are everywhere and quality software vendors put security code reviews as a release criteria to ship their product. The team behind MongoDB, the world's fastest growing database is no different in their approach to security, and this is why you see security enhancements like read-only views in their latest MongoDB 3.4 release.
A Word from our Sponsor!
Of course, no article on security is complete without reminding readers of one critical fact – before you go into production with your new MongoDB-based application, enable access control! Its quick and straightforward - the MongoDB Security Checklist steps you through what you need to do
What are Read-only Views?
Read-only views (ROV) are a similar concept to table views found in relational databases. They enable administrators to define a query that is materialized at runtime. ROVs do not store data and are considered first class objects in MongoDB. Being first class objects allows administrators to define permissions on who can access the views. ROVs become a separation layer between the data itself and the user. This is one of the biggest benefits of the feature: Users accessing the view do not need access to the underlying data the view is referencing. In addition it is important to note that since the view does not store data, as the source data changes so does the results of the view, so developers don’t need to concern themselves with working around the issues imposed by eventual consistency, such as returning stale or deleted data.
Why are read-only views (ROV) and security mentioned in the same sentence?
From a security standpoint think of the scenario where you have a customer service web application that queries for customer information. The database the application is using contains more information than the customer service representative needs such as social security numbers, and other sensitive information. At first glance you may think this is an easy problem to solve, just make sure the queries that the web application is submitting to the database does not request those sensitive fields. On paper this works and if everyone were honest the world would be a peaceful place and my article would be very short. However, there are some people that want to exploit this flawed assumption.
Imagine that we stick with this design where the credentials that the web application is using to connect to the database has direct access to the appropriate tables/collections. If an attacker compromised our web application they could be allowed to make a connection to our database. Once connected they could make their own ad-hoc queries and return the sensitive information that is contained in the database since the web application credentials has the appropriate access to the data. To mitigate this security issue there are different solutions depending on the database platform you are using. For example, within MongoDB you could have a job that copies just the key pieces of data into a new collection and give the web application just access to that collection, but this introduces moving parts in the application and the database. In the end you will find that read-only views make these work arounds redundant. At a high level our security issue can be mitigated using MongoDB's ROVs as follows:
- Create a view that contains an aggregation query on the data you wish to obtain
- Create a role that has "find" permission on the view
- Create or grant a user access to the role
The following is a step by step example on leveraging ROVs as a separation between a user and the data.
Example: Securing a customer service web application using MongoDB's Read-only Views To walk through this example you will need a MongoDB instance available. To download the latest version of MongoDB go to https://www.mongodb.com/download-center.
For help on installing MongoDB go to https://docs.mongodb.com/manual/installation/.
For purposes of this demonstration the MongoDB instance does not need to be configured in any special way, such as configuring a replica set or enabling sharding. A simple out of the box instance of MongoDB running is acceptable. For this example I created a new folder called, "ROVExample" and started the mongod process which will use the default port of 27017. If you wish to use a different port you may specify it with the "--port" parameter.
Command Prompt> mkdir ROVExample
Command Prompt> mongod --dbpath ROVExample
.......
2017-01-04T09:32:23.190-0500 I INDEX [initandlisten] build index done. scanned 0 total records. 0 secs
2017-01-04T09:32:23.190-0500 I COMMAND [initandlisten] setting featureCompatibilityVersion to 3.4
2017-01-04T09:32:23.191-0500 I NETWORK [thread1] waiting for connections on port 27017
At this point we have an instance of MongoDB running and waiting for our connections. Now we want to enable MongoDB to use authentication via traditional usernames and passwords. There are other authentication mechanisms such as X.509 certificates and leveraging LDAP. For more information on these other mechanisms check out the docs on MongoDB authentication. . To enable authentication with MongoDB we must first connect to our MongoDB instance and create an administrator user. In the below snippet we are connecting to our MongoDB instance via the Mongo Shell command line tool. Next we are switching to the ADMIN database and using the db.createUser() function to create a user that is an administrator.
Command Prompt> mongo
...
(mongod-3.4.0) test> use admin
switched to db admin
(mongod-3.4.0) admin> db.createUser(
{
user: "theadmin",
pwd: "pass@word1",
roles: [ { role: "root", db: "admin" } ]
}
)
Upon successful execution of the command you will get a message like this one:
Successfully added user: {
"user": "theadmin",
"roles": [
{
"role": "root",
"db": "admin"
}
]
}
Now that we have created the admin account we need to stop the MongoDB service and restart it with the "--auth" switch which tells MongoDB to require authentication. Note, if we were running a replica set, we could instead use a rolling restart as we enable authentication across the cluster, thus avoiding any service interruption
Command Prompt> mongod --dbpath ROVExample --auth
...
2017-01-04T09:52:32.713-0500 I CONTROL [initandlisten] options: { security: { authorization: "enabled" }, storage: { dbPath: "ROVExample" } }
...
2017-01-04T09:52:33.355-0500 I NETWORK [thread1] waiting for connections on port 27017
Now log in to MongoDB with the account we just created. We do this by passing the credentials and a parameter called, "--authenticationDatabase" which tells MongoDB where the user credentials for the given user are stored. Since we created the user in the admin database we will connect to MongoDB using the shell as follows:
Command Prompt>mongo --authenticationDatabase=admin -u theadmin -p pass@word1
MongoDB shell version v3.4.0
connecting to: mongodb:https://www.linkedin.com/redir/invalid-link-page?url=%2F%2F127%2e0%2e0%2e1%3A27017
...
MongoDB Enterprise >
We are now ready to create some sample data to use with this demonstration. A complete discussion of enabling authentication in MongoDB is available in the online documentation.
Side Note: When you supply a password on the command line for any application, including our example above, remember that anything you type is available in a command line history. For example, if you're on a linux platform, just type, "history" on the command line to see. If you are paranoid or connecting to your production database try launching the shell with just the "mongo --authenticationDatabase=admin" then once connected use the db.auth() command as follows:
MongoDB Enterprise > use admin
switched to db admin
MongoDB Enterprise > db.auth('root','pass@word1')
1
MacBook-Pro-121(mongod-3.4.0) admin>
Inserting Sample Data
In this example we will be inserting a simple document that contains both fields a customer service web application might use (i.e. first name, last name, and address) and some data that is related to a customer but is sensitive (i.e. social security number, date of birth, etc).
MongoDB Enterprise > use FooBarFinancial
switched to db FooBarFinancial
MongoDB Enterprise > db.Customers.insert(
{ first_name: "Rob", last_name: "Walters",
SSN: "123-45-6789", DOB: "01/01/1996",
address_line_1: "123 Main St.", city: "Boston", state: "MA" } )
(Yes I am 21, at least that's what I keep telling myself.. )
Side Note: The best practice is you only want to give users the least permission they need to do their job. In a production environment we may want to craft special custom roles that only do the tasks the users need. In some cases like this one where we are in a development/test environment I am ok with using a superuser role like ROOT. There are a few roles that are considered "superusers". These roles can elevate themselves and special care should be taken when using them. Be sure to audit your MongoDB instance to keep honest people honest. For more information on the superuser roles see the build-in roles section of the MongoDB online documentation.
At this point we have created the "FooBarFinancial" database and added a document to the Customers collection. If you perform a simple Find statement you can verify as follows:
MongoDB Enterprise > db.Customers.find()
{
"_id": ObjectId("586d1b6680ca46840069e50b"),
"first_name": "Rob",
"last_name": "Walters",
"SSN": "123-45-6789",
"DOB": "01/01/1996",
"address_line_1": "123 Main St.",
"city": "Boston",
"state": "MA"
}
Side Note: If you are not familiar with MongoDB you may notice a new field called, "_id" that we didn't add when we created the document. In MongoDB every document needs to have a unique field called, "_id". Although you can specifically provide one, if you don't you will get one created for you in the form of what looks like a Globally Unique ID (GUID). This GUID contains a timestamp of the creation time. You can see this yourself by simply copying and pasting in the ObjectId value and a .getTimestamp() command as follows:
MongoDB Enterprise > ObjectId("586d1b6680ca46840069e50b").getTimestamp()*
ISODate("2017-01-04T15:57:26Z")
Creating the view
At this point we are ready for our first step in the solution, configuring the view. Read-only Views (ROV) as mentioned previously are materialized at run-time and thus store no actual data. To create one we will go to the "FooBarFinancial" database and create the view as follows:
MongoDB Enterprise > db.createView("ViewCustomers", "Customers",
[
{ $project:
{ first_name: 1,
last_name: 1,
address_line1: 1,
city: 1,
state: 1
}
}
] )
The first argument is the name of the view, followed by the collection the view will be using, followed by the aggregation query we are looking to execute to retrieve our data. In this example we have a very simple aggregation query that just uses $project to return the 5 keys. Note that the "1" value means include this column, we could have also listed the other keys and said, "0" which means do not include. For additional reading on the aggregation pipeline please check out the following URL: https://docs.mongodb.com/manual/core/aggregation-pipeline/.
Once we created this view if you issue a query in the MongoDB shell (show collections) you will see that two new collections were created in the FooBarFinancial database: system.views and ViewCustomers. System.views stores the metadata for the views defined in the collection and ViewCustomers is our read-only view presented to us as a collection. Why does my ROV appear as a collection? This allows us as administrators to define access on this view.
Creating user-defined roles
Rather than give a specific user specific access to a specific resource, we want to avoid management headaches by grouping access privileges into roles. We can then grant access to these roles to the users themselves. Let's create the "CustomerServiceQuery" role and give it "find" permissions on the "ViewCustomers" view we just created.
MacBook-Pro-121(mongod-3.4.0) FooBarFinancial> use admin
switched to db admin
MacBook-Pro-121(mongod-3.4.0) admin> db.createRole(
{ role: "CustomerServiceQuery",
privileges: [
{ resource:
{ db: "FooBarFinancial", collection: "ViewCustomers"},
actions: ["find"] } ],
roles:[]
} )
Next, we will create a new user for our web application to use called, "webuser" using the createUser function:
MacBook-Pro-121(mongod-3.4.0) admin> db.createUser(
{ user: "webuser",
pwd: "pass@word1",
roles: [ { role: "CustomerServiceQuery", db: "admin" } ] } )
Now we are ready to test our new user's access to the data!
Querying the view with our new minimal privilege user
On a new window connect to MongoDB via the Mongo shell as follows:
Command Prompt> mongo --authenticationDatabase=admin -u webuser
-p pass@word1
MongoDB shell version v3.4.0
connecting to: mongodb:https://www.linkedin.com/redir/invalid-link-page?url=%2F%2F127%2e0%2e0%2e1%3A27017
MongoDB server version: 3.4.0
...
MongoDB Enterprise >
Note that we are connecting to MongoDB and specifying the admin database as our authentication database. This is because the admin database is where we created webuser. We could have also created the user in the FooBarFinancial database and used that as a authenticationDatabase. It is a best practice to leverage the admin database for user accounts. Now that we have authenticated notice that you as the webuser can't do much of anything. This user has no permissions other than executing the view which we gave them permission to do. For example, try viewing the databases via the "show dbs" command:
MongoDB Enterprise > show dbs
2017-01-04T12:16:59.399-0500 E QUERY [main] Error: listDatabases failed:{
"ok": 0,
"errmsg": "not authorized on admin to execute command { listDatabases: 1.0 }",
"code": 13,
"codeName": "Unauthorized"
} :
Now, drum roll please... our web application needs to get access to our customers so they simply query the view as follows:
MongoDB Enterprise > use FooBarFinancial
switched to db FooBarFinancial
MongoDB Enterprise > db.ViewCustomers.find()
{
"_id": ObjectId("586d2b17305dabd49d2c9417"),
"first_name": "Rob",
"last_name": "Walters",
"city": "Boston",
"state": "MA"
}
And there you have it all the sensitive info stripped and just the information the customer service web application needs. If an attacker compromised the webuser account or the web page they might be able to connect to MongoDB but all they would have access to is the information stored in this view.
Side Note: You may notice on the MongoDB shell an error following the execution of this query. Something with the text, "not authorized on FooBarFinancial to execute command { profile: -1.0 }". When we execute a query using the MongoDB shell it returns a cursor and some information like how many milliseconds the query took. This profile information requires yet another permission in order to be obtained and thus presents the error that you see. It is specific in this case to using the MongoDB shell in our example.
Some considerations when using read-only views
- When a view is queried MongoDB will materialize the view with the latest values from the underlying collection. For example, consider the scenario where a customer address view is queried at time T0. At time T1, the customer address is updated. At time T2 the view is queried again and the results now reflect the latest value of the customer address.
- Views can reference other views. They do not always have to reference a collection.
- Indexes can't be created on views. However, they can be created on the underlying collections that the views are referencing.
Conclusion
The world is insecure and today more than ever we as application developers and administrators need to think of the security implications of everything we deploy. The developers at MongoDB think no different and with the recent release of 3.4 provide another security related feature called Read-only Views(ROV). ROVs provide a way for administrators to separate access to the underlying data from the user requesting the data. This feature supports the principle of least privileged software architecture.
You can learn more about MongoDB read-only views from the documentation.
About the author: Robert Walters is a Senior Solutions Architect at MongoDB based in the Boston, Massachusetts area. Robert has spent almost 20 years working with database technologies and authored many technical books and whitepapers. In addition he has co-authored three patents while working on the SQL Server product team at Microsoft.