journald 日志持久化与防篡改实战配置

journald 日志持久化与防篡改是 Linux 服务器应急响应中最容易断链的一环:systemd-journald 默认只把日志放在内存文件系统里,重启即丢,攻击者一条 rm 就能抹掉全部痕迹。本文给出一套可直接落地的配置,把日志落到磁盘、开启密封校验(Forward Secure Sealing)、加固目录权限并转发到远端,最后附上篡改检测脚本。

适用场景

  • 服务器被入侵后需要复盘,却发现 journalctl 里只剩下本次开机之后的记录。
  • 只部署了 rsyslog 集中收集,但部分 systemd 服务(如 sshd、自定义 unit)的日志并未进入 rsyslog,出现审计盲区。
  • 等保或安全基线检查要求「日志保存不少于 6 个月」、「日志不可被非授权修改」。
  • 需要在内核崩溃、服务异常退出等场景下保留完整上下文,而 /run 目录会在重启时清空。

前置条件

  • 使用 systemd 的操作系统(CentOS 7+、Ubuntu 16.04+、Debian 9+ 等)。
  • 具备 root 权限,能够重启 systemd-journald 服务。
  • /var 分区有足够剩余空间,建议至少预留日志量 2 倍以上的容量。
  • 如计划转发到远端,需要一台可达的日志服务器(可用 systemd-journal-remote 或 rsyslog 接收)。

原理说明

journald 的存储位置由 Storage= 决定:

  • volatile:只写入 /run/log/journal/,位于内存文件系统,重启后全部丢失,这是多数发行版的默认行为。
  • persistent:写入 /var/log/journal/,掉电与重启后保留。目录不存在时 journald 会自动回退到 volatile,这是配置生效失败的最常见原因。
  • auto:目录存在就用持久化,否则用内存,看似方便实则隐患最大——目录被删除后静默降级,运维很难察觉。
  • none:关闭存储,仅转发给 syslog。

日志落盘后,防篡改依靠 Forward Secure Sealing(FSS,前向安全密封)。journald 用一把密钥对日志条目做 HMAC 链式计算,每 Seal 周期生成一次密封值;验证方持有校验密钥(Verification Key)即可离线校验整份日志是否被修改或截断,而无需知道密封密钥本身。这意味着即使攻击者获取了 root 并直接编辑 .journal 文件,篡改行为也会在 journalctl --verify 时暴露。

需要明确 FSS 的能力边界:它能发现篡改,但不能阻止篡改。因此日志防篡改的正确形态是「本地密封校验 + 异地转发留存」双管齐下——攻击者可以改本地文件,但改不了已经转发出去的副本。

操作步骤

第一步:创建持久化目录

# 创建目录(journald 不会自动创建,目录缺失会静默回退到内存存储)
sudo mkdir -p /var/log/journal

# 由 systemd-tmpfiles 设置正确的所有者与权限位
sudo systemd-tmpfiles --create --prefix /var/log/journal

# 手工确认权限:root 所有,组为 systemd-journal,权限 2755(含 setgid,保证新文件继承组)
sudo chown root:systemd-journal /var/log/journal
sudo chmod 2755 /var/log/journal
ls -ld /var/log/journal

预期的权限输出形如 drwxr-sr-x 2 root systemd-journal组权限与 setgid 位缺一不可,否则普通用户无法读取日志,而 journalctl 的非 root 使用会受限。

第二步:配置 journald.conf

编辑 /etc/systemd/journald.conf,替换为以下内容:

[Journal]
# 持久化存储:写入 /var/log/journal
Storage=persistent

# 启用前向安全密封,使篡改可被检出
Seal=yes

# 按 uid 拆分日志文件,便于隔离各服务与用户的日志
SplitMode=uid

# 压缩,节省磁盘占用
Compress=yes

# 落盘策略:每 5 分钟或在达到 MaxFileSize 时同步到磁盘
SyncIntervalSec=5m

# 限流:避免被异常刷屏的进程占满磁盘
RateLimitIntervalSec=30s
RateLimitBurst=10000

# 容量与保留策略
SystemMaxUse=2G
SystemKeepFree=4G
SystemMaxFileSize=128M
MaxRetentionSec=90day
MaxFileSec=1week

# 不再通过 syslog socket 重复转发,避免与 rsyslog 采集重复
ForwardToSyslog=no

关键项说明:

  • SystemMaxUseSystemKeepFree 必须同时设置。前者是日志占用上限,后者是保证分区始终保留的空闲空间;只设前者时,一旦分区本身写满,journald 仍可能无空间可用。
  • MaxRetentionSec=90day 是保留上限而非下限。日志实际保留时长同时受容量约束,容量先到就以容量为准。等保要求的 6 个月可按 180day 设置,并相应放大 SystemMaxUse
  • ForwardToSyslog=no 的场景化选择见下文 FAQ,配置 rsyslog 集中转发时容易踩坑。

应用配置:

sudo systemctl restart systemd-journald
systemctl status systemd-journald --no-pager | head -5

第三步:启用密封密钥并离线保管

Seal=yes 生效后 journald 会自动生成密封密钥,但校验密钥需要手工生成并离线保存

# 生成校验密钥,并设置密封周期为 1 小时
sudo journalctl --setup-keys --interval=1h

# 输出示例(务必立即离线保存,不要留在服务器上)
#   Verification key: a1b2c3d4e5f6...
#   Seed: 9f8e7d6c5b4a...

# 把校验密钥写入本地文件,然后转移到离线介质
sudo journalctl --setup-keys --interval=1h 2>/dev/null | awk -F': ' '/Verification key/{print $2}' > /tmp/fss-verification-key.txt
sudo chmod 600 /tmp/fss-verification-key.txt

校验密钥绝对不能放在被监控的主机上。正确的存放位置是运维终端、密码管理器或离线介质;攻击者拿到主机的 root 后若同时拿到校验密钥,就能重算 HMAC 让篡改「合法化」。

第四步:加固日志目录权限

# 日志文件只允许 root 与 systemd-journal 组读取
sudo chmod 640 /var/log/journal/*/*.journal
sudo chown root:systemd-journal /var/log/journal/*/*.journal

# 目录本身禁止其他用户写入
sudo chmod 2755 /var/log/journal

# 可选:把日志目录挂到独立分区并加 noexec,防止日志路径被用作落地目录
# /etc/fstab 示例(需先完成数据迁移)
# /dev/sdb1  /var/log  ext4  defaults,noexec,nosuid,nodev  0  2

如需对日志做更强的完整性保护,可叠加 AIDE 或 auditd 的文件监控,对 /var/log/journal 目录的写操作与文件变更做实时告警。

第五步:转发到远端日志服务器

本地密封只能「事后发现」,异地留存才能「事后追责」。两种常用方案:

方案 A:systemd-journal-upload(保留结构化字段)

# 被监控端配置 /etc/systemd/journal-upload.conf
[Upload]
URL=https://10.0.0.20:19532
ServerKeyFile=/etc/ssl/private/journal-upload.pem
ServerCertificateFile=/etc/ssl/certs/journal-upload.pem
TrustedCertificateFile=/etc/ssl/certs/ca.pem

# 启动并设为开机自启
sudo systemctl enable --now systemd-journal-upload
systemctl status systemd-journal-upload --no-pager | head -5

方案 B:rsyslog TCP 转发(配置更简单,兼容现有环境)

# 被监控端新建 /etc/rsyslog.d/90-remote.conf
# 使用 @@ 表示 TCP 转发,可靠性优于 UDP 的 @
*.* action(type="omfwd" target="10.0.0.20" port="514"
           protocol="tcp" TCP_Framing="octet-counted"
           action.resumeRetryCount="-1" queue.type="linkedList"
           queue.size="10000")

配套在被监控端确认 journald 与 rsyslog 的分工:若使用方案 B 且 rsyslog 加载了 imjournal 模块,应保持 ForwardToSyslog=no,让 rsyslog 直接从 journal 文件读取,避免同一条日志被采集两次。

第六步:篡改检测与告警脚本

把密封校验做成定时任务,篡改一旦发生即可告警:

#!/bin/bash
# /usr/local/sbin/journal-integrity-check.sh
set -u

LOG_DIR=/var/log/journal-audit
REPORT="$LOG_DIR/verify-$(date +%F).log"
mkdir -p "$LOG_DIR"

OUT="$(journalctl --verify 2>&1)"
echo "===== $(date -Is) =====" >> "$REPORT"
echo "$OUT" >> "$REPORT"

# 有 FAILED / 损坏 / 被截断即告警
if echo "$OUT" | grep -qE 'FAIL|Corruption|unlinked|truncated'; then
    echo "$OUT" | grep -E 'FAIL|Corruption|unlinked|truncated' | \
        logger -p auth.crit -t journald-integrity
    exit 1
fi

logger -p auth.info -t journald-integrity "journal verify OK"
exit 0
# 赋权并加入定时任务
sudo chmod 750 /usr/local/sbin/journal-integrity-check.sh
echo '17 */6 * * * root /usr/local/sbin/journal-integrity-check.sh' | sudo tee /etc/cron.d/journal-integrity
sudo systemctl restart cron

注意上面的 cron 表达式表示每 6 小时执行一次。告警写入 auth.crit 级别,可通过已有的日志告警链路(如 rsyslog 转发 + 日志平台规则)触发通知。

配置验证

逐项确认配置真正生效,而不是「看起来配了」:

# 1) 确认当前日志实际落盘路径(应指向 /var/log/journal,而非 /run/log)
journalctl --header | grep -iE 'File path|Sequential' | head -5

# 2) 确认目录中已有 journal 文件,且大小随时间增长
sudo ls -lh /var/log/journal/*/ | head -20
journalctl --disk-usage

# 3) 确认密封已开启(输出中应包含 sealed 标记)
journalctl --header | grep -i seal

# 4) 完整校验,期望全部 PASS
sudo journalctl --verify
sudo journalctl --verify 2>&1 | grep -c 'PASS'

# 5) 使用离线保管的校验密钥做独立校验
sudo journalctl --verify --verify-key="$(cat /path/to/fss-verification-key.txt)" 2>&1 | tail -5

# 6) 持久化生效验证:重启后仍能看到重启前的日志
sudo systemctl restart systemd-journald
journalctl -b -1 --no-pager | tail -5      # 查看上一次启动的日志,应有输出

# 7) 远端转发验证(在日志服务器上执行,确认能收到本机日志)
# journalctl -u systemd-journal-remote --no-pager | tail -20

判定标准:第 1 项输出路径为 /var/log/journal/...;第 3 项出现 sealed 字段;第 4 项无 FAILED 关键字;第 6 项能读到上一次启动的日志——这一项是持久化是否真正生效的决定性证据,因为内存存储重启后必然为空。

常见问题(FAQ)

Q1:配置了 Storage=persistent,重启后日志仍然丢失,怎么查?

排查顺序如下,前两项占绝大多数:

  • /var/log/journal 目录不存在:journald 在目录缺失时会静默回退到 volatile,错误日志中可能只有一行 ENOENT。执行 sudo mkdir -p /var/log/journal && sudo systemd-tmpfiles --create --prefix /var/log/journal 后重启服务。
  • 忘记重启服务journald.conf 修改后必须 systemctl restart systemd-journald,仅 reload 不生效。
  • 配置项被上游文件覆盖:部分发行版在 /usr/lib/systemd/journald.conf.d/ 下放有覆盖片段。journald.conf 的优先级是 /etc/systemd/journald.conf.d/*.conf > /etc/systemd/journald.conf > /usr/lib/systemd/journald.conf.d/*.conf,用下面命令确认最终取值:
systemd-analyze cat-config systemd/journald.conf | grep -nE 'Storage|Seal|SystemMaxUse'

若输出中看到 Storage=volatile 出现在你的配置之后,说明被上游片段覆盖,需要在 /etc/systemd/journald.conf.d/ 下新建文件并重新赋予优先级。

Q2:同时开启 journald 与 rsyslog 后日志出现重复,该如何取舍?

重复的根因是同一条日志走了两条路:rsyslog 的 imuxsock 模块监听 journald 转发的 syslog socket,而 imjournal 模块又直接读取 journal 文件。二者同时启用且 ForwardToSyslog=yes 时,日志必然双份。推荐二选一:

  • 保留 imjournal(推荐):设 ForwardToSyslog=no,注释掉 rsyslog 的 imuxsock,只留 imjournal,并配置 ImjournalStateFile 记录读取位点。优点是保留了 journal 的结构化字段。
  • 退回传统 syslog:设 ForwardToSyslog=yes,用 imuxsock 接收,禁用 imjournal。优点是兼容老配置,但会丢失部分元数据。

验证是否还有重复:

journalctl -u sshd --since '5 min ago' --no-pager | grep -c 'Accepted password'
grep -c 'Accepted password' /var/log/secure

两条命令的计数差异过大,说明存在重复采集或采集遗漏。

Q3:journalctl --verify 报 FAILED,一定是被入侵了吗?

不一定,但必须按「已被篡改」处理并立即取证。常见的非攻击性原因有三类:一是日志文件在写入过程中被强制中断(如虚拟机被硬关机、宿主 OOM 杀进程),形成截断的尾部记录;二是更换了机器 ID 或重装了系统,导致密封密钥与文件不匹配,校验时表现为 HMAC 不连续;三是使用了未带校验密钥--verify,在没有 Verification Key 的情况下只能做文件内部一致性校验,对「整体重写」类篡改无能为力。判定方法:

# 带离线校验密钥做完整校验,这一步才能识别整体重写
sudo journalctl --verify --verify-key="$(cat /path/to/fss-verification-key.txt)" 2>&1 | grep -E 'FAIL|PASS' | head -20

# 查看文件时间戳与入侵痕迹时间线是否吻合
sudo ls -l --time-style=full-iso /var/log/journal/*/*.journal
last -Fai | head -20

如果带密钥的校验仍报 FAILED,且文件 mtime 与可疑登录时间吻合,应按入侵事件启动应急响应:立即隔离主机、导出日志副本、比对远端转发的日志副本确认缺失区间。

Q4:日志量太大把磁盘写满怎么办?

先确认当前占用与策略是否生效:

journalctl --disk-usage
df -h /var/log
systemd-analyze cat-config systemd/journald.conf | grep -E 'SystemMaxUse|SystemKeepFree|RateLimit'

处理顺序:先设 SystemMaxUseSystemKeepFree 限制总量,再检查 RateLimitBurst——被 CC 攻击或应用疯狂刷错误日志时,限流是第一道闸门。确认策略无误后手工回收:

# 只保留最近 30 天,且总量不超过 1G
sudo journalctl --vacuum-time=30d
sudo journalctl --vacuum-size=1G
journalctl --disk-usage

注意 --vacuum-* 会物理删除历史日志,且删除后对应的密封校验链会出现断裂。如果日志需要用于取证或合规留存,应先完成异地转发,再执行回收。

总结

journald 日志治理的核心是两句话:日志必须先落盘,落到磁盘之后必须能证明没被动过。落地清单为:创建 /var/log/journal 并设 Storage=persistent,开启 Seal=yes 并把 Verification Key 转移到离线介质,用 SystemMaxUseSystemKeepFree 约束容量,同时通过 systemd-journal-upload 或 rsyslog 把日志转发到独立主机,最后用 journalctl --verify 定时校验并告警。本地密封负责「发现篡改」,异地留存负责「还原真相」,二者配合才能在入侵发生后拿到完整的攻击时间线。