开篇:一次让我睡不着觉的集群巡检
先交代一下背景,我在一家中大型互联网公司负责云原生基础设施的稳定性与安全合规。团队从2019年就逐步把业务迁到 Kubernetes 集群,日常规模大概有十几个集群、数千个节点,线上跑的 Pod 数量自己都数不过来。按理说我在容器安全这件事上应该有点发言权,但去年夏天一次例行巡检的结果,直接让我冷汗湿透了后背——用扫描工具全量扫下来,生产集群里有将近 40% 的镜像存在高危漏洞,个别镜像居然在以 root 身份跑特权容器,而且有相当一部分敏感信息通过环境变量被明文传给了业务 Pod。那次的结果让我意识到,很多团队包括我们自己,天天在喊“云原生安全”,实际上做的只是“能用”,远不到“安全”这个标准。
后来我花了两个多月的时间把线上所有业务从镜像构建到集群运行时的安全链路完整梳理了一遍,补齐了镜像扫描、运行时加固、RBAC 权限收敛、网络策略等一系列短板。这个过程里踩了不少坑,也沉淀了一套可以复用的方法论。今天这篇实践总结,就是想把“云原生应用安全”这件事从头讲透:从最容易被忽略的容器镜像,到最终承载业务的 k8s 集群,每一步该做什么、怎么做、为什么要这么做,我一并写清楚。内容主要面向正在一线维护集群、被安全合规搞得焦头烂额的运维和开发同学,希望能帮你们少走一些弯路,别再等扫描报告出来了才后悔。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
1. 安全这个事,为什么到了云原生时代反而更难了
1.1 传统边界消失:物理防火墙挡不住容器间的流量
很多人不理解,为什么传统架构里明明有防火墙、入侵检测、堡垒机,似乎挺安全的,一旦全量容器化反而事故频出。这里面的根本原因,是云原生把网络边界这个曾经最重要的安全锚点给打散了。
在传统方式下,一台物理机上的业务相对独立,不同业务之间走物理交换机、防火墙,安全策略可以精准作用在两个子网之间。但在容器和 Kubernetes 场景里,同一个节点上可能同时跑了几十个不同业务的 Pod,它们通过 CNI 网络插件创建的虚拟网络互相通信。网络包从 Pod A 到 Pod B,经的是一个虚拟网桥或 overlay 隧道,物理防火墙根本看不到这些流量,也没有机会施加规则。
这就意味着,在一台物理机上,只要有一个容器被攻破,攻击者就能借助内网网络横向移动到同节点甚至跨节点的其他业务容器里。边界没了,安全模型就必须从“外部防住”改为“内部纵深设防”。这是我踩坑后最先想明白的一件事。
1.2 攻击面的维度几何级放大:镜像、编排、API全变成了入口
传统架构里,我们要防护的主要就是操作系统、中间件、业务应用三个层面。到了云原生环境,攻击面一下延展到了镜像、容器运行时、编排层、服务发现、配置中心、Ingress、Service Mesh 等多个维度。每一个维度都有自己的体系、配置和默认值,而这些默认值里面有很多是从开发便利角度出发的,比如以 root 运行容器、Pod 之间全互通、RBAC 权限放得太宽,谁都能读 Secrets。
更麻烦的是,云原生体系里业务上线交付的链路是流水线自动化的,从代码提交到生产环境部署可能就是十几分钟的事。如果没有在流水线的每个环节设置安全卡口,等于把不安全的镜像和配置直接以自动化方式送上了生产。这种“高速交付+全自动化”的特点,导致安全问题的扩散比传统架构快得多。这就要求安全实践也必须自动化地内置到整个交付链路和运行时治理中,而不是靠每个月找一天集中做安全加固,那是根本来不及的。
1.3 建立自己的安全全景图:从镜像到集群的一条链路
搞清了问题以后,我给自己画了一张云原生安全全景图,把所有要防护的环节拉成了一条流水线。这张图到今天依然是我修改团队安全策略时的最高指引,建议你也先画一张放在手边。
整条链路从最上游的代码仓库开始,经过 CI 构建产物变成镜像,镜像推到镜像仓库,再由 CD 系统拉取镜像并部署到 Kubernetes 集群。在集群内部,实际的执行单元是容器,多个容器以 Pod 为单位被调度到工作节点上,然后通过 Service、Ingress 暴露流量。安全防护要覆盖的,就是这条链路上每一段。
我把它划分为四个核心环节:镜像供应链安全、容器运行时安全、编排与控制面安全、集群网络与数据安全。后续内容我基本会沿着这四个环节讲下来,并在最后附上日常巡检过程中最容易踩的坑和排查思路。这篇文章不会帮你实现“零漏洞”,但一定能帮你建立一套可持续运行的安全基线。
2. 镜像安全:漏洞不是扫描出来的,是构建时带你进来的
2.1 基础镜像选型:一条 Dockerfile 开头就决定了 90% 的风险面
镜像安全是整个链路里投入产出比最高的一个环节,而且越靠前收益越大。我见过不少团队,业务代码写得天花乱坠,结果 Dockerfile 第一行写的是 FROM ubuntu:latest 或者干脆 FROM node:16 不带版本标签。你稍微想想就明白了,这些“浮动标签”拉到的基础镜像今天和昨天可能就不是同一份内容,这意味着你昨天的安全扫描结果今天就会失效,线上跑的镜像和流水线里构建的镜像也未必一致。这种不可复现性,对一个安全敏感的公司来说是不可接受的。
我的建议非常明确:基础镜像必须锁定到不可变标签,也就是镜像的 digest。举例来说,你可以在 Dockerfile 里写成 FROM node:16.20.2-bullseye-slim@sha256:5c3e1d4a8b0c9d7e... 这样带摘要的形式。这样做的好处是只用一行代码就锁定了整个镜像的内容,任何人都无法用同名镜像做替换攻击。很多团队觉得写 sha256 太长太啰嗦,但配合 Dependabot 或 Renovate 这类依赖更新机器人,完全能够自动帮你升级 digest,并不会增加日常维护成本。
在基础镜像具体选型上,我的优先级顺序是这样的:优先选择官方镜像中的 distroless 或 alpine 变体。distroless 镜像里连 shell 都没有,攻击者即使打进去也只能干瞪眼,想下载工具、反弹一个 shell 都无从下手;alpine 则胜在体积小,musl libc 使它的攻击面比 glibc 的 Debian/Ubuntu 镜像小不少。但注意,如果业务依赖对 glibc 有强依赖,比如某些 Python 的二进制扩展模块、Oracle 客户端,你强行用 alpine 只会让编译阶段痛苦不堪。这种时候不必执念,可以用 debian-slim 系列,但一定要定期更新版本,不要使用已经停止维护的发行版版本。
建议把基础镜像策略直接做成一条准入规则:所有业务镜像的基础镜像必须来自组织内部私有仓库的可信清单,镜像标签必须使用 digest。如果后续扫描发现某个基础镜像所有者不再跟进 CVE,马上执行替换计划。
2.2 镜像扫描的时机选择:越早发现修复成本越低,但别耽误上线
在镜像安全这块,大家讨论最多的问题是扫描工具用什么,其实比这更关键的问题是“什么时候扫”。我见过很多团队把镜像扫描接到 CD 流程发布前那一刻,等到镜像都推到生产仓库了才发现高危漏洞,然后为了不阻塞发版就一路白名单“特批”下去。这种扫描方式基本是在给合规部门做样子,真正有用的扫描必须嵌到流水线更早的阶段里。
合理的时间点应该分布在两处:第一处在 CI 阶段,也就是代码构建出镜像、还没推送到远程仓库的时候。这时候如果扫描出高危漏洞,可以立刻阻断构建流程,让开发人员回去修改依赖或者换基础镜像。这个阶段的成本最低,只需要在 GitLab CI 或 Jenkins 里加一个 job 即可。第二处在镜像仓库端,每次镜像推送完成后触发一次异步扫描,用来发现 CI 阶段漏掉的历史存量镜像。仓库端的扫描尤其适合处理那些还没来得及整改的存量镜像,你至少能通过它知道线上到底有哪些风险敞口。
扫描工具方面,目前开源社区最推荐的是 Trivy。它扫描速度快、支持的语言和依赖类型多、误报率相对较低,而且既有 CLI 工具也有容器镜像,和 CI/CD 集成非常简单。如果你用的是 Harbor 作为镜像仓库,Harbor 自己就内置了 Trivy 或者 Clair,配置一下就能扫描。商业方案可以看看 Snyk、Aqua Security 或者 Prisma Cloud,它们的漏洞库更新更及时,也能覆盖 IaC 文件扫描,但价格比较感人。团队预算少的话,Trivy+Harbor 这套组合拳已经能防住绝大多数问题。
2.3 漏洞修复的分级策略:不能发现高危就一概不让上线
扫描工具跑起来以后,你会面临新的头疼事:高危漏洞一抓一大把,难道全部都要修复了再上线吗?如果这样,业务什么都不用干了,一天到晚就升级依赖了。这里真的需要一套分级处理策略,否则安全团队和开发团队一定会打起来。
我的经验是按照漏洞可达性来分。所谓可达性,指的是这个漏洞所在的组件是否真的被业务运行时的代码路径调用了。比如一个 Java 项目里传递依赖引入了某个存在漏洞的 JSON 解析库,但你业务全程没用 JSON 解析,或者解析的是不受外部控制的内部数据,那个漏洞真实威胁就非常有限。
具体修复优先级,我会按下面这张表判断:
| 风险级别 | 判断依据 | 处理方式 |
|---|---|---|
| P0 | 漏洞可被外部直接利用,且能拿到远程代码执行或权限提升,暴露在内网公网 | 阻断上线,立刻修复,必要时回滚版本或切流 |
| P1 | 漏洞可被内部低权限用户利用,或需要一定前置条件才能触发 | 排期到当前迭代修复,同步评估临时缓解措施 |
| P2 | 漏洞存在于传递依赖,业务不可达或被间接调用 | 记录在安全债,每个版本例行升级时顺带解决 |
| P3 | 基础镜像或系统库中的中低危漏洞 | 不阻塞上线,定期重建镜像即可 |
修复的时候要记住一个重要原则:优先升级依赖版本的次版本号,不要盲目追最新。比如某组件 2.3.1 有漏洞,修复版只有 2.3.5,那你升到 2.3.5;但如果项目里还在用 1.x,就不要脑子一热跨大版本升级,那会引入一堆不兼容问题,安全问题还没解决,业务已经炸了。实际工作中,我最常推荐的做法是,对所有 P0、P1 漏洞必须留下明确的所有者、截止时间、临时缓解措施三项记录,没有这三条就不允许加白名单。有了这条规矩之后,团队里“白名单”泛滥的情况收敛了不少,尤其是那种为了赶进度而很随意加白的问题。
2.4 别忽视软件供应链攻击:镜像不是代码构建完就万事大吉了
讲完扫描和修复,镜像这块还有一个特别隐蔽,但近两年真实发生过很多惨痛案例的问题:软件供应链攻击。之前 n°vm 的 ua-parser-js 被入侵者植入恶意代码这件事,应该让很多前端团队印象深刻。攻击者不是通过打漏洞进来的,而是直接控制了某个流行的开源依赖库并投毒,只要你的 CI 流水线或基础镜像里拉取了被污染的依赖,后门就顺着构建链一路进了你的生产镜像。
针对这类供应链问题,能做的防御手段主要是这两条。第一条是依赖锁定与哈希校验,锁死 package.json / requirements.txt / go.mod 这些依赖清单中的版本号,并在获取依赖包时校验包管理器生成的 hash 文件。第二条是构建过程可信与产物签名,用 Sigstore 或者 Notary 这类工具对构建出的镜像做签名,然后在部署准入阶段校验签名,确保线上跑的镜像一定是组织内部 CI 流水线构建并签过名的,防住注册表被拖库后恶意替换镜像这种场景。
真实发生过的一种情况是:某开发同学的笔记本被入侵,攻击者通过他本地缓存的镜像仓库凭证把带后门的镜像推送到了企业私有仓库,标签恰好覆盖了某个无人维护的旧镜像。如果我们在准入控制器里校验了镜像签名,这种攻击就没法落地。这件事我不止一次提过,如果你的集群还在直接从 Docker Hub 拉镜像而不经过私有仓库,建议尽早改掉。
3. 容器运行时安全:镜像干净不代表运行时就安全
3.1 先搞清楚容器隔离的本质:别把容器当成虚拟机
镜像安全解决了之后,下一个紧跟着的问题就是容器运行时安全。很多运维同学把容器和虚拟机混为一谈,认为 Docker 容器和虚拟机一样能提供硬隔离,这其实是天大的误解。虚拟机是通过 hypervisor 虚拟出一整台硬件设备,每个虚拟机有自己独立的内核;而容器更准确地说是宿主机上的多个进程,只是通过 Linux 内核的 namespace 和 cgroups 做了资源视图隔离与限额。
这就带来一个直接的推论:容器与宿主机共享同一个内核。一旦攻击者利用了某个内核漏洞逃逸出容器的 namespace 隔离,他拿到的权限就是宿主机 root 权限。所以,对容器运行时安全策略的每一分投入,本质上都是在降低“内核漏洞一旦被利用所能造成的危害”。你不可能彻底防住内核漏洞,但你有办法让攻击者就算拿到了容器内 root,也做不了多少动作。
3.2 最基础也最反直觉的规则:别再用 root 用户跑容器了
在镜像安全问题解决以后,第一个必须处理的运行时风险点是容器内用户权限。很多镜像在构建的时候偷懒,没有在 Dockerfile 里写 USER 指令,于是容器启动后默认就以 root 身份运行。问题是容器里的 root 和宿主机上的 root 在 Linux 权限模型下是同一个 UID 0。虽然容器有 namespace 隔离,权限边界并不像大家想的那样绝对,一旦攻击者获得容器内 root 并配合某些系统调用,就有机会对宿主机发起攻击。
现实中我见过最夸张的案例是,有团队直接在 Deployment 里加了 privileged: true 来让容器访问宿主机设备。这个操作实际上相当于把宿主机的大部分设备直接交给容器,攻击者在容器里做任何操作都等于在宿主机上做,连逃逸这一步都省了。我强烈建议你在集群的准入控制里直接禁止 privileged 容器。如果你的业务确实需要访问某些宿主机内核能力,可以仔细评估是不是能用 Kubernetes 官方支持的能力列白名单,或者用 device plugin 这类更受控的机制,而不是一刀切放开 privileged 配置。
一个好的默认策略是这样的:所有镜像在构建阶段就创建一个非 root 用户,并通过 USER 指令切换过去;如果镜像不允许改,就在部署清单里显式设置 securityContext.runAsNonRoot: true,让 kubelet 直接拒绝以 root 运行的 Pod。这个配置操作简单、成本低,是我在所有集群里最优先推行的第一条运行时安全加固项。
3.3 用 SecurityContext 做一次完整的最小权限加固配置
容器的安全策略目前最主流的落地方式是通过 Pod 的 securityContext 字段来声明。这个字段的作用,相当于告诉容器运行时“我这个容器需要什么权限、不需要什么权限,请你按需分配”。下面这份 Deployment 配置基本代表了我当前在团队里推动的标准模板,读者可以直接拿去参考。
yaml复制apiVersion: apps/v1
kind: Deployment
metadata:
name: secure-app
namespace: production
spec:
replicas: 3
selector:
matchLabels:
app: secure-app
template:
metadata:
labels:
app: secure-app
spec:
securityContext:
runAsNonRoot: true
runAsUser: 10001
runAsGroup: 10001
fsGroup: 10001
seccompProfile:
type: RuntimeDefault
containers:
- name: app
image: registry.internal.example.com/secure-app:v1.2.3@sha256:8b8f6c7dee1a
imagePullPolicy: Always
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
add:
- NET_BIND_SERVICE
resources:
limits:
cpu: "1"
memory: 1Gi
解释一下每段配置的含义。runAsNonRoot 是运行时校验,确保容器不会以 UID 0 运行;runAsUser 和 runAsGroup 显式指定 UID,避免镜像内用户和宿主机属主冲突导致文件权限问题;fsGroup 的作用是让挂载进来的 Volume 文件系统组归属到 10001,这样容器内用户才能正常读写持久化数据。如果你忘了设置 fsGroup,会遇到一个很经典的故障:Pod 启动正常但写入挂载盘时一直报 permission denied,因为卷的属主还是 root。
容器自身的 securityContext 里,allowPrivilegeEscalation: false 禁止了进程通过 setuid 提权,这个在容器里非常推荐。readOnlyRootFilesystem: true 会把容器根文件系统设成只读,强制业务把临时文件写到 emptyDir 卷里。很多应用最开始会因为这个配置跑不起来,比如有些 Java 应用默认往 /tmp 写文件,或者 Python 应用在根目录创建 .pyc 文件,只要加上一个临时卷映射即可解决,适应期不会太长。capabilities 这块,先 drop: ALL 丢弃 Linux 默认给容器的全部能力,再根据业务实际需要 add 回特定的能力。比如 Nginx 想监听 80 端口就需要 NET_BIND_SERVICE,如果业务上用不到任何特殊能力,就不需要加任何 add 项。
3.4 为什么默认的 seccomp 和 AppArmor 值得开启
Kubernetes 从 1.19 版本开始给容器运行时默认附带了 seccomp 配置文件,你在安全上下文里设置 seccompProfile.type: RuntimeDefault 就能启用它。这个配置会根据容器运行时预设的策略,限制容器能发起的系统调用,相当于给内核资源访问加了一道过滤闸门。比较常见的影响是针对某些依赖特殊系统调用的业务的,比如调试工具 strace 在容器里可能就失效了,但绝大多数常规业务应用并不会受影响。
AppArmor 和 SELinux 这类强制访问控制模块也有类似价值,不过 AppArmor 需要操作系统层面预先加载对应的 profile,配置复杂度略高。如果你团队的安全基线是 Ubuntu 或 Debian 系的宿主机,建议编排系统做好自定义 AppArmor profile 的管理;如果是 CentOS 系的宿主机,则考虑 SELinux 的针对性调优。这不是必须一步到位的配置,但每多一层,攻击者从容器内向外攻击的难度都会提高一个量级。我的建议是,先把 seccomp 默认策略开了,再逐步演进到 AppArmor,因为后者需要更多时间调优规则,避免误伤正常业务。
3.5 运行时安全监控:一定要建立进程和文件的异常基线
即使把上面的安全配置全部做了,我依然不会告诉你可以高枕无忧。安全是动态对抗,运行时监控的价值在于攻击者一旦突破前面所有的防线,你至少还有机会在几分钟内发现并止损。
运行时监控的核心,是给每个容器的正常行为建立基线,然后发现偏离。比如业务应用是一个 Web 服务,它的进程平时只监听 8080 端口,只连接固定的数据库 IP,文件中不会出现 Python 解释器或 /bin/bash 这类交互工具。如果某天这个容器的进程行为里突然出现了对公网 IP 的连接,或者容器内 file 系统出现了可执行文件的写入,那大概率说明容器已经失陷。开源方案里 Falco 在运行时告警这块做得比较成熟。它通过 Linux 内核 eBPF 技术监控系统调用,能够检测“容器内出现了 shell 执行”“容器的 /etc/passwd 被修改”这类高危事件并把告警送到 Slack 或 Webhook。Falco 刚接入时会有不少误报,建议先用一段时间跑日志模式,根据实际调整规则再逐步切到告警模式。
4. 从节点到集群:Kubernetes 控制面安全加固全流程
4.1 集群入口三大件:API Server、etcd 和证书的有效期管理
容器加固得差不多了,接下来到了集群这一层。Kubernetes 集群安全最核心的控制面是 API Server 和 etcd,这是整个集群的大脑和数据源。攻击者只要拿下了 API Server 的某个高权限账号,就能对集群做任何事,包括往里面部署后门 Pod、读取所有 Secrets、篡改部署配置。所以控制面加固的第一条原则就是,控制面组件所在的节点和普通业务节点必须尽量隔离,不要让业务 Pod 能直接调度到 master 节点上。现在用云厂商托管集群的同学在这块可以省不少心,因为控制面由云厂商帮忙维护,但这不代表你就可以不关注 API Server 的访问权限和审计日志了。
etcd 里面保存了集群全部对象数据,包括 Secret 的原始内容。很多自建集群的团队会把 etcd 端口直接暴露在集群内部网络中,然后所有能访问到该网段的 Pod 都能尝试连接它。etcd 又没有内建认证机制,它的安全完全依靠 TLS 客户端证书控制。如果你自建集群时没有给 etcd 开启客户端证书校验,这是最高优先级的安全事故,必须马上补上。另外,证书有效期这个问题往往是最没技术含量但会造成故障的坑。kubeadm 默认生成的证书有效期只有一年,如果你不记录证书到期时间,某天 kubelet 突然无法连接 API Server,排查到后面才发现是 apiserver 证书过期。建议为证书到期监控加一项,过期前 90 天自动告警,别让证书过期成为你凌晨三点起床的理由。
4.2 别再让 ServiceAccount 拿着集群管理员权限到处跑了
控制面安全里最容易权限失控的是 RBAC 的配置。Kubernetes 默认给每个 namespace 创建了一个名为 default 的 ServiceAccount,如果 Pod 没有显式指定 ServiceAccount,就会使用 default。默认的 default SA 权限很少,但如果你的部署 YAML 里为了省事手动绑定过 cluster-admin,那就等于给这个命名空间下所有 Pod 发了集群管理员钥匙。一旦其中一个应用被攻破,攻击者就能通过 Pod 内的服务账号 token 直接管理整个集群,这比容器逃逸还可怕。
在 RBAC 这块,我建议团队严格执行最小权限原则。每一个 Deployment 使用的 ServiceAccount,都应该单独创建,并且只授予它完成本职工作所需的最小权限。比如一个应用只需要读取 ConfigMap 和查看 Pod 状态,那就只给它这个权限。具体配置可以写成:
yaml复制apiVersion: v1
kind: ServiceAccount
metadata:
name: app-sa
namespace: production
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
namespace: production
name: app-reader
rules:
- apiGroups: [""]
resources: ["configmaps"]
verbs: ["get", "list", "watch"]
- apiGroups: [""]
resources: ["pods"]
verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: app-sa-reader-binding
namespace: production
subjects:
- kind: ServiceAccount
name: app-sa
namespace: production
roleRef:
kind: Role
name: app-reader
apiGroup: rbac.authorization.k8s.io/v1
这里特意用 Role 而不是 ClusterRole,就是为了把权限边界限制在单个 namespace。只有当业务确实需要跨 namespace 访问资源时才考虑用 ClusterRole,而且要尽量降低授权范围。
4.3 网络层策略:默认拒绝一切,再放行白名单
集群控制面加强了,应用之间的网络流量安全同样不可忽视。很多集群在部署后使用的是默认的允许全部策略,也就是说任何 Pod 都能访问任何 Pod,哪怕一个是支付服务,一个是内容展示服务。这种“全互联”模式如果被攻破,横向扩散几乎瞬间发生。Kubernetes NetworkPolicy 能够实现 Pod 粒度的防火墙,把默认的“允许所有”收敛成“只放行明确需要的通信”。
不过要提醒的是,NetworkPolicy 真正的执行方是 CNI 插件。如果你的集群还在用 Flannel 这类不实现 NetworkPolicy 的插件,配置了也不会生效。建议使用 Calico 或 Cilium 作为 CNI,它们对 NetworkPolicy 的支持比较成熟,Cilium 还能基于 eBPF 提供更强的可观测性,后面如果要做 Service Mesh 也能在同一套网络栈上扩展。
一个实际落地例子:假设前端服务需要访问后端的订单服务,后端订单服务只应被来自前端服务的流量访问,并且不允许主动访问外网。那订单服务的 NetworkPolicy 建议写成下面这样。
yaml复制apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: order-service-policy
namespace: production
spec:
podSelector:
matchLabels:
app: order-service
policyTypes:
- Ingress
- Egress
ingress:
- from:
- podSelector:
matchLabels:
app: frontend-service
ports:
- protocol: TCP
port: 8080
egress:
- to:
- podSelector:
matchLabels:
app: order-db
ports:
- protocol: TCP
port: 5432
这份 YAML 的表达逻辑是:订单服务只允许被打了 frontend-service 标签的 Pod 访问,并且只允许访问打了 order-db 标签的 Pod 的 5432 端口。其他方向的流量,包括访问外网,都会在底层被丢弃。这段写法再往上的常用实践是给所有 namespace 打上全局默认拒绝的 NetworkPolicy,然后再为每一个需要通信的服务添加白名单规则。开始推进的时候会比较累,因为要梳理各个服务之间的调用关系,我在团队里是通过请开发帮忙画出服务调用拓扑,再把缺失的规则补上的方式完成的。这个成本一次性的,但是换来的网络隔离效果非常值。
4.4 准入控制器与基线扫描:把违规配置阻拦在集群之外
有了网络策略、RBAC、特权限制这些规则之后,还需要思考一个问题:这些规则怎么保证每个人都有遵守?靠人盯着 dev 的 YAML 文件是盯不住的。Kubernetes 提供了一个强大的拦截点,也就是准入控制器,它会在资源创建或更新的时候执行自定义策略。目前最流行的做法是部署 Open Policy Agent(OPA)或者 Kyverno 这类策略引擎。
Kyverno 用 Kubernetes 原生的方式管理策略,写起来像 Kubernetes 资源,团队接受度更高。比如说,如果我想强制所有 Pod 不运行 privileged 容器以及必须设置 readOnlyRootFilesystem,在 Kyverno 里可以定义一条策略,这样任何新部署的 Deployment 如果不符合规范,资源创建请求会在到达集群前就被拒绝,直接在 CI 阶段就显现出来。
除此之外,官方工具 kube-bench 是一个非常推荐的集群基线安全扫描工具,它遵循 CIS Benchmark 规范检查 Kubernetes 的安全配置。每季度跑一次 kube-bench,对比之前的结果,就能看出集群安全水位是在上升还是下降。kube-hunter 则用来模拟攻击者视角主动扫描集群的薄弱点,对于测试新增的访问控制或网络策略是否真的生效非常有帮助。
4.5 场景扩展:多云与混合集群的安全策略一致性
这里额外补充一点,如果团队已经走到多集群甚至多云管理的阶段,安全策略的一致性问题就会变得很突出。A 集群用了 OPA,B 集群用了 Kyverno,C 集群还在裸奔,这种混乱状况本身就是最大的安全漏洞。现在比较成熟的思路是采用 GitOps 方式,把集群安全配置和策略代码统一放到 Git 仓库,通过 ArgoCD 或 Flux 这样的工具自动同步到所有集群。安全策略也像业务代码一样走 review、merge、deploy 的标准流程,这样,每个集群的安全水位都是一致的,任何一次变更都能追溯到责任人。
5. 常见问题排查与避坑技巧实录
5.1 生产环境高危镜像已经在线上了,怎么无缝修复?
这是存量业务迁移到安全体系过程中几乎一定会遇到的问题。生产集群里有几十个跑着高危漏洞镜像的 Deployment,直接全部滚动重启会让业务在午高峰产生大量错误,甚至引发雪崩。
处理方式建议用灰度滚动和分批发布。先评估这些漏洞的真实可利用性,如果漏洞在容器启动后没有被实际触达,可以按 Pod 副本数从少到多分阶段滚动更新。比如先更新一个副本,观察错误日志和业务指标,确认没问题再逐步扩大范围。在发布窗口内完成整个集群的镜像替换。整个过程要配合监控大盘,如果刚才错误率突然升高或者 p99 延迟明显劣化,立刻中止发布并回滚到旧镜像。灰度滚动的过程很考验运维脚本能力,但确实比一次性全部重建安全得多。
5.2 容器以非 root 用户跑了,但挂载的数据卷写入总是权限不足
这个问题我在指导其他团队时被问过无数次。原因在于,容器内指定的 runAsUser 和你给 Volume 设置的属主不一致。比如你挂载了宿主机的一个目录,这个目录默认属主是 root,然后容器内以 UID 10001 身份运行,它当然没有权限写这个目录。
Kubernetes 提供的解决方案是设置 Pod 级别的 fsGroup 字段。只要设置了 fsGroup,kubelet 在挂载 Volume 的时候就会自动把卷的组所有权改成指定 GID,这样容器内进程以组内用户运行时就拥有了写入权限。操作上,把 fsGroup 和 runAsGroup 设置成同一个 GID,比如都是 10001,就能稳稳解决这类问题。如果你用的是持久化存储,比如云盘或 NAS,还需要关注存储服务端是否支持 fsGroup 更改。某些存储驱动对 fsGroup 不支持,这种情况要提前规划,在存储类中设置 allowVolumeExpansion 或者在应用侧改用 emptyDir 中转。
5.3 在 Production 集群启用了 NetworkPolicy 后,DNS 解析突然失败了?
很常见的情况,部署了默认拒绝的 NetworkPolicy 之后,一批 Pod 突然解析不了服务域名了。很多人第一反应是网络策略把业务端口堵了,其实根因往往是忘了放行 kube-system 命名空间里 CoreDNS 的流量。
Kubernetes 默认的集群 DNS 服务 Pod 在 kube-system 下,如果默认拒绝策略不允许 Pod 访问该命名空间的 53 端口,Pod 发往 CoreDNS 的 DNS 查询就会被丢弃。解决方式是在全局默认拒绝策略之上,显式添加一条 Egress 策略,放行所有 Pod 访问 kube-system 中 CoreDNS 的 53 端口。使用 Cilium 的同学还额外注意,开启 DNS 代理或者 FQDN 策略时,也要明确配置 DNS 服务器地址。这里一个经验是,在正式启用默认拒绝之前,先在测试环境跑一段时间,把应用中涉及云厂商 metadata 服务的流量链路也一并梳理出来,因为很多云环境里访问元数据接口的流量也会被 NetworkPolicy 误伤。
5.4 扫描报告没漏洞,但安全合规就是过不去,问题出在哪?
合规检查的视角和单纯漏洞扫描还不太一样。你的镜像可能很干净,但合规检查会追问:这个集群有没有开启审计日志?审计日志保留多少天,有没有被随便删?Secrets 是不是被大量放在环境变量里而不是挂载的 Secret 卷里?镜像仓库有没有做访问权限隔离?CI 流程里有没有人在凭据里面直接写明文密码?
这类问题对应的都是流程和证据链层面。建议提前准备一份自检清单,把审计日志、镜像签名、策略执行记录、基线扫描报告全部沉淀为固定产物。审计日志默认保留至少 180 天,重要集群甚至建议同步到对象存储做永久归档。Secret 方面,Kubernetes 自带的 Secrets 并没有加密存储,只是 base64 编码,风险很高,建议部署一个外部密钥管理系统的 CSI 驱动方案,比如对接 Vault 来管理 Secrets,从根上解决 Secrets 泄漏问题。
5.5 安全工具和策略本身不要成为性能瓶颈
最后分享一个很多团队容易忽略的经验:安全策略和工具消耗的资源不能无限膨胀。我见过有团队在每个节点上同时跑了 Falco、审计日志采集、kube-bench 定时任务、多个 exporter,再加上 Cilium 的网络策略和监控,节点本身就快被安全组件吃满 CPU 了。这显然背离了安全加固的初衷——安全应该让业务更稳定,而不是让业务因为额外的监控组件而频繁抖动。
给安全组件划资源预算是一种必要手段,比如 Falco 建议内存限制在 512MB 到 1GB,CPU 限制在 0.5 到 1 核之间。上线前压测,观察对现有业务 Pod 的影响,这步绝对不能省。你还要审查工具的告警规则,规则太敏感让告警沦为噪声,到时候真正重要的告警被淹没在通知风暴里,系统也就名存实亡了。安全告警贵精不贵多,稳定的几条核心告警远胜过三十条没人看的告警规则。
一个普通工作日的“安全巡检”小模板
最后附上一份我在团队里日常使用的巡检清单。每次上线新业务或做例行安全检查时,按这个顺序过一遍,基本能把集群安全的盲区覆盖到大部分。
| 序号 | 检查项 | 操作方式 | 结果异常时的处理动作 |
|---|---|---|---|
| 1 | 镜像漏洞扫描 | Trivy 扫 CI 和仓库端镜像 | P0/P1 阻断上线,P2 排期修复 |
| 2 | 镜像签名校验 | Cosign 校验准入控制器 | 拒绝未签名镜像运行 |
| 3 | 运行时非 root 检查 | Kyverno 策略或 kubectl 排查 | 接管并修改 securityContext |
| 4 | privileged 容器扫描 | kubectl 查询 privileged 字段 | 立即下线或改为最小权限 |
| 5 | 安全上下文一致性 | 检查 readOnlyRootFilesystem 等配置 | 分批补上并做回归测试 |
| 6 | RBAC 权限检查 | 审计 cluster-admin 绑定和紧张权限 | 删除无用角色绑定,收紧权限 |
| 7 | NetworkPolicy 覆盖 | 检查所有 namespace 的网络策略 | 为缺失服务补白名单策略 |
| 8 | 证书有效期检查 | 检查 kubeadm 证书和 Ingress 证书 | 提前 90 天完成证书轮换 |
| 9 | 审计日志完整性 | 查看日志后端接收状态 | 恢复日志链路,避免静默抛弃 |
| 10 | 运行时异常事件 | 查看 Falco 告警控制台 | 高严重级别告警立即围剿 |
我第一次整理这份清单的时候大概花了两天时间,很多内容都是对照着各个安全组件的文档慢慢补齐的。后来用顺了,每次巡检基本就是重复地跑脚本和看报表,整个流程只要半天就能完成。
最后讲几句实在话
做云原生安全这一路下来,我个人最大的体会是:安全不是某个时间点上的一个任务,它更像一个持续运转的闭环。从镜像扫描到运行时加固、从 RBAC 收敛到网络策略,每一个环节都不是一劳永逸的,因为漏洞库在更新、业务镜像在变化、集群规模在增长。也许今天你已经把所有镜像修复到了零高危,下个月镜像仓库里可能又会出现新的高危组件。
所以,与其追求某个瞬间的“绝对安全”,不如把精力花在建立一套能持续发现、跟踪、闭环处理安全问题的流程上。工具和策略是可以通过文档直接抄走的,难的是团队里每一个开发甚至产品同学都形成安全习惯:写 Dockerfile 时会想到加一个非 root 用户,提交 YAML 时会先想想权限是否给多了,发布前会愿意等扫描那几十秒的结果。这种文化层面的东西,才是所有安全技术防线能否真正长期生效的土壤。
最后再分享一个小技巧:如果你所在的团队暂时没有专职的安全工程师,那就把安全巡检这件事拆成几个 30 分钟以内的例行操作,比如每周一早晨花半小时看一眼扫描报告和告警控制台,每周四下午花半小时处理本周新增的安全债。这样不会给运维同学造成过重负担,也能让安全问题保持在可控范围内。云原生安全的每一步可能都不那么性感,但当你有一天真的收到安全事件告警,而这些防线帮你挡住了攻击的时候,你会觉得之前做的事情全都值了。
