第 26 章 安全实战攻防与最小暴露:攻击者进了集群会怎么走
墨:第 13 章我们站在”设计者”这边,把认证、RBAC、NetworkPolicy、Pod 安全(PSA)这些防御工事搭了一遍。但设计者视角有个盲区——你以为的防线,在攻击者眼里是一张藏宝图。这章我们换座位,坐到攻击者那边,顺着 K8s 的真实结构走一遍:一个只拿到”一个普通 Pod”的入侵者,怎么一步步摸到 etcd、摸到节点、摸到云账号。
你:我一个普通 Pod,连 API Server 都未必能访问,能掀起多大风浪?
墨:你太小看”默认配置”了。绝大多数集群的翻车,不是攻击者多牛,是默认权限太宽松 + 攻击者踩着设计机制顺梯子爬。K8s 的设计(ServiceAccount 自动挂载、挂载宿主盘的便利、可访问云元数据的网络)每一处”贴心”,都可能是一级台阶。我们先把攻击链画出来,再反推每一级的”最小暴露”怎么收。
26.1 真实的攻击链:从 Pod 到控制面
别背 CVE 列表,先看”路径”。一个典型的 K8s 横向移动链:
普通 Pod(低权)
├─① 拿到 ServiceAccount Token(默认挂载)
│ └─→ 调 API Server:列举 Secret / 提权 / 看得到不该看的
├─② 读容器内可访问的云元数据(169.254.169.254)
│ └─→ 拿到云 IAM 临时凭证 → 直接操控云资源(S3、RDS、甚至控制台)
├─③ 挂载了 hostPath 或特权容器
│ └─→ 逃逸到宿主机:读 kubelet 凭证、改 kubeconfig、控制节点
├─④ 容器以 root 跑 + 内核漏洞
│ └─→ 容器逃逸(runC / Dirty Pipe 类漏洞)→ 宿主 root
├─⑤ NetworkPolicy 没配 → 东西向全通
│ └─→ 在Pod 网络里随意扫其他服务的未授权接口
└─⑥ 拿到 etcd 读权限 → 全集群 Secret 明文到手
墨:你看这条链,哪一环是”K8s 设计者故意留的”,哪一环是”运维没关默认门”?
你:① 默认挂载 SA Token 是 K8s 的默认行为(设计便利);② 云元数据是云厂商的便利;③④ 是”配置没收敛”(特权/hostPath/root);⑥ etcd 不设访控是部署失误。攻击者走的就是”便利 + 失误”搭的梯子。
墨:精辟。80% 的 K8s 安全事故,不是 0day,是”默认便利”撞上”没关的门”。所以安全的第一性原理——默认最小暴露,把每级梯子都抽掉或上锁。
26.2 第①级:ServiceAccount Token 与 API Server 之门
26.2.1 默认挂载的陷阱
- 默认每个 Pod 都会被挂载
/var/run/secrets/kubernetes.io/serviceaccount/token——一个 SA 的 JWT。如果这个 SA 恰好被绑了cluster-admin(很多 Helm chart 图省事就给clusterrolebinding到 admin),那这个 Pod 里跑的任何代码都能直接以集群管理员身份调 API Server。
墨:你部署一个第三方中间件(比如某个 chart),它的 SA 被绑了 cluster-admin,你觉不觉得有问题?
你:有问题。一个”缓存”凭什么需要集群管理员权限?这是典型的”权限过度授予”——chart 作者懒得精细授权,直接给最大权限,用户也无脑装。
墨:所以**最小权限(least privilege)**不是口号。每个 SA 只给”它确实要做的那几件事”的 verb+resource。第 13 章讲 RBAC 时说过,RBAC 是”白名单”——没显式允许的都拒绝。但很多人用 kubectl create clusterrolebinding ... --clusterrole=cluster-admin 一把梭,白名单瞬间变万能钥匙。
26.2.2 Token 的演化:从无限期到 Bound Service Account Token
老模型里 SA Token 是长期有效、可吊销但默认不轮换的静态 JWT。一旦泄露,攻击者能一直用。演化解法(1.21+ GA):Bound Service Account Token(TokenRequest API)——Token 是”短期、绑定 Pod/身份、可过期、可投影挂载”的。配合 automountServiceAccountToken: false,甚至可以完全不挂载 token 给不需要访问 API 的 Pod。
# 不需要访问 API Server 的 Pod,干脆不挂 token
spec:
automountServiceAccountToken: false
containers: [...]🔧 最小暴露动作 1:给所有”纯业务逻辑、不调 K8s API”的 Pod 设 automountServiceAccountToken: false。攻击者即便进了这个 Pod,也拿不到任何 SA Token——第①级梯子直接抽掉。
26.3 第②级:云元数据 169.254.169.254 的致命诱惑
这是云上 K8s 头号翻车点,值得单独讲。云厂商给虚机提供”实例元数据服务”(IMDS),Pod 里访问 http://169.254.169.254/latest/meta-data/iam/security-credentials/ 能拿到该节点所用 IAM 角色的临时凭证。如果这个 IAM 角色权限大(很多默认 Node 角色权限过宽),攻击者拿到凭证就能直接调云 API:列 S3、删 RDS、甚至改负载均衡把流量导走。
真实案例(2018 年 Capital One):攻击者通过一个有 SSRF 漏洞的 Web 应用(跑在 EC2 上的容器),访问 IMDS 拿到 IAM 凭证,进而读了 1 亿人的 S3 数据。容器没破,云账号先破。
墨:这跟 K8s 本身的安全模型有什么关系?不是云厂商的锅吗?
你:是云的便利,但 K8s 默认不阻断 Pod 访问 IMDS。Pod 的网络能直连元数据地址,等于把云凭证的门口向每个容器敞开。K8s 该怎么管?
墨:两条演化出路:
- IMDSv2(会话化):AWS 要求先拿 token 再访问,且可禁用”无 token 的明文访问”,缓解但没根除(Pod 仍能拿 token)。
- NetworkPolicy 拦截:用 NetworkPolicy / 防火墙禁止 Pod 访问 169.254.169.254(或限制只有特定 Pod 能访问)。这是 K8s 侧最实在的一道闸。
- 节点 IAM 角色收敛:给 Node 角色最小权限,不要给它能删库的大权(改用 IRSA——IAM Roles for Service Accounts,把权限精确到 Pod 级 SA,而非节点级)。
🔧 最小暴露动作 2:NetworkPolicy 默认拒绝 Pod 访问 169.254.169.254/32;用 IRSA 替代”节点共享 IAM 角色”。这道闸抽掉的是第②级梯子——攻击者进了 Pod 也摸不到云凭证。
26.4 第③④级:容器逃逸与宿主沦陷
26.4.1 特权与 hostPath:最粗暴的逃逸梯
privileged: true:容器拿到宿主的所有 capabilities,等于”宿主 root”。攻击者进这种容器,直接能mount宿主盘、改宿主机文件。hostPath挂载:把宿主目录(如/etc、/var/lib/kubelet)挂进容器。挂载/var/lib/kubelet就能读 kubelet 的 kubeconfig,进而控制节点。hostPID/hostNetwork:共享宿主 PID/网络命名空间,能看到宿主所有进程、能嗅探宿主网络。
第 13 章讲过 PSA(Pod Security Admissions)三档:privileged / baseline / restricted。生产命名空间应 enforce restricted——禁止 privileged、禁止 hostPath(除非显式例外)、禁止以 root 跑、要求 seccomp、要求 runAsNonRoot。
墨:PSA 的 restricted 档,是不是把一切逃逸都挡了?
你:挡了”配置类”逃逸(特权/hostPath/root)。但挡不了”内核 0day”——如果容器运行时或内核本身有漏洞(如 runC 逃逸 CVE-2019-5736、Dirty Pipe),即使 restricted,攻击者也能借漏洞逃出。所以 PSA 是第一道闸,补丁和运行时防护是第二道。
墨:对。安全是纵深防御(defense in depth),没有单点银弹。PSA 收配置、镜像扫描收漏洞、内核补丁收 0day、seccomp/AppArmor 收系统调用面——层层叠。
26.4.2 runAsNonRoot 与只读根文件系统
securityContext:
runAsNonRoot: true
runAsUser: 1000
readOnlyRootFilesystem: true # 根fs只读,写只进 emptyDir/volume
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"] # 丢掉所有 Linux capabilitiesreadOnlyRootFilesystem: true 让攻击者即使进了容器,也难以落地下载的恶意二进制(只能写挂载的卷);drop ALL capabilities 把容器的”超能力”清零——连 chown、net_bind_service 都没有,极大缩小逃逸面。
🔧 最小暴露动作 3:全集群 enforce PSA restricted + 上述 securityContext 模板化(用 Kyverno/Gatekeeper 策略强制)。第③④级梯子(特权/hostPath/root)被抽掉。
26.5 第⑤级:东西向全通的网络
默认 K8s 没有 NetworkPolicy 时,Pod 之间网络全通。攻击者落在一个 Pod 里,能扫整个集群的未授权服务:Redis 没密码?MySQL 弱口令?某个内部 admin 接口没鉴权?全在他射程内。
这是个横向移动的高速公路。真实攻击往往”先攻一个弱服务,再借它访问旁的服务”。NetworkPolicy 的作用就是把这条路切成段。
# 默认拒绝某命名空间的所有入向,再显式放行必要流量
kind: NetworkPolicy
apiVersion: networking.k8s.io/v1
metadata: { name: default-deny, namespace: production }
spec:
podSelector: {}
policyTypes: ["Ingress"] # 没列规则 = 拒绝所有入向原则:
- 默认拒绝(default-deny)打底。
- 按真实调用关系显式放行(前端→后端→数据库,逐跳授权)。
- 跨命名空间、跨 Pod 的访问都要有名有姓。
墨:NetworkPolicy 是 L3/L4 的,挡得了”应用层”的越权吗?比如一个服务被授权访问数据库,但它被攻陷后干坏事?
你:挡不了。NetworkPolicy 只管”能不能连”,不管”连了之后干啥”。应用层越权要靠应用自身鉴权 + 服务网格的 mTLS + 审计。又是纵深防御——NetworkPolicy 收”连通性”,网格/鉴权收”行为”。
🔧 最小暴露动作 4:全集群默认拒绝 + 按调用图放行 NetworkPolicy。第⑤级横向移动高速路被切断。
26.6 第⑥级:etcd 与 Secret 的终极暴露
如果攻击者一路爬到能读 etcd(比如拿到了 etcd 的客户端证书,或控制了 API Server 节点),那全集群的 Secret 都是明文——因为默认 etcd 里的 Secret 只是 base64(不是加密!第 12 章强调过)。base64 是编码不是加密,谁都能解。
收口动作:
- etcd 静态加密 + KMS:配
encryption-provider-config用 KMS(云密钥管理)加密 etcd 中的 Secret(甚至所有资源)。这样即使 etcd 落盘被拖走,没有 KMS 密钥也解不开。 - 严格管控 etcd 访问:etcd 的 peer/client 证书只发控制面节点,绝不进 Pod。
- Secret 外置:敏感凭证放外部密钥库(Vault/云 Secrets Manager),运行时注入,而不是长期躺 etcd。
🔧 最小暴露动作 5:启用 etcd 静态加密(KMS provider)+ Secret 外置。第⑥级的”拿到 etcd 就全集群裸奔”被封。
26.7 把五道闸汇成”最小暴露清单”
从攻击链反推,一份可落地的最小暴露清单(按性价比排序):
| 优先级 | 动作 | 抽掉的攻击梯 | 成本 |
|---|---|---|---|
| P0 | automountServiceAccountToken: false(无需 API 的 Pod) | ① SA Token | 极低 |
| P0 | 全集群 PSA enforce restricted | ③④ 特权逃逸 | 低 |
| P0 | NetworkPolicy 禁 169.254.169.254 + IRSA | ② 云凭证 | 低 |
| P1 | 默认拒绝 NetworkPolicy + 按调用图放行 | ⑤ 横向移动 | 中 |
| P1 | RBAC 最小权限,禁用 cluster-admin 乱绑 | ① 提权 | 中 |
| P1 | etcd 静态加密(KMS)+ Secret 外置 | ⑥ 全集群 Secret | 中 |
| P2 | securityContext 模板(nonroot/ro/drop cap) | ③④ 落地下载 | 低 |
| P2 | 镜像漏洞扫描 + 内核及时补丁 | 0day 逃逸 | 中 |
| P2 | 审计日志 + 异常告警(谁在列 Secret) | 全链检测 | 中 |
墨:你看这张清单,是不是每一条都对应前面某章的一个”默认便利”或”配置失误”?
你:是。①对应 SA 默认挂载(26.2),②对应云元数据(26.3),③④对应特权/hostPath/root 默认允许(26.4/第13章),⑤对应 NetworkPolicy 默认全通(第9/13章),⑥对应 Secret 默认明文(第12章)。攻防视角,就是把”设计便利”逐个收回。
墨:这就是这章的核心——安全不是加功能,是收暴露。设计师给你便利,攻击者用便利当梯子,你的活儿是把不必要的便利关掉,只留业务真正需要的那点开口。纵深防御 = 每层都收一点,让攻击者即使破一层也到不了底。
26.8 检测与响应:假设”已被攻破”
最后一层心智——永远假设攻击者已经进来了。边界防御(防火墙/认证)会被破,所以要在”内部”布置检测:
- 审计日志(Audit):API Server 的 audit log 记录”谁、何时、对什么资源、做了什么”。谁在半夜批量
get secret?谁在create clusterrolebinding给自己提权?这是最该告警的信号。 - 异常行为:用 Falco 等运行时安全工具,监控”容器里突然起了
bash""访问了 IMDS""挂载了宿主盘”这类异常系统调用。 - RBAC 使用分析:定期用
rakkess/kubectl-who-can查”谁有 pod/exec""谁有 secret 读”——把过度授权揪出来。
📦 案例:某团队靠 audit log 发现,一个第三方 chart 的 SA 每天凌晨 list secrets 全集群——一查,是 chart 里一个”健康检查”脚本写错了,本只想读自己的 ConfigMap,却拿了 cluster-admin 去 list 全量 Secret。虽然没恶意,但暴露面巨大。收掉权限后,攻击面直接少一大块。
26.9 演化线小结
- 问题:K8s 的”设计便利”(SA 默认挂载、云元数据可达、特权/hostPath 默认允许、NetworkPolicy 默认全通、Secret 默认明文)叠加”配置失误”,给攻击者搭了一条从 Pod 直达控制面/云账号的梯子。
- 设计分离(收暴露):
- 用
automountServiceAccountToken: false抽 SA 梯子; - 用 NetworkPolicy + IRSA 抽云凭证梯子;
- 用 PSA
restricted+ securityContext 抽特权/root 逃逸梯子; - 用默认拒绝 NetworkPolicy 切横向移动高速路;
- 用 RBAC 最小权限 + etcd KMS 加密封提权与 Secret 明文;
- 用 audit/Falco 做”已被攻破”假设下的检测。
- 用
- 演化规律:又是”兼容优先渐进收口”——PSA 用三档渐进(privileged/baseline/restricted)让老工作负载不被一刀切拒,新负载 enforce 最严;Bound SA Token 用短期可过期替代长期静态 JWT;IMDSv2 用会话化缓解而非强行禁用。K8s 的安全演化,是”把便利一层层收回,但给老负载留活路”的典型路径。
26.10 思考题
🔧 思考题 1(云元数据):为什么”容器没被攻破,但云账号先被攻破”在云上 K8s 里如此常见?K8s 默认给了攻击者什么便利?
【参考答案】因为云厂商的 IMDS(169.254.169.254)对所有虚机/Pod 默认可达,且节点 IAM 角色常被授予过大权限(为省事)。攻击者只要在一个有 SSRF/任意请求能力的容器里 curl 这个地址,就能拿到节点的云临时凭证,直接调云 API 操作 S3/RDS/控制台——整个过程完全不需要攻破 K8s 本身,也不需要容器逃逸。K8s 默认不阻断 Pod 访问 IMDS,等于把云凭证门口向每个容器敞开。解法:NetworkPolicy 禁 IMDS + IRSA 把权限精确到 Pod 级 SA(节点角色只留最小必要权限)。
🔧 思考题 2(PSA 边界):PSA restricted 档能挡住所有容器逃逸吗?如果不能,缺口在哪,用什么补?
【参考答案】不能。PSA restricted 只收”配置类”的逃逸面——禁止 privileged、hostPath、hostPID/Network、强制 runAsNonRoot、要求 seccomp。它挡不住容器运行时或内核自身的漏洞逃逸(如 runC CVE-2019-5736、Dirty Pipe CVE-2022-0847):这类攻击者借的是运行时/内核 bug,与 Pod 配置无关,即使 restricted 也会被逃出。补法是纵深防御的第二层:及时打容器运行时和内核补丁、用 seccomp/AppArmor 进一步收系统调用、用只读根文件系统 + drop ALL capabilities 缩小落地下载与提权空间、用 gVisor/Kata 等强隔离运行时作关键负载的”沙中沙”。PSA 是第一道闸,不是终点。
🔧 思考题 3(Secret 明文):有人说”我们的 Secret 是 base64 加密的,很安全”。这句话错在哪?正确的”加密”应该怎么做?
【参考答案】base64 是编码(encoding)不是加密(encryption)——它只是把二进制变成文本便于传输,任何人 base64 -d 一下就还原明文,没有任何密钥保护。etcd 里默认的 Secret 就是 base64,谁有 etcd 读权限谁就能看到所有密码。正确做法:① 启用 etcd 静态加密,配 encryption-provider-config 用 KMS(云密钥管理)或本地密钥 provider 对 etcd 中的 Secret(甚至所有资源)做 AES 加密,这样即使 etcd 落盘被拖走,没有 KMS 密钥也解不开;② Secret 外置,敏感值放 Vault/云 Secrets Manager,运行时注入,不长期躺 etcd;③ 配合 RBAC 严格限制 get secret 的权限,并 audit 监控谁在批量读 Secret。
🔧 思考题 4(进阶·总账):你发现生产集群里一个”第三方日志采集 agent”的 Pod 被绑了 cluster-admin,且 privileged: true、hostPath 挂了 /var/log,还默认挂了 SA Token。请按”最小暴露”思路,列出你会怎么在不破坏它功能的前提下一步步收口(它确实需要读宿主日志、确实需要访问 K8s API 拿 Pod 元数据)。
【参考答案】功能映射:读宿主日志 → 必须有 hostPath 挂载(不能禁);拿 Pod 元数据 → 必须能调 API(不能完全禁 SA)。所以”一刀切”会破坏功能,要做精准收口:
- SA 权限:cluster-admin 显然过度。用
kubectl-who-can查它到底调哪些 API(大概率是list/watch pods、get nodes),据此建一个最小 RBAC——只给特定 verb+resource 的 Role(命名空间级,非 ClusterRole),绑到该 SA。抽掉”提权到全集群”的梯子,保留”读 Pod 元数据”的能力。 - SA Token:保留挂载(因需调 API),但确保用短期 Bound Token(1.21+ 默认),并设合理过期,避免长期静态 JWT 泄露。
- hostPath:保留
/var/log挂载(功能必需),但禁止挂载其他宿主敏感路径(如/var/lib/kubelet、/etc/kubernetes),用 PSAbaseline及以上 + 显式 hostPath 白名单(Kyverno 策略只允许/var/log这一个路径)。抽掉”挂 kubelet 凭证逃逸”的梯子。 - privileged:降级。它读日志不需要 privileged 全部超能力——改用
securityContext只加必要 capability(如SYSLOG/DAC_READ_SEARCH视具体情况),drop ALL其余,runAsNonRoot尽量(若必须 root 读日志文件,限制readOnlyRootFilesystem: true防落地下载)。抽掉”容器=宿主 root”的梯子。 - 网络:NetworkPolicy 禁它访问
169.254.169.254(除非它明确要调云 API,通常日志 agent 不需要)、限制它只和 API Server、日志后端通信。抽掉云凭证梯子+横向移动。 - 命名空间隔离:把它放进独立命名空间,PSA enforce 最严档,与其他业务 Pod 网络隔离开。 核心思路:先问”它真实需要哪几个能力”,再逐个给最小权限,其余一律收掉——功能保住,暴露面从”全开”降到”刚好够用”。这就是最小暴露的工程落地。
墨:26 章把”攻防”这把刀也亮了。到这里,从序章的问题地图,到声明式/etcd/调度/控制器/网络/存储/安全/扩展机制/规模/灾备/攻防——K8s 作为”系统设计”的全貌,你算是走了一遍了。但别忘了,你最初要的不是”会用 K8s”,是”能自己造工具”。最后一章(第 19 章结语已写过,但我们这次扩展后还要把 20-26 收进目录)。真正的系统设计者,下一站是:带着这套”从问题出发、看演化”的方法论,去读 K8s 源码、去造你自己的编排/控制面。路还长,但你已经在路上了。
(第 26 章 完)