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.
Learning objectives
Explain MongoDB role-based access control as resource + action privileges, including inherited roles and union semantics.
Compare built-in roles with a custom role whose privileges match one AtlasMart service responsibility.
Create a least-privilege SCRAM service account and prove permitted and forbidden operations.
Inspect role/user definitions without reading internal system collections directly.
Rotate a service password and verify that the old credential stops working before the new credential is accepted.
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.
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.
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.
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.
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.
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
-
rolesInfoshows only the intended resources/actions fororderService. - 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.
docker rm -f atlasmart-ch22-l2 2>/dev/null || truedocker volume rm atlasmart-ch22-l2-db 2>/dev/null || true
Check your understanding
- What is a MongoDB privilege?
- Can a narrow role cancel a broad role already granted to the same user?
- Why are negative authorization tests important?
- When is a built-in role preferable to a custom role?
- 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.
- MongoDB Security
- Security Checklist for Self-Managed Deployments
- Localhost Exception in Self-Managed Deployments
- Role-Based Access Control in Self-Managed Deployments
- SCRAM Authentication
- createUser Command
- Built-In Roles
- Privilege Actions
- User-Defined Roles
- Configure MongoDB Instances for TLS
- Connection String TLS Options
- Self-Managed Internal/Membership Authentication
- Rotate Keys for Self-Managed Replica Sets
- rotateCertificates Command
- IP Binding in Self-Managed Deployments
- Auditing
- Configure Audit Filters
- System Event Audit Messages
- Atlas Database Auditing
- MongoDB 8.3 Release Notes
- mongosh Release Notes
- PyMongo Release Notes