先说个背景。前段时间在某团队帮忙做 Kubernetes 集群的安全巡检,发现很多人对“安全基线”的认知还停留在“装个工具跑一下看看输出”的阶段。等真正把 Kube-Bench 跑起来,面对一堆 FAIL 的时候,就不知道该从哪里下手了。这篇文章不打算讲太深的理论,就围绕 Kube-Bench 这个工具,从为什么做、怎么做、结果怎么解读,到怎么持续落地,完整梳理一遍我的实操经验。
1. 整体设计思路:为什么安全基线扫描不是“跑一下”那么简单
1.1 安全基线的本质:给集群定一个“体检指标”
Kube-Bench 的核心是 CIS Kubernetes Benchmark,这是一套由中立安全机构发布的 Kubernetes 加固建议,包含上百个检查项。你可以把它类比成一份“体检套餐”——每一项都对应一类常见的配置问题,比如 kube-apiserver 是否启用了匿名认证、etcd 是否设置了文件权限、kubelet 是否配置了证书轮换。每一条检查项都对应一组“应该怎么做”的配置值,工具检查实际环境里的配置,比对期望值,给出 PASS / FAIL / WARN / INFO 四类结果。
这套机制的价值在于,它把安全合规变成了一个可量化的过程。你不需要靠经验拍脑袋说“这台集群应该安全”,而是能明确知道:当前集群在控制平面、etcd、节点组件、RBAC、Pod 安全标准这几个维度上,到底有多少项没有达到推荐基线。对于交付验收、等保自查、内部安全审计来说,这个量化结果是很好的沟通介质。
1.2 选型考量:为什么是 Kube-Bench 而不是商业扫描器
市面上能扫描 K8s 安全配置的工具不少,商业产品也有。我个人的选择逻辑是这样的:
- 开源免费,社区活跃,更新及时,CIS Benchmark 每个新版本发布后,Kube-Bench 通常会在较短时间内跟进。
- 支持自定义配置,企业可以根据自身环境裁剪检查项、调整期望值,不用为“误报”无限妥协。
- 无 Agent 常驻,扫描时按需运行,跑完即停,对业务 Pod 的影响面很小,资源开销可控。
- 多种运行方式,既能单机二进制直接跑,也能以 Job 形式在集群内运行,结果还能落到 JSON 里,很容易接入已有的日志或告警体系。
商业扫描器当然有它的优势,比如资产视角、可视化大盘、跨账号统一管理等。但如果你手里的环境是多套独立集群、或者处于安全建设的早期,Kube-Bench 已经是“够用且好用”的起点。
我用一张表把几种常见跑法的适用场景列出来,方便你选:
| 运行方式 | 适合场景 | 特点 |
|---|---|---|
| 单机二进制 | 快速验证、开发/测试集群 | 需要本机能访问 kube-apiserver 和节点配置 |
| 容器运行(docker run) | 无法直接装二进制、又想快速看结果 | 依赖镜像仓库可用 |
| Kubernetes Job | 生产集群、多集群批量扫描 | 需要 RBAC 配置,结果集中采集方便 |
| 集成到 CI | 集群变更前做预检 | 把扫描结果作为质量门禁的一部分 |
1.3 扫描的边界:它能做什么,不能做什么
这点必须说清楚,不然容易产生过分信任。Kube-Bench 只检查“静态配置”,也就是说它看的是参数有没有设置、权限对不对、文件属主是否合理,它不负责动态探测运行时漏洞,也不检查容器内是否有恶意文件。镜像漏洞扫描、运行时行为检测、网络策略有效性验证,这些需要其他工具配合。所以正确的定位是:Kube-Bench 是安全建设里的“地基扫描”,不是全部。
2. 核心机制拆解:控制项分类、运行原理与关键配置
2.1 控制项到底长什么样
CIS Kubernetes Benchmark 的控制项结构,大致是按照组件来分组的。Kube-Bench 的输出也是按这套结构组织的,常见的组别包括:
- Control Plane:kube-apiserver、kube-controller-manager、kube-scheduler 的参数检查
- Etcd:etcd 服务的 TLS、文件权限、属主检查
- Node:kubelet、kube-proxy、文件权限、网络配置
- RBAC:集群角色、绑定、匿名访问相关的检查
- Pod Security Standards:Pod 安全上下文、容器特权模式等
- Policies:网络策略、服务账户密钥挂载等
每一组里都有具体编号,比如 1.2.7 是检查 kube-apiserver 是否设置了--authorization-mode=RBAC,4.2.1 是检查 kubelet 是否启用了匿名认证。这些编号就是你在修复和汇报时要用到的“工单号”,内部沟通时直接说“4.2.1 没过”,对方一听就知道是 kubelet 匿名认证的问题。
2.2 四类结果的判定逻辑
PASS、FAIL、WARN、INFO 这四个状态,含义差别很大,解读错误会直接导致误判。
- PASS:检查项配置符合推荐值。
- FAIL:配置缺失或不符合推荐值,这是需要优先处理的。
- WARN:Kube-Bench 无法确认该项是否达标。常见原因是集群以托管模式运行,部分组件不在你控制范围内,或者工具缺少某些运行时信息。WARN 不等于 FAIL,但也不应该直接忽略,最好逐条确认原因。
- INFO:不需要强制检查,或者默认项,通常用于补充信息,不算安全问题。
有一次我在某套托管集群上跑扫描,发现大量 WARN,细看才知道是控制平面相关检查根本拿不到 Pod 信息,因为托管集群不暴露这些组件。这类 WARN 就不需要处理,只需在报告里备注“该平台由服务商托管,相关项不适用”。
2.3 配置文件的加载逻辑
Kube-Bench 的行为由一个主配置文件(默认是 cfg/config.yaml)驱动。这个文件定义了要执行哪些组、每组包含哪些控制项、每个控制项如何检测。集团里不同环境如果差异很大,不建议直接改默认配置文件,而是复制一份到自定义目录,用--config-dir指定。我习惯在仓库里维护一个security-baseline/目录,里面放着每个环境的定制配置和说明文档,改起来有迹可循。
这里有一个关键点:Kube-Bench 对每个检查项的期望值定义在配置文件里,而实际值来自对 kube-apiserver 进程参数、kubelet 配置、文件系统权限等真实环境的探测。所以它并不是简单的“文本比对”,而是组合了进程参数抓取、文件属性读取、YAML 解析、命令执行等多种手段。
3. 部署与执行实操:从二进制到集群内 Job
3.1 本地快速跑:二进制方式
如果你只是想快速看结果,最简单的办法是拿到对应平台的二进制文件,直接运行。需要提前确认:执行用户有读取相关配置文件的权限,机器上能访问 kube-apiserver 地址。
# 下载并解压,以 v0.10.0 为例,实际版本以官方发行为准 wget https://github.com/aquasecurity/kube-bench/releases/download/v0.10.0/kube-bench_0.10.0_linux_amd64.tar.gz tar -xzf kube-bench_0.10.0_linux_amd64.tar.gz # 直接运行全部检查 ./kube-bench run # 只跑某个组,比如 etcd ./kube-bench run --targets etcd # 指定输出格式,方便后续处理 ./kube-bench run --json --outputfile result.json初次跑的时候,我建议先输出成 JSON,再用jq过滤一下,因为控制台默认输出是彩色的表格形式,一旦控制项超过一百项,看起来信息密度太高,不利于逐条确认。
3.2 集群内运行:以 Job 方式执行
生产环境里我更推荐用 Job 方式。因为二进制方式要求你手动在每台节点上执行,而 Job 方式只要创建一次,就会自动调度到某个节点,利用 RBAC 权限读取集群状态和节点配置,然后把日志输出集中到一个 Pod 里,采集非常方便。
先创建一个 ServiceAccount:
apiVersion: v1 kind: ServiceAccount metadata: name: kube-bench namespace: security然后创建 ClusterRole,至少需要读取节点、Pod、组件配置相关权限:
apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: kube-bench rules: - apiGroups: [""] resources: ["nodes", "pods"] verbs: ["get", "list"] - apiGroups: [""] resources: ["pods/eviction"] verbs: ["create"]再创建 ClusterRoleBinding 把两者绑定:
apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: name: kube-bench roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: kube-bench subjects: - kind: ServiceAccount name: kube-bench namespace: security最后创建 Job。这里要注意,Job 调度到的节点,它的容器运行时需要能访问宿主机的/etc/kubernetes等目录,所以通常要挂载宿主路径。下面是一个最小可运行的定义:
apiVersion: batch/v1 kind: Job metadata: name: kube-bench namespace: security spec: template: spec: serviceAccountName: kube-bench hostPID: true containers: - name: kube-bench image: docker.io/aquasec/kube-bench:latest command: ["kube-bench", "run", "--json", "--outputfile", "/var/output/result.json"] volumeMounts: - name: var-lib-etcd mountPath: /var/lib/etcd readOnly: true - name: etc-kubernetes mountPath: /etc/kubernetes readOnly: true - name: output mountPath: /var/output securityContext: readOnlyRootFilesystem: true restartPolicy: Never volumes: - name: var-lib-etcd hostPath: path: /var/lib/etcd - name: etc-kubernetes hostPath: path: /etc/kubernetes - name: output hostPath: path: /tmp/kube-bench-output创建之后:
kubectl apply -f rbac.yaml -f job.yaml # 等 Pod 跑完 kubectl -n security wait --for=condition=complete job/kube-bench # 查看日志,或者去宿主机的 /tmp/kube-bench-output 下拿 result.json kubectl -n security logs job/kube-bench这里有几个容易踩的坑,单独说:
- 如果你用的容器镜像版本比较新,它可能默认就包含一组针对云平台托管集群的 profile,例如 EKS、AKS、GKE 等,你需要显式指定 profile,否则某些检查项会判断不准。参数形式一般是
--benchmark指向具体的版本或平台。 - 挂载
/var/lib/etcd在某些系统上因为目录不存在导致 Job 挂载失败,建议提前确认节点上是否真的有这个目录,没有的话可以先创建空目录再挂载。 - Job 默认只会跑在一个节点上。如果集群是多节点,你只能得到那个被调度节点的结果。想全节点覆盖,要改成 DaemonSet 或单独为每个节点创建 Job,并配合
nodeSelector。
3.3 离线环境下的部署思路
有些环境不能直接从公网拉镜像,这是我在实际中遇到的比较头疼的问题之一。处理思路也不复杂:在一台能联网的机器上把 Kube-Bench 镜像保存为 tar 包,再导入到离线环境。
# 联网机器 docker pull docker.io/aquasec/kube-bench:latest docker save docker.io/aquasec/kube-bench:latest -o kube-bench.tar # 离线环境,导入到本地镜像仓库或节点 ctr -n k8s.io images import kube-bench.tar导入之后改一下 Job 里的镜像地址即可。另一个思路是直接把二进制文件和配置目录分发到节点上,用节点本地执行的方式替代容器方式,效果完全一样。
4. 结果解读与修复落地:从一堆 FAIL 到可复现的整改清单
4.1 输出格式与字段说明
Kube-Bench 的 JSON 输出大致结构是按组(group)划分,每个组里包含 control 列表,每个 control 里有 id、description、status、remediation、scored 等信息。我在处理结果时一般只保留 status 等于 FAIL 和 WARN 的项,因为全部输出太长了。
# 只过滤 FAIL 项,并输出 id、描述、修复建议 jq -r '.results[] | .checks[] | select(.status=="FAIL") | "\(.id) \(.desc) \(.remediation)"' result.json企业做整改跟踪,我会建议把这份结果作为附件,每条 FAIL 对应一个整改任务,关联到负责人。这里有一个很关键的认知:Kube-Bench 的 remediation 字段写的修复建议通常是一条命令或一个参数,但实际改完以后很可能需要重启 kube-apiserver 或 kubelet,影响面比较大。所以修复前一定要评估变更窗口,不要在一个跑着业务的大集群里贸然操作。
4.2 常见失败项与修复套路
根据不同团队的扫描结果,我整理了出现频率最高的几类失败项和对应的处理思路:
控制平面组件参数缺失。比如 kube-apiserver 缺少--authorization-mode=RBAC,或者 kube-controller-manager 缺少--terminated-pod-gc-threshold之类的参数。这类修复通常需要编辑静态 Pod 清单或 systemd 单元文件,然后重启对应组件。改静态 Pod 清单时,kubelet 会自动重启这个容器,但大集群里容易引起瞬时抖动,建议下游服务做好重试。
etcd 文件权限过宽。检查项要求 etcd 的数据目录和证书文件的权限不能超过 600 或 700。修复方法很简单:
chown -R etcd:etcd /var/lib/etcd chmod 700 /var/lib/etcd这类问题看似简单,但确实很常见,因为很多环境是用包管理器安装 etcd,安装时目录权限没有刻意收敛,而 etcd 又是整个集群的“信任根”,证书和私钥一旦泄漏,后果很严重。
kubelet 配置未启用认证鉴权。检查项要求--anonymous-auth=false或者对应 kubelet-config 里的authentication.anonymous.enabled为 false,同时要求authorization.mode为 Webhook。修复时修改 kubelet 配置后,需要重启 kubelet:
systemctl restart kubeletRBAC 绑定过多。部分检查项检查是否存在过大的 cluster-admin 绑定、是否存在用户名/密码基础认证等。修复方向是把最小权限原则落到实处。这类问题往往牵涉到业务方的使用习惯,改起来可能比改配置更麻烦,因为你需要帮业务方梳理权限需求,给出新的 Role 和 RoleBinding 方案。
4.3 哪些 FAIL 可以接受例外
不是所有 FAIL 都必须修。我见过有些团队追求“零 FAIL”,其实不一定合理。比如开发环境为了调试方便,开放了匿名访问;再比如某些内部信任网络里,kubelet 的 TLS 配置改起来成本很高,业务风险反而更大。更合理的做法是建立例外审批清单,每条例外注明原因、缓解措施、责任人、复审日期。Kube-Bench 本身不管理例外,但你可以把例外清单放在扫描配置的目录里,和扫描结果一起归档。
实操中我会在扫描结果后追加一个“例外分析”段落,逐条标注:这条为什么可以接受例外、替代控制是什么、多久复审一次。这样审计人员来看的时候,不需要你重复解释,看文档就能理解你的决策逻辑。
5. 常见问题与排错实录
5.1 Job 一直 Running 或者状态异常
最常见的原因是镜像拉取慢,尤其在大集群或网络受限环境。先kubectl describe pod看事件,通常能看到Failed to pull image或者ImagePullBackOff。如果是公网镜像,考虑先部署到本地仓库,再改 Job 镜像地址。
另外一种情况是挂载宿主机路径冲突。比如节点上/etc/kubernetes不存在,Job 就会一直创建容器失败。解决思路是先确认路径,或像前面说的先创建空目录。
5.2 结果里全是 WARN,PASS 很少
这种情况大概率是控制平面组件信息获取不到。如果你是二进制方式运行,Kube-Bench 会尝试从进程命令行读取参数。如果 kube-apiserver 不是以静态 Pod 方式运行、你也没有对应权限查看进程信息,它就只能标记 WARN。
排错思路是先确认运行方式。二进制方式可以通过ps -ef | grep kube-apiserver看进程是否可见;Job 方式则检查 Pod 是否有权限访问节点的/proc。如果确实拿不到,可以考虑把 Kube-Bench 容器以 privileged 模式运行,但这要看安全策略是否允许。
5.3 版本不匹配导致误报
CIS Benchmark 每隔一段时间会更新,Kube-Bench 的版本也跟着更新。如果你的集群版本比较老,但用了最新版 Kube-Bench,可能有一些检查项检查的新参数在你的组件里根本不存在,于是被判 FAIL。这种误报处理起来不复杂:用--benchmark指定适合自己集群版本的基准版本,或者直接定义自定义配置文件,把不适用的检查项移出扫描范围。
我在维护多套版本不一致的集群时,会为每套集群维护一个独立的配置目录和版本说明,避免“一把尺子量所有集群”。可能有人觉得麻烦,但这样做了之后,整改沟通效率反而高很多。
5.4 扫描结果文件丢失或格式损坏
Job 方式如果输出文件写到容器内临时目录,Pod 被清理后结果就丢了。所以一定要挂载一个宿主目录作为输出目录,或者直接通过日志方式保存。如果容器是readOnlyRootFilesystem: true,而你又没有挂载可写目录,Kube-Bench 会在写文件时报错,日志里会有明确提示。处理方式是挂载一个空目录,或者在命令里不指定--outputfile,直接取标准输出。
6. 从一次性扫描到持续审计:让基线检查“保鲜”
6.1 用 CronJob 定期扫描
安全基线不是做一次就完了。集群配置每天都有可能被改动,比如某个组件升级后配置漂移、有人为了临时调试改了权限,这些都是真实发生过的场景。我习惯用 CronJob 去驱动 Kube-Bench 定期扫描,频率可以根据变更频率来定,常规环境一周一次足够,变更频繁的环境考虑每天一次。
在已有的 Job 定义基础上,把 kind 改成 CronJob,加上schedule字段即可:
apiVersion: batch/v1 kind: CronJob metadata: name: kube-bench namespace: security spec: schedule: "0 2 * * 1" jobTemplate: spec: template: spec: serviceAccountName: kube-bench restartPolicy: Never containers: - name: kube-bench image: docker.io/aquasec/kube-bench:latest command: ["kube-bench", "run", "--json", "--outputfile", "/var/output/result.json"] volumeMounts: - name: output mountPath: /var/output volumes: - name: output hostPath: path: /tmp/kube-bench-output注意 CronJob 的schedule时区默认是控制平面的时区,要和团队约定一个统一的时区语义,避免后续排查看时间对不上。
6.2 结果如何进入告警与可视化体系
CronJob 跑完以后,结果只躺在宿主目录里没有意义的。我通常会在 Job 完成后,用一个小容器把 result.json 读取出来,解析出 FAIL 数量,推送到内部的监控或消息系统。推送内容不用太复杂:集群名称、扫描时间、FAIL 项数量、新增 FAIL 项列表,就已经能让值班人员快速判断是否需要介入。
有个细节值得注意:如果你只是关注 FAIL 数量,那么同一批老问题会每周都产生告警,大家很快就“告警疲劳”了。更好的做法是:首次扫描后把结果基线存下来,之后每次扫描只对比差异,只有“新增 FAIL”或“FAIL 数量上升”才告警。这样既不会淹没在旧问题里,又能及时发现配置漂移。
6.3 自定义配置的维护纪律
Kube-Bench 的自定义配置目录是整个扫描体系的“配置文件”,和代码一样需要做版本管理。我见过有人在生产集群上手工改配置,结果没有记录,后面审计时完全说不清为什么某项被跳过。建议把配置目录放到 Git 仓库里,每次修改走评审流程,并在扫描结果里记录当前配置的版本号或 commit 号。这样无论哪一次扫描结果,都能回溯到当时使用的检查规则,问题定位就变成一条直线。
7. 我的实操体会
最后分享几点个人经验。第一,Kube-Bench 的价值不在于“跑出成绩”,而在于它让你的安全建设从“感觉良好”变成“有据可查”。我第一次在某集群跑出 30 多项 FAIL 的时候,团队里确实有人觉得这工具太严格,但一条条对上具体组件参数之后,大家就都认同这是真实的差距了。第二,不要试图一次性解决所有 FAIL,先修风险等级最高、修复成本最低的项,比如 etcd 文件权限、匿名认证、RBAC 最小权限,这些改动风险低、收益明显。第三,把扫描结果和整改记录当作资产来管理,而不是应付检查的临时材料。每一次扫描、每一条例外审批、每一轮整改,最好都留下清晰的文档和责任人。这样做的好处会在半年之后体现出来:当有人问你“这个集群安全吗”,你不需要拍胸脯,直接拉出最新一轮的扫描报告和例外清单就够了。