开篇:一次让我睡不着觉的集群巡检
先交代一下背景,我在一家中大型互联网公司负责云原生基础设施的稳定性与安全合规。团队从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 阶段漏掉的历史存量镜像。仓库端的扫描尤其适合处理那些还没来得及整改的存量镜像,你至少能通过它知道线上到底有哪些风险敞口。
扫描工具方面,目前开源社区最推荐的是 Tri
