Translate application responsibilities into explicit resources and actions, then prove least privilege with positive and negative authorization tests.

Users, Roles, Privileges, Built-In Roles, Custom Roles, and Least-Privilege Service Accounts

AtlasMart now has order, reporting, and support services. Giving every service readWrite on the whole database would make a compromised service far more powerful than its business responsibility requires.

Advanced120–190 minutesLeast-privilege RBAC labMongoDB 8.3.8 · mongosh 2.10.0 · PyMongo 4.17.0Last reviewed: September 2026

Learning objectives

01

Explain MongoDB role-based access control as resource + action privileges, including inherited roles and union semantics.

02

Compare built-in roles with a custom role whose privileges match one AtlasMart service responsibility.

03

Create a least-privilege SCRAM service account and prove permitted and forbidden operations.

04

Inspect role/user definitions without reading internal system collections directly.

05

Rotate a service password and verify that the old credential stops working before the new credential is accepted.

Reproducible lab baseline

This lesson pins MongoDB Community Server 8.3.8 with mongodb/mongodb-community-server:8.3.8-ubuntu2204-slim, mongosh 2.10.0, and PyMongo 4.17.0 where driver behavior is useful. The mandatory path is free/local and disposable. Topology: one disposable standalone named atlasmart-ch22-l2. Access control is enabled. Authorization is the focus; TLS is introduced in Lesson 3, so this lab remains loopback-only and uses synthetic credentials. The database is never published on an untrusted interface; any host mapping is loopback-only on port 27176. Authentication/TLS state is stated next to each experiment rather than assumed. Default read/write concern and primary read preference are used unless an example says otherwise. FCV is observed and never changed. Atlas, Enterprise Advanced, KMS, LDAP, Kerberos, OIDC, and commercial SIEM products are optional discussion paths, not mandatory lab dependencies. Secrets shown are synthetic local-only example secrets or generated ephemeral material and must not be reused. Product runtime labs were not executed in the generation environment, so authentication outcomes, certificate fingerprints, rotation timing, network observations, and audit/log records must be measured locally rather than copied as invented output.

1. RBAC is additive: privileges are resource/action pairs

MongoDB uses Role-Based Access Control (RBAC). A privilege pairs a resource—such as one collection, a database, or the cluster—with one or more permitted actions such as find, insert, update, or createIndex. A role can contain direct privileges and can inherit other roles. Roles never subtract access: if one assigned role grants a powerful action, adding a narrower role does not cancel it.

AtlasMart identity Business responsibility Bad shortcut Better scope
order-api read/update orders and reserve inventory readWrite on all of atlasmart custom role on orders + inventory only
report-api read curated reporting collections readAnyDatabase read or collection-scoped custom privileges
deployment admin manage users/roles during deployment application root account separate administrative identity and workflow

2. Build a custom role from the operations the service actually needs

The order service needs to read and insert order records and read/update inventory. It does not need to read payments, drop collections, create users, or modify cluster configuration. The custom role expresses that contract directly. On self-managed MongoDB, roles can be created with db.createRole()/createRole; Atlas custom database roles are managed through Atlas-specific interfaces rather than the self-managed role command.

bootstrap admin, seed data, and create the order-service role
docker rm -f atlasmart-ch22-l2 2>/dev/null || truedocker volume rm atlasmart-ch22-l2-db 2>/dev/null || truedocker run -d --name atlasmart-ch22-l2 \  -p 127.0.0.1:27176:27017 \  -v atlasmart-ch22-l2-db:/data/db \  mongodb/mongodb-community-server:8.3.8-ubuntu2204-slim \  --auth --bind_ip_all --port 27017until docker logs atlasmart-ch22-l2 2>&1 | grep -q "Waiting for connections"; do sleep 1; done# The first administrator is created from inside the server container so the# connection is genuinely local to mongod. Do not depend on Docker NAT for the# localhost exception.docker exec atlasmart-ch22-l2 mongosh --quiet --host 127.0.0.1 --port 27017 --eval '  const a=db.getSiblingDB("admin");  a.createUser({user:"atlasAdmin",pwd:"local-only-change-me",roles:[{role:"root",db:"admin"}],mechanisms:["SCRAM-SHA-256"]});  printjson(a.runCommand({connectionStatus:1}));'mongosh "mongodb://atlasAdmin:local-only-change-me@127.0.0.1:27176/admin?authSource=admin" --quiet --eval 'db.runCommand({ping:1})' mongosh "mongodb://atlasAdmin:local-only-change-me@127.0.0.1:27176/admin?authSource=admin" --quiet --eval '  const app=db.getSiblingDB("atlasmart");  app.orders.insertOne({orderId:"O-2202",tenantId:"tenant-a",status:"created"});  app.inventory.insertOne({sku:"SKU-22",tenantId:"tenant-a",available:10});  app.payments.insertOne({paymentId:"P-22",tenantId:"tenant-a",state:"authorized"});  app.createRole({    role:"orderService",    privileges:[      {resource:{db:"atlasmart",collection:"orders"},actions:["find","insert","update"]},      {resource:{db:"atlasmart",collection:"inventory"},actions:["find","update"]}    ],    roles:[]  });  app.createUser({    user:"orders-api",    pwd:"orders-v1-local-only",    roles:[{role:"orderService",db:"atlasmart"}],    mechanisms:["SCRAM-SHA-256"]  });  printjson(app.runCommand({rolesInfo:"orderService",showPrivileges:true}));  printjson(app.runCommand({usersInfo:"orders-api",showPrivileges:true}));' 

3. Positive tests are necessary; negative tests prove least privilege

A successful allowed operation proves only that the role is sufficient. The stronger least-privilege test also attempts operations that the service must never need. An authorization failure on those operations is a desired signal: it shows the boundary exists where the design says it should.

exercise allowed and denied operations as the service account
URI="mongodb://orders-api:orders-v1-local-only@127.0.0.1:27176/atlasmart?authSource=atlasmart"mongosh "$URI" --quiet --eval '  const app=db.getSiblingDB("atlasmart");  printjson(app.orders.findOne({orderId:"O-2202"}));  printjson(app.inventory.updateOne({sku:"SKU-22",available:{$gte:1}},{$inc:{available:-1}}));  printjson(db.runCommand({connectionStatus:1,showPrivileges:true}));'# Each command below should fail with Unauthorized. Those failures are part of# the acceptance criteria, not test noise.mongosh "$URI" --quiet --eval 'db.getSiblingDB("atlasmart").payments.findOne({})' || truemongosh "$URI" --quiet --eval 'db.getSiblingDB("atlasmart").orders.drop()' || truemongosh "$URI" --quiet --eval 'db.getSiblingDB("admin").createUser({user:"x",pwd:"x",roles:[]})' || true

4. Built-in roles are useful, but convenience can silently widen blast radius

Built-in roles such as read, readWrite, dbAdmin, and administrative roles cover common responsibility groups. They are preferable when their scope actually matches the service. The wrong approach is to grant readWrite on atlasmart because one method failed, or to grant root temporarily and forget to remove it. Because role grants are additive, the extra role remains effective even if a narrow custom role is also present.

Concrete overprivilege test

If orders-api also receives { role: "readWrite", db: "atlasmart" }, its previously denied read of payments becomes legal. The custom role did not become weaker; the identity accumulated another grant. Remove the broad grant and rerun the negative test.

show additive grants, then repair them
const admin = db.getSiblingDB("atlasmart");admin.grantRolesToUser("orders-api", [{role:"readWrite", db:"atlasmart"}]);printjson(admin.runCommand({usersInfo:"orders-api", showPrivileges:true}));// After observing the widened privileges, remove the shortcut again.admin.revokeRolesFromUser("orders-api", [{role:"readWrite", db:"atlasmart"}]);printjson(admin.runCommand({usersInfo:"orders-api", showPrivileges:true}));

5. Rotate credentials as a controlled application change

Password rotation is not just a database command; it is a two-sided deployment. The application must receive the replacement secret, establish new connections, and stop depending on the old credential. In this disposable lab we rotate in one step and verify old/new behavior. In production, coordinate secret distribution and connection-pool replacement so a rotation does not become an outage.

rotate the service password and verify both sides of the cutover
mongosh "mongodb://atlasAdmin:local-only-change-me@127.0.0.1:27176/admin?authSource=admin" --quiet --eval '  db.getSiblingDB("atlasmart").updateUser("orders-api", {pwd:"orders-v2-local-only"});'mongosh "mongodb://orders-api:orders-v1-local-only@127.0.0.1:27176/atlasmart?authSource=atlasmart" --quiet --eval 'db.runCommand({ping:1})' || truemongosh "mongodb://orders-api:orders-v2-local-only@127.0.0.1:27176/atlasmart?authSource=atlasmart" --quiet --eval 'db.runCommand({ping:1})' 

6. Verification, cleanup, and production judgment

  • rolesInfo shows only the intended resources/actions for orderService.
  • The service can read/insert/update orders and read/update inventory.
  • Payments reads, collection drops, and user administration fail.
  • A temporary broad role visibly widens access and revoking it restores the negative tests.
  • The old password fails after rotation and the new password succeeds.

In production, build role tests into deployment verification and treat any unexpected authorization success as a security regression. Review inherited roles, not just role names. Use separate identities for application, deployment, backup, monitoring, and incident response. Prefer SCRAM-SHA-256 or an appropriate managed/enterprise identity mechanism, and protect all password exchange with TLS. The next lesson adds server identity and member-to-member authentication so a correct user cannot be redirected onto the wrong transport endpoint.

cleanup
docker rm -f atlasmart-ch22-l2 2>/dev/null || truedocker volume rm atlasmart-ch22-l2-db 2>/dev/null || true

Check your understanding

  1. What is a MongoDB privilege?
  2. Can a narrow role cancel a broad role already granted to the same user?
  3. Why are negative authorization tests important?
  4. When is a built-in role preferable to a custom role?
  5. What must credential rotation verify besides updateUser succeeding?
Review the answers

1. A resource plus one or more permitted actions.

2. No. Role grants are additive; the effective privileges are the union of direct and inherited grants.

3. They prove operations outside the service responsibility are actually denied instead of merely assuming the role is narrow.

4. When its resource/action scope accurately matches the service responsibility without unnecessary privileges.

5. The application can authenticate with the replacement secret and the old secret is no longer accepted, with connection-pool cutover handled operationally.

Authoritative references

Version-sensitive behavior in this lesson was checked against current MongoDB documentation at generation time. Re-check these sources before applying the procedures to a later server or managed-service release.

Keep knowledge open

Help the academy stay free and grow.

If these tutorials save you time, a small donation supports new lessons, technical review, diagrams, examples, and long-term maintenance.

ETHEthereum / ERC-20 only
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0

Send only Ethereum or ERC-20 compatible assets to this address.