上周安全部门丢过来一张表格,密密麻麻列着:集群版本多少、API Server有没有开匿名访问、etcd是否启用了TLS、kubelet的只读端口关没关……要求一周内交一份基线合规报告。以前遇到这种事,基本都是对着官方文档一项一项人工核对,一个中型集群几十个配置参数,眼睛看花不说,还容易漏项。后来我把Kube-Bench捡起来,配合CIS Benchmark对照检查,才慢慢把这件事从“人工核对”变成了“定时自动化”。这篇文章就把我怎么落地Kube-Bench做K8s安全基线扫描的完整过程写出来,包括它到底怎么工作、集群里怎么跑、扫描报告怎么解读,以及怎么把扫描接入自动化流程。如果你是运维、安全工程师,或者正在被各种安全合规问题追着跑,这篇应该能帮你省不少时间。
我默认你已经有一个可以操作的Kubernetes集群,不需要提前装任何东西,Kube-Bench可以临时下载,也可以直接用容器运行。下面从原理讲到实操,最后把我踩过的坑一并列出来。
1. 先搞清楚一件事:K8s安全七分靠基线条,三分靠工具
Kubernetes的默认配置设计初衷是“能用、好用”,优先级排在“默认安全”之前。早期很多集群甚至直接允许匿名访问API Server,kubelet的只读端口也开着,etcd没有配TLS,这类问题在内部测试环境可能无伤大雅,一旦集群暴露到生产环境,就是实打实的突破口。
安全基线的作用,就是给“集群应该处于什么样的安全状态”定一个最低标准。你不会想在生产环境里突然被提醒“etcd没有开认证”,更不会想靠记忆去确认每一个组件参数是否合规。基线把这一切固化下来,让安全状态变得可量化、可审计、可追踪。
1.1 为什么需要一条可量化的安全基线
K8s的安全配置散落在很多地方:控制面组件的启动参数、etcd的证书配置、kubelet的服务配置、RBAC的权限绑定、Pod的安全上下文。这些配置分布在不同的配置文件里,人工检查不但效率低,而且不同人检查的标准还不一样。今天A管理员觉得“这个参数不用管”,明天B管理员可能觉得“这个必须改”,最后集群配置全凭个人经验。
有了基线标准之后,所有检查项都是白纸黑字,每一条都有编号、有描述、有修复建议。不管谁来检查,标准是一致的,结果是可以横向对比的。这也是为什么等保、客户尽调、内部安全审计都会要求提供基线合规报告——他们不关心你集群跑得多快,只关心你是否达到了一个公认的安全基线水平。
另一个价值在于,基线把很多“听起来很专业”的安全议题拆成了具体、可操作的检查点。比如“防止未授权访问”听起来很虚,但落到基线里就是“API Server是否设置了--anonymous-auth=false”“是否启用了RBAC授权模式”这样明确的技术动作。这种从抽象到具体的拆解,对团队协作特别重要。
1.2 CIS Benchmark到底检查哪些东西
CIS Kubernetes Benchmark把K8s的安全配置划分成十几个大块,核心检查范围包括控制面组件、etcd、工作节点组件和集群策略。每个大块下面又有细化的检查项,每一条都对应一个具体的配置要求。
控制面组件里,API Server的检查项最多,因为它几乎是整个集群的入口,涉及匿名访问、授权模式、TLS配置、审计日志、ServiceAccount令牌等。etcd的检查项集中在是否启用了TLS认证、是否限制了访问来源。工作节点方面,kubelet的只读端口、认证授权方式、Pod运行权限都是重点。集群策略部分主要看Pod Security Admission、网络策略、RBAC默认策略等是否生效。
这里有一个很现实的问题——K8s版本更新很快,不同版本的默认配置和参数名都会有变化,所以CIS Benchmark也会跟着迭代。Kube-Bench自身会区分版本,运行时会根据你指定的K8s版本去套对应的检查规则。版本匹配对不上,扫描报告就没有参考意义,这一块后面我会详细说。
2. Kube-Bench的工作方式:它凭什么判断你的集群“违规”
Kube-Bench本质上是一个配置合规检查器,不是一个攻防渗透工具。它不会去尝试利用漏洞,也不会产生攻击流量,只做一件事:读取集群组件的配置文件、检查运行参数和文件权限,然后和CIS Benchmark里的规则一条一条做比对,输出PASS、FAIL、WARN、INFO。
理解这个定位很重要。很多第一次接触Kube-Bench的人会误以为它像漏洞扫描器一样能“扫出漏洞”,跑完发现报告里全是配置项,反而一脸疑惑。实际上它回答的是“你的集群配置是否符合标准”,而不是“你的集群能不能被攻破”。
2.1 检查项从哪来:版本对应关系与配置文件
Kube-Bench的检查规则以yaml文件形式存放在工具目录下,里面按照章节和组织结构维护了所有CIS控制项及其检查方法。不同K8s版本对应不同的规则文件,规则和版本错配是最常见的误报来源。
从K8s 1.20开始,CIS Benchmark的版本和K8s版本基本形成了一一对应的关系。比如K8s 1.20到1.22对应CIS Kubernetes Benchmark v1.6,1.23到1.25对应v1.7,1.26以后对应v1.8。如果你的集群是1.31版本,却用针对1.20时代的默认规则去扫描,大概率会扫出很多“不存在”的问题,或者漏掉新版里的新检查项。
Kube-Bench检查的配置来源也很固定,主要读这几类文件:控制面组件,比如kube-apiserver、kube-controller-manager、kube-scheduler,它们的启动参数位于/etc/kubernetes/manifests目录下的静态Pod yaml文件;etcd的配置同样在静态Pod里;kubelet的配置文件一般在/var/lib/kubelet/config.yaml,启动参数则可能分布在/etc/kubernetes/kubelet.env或系统服务文件里。
Kube-Bench还检查文件权限和目录归属,比如证书文件是否对组可读、容器运行时目录是否被普通用户写权限等。这些检查不需要连接的集群,完全依赖本地文件系统,所以它必须运行在集群节点上,而不能跑在集群外。
2.2 一个失败项的产生过程:检查逻辑实例拆解
以API Server的匿名访问检查为例,对应CIS规则编号一般是1.2.1或类似条目,要求是“确保匿名请求被禁止”。Kube-Bench的执行逻辑分三步:
第一步,定位api-server的静态Pod配置文件,默认路径是/etc/kubernetes/manifests/kube-apiserver.yaml。第二步,在配置文件中找到command或args段,提取所有启动参数。第三步,检查是否存在--anonymous-auth=false。如果参数存在就返回PASS,不存在或者被显式配成true就返回FAIL。
看起来很简单,但实际场景里有一个很常见的坑:有些集群通过systemd管理组件,或者参数配置在单独的env文件里,Kube-Bench默认路径读不到这些自定义位置。这种情况下它会诚实地报告WARN,表示“我知道有这个检查项,但我没法确认”,而不是直接判FAIL。
再比如etcd的TLS配置检查,Kube-Bench会同时检查--cert-file和--key-file两个参数是否存在,并且还会检查这两个参数是否指向有效的证书文件。有些集群只配置了cert-file而漏了key-file,Kube-Bench一样判FAIL。这种小细节,人工检查很容易漏。
理解了检查原理,再看后面的操作就会清晰很多。Kube-Bench不是一个黑盒工具,它的每条规则都是可追踪的,你完全可以知道它为什么给某个检查项打了FAIL。
3. 实操:把Kube-Bench在集群里跑起来
Kube-Bench支持三种常见运行方式:直接在节点上跑二进制、用容器临时跑、或者作为Job在集群里跑。不同方式适用不同场景,我建议你三个都会,按实际情况切换。
需要先提醒一点:无论哪种方式,Kube-Bench都必须能读取节点的宿主目录,包括/etc、/var、/opt和/usr/bin。因为它要检查的是节点上的组件配置、二进制文件权限和证书文件,不是集群内部运行的东西。
3.1 本地二进制方式:最快能出结果
如果你想在单个master节点上快速验证一下结果,本地二进制方式最直接。
# 从GitHub Releases下载对应版本 curl -L https://github.com/.../releases/download/v0.9.5/kube-bench_0.9.5_linux_amd64.tar.gz -o kube-bench.tar.gz tar -xzf kube-bench.tar.gz # 赋予执行权限并查看帮助 chmod +x kube-bench sudo ./kube-bench --help # 指定K8s版本运行(比如集群是1.28) sudo ./kube-bench run --version 1.28命令执行过程中,Kube-Bench会逐个读取组件配置,并实时输出当前检查项的结果。全部跑完后,会汇总出一个结果统计表,按检查项分组显示PASS、FAIL、WARN、INFO的数量。
二进制方式的优点是简单直接,缺点是它会下载二进制到节点里,而且需要root权限,因为这涉及读取配置文件。对于一次性的排查工作,这种方式够用了。注意命令里的--version参数,这里的版本指K8s版本,不是Kube-Bench自身版本。
3.2 容器与Job方式:不污染宿主机才是正解
如果不想在节点上留任何东西,容器方式更合适。Kube-Bench官方提供了容器镜像,运行方式如下:
docker run --rm \ -v /etc:/etc:ro \ -v /var:/var:ro \ -v /opt:/opt:ro \ -v /usr/bin:/usr/bin:ro \ -v /usr/local/bin:/usr/local/bin:ro \ -v /etc/kubernetes:/etc/kubernetes:ro \ <kube-bench镜像> run这里把所有需要检查的宿主目录以只读方式挂载进容器,容器内部执行检查后直接退出。这种方式的优势非常明显:不需要在节点上安装任何二进制,不会留下临时文件,挂载全部只读,对宿主机几乎零侵入。
如果用集群内Job方式运行,思路是一样的,只是把容器挂到了Kubernetes的调度体系里。核心优势是可以通过定制的镜像和调度策略,实现批量扫描多个节点。一个经验做法是,用NodeSelector把扫描Pod调度到目标节点上,比如给master节点加一个专用标签,然后通过CronJob定时在master节点上运行扫描。
这里有个细节:控制面组件和etcd只存在于master节点(或控制面节点),所以针对控制面的扫描Pod必须调度到这些节点上。而worker节点的检查需要跑在所有工作节点上,通常会用DaemonSet方式在每个节点起一个扫描Pod。如果集群规模大,这会生成大量扫描结果,需要注意结果的汇总和去重。
4. 一屏看懂扫描报告:哪些是真风险,哪些是误判
Kube-Bench的默认输出是人类可读的文本格式,每个检查项显示编号、描述和结果状态。但这个输出结果用于自动化处理并不友好,所以官方还支持JSON、JUnit XML等结构化输出格式。我建议自动化管道统一用JSON,手工查看时用文本格式。
一个真实集群的扫描报告通常会有几十个FAIL项。看到这么多红色标记别慌,不是所有FAIL都意味着集群马上要被攻破了,需要按风险级别和影响面来分类处理。
4.1 输出格式与结果解读
Kube-Bench的结果状态分四种:PASS表示配置满足基线要求;FAIL表示配置不满足,存在风险;WARN表示工具无法准确判断,需要人工核对;INFO表示仅仅提供信息,不判定合规性。
处理WARN项要特别谨慎。它不代表安全,只代表“Kube-Bench没法确认”。常见的导致WARN的情况是组件启动参数不在默认路径里,或者检查项依赖的条件在当前环境不适用。这种状态不处理,审计的人可能会追问。
用JSON输出方便后续解析:
sudo ./kube-bench run --json > /tmp/kube-bench-result.jsonJSON里每一项包含rule_id、desc、remediation、status和实际检测到的配置值。拿到这份JSON,后续无论是做数据统计、告警判断还是报告生成,都很方便。
4.2 失败项的修复优先级判断
以我实际扫描过的一批集群为例,下面这几个是出现频率最高的FAIL项,修复优先级也最高:
| 检查项编号 | 风险描述 | 常见修复方式 | 说明 |
|---|---|---|---|
| 1.2.6 / 匿名访问 | API Server允许匿名请求 | 在kube-apiserver启动参数中加--anonymous-auth=false | 风险极高,被扫到基本等于裸奔 |
| 1.2.1 / 授权模式 | API Server使用AlwaysAllow | 改为--authorization-mode=Node,RBAC | 需要确认现有RBAC策略是否完备 |
| 2.2 / etcd权限 | etcd目录存在不安全文件权限 | chmod 700 或 chown etcd:etcd | 防止非授权用户读取证书和密钥 |
| 4.2.1 / kubelet匿名访问 | kubelet允许匿名请求 | kubelet配置中设置authentication.anonymous.enabled: false | 高危,容易被横向移动 |
| 4.2.2 / kubelet授权模式 | kubelet使用AlwaysAllow | 改为Webhook模式 | 让kubelet与API Server的RBAC联动 |
| 1.2.22 / 审计日志 | API Server未启用审计日志 | 配置--audit-log-path与--audit-log-maxsize | 需要规划日志存储容量,防止磁盘打满 |
从经验看,修复顺序应该遵循“先网络边界、再身份认证、后审计追溯”的逻辑。匿名访问、弱授权模式这类问题直接决定集群对未授权请求的暴露面,必须最先处理。审计日志这类属于追踪能力,虽然不直接影响被不被攻击,但没有日志意味着出问题后没有任何线索,同样不能拖太久。
这里还需要注意:每一项修复都不能脱离业务现状。直接把--authorization-mode改成RBAC之前,要确保集群里已有的ServiceAccount和RBAC绑定关系是完整的,否则改完后现有工作负载可能全部失去权限,造成大面积故障。这种“修了一个安全项,炸了整个集群”的事故,我在实际运维里见过不止一次。
5. 别让扫描变成一次性行为:自动化闭环设计
手工跑一次扫描容易,难的是让它持续运转。K8s集群是动态系统,配置可能在一次升级、一次节点重建后回退,也可能因为某人的一次临时调试而引入新的安全风险。所以,自动化的目标不是“扫一次”,而是“每次配置变更后都能自动扫,且结果能及时送达负责人”。
我落地过的自动化方案有三层:定时扫描、结果采集、触发告警。
5.1 定时任务方案:CronJob + 结果落库
定时扫描最常用的载体就是Kubernetes CronJob。下面是个可以直接参考的示例,它会在每天凌晨2点扫描控制面节点,并把JSON结果写入持久化存储。
apiVersion: batch/v1 kind: CronJob metadata: name: kube-bench-master namespace: security spec: schedule: "0 2 * * *" jobTemplate: spec: template: spec: serviceAccountName: kube-bench-sa hostPID: true restartPolicy: Never nodeSelector: node-role.kubernetes.io/master: "" tolerations: - key: node-role.kubernetes.io/master operator: Exists effect: NoSchedule containers: - name: kube-bench image: kube-bench:latest args: - run - --json - --version - "1.28" volumeMounts: - name: etc mountPath: /etc - name: var mountPath: /var - name: opt mountPath: /opt - name: usr-bin mountPath: /usr/bin - name: usr-local-bin mountPath: /usr/local/bin - name: etc-kubernetes mountPath: /etc/kubernetes volumes: - name: etc hostPath: path: /etc - name: var hostPath: path: /var - name: opt hostPath: path: /opt - name: usr-bin hostPath: path: /usr/bin - name: usr-local-bin hostPath: path: /usr/local/bin - name: etc-kubernetes hostPath: path: /etc/kubernetes这里有几个容易踩的坑。第一,扫描容器的hostPID必须开,否则Kube-Bench无法读取节点的进程信息,部分检查项会变成WARN。第二,CronJob的Pod默认会被调度到任意节点,必须用nodeSelector和toleration把扫描Pod固定到目标节点上,否则结果没有意义。第三,很多集群的master节点默认有NoSchedule污点,不加toleration的话,调度根本不会成功。
扫描结果如果直接打印在Pod日志里,每次查看都要去翻历史日志。更好的做法是把JSON结果通过脚本发送到对象存储或企业内部的数据接收服务,这样可以跨时间维度统计风险趋势。我通常的做法是让扫描Pod多挂一个脚本容器,把JSON解析后发送到消息队列,后面接一个数据管道做入库和可视化。
5.2 接入CI流水线:让配置变更先过关再上线
定时扫描只能发现“已经存在的风险”,真正的安全自动化还应该在“变更发生前”拦截风险。具体来说,就是在集群升级、组件配置变更、应用大版本发布之前,用Kube-Bench做一次增量检查,把基线扫描塞进CI流程。
很多团队用Jenkins或Argo CD做集群配置的持续部署。可以在流水线中加入一个Kube-Bench扫描阶段:流水线生成的集群偷数,先在临时环境或目标节点上跑一次基线扫描,如果高危FAIL项数量超过阈值,流水线直接中断,变更不会进生产。
这个方案的实现难点在于,Kube-Bench是静态检查工具,它不能自动理解“当前变更可能影响哪些检查项”。我的做法是将扫描结果做diff——上一次扫描的基线结果和本次扫描结果对比,新增的FAIL项就是本次变更引入的风险,需要对它做特别审查。
接入CI的另一个价值是让扫描结果“有人负责”。定时扫描的结果如果没人看,等于白扫。而CI流水线中扫描不过就发布失败,会迫使配置变更的所有者去处理安全项,责任自然落到对应团队头上。
5.3 结果可视化与告警联动
自动化扫描除了结果落库,还需要具备实时告警能力。我配置过一个简单的告警链路:扫描完成之后,脚本解析JSON结果,统计高危FAIL项的数量,通过与Prometheus格式兼容的方式暴露成指标,再配置告警规则。
比如,设置一个“API Server匿名访问处于开启状态”的告警,一旦扫描发现这个配置项不是PASS状态,就立刻通过企业IM或邮件通知安全团队。这类告警的优先级应该比普通的资源类告警高,因为配置回退往往发生在无意的变更之后,越早发现,越容易定位到是哪次操作改坏了配置。
告警阈值也不要只设置成“有FAIL就告警”,否则每天都能收到几十条告警,团队很快就麻木了。更合理的做法是对FAIL项分级:高危项(匿名访问、AlwaysAll授权、etcd无TLS)一旦出现就告警;中低危项累积到一定数量才告警,或者合并成日报周报发送。
6. 实际运行一段时间后,我总结的注意点
自动化扫描跑起来之后,真正的麻烦才刚刚开始。工具部署本身不复杂,复杂的是让它持续输出有价值的报告,而不是变成一堆没人看的日志。下面这几个问题是我在落地过程中遇到过的,写出来免得你重复踩坑。
6.1 版本匹配是最大的坑
Kube-Bench和K8s版本必须严格匹配这一点,怎么强调都不过分。我遇到过最典型的情况是:集群已经从1.24升级到1.30,但CronJob里的--version参数还停留在1.24,扫描结果里出现大量莫名其妙的FAIL项。
规则文件不匹配会带来的问题有两种。一种是旧规则检查的参数在新版本里已经改名或废弃,导致误报;另一种是新版本新增的检查项没有被规则包覆盖,导致漏报。误报会让团队对报告失去信任,漏报更严重,会让安全团队误以为集群很安全。
建议把扫描版本和集群版本的对应关系维护在配置管理平台里,集群升级流程里强制包含“更新Kube-Bench版本参数”这一条,防止升级后扫描规则还挂着旧版本。另外,Kube-Bench的规则文件在它的GitHub仓库里持续更新,但镜像发布可能会有延迟,是不是要用最新镜像建议先验证一下是否包含对应的规则文件。
6.2 托管集群、自建集群的扫描边界
如果你用的是托管集群,比如云厂商提供的控制面服务,那你对控制面节点基本没有访问权限。这种情况下,Kube-Bench扫描控制面组件和etcd的检查项是跑不了的,只能在worker节点范围内执行扫描。
我建议把扫描计划分成两部分:托管侧能扫的worker节点配置、以及控制面配置参数的手动记录。对于无法自动扫描的部分,可以向云厂商索取他们的安全基线报告,一般托管服务商都会提供现成的合规文件。千万别拿着Kube-Bench结果直接甩给老板说“集群不安全”,得先讲清楚扫描边界在哪里。
自建集群虽然能完全掌控,但也意味着所有修复都要自己处理。控制面组件的yaml文件修改、etcd证书轮换、kubelet配置调整,每一步都可能影响集群稳定性。如果你在一个生产集群上第一次做扫描和修复,建议先在测试环境完整走一遍流程,避免在业务高峰期动控制面参数。
6.3 避免修了一个项,又带崩了集群
修复扫描发现的FAIL项,比运行扫描本身需要注意更多事情。我见过最严重的一次事故是:某集群的API Server授权模式一直是AlwaysAllow,扫描判FAIL后运维直接改成RBAC,结果部署在集群里的旧控制器全部同时失去权限,瞬间大量Pod重建,整个服务雪崩。
所以,修复高危项之前一定要评估影响面。改授权模式、关匿名访问这类操作,最好先在一个非核心业务集群上验证,同时准备好回滚方案。Kube-apiserver的配置如果在静态Pod里,改完文件后kubelet会自动重启API Server,这个重启过程本身就是一次小的集群中断,业务量大的时候要谨慎。
另外两个细节也容易出错:一是etcd目录权限的调整,要确认etcd进程的用户和组,chown错了会把整个集群的读写权限破坏掉;二是审计日志的开启,它会在短时间内产生大量日志数据,如果没有配置日志轮转策略,很可能直接把宿主机磁盘写满,反而引发更严重的问题。
6.4 扫描结果的价值在于持续跟进,不在于一次报告
最后说一点比较“软”的经验。Kube-Bench单独跑一次的结果,价值非常有限,它只是某个时间点的快照。安全基线的真正价值在于趋势变化——这周比上周多出了哪几个FAIL项,哪个节点反复出现同样的问题,哪些检查项在变更之后被无意关掉了。
我把这些变化跟踪下来之后,发现很多安全问题其实是重复发生的过程性问题,而不只是技术问题。比如kubelet的只读端口被打开,很有可能是某次排障时顺手开的;API Server的匿名访问被打开,可能是某个调试脚本干的。这些问题如果不持续跟踪,今天修完,下周又会冒出来。
所以我在实际落地时,会把自动扫描结果和周会同步结合起来:每周固定时间汇总一次风险趋势,让相关团队看一眼新增的高危项,比一次性堆出几十条告警有效得多。安全扫描不是目的,把修复动作持续落到日常运维节奏里,才是这件事真正的价值所在。