LDAP, Kerberos and x.509 authentication
Connect self-hosted MongoDB to corporate identity: authenticate users against LDAP or Kerberos, map directory groups to MongoDB roles, and use x.509 certificates for services. Covers the $external database, configuration, testing and the failure modes that lock people out.
- Explain the $external database and the PLAIN, GSSAPI and MONGODB-X509 mechanisms
- Configure LDAP authentication and LDAP authorization with group-to-role mapping
- Configure Kerberos with a keytab and connect with GSSAPI
- Plan for directory outages and keep a local emergency account
conceptThe concept
Users created with db.createUser in a normal database are internal users with SCRAM credentials stored in MongoDB. External users live in the virtual $external database: MongoDB stores no password and asks another system to verify the identity.
- LDAP (mechanism
PLAIN): the client sends the username and password to mongod over TLS, and mongod verifies them by binding to the directory (or throughsaslauthd). - Kerberos (mechanism
GSSAPI): the client presents a ticket from the KDC; mongod validates it with its service keytab. No password reaches mongod. - x.509 (mechanism
MONGODB-X509): the client certificate's subject is the username. Good for services and for cluster members (security.clusterAuthMode: x509).
LDAP authorization goes one step further: instead of creating each user in $external with roles, mongod queries the directory for the user's groups (security.ldap.authz.queryTemplate) and grants the roles defined in the admin database whose names are the group DNs. Joining or leaving a directory group then changes database access with no change in MongoDB.
Editions: LDAP and Kerberos are available in MongoDB Enterprise and in Percona Server for MongoDB (free). MongoDB Community supports SCRAM and x.509 only.
whyWhy it matters
Security teams require central identity: one place to disable a leaver, enforce password policy and MFA, and audit access. Local database users created by hand are exactly what audits flag. Getting the mapping right also means DBAs stop creating and deleting users for every team change.
Use caseReal-world scenario
A bank runs Percona Server for MongoDB on its own hosts. Analysts log in with their Active Directory accounts over LDAP; their access comes from AD groups such as cn=mongo-readonly,ou=groups,dc=bank,dc=local, mapped to MongoDB roles. Batch jobs authenticate with Kerberos keytabs, cluster members use x.509, and one local SCRAM admin account, stored in the vault, exists for the day the directory is unreachable.
Try itExamples
Run and edit these in the interactive lesson.
const ext = db.getSiblingDB("$external")
if (!ext.getUser("asha@EXAMPLE.COM")) ext.createUser({ user: "asha@EXAMPLE.COM", roles: [{ role: "read", db: "shop" }] })
if (!ext.getUser("CN=billing-svc,OU=services,O=Example")) ext.createUser({ user: "CN=billing-svc,OU=services,O=Example", roles: [{ role: "readWrite", db: "shop" }] })
ext.getUsers().users.map(u => u.user)const admin = db.getSiblingDB("admin")
const dn = "cn=mongo-dba,ou=groups,dc=example,dc=com"
if (!admin.getRoles().some(r => r.role === dn)) admin.createRole({ role: dn, privileges: [], roles: [{ role: "clusterMonitor", db: "admin" }, { role: "dbAdmin", db: "shop" }] })
admin.getRoles().filter(r => r.role.startsWith("cn=")).map(r => ({ role: r.role, grants: r.roles.map(x => x.role + "@" + x.db) }))security:
authorization: enabled
ldap:
servers: "ldap1.example.com,ldap2.example.com"
transportSecurity: tls
bind:
method: simple
queryUser: "cn=mongo-bind,ou=services,dc=example,dc=com"
queryPassword:
__exec: "/usr/local/bin/read-secret ldap-bind" # keep the secret out of the file
userToDNMapping: '[{ match: "(.+)", substitution: "uid={0},ou=people,dc=example,dc=com" }]'
authz:
queryTemplate: "{USER}?memberOf?base"
setParameter:
authenticationMechanisms: "PLAIN,SCRAM-SHA-256" # keep SCRAM for the emergency admin
# connect as a directory user
mongosh "mongodb://asha@db1.example.com:27017/?authMechanism=PLAIN&authSource=%24external&tls=true"# on the server: principal mongodb/db1.example.com@EXAMPLE.COM in the keytab
KRB5_KTNAME=/etc/mongodb/mongodb.keytab mongod --config /etc/mongod.conf
# setParameter:
# authenticationMechanisms: "GSSAPI,SCRAM-SHA-256"
# on the client
kinit asha@EXAMPLE.COM
mongosh "mongodb://asha%40EXAMPLE.COM@db1.example.com:27017/?authMechanism=GSSAPI&authSource=%24external&tls=true"# the certificate subject must match the $external user exactly
openssl x509 -in billing-svc.pem -noout -subject -nameopt RFC2253
# subject=CN=billing-svc,OU=services,O=Example
mongosh "mongodb://db1.example.com:27017/?authMechanism=MONGODB-X509&authSource=%24external&tls=true&tlsCertificateKeyFile=billing-svc.pem&tlsCAFile=ca.pem"db.runCommand({ connectionStatus: 1 })internalsUnder the hood
With LDAP, security.ldap.userToDNMapping turns the login name into a DN (for example asha into uid=asha,ou=people,dc=example,dc=com), and the authorization query returns group DNs. Results are cached; ldapUserCacheInvalidationInterval (30 seconds by default) controls how quickly group changes take effect. Every new connection that authenticates can cause an LDAP round trip, so connection storms become directory load: keep connection pools warm and point mongod at more than one LDAP server.
Kerberos needs a service principal mongodb/<fqdn>@REALM in the keytab, forward and reverse DNS that match the host name clients use, and clocks within the KDC's allowed skew (five minutes by default).
Avoid / preferDo this, not that
setParameter: authenticationMechanisms: "PLAIN"
setParameter: authenticationMechanisms: "PLAIN,SCRAM-SHA-256" # plus one local admin user whose password lives in the vault
If the directory is unreachable (network change, expired bind password, certificate rotation), PLAIN-only means nobody can log in to fix anything.
ldap: servers: "ldap.example.com" transportSecurity: none
ldap: servers: "ldap1.example.com,ldap2.example.com" transportSecurity: tls
Without TLS, user passwords travel from mongod to the directory in clear text, and a single server makes every login depend on one host.
PitfallsCommon mistakes
- Enabling PLAIN without TLS. The password travels to mongod in clear text.
- Removing SCRAM from authenticationMechanisms, so nobody can log in when the directory is down.
- Naming the role after the group's short name instead of the full DN returned by the authorization query.
- Connecting to Kerberos-enabled servers by IP address or an alias, so the service principal does not match and authentication fails.
LimitsWhen not to use it
For a small team or a single application, SCRAM users with strong generated passwords and x.509 for services may be enough, and they work in Community Edition. Directory integration adds a dependency on the directory's availability and latency; it pays off when there are many human users or strict joiner-and-leaver requirements.
SecuritySecurity notes
The LDAP bind account should only be able to read users and groups. Store its password with a config expansion (__exec or __rest) rather than in mongod.conf. Protect keytabs like private keys (owner mongod, mode 600). Enable auditing of authentication events so failed logins from directory users are visible.
ChallengeTry it yourself
Create (only if missing) an admin role for the directory group cn=mongo-readonly,ou=groups,dc=example,dc=com that grants read on shop, then return that role's inherited roles.
Show one solution
const admin = db.getSiblingDB("admin")
const dn = "cn=mongo-readonly,ou=groups,dc=example,dc=com"
if (!admin.getRoles().some(r => r.role === dn)) admin.createRole({ role: dn, privileges: [], roles: [{ role: "read", db: "shop" }] })
admin.getRoles().filter(r => r.role === dn)[0].rolesInterviewQuestions you should be able to answer
How do you integrate self-hosted MongoDB with Active Directory?
Use MongoDB Enterprise or Percona Server for MongoDB. Configure security.ldap with several AD servers over TLS, a least-privilege bind account whose password comes from a secret store, userToDNMapping for the login format, and authz.queryTemplate to read group membership. Create roles in admin named after the group DNs with the privileges each group needs. Enable PLAIN plus SCRAM, keep one vaulted local admin for directory outages, require TLS on client connections, and test with a real user before rolling out, then watch connection authentication latency because each new connection may query AD.
An LDAP user can log in but gets "not authorized" on every command. What do you check?
Run db.runCommand({ connectionStatus: 1 }) as that user: if no roles appear, authorization mapping is the problem. Check that the authorization query returns the expected group DNs (MongoDB Enterprise ships mongoldap to test the configuration), that a role named exactly with that DN exists in admin (case and spacing matter), and that the user really is in the group. Remember the user cache: group changes can take up to ldapUserCacheInvalidationInterval to apply.
QuizCheck your understanding
5 questions with instant feedback in the interactive lesson; a wrong answer goes into your review deck.