Appearance
Syslog 基础篇:协议解剖与 rsyslog 配置
安全日志生态的“通用语”——从协议格式到配置实战,掌握日志采集的底层原理
引言:为什么安全产品离不开 syslog?
在现代安全基础设施中,SIEM(安全信息与事件管理)、HIDS(主机入侵检测系统)、NIDS(网络入侵检测系统)都需要集中采集各类设备的日志。而 syslog 正是这个生态的“通用语”:
- 防火墙、路由器、交换机 → syslog
- Linux 服务器的
sshd、sudo、cron→ syslog - 堡垒机、数据库审计 → syslog
作为安全开发者,理解 syslog 协议细节,是正确解析日志、避免漏报误报的基础。
syslog 协议格式深度解剖
syslog 主要有两个 RFC 标准:RFC 3164(传统格式,BSD-syslog)和 RFC 5424(增强格式)。安全场景中两者并存,解析时需要兼容处理。
完整格式 vs 原始格式
一条典型的 RFC 3164 syslog 消息:
log
<34>Oct 11 22:14:15 mymachine su: 'su root' failed for lonvick on /dev/pts/8拆解为:
| 部分 | 示例 | 含义 |
|---|---|---|
| PRI | <34> | Facility + Priority 编码值 |
| HEADER | Oct 11 22:14:15 mymachine | 时间戳 + 主机名 |
| MSG | su: 'su root' failed... | 标签(TAG) + 内容(CONTENT) |
PRI 计算与反解(Golang 解析示例)
PRI = Facility × 8 + Priority
- Facility 范围:0-23(设备类型,如 kern=0, auth=4, daemon=3)
- Priority 范围:0-7(严重级别,0=emerg, 3=err, 6=info)
示例计算:<34> → 34 / 8 = 4 余 2 → Facility=4(auth), Priority=2(crit)
Golang 解析 demo:
go
package main
import (
"fmt"
"strconv"
"strings"
)
// parsePRI 解析 PRI 字段,返回 facility 和 priority
func parsePRI(priStr string) (facility, priority int, err error) {
priStr = strings.Trim(priStr, "<>")
pri, err := strconv.Atoi(priStr)
if err != nil {
return 0, 0, err
}
facility = pri / 8
priority = pri % 8
return facility, priority, nil
}
func main() {
facility, priority, _ := parsePRI("<34>")
fmt.Printf("Facility: %d (auth), Priority: %d (crit)\n", facility, priority)
}HEADER:时间戳与主机名
RFC 3164 时间戳格式:Mmm dd hh:mm:ss(无年份,安全解析时需补充)
go
import "time"
// parseRFC3164Time 解析 RFC 3164 时间戳(需补充年份)
func parseRFC3164Time(timestamp string, currentYear int) (time.Time, error) {
layout := "Jan 2 15:04:05"
t, err := time.Parse(layout, timestamp)
if err != nil {
return time.Time{}, err
}
return time.Date(currentYear, t.Month(), t.Day(),
t.Hour(), t.Minute(), t.Second(), 0, time.Local), nil
}主机名:可能是短主机名或 FQDN,用于标识日志来源。
MSG:TAG + CONTENT
- TAG:通常是进程名,如
su、sshd、kernel - CONTENT:具体日志内容
go
// splitTagContent 分割 TAG 和 CONTENT
func splitTagContent(msg string) (tag, content string) {
parts := strings.SplitN(msg, ":", 2)
if len(parts) == 2 {
return parts[0], strings.TrimSpace(parts[1])
}
return msg, ""
}Facility 与 Priority:安全日志的分类与分级
安全相关的 Facility
| Facility | 值 | 安全含义 |
|---|---|---|
kern | 0 | 内核日志:网络过滤、SELinux、iptables 拒绝 |
auth | 4 | 认证日志:login、su、sudo(非特权) |
authpriv | 10 | 特权认证日志:SSH 登录、密码修改 |
daemon | 3 | 守护进程:各类服务的安全事件 |
local0 ~ local7 | 16-23 | 自定义:应用层安全日志 |
Priority 到告警级别的映射
| Priority | 值 | 安全告警级别 | 典型事件 |
|---|---|---|---|
emerg | 0 | 🔴 严重 | 系统不可用 |
alert | 1 | 🔴 高危 | 需要立即处理(如 root 被暴力破解) |
crit | 2 | 🟠 高危 | 关键服务失败(如 PAM 崩溃) |
err | 3 | 🟡 中危 | 认证失败、权限错误 |
warning | 4 | 🟡 低危 | 可疑行为但不致命 |
notice | 5 | 🔵 信息 | 重要但正常的操作 |
info | 6 | 🔵 调试 | 常规访问日志 |
debug | 7 | ⚪ 忽略 | 调试信息 |
rsyslog 配置实战
基础配置结构
rsyslog 配置文件 /etc/rsyslog.conf 核心语法:
apache
# 模块加载
module(load="imudp") # 加载 UDP 输入模块
input(type="imudp" port="514")
# 模板定义
template(name="DynamicFile" type="string"
string="/var/log/%HOSTNAME%/%PROGRAMNAME%.log")
# 选择器规则
auth,authpriv.* /var/log/auth.log
kern.* /var/log/kern.log
*.emerg :omusrmsg:* # 紧急消息广播所有用户安全产品常用配置
1. 按 facility 分流,保留原始格式
apache
# /etc/rsyslog.conf
auth,authpriv.* /var/log/auth.log
kern.* /var/log/kern.log
daemon.* /var/log/daemon.log
*.emerg :omusrmsg:*2. 模板配置:兼容 RFC 5424(高精度时间戳)
apache
# 高精度时间戳模板
template(name="RFC5424" type="string"
string="%timestamp:::date-rfc3339% %hostname% %syslogtag%%msg%\n")
$ActionFileDefaultTemplate RFC54243. 安全采集推荐配置
apache
# 确保日志不丢失
module(load="imuxsock") # 本地 socket 输入
$SystemLogRateLimitInterval 0 # 关闭限流(生产环境谨慎)
# 远程接收(安全产品作为日志中心时启用)
module(load="imtcp")
input(type="imtcp" port="514")日志文件的安全含义
| 文件路径 | 记录的安全事件 |
|---|---|
/var/log/auth.log | SSH 登录成功/失败、sudo 执行、用户创建/删除 |
/var/log/kern.log | iptables 拒绝、SELinux 拒绝、内核漏洞告警 |
/var/log/syslog | 综合日志,包含 cron、mail 等服务 |
/var/log/btmp | 失败的登录尝试(二进制格式,用 lastb 读取) |
/var/log/wtmp | 用户登录/登出历史(last 命令) |
/var/log/secure | RedHat/CentOS 系列的认证日志 |
Golang 基础解析:完整示例
以下是一个从 TCP 读取原始 syslog 并解析的 demo 框架:
go
package main
import (
"bufio"
"fmt"
"net"
"strconv"
"strings"
"time"
)
// SyslogMessage 表示解析后的 syslog 消息
type SyslogMessage struct {
Facility int
Priority int
Severity string
Timestamp time.Time
Hostname string
Tag string
Content string
Raw string
}
// severityMap Priority 值到严重级别字符串的映射
var severityMap = map[int]string{
0: "emerg", 1: "alert", 2: "crit", 3: "err",
4: "warning", 5: "notice", 6: "info", 7: "debug",
}
// parseSyslog 解析原始 syslog 消息(RFC 3164 简化版)
func parseSyslog(raw string) (*SyslogMessage, error) {
if len(raw) < 4 || raw[0] != '<' {
return nil, fmt.Errorf("invalid syslog format")
}
// 提取 PRI
end := strings.Index(raw, ">")
if end == -1 {
return nil, fmt.Errorf("missing PRI terminator")
}
pri, _ := strconv.Atoi(raw[1:end])
facility := pri / 8
priority := pri % 8
// 剩余部分
rest := strings.TrimSpace(raw[end+1:])
// 简单分割:时间戳 + 主机名 + 消息
parts := strings.SplitN(rest, " ", 3)
if len(parts) < 3 {
return nil, fmt.Errorf("invalid syslog header")
}
// 解析时间戳(格式:Mmm dd hh:mm:ss)
timestampStr := parts[0] + " " + parts[1]
timestamp, _ := parseRFC3164Time(timestampStr, time.Now().Year())
// 主机名和消息
hostname := parts[2]
return &SyslogMessage{
Facility: facility,
Priority: priority,
Severity: severityMap[priority],
Timestamp: timestamp,
Hostname: hostname,
Raw: raw,
}, nil
}
// parseRFC3164Time 解析 RFC 3164 格式时间戳
func parseRFC3164Time(timestamp string, currentYear int) (time.Time, error) {
layout := "Jan 2 15:04:05"
t, err := time.Parse(layout, timestamp)
if err != nil {
return time.Time{}, err
}
return time.Date(currentYear, t.Month(), t.Day(),
t.Hour(), t.Minute(), t.Second(), 0, time.Local), nil
}
// handleConnection 处理单个 TCP 连接
func handleConnection(conn net.Conn) {
defer conn.Close()
scanner := bufio.NewScanner(conn)
for scanner.Scan() {
raw := scanner.Text()
msg, err := parseSyslog(raw)
if err != nil {
fmt.Printf("Parse error: %v\n", err)
continue
}
fmt.Printf("[%s] [%s] Facility=%d Host=%s\n",
msg.Timestamp.Format("2006-01-02 15:04:05"),
msg.Severity, msg.Facility, msg.Hostname)
}
}
func main() {
ln, err := net.Listen("tcp", ":5514")
if err != nil {
panic(err)
}
defer ln.Close()
fmt.Println("🚀 Syslog collector listening on :5514")
for {
conn, err := ln.Accept()
if err != nil {
continue
}
go handleConnection(conn)
}
}安全开发者的实战要点
常见解析陷阱
| 陷阱 | 解决方案 |
|---|---|
| 时间戳无年份 | 上下文判断(日志文件写入时间 vs 事件时间) |
| 多行消息(如堆栈跟踪) | 需要状态机解析,不能按行简单分割 |
| 非标准格式(如网络设备) | 配置设备使用标准格式,或编写适配器 |
| UDP 丢包 | 生产环境使用 TCP 或 RELP 协议 |
日志采集的合规要求
等保 2.0、GDPR、PCI-DSS 等标准对日志有明确要求:
- 完整性:日志不能被篡改(考虑签名或安全传输)
- 可用性:日志不能丢失(TCP + 本地缓存)
- 机密性:传输加密(TLS,在第二篇详述)
小结
本文从协议底层到配置实战,为安全开发者构建了 syslog 的知识基础:
- ✅ 理解
PRI = Facility × 8 + Priority,能手动解析和构造 - ✅ 区分 RFC 3164 / 5424 的时间戳格式差异
- ✅ 掌握 rsyslog 的核心配置语法和分流策略
- ✅ 用 Golang 实现基础的 syslog TCP 接收端
下一篇预告:《Syslog 实战篇:Golang 安全产品集成指南》将深入 TLS 证书生成、高性能解析库、对接 SIEM 等生产级话题,并给出完整的可运行 demo。
