05 · Authentication, Roles & TLS¶
Every lab in this course so far has run without authentication, bound to 127.0.0.1. That is fine on a laptop and
catastrophic anywhere else: for years, automated scanners have found MongoDB servers exposed to the internet with
no authentication, copied or deleted their data, and left ransom notes in its place. None of those attacks needed
an exploit — only a server that accepted connections from anyone and trusted everyone who connected.
This lesson turns on the three defences that matter most — authentication (who are you?), authorization
(what may you do?) and encryption in transit (can anyone read or alter the traffic?) — and shows the exact
errors you'll hit along the way, because most of the real work of securing MongoDB is understanding those errors.
Everything ran on MongoDB 9.0.2 Community with OpenSSL 4.0 for certificates. Passwords in the examples are
throwaway lab values; in real use, prompt for them (passwordPrompt() in mongosh) or load them from a secret store.
Turning on authentication¶
Start mongod with --auth (or security.authorization: enabled in the config file):
There are no users yet, so nothing is permitted:
before any user: Unauthorized - not authorized on shop to execute command { find: "orders", filter: {}, limit: 1, ... }
The localhost exception solves the chicken-and-egg problem: while a server has no users, a client connecting
from the same machine may create one user in the admin database. Make it a user administrator, not an
all-powerful root:
db.getSiblingDB("admin").createUser({user: "ops", pwd: passwordPrompt(),
roles: ["userAdminAnyDatabase", "clusterMonitor"]})
The exception closed the moment the first user existed. From now on, even on localhost, you must authenticate.
Users live in a database¶
A user is defined in an authentication database — admin for administrators, or the application's own
database — and its roles can grant privileges on any database:
const shop = db.getSiblingDB("shop")
shop.createUser({user: "shop_app", pwd: passwordPrompt(), roles: [{role: "readWrite", db: "shop"}]})
Clients must name the authentication database when they connect (authSource, or the path in the URI):
Passwords are stored as SCRAM credentials (salted, iterated hashes), never in plain text:
A failed login is logged with the mechanism and reason — useful when debugging, and worth alerting on in bulk:
"msg":"Failed to authenticate","attr":{"client":"127.0.0.1:54067", ... "mechanism":"SCRAM-SHA-256",
"user":"reporter","db":"shop","error":"AuthenticationFailed: SCRAM authentication failed, storedKey mismatch"}
What the built-in roles actually allow¶
Roles are often assigned by name without anyone checking what the name permits. Three surprises from this lab:
ops (userAdminAnyDatabase) inserting: Unauthorized
shop_app reading admin.system.users: Unauthorized
shop_app dropped orders? [ 'customers' ]
userAdminAnyDatabasecan't read or write data, only manage users and roles. But it can grant itself any role — it is effectively root with one extra step, so guard it as carefully.readWriteincludes dropping collections. The application user deleted the wholeorderscollection with one call. If your application never drops collections,readWriteis more than it needs.- Users in one database can't see another's data unless granted — the app user can't read
admin.
The common built-in roles:
| Role | Scope | Can |
|---|---|---|
read / readWrite |
one database | query / query, insert, update, delete, create and drop collections and indexes |
dbAdmin |
one database | indexes, stats, profiler, validation — but not read data |
userAdmin / userAdminAnyDatabase |
one / all databases | create users and roles, grant roles (including to itself) |
clusterMonitor |
cluster | read-only monitoring commands (serverStatus, replSetGetStatus, …) |
backup / restore |
cluster | what mongodump / mongorestore need |
root |
everything | everything |
Custom roles for least privilege¶
A reporting service should read orders and nothing else. A custom role lists exact actions on exact resources:
shop.createRole({role: "ordersReporter",
privileges: [{resource: {db: "shop", collection: "orders"}, actions: ["find"]}],
roles: []})
shop.createUser({user: "reporter", pwd: passwordPrompt(), roles: ["ordersReporter"]})
reporter find orders: 2
reporter find customers: Unauthorized
reporter update orders: Unauthorized
200
reporter $lookup into customers: Unauthorized
The find action covers queries and aggregation on that collection (the 200 is a $group total), but a
$lookup into customers is refused: every collection a pipeline reads is checked. Writing roles this narrowly
takes a few minutes per service and turns a leaked credential from "attacker has the database" into "attacker can
read one collection".
A sensible set for most applications:
- one user administrator (stored in a password manager, rarely used);
- one application user per service, with a custom role covering only the collections and actions it uses
(often
find,insert,update,remove— withoutdropCollection); - a migration user with index and collection management, used only by the deploy pipeline;
- read-only users for humans and reporting;
backupfor the backup job;clusterMonitorfor monitoring.
TLS: encrypting the connection¶
Without TLS, every query, result and SCRAM exchange crosses the network in readable form. A lab certificate authority and server certificate with OpenSSL — the server's certificate must name the host clients will use, in its Subject Alternative Name:
openssl req -x509 -newkey rsa:2048 -nodes -days 30 -subj "/CN=Lab CA" -keyout ca.key -out ca.pem
openssl req -newkey rsa:2048 -nodes -subj "/CN=127.0.0.1" -keyout server.key -out server.csr
printf "subjectAltName=IP:127.0.0.1,DNS:localhost\nextendedKeyUsage=serverAuth" > san.ext
openssl x509 -req -in server.csr -CA ca.pem -CAkey ca.key -CAcreateserial -days 30 -extfile san.ext -out server.crt
cat server.crt server.key > server.pem
mongod --port 27502 --dbpath tlsdb --bind_ip 127.0.0.1 --tlsMode requireTLS \
--tlsCertificateKeyFile server.pem --tlsCAFile ca.pem --logpath tls.log &
Then four attempts to connect, each failing for a different, instructive reason:
--- plain connection:
MongoServerSelectionError: connection <monitor> to 127.0.0.1:27502 closed
--- TLS without trusting the CA:
MongoNetworkError: unable to get local issuer certificate
--- TLS with CA:
MongoServerSelectionError: connection <monitor> to 127.0.0.1:27502 closed
The first is requireTLS refusing plain text. The second is the client refusing a certificate signed by an
authority it doesn't trust — exactly what should happen. The third is the surprise: the client trusted the server,
but the server's log says why it hung up:
"msg":"SSL peer certificate validation failed; connection rejected","attr":{"error":"No SSL certificate provided by peer"}
Giving mongod a --tlsCAFile also makes it require client certificates signed by that CA. That's a strong
setting (mutual TLS) but it's a choice. If clients should authenticate with passwords over TLS instead, add
--tlsAllowConnectionsWithoutCertificates. And the CA file can't simply be left out — this version refuses to
start:
"errmsg":"The use of TLS without specifying a chain of trust is no longer supported. See https://jira.mongodb.org/browse/SERVER-72839 for details."
Host name verification is the other half of TLS. A server whose certificate names db.lab.internal, reached as
127.0.0.1:
MongoServerSelectionError: Hostname/IP does not match certificate's altnames: IP: 127.0.0.1 is not in the cert's list:
--- same with tlsAllowInvalidHostnames:
connected 9.0.2
tlsAllowInvalidHostnames (and its cousin tlsAllowInvalidCertificates) make the error go away by turning off
the check that stops a man-in-the-middle. Fix the certificate or the connection string instead.
x.509: certificates as identities¶
With mutual TLS in place, the client certificate can be the credential. A certificate for a service:
openssl req -newkey rsa:2048 -nodes -subj "/O=Lab/OU=apps/CN=billing-svc" -keyout client.key -out client.csr
printf "extendedKeyUsage=clientAuth" > cli.ext
openssl x509 -req -in client.csr -CA ca.pem -CAkey ca.key -CAcreateserial -days 30 -extfile cli.ext -out client.crt
cat client.crt client.key > client.pem
openssl x509 -in client.pem -noout -subject -nameopt RFC2253
The user is created in the virtual $external database, named by the certificate's subject exactly as RFC 2253
prints it:
db.getSiblingDB("$external").createUser({user: "CN=billing-svc,OU=apps,O=Lab",
roles: [{role: "read", db: "billing"}]})
mongodb://127.0.0.1:27502/?tls=true&tlsCAFile=ca.pem&tlsCertificateKeyFile=client.pem&authMechanism=MONGODB-X509&authSource=$external
[ { user: 'CN=billing-svc,OU=apps,O=Lab', db: '$external' } ]
No password exists to leak; revoking access means removing the user or letting the certificate expire. The cost
is running a certificate authority properly — issuing, rotating and revoking. (This TLS server ran without --auth
to keep the experiment focused on TLS; in production you'd combine both.)
Replica sets and clusters: member authentication¶
Members of a replica set or sharded cluster must authenticate to each other. With --auth, that's not optional:
The simplest mechanism is a keyFile: a shared secret every member reads.
openssl rand -base64 756 > keyfile && chmod 400 keyfile
mongod --replSet rs4 --keyFile keyfile ... # --keyFile implies --auth
Every mongod and mongos in the deployment needs the same file. In production, x.509 member certificates
(--clusterAuthMode x509) are stronger: no shared secret, and each member has its own identity. Lesson 10 builds
a cluster with keyFile auth end to end.
Beyond the basics¶
- Network exposure is the first control:
bindIponly on private interfaces, firewall rules or security groups allowing only application hosts, never a public address. - Encryption at rest (WiredTiger's encrypted storage engine), auditing, LDAP and Kerberos authentication are MongoDB Enterprise / Atlas features — not in the Community server used here, so not demonstrated. Community deployments usually rely on encrypted disks at the OS or cloud level.
- Client-side field level encryption and Queryable Encryption encrypt chosen fields in the driver so the server never sees plaintext; they need extra libraries and key management, and parts depend on edition and version — check current documentation before designing around them.
How It Actually Works¶
SCRAM (Salted Challenge Response Authentication Mechanism) never sends the password. The server stores a salt, an
iteration count, and two keys derived from the password (StoredKey, ServerKey). On login the client and server
exchange nonces; the client proves it knows the password by sending a proof computed from the salted password and
the conversation, and the server proves it knows the stored credential by returning a signature the client checks.
A stolen user document allows offline guessing (which the salt and iterations slow down) but not direct login —
that's what "storedKey mismatch" in the log refers to.
After authentication the connection has a set of users, each with roles; roles resolve (recursively,
through inherited roles) into privileges — pairs of a resource (cluster, database, collection) and a list of
actions. Every command declares which actions on which resources it needs; before running it, the server checks
the connection's privilege set. An aggregation's check includes every namespace it touches, which is why the
$lookup failed. TLS happens a layer below all of this: the TLS handshake (certificate validation on both sides
when client certificates are required) completes before MongoDB's wire protocol starts, so a failed handshake
never reaches authentication at all — which is why the "TLS with CA" attempt showed only a closed connection on
the client and the real reason only in the server log.
Common mistakes¶
- Exposing
mongodon a public address, with or without auth. - Giving applications
rootordbOwner, or one shared user for every service. - Assuming
readWriteis harmless — it can drop collections. - Treating
userAdminAnyDatabaseas low-risk. It can grant itself anything. - Disabling host name or certificate checks (
tlsAllowInvalidHostnames,tlsAllowInvalidCertificates) to silence errors. - Forgetting the authentication database (
authSource), then debugging "Authentication failed" for an hour. - Passwords in connection strings committed to git, or special characters in passwords not URL-encoded.
- Copying one keyFile into a container image shared across environments.
Exercise¶
- Start a fresh
mongod --auth, create the user administrator through the localhost exception, and confirm you can't create a second user without logging in. - Write a custom role
shopServicewithfind,insert,updateandremoveonshop.ordersandfindonshop.products— and nothing else. Prove that it can't drop a collection, create an index, or readcustomers. - Rotate the
shop_apppassword withdb.changeUserPasswordwhile a Python client stays connected. Do existing connections break? Do new ones work with the old password? - Set up TLS with a CA, a server certificate and
--tlsAllowConnectionsWithoutCertificates. Connect with PyMongo usingtlsCAFile. Then capture traffic withtcpdump(or Wireshark) with and without TLS and compare what you can read. - Convert your three-node replica set from Lesson 01 to keyFile authentication with a rolling restart (start with
transitionToAuthon each member, then without it), keeping a writer running throughout.