Exposed MongoDB

With authorization off, MongoDB lets anyone who can connect read, change or delete any database. That's the default. All that stands between you and the internet is net.bindIp, and it's one line in a config file.

What happened to open MongoDB servers

In early 2017, attackers used search engines for internet-connected devices to find MongoDB servers that answered on the internet with no authentication. They deleted the databases and left a note asking for a ransom to get the data back. Several groups competed for the same victims, and people who paid mostly got nothing back. Whether the data was copied before it was deleted was often unclear, so paying was no guarantee of anything.

None of this needed a bug. The servers did exactly what they were configured to do: accept anyone who connected, and let them do anything.

Two settings decide it

So a fresh install is safe only because it listens on localhost. The moment someone sets bindIp: 0.0.0.0 or bindIpAll: true to connect from another machine, the database is open. MongoDB's own docs say to secure the deployment before you bind to a public address.

Check from outside

From a machine that is not the server:

nmap -Pn -p 27017 your.server.example
mongosh "mongodb://your.server.example:27017" --eval "db.adminCommand({ listDatabases: 1 })"

If the second command prints your databases, anyone on the internet can do the same. An authentication error means authorization is on but the port is still reachable. A timeout is what you want. On the server, sudo ss -tlnp | grep 27017 shows which addresses mongod listens on.

Bind to localhost or a private address

In /etc/mongod.conf:

net:
  port: 27017
  bindIp: 127.0.0.1

If an app server on a private network needs access, add only the address of the private interface, comma-separated: bindIp: 127.0.0.1,198.51.100.5. Don't use 0.0.0.0, and don't set bindIpAll next to bindIp: the two are mutually exclusive and mongod refuses to start with both. Restart afterwards:

sudo systemctl restart mongod

Back up first, and keep an admin login you know works. If the server was open for a while, check that your data is still all there before you change anything, and take a dump. When you enable authorization, every app that connects without credentials stops working at the next restart, so update their connection strings in the same change.

Create the admin user first

Create a user administrator while authorization is still off, connected locally. passwordPrompt() asks for the password so it never lands in your shell history:

mongosh --port 27017

use admin
db.createUser({
  user: "admin",
  pwd: passwordPrompt(),
  roles: [
    { role: "userAdminAnyDatabase", db: "admin" },
    { role: "readWriteAnyDatabase", db: "admin" }
  ]
})

userAdminAnyDatabase lets this user create other users and grant roles. Give your app its own user with only the role it needs on its own database:

use appdb
db.createUser({
  user: "appuser",
  pwd: passwordPrompt(),
  roles: [ { role: "readWrite", db: "appdb" } ]
})

If you enable authorization before any user exists, the localhost exception still lets you create the first user administrator from the server itself. After that, you have to log in as that user to create more.

Turn on authorization

Add this to /etc/mongod.conf and restart mongod:

security:
  authorization: enabled

Write enabled in lowercase. Recent releases reject any other spelling at startup, but older ones silently ignore a value like Enabled and leave authorization off. Then log in to check it worked:

mongosh --port 27017 --authenticationDatabase admin -u admin -p

Your app's connection string now needs the user name, password and auth database, for example mongodb://appuser:[email protected]:27017/appdb?authSource=appdb.

Docker publishes 27017

The official mongo image only turns on authentication when you set both MONGO_INITDB_ROOT_USERNAME and MONGO_INITDB_ROOT_PASSWORD. Without them, the container runs with no login at all. These variables only take effect when the data directory is empty, so adding them to an existing container changes nothing; you have to create the users yourself as above.

Then there's ports: - "27017:27017". Docker's firewall rules let that traffic in even when UFW blocks 27017. If only other containers use the database, remove ports:. If the host needs it, publish on loopback with "127.0.0.1:27017:27017" and recreate the container. More in Docker bypasses UFW.

Remote admin over SSH

For Compass or mongosh on your laptop, forward a local port instead of opening 27017:

ssh -N -L 27018:127.0.0.1:27017 [email protected]

Connect to mongodb://localhost:27018 while that runs, with your admin user. Finish by running the outside check again: the port should no longer answer.

Stenvakt checks this on every server. Its Review is a read-only check of each server, and the database guard finds a MongoDB that answers from outside or runs without authorization, with Back up now before you change anything. You see a Preview of every fix before it runs, and fixes run in safe mode: a snapshot first, a rollback timer and a fresh-connection test. It's a Mac app and isn't out yet: get early access.

Sources

  1. MongoDB docs: IP Binding in Self-Managed Deployments
  2. MongoDB docs: Configuration File Options (net.bindIp, net.bindIpAll, security.authorization)
  3. MongoDB docs: Configure SCRAM authentication (create the user administrator)
  4. MongoDB docs: Security Checklist
  5. Krebs on Security: Extortionists Wipe Thousands of Databases, Victims Who Pay Up Get Stiffed (2017)
Next guideMySQL bind-address