Grant least privilege roles in MongoDB
use admindb.createUser({ user: "app", pwd: passwordPrompt(), roles: [ { role: "readWrite", db: "appdb" } ]})One role, one database. That is what an application needs.
Why it matters
Section titled “Why it matters”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.
Authentication is per-database
Section titled “Authentication is per-database”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=adminWithout 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.
Custom roles when built-ins don’t fit
Section titled “Custom roles when built-ins don’t fit”A reporting account that must never write:
use admindb.createUser({ user: "reporting", pwd: passwordPrompt(), roles: [ { role: "read", db: "appdb" } ]})And for genuinely narrow access, a custom role scoped to specific collections:
use admindb.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.
Related
Section titled “Related”- Enable authorization — roles do nothing until this is on.
- Enable TLS — protects the credential these roles attach to.