操作系统内功 · 第 13 篇:Linux 安全审计——auditd、journald 与合规日志

出了安全事件,谁来告诉你「谁干的」

半夜三点的告警里有一句「生产目录 /data/release 下的配置文件被删了」。接下来的问题链条很具体:是哪个进程删的?以哪个登录身份执行的?从哪个终端发起的?前后还动了哪些文件?应用日志回答不了这些问题——应用只知道自己的业务行为;/var/log/messages 里的 syslog 也回答不了——syslog 记录的是「程序主动说了什么」,而不是「内核里真实发生了什么」。能回答这条问题链的是 Linux 内核的审计子系统(audit subsystem)加上用户态的 auditd 守护进程:它挂在系统调用路径上,把每次文件删除、权限变更、进程执行的完整上下文——进程 PID、登录身份、可执行文件、参数——原样记录下来。而 journald 是另一条管线:它汇总 systemd 单元和各类服务的应用日志,是排障的主力,但它看不到系统调用层。两条管线职责完全不同,混为一谈是安全事件复盘时最常见的第一个坑。这篇讲清楚两者的机制边界、规则写法和合规留痕的工程做法。

flowchart LR
    A["进程执行系统调用<br/>unlink / execve / chmod"] --> B["内核审计子系统<br/>规则过滤(auditctl 规则集)"]
    B -- "命中规则" --> C["组装审计事件<br/>SYSCALL + PATH + CWD + PROCTITLE"]
    C --> D["netlink 套接字<br/>(内核到用户态通道)"]
    D --> E["auditd 守护进程"]
    E --> F["/var/log/audit/audit.log"]
    E --> G["audisp 转发插件<br/>→ 远端日志服务器/SIEM"]
    B -- "未命中" --> H["不产生事件<br/>(零开销直通)"]
    T1["systemd 单元 stdout/stderr"] --> J["journald"]
    T2["服务写 /dev/log(syslog 接口)"] --> J
    J --> K["/run/log/journal(内存)<br/>/var/log/journal(持久化)"]

auditd:内核态的审计流水线

audit 子系统的特殊之处在于事件产生于内核态。审计规则加载进内核后,内核在系统调用的退出路径上做过滤:命中的调用会触发事件组装,事件里包含这条系统调用的一整套上下文——系统调用号与参数、返回值、进程的 PID/PPID、有效 UID/GID、审计 UID(auid,稍后详述)、可执行文件路径、当前工作目录、以及涉及文件的 inode 与权限位。事件通过 netlink 套接字推给用户态的 auditd 进程,auditd 落盘到 /var/log/audit/audit.log,并可通过 audispd(新版内置为 audisp-remote 等插件)转发到远端。这个架构决定了 auditd 的三个特性:它记录的是内核视角的客观事实,不依赖应用自觉打日志;规则在内核里执行,auditd 进程死了规则依然生效(事件会堆在内核缓冲区,auditd 恢复后补录);未命中规则的系统调用零额外开销,这是它和全量 strace 类工具的本质区别。

看一条真实的审计记录长什么样:

type=SYSCALL msg=audit(1772925821.312:874) : arch=x86_64 syscall=unlinkat
success=yes exit=0 a0=AT_FDCWD a1=0x558e2c1a4ea0 items=1 ppid=3182 pid=3455
auid=1000 uid=1000 gid=1000 euid=1000 tty=pts1 ses=4 exe=/usr/bin/rm
key="file-delete"
type=CWD msg=audit(1772925821.312:874) : cwd=/home/deploy
type=PATH msg=audit(1772925821.312:874) : item=0 name="app.lock"
inode=1182353 dev=fc:01 mode=0100644 ouid=1000 ogid=1000
type=PROCTITLE msg=audit(1772925821.312:874) : proctitle=726D00...2F...

同一事件号(874)下的多条记录拼出完整现场:rm 进程(PID 3455)由 auid=1000 的登录会话(ses=4)在 pts1 终端上发起,删除了 /home/deploy 下的 app.lock。注意 auid(audit uid)——它是登录时固化在进程身上的登录身份,之后即使 sudo su、切到 root,auid 也不变。这是审计系统最精妙的设计:你永远能回答「哪个登录账号干的」,哪怕操作时已经变成了 root。相对地,uid 字段只反映执行瞬间的有效身份。

auditctl 规则:文件监控与系统调用监控两类语法

审计规则分两类。文件监控(watch)盯住特定路径的属性变化,语法最简:

$ sudo auditctl -w /etc/passwd -p wa -k identity
$ sudo auditctl -w /etc/sudoers -p wa -k scope
$ sudo auditctl -w /etc/ssh/sshd_config -p rwxa -k sshd-config

-p 后是关注的事件位:w 写、a 属性变更(chmod/chown/时间戳)、r 读、x 执行;-k 是检索用的标签。对关键系统文件的监控,POSIX ACL、SELinux 上下文变更等属性操作都会被 -p a 捕获。系统调用规则则更灵活,按架构 + 系统调用 + 过滤条件组合:

$ sudo auditctl -a always,exit -F arch=b64 -S unlink,unlinkat -F auid>=1000 -F auid!=4294967295 -k file-delete
$ sudo auditctl -a always,exit -F arch=b64 -S execve -F auid>=1000 -k commands

第一条监控所有登录用户(auid≥1000,4294967295 即 0xFFFFFFFF 是「无登录身份」的哨兵值)的文件删除;第二条记录所有人工登录会话执行过的命令(execve),配合 proctitle 记录等于拿到了操作历史。生产环境的规则集写在 /etc/audit/rules.d/*.rules 里,重启或 augenrules --load 时统一加载:

# /etc/audit/rules.d/90-custom.rules
-D
-b 8192
-w /etc/passwd -p wa -k identity
-w /etc/shadow -p wa -k identity
-w /etc/sudoers -p wa -k scope
-a always,exit -F arch=b64 -S unlink,unlinkat -F auid>=1000 -k file-delete
-a always,exit -F arch=b64 -S execve -F auid>=1000 -F auid!=unset -k commands
-e 2

-b 8192 设内核事件缓冲区;最后一行 -e 2 是不可变模式:规则加载后任何进程(包括 root)都不能增删审计规则,直到重启——这是合规场景的标配,代价是改规则必须计划重启。

ausearch 与 aureport:把内核事件变成人话

内核审计记录的时间戳是 epoch、字段是机器格式,直接 grep audit.log 很痛苦。ausearch 和 aureport 是配套的解码工具:

$ sudo ausearch -k file-delete --format text | tail -3
rm.unlinkat succeeded by deploy (pid=3455 parent=3182 session=4)
    cwd=/home/deploy, exe=/usr/bin/rm, path=app.lock
    time=Thu Nov  5 10:23:41 2026

$ sudo aureport --summary
Summary Report
======================
Range of time in logs: 11/04/2026 08:00:01.521 - 11/05/2026 11:12:40.003
Number of changes in configuration: 4
Number of login records: 17
Number of failed login records: 2
Number of user sessions: 9
Number of terminal I/O records: 0
Number of commands: 1384
Number of files: 214
Number of file deletes: 3

日常巡检最常用的组合是 aureport --failed(失败登录与权限拒绝)、aureport -x --summary(命令执行统计)、ausearch -m USER_LOGIN(登录明细)、ausearch --start today -k scope(今天对 sudoers 的一切改动)。对容器环境再补一条:docker exec 的命令执行同样会走 execve 被记录,exe 指向容器内路径、auid 是发起 exec 的宿主机登录者——这意味着给容器宿主机配上 execve 审计,就能回答「谁进了哪个容器执行了什么」这类云原生安全事件的核心问题,很多容器安全产品底层就是解析这份日志。

journald:排障主力与它的边界

journald 的定位完全不同:它接收 systemd 各单元的 stdout/stderr、通过 /dev/log 进来的 syslog 消息、以及内核日志(kmsg),按服务、时间、优先级、机器统一索引成结构化二进制日志:

$ journalctl -u nginx --since "10 min ago" --no-pager | tail -4
Nov 05 10:31:02 web-1 systemd[1]: Reloading nginx.service...
Nov 05 10:31:02 web-1 nginx[3231]: nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
Nov 05 10:31:02 web-1 systemd[1]: Reloaded nginx.service - A high performance web server.

它有几个审计场景下必须知道的边界。第一,journald 看不到系统调用——它记录的是进程「说出来的话」,进程不说话、或者绕过日志直接 syscall,journald 一无所知。「谁删了文件」这类问题必须走 auditd。第二,默认内存态、重启即失:只有 /var/log/journal 目录存在(或 Storage=persistent)时才持久化,journalctl --verify 可以校验日志完整性。第三,速率限制会静默丢日志:默认配置下单个服务 30 秒窗口内超过 10000 条消息,超出部分被丢弃并只留一条 _TRANSPORT=stdout 的丢弃提示——日志风暴时你以为服务没打日志,其实是 journald 掐了:

# /etc/systemd/journald.conf 关键项
[Journal]
Storage=persistent
RateLimitIntervalSec=30s
RateLimitBurst=10000
ForwardToSyslog=no

对高吞吐服务(每秒几千行日志的网关),要么调大这两个值,要么让服务自己落文件并配 logrotate,不要把全量日志压力放在 journald 上。第四,journald 的字段查询能力值得用:journalctl _PID=3455 _COMM=rm -o verbose 能按进程字段检索,journalctl -k 只看内核,journalctl --disk-usage 看占用,SystemMaxUse=2G 控制上限。日常排障它是最快路径;合规取证才轮到 auditd。

合规视角:不可篡改、集中留存与完整性校验

等保 2.0、PCI-DSS、SOC2 这类合规要求对 Linux 日志的共性条款是三条:集中留存(日志必须实时转发到独立于业务机的日志服务器,本机篡改不影响远端)、防篡改(本机日志删除/修改可被发现或不可行)、留存期(通常 6 个月以上)。落地到 Linux 的标准组合:auditd 通过 audisp-remote 插件把事件实时推到远端(网络断开时本地暂存,恢复后补传);journald/syslog 通过 ForwardToSyslog + rsyslog 的 TLS 远端转发实现第二路备份;本机用 -e 2 不可变规则锁死审计配置;远端接收侧把日志落到 append-only 介质或哈希链存储。一个常被问的细节是「auditd 能否被 root 绕过」:规则层面 -e 2 挡住了运行期修改,但进程仍可以通过不触发规则的方式操作(比如用 ptrace 家族、或直接改内核)。所以合规的纵深还要求:内核加锁(module 签名、secure boot)、最小化 root 使用(sudo 白名单 + auditd 的 sudoers 监控互相印证)、以及用「事实缺失」做告警——auditd 心跳(定期 aureport 的事件数)突然归零本身就是事件,说明有人动了审计链。

性能与陷阱:速率、缓冲与热路径规则

auditd 的开销模型是「未命中零开销、命中一次全套采集」。因此规则设计的第一原则是精确过滤,绝不上全量。反面案例:-a always,exit -S open 不加任何过滤,会让每次文件打开都产生事件——一次 npm install 就能生成几十万条记录,瞬间撑爆缓冲区。内核缓冲区(-b)满后新事件被丢弃,auditctl -s 的 lost 计数会增长:

$ sudo auditctl -s
enabled 1
failure 1
pid 891
rate_limit 0
backlog_limit 8192
lost 0
backlog 0

lost > 0 说明缓冲溢出过,事件有缺口,合规报告里这属于事故。另一个陷阱是32/64 位双架构:32 位程序的系统调用走 arch=b32 路径,只写了 -F arch=b64 的规则对它们无效——规则集里 32/64 各写一遍是标准做法。journald 侧的性能要点前面说过:限流参数按服务吞吐校准,持久化目录必须存在,SystemMaxUse 防止撑爆磁盘。最后一条运维纪律:审计规则变更要走变更单——因为规则直接决定「安全事件能否被看见」,一次手滑的 -D(清空规则)就是安全盲区,而这恰恰也是 -e 2 不可变模式存在的理由。

为什么你的应用会受这个影响

对应用工程师,这篇的落点是四件可以本周就做的事。其一,给关键路径上保险:花十分钟给 /etc/passwd、/etc/sudoers、应用配置目录加上 -w ... -p wa 监控,成本是规则未命中时的零开销,收益是任何配置漂移都有据可查。其二,把「谁干的」问题链纳入预案:文件被删/被改类事件的第一响应命令是 ausearch -f <path> -i,而不是翻业务日志;演练一次这个流程,事件响应速度完全不同。其三,校准自己服务的日志速率:压测时顺手统计服务的日志行/秒,对照 journald 限流参数决定走 journald 还是独立文件,避免事故时刻才发现日志被限流掐掉。其四,容器宿主机必配 execve 审计:云原生环境里容器逃逸、exec 横向移动的第一手证据都在 audit.log 里,这是宿主机基线(CIS Benchmark)的明确条目,多数安全基线扫描工具会直接检查它。审计系统的价值不在平时——平时它几乎不消耗任何资源——而在事件发生的那一刻,你是拿着完整的现场回放,还是一片空白。

小结

Linux 的日志有两套职责完全不同的管线:journald 是应用与服务的「发言记录」,排障快查的主力,受速率限制与持久化配置约束;auditd 是内核系统调用层的「行为记录」,靠内核规则过滤产生事件,能精确回答「哪个登录身份、哪个进程、在什么上下文里对哪个文件做了什么」,auid 的固化设计与 netlink 管道是它的两个技术支柱。合规留痕的标准组合是:精确规则 + 32/64 双架构覆盖 + -e 2 不可变 + audisp 远端实时转发 + lost 计数监控。对工程师而言,理解这套机制的最好方式是给一台测试机配上文件监控和 execve 审计,删一个文件、docker exec 进一个容器,然后用 ausearch 回放全过程——见过一次完整现场,你就再也不会在安全事件里翻错日志了。

← 返回资讯列表

读者留言

COMMENTS 暂无
仅本站原创文章开放留言 · 请勿留下手机号、邮箱等个人信息

还没有留言,来说第一句?