Connection Strings and Security

A connection string is the URI that tells a driver, mongosh, or GUI tool where MongoDB lives, who is connecting, and how. Getting it right matters more than it looks: a leaked or overly permissive connection string is one of the most common ways real MongoDB deployments get breached, especially clusters left running with authentication disabled. This lesson covers both connection string formats, how MongoDB authenticates and authorizes a client once it connects, and how TLS keeps credentials and data off the wire in plain text.

Overview / How it works

MongoDB ships with authentication disabled by default on a fresh local install, but any server reachable from outside localhost should run with --auth (or security.authorization: enabled in mongod.conf) so that every operation requires a logged-in user. Once auth is on, MongoDB checks two independent things for every connection: authentication (who are you? verified via a SCRAM-SHA-256 challenge-response by default, or x.509 certificates, or an external identity provider on Atlas) and authorization (what are you allowed to do? governed by role-based access control, or RBAC). These layers are separate — a user can authenticate successfully and still have a write rejected because their assigned role doesn’t grant it.

Users and the roles they hold live inside a specific database, referenced by the authSource parameter of the connection string. By convention, application users are usually created in the admin database even when they will only ever operate on another database, because authSource defaults to whatever database you’re connecting to, not admin. Forgetting this is the single most common cause of a mysterious “authentication failed” error. Roles themselves are either MongoDB’s built-in roles (read, readWrite, dbAdmin, userAdmin, clusterAdmin, root, and more) scoped to one database or all databases, or custom roles you define yourself as an explicit list of resources and the actions permitted on them.

Encryption is a separate concern from authentication. TLS encrypts the bytes traveling between the client and mongod/mongos so a network eavesdropper cannot read your credentials or your query results in transit; it says nothing about who the connecting user actually is. MongoDB Atlas enforces TLS on every connection automatically. Self-managed clusters need it turned on explicitly with --tls and certificate files on the server; the connection string can then request it with ?tls=true (implied automatically by the mongodb+srv:// scheme).

Syntax

MongoDB drivers accept two connection string formats:

mongodb://[username:password@]host1[:port1][,host2[:port2],...][/[defaultauthdb]][?options]

mongodb+srv://[username:password@]host[/[defaultauthdb]][?options]

The mongodb+srv:// form takes a single hostname and asks DNS for a SRV record listing the real hosts and ports (and a TXT record for default options) — this is what Atlas gives you, and it means the client always has an up-to-date member list without you hardcoding every replica set node. It also implies tls=true unless you override it.

Option Purpose
authSource Database that holds the user’s credentials (often admin)
authMechanism How to authenticate: SCRAM-SHA-256 (default), SCRAM-SHA-1, MONGODB-X509, MONGODB-AWS
replicaSet Name of the replica set to connect to (non-SRV form)
tls Encrypt the connection with TLS (true/false)
retryWrites Automatically retry certain writes once on a transient network error
w Write concern — how many nodes must acknowledge a write, e.g. majority
readPreference Which replica set members can serve reads, e.g. primary, secondaryPreferred
appName Label shown in server logs and currentOp() to identify this client

Examples

Example 1: Connecting with a username, password, and authSource

mongosh "mongodb://appUser:<password>@localhost:27017/shopDB?authSource=admin"

Output:

Current Mongosh Log ID: 66b1f2a0c9d4e2a1b3c4d5e6
Connecting to: mongodb://<credentials>@localhost:27017/shopDB?authSource=admin&appName=mongosh+2.1.1
Using MongoDB: 7.0.5
Using Mongosh: 2.1.1

shopDB>

The user appUser was originally created in the admin database, so authSource=admin tells the driver where to verify the password even though the session lands on shopDB once connected. Without that parameter, MongoDB would look for appUser inside shopDB itself, not find it, and reject the login.

Example 2: Creating a least-privilege user

use admin
db.createUser({
  user: "reportViewer",
  pwd: passwordPrompt(),
  roles: [
    { role: "read", db: "shopDB" }
  ]
});

Output:

Enter password: ****
{ ok: 1 }

passwordPrompt() is a mongosh helper that reads the password interactively so it never appears in your shell history or a script file. The new user reportViewer can only read from shopDB — it cannot write, cannot touch any other database, and cannot manage users or indexes.

Example 3: Connecting to an Atlas cluster with the SRV form

mongosh "mongodb+srv://reportViewer:<password>@cluster0.ab1cd.mongodb.net/shopDB"

Output:

Connecting to: mongodb+srv://<credentials>@cluster0.ab1cd.mongodb.net/shopDB&appName=mongosh+2.1.1
Using MongoDB: 7.0.5 (API Version 1)

Atlas atlas-shard-00>

No authSource was needed here because Atlas connection strings generated for a database user already default authSource to admin, and TLS is on automatically thanks to the +srv scheme.

Example 4: Connecting from Node.js with the official driver

import { MongoClient } from "mongodb";

const uri = process.env.MONGODB_URI;
// e.g. mongodb+srv://appUser:<password>@cluster0.ab1cd.mongodb.net/shopDB?retryWrites=true&w=majority

const client = new MongoClient(uri, { tls: true });

async function main() {
  await client.connect();
  const db = client.db("shopDB");
  const orders = await db.collection("orders")
    .find({ status: "shipped" })
    .limit(5)
    .toArray();
  console.log(orders);
  await client.close();
}

main().catch(console.error);

Output:

[
  { _id: ObjectId("66b1..."), status: "shipped", total: 89.99, customerId: ObjectId("66a2...") },
  { _id: ObjectId("66b2..."), status: "shipped", total: 42.50, customerId: ObjectId("66a3...") }
]

The URI is read from an environment variable instead of being written into the source file, so the credentials never end up in version control.

How it works step by step

When a driver opens a connection with credentials, four things happen in order:

  • DNS resolution (SRV only): the driver queries a TXT record for default connection options, then a SRV record that returns the actual hostnames and ports of every node in the replica set or sharded cluster.
  • TCP + TLS handshake: a socket opens to each host; if TLS is enabled, the client and server negotiate a cipher suite and verify certificates before any MongoDB protocol traffic is sent.
  • SCRAM-SHA-256 authentication: the client sends the username; the server replies with a random salt and iteration count; the client combines these with the password using PBKDF2 to compute a proof without ever sending the plaintext password over the wire; the server independently verifies the proof against its stored salted hash and confirms or rejects the login.
  • Authorization on every command: once authenticated, every subsequent command (a query, an insert, a createIndex) is checked against the roles granted to that user before it runs, not just once at login time.

Common Mistakes

Mistake 1: Hardcoding credentials in source code

import { MongoClient } from "mongodb";

const client = new MongoClient(
  "mongodb+srv://appUser:Sup3rSecret!@<cluster-url>/shopDB"
);

Anyone with read access to the repository (or its git history, even after a later commit removes the line) now has a working password. Read it from configuration instead:

import { MongoClient } from "mongodb";

const client = new MongoClient(process.env.MONGODB_URI);

Mistake 2: Omitting authSource for a user created outside the target database

mongosh "mongodb://appUser:<password>@localhost:27017/shopDB"

Output:

MongoServerError: Authentication failed.

appUser was created in admin, but this connection string never says so, so MongoDB looks for the user in shopDB and fails. Add authSource:

mongosh "mongodb://appUser:<password>@localhost:27017/shopDB?authSource=admin"

Mistake 3: Granting an application user the root role

db.createUser({
  user: "appUser",
  pwd: passwordPrompt(),
  roles: [ "root" ]
});

It’s tempting to grant root so the app “just works” against every database and never hits a permissions error — but it also means a single SQL-injection-style bug or a stolen connection string can drop collections, create new admin users, or shut down the server. Scope the role to what the application actually needs:

db.createUser({
  user: "appUser",
  pwd: passwordPrompt(),
  roles: [
    { role: "readWrite", db: "shopDB" }
  ]
});

Best Practices

  • Enable --auth on every deployment reachable outside localhost; never rely on network isolation alone.
  • Create a separate database user per application or service, each scoped with the narrowest role that lets it do its job.
  • Store connection strings in environment variables or a secret manager, never in source control.
  • Always enable TLS for any connection that crosses a network boundary; Atlas does this automatically, self-managed clusters need --tls configured explicitly.
  • Percent-encode any special characters (@, :, /, %) in a password before putting it in a URI, or the URI parser will misread the string.
  • Use IP allowlisting or VPC/private networking in addition to authentication — auth protects against a compromised credential, network restrictions reduce who can even attempt to use one.
  • Rotate credentials periodically and immediately after any suspected leak, and remove unused users.
  • Prefer custom roles over broad built-in roles like dbOwner or root for anything but genuine administrators.

Practice Exercises

  • Create a user named analyticsReader in the admin database with read-only access to a database called salesDB, then write the full mongosh connection string you’d use to log in as that user, including the correct authSource.
  • A teammate reports MongoServerError: Authentication failed when connecting with mongodb://svcUser:pw@localhost:27017/inventoryDB, even though the password is correct and the user was created with db.getSiblingDB("admin").createUser(...). Identify the missing piece and fix the connection string.
  • Design a custom role called orderProcessor that can insert and update documents in the orders collection but cannot delete documents or touch any other collection. Sketch the db.createRole() call’s privileges array (you don’t need to run it) and explain why this is safer than granting readWrite on the whole database.

Summary

  • A connection string identifies the server(s) to connect to and, optionally, the credentials and options to use — either as mongodb:// with explicit hosts or mongodb+srv:// with DNS-based discovery.
  • Authentication (who you are, via SCRAM-SHA-256 by default) and authorization (what you can do, via RBAC roles) are separate, independently enforced layers.
  • authSource tells MongoDB which database holds a user’s credentials and is the most common source of unexplained login failures.
  • TLS encrypts traffic on the wire and is unrelated to authentication; enable it on any connection that leaves a trusted network.
  • Grant the narrowest role that a user or service actually needs instead of defaulting to root or dbOwner.
  • Never hardcode credentials in source code — load connection strings from environment variables or a secret manager.