MongoDB 的权限控制有一个很常见的误区:很多人为了省事,直接给应用账号分配 root 角色或者 readWriteAnyDatabase 这种超级权限。一旦应用代码存在注入漏洞或者配置泄露,攻击者就能直接拖走整个数据库,甚至删库。解决这个问题的核心思路就是遵循最小权限原则,只授予应用完成其工作所必需的最低限度权限。下面直接拆解 MongoDB 内置的最小权限角色,以及如何针对真实业务场景自定义这种角色。
理解内置角色的权限边界MongoDB 提供了几个细颗粒度的内置角色,它们远比 readWrite 这类角色更接近最小权限。首先是 read 角色,它只允许读取指定数据库中的普通集合数据,无法写入任何数据,也无法读取系统集合。如果你的微服务只是一个查询服务,那么 read 角色就是它的上限,不要再给多余权限。
其次是 readWrite 角色,它包含 read 的所有权限,外加对普通集合的插入、更新、删除操作。但很多人不知道,readWrite 依然无法执行创建或删除集合、创建索引这类结构性操作。如果你的应用需要动态建表或者创建索引,那么 readWrite 是不够的,这时候需要评估是否真的需要这种能力,还是可以在部署阶段预先建好集合。
还有一个容易被忽略的角色是 clusterMonitor。运维监控工具只需要查看服务器状态、慢查询日志、副本集状态,完全不需要访问业务数据。给监控账号分配 clusterMonitor 角色,它就只拥有查看集群信息的权限,无法读写任何用户数据。这是典型的最小权限实践。
针对集合级别的精细化控制内置角色通常作用在整个数据库级别,但很多场景下,一个应用只需要访问数据库中的某几个集合。MongoDB 支持在角色定义中限定具体的集合名称,实现集合级别的权限控制。你可以创建一个自定义角色,精确指定允许操作的集合和操作类型。
具体做法是使用 createRole 命令,在 privileges 数组中为每个集合单独定义权限。例如,一个订单查询服务只需要读取 orders 和 products 两个集合,你可以这样定义:
db.createRole({
role: "orderQueryRole",
privileges: [
{
resource: { db: "ecommerce", collection: "orders" },
actions: ["find"]
},
{
resource: { db: "ecommerce", collection: "products" },
actions: ["find"]
}
],
roles: []
})
这个角色创建完成后,拥有该角色的用户就只能对 ecommerce 数据库下的 orders 和 products 集合执行 find 操作,无法读取其他任何集合,也无法执行 count、aggregate 等其他读操作。如果你的查询确实需要 aggregate,那么就在 actions 数组中加上 "aggregate",但不要一股脑把所有读操作都加上。
限定具体操作动作MongoDB 的权限动作可以精确到单个操作命令。find、insert、update、remove 这些是常见的 CRUD 操作,但还有更细的区分。比如 update 操作可以拆分为更具体的权限需求:如果应用只允许更新某个字段,你可以在应用层控制,但数据库层面仍然需要 update 权限。不过你可以限制该角色只能对特定集合执行 update,而不能删除文档,这就需要在 actions 中只包含 "update",不包含 "remove"。
还有一个关键动作是 "killCursors"。很多开发者不知道,如果用户没有 killCursors 权限,当查询返回游标后,游标会在服务器端自动超时关闭,但应用无法主动关闭游标。这在大多数场景下不是问题,但如果你的应用需要精确控制游标生命周期,就需要加上这个权限。反之,如果不加,反而是一种隐式的安全限制。
对于需要执行聚合管道的场景,不要直接给 "aggregate" 权限就完事。聚合管道中如果使用了 $out 或 $merge 阶段,实际上是在写入数据,这需要额外的写入权限。如果角色只有 "aggregate" 而没有 "insert" 或 "update",那么包含 $out 的聚合命令会失败。这是一种安全机制,确保你明确知道自己在授予写入能力。
使用视图隔离敏感字段最小权限不只是在操作类型上做限制,还可以在数据可见性上做文章。假设一个用户表包含密码哈希、手机号等敏感字段,而客服系统只需要查询用户的基本信息。你可以创建一个视图,只暴露非敏感字段,然后给客服系统的账号只授予对这个视图的 find 权限。
具体操作是先创建视图:
db.createView(
"userPublicInfo",
"users",
[
{ $project: { username: 1, email: 1, registerDate: 1, _id: 0 } }
]
)
然后创建一个只对该视图有读取权限的角色:
db.createRole({
role: "csQueryRole",
privileges: [
{
resource: { db: "app", collection: "userPublicInfo" },
actions: ["find"]
}
],
roles: []
})
这样客服系统连原始 users 集合都看不到,更谈不上读取敏感字段。即使应用层出现 SQL 注入式的拼接错误,攻击者也只能拿到视图中的有限字段。这种视图加最小权限的组合策略,是纵深防御的典型手段。
限制系统集合的访问MongoDB 的每个数据库都包含系统集合,比如 system.indexes、system.profile 等。内置的 readWrite 角色默认不允许读取这些系统集合,但 dbAdmin 角色可以。如果你自定义角色时使用了通配符资源或者直接给了对整个数据库的读权限,可能会意外暴露系统集合。
在定义 privileges 时,resource 可以指定 collection 为空字符串来表示对整个数据库的所有集合生效,但这样做风险较大。更好的做法是明确列出需要访问的普通集合,系统集合自然就被排除在外。如果你的应用确实需要读取某个系统集合,那就单独为它添加一条 privilege,而不是放开整个数据库。
跨数据库权限的隔离有些应用需要跨数据库读取数据,比如一个报表服务需要从多个业务库中聚合数据。这种情况下,很多人会直接授予 readAnyDatabase 角色,这完全违背了最小权限原则。正确的做法是创建一个自定义角色,在 privileges 数组中分别为每个需要的数据库和集合添加读取权限。
例如,报表服务需要读取 sales 库的 orders 集合和 inventory 库的 products 集合:
db.createRole({
role: "reportRole",
privileges: [
{
resource: { db: "sales", collection: "orders" },
actions: ["find", "aggregate"]
},
{
resource: { db: "inventory", collection: "products" },
actions: ["find"]
}
],
roles: []
})
这个角色只能访问两个指定的集合,其他数据库和集合完全不可见。即使报表服务的凭证泄露,攻击者也无法访问用户库、日志库等其他数据。
分片集群中的角色考量在分片集群环境下,最小权限还需要考虑 config 数据库的访问。mongos 路由层需要读取配置信息来路由查询,但应用账号通常不需要直接访问 config 数据库。如果你给应用账号授予了 clusterAdmin 或者对 config 数据库的读取权限,就暴露了分片集群的拓扑信息、分片键范围等敏感元数据。
除非你的应用确实需要执行 splitChunk、moveChunk 这类管理操作,否则永远不要给应用账号授予任何集群管理角色。即使是监控工具,也应该使用 clusterMonitor 角色,而不是 clusterAdmin。clusterMonitor 只能读取集群状态,不能执行任何修改操作。
定期审计和回收权限最小权限不是一次性配置就完事的。业务需求会变化,应用可能会新增功能,原本不需要的权限后来变得需要了,或者原本需要的权限后来不再使用了。建议每季度对数据库账号进行一次权限审计,检查每个自定义角色的 privileges 是否仍然合理。
MongoDB 提供了 db.getRole() 和 db.getUser() 命令来查看当前角色和用户的权限定义。你可以导出这些定义,与业务方确认每个权限的必要性。对于不再使用的角色,及时删除;对于权限过大的角色,及时收紧。这种持续治理的机制,才是最小权限原则落地的保障。
还有一个容易被忽视的点:临时权限。开发人员排查线上问题时,有时需要临时提升权限。这种情况下,不要直接给现有账号加角色,而是创建一个临时账号,设置过期时间,用完即删。MongoDB 支持在创建用户时通过 createUser 命令的 authenticationRestrictions 和自定义过期机制来管理临时访问,但最简单的方式是手动创建并及时删除。
通过以上这些具体的方法,你可以构建一个真正符合最小权限原则的 MongoDB 权限体系。核心思路始终是:先搞清楚应用到底需要执行哪些操作、访问哪些数据,然后精确授予这些权限,多一个动作都不给。这样做虽然前期配置稍显繁琐,但能从根本上降低数据泄露和误操作的风险。
