土法炼钢 · 系统与基础设施

【Tetragon / eBPF】事件导出路径:JSON、gRPC、tetra 与过滤边界

文章导航

分类入口
kubernetessecurity
标签入口
#tetragon#ebpf#grpc#tetra#export-filter#redaction#field-filter#v1.7.0

目录

第 3 篇 把事件送进 ring;本篇钉 过滤分层轴在用户态的一半。读者最常见的误判是:Helm 里配了 denylist,于是认定「集群里已经没有这类事件」。v1.7.0 Events 文档用 Caution 把这句话钉死——allowlist / denylist 只作用于 JSON 文件 sink;gRPC 与默认的 tetra getevents 仍看到未按这两份名单过滤的事件。

本篇在系列中的位置

篇目 核心内容
第 3 篇 · Sensor 与 Hook 事件从哪类 hook 来
第 4 篇 · 事件导出路径 JSON / gRPC / tetra;allow·deny、field filter、redaction
第 5 篇 · Agent 生命周期 BTF 加载与三条策略路径
系列目录 全部篇目

版本锚定:Tetragon v1.7.0docs/.../concepts/events.mdpkg/exporter/exporter.gopkg/filters/pkg/fieldfilters/api/v1/tetragon/events.proto)。JSON 默认路径以 tag 文档为准:/var/run/cilium/tetragon/tetragon.log。gRPC 默认:unix:///var/run/tetragon/tetragon.sock。不以 live 站点的迁仓说明覆盖这两处。无真实节点则不粘贴伪造导出;配置 JSON 来自官方文档示例。

内核 selector(matchArgs 等)在 hook 内丢事件,属于第 7 篇。本篇只讨论 已经通过内核、到达 agent 之后 的分流。


一、三条出口

flowchart LR
  ring["Ring to agent"] --> split{"Sink"}
  split -->|"JSON allow deny"| json["tetragon.log"]
  split -->|"gRPC unfiltered"| grpc["tetra getevents"]
  json --> pipe["tetra reading JSON stdin"]

图:与第 1 篇五轴图中的 sink 分流一致。第三条路不是独立内核出口,而是 JSON 文件的消费者。

v1.7.0 Events 文档把观察方式分成 JSON、tetra CLI、gRPC。工程上要拆成三条可见集合不同的路:

出口 文档中的用法 过滤从哪来 默认落点
文件 JSON 容器 export-stdout 日志,或读 agent 写出的文件 agent 为 exporter 构造的 GetEventsRequest(allow/deny、可选 field filter) /var/run/cilium/tetragon/tetragon.log,默认轮转压缩
gRPC 监听地址 --server-address;Helm tetragon.grpc.address 该次 GetEventsRequest 自己的 allow/deny/field_filters 默认 unix:///var/run/tetragon/tetragon.sock
把 JSON 再喂给 tetra kubectl logs ... -c export-stdout -f \| tetra getevents 已经过文件 sink 过滤;tetra 只做二次显示过滤(进程名、pod 等 CLI 旗标) 标准输入

pkg/exporter/exporter.goStart 调用 server.GetEventsWG,把自身当作 gRPC 流的接收端。NewExporteroption.Config.ExportFilename 拆出文件名与目录;rotationInterval < 0 直接报错。Send 里做可选速率限制,再 encoder.Encode 写成 JSON:

// project: cilium/tetragon, tag: v1.7.0
// path: pkg/exporter/exporter.go
// function: (*Exporter).Send
func (e *Exporter) Send(event *tetragon.GetEventsResponse) error {
    if e.rateLimiter != nil && !e.rateLimiter.Allow() {
        e.rateLimiter.Drop()
        rateLimitDropped.Inc()
        return nil
    }
    if err := e.encoder.Encode(event); err != nil {
        logger.GetLogger().Warn("Failed to JSON encode", logfields.Error, err)
    }
    eventsExportedTotal.Inc()
    eventsExportTimestamp.Set(float64(event.GetTime().GetSeconds()))
    return nil
}

因此「JSON 导出」在实现上是 同一套 GetEvents API 的一个客户端,配置来自 daemon 的 export 旗标,而不是另一套 BPF。编码失败只打日志,函数仍返回 nil——文件里缺行时不要只查 denylist。

文档给出的对照命令语义(未在本仓库执行):

# 消费已经按 JSON export 规则过滤过的行
kubectl logs -n kube-system -l app.kubernetes.io/name=tetragon -c export-stdout -f | tetra getevents -o compact

# 容器内直接跑 tetra:走 gRPC,不经过文件 allow/deny
kubectl exec -it -n kube-system ds/tetragon -c tetragon -- tetra getevents -o compact

文档在 gRPC 节写明:tetra 没有把 JSON piped 进来时,就是 gRPC 客户端。Helm 示例里的 localhost:54321 是改地址的方法,不是 v1.7.0 的默认监听。默认 Unix socket 属于 Upgrade notes 与 events.md 共同钉死的行为;第三方客户端若仍连旧 TCP 端口,会表现为「agent 正常、订阅为空」。第 5、12 篇回收地址变更。

EventLogServiceeventlogservice.proto)只调整 JSON 文件的 lumberjack 参数(max_sizerotation_intervalmax_backups),不改变 gRPC 可见集合。


二、allow / deny 只作用于 file sink

Export filter 是一行一个 JSON 对象。行与行之间 OR,同一对象内字段 AND。仅配置 allowlist 时导出是默认拒绝;denylist 与 allowlist 同时命中时 denylist 优先。两边都不配则 JSON 导出全部事件。

官方文档示例(配置语义,不是事件样本):

{"event_set": ["PROCESS_EXEC", "PROCESS_EXIT"], "namespace": ["foo"]}
{"event_set": ["PROCESS_KPROBE"]}

匹配含义:(PROCESS_EXEC|PROCESS_EXIT) ∧ namespace=foo PROCESS_KPROBE

实现:pkg/filters/filters.goApplywhitelist.MatchOne(ev) && blacklist.MatchNone(ev);空名单视为通过。ParseFilterList 按 JSON 流解码为 tetragon.Filter

// project: cilium/tetragon, tag: v1.7.0
// path: pkg/filters/filters.go
// function: Apply
func Apply(whitelist, blacklist FilterFuncs, ev *event.Event) bool {
    return whitelist.MatchOne(ev) && blacklist.MatchNone(ev)
}

events.protoGetEventsRequest 把这两份名单叫做 allow_list / deny_list,并写明结果是集合差 allow_list - deny_list。同一 Filter 对象内部多个字段是 AND(BuildFilterList 里每行一个 MatchAll);多行之间是 OR(MatchOne)。这与文档「行 OR、行内 AND」一致。

Caution(v1.7.0 events.md 原文语义):上述 denylist / allowlist 应用于导出成 JSON 文件的事件(file sink)。它们 影响经 gRPC API 流出、被 tetra CLI 消费的事件。即使某事件会被 JSON denylist 丢掉,使用 tetra CLI 时你仍会看到未按该名单过滤的事件。

这不是 CLI 的 bug。JSON exporter 与交互式 tetra 是两个 GetEvents 客户端:前者带 daemon 配置的名单,后者默认不带那份名单。把 JSON 管道进 tetra 时,你看到的是已经按文件规则滤过的子集,再叠加 CLI 自己的 --processes / --pod。排障时必须先问「这条 tetra 命令有没有 stdin JSON」。

Filter 字段(events.proto @ v1.7.0)与文档表对照如下。写过滤器以 proto 为准:文档表未列全 PROCESS_THROTTLE / PROCESS_LSM / PROCESS_USDT

字段 匹配对象
event_set EventType 枚举
binary_regex / parent_binary_regex / ancestor_binary_regex RE2,后者要求该事件类型开启 ancestors
namespace / namespace_regex Pod 命名空间;空字符串匹配非 Pod 进程
pod_regex / labels / container_id / container_name_regex K8s / 容器元数据;labels 永不匹配无 pod 的主机事件
in_init_tree 容器 init 子树,见 第 2 篇
cel_expression 用户态 CEL(可带 k8s IP/CIDR 扩展)
health_check 二进制是否像对应 Pod 的 liveness/readiness exec
pid_set PID 及其子孙;注释写明面向测试,生产需 --enable-pid-set-filter

CEL 这里不是第 7 篇的 CEL-in-BPF 选择器。ancestor_binary_regex 在未开启 ancestors 时,CheckAncestorsEnabled 会让构建过滤器失败。

内核已经 NoPost 的事件不会到达本层。allow/deny 不能把内核丢掉的事件找回来,只能再砍 agent 已经拿到的集合。


三、field filter

Field filter 限制留下哪些 protobuf 字段,不决定事件是否存在。写法同样是一行一个 JSON,fields 用 protobuf field mask(点分路径,逗号分隔多路径)。actionINCLUDEEXCLUDE。可按 event_set 限定类型;invert_event_set 反转该集合。

默认:某事件类型没有任何 field filter 时导出全部字段。一旦对该类型定义了 INCLUDE,未列入的字段默认不再出现。EXCLUDE 与 INCLUDE 冲突时,GetEventsRequest 注释写 exclusion always takes precedence

文档示例:除 PROCESS_EXEC 外,只保留 process.exec_idprocess.parent_exec_id

{"fields":"process.exec_id,process.parent_exec_id", "event_set": ["PROCESS_EXEC"], "invert_event_set": true, "action": "INCLUDE"}

pkg/fieldfilters/fields.go 在 JSON 反序列化前把 snake_case 改成 protobuf JSON 期望的 camelCase(fixupFieldFilterString)。Filter 方法只改 event oneof 内部,node_name / time 等公共字段保留。

Field filter 活在 GetEventsRequest.field_filters 里,因此:

这与 allow/deny 同构:都是 每个 GetEvents 客户端一份,不是「内核里已经删了字段」。SIEM 映射失败时先看 field filter 是否把 pod 砍掉,再看策略。


四、redaction 与环境变量

Redaction 针对参数和环境变量里的敏感子串,避免它们经 JSON 导出被带出集群——文档这一节的标题如此。实现却比 allow/deny 更早:initProcessInternalExecpkg/process/process.go)在写入 process cache 之前调用 fieldfilters.RedactionFilters.Redact(binary, args, envs)。因此 gRPC 客户端拿到的 Process.arguments 同样可能已被打码。排障时不要假设「tetra 看见明文、文件里是 *****」——对 redaction 而言两套 sink 共享 cache 里那份已经改过的字符串。

配置:--redaction-filters 或 Helm redactionFilters。JSON 对象含 redact(RE2 列表)与可选 binary_regex只有捕获组被替换成 *****(常量 REDACTION_STR)。文档 Warning:JSON 里反斜杠要再转义。RE2 语法以 Google RE2 为准,不要按 PCRE 去写回溯。

文档示例(配置,非本机事件):

{"redact": ["--password(?:\\s+|=)(\\S*)"]}

含义:参数里 --password=foo 变为 --password=*****。按二进制限制:

{"binary_regex": ["(?:^|/)foo$"], "redact": ["-p(?:\\s+|=)(\\S*)"]}

环境变量同理,例如 {"redact": ["(?:SSHPASS=)+(\\S+)"]}

--filter-environment-variables VAR1[,VAR2..] 在 redaction 之前把未点名的环境变量删掉(slices.DeleteFunc)。这是白名单保留,不是打码。未开此旗标且未配 redaction 时,开启 env 采集会把机密送进所有 sink——采集开关在 __base__ENV_VARS_ENABLED(第 3 篇),本篇管的是已经采集之后如何裁剪。

Redact 对每个 redaction 对象:若配置了 binary_regex 且全部不匹配,则跳过;否则对 argv 与每个 env 字符串做 ReplaceAllStringFunc,只替换捕获组区间。嵌套捕获组若起点落在已打码区间则跳过,避免把 ***** 再切碎。RE2 不支持回溯引用的写法在 Go regexp 上会编译失败,agent 启动时应直接报错,而不是静默不过滤。

RedactionFilter.match 在 proto 里已 deprecated,不要写进新配置。

Redaction 在用户态、在 hook 返回之后。它不阻止内核日志或审计的其他通道看见同一 argv;它只约束 Tetragon 事件对象。TOCTOU 与「参数在用户态被改写」仍属 hook 轴(第 3、10 篇)。


五、失败表象

表象 先点哪一层 不要先做什么
SIEM(读 tetragon.log)没有,节点上 tetra getevents(无管道)有 JSON allow/deny;官方 Caution 重装 DaemonSet;改 TracingPolicy 符号
管道 logs \| tetra 与无管道 tetra 不一致 你在比较 file sink 与 gRPC 认定 tetra 解析 JSON 坏了
事件在,但没有 pod / parent field filter INCLUDE;或血缘轴 cache miss 加大 denylist
arguments 里是 *****,策略却按明文写的 redaction 已改 cache 在选择器里匹配打码后的字符串却期望明文
两边都没有事件 内核 selector / hook / 加载轴(第 3、6–7 篇) 只调 export Helm 值
配置了 CEL export filter 无效果 是否配在 JSON exporter 的名单里;gRPC 请求是否带同一 cel_expression 把它当成内核 CEL-in-BPF
日志文件找不到 tag 文档路径 /var/run/cilium/tetragon/tetragon.log;Helm 是否改了位置 用 live 文档的另一默认目录当 v1.7.0

Exporter.SendrateLimiter 不允许时丢弃并计数,返回 nil(不报错给调用方)。这是用户态第二层丢事件,表象与 denylist 相似,指标不同。节流与 cgroup-rate 见第 11 篇。

谱系:McCanne & Jacobson(1993)把过滤放进内核 VM——对应本系列的 selector,不对应 JSON denylist。Garfinkel(NDSS 2003)关心拦截完整性:用户态再滤一层会让「监控面」与「强制执行面」分叉。Tetragon 把分叉写成文档 Caution,而不是藏起来。Höiland-Jørgensen et al.(2018)的 XDP 丢包发生在驱动路径,与 file sink 丢 JSON 行没有同一套计数语义。

争论:导出采样与真实事件完备性。A 侧要 SIEM 体积可控,用 allow/deny 与 field filter 砍字段。B 侧取证要求 gRPC 所见与落盘所见可对齐。v1.7.0 明确选择不对齐:同一 agent、两套客户端配置。可检验命题是:runbook 是否强制「对账必须点名 sink」。若合规审计把 tetra 屏幕当证据,却用 denylist 证明「未发生」,结论不成立。

开放问题

  1. 能否在不改变 Caution 语义的前提下,让两个 sink 的差异可被单一 metric 证明——第 12 篇看 exporter 指标,不在本篇编数字。
  2. redaction 发生在 cache 构造时,对「只想打码 JSON、保留 gRPC 明文」没有开关——这是 v1.7.0 实现边界。
  3. 内核过滤与用户态过滤的集合差,如何在排障坐标系里变成检查清单——第 13 篇。

六、小结

  1. 三条出口:JSON 文件、gRPC、把 JSON 喂给 tetra。后一条是文件的消费者。
  2. allow/deny 只作用于 file sink;官方 Caution 覆盖 tetra 默认 gRPC 路径。
  3. field filter 按 GetEvents 客户端裁字段;管道 JSON 不可恢复。
  4. redaction 在写入 process cache 时改 argv/env,因此进入所有 sink。
  5. 默认 JSON 路径是 /var/run/cilium/tetragon/tetragon.log。下一篇回到 agent:程序没挂上时,导出层再干净也没有事件。

七、参考资料

规范 / 官方文档(A)

源码(A)

核心论文

实验 / 工具

站内对照


上一篇:Sensor 与 Hook · 系列目录 · 下一篇:Agent 生命周期

读完这篇,下一步读什么

优先读同系列或同问题的下一篇,把单篇消费变成主题集群。


By .