Skip to content

Grant least privilege roles in MongoDB

Severity: highApplies to: MongoDB 6.x / 7.x / 8.x
The fix
use admin
db.createUser({
user: "app",
pwd: passwordPrompt(),
roles: [ { role: "readWrite", db: "appdb" } ]
})

One role, one database. That is what an application needs.

MongoDB’s built-in roles are convenient and several of them are far wider than their names suggest. The usual setup hands an application one of these:

Role What it actually grants
root Everything, on every database. Superuser.
readWriteAnyDatabase Read and write every database, including other apps’
dbOwner readWrite + dbAdmin + userAdmin on that database
readWrite Read and write one database. This is the one you want.

dbOwner is the trap. It reads like “owns this database, scoped, fine” — and it includes userAdmin, meaning the account can create users and grant roles within that database. An injection against a dbOwner connection can mint itself a persistent credential. It is not a scoped version of readWrite; it’s a scoped version of admin.

root on an application account is worse and depressingly common, because it’s what the tutorial used to get things working.

This trips people up more than the roles do.

Users belong to the database they were created in, which is their authentication database — and it has nothing to do with which database they can access. A user created in admin with readWrite on appdb authenticates against admin and works on appdb.

That’s why connection strings need authSource:

mongodb://app:secret@10.0.1.5:27017/appdb?authSource=admin

Without authSource=admin, the driver tries to authenticate against appdb, finds no user there, and fails with an authentication error that tells you nothing about the real cause. It is the single most common MongoDB connection problem, and it isn’t a permissions bug.

Create all users in admin and always set authSource=admin. One place to look, one thing to remember.

A reporting account that must never write:

use admin
db.createUser({
user: "reporting",
pwd: passwordPrompt(),
roles: [ { role: "read", db: "appdb" } ]
})

And for genuinely narrow access, a custom role scoped to specific collections:

use admin
db.createRole({
role: "appReadOrders",
privileges: [{
resource: { db: "appdb", collection: "orders" },
actions: [ "find" ]
}],
roles: []
})

Collection-level scoping is worth reaching for when one credential shouldn’t see everything in the database — a reporting tool that needs orders but not users.