MongoDB Enterprise LDAP Integration






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_27017


Step 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

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