Trivy Operator 集群安全报告
一个团队接入集群安全平台后,仪表盘连续几天显示高危漏洞为零。直到节点扩容失败,工程师才发现扫描 Job 一直因私有仓库凭证不可见而报错,报告对象又在过期后被回收。“零”其实来自没有完成扫描,而不是镜像没有漏洞。由于告警只统计报告里的严重项,没有统计待扫描对象、失败 Job 和数据库年龄,最值得关注的失败恰好被过滤掉了。
另一个团队在业务高峰执行大规模滚动发布,每个新 ReplicaSet 都触发漏洞、SBOM 和配置审计。扫描并发、首次数据库下载与镜像仓库限流叠加,Job 大量排队,API Server 和节点临时存储同时承压。Operator Pod 仍是 Ready,安全报告却数小时没有收敛。问题不在某个漏洞规则,而在持续控制器把对象变化放大成了调度、下载、扫描、写 CRD 和周期重扫的完整负载。
先把持续扫描看成一条控制器链路
Trivy Operator 是运行在 Kubernetes 内的控制器。它监听工作负载和集群对象的变化,创建临时扫描 Job,再把结果写入 aquasecurity.github.io/v1alpha1 API 组下的 Security Report CRD。VulnerabilityReport 记录工作负载与容器漏洞,ConfigAuditReport 记录配置检查,ExposedSecretReport 记录暴露秘密线索,SBOM、RBAC、基础设施与合规报告则覆盖不同证据面。CRD 仍是 alpha API,业务系统不应把字段结构当成永不变化的数据合同。
这条链路至少有五种状态:对象尚未进入监听集合、对象已排队但 Job 未创建、Job 正在扫描、Job 失败、报告已生成。报告还可能因 TTL 到期、对象 UID 变化、ownerReference 回收或 CRD 删除而消失。因此查询不到报告不能直接映射成“通过”,查询到空结果也必须同时验证扫描状态、扫描器版本、数据库信息、目标 UID 与报告创建时间。
Trivy CLI 的镜像或目录扫描适合在开发机和 CI 对固定制品做确定性检查;Operator 处理的是对象进入集群后的持续发现与重扫。两者可以消费相同扫描能力,却有完全不同的调度、权限和证据生命周期。CI 证据绑定制品 digest,Operator 证据还绑定集群对象、命名空间、控制器状态与报告存活期,不能互相替代。
安装前先准备集群身份与回滚材料
以应用 v0.32.0、Chart 0.34.0 作为一组可复核基线。安装机需要 Helm 3、能够创建命名空间和集群级 CRD/RBAC 的 kubeconfig,并且必须先确认当前 context,避免把实验控制器装入错误集群。Chart 没有声明 kubeVersion 并不代表支持任意 Kubernetes 版本;目标发行版、节点架构、私有仓库和托管控制面都要进入兼容性回归。
先查看 官方 Release、官方 Helm 索引 和目标 tag 的 Chart values,核对 Chart version、appVersion 与镜像引用。随后把默认值保存到仓库外的变更工件中,评审后再安装:
kubectl config current-context
kubectl auth can-i create customresourcedefinitions.apiextensions.k8s.io
kubectl auth can-i create clusterroles.rbac.authorization.k8s.io
helm repo add aqua https://aquasecurity.github.io/helm-charts/
helm repo update
helm show chart aqua/trivy-operator --version 0.34.0
helm show values aqua/trivy-operator --version 0.34.0 > trivy-operator-values.yaml
helm upgrade --install trivy-operator aqua/trivy-operator \
--namespace trivy-system \
--create-namespace \
--version 0.34.0 \
--values trivy-operator-values.yaml \
--wait --timeout 10m预期证据不是一句“Helm 成功”,而是 release 已部署、Operator Deployment 可用、报告 CRD 建立、ServiceAccount 与绑定对象符合评审后的清单。kubectl get events -n trivy-system --sort-by=.lastTimestamp 应没有持续的拉取、调度或权限失败。若安装超时,先保存 Deployment condition、Pod 事件、容器日志和 Helm manifest,再决定修复或卸载;不要在证据缺失时反复重装。
用关键字段控制扫描爆炸半径
targetNamespaces 为空通常意味着监听全部命名空间,不是“不扫描任何命名空间”。首轮接入更稳妥的方式是明确选择实验命名空间,并通过 excludeNamespaces 排除系统、Operator 自身和不允许采集的区域。命名空间选择改变发现覆盖,必须由“目标对象数与已生成报告数能否闭合”验证,而不是只看 values 文件。
operator.scanJobsConcurrentLimit 决定并行扫描 Job 上限。提高它会缩短理想条件下的收敛时间,也会同步提高 API 请求、节点调度、镜像仓库请求和数据库下载压力。operator.scanNodeCollectorLimit 控制节点采集并发;operator.scanJobTimeout 过短时,大镜像、慢代理或首次数据库下载会被误判为失败;operator.scanJobsRetryDelay 太短则可能在仓库 429 或网络故障时形成重试风暴。
operator.scannerReportTTL 的默认量级为 24 小时。TTL 到期会删除报告并触发新扫描,它既是新鲜度机制,也是稳定存在的周期负载。operator.scanJobTTL 为空时,扫描 Job 不会因该字段自动清理,集群需要明确由谁保留失败现场、何时清理完成 Job。alternateReportStorage.enabled 打开后,报告从 etcd 中的 CRD 转向可选 PVC JSON 存储,查询、备份、并发和恢复语义都会改变,不能把它当作无成本的容量开关。
最小正向实验要证明报告来自哪个对象
在隔离命名空间创建一个固定镜像 digest 的 Deployment。正例应显式设置资源请求、非 root、禁止提权和只读根文件系统,使配置审计结果更容易解释。镜像内容仍可能产生漏洞发现,正例不是承诺“零漏洞”,而是验证对象、Job、扫描器和报告之间的关联。
apiVersion: apps/v1
kind: Deployment
metadata:
name: posture-good
namespace: posture-lab
spec:
replicas: 1
selector:
matchLabels: { app: posture-good }
template:
metadata:
labels: { app: posture-good }
spec:
containers:
- name: app
image: nginx@sha256:<approved-digest>
resources:
requests: { cpu: 20m, memory: 32Mi }
limits: { cpu: 100m, memory: 128Mi }
securityContext:
runAsNonRoot: true
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities: { drop: ["ALL"] }创建后依次保存 Deployment UID、相关 Job、Event 和报告 YAML。预期 VulnerabilityReport、ConfigAuditReport 或已启用的报告类型带有目标工作负载引用、扫描器信息和结果摘要。手工删除一项实验报告后,控制器应根据对象仍然存在这一事实重新调谐并重建报告;删除 Deployment 后,与其绑定的命名空间级报告应随 owner 生命周期回收。重建耗时必须用目标规模基线评估,不能把单对象速度直接外推到生产集群。
反向实验要让权限失败显式暴露
反例使用私有镜像,并让扫描身份无法读取所需的 imagePullSecret。预期不是产生一个空的 VulnerabilityReport,而是扫描 Job、Pod 或事件中出现 Forbidden、镜像拉取失败或凭证获取失败;待扫描对象仍应被统计为未完成。随后恢复经过批准的最小凭证路径,确认失败 Job 不再增长且报告最终生成。
另一组反例可以把 operator.scanJobTimeout 暂时调到低于已知扫描耗时,或让内部 registry 返回限流。预期出现 timeout、429 或重试证据,报告数量暂时落后于目标数量。恢复配置后观察三条曲线:待扫描对象下降、失败增量停止、有效报告数收敛。若只有报告总数上升,却没有关联目标 UID 和失败归零,仍不能证明恢复完成。
kubectl get jobs,pods -n trivy-system
kubectl get events -A --field-selector reason=Failed --sort-by=.lastTimestamp
kubectl get vulnerabilityreports,configauditreports -A -o yaml
kubectl logs -n trivy-system deploy/trivy-operator
kubectl auth can-i get vulnerabilityreports.aquasecurity.github.io \
-n team-a --as system:serviceaccount:team-a:report-reader
kubectl auth can-i get vulnerabilityreports.aquasecurity.github.io \
-n team-b --as system:serviceaccount:team-a:report-reader两条授权查询应分别返回:team-a 为 yes,team-b 为 no。这是预期判据,不是预先宣称的执行结果。实验结束后删除反例 Deployment、测试 ServiceAccount 与临时绑定,并确认不存在持续重试的 Job。
把报告接进项目而不是接进一块总分看板
项目接入应以稳定资产键关联代码与运行对象:仓库、环境、集群、命名空间、workload kind/name、对象 UID、容器名和镜像 digest。工单至少保存报告类型、检查或漏洞 ID、严重度、扫描器版本、数据库时间证据、首次与最近观察时间、原始报告引用和 owner。只保存一个“风险分”会丢掉规则变化、对象重建和数据库更新造成的差异。
持续集成保留固定制品的扫描证据,部署系统记录镜像 digest 与 rollout revision,Operator 再补充集群对象与持续重扫事实。三者通过 digest 和工作负载身份汇合。Operator 发现运行对象使用了未经 CI 归档的 digest 时,应产生供应链漂移信号;CI 已通过而 Operator 后续发现新漏洞时,则进入重新评估与修复队列,而不是倒推当时流水线失效。
报告事件送往外部系统时,应优先传递报告引用和必要摘要。Webhook Header、仓库令牌和 Trivy Server token 由 Secret 注入,日志禁止输出完整凭证。下游系统必须幂等处理更新和删除:同一资产与规则的新报告更新现有发现项,报告因 TTL 消失不应自动把工单判定为已修复。
多租户权限必须拆开控制器与读取者
Operator 需要监听目标对象、创建 Job、写报告 CRD,并可能读取 ServiceAccount 和 imagePullSecret。operator.accessGlobalSecretsAndServiceAccount 默认开启全局访问能力时,单个控制器拥有跨命名空间凭证读取面。多租户集群应优先限定目标命名空间,审查 privateRegistryScanSecretsNames 等显式凭证入口,并用反向实验确认收紧后失败可观测。
开发者读取报告不需要继承控制器权限。为每个团队命名空间创建只含报告 get/list/watch 的 Role,再把它绑定给团队读取身份。集群级 RBAC 报告、节点报告和跨命名空间汇总由安全运营角色读取。报告可能暴露软件包、镜像、秘密位置与权限缺陷,本身就是敏感资产;Dashboard、导出文件和日志平台都需要访问审计、保留期与脱敏规则。
node-collector 的风险更高。它会以 Job 读取 /var/lib/etcd、/var/lib/kubelet、/etc/kubernetes、/etc/cni/net.d 等宿主目录。只读 hostPath 仍然意味着高价值配置可见。托管控制面、Windows 节点、虚拟节点或定制目录可能无法提供完整证据,应通过节点覆盖矩阵标记“不可采集”,而不是让缺失报告进入合规分母。
离线环境真正需要一条数据库供应链
trivy.offlineScan=true 只能禁止扫描阶段的额外外部查询,不能自动提供漏洞 DB、Java DB 与 checks bundle。受限网络需要联网同步区拉取官方 OCI 工件,验证来源、摘要、schema 与生成时间,再镜像到内部仓库。集群使用最小只读凭证访问内部仓库,并把 trivy.dbRegistry、trivy.dbRepository、Java DB 与 checks 入口明确指向内网。
正向验证是在阻断扫描命名空间公网出站后仍能生成带数据库证据的报告,网络日志没有回退到外部 registry。反向验证把 DB repository 指向不存在的内部路径,预期 Job 明确失败且没有空成功报告;再放入不兼容 schema 的数据库,错误也应可见。数据库 digest、生成时间、同步延迟和兼容性要成为监控对象,因为扫描器健康但数据库陈旧同样会制造假安全感。Trivy DB 文档可用于核对数据库更新与镜像方式。
容量成本要从对象放大系数计算
容量预算从目标 workload 数、容器数、报告类型数和单报告大小开始,再叠加滚动发布产生的 ReplicaSet、报告 TTL、重试率与节点采集。一个 Deployment 的一次更新可能产生新对象、新镜像拉取、多类扫描 Job 和多份报告;大规模发布时,这些动作会在同一时间窗汇聚到调度器、registry、DB 仓库、API Server 与 etcd。
观察 Operator CPU 只能说明控制循环本身。至少还要跟踪待扫描对象、活跃与失败 Job、从对象创建到有效报告的年龄、registry 429、扫描超时、报告对象数量与字节、API 限流以及 etcd 增量。生产阈值来自集群基线和安全发现 SLO;演示环境中的固定分钟数不能直接成为通用承诺。
小集群可以使用内置 standalone 模式简化链路;大量重复扫描若采用 Trivy Server,需要独立考虑 Server 副本、缓存、DB 更新、认证和服务故障域。Server 降低重复下载不等于消除 registry 与 API 压力,也会新增一个共享瓶颈。选型时要比较节省的带宽与新增的状态、令牌和恢复责任。
排障时沿对象、Job、数据库和报告逐层收敛
报告缺失先确认目标是否被 namespace selector 排除,再查工作负载 UID 和对应 Job。Job 没创建通常指向监听、队列或控制器权限;Job Pending 指向配额、污点、亲和性或资源请求;ImagePullBackOff 指向扫描镜像或目标镜像凭证;OOM、timeout 和 429 分别指向资源、时限与仓库容量。Job 完成但没有报告,则继续检查 Operator 写 CRD 的权限、对象 owner 变化与报告存储错误。
报告突然减少,要区分 TTL 回收、工作负载删除、CRD 变更、alternate storage 切换和误删。严重项突然归零,还要检查数据库是否更新失败、扫描器版本是否变化、目标镜像 digest 是否漂移。每次故障记录 controller revision、Chart values digest、对象 UID、Job 名、scanner version、DB 证据和第一条错误事件,才能把同类故障从“看起来没扫”收敛到具体失败节点。
高可用不是把副本数改成二
Chart 默认 operator.replicas: 1。即使 Deployment 增加副本,也必须从渲染结果、启动日志和 Lease 证明 leader election 实际生效,再执行 leader Pod 删除实验。控制器接管只提高调谐面的可用性,不会自动增加扫描吞吐;并发上限、节点容量、registry、DB 和 API 限流仍决定数据面收敛速度。
跨故障域部署还要补充 topology spread、PodDisruptionBudget、镜像可达性和凭证可用性。若所有副本依赖同一个不可用的内部 DB 仓库,控制器 HA 无法产生报告。恢复实验既要测 Lease 转移,也要确认没有重复 Job 风暴、没有遗漏对象,并观察报告年龄恢复到基线。
升级、回滚与退出必须保护报告证据
升级前导出 Helm values、manifest、CRD schema 与样本报告,阅读跨版本 Release 中的 CRD、扫描器、RBAC 和默认值变化。在相同规模的 fixture 上做双版本比较,关注报告数量、字段、误报、收敛时间和全量重扫。helm rollback 只能恢复 release 资源;若新 CRD schema 或报告已经不兼容,旧控制器未必能读取,因此回滚判据必须包含 CRD 向后兼容和样本报告可读性。
退出时先暂停新接入和外部通知,导出仍有保留要求的报告,再卸载控制器:
kubectl get vulnerabilityreports,configauditreports,exposedsecretreports \
-A -o yaml > trivy-operator-reports-backup.yaml
helm get values trivy-operator -n trivy-system -a > trivy-operator-values-backup.yaml
helm get manifest trivy-operator -n trivy-system > trivy-operator-manifest-backup.yaml
helm uninstall trivy-operator -n trivy-system
kubectl get crd -o name | grep aquasecurity.github.io官方 Helm 安装与卸载文档明确说明 CRD 需要单独处理。卸载 release 后报告 CRD 可能仍在;删除 CRD 会连同该类型全部报告一起删除。CRD 与 PVC 的删除必须经过证据保留审批,并在临时集群验证恢复材料。最后撤销 registry、DB、Webhook 和 Trivy Server 凭证,删除测试命名空间,确认没有残留 Job、ClusterRoleBinding、外部通知和持续费用,持续扫描能力才算真正退出。
