有一次客户现场做安全基线核查,对方工程师坐下来第一句话就把我问住了:“你们的MongoDB审计日志开在哪台节点?现在拉一份上周管理员登录和删数据的记录出来看看。”当时我们监控、备份、慢日志都有,唯独“审计日志”这件事一直挂在待办清单上没落地。核查结果自然多了一条整改项。那之后我把这套东西从头到尾研究了一遍,才发现MongoDB审计日志真不是“打开一个开关”这么简单。它牵扯到版本边界、过滤策略、格式选型、磁盘预算、日志轮转,哪一环没想清楚都会在生产环境给你造成事故级别的反噬。这篇文章把我自己在生产环境配置MongoDB审计日志,并用来满足合规性安全记录要求的完整过程写出来,包括配置方法、过滤器设计、日志格式解读、性能影响和踩坑记录。无论你是正在应付外部审计、内部合规检查,还是提前给自己留一条后路,都可以直接参考。
1. 合规审查最常问的其实是“可证明性”:审计日志到底在补什么洞
1.1 从合规框架里反推数据库要留什么记录
大家习惯把合规理解成“一堆条条框框”,但落到数据库层面,各种合规框架的要求其实非常集中,翻来覆去就是几个共性点:
- 身份认证要留痕:谁在什么时间、从哪个IP、用什么账号尝试登录,成功还是失败。
- 权限变更要留痕:谁创建了用户、删除了角色、改了密码、授予或回收了权限。
- 敏感操作要留痕:谁删了集合、删了库、改了索引、批量更新或删除了数据。
- 数据可追溯:一个数据被人为改动或删除后,能不能回查到操作者、时间和来源地址。
这几条对MongoDB来说,天然对应的就是认证事件、用户角色管理事件、DDL事件和CUD事件。所以配置审计日志之前,先不要急着抄别人的filter,而是把你所在环境的合规基线列出来,然后回推“到底哪些动作必须记录”。否则你很可能要么漏掉了关键动作,要么把日志量撑爆。
1.2 为什么普通的mongod.log和慢日志替代不了审计
很多人会觉得,MongoDB已经有日志了,怎么还要单独搞一套审计?这里必须把三件事分开:
- mongod.log:主要记录启动信息、断言、复制集状态、慢操作警告等,它不会告诉你“某个用户执行了某个查询”。
- 数据库剖析器(profiler):本质是性能诊断工具。它把超过阈值的操作写进system.profile集合,记录的是耗时,不是安全事件。你可以把它理解为业务巡检,不是合规取证。
- 审计日志:它在数据库内核里按事件切片记录身份认证、授权检查、命令执行等行为,每条记录都带时间戳、用户、角色、来源地址和结果状态。这才是一条完整的“证据链”。
合规场景里,审查方要的是一个动作发生之后可以追溯全过程的客观记录。mongod.log帮不了你这个忙,profiler也不行。只有审计日志能把“谁、何时、从哪、做了什么、结果如何”一次性给你。
1.3 版本边界决定了你的方案起点
这里有一个必须一开始就说清的硬边界:MongoDB系统内置的审计日志能力属于企业版功能,社区版默认不带。如果你用的是MongoDB Community Server,那这篇文里讲的原生审计配置是没法直接用的。对应的两个现实方案是:
- 升级到MongoDB Enterprise,或者使用MongoDB Atlas云托管,Atlas默认提供审计日志能力,开一下就能用。
- 继续留在社区版,但需要通过统一接入层、代理层做额外的访问记录,或者用旁路抓包之类的方案做变通,这种做法的取证效力远不如原生审计,而且实现成本不低。
所以配置之前,先执行下db.version()和db.serverCmdLineOpts()确认版本。我见过不止一个人在企业版和社区版之间反复横跳,照着企业版文档配置社区版,最后启动都起不来。
2. 系统审计、慢日志与授权日志,三条线先分清再动手
2.1 三个容易混淆的日志机制
我在刚开始接触MongoDB审计时,最困惑的就是“为什么有好几个开关都和日志有关”。后来我把它们逐一同实际效果对照,才算彻底理清。
第一个是系统审计日志。它由mongod.conf里的auditLog配置段控制,负责把审计事件持久化到文件、syslog或控制台。这条线才是合规性安全记录的正主。
第二个是数据库剖析器。你通过设置profile级别打开,慢操作会被写入system.profile集合。它适合用来抓慢查询,不适合用来做安全记录。因为profiler的采样和阈值机制决定了它天然会漏事件,而且它本身存在数据库内,用户如果权限够大是可以改的。
第三个是auditAuthorizationSuccess参数。这个参数很多人第一次看到时会懵。它控制的是“授权检查成功”的事件是否被记录。默认值是false,也就是系统只记录授权失败,授权成功不记。如果把它设为true,数据库中每一次授权检查通过都会生成一条审计事件,日志量会以肉眼可见的速度爆炸。你要清楚自己到底要不要开它。
2.2 这三条线的分工和取舍
用一张表把这三种机制放在一起,对照看会更清楚:
| 机制 | 主要用途 | 能否作为合规审计依据 | 开销特征 |
|---|---|---|---|
| 系统审计日志(auditLog) | 安全记录、合规取证 | 能,且是原生能力 | 与过滤事件量成正比,需规划磁盘 |
| profiler慢日志 | 性能诊断、慢查询分析 | 不能,会漏事件,且存在库内 | 只记录超阈值操作,开销较低 |
| auditAuthorizationSuccess | 授权成功事件的补充记录 | 可作辅助,但需谨慎开启 | 开启后事件量剧增,不建议全局开 |
从我落地的经验看,合规性安全记录以系统审计日志为主,auditAuthorizationSuccess只在特殊场景下按需开,profiler老老实实管性能,三者各管一摊,别混在一起用。
3. 从零落地配置:yaml参数、运行时切换与过滤器设计
3.1 前置检查:磁盘、目录、权限
真正动手写配置之前,有三件事必须提前确认,不然后面会返工:
- 磁盘空间:审计日志是写盘动作,必须先估算空间,预留出至少30天的容量,再把审计目录放到独立分区或独立磁盘上。这个估算细节我会在后面单独讲。
- 目录权限:审计日志文件需要由运行mongod的操作系统用户写入。比如mongod以mongod用户运行,那审计日志目录就必须是mongod用户可写的。最常见的翻车点就是目录归root所有,配好之后mongod直接启动失败。
- 运行时切换能力:
auditLog参数可以通过setParameter在运行时开启,但如果你想在配置文件里固化,就需要重启mongod生效。从我的习惯来说,生产环境我建议直接写进配置文件,避免某个实例重启后审计状态和预期不一致。
3.2 配置文件方式:最稳妥的落地方式
在mongod.conf里增加下面的配置段:
auditLog: destination: file format: JSON path: /var/log/mongodb/audit/audit.log filter: '{ atype: { $in: [ "authenticate", "createUser", "dropUser", "alterUser", "grantRolesToUser", "revokeRolesFromUser", "createCollection", "dropCollection", "dropDatabase", "renameCollection", "insert", "update", "delete", "createIndex", "dropIndex", "killOp" ] } }'简单解释下几个参数:
destination:可选file、syslog、console。生产环境一般用file,便于独立采集和轮转;有多节点统一收日志的需求时,可以选syslog对接系统日志。代价是syslog模式下格式会被打平,部分结构化信息不好还原。format:可选JSON或BSON。JSON直观、grep方便;BSON体积更小,但需要bsondump工具来读取。我建议生产优先用JSON,排查问题效率高,存储成本差别可控。path:destination为file时必须指定,目录要提前创建好,并且让mongod启动用户具备写权限。filter:这是整个审计配置里最关键的字段。它决定哪些事件被记录,哪些被丢弃。
3.3 运行时开启方式:应急场景下的备用手段
如果配置已经写进yaml,正常重启生效就够了。但有些场景下你不可能马上重启实例,比如业务高峰期或主从切换窗口不好申请,这时候可以使用运行时参数动态开启:
db.getSiblingDB("admin").runCommand({ setParameter: 1, auditLog: { destination: "file", format: "JSON", path: "/var/log/mongodb/audit/audit.log", filter: '{ atype: { $in: [ "authenticate", "createUser", "delete", "dropCollection" ] } }' } })执行成功后,审计日志会立即开始写入。注意,setParameter动态修改的对象是内存配置,重启后如果mongod.conf里没有对应配置,审计会恢复原状。所以我通常把运行时开启当作应急手段,事后一定会把配置固化进yaml,再找窗口重启一次。
3.4 过滤器设计:合规要求的最小必要集
过滤器设计是整个配置里最见功力的地方。logs全量开着当然最简单,但生产环境的操作频率非常高,全量记录不仅浪费磁盘,还会把真正要紧的事件淹没在海量无关记录里。我推荐按“最小必要集”来设计filter。
我的思路是先把合规要求拆成事件类型,再组合成一个filter。常用的atype不熟悉的话,可以先看第4节的枚举表。一个比较通用的生产配置是这样的:
{ "atype": { "$in": [ "authenticate", "createUser", "dropUser", "alterUser", "grantRolesToUser", "revokeRolesFromUser", "createCollection", "dropCollection", "dropDatabase", "renameCollection", "insert", "update", "delete", "createIndex", "dropIndex" ] } }这套filter覆盖了登录认证、用户权限变更、DDL结构和数据增删改。要注意的是,这里刻意没有加find。为什么?因为查询操作太频繁,一个正常的业务库每秒几十上百次find查询很正常,一开find,日志盘立刻告急。如果确实需要追踪敏感集合的读取行为,比如用户表、订单表,建议把这些集合的查询独立处理,而不是全库开启。
还可以用$or做更精细的用户维度过滤,比如只记录特定管理员账号的操作:
{ "$or": [ { "atype": { "$in": [ "authenticate", "createUser", "dropUser" ] } }, { "users.user": "admin" } ] }这样既能记录认证和用户管理事件,又能把admin的所有操作都纳入审计,覆盖面更贴近“特权账号重点监控”的合规思路。
3.5 验证配置是否生效
配置完成之后,我不建议直接重启完就撒手。验证这件事分两步走:
第一步,用getParameter确认运行时状态:
db.getSiblingDB("admin").runCommand({ getParameter: 1, auditLog: 1 })返回结果会显示当前生效的auditLog配置,包括destination、format、path和filter。你要核对filter里的转义是否正确,路径是否和你预期一致。
第二步,人为触发一条审计事件,然后去文件里翻记录。最简单的做法是执行一条db.createCollection("__audit_test__"),再drop掉它,然后到审计日志文件里查找对应的createCollection和dropCollection事件。如果一条都找不到,那多半是filter写错了或者文件权限有问题,需要立刻排查。
4. 日志内容逐字段拆解,拿到一条记录怎么读
4.1 一条认证日志是怎么写的
审计日志本质上是事件流,每条事件是一个JSON文档。先看一个常见的登录认证记录:
{ "atype": "authenticate", "ts": { "$date": "2024-06-12T03:21:45.123+0000" }, "uuid": { "$binary": "5f9c9c1e9cb1d1f6e6b0a0f0", "$type": "04" }, "local": { "ip": "10.0.3.11", "port": 27017 }, "remote": { "ip": "10.0.1.88", "port": 52844 }, "users": [{ "user": "app_service", "db": "admin" }], "roles": [{ "role": "readWrite", "db": "orders" }], "param": { "user": "app_service", "db": "admin", "mechanism": "SCRAM-SHA-256" }, "result": 0 }核心字段的含义:
atype:事件类型。authenticate就是认证。ts:事件发生时间,格式是UTC时间戳。local和remote:local是mongod所在节点IP和端口,remote是发起操作的客户端IP和端口。调查安全问题的时候,remote的价值甚至高于用户名。users和roles:操作关联到的用户及其当前角色。param:事件附加参数,认证事件里是用户名、认证库、认证机制。result:执行结果,0表示成功,非0表示失败,具体错误码需要对照MongoDB错误码表。
如果登录失败,result会是一个非0值,对应的param里一般会出现错误信息。排查暴力破解或者错误密码告警时,就是靠这些字段来聚合统计的。
4.2 敏感数据删除事件如何解读
再看一个数据删除的审计事件。我在配置filter时把delete列进去了,所以一旦有人执行删除,日志里会留下类似下面的记录:
{ "atype": "delete", "ts": { "$date": "2024-06-12T09:15:02.456+0000" }, "remote": { "ip": "192.168.1.25", "port": 59321 }, "users": [{ "user": "data_ops", "db": "admin" }], "roles": [{ "role": "dbOwner", "db": "orders" }], "param": { "command": "delete", "ns": "orders.orders", "query": { "status": "cancelled" }, "limit": 0 }, "result": 0 }这里的param.ns是完整的“库名.集合名”,param.query是删除条件。拿到这条记录,你就能还原“某人在某个时间删掉了orders集合里所有status为cancelled的文档”。这就是合规审计里最典型的“可证明性”。
4.3 常用atype枚举速查表
掌握审计日志的关键在于对atype做到心里有数。我整理了一份高频事件表:
| atype | 含义 | 合规相关性 |
|---|---|---|
| authenticate | 认证成功或失败 | 高 |
| logout | 用户登出 | 中 |
| createUser / dropUser | 创建/删除用户 | 高 |
| alterUser | 修改密码、自定义数据 | 高 |
| grantRolesToUser / revokeRolesFromUser | 授权/回收角色 | 高 |
| createCollection / dropCollection | 创建/删除集合 | 高 |
| dropDatabase | 删除数据库 | 高 |
| renameCollection | 重命名集合 | 高 |
| insert / update / delete | 增删改数据 | 高 |
| createIndex / dropIndex | 创建/删除索引 | 中 |
| killOp | 终止操作 | 中 |
| find | 查询操作 | 低(按需开启) |
你这套filter如何组合,完全可以按照这张表去和自己的合规要求一一对应。
4.4 日志文件不是给人直接看的,要会用工具
如果format配置的是JSON,读取很简单,grep、jq都能处理。如果要快速查看一条记录,直接tail文件即可。但审计日志通常很大,靠人眼不太现实。
我平时最常用的动作是按atype过滤:
grep '"atype":"delete"' /var/log/mongodb/audit/audit.log | tail -20更进阶一点,用jq做结构化筛选,比如找出所有authenticate失败记录:
jq -c 'select(.atype=="authenticate" and .result != 0) | {ts, remote, users, param}' /var/log/mongodb/audit/audit.log一次输出一条,内容很干净,适合直接交给排查或检查方阅读。
如果当初图省事选择了BSON格式,那么就需要用MongoDB自带的bsondump来转换:
bsondump /var/log/mongodb/audit/audit.log | head -20这一点决定了你刚开始选格式时要想清楚。我自己第一套审计配置选了BSON,后来排查问题时每次都先转格式,非常别扭,之后新环境基本统一用JSON。
5. 性能账单和磁盘预算:开审计之前先算清楚
5.1 审计日志的写入链路和性能开销
审计日志不是异步队列,它对每条匹配到过滤器的事件,在事件发生路径上同步追加记录。也就是说,一条insert或delete操作在被记录之后,客户端才会收到响应。这个设计保证了“操作发生了,记录一定存在”,但也意味着它会增加请求链路的耗时。
写操作本身就是磁盘动作,审计日志再来一次磁盘写,IO压力自然翻倍。实际影响有多大,取决于两个因素:一是匹配到过滤器的操作量,二是日志文件落在什么存储上。我把审计目录独立放在SSD上时,业务侧几乎没有感知;但如果和数据文件挤在同一块饱和机械盘上,延迟能明显上升。所以我的建议很简单:审计目录独立、SSD、不与数据文件抢IO。
5.2 日志量估算公式和实际案例
磁盘预算必须在配置前算清楚。这里给一个可套用的估算思路:
单条审计事件体积约0.5KB到1KB,假设你的业务每秒产生100条匹配过滤器的审计事件,平均值取800B,那么一天新增量大约是:
100 events/s × 800B × 86400s ≈ 6.9GB/day按这个体量,保留30天就需要200GB以上空间。这还不包括并发尖峰、大量登录失败、开启find等额外因素。如果直接全量审计或者开了auditAuthorizationSuccess,这个数值会轻松翻几倍到几十倍。
我在一个日均请求量较高的实例上看过一次教训,配置时为了“全面记录一切”,加上了find和auditAuthorizationSuccess: true,一周之后审计日志吃了将近500GB磁盘,最后还是靠紧急扩容和重建过滤器才缓过来。
5.3 轮转策略:不轮转就是把磁盘当炸弹
审计日志如果不做轮转,必然会把磁盘写满。磁盘满了对MongoDB的影响不是报错那么简单,WiredTiger会因为无法继续写入而进入异常状态,整个实例的可用性都会受到威胁。所以轮转不是可选项,是必选项。
我在生产环境用的是两层配合:
第一层,使用MongoDB自带的主动轮转命令:
db.adminCommand({ logRotate: "audit" })这条命令会将当前审计文件关闭并重命名,然后创建新的日志文件。可以在脚本里定期执行。
第二层,配合系统logrotate配置,实现按天或按大小轮转:
/var/log/mongodb/audit/audit.log { daily rotate 30 missingok compress delaycompress copytruncate notifempty }copytruncate是这里的关键,它先复制文件再清空原文件,可以避免mongod持有的文件描述符出问题。轮转之后顺手启用压缩,一个月前的日志能被压掉一大半,对空间预算非常友好。
5.4 高并发连接和认证风暴容易被忽视
还有一类隐蔽的性能隐患是认证事件本身。客户端短连接模式下,每次请求都重新建连认证,审计日志里就会持续写入authenticate事件。如果你用的是连接池,长期复用连接,认证事件量就小很多。这是架构层面影响审计日志量的一个隐形变量。我在配置完审计之后还顺手检查了业务侧MongoDB驱动配置,统一改成连接池复用,日志量肉眼可见地下降了一大截。
6. 翻车现场记录:权限、语法和轮转的四个典型案例
6.1 案例一:filter语法错了却以为开了审计
某次我在一台测试库上手动执行setParameter开启审计,命令返回了ok: 1,我就没再管。过了两天想看日志,发现文件里只有一条初始化记录,什么操作都没录进去。排查后发现是我在写filter时把JSON写成了EJSON格式,MongoDB解析后认为这个filter匹配不到任何事件,但setParameter本身并没有报致命错误。
这个坑提醒我两件事:一是filter字符串必须严格使用MongoDB查询语法,二是开启后必须按3.5节的步骤做一次事件验证。以后再也没敢只看到ok: 1就收工。
6.2 案例二:审计目录权限不对,mongod直接起不来
还有一次是配置完yaml后重启mongod,启动失败,错误日志里明确写着权限拒绝。查了下原因,是我提前用root创建了/var/log/mongodb/audit/目录,但mongod是以mongod用户运行的,对那个目录没有写权限。这类问题在缺少系统管理经验的场景里特别容易遇到。
解决办法也简单,把目录属主改给mongod用户:
chown -R mongod:mongod /var/log/mongodb/audit启动脚本里最好再加一步目录自动创建的逻辑,避免下次部署新节点时再栽一遍。
6.3 案例三:开了find和授权成功记录,一周撑爆磁盘
这个案例前面提到过,是我自己走过的一段弯路。刚开始觉得“审计要全面一点,所有操作都记下来才安全”,于是filter里加上了find,还把auditAuthorizationSuccess设成了true。结果一周后磁盘告警,500GB磁盘空间被审计日志塞得快要溢出。
后来我把filter恢复成最小必要集,去掉find,auditAuthorizationSuccess改回false,只有真正需要追踪的敏感集合查询才单独加规则。磁盘占用立刻降了下来。这件事给我最深的体会是:审计的价值在于可追溯关键事件,而不是把所有流量变成一堆没人看的数据。
6.4 案例四:没有做轮转,审计日志反噬了业务
最后一个案例也很有代表性。某套实例配置审计之后长期稳定,我就没再关注日志文件。直到某天发现MongoDB写入延迟异常升高,部分写入超时,一查才发现审计日志文件已经涨到了几十GB,所在的文件系统接近写满,WiredTiger在自动做大量额外处理来应对剩余空间不足。
处理过程中一边紧急清理旧日志,一边把对应命令写进轮转配置。从那以后,我养成了一个习惯:每次部署审计配置,会把磁盘剩余空间、预计写入速率、轮转策略一起作为交付项纳入检查单,不在这件事上留任何侥幸空间。
审计日志这东西平时确实没人看,但真到了要举证、要复盘、要应付检查的时候,它的价值比监控面板高得多。建议你按这篇文章里的思路检查一下自己手上的实例,看看解析器版本是否支持原生审计,配置是否已经固化到yaml文件,日志目录是否撑得住30天的留存。别等到审计人员开口问记录的时候,才临时跑去开开关。