You create a MongoDB user with db.createUser, run against the database the user should belong to, and give it the narrowest role that still lets the app work.
The command itself takes about thirty seconds. The part that takes longer is deciding which database the user lives in and which role it gets, because MongoDB scopes both, and getting either wrong produces an auth error that reads like a password problem when it is really a permissions problem.
Table of contents
- The command, and the two things it depends on
- Authentication database vs role database
- Picking a role that is not root
- Multiple roles and multiple databases
- Changing your mind afterwards
- Authentication has to actually be switched on
- The operational version of all this
- How this fits the rest of the stack
- FAQ
The command, and the two things it depends on
Here is the whole thing. Switch to the database the user should be created against, then call db.createUser.
use appdb
db.createUser({
user: "app_service",
pwd: passwordPrompt(),
roles: [ { role: "readWrite", db: "appdb" } ]
})
Two things in that snippet decide everything that happens afterwards. The use appdb line sets the user’s authentication database — the place MongoDB will look when this user tries to log in. The db field inside roles sets where the permission applies. They are usually the same database, which is exactly why people assume they are one setting and then get confused when they are not.
Use passwordPrompt() rather than typing the password inline. An inline password lands in your shell history, and shell history has a way of outliving the credential it contains.
Authentication database vs role database
This is the distinction that generates most of the confusion, so it is worth being blunt about it.
The authentication database is where the user record is stored. When the client connects, it has to say which database to authenticate against. If the user was created in appdb but the connection string says authSource=admin, MongoDB looks in admin, finds nothing, and rejects the login. The password was fine. The lookup was in the wrong place.
The role database is what the user can touch. A user stored in admin can hold a readWrite role on appdb — that is a normal and often good arrangement, because it centralises the user records while keeping the permissions narrow.
The practical rule: whatever database you ran use against before createUser, that string has to appear as authSource in the connection URI.
mongodb://app_service:PASSWORD@host:27017/appdb?authSource=appdb
Picking a role that is not root
MongoDB ships a set of built-in roles, and the temptation is to reach for the biggest one because it makes the error go away. It does. It also means a compromised application credential can drop every database on the cluster.
The ones worth knowing:
read— queries only. Correct for reporting jobs, analytics readers, and anything with a dashboard attached.readWrite— queries plus inserts, updates, and deletes on one database. This is the default answer for an application service account.dbAdmin— index and collection management, schema operations, statistics. Notably it does not include reading the data, which surprises people.userAdmin— creates and modifies users on that database. Keep this away from application accounts entirely.dbOwner— the combination ofreadWrite,dbAdmin, anduserAdmin. Convenient, and a bigger blast radius than most services need.root— everything, cluster-wide. For your break-glass admin account and nothing else.
A service that reads and writes its own collections wants readWrite on one database. That is it. If a migration needs to create indexes, run the migration under a separate account with dbAdmin rather than permanently upgrading the service account to cover an operation that happens twice a year.
Multiple roles and multiple databases
The roles array takes as many entries as you need, and each one carries its own database scope. A job that reads from one database and writes to another is a legitimate shape.
use admin
db.createUser({
user: "etl_worker",
pwd: passwordPrompt(),
roles: [
{ role: "read", db: "events" },
{ role: "readWrite", db: "warehouse" }
]
})
Created in admin, so the connection uses authSource=admin. Reads from events, writes to warehouse, and cannot do anything at all to the other databases on the cluster. That is the shape to aim for: explicit about what it touches, silent about everything else.
The shorthand form roles: ["readWrite"] applies the role to the current database. It works, and it reads ambiguously six months later. Write the object form and let the config say what it means.
Changing your mind afterwards
You will get a role wrong. The commands to fix it without deleting and recreating the user:
// add a role to an existing user
db.grantRolesToUser("app_service", [ { role: "read", db: "analytics" } ])
// take one away
db.revokeRolesFromUser("app_service", [ { role: "read", db: "analytics" } ])
// replace the password
db.changeUserPassword("app_service", passwordPrompt())
// see what a user currently holds
db.getUser("app_service")
Run these against the user’s authentication database, not against the database the role points at. Same trap as before, wearing a different hat.
db.getUser before and after any grant is a cheap habit. It takes a second and it turns a guess about the current state into a fact.
Authentication has to actually be switched on
Here is the one that turns a security review into a bad afternoon. MongoDB does not enforce authentication by default. A fresh mongod with no --auth flag and no security.authorization setting in the config will happily accept every connection, users and roles present or not.
So you can create a beautifully scoped user, connect with the wrong password, and get in anyway. The user existed. The enforcement did not.
# /etc/mongod.conf
security:
authorization: enabled
Restart, then verify by connecting with deliberately wrong credentials. If that connection succeeds, authorization is still off and everything above is decoration.
Bind the listener too. A database reachable on 0.0.0.0 with authentication enabled is one credential leak away from being someone else’s database. A database reachable only from your application’s private network is a much smaller target.
The operational version of all this
Every step above is a thing that can be skipped under deadline pressure and then discovered later by someone reading logs at an unhelpful hour. The authorization flag is off. The service account is running as root because that made the connection error go away in week one. The password is in a compose file that got committed.
The alternative is having those defaults set before you get there. A managed database on RunxBuild starts with authentication enforced, credentials injected as environment variables rather than typed into config files, and a listener that is not open to the internet. You still create application users with the roles your app needs — but the base you are creating them on top of is not quietly permissive.
Point the connection string at the managed instance, keep the readWrite-on-one-database discipline for your service accounts, and the credential handling stops being a thing you have to remember.
How this fits the rest of the stack
Database users are one line of a bigger bill. The connection limits, the storage, the backups, and the service that talks to it all have their own numbers, and the sum is what the project actually costs. The RunxBuild hosting calculator puts those line items on one page so you can model them before committing.
Useful related references:
- Install MongoDB on Ubuntu: Official Repo, systemd, and 7.0 Setup
- MongoDB push: Append to Arrays Without Growing Documents Forever
- Mongo Express: The Web Admin for MongoDB That Shouldn’t Run in Production
- Databases on RunxBuild
FAQ
What is the difference between the authentication database and the role database in MongoDB?
The authentication database is where the user record is stored and is what the client authenticates against — it must match the authSource in your connection string. The role database is what the user is permitted to access. A user stored in admin can hold a readWrite role scoped to appdb. They are frequently the same database, which is why the distinction only becomes visible when it breaks.
Why does my MongoDB user get an authentication failed error with the correct password?
Almost always the authSource. If the user was created after use appdb, the connection string needs authSource=appdb. Point it at admin and MongoDB searches admin, finds no such user, and returns an authentication error that looks exactly like a wrong password.
Which role should an application service account get?
readWrite scoped to the single database the application owns. Not dbOwner, not root. If a migration needs to create indexes, run it under a separate account holding dbAdmin rather than permanently widening the service account for an operation that runs rarely.
Do I have to delete a user to change their roles?
No. Use db.grantRolesToUser and db.revokeRolesFromUser against the user’s authentication database, and db.changeUserPassword to rotate the credential. Run db.getUser afterwards to confirm the result rather than assuming it.
Does creating a user turn on MongoDB authentication?
No, and this catches people. Authorization is a server setting — security.authorization: enabled in mongod.conf, or the —auth flag. Without it, MongoDB accepts every connection regardless of which users exist. Test it by connecting with a deliberately wrong password; if that works, enforcement is off.