Skip to content
ExponaBLOG
MONGODB, NOSQL & DATA MODELING · DAY 049 / 100

MongoDB security checklist: patterns, pitfalls and fixes

Protect your data.

Expona Admin8 min read

Introduction

We've all heard the story: a major breach happens because someone forgot to enable authentication on a production MongoDB instance. Data leaks, compliance violations follow, and suddenly security becomes everyone's job. This MongoDB security checklist: patterns, pitfalls and fixes distills what we've learned from securing databases across e-commerce, SaaS, and trading platforms into a practical guide you can implement today.

MongoDB is powerful and flexible, but that flexibility comes with responsibility. Default configurations often prioritize ease of development over protection. By the time you're running production workloads, security isn't optional—it's foundational. Let's walk through the four pillars that keep your data safe.

Authentication — Never run without auth

The simplest security mistake is also the most catastrophic: running MongoDB without authentication enabled. We've seen development servers exposed to the internet with no password protection, internal staging instances accessible to contractors, and even production replicas with default credentials. Authentication is your first gate.

The pattern: Enable authentication at startup and create user accounts before you open any network port. In development, this feels like friction. In production, it's non-negotiable.

Start by launching MongoDB with authentication enforced:

javascript
// mongod.conf
security:
  authorization: enabled
  
// Start the server
mongod --config mongod.conf

On first start, create an admin user in the admin database. This user has full cluster permissions and acts as your root-level access:

javascript
// Connect locally without auth (only possible on first startup)
mongosh

// Create admin user
use admin
db.createUser({
  user: "admin_prod",
  pwd: "strong-random-password-here",
  roles: [{ role: "root", db: "admin" }]
})

// Now all connections require auth
db.auth("admin_prod", "strong-random-password-here")

From that point on, every connection—including your application, scripts, and monitoring tools—must provide credentials. Store credentials in environment variables or a secrets manager, never hardcoded in your repository.

Real scenario: A ride-hailing platform we worked with had a staging replica that matched production schema but lacked authentication. A junior developer accidentally pointed their load tests at it instead of the local dev instance, flooding it with test data. Because there was no auth, a competitor's crawler had already indexed the replica's publicly exposed API endpoint weeks earlier. Authentication wouldn't have prevented the misconfiguration, but it would have caught the data exposure.

In your Node.js or Python application, connect with credentials:

javascript
// Node.js + Mongoose
const mongoUri = `mongodb://${process.env.MONGO_USER}:${process.env.MONGO_PASS}@${process.env.MONGO_HOST}:27017/mydb?authSource=admin`;
const conn = await mongoose.connect(mongoUri, {
  authSource: "admin"
});

Network — Bind to private networks and use IP allowlists

Even with authentication enabled, exposing MongoDB on a public IP is risky. Attackers will brute-force passwords, exploit unpatched versions, and probe for misconfigurations. Your database should only be reachable from known, trusted sources.

The pattern: Bind MongoDB to a private network interface, and use your cloud provider's security groups or firewall rules to allowlist specific application servers and admin jump hosts.

Configure MongoDB to listen only on private IPs:

javascript
// mongod.conf
net:
  bindIp: "10.0.1.5"  // Private IP only
  port: 27017
  
security:
  authorization: enabled

Never bind to 0.0.0.0 in production. If you must accept connections from multiple servers, use a private subnet and rely on network policies.

Real scenario: A CRM platform we secured had MongoDB running in a Kubernetes cluster. The cluster's network policy was too permissive—any pod could reach the database. When a dependency with a known vulnerability was deployed, that container was compromised and used as a pivot point to scan the cluster. By restricting network access to only the application pods' IP ranges and the admin bastion host, we eliminated that lateral movement path.

In AWS, this looks like:

javascript
// Security group for MongoDB (AWS example)
Inbound rule:
  Protocol: TCP
  Port: 27017
  Source: 10.0.1.0/24  (application subnet only)
  
Inbound rule:
  Protocol: TCP
  Port: 27017
  Source: 203.0.113.42/32  (admin bastion IP)

Always document which services and IPs need database access. Treat it like any other change control: review, approve, and audit.

Least privilege — Separate users for apps, admins and analytics

One shared admin password used by your application, your analytics pipeline, your support team, and your DevOps engineers is a recipe for accidents and breaches. When a password leaks—and they do—you can't revoke it for just one team. When an analytics script runs a resource-intensive query, you can't rate-limit it without affecting production traffic.

The pattern: Create separate users with minimal permissions for each functional role. Applications get read-write access only to their own database. Analytics gets read-only access. Admins get admin roles but are tracked.

MongoDB's role-based access control (RBAC) is granular:

javascript
// Admin user - full permissions
db.createUser({
  user: "admin_ops",
  pwd: "admin-password",
  roles: [{ role: "root", db: "admin" }]
})

// Application user - read/write to one database only
db.createUser({
  user: "app_orders",
  pwd: "app-password",
  roles: [{ role: "readWrite", db: "orders_db" }]
})

// Analytics user - read-only across databases
db.createUser({
  user: "analytics_reader",
  pwd: "analytics-password",
  roles: [
    { role: "read", db: "orders_db" },
    { role: "read", db: "users_db" },
    { role: "read", db: "payments_db" }
  ]
})

// Support user - read-only to specific collections
db.createRole({
  role: "supportReadCustomers",
  privileges: [
    {
      resource: { db: "orders_db", collection: "customers" },
      actions: [ "find" ]
    }
  ],
  roles: []
})
db.createUser({
  user: "support_team",
  pwd: "support-password",
  roles: [{ role: "supportReadCustomers", db: "orders_db" }]
})

This layering provides multiple benefits. If the analytics password is discovered, you rotate it without affecting application availability. If a junior developer's script misbehaves, it's scoped to one database. If your payment processing service is compromised, it only has access to the payments collection—not customer PII in a different database.

Real scenario: An e-commerce platform had one shared admin credential shared via a wiki page. When they rotated it, the rotation broke three unrelated systems that had secretly embedded it. One was a legacy reporting tool, another was a third-party data sync, and the third was a backup script. By creating specific users per system and requiring explicit approvals for credential changes, they eliminated both the technical debt and the operational chaos.

Encryption — TLS in transit, encryption at rest, field-level encryption

Data moving between your application and MongoDB can be intercepted. Data sitting on disk can be stolen if the server is physically compromised or a cloud storage snapshot is leaked. Sensitive fields like credit cards, social security numbers, and API keys need individual protection.

The pattern: Enable TLS/SSL for all connections, configure encryption at rest on your MongoDB instances, and use field-level encryption for data that requires additional protection.

TLS in transit: Require encrypted connections between client and server. Generate certificates and configure MongoDB:

javascript
// mongod.conf
net:
  ssl:
    mode: requireSSL
    PEMKeyFile: /etc/mongodb/server.pem
    
security:
  authorization: enabled

Update your connection string to enforce TLS:

javascript
// Node.js
const mongoUri = `mongodb+srv://${user}:${pass}@cluster.mongodb.net/mydb?ssl=true&retryWrites=true`;
const conn = await mongoose.connect(mongoUri);

Encryption at rest: MongoDB Enterprise and cloud-hosted MongoDB Atlas offer encrypted storage. Data written to disk is encrypted automatically. Your deployment should enable this by default:

javascript
// MongoDB Enterprise configuration
security:
  encryptionCipherMode: AES256-CBC
  encryptionKeyFile: /etc/mongodb/encryption-key

Field-level encryption: For highly sensitive data, encrypt individual fields at the application layer. This way, even if someone gains database access or reads a backup, they see gibberish for protected fields.

Using the Queryable Encryption feature (available in MongoDB 6.0+):

javascript
// Encrypt sensitive fields before insert
const client = new MongoClient(uri, {
  autoEncryption: {
    keyVaultNamespace: "encryption.__keyVault",
    kmsProviders: {
      aws: {
        accessKeyId: process.env.AWS_ACCESS_KEY_ID,
        secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY
      }
    },
    schemaMap: {
      "mydb.users": {
        bsonType: "object",
        encryptedFields: {
          ssn: {
            encrypt: {
              algorithm: "AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic",
              bsonType: "string"
            }
          },
          creditCard: {
            encrypt: {
              algorithm: "AEAD_AES_256_CBC_HMAC_SHA_512-Random",
              bsonType: "string"
            }
          }
        }
      }
    }
  }
});

// Fields are encrypted automatically on insert and decrypted on read
db.collection("users").insertOne({
  name: "Alice",
  ssn: "123-45-6789",
  creditCard: "4111-1111-1111-1111"
});

Real scenario: A trading platform stored API keys in MongoDB in plain text "temporarily." Years later, those keys were still there. An attacker who gained read access (via a SQL injection in a related service, not the database itself) exfiltrated thousands of keys and used them to place unauthorized trades. Field-level encryption would have rendered those keys useless even in the attacker's hands.

Common mistakes

  • Hardcoding credentials in application code: This leaks in git history, container images, and logs. Always use environment variables or secrets managers (AWS Secrets Manager, HashiCorp Vault, etc.).
  • Skipping authentication in development: Your dev environment becomes your production template. Habits matter. Use auth everywhere, even locally.
  • Overly permissive RBAC: Creating one read-write user for everything is simple but defeats the purpose. The overhead of managing multiple users is worth the safety boundary.
  • Ignoring network security: Authentication and encryption don't matter if attackers can brute-force or DoS your public endpoint.
  • Not rotating credentials: Set a schedule (quarterly or annually) to rotate all database passwords. Test the rotation process before it's an emergency.
  • Backing up without encryption: Backups often have weaker access controls than live databases. Always encrypt them and store them separately from the production environment.

Key takeaways

  • Enable authentication on day one—even in development—to build secure habits and prevent accidental exposure.
  • Bind MongoDB to private networks and use firewall rules to allowlist only trusted sources; never expose port 27017 to the public internet.
  • Create separate users for applications, analytics, and administrative tasks, each with minimal required permissions.
  • Layer encryption: TLS for data in transit, encryption at rest for data on disk, and field-level encryption for your most sensitive information.
  • Treat database credentials as highly sensitive secrets; rotate them regularly and audit access logs for unusual activity.
  • Document your security model, test it as part of your CI/CD pipeline, and review it quarterly as your system evolves.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles