本节摘要:认证用 SCRAM 机制验证身份,授权用基于角色的权限控制(RBAC)限定操作范围。本节从一次公网裸奔实例被拖库的事故讲认证启用流程、内置角色体系与最小权限实践。
安全团队扫描发现某测试实例以默认配置暴露在公网:bindIp: 0.0.0.0、未启用认证、云安全组放行 27017 全网。两周内被扫描器发现,数据库被清空,留下一条勒索留言,要求付比特币恢复数据。没有备份,测试数据全部重建。同一年,全球有数万个 MongoDB 实例经历了同样的剧本。
拖库者甚至不需要漏洞——门开着,锁没装。
# 1. 无认证模式下启动,先建管理员 mongod --config /etc/mongod.conf # 尚未开 authorization
use admin db.createUser({ user: "rootAdmin", pwd: passwordPrompt(), roles: [ { role: "root", db: "admin" } ] });
# 2. 配置文件开启认证后重启 security: authorization: enabled
// 3. 为应用建最小权限业务账号(不要用管理员连应用) use trade db.createUser({ user: "tradeApp", pwd: passwordPrompt(), roles: [ { role: "readWrite", db: "trade" } ] });
| 角色 | 权限 | 用途 |
|---|---|---|
| read / readWrite | 单库读写 | 应用账号 |
| dbAdmin | 单库管理(索引、统计) | DBA 例行维护 |
| userAdmin | 单库用户管理 | 权限自助 |
| clusterMonitor | 集群只读监控 | 监控探针 |
| backup / restore | 备份恢复工具 | mongodump 专用 |
| root | 全部 | 唯一管理员,人肉登录 |

⚠️ 应用直连 root 账号是最普遍的坏习惯:一次 SQL 注入式的条件注入攻击,就能拿到删库权限。每个应用一个库、一个 readWrite 账号。
事后从云厂商的流量记录倒推出了完整时间线。第 0 天,测试实例为"方便联调"临时改成 bindIp 0.0.0.0 并在安全组放行 27017,计划联调结束就收回,但没人记得。第 2 天凌晨,来自三个不同 IP 的扫描器完成端口指纹识别——27017 加上未认证的 mongod 响应特征,在扫描器眼里和路灯一样显眼。第 2 天到第 9 天,有人试探性连接,建了一个名为 warning 的集合留言"联系邮箱恢复数据",此时还没有破坏。第 10 天,第二次访问执行了 listDatabases 加 dropDatabase,全部数据消失,库被替换成一条勒索留言。整个过程中实例没有任何异常告警——因为没有任何监控在盯一个"临时"实例。
// 拖库者视角只需要三条命令,门开着时 show dbs; // 1. 看看有什么 use trade; show collections; db.getSiblingDB("trade").dropDatabase(); // 3. 删库勒索
整改分四步:全部实例清点 bindIp 与安全组,临时放行必须带到期时间;认证当天启用,管理员与应用账号分离;为测试环境也配每日备份——测试数据也是劳动成果;把"新增对外端口"纳入变更审批。四步里最便宜也最有效的是第一条,机器学习扫描器找的就是那一扇没关的门。
内置角色不够细时,用自定义角色把权限压到集合与动作级。典型场景:报表服务只需要读两个集合、连 delete 权限都不该有;数据分析同学需要临时查询,但绝不能触碰用户库。
db.getSiblingDB("admin").createRole({ role: "tradeReportReader", privileges: [ { resource: { db: "trade", collection: "orders" }, actions: [ "find" ] }, { resource: { db: "trade", collection: "users" }, actions: [ "find" ] } ], roles: [] }); db.getSiblingDB("trade").createUser({ user: "reporter", pwd: passwordPrompt(), roles: [ "tradeReportReader" ] }); // 验证:reporter 尝试写入应被拒绝
权限评审的口径与第 8 章的事务口径同构:说不出"这个账号为什么需要这个动作",权限就不该给。账号每季度过一遍 connectionStatus 与用户清单,离职与下线服务的账号当天回收——拖库事故里最贵的从来不是攻击者,是忘了收回的钥匙。
认证的实际强度落在口令管理上。三条现实版策略:第一,所有账号口令走密码管理工具生成,长度二十起步,SCRAM 对线下爆破的抵抗能力足够,弱在人对口令的复用;第二,应用账号的口令进配置中心并每季度轮换,轮换动作写成脚本、先双写新旧口令观察一天再废弃旧的,避免轮换变事故;第三,人肉登录一律走跳板机并记录会话,root 账号的每次登录都该留痕。口令策略不需要多复杂,需要的是真的被执行——拖库事故的复盘里,攻击者用的从来不是破译,是复用与撞库。
// 口令轮换的零中断做法:新建新账号 → 观察流量 → 删旧账号 db.getSiblingDB("trade").createUser({ user: "tradeApp2", pwd: passwordPrompt(), roles: [ { role: "readWrite", db: "trade" } ] }); db.getSiblingDB("trade").dropUser("tradeApp"); // 确认新账号承接全部流量后再执行
还有一条容易被忽略的防线:authenticationRestrictions 可以把账号钉死在指定来源 IP 上,即使口令泄露,来自其他地址的连接也会被直接拒绝。对应用账号与 root 账号都值得配上。