Series: Centralized DB Authentication in the Wild | Part 4 of 9
Introduction
- In Part 3, PostgreSQL authenticated through LDAP while ldap2pg kept local roles synchronized with LDAP group membership.
- In this article we will look at how we can integrate same thing with MongoDB Enterprise.
Note
- MongoDB Enterprise 7.0 runs in Docker on db1.dbacraft.com. The OpenLDAP directory from Part 2 continues to run on iam.dbacraft.com.
- LDAP authentication and authorization is deprecated starting MongoDB 8.0, still functional through the 8.x lifecycle, removed in a future major release. This series uses 7.0.
- Always test authentication changes in a non-production environment first.
Demo environment
| Component | Host | Detail |
|---|---|---|
| OpenLDAP | iam.dbacraft.com | osixia/openldap:1.5.0, port 389 |
| MongoDB | db1.dbacraft.com | mongodb/mongodb-enterprise-server:7.0.26, port 27017, container: dbacraft_mongo_27017 |
| Network | db1.dbacraft.com | Update mongod.conf LDAP block to match your network topology |
Step 1: Deploy MongoDB Enterprise
LDAP authentication requires MongoDB Enterprise. The official Enterprise image is published under mongodb/mongodb-enterprise-server on Docker Hub. This is solely for evaluation purpose.
# Run on: db1.dbacraft.com version: "3.8" services: mongodb: image: mongodb/mongodb-enterprise-server:7.0.26 container_name: dbacraft_mongo_27017 hostname: db1.dbacraft.com restart: unless-stopped ports: - "27017:27017" volumes: - /data/mongo/mongodata:/data/db - /log/mongo:/var/log/mongodb - /audit/mongo:/var/log/mongo-audit - /data/mongo/conf/mongod.conf:/etc/mongod.conf command: ["mongod", "--config", "/etc/mongod.conf"] logging: driver: json-file options: max-size: "5m" max-file: "2"
root@db1:/script/mongo# docker compose up -d
root@db1:/script/mongo# docker ps -a | grep dbacraft_mongo_27017
853e2c166262 mongodb/mongodb-enterprise-server:7.0.26 "python3 /usr/local/…" 4 days ago Up 4 minutes 0.0.0.0:27017->27017/tcp, [::]:27017->27017/tcp dbacraft_mongo_27017Step 2: Create the local administrative user
Before enabling LDAP, create a local administrator on the admin database. This account is the break-glass path if LDAP becomes unavailable.
root@db1:~# mongosh --host 127.0.0.1 --port 27017
test> use admin
switched to db admin
admin> db.createUser({
user: "svc_admin_local",
pwd: "********",
roles: [ { role: "root", db: "admin" } ]
})
{ ok: 1 }
Confirm the user exists, then exit and restart the container with authorization enabled.
admin> db.getUsers()
{
users: [
{ _id: 'admin.svc_admin_local', user: 'svc_admin_local', db: 'admin', roles: [ { role: 'root', db: 'admin' } ] }
]
}
Step 3: Configure mongod.conf for LDAP
MongoDB uses a single security.ldap block for both authentication and authorization. Unlike PostgreSQL's pg_hba.conf, there is no per-rule matching by source network. LDAP applies globally once PLAIN is added to the authentication mechanism list.
# Path: /data/mongo/conf/mongod.conf # Run on: db1.dbacraft.com storage: dbPath: /data/db systemLog: destination: file path: /var/log/mongodb/mongod.log logAppend: true net: bindIp: 0.0.0.0 port: 27017 security: authorization: enabled ldap: servers: "iam.dbacraft.com:389" transportSecurity: none bind: method: "simple" queryUser: "uid=svc_ldap_bind_ro,ou=people,dc=dbacraft,dc=com" queryPassword: "********" userToDNMapping: '[{match: "(.+)", substitution: "uid={0},ou=people,dc=dbacraft,dc=com"}]' authz: queryTemplate: "ou=groups,dc=dbacraft,dc=com??sub?(member={USER})" auditLog: destination: file path: /var/log/mongo-audit/audit.json format: JSON setParameter: authenticationMechanisms: "PLAIN,SCRAM-SHA-256"
| Parameter | Value | Purpose |
|---|---|---|
| servers | iam.dbacraft.com:389 | LDAP server and port |
| bind.queryUser | uid=svc_ldap_bind_ro | Same read-only bind account used in Part 3 |
| userToDNMapping | uid substitution | Builds the bind DN from the usernam |
| authz.queryTemplate | ou=groups??sub?(member={USER}) | Searches groups for entries listing the user as member |
| authenticationMechanisms | PLAIN,SCRAM-SHA-256 | PLAIN carries the LDAP credential exchange. SCRAM-SHA-256 keeps local accounts working |
[WARNING] PLAIN sends the password to MongoDB in plaintext so it can be forwarded to LDAP. Use TLS on the client connection in production environment.
Restart the container to apply the configuration.
root@db1:/script/mongo# docker compose restart mongodb
Step 4: Create roles that match LDAP group DNs
This step has no PostgreSQL equivalent. PostgreSQL authenticates through LDAP and checks locally created roles by name. MongoDB skips the name mapping: the role on admin must be named with the exact distinguished name of the LDAP group.
Connect using the local administrator from Step 2.
root@db1:~# mongosh --host 127.0.0.1 --port 27017 \
-u svc_admin_local -p '********' --authenticationDatabase admin
admin> use admin
switched to db admin
admin> db.createRole({
role: "cn=db-admins,ou=groups,dc=dbacraft,dc=com",
privileges: [],
roles: [ { role: "dbAdminAnyDatabase", db: "admin" },
{ role: "readWriteAnyDatabase", db: "admin" } ]
})
{
role: 'cn=db-admins,ou=groups,dc=dbacraft,dc=com',
db: 'admin',
privileges: [],
roles: [
{ role: 'dbAdminAnyDatabase', db: 'admin' },
{ role: 'readWriteAnyDatabase', db: 'admin' }
]
}
admin> db.createRole({
role: "cn=developers,ou=groups,dc=dbacraft,dc=com",
privileges: [],
roles: [ { role: "readWrite", db: "app_orders" } ]
})
{ ok: 1 }
admin> db.createRole({
role: "cn=bi-users,ou=groups,dc=dbacraft,dc=com",
privileges: [],
roles: [ { role: "read", db: "app_orders" } ]
})
{ ok: 1 }
The role name is the literal LDAP group DN, character for character, including case. A mismatched OU spelling or a stray space breaks the mapping silently. No error at role creation, no error at login, the user just ends up with no privileges.
Step 5: Verify LDAP authentication and authorization
Connect as an LDAP user with the PLAIN mechanism against $external.
root@db1:~# mongosh --host db1.dbacraft.com --port 27017 \
--authenticationMechanism PLAIN \
--authenticationDatabase '$external' \
-u user_ahmad_dba -p '********'
Current Mongosh Log ID: 691f2a3c8e9d1b0044e7c112
Connecting to: mongodb://db1.dbacraft.com:27017/?authMechanism=PLAIN&authSource=%24external
Using MongoDB: 7.0.14
Using Mongosh: 2.2.10
db1 [direct: primary] test> db.runCommand({ connectionStatus: 1 })
{
authInfo: {
authenticatedUsers: [ { user: 'user_ahmad_dba', db: '$external' } ],
authenticatedUserRoles: [
{ role: 'cn=db-admins,ou=groups,dc=dbacraft,dc=com', db: 'admin' }
]
},
ok: 1
}
Authentication and authorization happened in one step. No sync process, no scheduled job, nothing resembling ldap2pg running in the background. MongoDB queries LDAP for group membership at connection time, every time.
Confirm the privileges apply.
db1 [direct: primary] test> use app_orders
switched to db app_orders
app_orders> db.orders.insertOne({ order_id: 1001, status: "test" })
{
acknowledged: true,
insertedId: ObjectId('691f2a5f8e9d1b0044e7c113')
}
Step 6: Comparing the authorization model against Postgres
| Behavior | PostgreSQL + ldap2pg | MongoDB native LDAP |
|---|---|---|
| Role creation | Individual role created per LDAP user at sync time | No per-user role. Group role attaches at connection time only |
| Group to role mapping | Configured in ldap2pg.yml, name is arbitrary | Role name must equal the LDAP group DN exactly |
| Propagation of LDAP changes | Only after the next scheduled sync run | Immediate on the user's next connection |
| Offboarding an active session | ldap2pg terminates the session and drops the role during sync | Existing session keeps its roles until it disconnects and reconnects |
| Auditability of role objects | pg_roles lists every synced user as a real role | db.getRoles() lists the role definitions, not who holds them. Finding which users belong to a group means querying LDAP directly with ldapsearch or adquery |
Step 7: Verify offboarding behavior
Remove user_ahmad_dba from db-admins in LDAP, the same action taken in Part 3 for Postgres Auth.
root@iam:/opt/auth/ldap# ldapmodify -x -H ldap://localhost:389 \
-D "cn=admin,dc=dbacraft,dc=com" -W << 'EOF'
dn: cn=db-admins,ou=groups,dc=dbacraft,dc=com
changetype: modify
delete: member
member: uid=user_ahmad_dba,ou=people,dc=dbacraft,dc=com
EOF
Enter LDAP Password:
modifying entry "cn=db-admins,ou=groups,dc=dbacraft,dc=com"
root@db1:~# mongosh --host db1.dbacraft.com --port 27017 \
--authenticationMechanism PLAIN \
--authenticationDatabase '$external' \
-u user_ahmad_dba -p '********'
db1 [direct: primary] test> db.runCommand({ connectionStatus: 1 })
{
authInfo: {
authenticatedUsers: [ { user: 'user_ahmad_dba', db: '$external' } ],
authenticatedUserRoles: []
},
ok: 1
}
On reconnect, MongoDB finds no group membership. The role list is empty. Same end state as PostgreSQL after ldap2pg drops the role, reached through a completely different mechanism.
Production considerations
| Consideration | Detail |
|---|---|
| Transport security | This demo uses port 389 and PLAIN unencrypted. In production, enable TLS on both the LDAP bind and the client to mongod connection. |
| Immediate revocation | Group changes apply on reconnect only. For urgent offboarding, kill the active session with db.currentOp() and db.killOp(). |
| Role DN accuracy | Role names must match LDAP group DNs exactly. |
| No role sync tooling | No MongoDB equivalent to ldap2pg exists. Role objects are created once and left in place. Track them in version control alongside the mongod.conf LDAP block. |
| LDAP HA | If LDAP is unavailable, new $external connections fail. Keep the local admin database user as a break-glass path and implement LDAP HA if single LDAP server goes offline can still be authenticated using next available LDAP server. |
| Deprecation timeline | LDAP is deprecated starting MongoDB 8.0. Plan a migration path to OIDC or x.509 before upgrading past the 7.x line long term. |
Wrapping Up
- MongoDB authenticates and authorizes against LDAP in a single step, no intermediate role sync. That removes the scheduling and drift problems ldap2pg solves for PostgreSQL, but introduces a different one: privilege changes only take effect on reconnect, and role objects must be created manually with exact LDAP group DNs.
- Source files are available on GitHub.
What is next
In Part 5, MySQL Enterprise connects to the same OpenLDAP directory using the PAM authentication plugin. Its own tradeoffs around plugin availability across MySQL editions.
Credits and References
- osixia/openldap Docker container
- mongodb/mongodb-enterprise-server official Docker image
- MongoDB LDAP Authorization documentation
Series: Centralized DB Authentication in the Wild
- Part 1: Why Local Database Authentication Creates Operational Chaos
- Part 2: Building an Enterprise LDAP Directory with OpenLDAP
- Part 3: Integrating PostgreSQL with a Centralized LDAP Directory
- Part 4: MongoDB Enterprise LDAP Integration
- Part 5: MySQL Enterprise LDAP Integration (coming soon)
- Part 6: MariaDB LDAP Authentication using PAM and POSIX Groups (coming soon)
- Part 7: Why LDAP Is Not Enough - Introducing Kerberos (coming soon)
- Part 8: Certificate-Based Authentication Across PostgreSQL, MySQL, MariaDB and MongoDB (coming soon)
- Part 9: Production Best Practices and Cross Platform Comparison (coming soon)

No comments:
Post a Comment