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

【Tetragon / eBPF】文件、网络与能力观测面:路径解析、socket 与 cred 的失败模式

文章导航

分类入口
kubernetessecurity
标签入口
#tetragon#ebpf#tracingpolicy#d_path#sock#cred#capabilities#v1.7.0

目录

第 8 篇 把策略限制到 Pod / 容器 / host。本篇不写规则库:贴一条「监控 /etc」YAML 并不等于看见了每一次访问。真正要钉死的是三类观测各自挂在什么 hook、参数类型把内核对象收成什么字段、以及路径走不完、socket 层选错、cred 不翻译 user namespace 时事件会怎样缺。

本篇在系列中的位置

篇目 核心内容
第 8 篇 · K8s 身份感知策略 策略打中哪些 Pod
第 9 篇 · 文件 / 网络 / 能力观测面 三类 hook 模式与失败表象
第 10 篇 · 强制执行 Override 与 SIGKILL
系列目录 全部篇目

版本锚定:Tetragon v1.7.0(源码 tag v1.7.0,2026-04-29)。YAML 来自 tag examples/tracingpolicy/;参数类型来自 docs/content/en/docs/concepts/tracing-policy/argument_types.md;路径解析来自 bpf/lib/bpf_d_path.h。无真实节点则不粘贴伪造 tetra getevents / JSON。

本篇落到五轴里的 Sensor/Hook:选错 hook 或参数类型,表象是「策略已 apply、该文件/连接/提权却没有事件」,不要先改导出过滤。


一、文件路径:在内核对象上走 d_path,不是在 syscall 字符串上猜

1. 为什么不挂 sys_openat 当默认答案

v1.7.0 hooks.md 开篇警告:把 kprobe 挂在系统调用上、参数又是用户态指针时,会引入检查时刻与使用时刻不一致(TOCTTOU)——用户空间可以在 hook 跑完之后、内核真正消费之前改掉指针指向的数据。挂到更靠后的内核函数(例如 LSM security_ hook)时,数据已经拷进内核对象,这条窗口关掉。

文件观测因此有一条工程分叉:

挂载点 看见的对象 典型失败
syscall(sys_openat / sys_write 用户指针、fd 整数 指针可被改写;write 的 fd 整数本身不是路径
LSM security_* 内核 struct file / struct path hook 名随内核版本消失时整份策略加载失败
fd_install 新装入 fd 表的 struct file 只覆盖「安装 fd」这一刻,不覆盖后续 read/write

v1.7.0 用例文档 filename-access.md 的选择是:读/写走 security_file_permission(覆盖 read/writesendfilecopy_file_range、aio/io_uring 等共同权限检查点),mmap 走 security_mmap_file,截断走 security_path_truncate。这是观测模式,不是 MITRE 映射。

2. 参数类型决定「路径」从哪来

argument_types.md 把路径相关类型分成几层,不能互换:

类型 内核对象 选择器能匹配的内容
file struct file * 解析出的完整路径;可用 Prefix / Equal
path struct path * 同样解析路径
dentry struct dentry * 单个挂载点内的路径——没有 mount 信息,过不了挂载点
filename struct filename 用户提供的文件名字符串
fd 文件描述符 Tetragon 解析 fd 得到关联路径后再匹配

dentry 的边界写得很硬:只有 struct dentry 时没有挂载信息,路径解析停在一个 mountpoint 里。跨 bind mount 或从另一个挂载点走进同一 inode,前缀匹配会 miss。

完整路径检索只在内核 v5.3 及以后可用。更老的内核限制为 256 个路径分量:总长仍可到 4096 字节,但分量个数先到上限。这是文档 caution,不是本机实测。

3. __d_path_local:走不完就标未解析

源码 bpf/lib/bpf_d_path.hd_path_local() 从 per-CPU buffer_heap_map 取缓冲,调用 __d_path_local()。后者按 Linux d_path 同类思路,从目标 path 往当前任务 fs->root 回溯,prepend_name() 把 dentry 名反着写入缓冲。

失败被编码进 error,不是静默写成空字符串:

排障含义:前缀选择器匹配的是解析成功的字符串。路径被标成未解析时,不要把「没事件」写成「进程没碰这个文件」。

4. FollowFD:文档仍在,但已标废弃

selectors.mdFollowFD / UnfollowFD / CopyFD 标为 deprecated due to being unsafe,并写计划在 1.5 移除。v1.7.0 源码与文档里这些动作还在,但本篇不当成现行模式。需要「fd → 路径」时,优先用参数类型 fd(文档:Tetragon 解析 fd 得到关联路径),或在 fd_install 上直接取 file

tag 示例 examples/tracingpolicy/write.yamlsys_write 的第一个参数写成 intEqual: "1",匹配的是标准输出这个 fd 号,不是路径。这是另一种合法模式,不要和 type: fd 的路径匹配混为一谈。

5. 最小 YAML(文件)

以下摘自 tag examples/tracingpolicy/filename_monitoring.yaml,只保留 security_file_permission 一则;原文件还挂了 security_mmap_filesecurity_path_truncate

apiVersion: cilium.io/v1alpha1
kind: TracingPolicy
metadata:
  name: "file-monitoring"
spec:
  kprobes:
  - call: "security_file_permission"
    syscall: false
    return: true
    args:
    - index: 0
      type: "file"
    - index: 1
      type: "int"
    returnArg:
      index: 0
      type: "int"
    selectors:
    - matchArgs:
      - index: 0
        operator: "Prefix"
        values:
        - "/etc/"

同一文件里的 security_path_truncateLinux 6.2+ 上是已知加载地雷。filename-access.md(v1.7.0)写明:该 hook 在 6.2 被重构为 security_file_truncate(内核提交 3350607dc563);符号往往不再作为可 kprobe 入口。策略引用不存在的 call 时,整份 TracingPolicy 加载失败,于是读/写那两段 hook 也不会出事件。修复方向是换成 security_file_truncate(参数类型改为 file),或两边都写并设 ignore.callNotFound: true

另一条文档限制:策略匹配的是应用用来访问的那条文件名。经硬链接或不同 bind mount 访问同一文件,不会产生事件。这与 Bishop & Dilger 1996 讨论的「按路径检查、按 inode 使用」是同一类间隙,本篇只把它写成观测失败模式;强制执行侧的 TOCTOU 放到第 10 篇。


二、网络 / socket:层选错,运算符就不成立

1. 参数类型不是「随便填 sock」

argument_types.md 把网络对象拆开:

类型 内核对象 matchArgs 可用运算符
sock struct sock(网络层) SAddr / DAddr / SPort / DPort / Protocol / Family / State
socket struct socket(BSD 套接字层) 同上
skb struct sk_buff 地址/端口/协议及 Not* 变体;地址支持 CIDR
sockaddr struct sockaddr 只有 SAddr / SPort / Family没有 DAddr / DPort
sockaddr_un struct sockaddr_un Equal / Prefix / Family 等;抽象套接字名带前导 @;无端口/协议/State
net_device struct net_device 设备名字符串

connect() 的地址参数标成 sock、或在 sockaddr 上写 DAddr,属于未在该类型文档中出现的字段用法,本篇不发明。

2. 最小 YAML(网络)

tag examples/tracingpolicy/tcp-connect.yaml 挂的是内核 TCP 函数,不是 sys_connect

apiVersion: cilium.io/v1alpha1
kind: TracingPolicy
metadata:
  name: "connect"
spec:
  kprobes:
  - call: "tcp_connect"
    syscall: false
    args:
    - index: 0
      type: "sock"
  - call: "tcp_close"
    syscall: false
    args:
    - index: 0
      type: "sock"
  - call: "tcp_sendmsg"
    syscall: false
    args:
    - index: 0
      type: "sock"
    - index: 2
      type: int

这是「已经进入 TCP 状态机」的观测:Unix 域套接字、未走到 tcp_connect 的 UDP、以及在 LSM security_socket_connect 就被拒绝的连接,都不会出现在这三条 kprobe 上。

对照 tag examples/tracingpolicy/security-socket-connect.yaml:hook security_socket_connect,参数用 sockaddr,选择器用 Family 限制 AF_INET / AF_INET6。这是更靠前的安全检查点,看见的是连接参数而不是已建立的 struct sock。v1.7.0 变更日志还点了 sockaddr_un 解码(PR #3980):容器运行时的 containerd.sock / docker.sock 一类 AF_UNIX 路径可以出现在事件里,前提是内核与参数类型对得上。

selectors.md 另有 TrackSock / UntrackSock:在 hook 处把 sock 放进 BPF map,供后续 hook 引用。文档要求成对维护,否则 map 会坏;且 仅内核 ≥ 5.3。本篇不展开成套跟踪协议。

失败表象对照:

预期 实际 hook 常见缺口
「所有出站连接」 只挂 tcp_connect 非 TCP、被 LSM 提前拒绝、内核绕过该函数的路径
「connect 参数」 sockaddr 却写 DAddr 该类型没有目的运算符
「Unix 套接字路径」 未用 sockaddr_un 路径与抽象名(@...)不会出现

三、能力与 cred:记录安装时刻的凭证,不翻译 user namespace root

1. msg_cred 里有什么

bpf/lib/bpf_cred.h 把凭证收成 struct msg_cred:uid/gid 的 real / saved / effective / fs 四套、securebits、三份 capability 位图(permitted / effective / inheritable)、以及 msg_user_namespaceleveluidgidns_inum)。capability 也可以经 c[3] 数组访问,下标与 caps_permitted / caps_effective / caps_inheritable 对齐。

执行相关标志与用户态共享:EXEC_SETUIDEXEC_SETGIDEXEC_FILE_CAPSEXEC_SETUID_ROOTEXEC_SETGID_ROOT__is_uid_global_root() 只判断 uid == 0,注释写明当前不支持按 user namespace 翻译「该命名空间里的 root」。容器里 uid=0 但映射到宿主机非 0 的情况,不能靠这个辅助函数当「宿主机特权」结论。

argument_types.md 对应的观测积木:cred 取整份 struct credcap_permitted / cap_effective / cap_inheritable / kernel_cap_t / capability 取能力集合或单次检查值。

2. 最小 YAML(能力 / cred)

tag examples/tracingpolicy/kprobe_commit_creds.yaml 挂的是安装新凭证的内核函数:

apiVersion: cilium.io/v1alpha1
kind: TracingPolicy
metadata:
  name: "commit-creds"
spec:
  kprobes:
  - call: "commit_creds"
    syscall: false
    args:
    - index: 0
      type: "cred"

这是「凭证被提交」的观测,不是 capset(2) 系统调用本身。只挂 sys_setuid(tag examples/tracingpolicy/sys_setuid.yaml 把 uid 标成 int)会漏掉文件 capabilities、setresuid、以及内核路径里直接 commit_creds 的提权。

另一则 tag 示例 fd_install_caps.yaml 不解析 capset,而是用选择器 matchCapabilities:在 fd_install 上要求 Effective 集合 In CAP_CHOWN / CAP_NET_RAWisNamespaceCapability: true)。这是「带某能力的进程安装 fd」过滤,不是「能力集合如何变化」的时间线。

失败表象:

模式 失败
只挂 sys_setuid 非该 syscall 的凭证变更不可见
把 namespace root 当全局 root bpf_cred.h 明确不做 userns 翻译
matchCapabilities 当审计日志 未匹配则无事件;不是 capset 前后差分

四、三类观测 × 失败表象

flowchart TD
  hook["Choose hook"] --> obj["Typed kernel object"]
  obj --> path["d_path_local / sock extract / msg_cred"]
  path --> sel["In-kernel matchArgs"]
  sel -->|"resolved string or fields match"| post["Post event"]
  path -->|"UNRESOLVED_PATH_COMPONENTS"| missPath["Prefix match misses"]
  hook -->|"symbol missing e.g. truncate 6.2+"| loadFail["Whole policy fails to load"]
  hook -->|"syscall pointer"| toctou["User memory changed after check"]
观测面 推荐模式(v1.7.0 文档/示例) 失败表象
文件 security_file_permission + type: file;截断注意 6.2 符号 硬链接/bind mount 无事件;路径未解析;truncate hook 拖垮整份策略
网络 tcp_connect + sock,或 security_socket_connect + sockaddr 只看见 TCP;sockaddr 上误用 DAddr;Unix 路径未用 sockaddr_un
能力 commit_creds + cred,或 matchCapabilities 漏掉非 setuid syscall;userns root 被当成全局 root

开放问题(本篇立靶,第 10、13 篇回收):生产策略能否接受「按路径字符串匹配」与「按 inode 使用」之间的差;以及 LSM hook 随内核重构消失时,加载失败是否被当成「没有攻击」而不是「策略没挂上」。


五、学术谱系、争论与开放问题

谱系:Bishop & Dilger, Checking for Race Conditions in File Accesses, Computing Systems 9(2), 1996 把文件访问里「检查用的名字」和「随后使用的对象」拆开。Garfinkel, Traps and Pitfalls, NDSS 2003 §4.3 把同一问题写进系统调用插入式安全工具:在用户指针上做检查,内核稍后才用。Tetragon 的文件观测默认偏向 LSM security_ hook(Wright, Cowan, Smalley, Morris, Kroah-Hartman, Linux Security Modules, USENIX Security 2002 给出 hook 框架),是为了在内核驻留对象上工作,而不是在 syscall 字符串上工作。路径字符串仍由 d_path 同类遍历生成,硬链接与 bind mount 限制说明:对象身份与路径字符串仍不是同一键

争论:syscall 覆盖面 vs LSM 稳定性

主张 代价
挂 syscall 心智接近 strace;符号相对稳定(Tetragon 可补 __x64_ 前缀) 用户指针 TOCTOU;一个语义对应多个 syscall
security_* / tcp_* 数据已在内核对象;一个 hook 覆盖多条 syscall 路径 符号随内核重构消失;整份策略加载失败

没有文献宣布某一侧在所有内核版本上更优。可检验命题是:策略加载失败是否有独立信号(第 12–13 篇),以及路径未解析是否被当成「无访问」。

开放问题

  1. 路径键 vs inode 键:硬链接与 bind mount 下按前缀过滤的漏报,能否被策略作者在不引入 inode 匹配的前提下接受。入口:v1.7.0 filename-access.md Limitations;Bishop & Dilger 1996。
  2. hook 符号寿命security_path_truncatesecurity_file_truncate 这类重构,加载失败是否应 fail-open 到其余 hook。入口:同文档 Kernel compatibility;ignore.callNotFound
  3. user namespace 根__is_uid_global_root 不做 userns 翻译时,「特权进程」事件的语义边界。入口:bpf/lib/bpf_cred.h

六、小结

  1. 文件观测的默认模式是 LSM hook + file/path 类型,由 __d_path_local 生成字符串;走不完则 UNRESOLVED_PATH_COMPONENTS
  2. dentry 停在一个挂载点;硬链接与 bind mount 按文件名匹配会漏。
  3. 网络层 sock / sockaddr / sockaddr_un 的运算符集合不同;tcp_connect 不是全部套接字。
  4. commit_creds + cred 看见凭证安装;全局 root 判断不翻译 user namespace。
  5. v1.7.0 示例 filename_monitoring.yaml 在 6.2+ 内核上可能因 truncate 符号整份加载失败。

下一篇把这些 hook 上的动作从观测推进到 Override / SIGKILL,并钉 TOCTOU 仍拦不住什么。


七、参考资料

规范 / 官方文档(A)

源码(A)

核心论文

站内对照

实验台账


上一篇:K8s 身份感知策略 · 系列目录 · 下一篇:强制执行

读完这篇,下一步读什么

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


By .