「策略已经 apply,为什么还会通 / 为什么突然全断?」在 iptables 实现里,答案常落在规则顺序与 IP 集合刷新;在 Cilium 里,答案落在 selector 解析 → 数字 Identity → per-endpoint BPF policy map 这条编译链,以及 按方向切入 default-deny 的时机。本篇只钉编译与窗口,不写策略 DSL 教程全书。
本文是「Cilium / eBPF 数据面内核」系列第 7 篇(共 16 篇)。→ 系列目录
篇目 核心内容 第 2 篇 · Endpoint 与 Identity 数字身份与标签 第 6 篇 · kube-proxy 替代 Service BPF 路径 第 7 篇 · 策略编译 NP/CNP → BPF;default-deny 窗口 第 8 篇 · Hubble 信号 flow / verdict 语义
版本锚定:Cilium v1.20.0。策略语义以
docs.cilium.io/en/v1.20/security/policy/与源码pkg/policy/、pkg/maps/policymap/@ v1.20.0 为准。无集群不伪造 Hubble deny 截图。
一、编译链全景:从声明到 map 条目
一次可强制执行的策略,最少穿过四层:
| 层 | 对象 | 职责 |
|---|---|---|
| API | NetworkPolicy /
CiliumNetworkPolicy /
CiliumClusterwideNetworkPolicy |
声明式意图 |
| 解析 | agent 把 K8s/Cilium 规则译成内部
PolicyEntry |
统一模型、标 default-deny 等 |
| 解析与蒸馏 | selector → 当前 Identity 集合;生成
MapState |
把「标签选择」变成「数字键」 |
| 数据面 | per-endpoint policy map(如
cilium_policy_*) |
TC 路径上 O(查表) 判定 |
flowchart LR
api["NetworkPolicy / CNP"] --> parse["Parse to PolicyEntry"]
parse --> resolve["Resolve selectors\nto identities"]
resolve --> mapstate["MapState + precedence"]
mapstate --> bpf["Endpoint policy BPF map"]
bpf --> verdict["ALLOW / DENY / proxy_port"]
相对「标签 → IP → iptables」链,Cilium 的关键省略是:策略规则本身尽量不绑定 Pod IP。IP 仍出现在 ipcache(IP→Identity)里;策略 map 的键是 对端 Identity + 方向 + L4(协议/端口前缀)。因此同标签集合的 Pod 扩缩容时,策略条目不必为每个新 IP 重写一遍——这是 Identity 模型相对 IP 策略的核心收益(产品叙述见 k8s-network/13;机制钉在第 2 篇)。
源码锚点(tag v1.20.0):
- K8s NP
解析:
pkg/k8s/network_policy.go(ParseNetworkPolicy)——把 ingress/egress 译成内部条目,并处理空规则时的 default-deny 语义。 - 蒸馏与 map 状态:
pkg/policy/(含 resolve、mapstate;社区叙述中 LPM trie 索引方向/协议/端口,Identity 集合侧挂,以避免「一规则选中成千上万 Identity」撑爆 trie)。 - BPF 侧:
pkg/maps/policymap/policymap.go与bpf/中policy_entry——条目含 deny 标志、proxy port(L7 重定向)、auth/precedence/cookie 等字段。
二、Kubernetes NetworkPolicy 如何进 Cilium 模型
2.1 允许语义与 default-deny 触发
K8s NP 只有「允许」语义:被选中的 Pod 在某一方向一旦有策略,该方向进入默认拒绝,只放行显式允许的流量。Cilium 解析时把这一契约映射过来:官方 Policy Intro 写明——
- 默认(无策略选中)时,endpoint 出入站皆允许。
- 任一规则选中该 endpoint 且含 ingress 段 → 该 endpoint 入站切入 default-deny。
- 任一规则选中且含 egress 段 → 出站切入 default-deny。
- 切入是 按方向独立 的:只有 ingress 策略时,egress 仍可保持允许,反之亦然。
ParseNetworkPolicy 路径上,K8s 规则的
verdict 为 Allow,并带 DefaultDeny = true
一类标记(与空 ingress/egress 列表如何合成 deny
的细节以源码与 K8s
语义为准)。工程含义:第一条「看起来很宽」的
NP,也可能瞬间把命名空间切进默认拒绝——若允许列表不完整,Hubble
会看到大量 Policy denied,而不是「策略没生效」。
2.2 CiliumNetworkPolicy 的增量能力
CNP 在同一编译框架上扩展:
fromEndpoints/toEndpoints、toServices、toFQDNs、toCIDR等选择器;- L7 规则(HTTP/Kafka 等)——编译结果往往不是纯 ALLOW,而是带 proxy_port,把流量重定向到节点 Envoy(边界见 第 10 篇);
- 显式 Deny(相对 K8s NP 的表达力);
EnableDefaultDeny:允许写「选中 endpoint 但不触发该方向 default-deny」的规则(例如集群范围 DNS 可见性策略),避免「第一条策略误杀」。
EnableDefaultDeny 对 L7 策略的适用边界以官方
Layer-7 章为准;不要假设「所有 CNP 字段对 L3/L4/L7
同质」。
三、蒸馏:selector 变成 Identity 键
编译的中段是 CachedSelector → NumericIdentity 集合。agent 维护「哪些 Identity 满足该 selector」的缓存;标签变化、Pod 起停、ClusterMesh 远端 Identity 同步,都会使集合变动,从而触发相关 endpoint 的 map 重算。
伪过程(语义,非完整源码):
- 对 endpoint \(E\),收集所有选中它的规则,按方向合并为 L4Policy。
- 对每个 L4 filter,把对端 selector 解析成 Identity 集合 \(I\)。
- 为每个 \(id \in I\) 生成 map 键:方向、协议、端口(可带前缀长度)、Identity。
- 插入
MapState时应用优先级:Deny 压过 Allow;更具体端口压过通配;被更高优先级宽规则覆盖的窄规则可被抑制(mapstate叙述)。
数据面查找不再扫 YAML,而是:对端 Identity(经 ipcache 由源/目的 IP 解析)+ L4 → 条目 → ALLOW / DENY / 重定向代理。
与 Service 的交界:到 ClusterIP 的策略经常要表达「允许访问某 Service」。编译必须把 Service 选择器展开为后端 Identity 或特殊实体;Service 后端滚动时,允许集合跟着 Identity 变,而不是跟着临时 IP 变。若只按 IP 写 CIDR 例外,会重新引入 IP 策略的脆弱性。
四、默认拒绝窗口:三类时间缝
「窗口」不是玄学,是控制面与数据面的时序差。
4.1 切入 default-deny 的瞬间
时刻 \(t_0\):命名空间无策略,流量全通。
时刻 \(t_1\):第一条含
ingress 的 NP 被 agent 接受并开始计算。
在 map 写完之前,可能出现:
- 短暂仍按旧模式放行(尚未切入 deny);或
- 已切入 deny 但允许条目未写全 → 合法流量被拒。
第二种更常见于「先 apply
窄策略、后补例外」的操作顺序。工程纪律:先在非生产验证完整允许集,或使用不触发
default-deny
的可见性策略作第一刀(EnableDefaultDeny)。
4.2 Identity 分配与标签风暴
Pod 创建或标签变更时,要经过:标签 → Security Identity
分配(本地或 kvstore/CRD 路径)→ ipcache 传播 → 策略 map
重算。在 Identity 尚未稳定前,对端可能被看成
reserved:world / init / 错误
Identity,策略结果与稳态不一致。
这对应系列关键问题 2:Identity 消除了 IP 刷新税,但把一致性问题移到「身份分配与传播窗口」。标签频繁抖动(控制器乱改 label)会放大窗口,表现为「偶发拒绝」。
4.3 远端与 ClusterMesh
跨集群时,远端 Identity 与 ipcache 依赖 ClusterMesh 控制面同步(第 11 篇)。同步滞后时,本地策略已引用远端 selector,但 map 中尚无对应 Identity → 该通的跨集群流被拒。这不是「策略写错」,而是 编译输入集合不完整。
五、Identity 与策略的交互:值班归因表
| 现象 | 更可能机制 | 核对方向 |
|---|---|---|
| apply 后立刻大面积 DROPPED | 切入 default-deny,允许集不全 | 策略 YAML 是否覆盖 DNS/指标刮取等 |
| 仅新 Pod 被拒,旧 Pod 正常 | 新 Identity 未进对端 map / init Identity | 第 2 篇生命周期;cilium identity get
类语义 |
| 改标签后间歇失败 | 旧 Identity 残留与新 Identity 并存窗口 | 谁还在用旧 ID 发流 |
| 同标签 Pod 行为不一致 | 实际安全标签集并不相同(含命名空间标签) | 打印 endpoint 的 security labels |
| L7 规则「L4 通但业务 403」 | 流量进 Envoy,L7 拒绝 | Hubble L7 字段;第 10 篇 |
| CIDR 放行但 Identity 策略仍拒 | 多规则优先级 / Deny 优先 | 查是否另有 deny CNP |
无伪造输出前提下,常用命令语义:
# 语义:看 endpoint 策略是否 enforcing、identity、策略来源
cilium endpoint list
# cilium endpoint get <id>
# 语义:策略相关 dump(以本机 agent 为准)
# cilium bpf policy get <endpoint-id>六、L7 编译出口:proxy_port 不是 ALLOW
当规则含 L7 匹配时,policy map 条目可携带 proxy port:BPF 不在内核解析 HTTP,而是把连接重定向到节点上的 Envoy,由用户态做路径/方法判定。这改变了失败面:
- L4 map 可能显示「允许重定向」,真正拒绝发生在 Envoy;
- Envoy 过载或配置滞后时,表象是超时而非干净的 Policy denied;
- 观测必须同时看 Hubble 的 L7 verdict(第 8 篇)与 Envoy 指标。
因此「策略编译」在 L7 边界上分裂成 BPF 编译 与 Envoy 配置下发 两条管线;只盯一张 policy map 会误判。
七、谱系、争论与开放问题
谱系:主机防火墙 / netfilter 按地址端口过滤 → SDN 与标签化策略(多系统并行演进)→ Kubernetes NetworkPolicy 以 Pod selector 表达意图但实现常退回 IP → Cilium 把 Security Identity 抬成数据面一等公民,策略编译成 Identity 键的 BPF map。学术上「基于身份的访问控制」谱系很长;工程上这里的 Identity 是 集群内安全标签等价类的数字编号,不是 SPIFFE 全文。
争论:
- IP 策略简单性 vs Identity 一致性窗口:IP 规则 intermediate 状态好懂;Identity 稳态更稳,但分配与传播窗口是新故障类。
- 默认允许起步 vs 默认拒绝起步:Cilium
对齐 K8s「无策略则允许、有策略则按方向
deny」;零信任偏好者抱怨窗口过大,平台偏好者抱怨第一条策略太容易误杀——
EnableDefaultDeny是产品层缓解,不是形式化证明。 - Deny 优先与可理解性:显式 Deny 增强表达力,也让「为何被拒」更依赖 precedence 实现细节。
开放问题:
- 标签风暴下 Identity 分配延迟的可观测 SLO:能否把「pending identity」暴露成与调度延迟同级的平台指标?
- 编译结果的差分可解释性:一次 CNP 变更影响哪些 endpoint map 条目、是否触发 default-deny 切换——仍高度依赖 agent 日志与手工 dump。
八、实体、世界流量与「看不见的依赖」
策略编译不只处理 Pod selector。Cilium 还把若干
实体(entity) 与保留 Identity(如
host、world、remote-node、init
等,具体集合以 v1.20 文档为准)编进同一套 map
键空间。出站访问公网、节点进程访
Pod、健康检查路径,常常依赖这些实体是否被显式允许——尤其在
egress 已切入 default-deny 之后。
典型误伤清单:
- 应用要解析 DNS,但策略只放行到 backend Identity,未放行 kube-dns / CoreDNS;
- 指标刮取从
world或节点 CIDR 进来,ingress deny 后 Prometheus 变空,业务却「看起来还通」; - 健康检查来自节点或云 LB,被当成未允许的
world/ 未识别 Identity。
这些不是编译器 bug,而是 允许集相对真实依赖不完整。变更策略前应列出进程的实际对等体(DNS、身份提供者、旁路 sidecar),再翻译成 Identity/实体规则。toFQDN 规则还会引入 DNS 代理与 IP 缓存侧效应:解析结果写入可匹配集合的时序,本身又是一类窗口——细则见官方 FQDN 章,本篇只强调「FQDN 规则的数据面依赖 DNS 路径活着」。
审计模式(audit)下,编译仍可产生「本应拒绝」的判定,但数据面放行并记日志。它适合演练 default-deny,不能当成生产强制;把 audit 面板上的 deny 计数直接当「已拦截攻击」会高估安全水位。
同一 endpoint 上多层策略(命名空间 NP、CNP、clusterwide CNP)合并时,最终 map 是 蒸馏后的并集与优先级结果,不是「文件里 sal 靠前的规则先匹配」。排障应 dump 生效 map 或 endpoint 策略视图,而不是只 diff 最新一次 YAML。
九、本篇边界
不写:每一种 toFQDN/DNS 代理细节、Tetragon、完整 CNP
字段百科。
下一篇把 Hubble flow 字段与 metrics
分工钉死,回答「指标全绿为何仍丢包」。
一句话收束:策略编译的本质是 selector→Identity→per-endpoint map;default-deny 按方向切入,窗口来自写 map 时序、Identity 传播与跨集群同步,而不是 YAML 里多写了一个空格。
参考资料
规范 / 官方文档(A)
- Cilium v1.20 — Policy
Enforcement / Intro — endpoint default
policy、
EnableDefaultDeny - Cilium v1.20 — NetworkPolicy / CiliumNetworkPolicy 语言参考(security/policy 章)
- Kubernetes — NetworkPolicy 语义(默认拒绝按方向)
源码(A)
cilium/ciliumv1.20.0:pkg/k8s/network_policy.go、pkg/policy/、pkg/maps/policymap/policymap.go、bpf/策略相关头文件
论文 / 对照(A/B)
- 可编程数据面与策略 offload 的工程对照:Høiland-Jørgensen et al., CoNEXT 2018(XDP);站内 eBPF vs netfilter 争论章
- Identity 相对 IP 策略的动机叙述:Cilium 官方 Concepts(Identity)——作工程文献,不作独立学术定理
站内
→ 上一篇:kube-proxy 替代 · 系列目录 · 下一篇:Hubble 信号
读完这篇,下一步读什么
优先读同系列或同问题的下一篇,把单篇消费变成主题集群。
【Cilium / eBPF】Endpoint 与 Identity:数字身份、标签选择与生命周期
钉清 Cilium Endpoint 与 Numeric Identity 的关系:安全相关标签如何导出集群范围数字身份、保留身份与 well-known 身份、标签变更时的短暂 deny/allow 窗口,以及相对 IP 策略消除了什么。
【Cilium / eBPF】排障坐标系:丢包、policy deny、identity 漂移、Service 黑洞与加密失败
按五轴映射 Cilium 东西向故障:通用丢包、Policy denied、identity 漂移窗口、Service/ClusterIP 黑洞、透明加密失败;每轴绑定 status/Hubble/map 层,一次否证一轴。
Cilium / eBPF 数据面内核:从 Identity 到包路径
补齐站内 Cilium 产品单篇与 eBPF 内核实现之上的数据面内核层:Endpoint/Identity、BPF 挂载与 Map、东西向包路径、kube-proxy 替代、策略编译、Hubble、加密与 Mesh/ClusterMesh 边界,并以排障与选型收束。
【Cilium / eBPF】数据面全景:五轴坐标系与 16 篇路线
相对 k8s-network/13、ebpf/ 与 Envoy/HAProxy 排除树,钉清 Cilium eBPF 数据面内核缺口;定义 Identity、BPF 挂载、Map 状态、包路径、策略与观测五条坐标系,给出 16 篇阅读路线。