邮件安全态势感知与异常检测 — 日志分析、行为建模与安全编排自动化
📑 目录
一、邮件日志标准化:从 maillog 到结构化数据
邮件系统的安全分析能力,起点永远是日志的可读性与结构化程度。Postfix 默认写 /var/log/maillog,格式为传统 syslog 文本行:
Jul 4 04:12:31 mx1 postfix/smtpd[21045]: connect from unknown[203.0.113.45]
Jul 4 04:12:33 mx1 postfix/smtpd[21045]: 3f8A2b1Lkz: client=unknown[203.0.113.45], sasl_method=LOGIN, sasl_username=user@example.com
Jul 4 04:12:35 mx1 postfix/cleanup[21048]: 3f8A2b1Lkz: message-id=<abc123@example.com>
Jul 4 04:12:36 mx1 postfix/qmgr[1203]: 3f8A2b1Lkz: from=<user@example.com>, size=4512, nrcpt=1
Jul 4 04:12:38 mx1 postfix/smtp[21052]: 3f8A2b1Lkz: to=<recip@external.com>, relay=mx.external.com[198.51.100.1]:25, delay=5.1, delays=0.2/0.1/2.1/2.7, dsn=2.0.0, status=sent (250 OK)
这种格式在几十封邮件时勉强接受,当日志量达到数十万条/天时,靠 grep 逐一排查已不现实。必须做两件事:统一字段定义,并将关键事件转换为结构化格式。
1.1 Postfix 日志字段定义
| Postfix 进程 | 关键事件 | 可提取字段 |
|---|---|---|
| smtpd | connect / disconnect | remote_ip, helo_name, tls_version, tls_cipher |
| smtpd | sasl_username 认证 | sasl_username, sasl_method, remote_ip |
| cleanup | message-id 入队 | queue_id, message_id, from, size |
| qmgr | 投递队列状态 | queue_id, from, size, nrcpt, delay |
| smtp | 投递完成 / 延迟 | to, relay, delay, dsn, status |
| bounce | 退信 | reason, original_recipient, status_code |
| anvil | 速率统计 | rate, connections_per_min, message_count |
1.2 awk 聚合示例
用 awk 把 maillog 按 SASL 用户名聚合,统计每个认证用户的外发邮件数:
awk '/sasl_username=/ {
match($0, /sasl_username=([^, )]+)/, u)
match($0, /nrcpt=([0-9]+)/, r)
user = u[1]; rcpts = r[1] ? r[1] : 1
count[user] += rcpts
if (!(user in first)) first[user] = $1" "$2" "$3
last[user] = $1" "$2" "$3
}
END {
printf "%-40s %8s %8s %s -- %s\n", "USER", "SENDS", "RCPTS", "FIRST_SEEN", "LAST_SEEN"
for (u in count)
printf "%-40s %8d %8d %s -- %s\n", u, count[u], count[u], first[u], last[u]
}' /var/log/maillog | sort -t\| -k2 -rn | head -20
1.3 输出为结构化 JSON / CEF
用 rsyslog 的 mmjsonparse 模块,配合自定义模板,将 Postfix 日志实时转写为 JSON 一行一条:
# /etc/rsyslog.d/30-postfix-json.conf
module(load="mmjsonparse")
template(name="postfix_json" type="list") {
constant(value="{")
constant(value="\"timestamp\":\"") property(name="timereported" dateFormat="rfc3339")
constant(value="\",\"hostname\":\"") property(name="hostname")
constant(value="\",\"program\":\"") property(name="programname")
constant(value="\",\"pid\":") property(name="procid")
constant(value=",\"msg\":\"") property(name="msg" format="jsonfr")
constant(value="\"}\n")
}
if $programname startswith "postfix" then {
action(type="omfile" file="/var/log/postfix-json.log" template="postfix_json")
action(type="omfwd" target="192.168.10.50" port="5140" protocol="tcp" template="postfix_json")
}
对于 SIEM 产品,CEF(Common Event Format)是更通用的选择。下面是一段 CEF 格式模板,直接在 rsyslog template 中使用:
template(name="postfix_cef" type="string"
string="CEF:0|MailSystem|Postfix|3.6|smtp-auth|SMTP Authentication|5|src=%fromhost-ip% suser=%sasl_username% msg=%msg%\n"
)
RFC 5424(The Syslog Protocol)定义了结构化 syslog 消息的完整头部格式,包括 MSGID 与 STRUCTURED-DATA 字段。rsyslog 的 RainerScript 语法完整支持 RFC 5424 的 structured-data 规范。若使用 TLS 传输 syslog,还应遵循 RFC 5425(TLS Transport Mapping for Syslog),确保日志在网络传输中的机密性与完整性。
二、日志采集管道:rsyslog → 集中存储
在生产环境中,日志采集的总架构通常是:
Postfix maillog (多台 MX)
│
├── rsyslog (本地预处理 + 格式化为 JSON/CEF)
│ │
│ ├── TCP/5140 ──→ 集中 rsyslog 汇聚节点
│ │ │
│ │ ├── 写出本地 /var/log/aggregated/*.log (轮转)
│ │ └── 转发 TCP/24224 ──→ Fluentd
│ │ │
│ │ ├── filter: 解析 JSON
│ │ ├── output: ElasticSearch (daily index)
│ │ └── output: Kafka (实时流)
│ │
│ └── 本地轮转 + logrotate 压缩保留 90 天
│
└── filebeat (备选轻量采集) ──→ Logstash ──→ ElasticSearch
2.1 rsyslog 汇聚节点配置
# /etc/rsyslog.d/00-aggregator.conf
module(load="imtcp")
module(load="imudp")
input(type="imtcp" port="5140" Ruleset="remote")
input(type="imudp" port="5140" Ruleset="remote")
ruleset(name="remote") {
# 按来源 IP 分文件
$template PerHostLog,"/var/log/aggregated/%FROMHOST-IP%/%$YEAR%-%$MONTH%/%$YEAR%-%$MONTH%-%$DAY%.log"
*.* ?PerHostLog
# 同时转发给 Fluentd
*.* action(type="omfwd" target="127.0.0.1" port="24224" protocol="tcp")
}
2.2 用于离线取证的 sed 技巧
当需要提取特定时间段(如凌晨 2:00-5:00)的 SASL 登录事件:
sed -n '/^Jul 4 02:/,/^Jul 4 05:/p' /var/log/maillog \
| grep 'sasl_username=' \
| awk '{print $1,$2,$3,$NF}' > night_auth_events.txt
从退信日志中提取被投递到外部的高频目标域名:
grep 'status=bounced' /var/log/maillog \
| grep -oP 'to=<\K[^>]+' \
| awk -F@ '{print $2}' | sort | uniq -c | sort -rn | head -15
三、行为基线建模:正常画像的数学表达
没有基线就没有异常。基线建模的核心思路是:在一个足够长的窗口(建议 ≥ 30 天)内,为每个维度的指标建立统计学分布,然后用该分布判定新观测值是否离群。
3.1 用户发信量基线
为每个已认证用户统计以下指标:
| 指标 | 统计方法 | 典型正常范围 |
|---|---|---|
| 每小时发信量 | 滚动 1h 窗口 sum | < μ + 3σ(约 5-30 封) |
| 每日发信总量 | 按天聚合 | < P95 值(通常 < 200 封) |
| 每日唯一收件域数 | COUNT DISTINCT domain | < 15 个域 |
| 收信域熵值 | -Σ pi × log₂(pi) | < 2.5(集中比分散更正常) |
| 认证 IP 数 / 天 | COUNT DISTINCT auth_ip | 1-3 个 |
| 工作时间占比 | 08:00-20:00 发信 / 全天 | > 85% |
3.2 收信域名拓扑熵
一个正常用户发信给 3 个内部同事和 1 个客户,域分布是 {corp.com: 3, client-a.com: 1},熵为 0.81。而僵尸账号在 1 小时内向 50 个不同域各发出 1 封邮件,域分布均匀,熵值会飙升至 log₂(50)≈5.64。这个差值是最直接的异常信号。
用 Python 在日志入库后做批量计算:
import math, json
from collections import Counter
def domain_entropy(domains: list[str]) -> float:
"""计算收件域分布的 Shannon 熵"""
total = len(domains)
if total == 0:
return 0.0
counter = Counter(domains)
return -sum((c / total) * math.log2(c / total) for c in counter.values())
# 示例:从 ES 查询某用户最近 1h 外发记录
# domains = [r['_source']['recipient_domain'] for r in results]
# entropy = domain_entropy(domains)
# if entropy > 3.0: trigger alert
四、异常检测模式与实战规则
4.1 突发外发量(僵尸邮件账号)
这是最常见的沦陷信号。攻击者获取 SMTP 凭证后,通常会在短时间内对外群发垃圾/钓鱼邮件。检测方法:
# 从结构化日志(JSON)中用 jq 快速过滤
cat /var/log/postfix-json.log | jq -r '
select(.msg | test("sasl_username="))
| [.timestamp, (.msg | capture("sasl_username=(?<u>[^ ,]+)").u)]
| @tsv
' | awk '{
user=$2
ts=$1
count[user]++
if (count[user]==1) first[user]=ts
last[user]=ts
}
END {
for (u in count)
if (count[u] > 100) printf "%s\t%d\t%s\t%s\n", u, count[u], first[u], last[u]
}'
在生产环境中,这条规则应该做成流式窗口聚合——Fluentd 每 60 秒输出一个滑动窗口内的用户发信计数,写入 Kafka 后由实时消费者裁决:
# Fluentd config: 按 sasl_username 聚合 60s 窗口
<filter postfix.*>
@type record_transformer
enable_ruby true
<record>
sasl_user ${record["msg"].match(/sasl_username=([^, ]+)/)&.captures&.first || "unknown"}
</record>
</filter>
<match postfix.output>
@type flowcounter
unit minute
aggregate tag
count_keys sasl_user
<store>
@type kafka2
brokers kafka1:9092,kafka2:9092
topic postfix-user-stats
compression_codec gzip
</store>
</match>
4.2 非工作时间异常登录
结合两个维度:时间窗口(凌晨 00:00-06:00)+ 地域突变(平时在北京登录,突然从境外 IP 认证)。
# 提取非工作时间的 SASL 认证事件
grep 'sasl_username=' /var/log/maillog \
| awk '{
split($3, t, ":")
hour = t[1] + 0
if (hour >= 0 && hour < 6) print
}' \
| grep -oP 'sasl_username=\K[^, ]+' \
| sort | uniq -c | sort -rn
4.3 同一 IP 多账号登录
正常情况下,一个 IP 属于一个 NAT 出口或办公网段,上面可能有多个用户通过 SMTP 认证发信——这本身不异常。但如果同一 IP 在短时间内认证了 10+ 个不同账号,而这些账号的历史登录 IP 没有交集,则极可能是凭证攻击。
awk '/sasl_username=/ {
match($0, /sasl_username=([^, )]+)/, u)
match($0, /\[([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)\]/, ip)
user = u[1]; addr = ip[1]
ip_users[addr][user] = 1
}
END {
for (ip in ip_users) {
n = 0; for (u in ip_users[ip]) n++
if (n > 5) printf "%-20s %d users\n", ip, n
}
}' /var/log/maillog
4.4 收发信比偏离
大多数企业用户的收发信比大约在 1:3 到 1:10 之间(发出去的远少于收到的)。如果某个用户突然变成 10:1(发远超收),说明该账号可能在滥发。
| 场景 | 收发比 | 判定 |
|---|---|---|
| 普通文职用户 | 1:5 ~ 1:15 | 正常 |
| 市场/公关岗位 | 1:1 ~ 3:1 | 合理(岗位特性) |
| 任意用户突然飙到 | 50:1 以上 | 高度可疑 |
| 新建账号 24h 内 | 100:0 | 几乎确定是恶意注册 |
MITRE ATT&CK T1566 (Phishing):钓鱼邮件分为 T1566.001(Spearphishing Attachment,带附件定向攻击)和 T1566.002(Spearphishing Link,含恶意链接)。当检测到用户外发邮件中嵌入的 URL 命中威胁情报库(如 URLhaus、PhishTank),应立即归入此战术标签进行追踪。内部被钓鱼沦陷后转为外发钓鱼节点,对应 T1566 + T1584.006(Compromise Infrastructure: Web Services)的连锁攻击链。
五、洞见式警报 vs 阈值式警报
传统阈值式警报容易产生两个极端:阈值太松则漏报,阈值太紧则告警疲劳。操作人员很快学会忽略。
洞见式警报的核心思想是:不依赖单一数值触发,而是组合多个弱信号形成一个强判断。每条警报本身已经附带了解释——"这个用户为什么可疑"。
示例对比
| 阈值式(差) | 洞见式(好) |
|---|---|
| "用户 alice 过去 1h 发了 180 封邮件,超过阈值 150" | "用户 alice 过去 1h 发信量(180)是其 30 天同期均值的 15 倍,且收件域从平日的 3 个陡增至 47 个,IP 从 2 个变为 1 个(首次出现的境外地址 185.x.x.x),凌晨 03:12 开始活跃。综合评分 0.91/1.00,建议立即冻结。" |
实现上,每个维度独立打分(0-1),最后按权重加总,总分超过 0.70 即触发:
# 综合评分公式(Python 伪代码)
def risk_score(user, window="1h"):
s = 0.0
w = {
"volume_zscore": 0.25, # 发信量 Z-score
"entropy": 0.20, # 收件域熵
"off_hours": 0.15, # 非工作时段
"ip_mutation": 0.15, # IP 突变
"send_recv_ratio": 0.15, # 收发比偏离
"new_contact": 0.10, # 首次出现的收件域
}
metrics = compute_metrics(user, window)
s += w["volume_zscore"] * sigmoid(metrics["zscore"] / 3.0)
s += w["entropy"] * min(metrics["entropy"] / 5.0, 1.0)
s += w["off_hours"] * (1 if metrics["is_off_hours"] else 0)
s += w["ip_mutation"] * (1 if metrics["ip_not_in_history"] else 0)
s += w["send_recv_ratio"] * min(metrics["ratio_deviation"] / 20.0, 1.0)
s += w["new_contact"] * min(metrics["new_domains_ratio"], 1.0)
return s
def sigmoid(x):
return 1.0 / (1.0 + math.exp(-x)) if x >= 0 else 0.0
针对不同的组织规模和邮件文化,权重应可调。例如技术服务团队可能给外部客户发大量邮件,此时应提高 entropy 和 ip_mutation 的权重,降低 volume_zscore 的权重以避免误报。
六、SIEM 集成与看板建设
日志从采集到可视化的完整数据流:
Postfix ──rsyslog──▶ ElasticSearch ◀── Kibana 看板
│
├── 每日索引: postfix-YYYY.MM.DD
├── 告警索引: postfix-alerts-YYYY.MM
└── 滚动策略: 热数据 7d → 温 30d → 冷 90d → 删除
│
▼
SOAR playbook (webhook)
6.1 Kibana 看板设计
建议建设以下四个核心看板:
| 看板名称 | 核心面板 | 刷新频率 |
|---|---|---|
| 邮件流量概览 | 每小时收发总量趋势、Top10 发件人、Top10 收件域、投递成功率 | 5min |
| 安全检测面板 | 实时风险评分 Top20 用户、异常登录IP地图、突发外发事件时间线 | 1min |
| 退信与声誉 | 退信率趋势、被拒原因分布、发件 IP 在 DNSBL 中的命中情况 | 15min |
| 系统健康 | 队列长度、延迟 P50/P95/P99、磁盘使用率、SMTP 并发连接数 | 1min |
6.2 ElasticSearch 索引模板
{
"index_patterns": ["postfix-*"],
"settings": {
"number_of_shards": 3,
"number_of_replicas": 1,
"refresh_interval": "30s"
},
"mappings": {
"properties": {
"timestamp": { "type": "date" },
"hostname": { "type": "keyword" },
"remote_ip": { "type": "ip" },
"sasl_username": { "type": "keyword" },
"queue_id": { "type": "keyword" },
"message_id": { "type": "keyword" },
"from_address": { "type": "keyword" },
"to_address": { "type": "keyword" },
"recipient_domain": { "type": "keyword" },
"status": { "type": "keyword" },
"dsn": { "type": "keyword" },
"delay": { "type": "float" },
"size": { "type": "integer" },
"nrcpt": { "type": "integer" },
"tls_version": { "type": "keyword" },
"risk_score": { "type": "float" }
}
}
}
NIST SP 800-92(Guide to Computer Security Log Management)提出了日志管理的三个优先级:日志生成(确保日志格式一致且包含必要字段)、日志传输与存储(保护日志在传输中的完整性和机密性)、日志分析与处置(建立告警、关联分析和报告机制)。本架构的 rsyslog→ES→Kibana 管道与之完全对齐。日志保留策略建议参考该标准的 6.3 节,至少保留 90 天用于取证回溯。
七、文件完整性监控与主机层防护
邮件系统的安全不能只靠日志分析。攻击者一旦获取服务器权限,第一件事往往是篡改配置文件、注入后门、修改认证模块。文件完整性监控(FIM)是最后一道防线。
7.1 关键文件清单
# 以下文件/目录应被 FIM 监控(任何非预期变更应立即告警)
/etc/postfix/main.cf
/etc/postfix/master.cf
/etc/postfix/virtual
/etc/postfix/transport
/etc/postfix/sasl_passwd
/etc/postfix/aliases
/etc/dovecot/
/etc/ssl/certs/postfix/
/usr/libexec/postfix/
/etc/sysconfig/saslauthd
7.2 OSSEC 监控规则
<!-- /var/ossec/rules/postfix_fim.xml -->
<group name="syscheck,postfix">
<rule id="100200" level="12">
<if_sid>550</if_sid>
<match>/etc/postfix/</match>
<description>Postfix configuration file modified.</description>
</rule>
<rule id="100201" level="14">
<if_sid>100200</if_sid>
<match>main.cf|master.cf|sasl_passwd</match>
<description>Critical Postfix config altered — possible compromise.</description>
</rule>
</group>
Wazuh(OSSEC 的现代分支)支持更灵活的配置:
<!-- /var/ossec/etc/ossec.conf -->
<syscheck>
<directories check_all="yes" realtime="yes"
report_changes="yes">/etc/postfix</directories>
<directories check_all="yes" realtime="yes"
report_changes="yes">/etc/dovecot</directories>
</syscheck>
当主配置文件发生非授权变更时,Wazuh agent 会在数秒内推送告警到管理节点,经规则匹配后触发 Syslog 通知管理员。
八、安全编排自动化响应(SOAR)
检测到异常之后,延迟每多一秒,攻击者就可能多发数百封邮件。手动响应不可靠。自动化 SOAR playbook 的目标是将"检测→确认→遏制→通知"压缩到秒级。
8.1 自动化响应 Playbook
触发条件: risk_score >= 0.85 且 持续 >= 2 个评估周期
Step 1 — 确认(自动化交叉验证)
├── 查询该用户过去 30 天是否有类似高评分
├── 检查该 IP 是否在内部白名单中(CEO 办公室 VPN 出口等)
└── 确认命中多个弱信号(不是单一抖动)
Step 2 — 遏制
├── 通过 SSH/API 在 Postfix 执行: postconf -e "smtpd_sender_restrictions =
│ check_sender_access hash:/etc/postfix/blocked_senders"
│ → 将 user@domain 写入 blocked_senders → postmap → postfix reload
├── 或者通过 REST API 调用邮件网关的账号冻结接口
└── 记录到事件系统(ticket_id 关联整个响应链)
Step 3 — 通知
├── Syslog WARNING 级别发出结构化告警:
│ { "action":"account_frozen", "user":"alice", "score":0.92,
│ "reason":"burst+suspect_ip+entropy", "ticket":"INC-20260704-001" }
├── 邮件通知安全运维团队
└── 若关联到 SIEM 看板,在告警仪表盘中以红色标记高亮
8.2 Postfix 快速拒绝一个发件地址
# 将异常用户加入拒绝列表
echo "user@example.com REJECT: Account suspended due to suspicious activity" \
>> /etc/postfix/blocked_senders
postmap /etc/postfix/blocked_senders
# 确保 main.cf 中已配置
postconf -e "smtpd_sender_restrictions = check_sender_access hash:/etc/postfix/blocked_senders, permit_mynetworks, permit_sasl_authenticated"
# 重新加载配置(不中断服务)
postfix reload
8.3 Syslog 通知格式
# 由 SOAR 引擎发出的标准化告警日志
logger -p local0.warning -t SOAR-EMAIL \
"ACCOUNT_FROZEN | user=alice@corp.com | score=0.92/1.00 | triggers=burst_anomaly,entropy_spike,off_hours,suspect_ip | action=frozen | ticket=INC-20260704-001 | src_ip=185.213.x.x | timestamp=$(date -Iseconds)"
M3AAWG Best Practices for Logging:邮件反滥用工作组(M3AAWG)在其日志最佳实践中强调,邮件服务日志应记录每条消息的"发送方身份认证路径"(SPF/DKIM/DMARC 验证结果、SASL 用户名、TLS 协商信息),以便滥用调查时能完整还原消息来源。本文的结构化日志字段定义正是基于此建议扩展而来。
九、威胁情报联动
日志分析解决的是内部视角,威胁情报补充外部视角——哪些 IP/域名/URL 已经是已知恶意实体。
9.1 自动拉取并下发到 Postfix 拒绝列表
#!/bin/bash
# fetch-badips.sh — 每小时从 OSINT 情报源拉取恶意 IP,更新 Postfix 拒绝规则
# 建议通过 cron 每小时执行一次
TMPFILE=$(mktemp)
REJECT_DB="/etc/postfix/bad_ips"
# 从多个开源威胁情报源聚合(示例)
curl -s https://feodotracker.abuse.ch/downloads/ipblocklist_recommended.txt \
| grep -v '^#' | awk '{print $1}' > "$TMPFILE"
curl -s https://sslbl.abuse.ch/blacklist/sslipblacklist.csv \
| grep -v '^#' | cut -d, -f2 >> "$TMPFILE"
# 去重后生成 Postfix cidr 格式
sort -u "$TMPFILE" \
| awk '{print $1, "REJECT: Listed in threat intelligence feed"}' \
> "$REJECT_DB"
postmap "$REJECT_DB"
rm -f "$TMPFILE"
# 验证配置并平滑重载
postfix check && postfix reload
9.2 在 main.cf 中挂载
# 放在 smtpd_client_restrictions 的首位
smtpd_client_restrictions =
check_client_access cidr:/etc/postfix/bad_ips,
permit_mynetworks,
permit_sasl_authenticated,
reject_unauth_destination
9.3 DNSBL 查询作为补充层
smtpd_recipient_restrictions =
reject_invalid_hostname,
reject_non_fqdn_sender,
reject_unknown_sender_domain,
reject_rbl_client zen.spamhaus.org,
reject_rbl_client bl.spamcop.net,
reject_rhsbl_sender dbl.spamhaus.org,
permit_mynetworks,
permit_sasl_authenticated,
reject_unauth_destination
DNSBL 和本地 IP 黑名单形成双层过滤:DNSBL 负责已知大规模恶意源,本地列表负责从自有情报和分析结果中积累的针对性拒绝规则。
邮件安全态势感知不是一次性的工程交付,而是一个持续运行、持续优化的分析闭环。日志标准化是地基,行为基线是标尺,异常检测是引擎,自动化响应是把引擎的发现转化为行动的最后一步。把这几个环节跑通之后,运维团队将从"告警太多看不完"变成"每天只看真正有料的几条综合告警"——这才是态势感知的本质。
参考文献
- RFC 5424 — The Syslog Protocol (IETF, 2009)
- RFC 5425 — TLS Transport Mapping for Syslog (IETF, 2009)
- M3AAWG — Best Common Practices for Logging (Messaging, Malware and Mobile Anti-Abuse Working Group)
- NIST SP 800-92 — Guide to Computer Security Log Management (National Institute of Standards and Technology, 2006)
- MITRE ATT&CK — T1566 Phishing (Initial Access)