The MongoDB Role Pitfalls That Bit Me: Lessons in Multi-Customer Access Control
TL;DR:
- ❌ Global roles don't auto-scope to each customer databases
- 🎯 Create roles per customer database
- 🔐 Define privileges exactly where they apply
- 🤖 Automate role/user creation for scale
- 🔄 Always restart after config changes
🔍 Intro: "I thought I understood MongoDB access control... until I didn't."
I'd just published a guide on setting up MongoDB users and custom roles. See the tutorial-style primer on Dev.to version or Medium version. It covered the commands, the theory, and a neat walkthrough.
Confidently, I thought: “Great—now I can lock down any MongoDB instance.” But when I tried to adapt that for a multi-customer, multi-module SaaS environment, things fell apart in unexpected ways.
In this post, I'll share exactly what I tried, why it didn't work, and how I fixed it. If you're building a MongoDB setup where each customer has its own database and each module/service should only see its own collections, these lessons will save you headaches. And if you haven't read the tutorial-style primer yet, check it out here:
🏗️ What I Was Trying to Build
-
Architecture in simple terms:
- A SaaS-like application where each customer has its own database.
- Within each customer database, multiple modules/services (e.g., orders, products, analytics) each have dedicated collections.
- Each module (i.e., the code/service) uses a MongoDB user to access only its own collections for that specific customer.
End goal: When Service A runs for Customer X, it connects with credentials that allow only the “CustomerX*ServiceA” collections. It must _never* access ServiceB collections or CustomerY data.
Initial idea: Create roles at the admin level—one role per module/service that spans all customer databases. Then, for each customer, assign that role scoped to the customer database. I assumed that specifying
{ role: "<moduleRole>", db: "<customerDb>" }
indb.createUser()
would filter the module role's privileges to that customer database.
In short: I thought I could define one “ServiceA” role globally, then assign it with db= to restrict it to that DB. Spoiler: that assumption was wrong.
⚠️ The Misunderstanding: “db” in createUser
Is Where the Role Lives, Not Its Scope Filter
Initial Plan (Wrong assumption)
// Pseudocode for each module role created once under admin database:
use admin;
db.createRole({
role: "serviceA",
privileges: [
// Suppose we try to list resource patterns under many DBs:
{ resource: { db: "customer1_db", collection: "serviceA_*" }, actions: ["find", "insert", "update", "remove"] },
{ resource: { db: "customer2_db", collection: "serviceA_*" }, actions: ["find", "insert", "update", "remove"] },
// ...and so on for each customer
],
roles: []
});
Then for user creation:
use customerX_db;
db.createUser({
user: "customerX_serviceA_user",
pwd: passwordPrompt(),
roles: [
{ role: "serviceA", db: "customerX_db" }
]
});
I assumed:
“By specifying
db: "customerX_db"
, the serviceA role's privileges get filtered to customerX_db only.”
Reality: Role's db
parameter is where the role is defined, not a filter of where it applies.
- The
{ role: "...", db: "customerX_db" }
increateUser
means “look for a role named ‘serviceA' in thecustomerX_db
database.” It does not say “apply the global serviceA role only in this DB.” - If “serviceA” role was created under
admin
(or any other DB), referencing it undercustomerX_db
fails (role not found), or if found under another DB, privileges don't magically filter to the user's connect-DB context. - MongoDB's RBAC: A role's actions/resources are defined exactly where you create the role. Assigning that role to a user requires you refer to that same DB where the role exists.
Lesson: You cannot “create once globally and assign with filtering by specifying db in createUser.” The
db
is the namespace of the role definition.
🔄 How I Fixed It: Role Creation Per Customer Database
Approach:
- For each customer database, create distinct roles for each module/service.
- When creating the user for a module in that customer DB, reference the role in the same DB.
Example Flow (pseudo-automation script style):
// For each customer (e.g., customerX_db):
use customerX_db;
// 1. Create or update role for ServiceA in this customer DB:
const roleName = "serviceA"; // consider prefixing with module name or include customer in name if desired
db.createRole({
role: roleName,
privileges: [
{
resource: { db: "customerX_db", collection: "serviceA_orders" },
actions: ["find", "insert", "update", "remove"]
},
// Note: MongoDB privileges require exact collection names; wildcards aren't supported here, so list each collection explicitly.
],
roles: []
});
// 2. Create user for ServiceA in this customer DB:
db.createUser({
user: "customerX_serviceA_user",
pwd: passwordPrompt(),
roles: [
{ role: roleName, db: "customerX_db" }
]
});
- You repeat this for ServiceB, ServiceC, etc., in each customer DB.
- If you have many customers, script this process (e.g., a Node.js or Bash script that loops customer list).
Why This Works:
- Role is defined in the same database where its privileges apply, so no confusion.
- When the service connects with
mongodb://customerX_serviceA_user:pwd@host:27017/customerX_db?authSource=customerX_db
, it gets only the privileges defined in that DB's role.
Tip: If you have common privilege patterns per module, template the JSON structure and programmatically fill in the
db
andcollection
names per customer.
🔧 Bonus Pitfall: Enabling Authentication in mongod.cfg
-
What happened: After editing
mongod.cfg
to add:
# In mongod.cfg, ensure 'security:' is at root indentation level; YAML is indentation-sensitive.
security:
authorization: enabled
I tried connecting with the new users, but MongoDB still allowed unauthenticated access.
- Root cause: Didn't restart the MongoDB service/process after config change.
-
Fix: Always restart or reload the
mongod
service after modifying the config file.- On Windows, use Services UI or
net stop MongoDB
,net start MongoDB
(or the equivalent PowerShell commands). - On Linux,
sudo systemctl restart mongod
.
- On Windows, use Services UI or
Lesson: Authentication “not working” is often because the config change isn't live yet.
📈 What I Learned (and You Should Know)
-
Understand that the
db
field specifies where roles live, not their access scope.- The
db
field increateRole
and in user'sroles
assignment refers to where the role is defined, not a dynamic filter. Always create roles in the DB where they should apply.
- The
-
Multi-customer (multi-DB) adds complexity
- One-size-fits-all global roles often don't work. Better to script role/user creation per customer DB.
-
Automate repetitive setup
- If you have tens or hundreds of customers, manually creating roles/users is error-prone. Build scripts or use Infrastructure-as-Code.
-
Test early and often
- Spin up a staging customer database, create roles/users, verify permissions before rolling out widely.
-
Restart after config changes
- Simple but easily overlooked; always confirm
authorization: enabled
is in effect.
- Simple but easily overlooked; always confirm
✅ Conclusion & Next Steps
-
Key takeaway: Don't assume MongoDB will scope a global role by specifying a different
db
at user-creation time. Define roles in the exact database where their privileges apply. (For the basic tutorial on custom roles, see My previous MongoDB ACL tutorial on Dev.to.) - Call to action: Have you tackled MongoDB RBAC in a multi-DB environment? What strategies or scripts did you use? Share your approaches or questions in the comments below!
Top comments (0)