RE:NODE

MongoDB users and roles: createUser and least privilege

By RE:NODE Team
16 min read


A MongoDB user is a name plus the database it was created in, and it can do exactly what its roles allow and nothing else. You create one with db.createUser(), give it one or more roles - each role scoped to a database - and the user then authenticates against the database it was created in. For an application, the right answer is almost always a dedicated user holding readWrite on that application's database and nothing more. For a dashboard, read. For a backup job, backup. The superuser with root stays in the password manager and is used by a human, rarely.

That is the whole model. The rest of this guide is the detail that makes it work: which built-in role does what (including the two that are far more powerful than their names suggest), where users are actually stored, how to list them, change their passwords and adjust their roles, and how to write a custom role when no built-in one is narrow enough. Everything here uses mongosh and applies to MongoDB 5.0 and later; most of it is unchanged back to 4.x.

Where users live: the authentication database#

Every user is created in a database, and that database is part of its identity. shopapp created in shop and shopapp created in admin are two different users, with separate passwords and separate roles. The database a user was created in is called its authentication database, and it is what the authSource option in a connection string names. MongoDB connection strings covers how that option is resolved and why getting it wrong produces Authentication failed with a correct password.

Physically, all users sit in one collection regardless of where they were created: admin.system.users. The db field on each document records the authentication database. That is why you sometimes see users written as shopapp@shop - name, then authentication database.

The authentication database and the databases a user can work in are independent. A user created in admin can hold readWrite on shop and nothing on admin itself, and a user created in shop can be granted roles on analytics as well. Where the user lives decides only where it logs in; the roles decide what it can touch.

Two conventions both work:

  • Every user in `admin`. One place to look, one authSource=admin in every connection string, and roles scoped per database. This is the more common setup on single servers.
  • Each application user in its own database. shopapp lives in shop, blogapp lives in blog, and show users inside each database lists exactly the accounts that belong to it. The connection string uses authSource=shop.

Pick one and keep to it. The mixed state - some users in admin, some elsewhere, nobody sure which - is where wasted afternoons come from.

Creating a user with db.createUser#

Authentication has to be enabled first - security.authorization: enabled in mongod.conf. Without it, roles are not enforced and every connection can do anything. On a hosted database this is already on and you receive a first set of credentials. On your own installation, the localhost exception lets a connection from the server itself create the first user in admin while no users exist; it closes as soon as that user is created.

Connect as a user allowed to manage users, switch to the authentication database you have chosen, and create the user:

javascript
use admindb.createUser({  user: "shopapp",  pwd: passwordPrompt(),  roles: [ { role: "readWrite", db: "shop" } ]})

The fields:

FieldRequiredWhat it holds
useryesThe name, unique within this database
pwdyesThe password; passwordPrompt() asks for it
rolesyesAn array, which may be empty []
customDatanoAny document, for your own notes
authenticationRestrictionsnoAllowed client and server addresses
mechanismsnoSCRAM-SHA-1, SCRAM-SHA-256, or both

passwordPrompt() matters more than it looks. A password typed inline, pwd: "hunter2", sits in terminal scrollback, in screen shares and in any session recording or script you paste it into. mongosh already keeps known credential commands out of its history file, but prompting means the question never comes up.

Roles can be written two ways. { role: "readWrite", db: "shop" } is explicit and always correct. A bare string, "readWrite", means that role on the database you are currently in - so the same command run in admin grants readWrite on admin, which is almost never what you meant. Use the document form every time and the question does not arise.

customData is free-form and worth using for the one fact you will want later: customData: { owner: "checkout-api", created: "2026-10-15" }. It shows up when you list users, which is the moment you are trying to work out what an old account is for.

Restricting where a user may connect from

authenticationRestrictions limits a user to particular client addresses, which is a cheap second layer behind the firewall:

javascript
use admindb.createUser({  user: "reporting",  pwd: passwordPrompt(),  roles: [ { role: "read", db: "shop" } ],  authenticationRestrictions: [ { clientSource: ["10.0.0.0/24"] } ]})

A login from any other address fails authentication even with the correct password. It is only as useful as your addresses are stable: an application on a platform without a fixed outbound address cannot use it. It also does not replace binding and firewalling - an unreachable port is still the better control, as the database security checklist argues at length.

The built-in roles, and what they really allow#

MongoDB ships with a set of roles that cover most needs. Database-scoped roles are granted on one database at a time:

RoleAllowsTypical holder
readQuery every non-system collectionDashboards, reporting, humans
readWriteread plus insert, update, deleteThe application
dbAdminIndexes, stats, profiling, validationRarely anyone on its own
userAdminCreate and modify users and rolesNobody routinely
dbOwnerreadWrite, dbAdmin and userAdminA trusted admin of one database

Roles that only exist on the admin database and apply to the whole server:

RoleAllowsTypical holder
readAnyDatabaseread on every databaseServer-wide reporting
readWriteAnyDatabasereadWrite on every databaseMigration tools, briefly
dbAdminAnyDatabasedbAdmin on every databaseOperations tooling
userAdminAnyDatabaseuserAdmin on every databaseEffectively the superuser
clusterMonitorRead-only server and replication statsMonitoring agents
backupEverything mongodump needsBackup jobs
restoreEverything mongorestore needsRestore jobs, briefly
rootAll of the above combinedThe emergency account

Three of these need an honest note, because the names undersell them.

`readWrite` can drop collections. Besides inserts, updates and deletes, it includes createCollection, createIndex, dropIndex and dropCollection. It cannot drop the database, but an application with readWrite and a bad migration script can still empty it collection by collection. If your application never creates or drops collections at runtime, a custom role (below) can take that away.

`dbAdmin` is not read access - but it can delete. It covers indexes, statistics, the profiler, collMod and validation, and it does not let you find documents in ordinary collections. It does include dropCollection and dropDatabase. So a user with only dbAdmin cannot read your data and can destroy all of it. That is a strange combination to hand anyone, which is why it rarely appears on its own.

`userAdmin` is root in disguise. A user who can create users and grant roles can grant any role on that database to anyone, including themselves. On a single database that makes userAdmin equivalent to dbOwner. On admin, userAdmin or userAdminAnyDatabase can grant root. MongoDB's own documentation says as much. Treat any account holding either as a superuser, protect it like one, and never put it in an application's connection string.

dbOwner and anything ending in AnyDatabase belong in the same bucket for the purpose of applications: never. If the application's user can reach every database, a SQL-injection-style query bug or a leaked .env file exposes all of them.

Least privilege: one user per job#

The useful way to think about users is by job, not by person or by system. Each thing that connects gets its own user with the narrowest role that lets it work. A typical small setup looks like this:

javascript
use admin// The running application: read and write its own database only.db.createUser({  user: "shop_app", pwd: passwordPrompt(),  roles: [ { role: "readWrite", db: "shop" } ],  customData: { owner: "checkout-api" }})// Dashboards and people poking about: read only.db.createUser({  user: "shop_ro", pwd: passwordPrompt(),  roles: [ { role: "read", db: "shop" } ]})// Nightly dump: the backup role, nothing else.db.createUser({  user: "backup_job", pwd: passwordPrompt(),  roles: [ { role: "backup", db: "admin" } ]})// The monitoring check: server stats, no data.db.createUser({  user: "monitor", pwd: passwordPrompt(),  roles: [ { role: "clusterMonitor", db: "admin" } ]})

Separate users pay off in three ways. A leaked credential exposes one job's rights, not everything. Rotating one password touches one consumer, not every service at once. And the server log and db.currentOp() show the user on every operation, so when a nine-second query appears you know which service sent it before you start guessing. Combine that with appName in each connection string and the question "who is doing this" answers itself.

For a backup user that only ever dumps one database, read on that database is enough for mongodump to export its collections. The backup role is for dumping the whole server, including users and roles with --dumpDbUsersAndRoles. The mechanics of both are in mongodump and mongorestore. Give restore to a user only for the duration of a restore; it can write into any database.

readWrite on shopread on shopbackupclusterMonitorApplicationshop_appDashboardshop_roBackup jobbackup_jobMonitoringmonitormongodauth via adminshop database
Each consumer connects as its own user

The same idea in other engines has different syntax and the same reasoning: MySQL users and privileges and PostgreSQL roles and permissions cover them, if you run more than one.

Listing users and checking what they can do#

Four commands answer almost every question about who exists and what they may do:

javascript
use adminshow users                                   // users in the current databasedb.getUsers()                                // the same, as documentsdb.getUser("shop_app", { showPrivileges: true })db.adminCommand({ usersInfo: { forAllDBs: true } })   // every user on the server

show users and db.getUsers() only list users whose authentication database is the one you are in. If you create users in several databases, the forAllDBs form is the only complete list, and it needs a user with rights to view users across the server.

showPrivileges: true expands a user's roles into the actual actions they grant, including roles inherited from other roles, under inheritedPrivileges. It is long, but it is the answer to "can this user drop that collection" without testing it in production.

To check the user you are currently connected as:

javascript
db.runCommand({ connectionStatus: 1, showPrivileges: true })

authInfo.authenticatedUsers shows who you are and where you authenticated; authenticatedUserRoles shows the roles in effect. When an application gets not authorized on shop to execute command, running this with the application's credentials in mongosh tells you exactly what it has, which is faster than reading the code that created it.

Changing passwords, roles and removing users#

Users change over their life, and every change has its own helper:

javascript
use admindb.changeUserPassword("shop_app", passwordPrompt())db.grantRolesToUser("shop_ro", [ { role: "read", db: "analytics" } ])db.revokeRolesFromUser("shop_ro", [ { role: "read", db: "analytics" } ])db.updateUser("shop_app", { customData: { owner: "checkout-api-v2" } })db.dropUser("old_worker")

Run them in the user's authentication database - a changeUserPassword in shop for a user that lives in admin fails with User shop_app@shop not found, which is the same mistake as a wrong authSource seen from the other side.

db.updateUser() with a roles field replaces the whole array rather than adding to it. Passing one role to "add" it silently removes every other role the user had. Use grantRolesToUser and revokeRolesFromUser for incremental changes and keep updateUser for customData, authenticationRestrictions and full rewrites you meant.

Changes to roles take effect immediately on a standalone mongod, including for connections that are already open. In a sharded cluster, mongos caches user information and refreshes it on userCacheInvalidationIntervalSecs, 30 seconds by default.

Rotating an application password without downtime

Changing the password of a user that a running application is using breaks new connections the moment you do it. Existing pooled connections stay authenticated, so the failure shows up later and looks random. The clean way is two users:

  1. Create shop_app_2 with exactly the same roles as shop_app.
  2. Put the new user and password into the application's environment and restart or redeploy it.
  3. Watch db.currentOp() or the server log until nothing authenticates as shop_app.
  4. db.dropUser("shop_app").

Next rotation, go the other way. It costs one extra user for a few minutes and avoids the half-broken state entirely. Where the credentials should live while you do this is covered in environment variables and secrets.

Letting users change their own password

No built-in role below userAdmin includes changing your own password, so a user with only readWrite cannot run changeUserPassword on itself. If you want people to rotate their own credentials, grant a small custom role holding the changeOwnPassword action, shown in the next section.

Custom roles for anything narrower#

When no built-in role fits - a worker that may insert into one collection and read one other, a reporting user that must not see a customers collection - write a role. A role is a list of privileges, each a resource plus actions, and optionally other roles it inherits:

javascript
use shopdb.createRole({  role: "ordersWriter",  privileges: [    { resource: { db: "shop", collection: "orders" },      actions: [ "find", "insert", "update" ] },    { resource: { db: "shop", collection: "products" },      actions: [ "find" ] }  ],  roles: []})use admindb.grantRolesToUser("order_worker", [ { role: "ordersWriter", db: "shop" } ])

That worker can read products, read and add and change orders, and do nothing else: no remove, no dropCollection, no access to any other collection.

Resource forms worth knowing:

ResourceMeans
{ db: "shop", collection: "orders" }One collection
{ db: "shop", collection: "" }Every non-system collection in shop
{ db: "", collection: "orders" }orders in any database
{ cluster: true }Server-wide actions such as serverStatus

Two rules about where roles live. A role created in an ordinary database can only grant privileges on that database. A role created in admin can grant privileges on any database and on the cluster, so cross-database roles go in admin. And a role is referenced by name and database together, exactly like a user - ordersWriter in shop is { role: "ordersWriter", db: "shop" }.

A few actions cover most custom roles: find, insert, update, remove for data; createIndex, dropIndex, createCollection, dropCollection for structure; listCollections, listIndexes, collStats, dbStats for tools that inspect; changeOwnPassword and changeOwnCustomData for self-service. The full list is in the MongoDB manual's privilege actions page, and it is worth checking there rather than guessing, because a misspelt action is rejected at createRole time but a missing one only shows up when the application hits it.

The self-service password role mentioned above:

javascript
use admindb.createRole({  role: "changeOwnPassword",  privileges: [    { resource: { db: "admin", collection: "" },      actions: [ "changeOwnPassword", "changeOwnCustomData" ] }  ],  roles: []})

The resource is the user's authentication database. Grant it alongside the user's real role and they can run db.changeUserPassword() on themselves and nobody else.

Inspect and maintain roles with db.getRoles({ showPrivileges: true }), db.getRole("ordersWriter", { showPrivileges: true }), db.grantPrivilegesToRole(), db.revokePrivilegesFromRole() and db.dropRole(). Changing a role changes every user holding it at once, which is the point - and also the reason to name roles after a job, not a person.

Errors you will meet#

`Authentication failed` (code 18). Usually the right password checked against the wrong authentication database. Run db.adminCommand({ usersInfo: { forAllDBs: true } }) as an admin and look at the db field of the user, then match authSource to it.

`not authorized on shop to execute command { ... }` (code 13). Authenticated, but the roles do not include the action. The message names the command; connectionStatus with showPrivileges shows what the user does have. Grant the narrowest role that adds it, not dbOwner.

`command createUser requires authentication`. You are connected anonymously to a server with authorization on. Reconnect with credentials.

User already exists, or user not found. The use before the command put you in a different database than you thought, so MongoDB looked for shop_app@shop instead of shop_app@admin. Running db.getName() before user commands saves this.

`Could not find role: ordersWriter@admin`. You referenced a custom role by name in a database where it was not created. Give the full { role, db } document with the database it lives in. The same error appears for a built-in role with the wrong case: the names are readWrite, dbAdmin, userAdmin and clusterMonitor, and readwrite does not exist.

On a hosted MongoDB#

On a server you rent rather than install, authentication is already on and the first credentials are given to you - on RE:NODE they are generated for each server and shown in the panel alongside the host and port, so there is no shared default to change. Treat that account as the superuser it is: use it to create the per-job users above, store it in a password manager, and do not put it in an application's environment.

Managed cluster services sometimes take user management out of the shell entirely. MongoDB Atlas, for example, manages database users through its own interface and API rather than db.createUser(), though the roles are the same ideas. On a standalone server you reach by host and port, everything in this guide works as written.

Users and roles are stored inside the database, so they are part of what a full backup has to capture. A whole-server mongodump with the backup role, or a filesystem-level copy of the data directory, includes admin.system.users; a single-database dump does not include users unless you ask for them. Database backups and restores covers which kind to take for which disaster.

FAQ#

Which database should I create MongoDB users in?

Either admin or the application's own database works, and a user in either can hold roles on other databases. admin keeps every user in one place with authSource=admin everywhere; a user in its own database keeps each application's accounts next to its data. Choose one convention and use it consistently, because authSource has to match it.

What is the difference between readWrite and dbOwner?

readWrite lets a user read and change data and manage collections and indexes. dbOwner adds dbAdmin and userAdmin, which means it can also drop the database and create users with any role on it. An application needs the first and should never hold the second.

How do I see all MongoDB users on a server?

Run db.adminCommand({ usersInfo: { forAllDBs: true } }) as a user with permission to view users. show users only lists users created in the database you are currently in, so it looks incomplete if you have users in more than one place.

Can I give a user read-only access to just some collections?

Not with a built-in role - read covers every non-system collection in the database. Create a custom role with find on each collection you want visible and grant that instead. Collections not listed stay invisible to queries from that user.

Do role changes need a restart or a reconnect?

No. On a standalone mongod, granting or revoking a role applies at once, even to connections that are already open. In a sharded cluster, mongos may take up to userCacheInvalidationIntervalSecs - 30 seconds by default - to notice.

Is a strong password enough if the application user has root?

No. The password protects against guessing, not against the application being tricked into running a query it should not, or its environment file leaking. Limiting the role limits the damage in both cases, which is the part a password cannot do.