Mango DBLesson 36 – Security & Authentication | Dataplexa

Security & Authentication

A freshly installed MongoDB server accepts connections from anyone on the network with no username, no password, and no encryption. This is fine for local development but is a direct path to a breach in any other environment. Real MongoDB deployments require three layers of security working together: authentication to verify who is connecting, TLS encryption to protect data in transit, and network hardening to limit which machines can connect at all. This lesson covers enabling authentication, creating users with the correct credentials, configuring SCRAM and x.509 certificate authentication, enforcing TLS connections from PyMongo, and the network-level controls that form the outer perimeter of a secure MongoDB deployment.

1. Enabling Authentication — Access Control

MongoDB ships with authentication disabled. The first step in securing any deployment is enabling access control — after which every connection must provide a valid username and password before executing any command. The process requires creating at least one admin user before enabling access control, otherwise you lock yourself out.

# Enabling access control — the correct sequence

from pymongo import MongoClient

# ── STEP 1: Connect WITHOUT authentication (only possible before enabling) ─
client_no_auth = MongoClient("mongodb://localhost:27017/")
admin_db = client_no_auth["admin"]

# ── STEP 2: Create the first admin user BEFORE enabling access control ──────
# Must be done in the admin database
# roles: userAdminAnyDatabase — can manage users across all databases
#        readWriteAnyDatabase  — can read and write to any database
#        clusterAdmin          — can manage the replica set / cluster

admin_db.command("createUser", "dataplexa_admin",
    pwd="StrongP@ssword123!",
    roles=[
        {"role": "userAdminAnyDatabase", "db": "admin"},
        {"role": "readWriteAnyDatabase", "db": "admin"},
        {"role": "clusterAdmin",         "db": "admin"},
    ]
)
print("Admin user created: dataplexa_admin")

# ── STEP 3: Enable access control in mongod.conf ─────────────────────────
# Edit /etc/mongod.conf and add:
mongod_conf_security = """
# /etc/mongod.conf — security section
security:
  authorization: enabled
"""
print("\nmongod.conf security section:")
print(mongod_conf_security)

# ── STEP 4: Restart MongoDB ───────────────────────────────────────────────
print("sudo systemctl restart mongod")
print()

# ── STEP 5: Connect WITH authentication ──────────────────────────────────
client_auth = MongoClient(
    "mongodb://dataplexa_admin:StrongP@ssword123!@localhost:27017/",
    authSource="admin"    # the database where the user was created
)
print("Authenticated connection successful:")
print(f"  server info: {client_auth.admin.command('serverStatus')['host']}")

# Verify authentication is enforced
print("\nAuthentication enforcement check:")
try:
    client_unauth = MongoClient(
        "mongodb://localhost:27017/",
        serverSelectionTimeoutMS=3000
    )
    client_unauth.admin.command("listDatabases")
    print("  ✗ Unauthenticated connection succeeded — access control NOT enabled")
except Exception as e:
    print(f"  ✓ Unauthenticated connection rejected: {type(e).__name__}")
Admin user created: dataplexa_admin

mongod.conf security section:

# /etc/mongod.conf — security section
security:
authorization: enabled

sudo systemctl restart mongod

Authenticated connection successful:
server info: dataplexa-server-01

Authentication enforcement check:
✓ Unauthenticated connection rejected: OperationFailure
  • Always create the admin user before enabling access control — if you enable it first, no connection can authenticate and you must restart MongoDB in a special maintenance mode to regain access
  • The authSource parameter tells PyMongo which database to look in for the user's credentials — it must match the database the user was created in, which for admin users is always admin
  • Never store credentials directly in connection strings in source code — use environment variables, a secrets manager, or a .env file that is excluded from version control

2. SCRAM Authentication — Creating Application Users

MongoDB's default authentication mechanism is SCRAM (Salted Challenge Response Authentication Mechanism). It is a password-based protocol that never sends the actual password over the wire — instead it exchanges cryptographic proofs. Every application connecting to MongoDB should use a dedicated user with the minimum privileges it needs, not the admin user.

# SCRAM authentication — creating purpose-built application users

from pymongo import MongoClient
import os

# Connect as admin to create application users
client = MongoClient(
    "mongodb://dataplexa_admin:StrongP@ssword123!@localhost:27017/",
    authSource="admin"
)
admin_db = client["admin"]

# ── Application user — read/write on dataplexa only ───────────────────────
# This is what your application server uses
admin_db.command("createUser", "app_user",
    pwd=os.environ.get("APP_DB_PASSWORD", "AppP@ss999!"),
    roles=[
        {"role": "readWrite", "db": "dataplexa"}   # only dataplexa, nothing else
    ]
)
print("Created: app_user  (readWrite on dataplexa only)")

# ── Read-only user — for reporting and analytics ──────────────────────────
admin_db.command("createUser", "report_user",
    pwd=os.environ.get("REPORT_DB_PASSWORD", "ReportP@ss777!"),
    roles=[
        {"role": "read", "db": "dataplexa"}   # read-only
    ]
)
print("Created: report_user  (read-only on dataplexa)")

# ── Monitoring user — for ops tools like MongoDB Compass or Grafana ───────
admin_db.command("createUser", "monitor_user",
    pwd=os.environ.get("MONITOR_DB_PASSWORD", "MonitorP@ss555!"),
    roles=[
        {"role": "clusterMonitor", "db": "admin"},
        {"role": "read",           "db": "local"}
    ]
)
print("Created: monitor_user  (cluster monitoring only)")

# ── List all users ────────────────────────────────────────────────────────
print("\nAll database users:")
users = admin_db.command("usersInfo", 1)  # 1 = all users
for user in users.get("users", []):
    roles = [f"{r['role']}@{r['db']}" for r in user.get("roles", [])]
    print(f"  {user['user']:15}  roles: {', '.join(roles)}")

# ── Connect as app_user and verify privileges ─────────────────────────────
app_client = MongoClient(
    "mongodb://app_user:AppP@ss999!@localhost:27017/dataplexa",
    authSource="dataplexa"
)
app_db = app_client["dataplexa"]

# This should work — app_user has readWrite on dataplexa
app_db.products.find_one({}, {"name": 1})
print("\napp_user read from dataplexa.products:  ✓ allowed")

# This should fail — app_user has no access to admin database
try:
    app_client["admin"].command("listDatabases")
    print("app_user listed admin databases:  ✗ should have been blocked")
except Exception:
    print("app_user access to admin database:  ✓ correctly blocked")
Created: app_user (readWrite on dataplexa only)
Created: report_user (read-only on dataplexa)
Created: monitor_user (cluster monitoring only)

All database users:
dataplexa_admin roles: userAdminAnyDatabase@admin, readWriteAnyDatabase@admin, clusterAdmin@admin
app_user roles: readWrite@dataplexa
report_user roles: read@dataplexa
monitor_user roles: clusterMonitor@admin, read@local

app_user read from dataplexa.products: ✓ allowed
app_user access to admin database: ✓ correctly blocked
  • The principle of least privilege — give each user only the permissions it needs and nothing more. An application that only reads products should never have write access to orders
  • Create one database user per application component — separate users for the API server, the background worker, the reporting service, and the monitoring agent. This way a compromised component cannot access what the others can
  • SCRAM-SHA-256 is the default and current standard — SCRAM-SHA-1 is supported for backwards compatibility but should not be used for new deployments

3. TLS Encryption — Protecting Data in Transit

Without TLS, all data between your application and MongoDB — including query results, document contents, and credentials — travels as plaintext across the network. Anyone with access to the network can read it. TLS (Transport Layer Security) encrypts the connection end-to-end, making the data unreadable to anyone who intercepts it in transit.

# TLS configuration — enabling encrypted connections

from pymongo import MongoClient
import ssl

# ── mongod.conf — enable TLS ──────────────────────────────────────────────
tls_conf = """
# /etc/mongod.conf — TLS section
net:
  tls:
    mode: requireTLS              # reject all non-TLS connections
    certificateKeyFile: /etc/ssl/mongodb/server.pem    # server cert + key
    CAFile: /etc/ssl/mongodb/ca.pem                    # CA certificate
    allowConnectionsWithoutCertificates: true          # client cert optional
"""
print("mongod.conf TLS configuration:")
print(tls_conf)

# ── Connect with TLS — certificate verification ───────────────────────────
print("TLS connection scenarios:\n")

# Scenario 1: TLS with CA verification (production standard)
print("1. Production — TLS with CA certificate verification:")
print("""
   client = MongoClient(
       "mongodb://app_user:pass@server:27017/dataplexa",
       tls=True,
       tlsCAFile="/path/to/ca.pem",      # verify server cert against this CA
       authSource="dataplexa"
   )
""")

# Scenario 2: TLS with mutual authentication (x.509)
print("2. Mutual TLS — both server and client present certificates:")
print("""
   client = MongoClient(
       "mongodb://server:27017/dataplexa",
       tls=True,
       tlsCAFile="/path/to/ca.pem",
       tlsCertificateKeyFile="/path/to/client.pem",  # client presents its cert
       authMechanism="MONGODB-X509"                  # authenticate via cert CN
   )
""")

# Scenario 3: TLS in development — allow self-signed cert (NOT for production)
print("3. Development only — allow self-signed / unverified cert:")
print("""
   client = MongoClient(
       "mongodb://localhost:27017/",
       tls=True,
       tlsAllowInvalidCertificates=True   # ← NEVER use in production
   )
""")

# TLS mode options
print("TLS mode options (mongod.conf net.tls.mode):\n")
modes = [
    ("disabled",        "No TLS — plaintext only (default, never use in production)"),
    ("allowTLS",        "Accept both TLS and non-TLS connections"),
    ("preferTLS",       "Prefer TLS but accept non-TLS — for migration periods"),
    ("requireTLS",      "Reject all non-TLS connections (production standard)"),
]
for mode, desc in modes:
    prefix = "✓" if mode == "requireTLS" else "○"
    print(f"  {prefix} {mode:16}  {desc}")
mongod.conf TLS configuration:

# /etc/mongod.conf — TLS section
net:
tls:
mode: requireTLS
certificateKeyFile: /etc/ssl/mongodb/server.pem
CAFile: /etc/ssl/mongodb/ca.pem
allowConnectionsWithoutCertificates: true

TLS connection scenarios:

1. Production — TLS with CA certificate verification:
client = MongoClient(
"mongodb://app_user:pass@server:27017/dataplexa",
tls=True,
tlsCAFile="/path/to/ca.pem",
authSource="dataplexa"
)

2. Mutual TLS — both server and client present certificates:
client = MongoClient(
"mongodb://server:27017/dataplexa",
tls=True,
tlsCAFile="/path/to/ca.pem",
tlsCertificateKeyFile="/path/to/client.pem",
authMechanism="MONGODB-X509"
)

3. Development only — allow self-signed cert:
client = MongoClient(
"mongodb://localhost:27017/",
tls=True,
tlsAllowInvalidCertificates=True # ← NEVER use in production
)

TLS mode options:
○ disabled No TLS — plaintext only (default, never use in production)
○ allowTLS Accept both TLS and non-TLS connections
○ preferTLS Prefer TLS but accept non-TLS — for migration periods
✓ requireTLS Reject all non-TLS connections (production standard)
  • Always use requireTLS in production — allowTLS and preferTLS exist only as migration stepping stones between disabling and requiring TLS, not as permanent modes
  • tlsAllowInvalidCertificates=True disables certificate verification entirely — it is useful only for local development with self-signed certificates and must never appear in any production configuration
  • Mutual TLS (x.509) is stronger than SCRAM because authentication is based on cryptographic certificates rather than passwords — there is no password to steal, phish, or brute-force

4. Network Hardening — Binding and Firewalls

TLS and authentication protect the connection after it is established — but the outer layer of security is controlling which machines can even attempt a connection. MongoDB should never listen on a public IP address. Network binding restricts which interfaces MongoDB listens on, and firewall rules restrict which source IPs can reach those interfaces.

# Network hardening — bindIP, firewall rules, and auditing open connections

from pymongo import MongoClient

# ── mongod.conf — network binding ────────────────────────────────────────
net_conf = """
# /etc/mongod.conf — network section (hardened)
net:
  port: 27017
  bindIp: 127.0.0.1,10.0.1.50   # localhost + specific private IP only
  # bindIpAll: false             # NEVER set bindIpAll: true in production
  tls:
    mode: requireTLS
    certificateKeyFile: /etc/ssl/mongodb/server.pem
    CAFile: /etc/ssl/mongodb/ca.pem
"""
print("mongod.conf network hardening:")
print(net_conf)

# ── What bindIp controls ──────────────────────────────────────────────────
print("bindIp explained:\n")
bindings = [
    ("127.0.0.1",           "Localhost only — no remote connections possible"),
    ("127.0.0.1,10.0.1.50", "Localhost + one private IP — app server only"),
    ("0.0.0.0",             "All interfaces — exposed to public network (dangerous)"),
]
for binding, desc in bindings:
    flag = "✓" if "10.0.1" in binding or binding == "127.0.0.1" else "✗"
    print(f"  {flag} bindIp: {binding:25}  {desc}")

# ── Firewall rules (iptables example) ────────────────────────────────────
print("\nLinux firewall rules (iptables):")
fw_rules = [
    "# Allow MongoDB only from app server IP",
    "iptables -A INPUT -p tcp --dport 27017 -s 10.0.1.50 -j ACCEPT",
    "# Allow localhost",
    "iptables -A INPUT -p tcp --dport 27017 -s 127.0.0.1 -j ACCEPT",
    "# Block all other MongoDB connections",
    "iptables -A INPUT -p tcp --dport 27017 -j DROP",
]
for rule in fw_rules:
    print(f"  {rule}")

# ── Check current connections ─────────────────────────────────────────────
client = MongoClient(
    "mongodb://dataplexa_admin:StrongP@ssword123!@localhost:27017/",
    authSource="admin"
)
print("\nCurrent active connections:")
server_status = client.admin.command("serverStatus")
connections = server_status.get("connections", {})
print(f"  current:   {connections.get('current', 0)}")
print(f"  available: {connections.get('available', 0)}")
print(f"  total:     {connections.get('totalCreated', 0)}")

# List active operations — find any suspicious long-running ops
print("\nActive operations (currentOp):")
current_ops = client.admin.command("currentOp", {"active": True})
for op in current_ops.get("inprog", [])[:5]:
    print(f"  opid: {op.get('opid'):6}  "
          f"type: {op.get('type','?'):10}  "
          f"client: {op.get('client','?'):20}  "
          f"secs: {op.get('secs_running', 0)}")
mongod.conf network hardening:

# /etc/mongod.conf — network section (hardened)
net:
port: 27017
bindIp: 127.0.0.1,10.0.1.50
tls:
mode: requireTLS
certificateKeyFile: /etc/ssl/mongodb/server.pem
CAFile: /etc/ssl/mongodb/ca.pem

bindIp explained:

✓ bindIp: 127.0.0.1 Localhost only — no remote connections possible
✓ bindIp: 127.0.0.1,10.0.1.50 Localhost + one private IP — app server only
✗ bindIp: 0.0.0.0 All interfaces — exposed to public network (dangerous)

Linux firewall rules (iptables):
# Allow MongoDB only from app server IP
iptables -A INPUT -p tcp --dport 27017 -s 10.0.1.50 -j ACCEPT
# Allow localhost
iptables -A INPUT -p tcp --dport 27017 -s 127.0.0.1 -j ACCEPT
# Block all other MongoDB connections
iptables -A INPUT -p tcp --dport 27017 -j DROP

Current active connections:
current: 3
available: 838597
total: 47

Active operations (currentOp):
opid: 1234 type: op client: 10.0.1.50:54321 secs: 0
opid: 1235 type: op client: 127.0.0.1:49200 secs: 0
  • Never set bindIpAll: true or bindIp: 0.0.0.0 on a production server — this exposes MongoDB to every network interface including any public-facing IP, making it reachable from the internet
  • Network controls and application-level authentication are complementary — a firewall that only allows your app server to connect means a stolen credential is only useful from that one machine
  • Use currentOp to identify and kill suspicious long-running operations — an unusually slow query from an unexpected client IP is a warning sign worth investigating immediately

5. Security Checklist and Audit Logging

Security is not a one-time task — it requires regular auditing to catch configuration drift, newly created users with excessive privileges, or connections that should not exist. MongoDB Enterprise and Atlas provide built-in audit logging. For Community Edition, a Python-based security audit covers the most important checks.

# Security audit — automated checks from PyMongo

from pymongo import MongoClient

client = MongoClient(
    "mongodb://dataplexa_admin:StrongP@ssword123!@localhost:27017/",
    authSource="admin"
)
admin_db = client["admin"]

print("MongoDB Security Audit\n" + "═" * 40)

issues   = []
warnings = []

# ── Check 1: Authentication enabled ──────────────────────────────────────
cmd_line = client.admin.command("getCmdLineOpts")
auth_enabled = cmd_line.get("parsed", {}).get(
    "security", {}
).get("authorization", "") == "enabled"

status = "✓" if auth_enabled else "✗"
print(f"\n{status} Authentication: {'ENABLED' if auth_enabled else 'DISABLED'}")
if not auth_enabled:
    issues.append("Authentication is disabled — enable security.authorization in mongod.conf")

# ── Check 2: TLS mode ─────────────────────────────────────────────────────
tls_mode = cmd_line.get("parsed", {}).get("net", {}).get("tls", {}).get("mode", "disabled")
tls_ok   = tls_mode == "requireTLS"
status   = "✓" if tls_ok else "✗"
print(f"{status} TLS mode: {tls_mode}")
if not tls_ok:
    issues.append(f"TLS mode is '{tls_mode}' — set to requireTLS in production")

# ── Check 3: Users with excessive privileges ──────────────────────────────
print("\nUser privilege audit:")
all_users = admin_db.command("usersInfo", 1).get("users", [])
high_priv_roles = {"root", "dbOwner", "__system", "userAdminAnyDatabase"}
for user in all_users:
    user_roles = {r["role"] for r in user.get("roles", [])}
    excess = user_roles & high_priv_roles
    if excess and user["user"] != "dataplexa_admin":
        warnings.append(f"User '{user['user']}' has high-privilege roles: {excess}")
        print(f"  ⚠ {user['user']:15}  high-privilege roles: {excess}")
    else:
        print(f"  ✓ {user['user']:15}  roles look appropriate")

# ── Check 4: Default port in use ─────────────────────────────────────────
port = cmd_line.get("parsed", {}).get("net", {}).get("port", 27017)
port_ok = port != 27017
status  = "○" if not port_ok else "✓"
print(f"\n{status} Port: {port} {'(non-default — good)' if port_ok else '(default 27017 — consider changing)'}")

# ── Check 5: No users with empty passwords ────────────────────────────────
print("\nEmpty password check: ✓ not checkable via driver — verify in mongod logs")

# ── Summary ───────────────────────────────────────────────────────────────
print(f"\n{'═'*40}")
print(f"Issues:   {len(issues)}")
for i in issues:
    print(f"  ✗ {i}")
print(f"Warnings: {len(warnings)}")
for w in warnings:
    print(f"  ⚠ {w}")
if not issues and not warnings:
    print("  ✓ All security checks passed")
MongoDB Security Audit
════════════════════════════════════════

✓ Authentication: ENABLED
✓ TLS mode: requireTLS

User privilege audit:
✓ dataplexa_admin roles look appropriate
✓ app_user roles look appropriate
✓ report_user roles look appropriate
✓ monitor_user roles look appropriate

○ Port: 27017 (default 27017 — consider changing)

Empty password check: ✓ not checkable via driver — verify in mongod logs

════════════════════════════════════════
Issues: 0
Warnings: 0
✓ All security checks passed
  • Run a security audit after every infrastructure change, every new user creation, and at least monthly as a routine check — configuration drift is the most common source of security regressions
  • MongoDB Enterprise and Atlas both provide built-in audit logging that records every authentication attempt, privilege escalation, and schema change to a tamper-evident log — essential for compliance requirements like PCI-DSS, HIPAA, and SOC 2
  • Changing the default port from 27017 is a minor deterrent against automated scanning tools but is not a security control on its own — it must be combined with authentication, TLS, and network controls

Summary Table

Control What It Protects Key Setting
Access control Blocks unauthenticated connections security.authorization: enabled in mongod.conf
SCRAM authentication Password-based identity verification Create users with createUser — one per component
Least privilege Limits blast radius of a compromise Assign only readWrite or read per database
TLS requireTLS Encrypts all data in transit net.tls.mode: requireTLS in mongod.conf
x.509 mutual TLS Certificate-based auth — no passwords tlsCertificateKeyFile + authMechanism="MONGODB-X509"
bindIp Restricts which interfaces MongoDB listens on Never use 0.0.0.0 — bind to private IPs only
Security audit Detects configuration drift and over-privileged users Run monthly — check auth, TLS, user roles, open ports

Practice Questions

Practice 1. What is the correct order of steps to enable authentication on a MongoDB server without locking yourself out?



Practice 2. Why should each application component use its own dedicated MongoDB user rather than sharing one?



Practice 3. What is the difference between TLS with CA verification and mutual TLS (x.509)?



Practice 4. What is the danger of setting bindIp: 0.0.0.0 in mongod.conf?



Practice 5. Why is tlsAllowInvalidCertificates=True dangerous in production and when is it acceptable?



Quiz

Quiz 1. What happens if you enable security.authorization in mongod.conf before creating any admin user?






Quiz 2. Which authentication mechanism uses cryptographic certificates instead of passwords, making it immune to credential theft?






Quiz 3. What role should an application server's MongoDB user have if it only needs to read and write to the dataplexa database?






Quiz 4. What TLS mode should always be used in production MongoDB deployments?






Quiz 5. Which PyMongo parameter specifies the database where MongoDB should look for the connecting user's credentials?






Next up — Authorization & Roles: Built-in roles, custom roles, collection-level privileges, and field-level redaction for fine-grained access control.