生产环境里,Flux 就像一位不知疲倦的搬运工,它把 Git 仓库里的配置原封不动地搬到 Kubernetes 集群里。这位搬运工手脚勤快,但问题是它从不思考——万一 Git 仓库里出现一份有问题的配置,它也会照搬不误,轻则让服务告警,重则让整个生产环境“翻车”。所以,我们需要在 Flux 的工作流程中加一道“配置审计”的安检门,让错误配置在到达集群之前就被拦下来。下面我就结合自己落地这套方案的经验,掰开揉碎讲讲怎么设计、怎么实现,以及路上有哪些坑。

一、先想明白:审计到底在审什么

很多朋友一提到审计就想到“查日志”,但 Flux 配置审计没那么简单。它不是看 Flux 自己运行得顺不顺,而是要检查从 Git 到集群这条链路里所有需要被管住的东西。我总结了一下,主要盯四类事。

1.1 偏差审计:Git 里说的和集群里干的,是不是一回事

Flux 的职责是让集群状态不断接近 Git 仓库里的期望状态。但现实中可能发生这种情况:有人直接在生产集群里用 kubectl edit 改了资源,Flux 发现了也懒得改回去(因为某些字段被忽略了);或者 Flux 因为权限不足根本改不动。偏差审计就是要找出“Git 里写的是 A,集群里实际是 B”的资源。

1.2 权限审计:Flux 是不是被惯坏了

Flux 在集群里运行时需要一定的 Kubernetes 权限。有的团队图省事,直接给 Flux 绑定 cluster-admin,这样它确实能删除一切,但风险极大。万一 Git 仓库被攻击者改动,攻击者就等于拿到了集群最高权限。权限审计就是检查 Flux 使用的 ServiceAccount 是否被赋予了超出必要范围的权限。

1.3 资源合规审计:配置本身是不是“良民”

这块主要看 Git 仓库里定义的工作负载是不是符合公司规范。比如镜像是不是用了 latest 标签、副本数是不是小于最低要求、探针配置是不是缺失了、资源请求和限制是不是乱写。这些规则每家都不一样,但共同点是可以用自动化脚本去扫。

1.4 变更审计:谁在什么时候动过关键配置

生产环境会频繁更新,我们得能回答“这个 Kustomization 是谁改的?是不是经过审批?”这类问题。这就是 Git 历史审计,Flux 本身不记录这些,但我们可以从 Git 提交记录里提取,再和发布流程关联起来。

二、方案设计:像搭积木一样把审计拼起来

审计方案听起来很玄,其实拆开就是三部分:什么时候审、审哪些规则、发现问题怎么办。

2.1 定好审计触发方式

我建议用两种触发方式结合:一种是定时触发,比如每小时跑一次全量审计,生成报告;另一种是事件触发,比如 Flux 每次同步完、或者有人往 Git 仓库推送了新提交时,马上跑快速审计。事件触发能最快发现问题,定时触发能兜底,防止事件机制本身出故障。

2.2 定好审计规则引擎

规则引擎是整个方案的核心。我不建议把所有规则硬编码在一个大脚本里,那样维护起来太痛苦。更好的做法是把规则变成配置文件,比如用一份 YAML 文件描述“镜像不允许带 latest”“repo 地址必须在白名单内”等。审计程序读这些规则,再扫描 Flux 的资源配置,输出结果。这样以后想加新规则,改配置文件就行,不用动主程序。

2.3 定好审计结果怎么用

审计结果不能躺在文件里没人看。我通常让审计脚本输出结构化 JSON,一份送到监控系统,一份用于生成人类可读的摘要。如果发现严重问题(比如绑定了 cluster-admin),立刻触发钉钉或企业微信告警;如果只是轻微问题(比如同步间隔稍微短了点),记入报表就行,不用半夜叫醒值班人。

三、落地实操:用 Bash 写一个真能用的审计脚本

技术栈我选的是 Bash,配合 kubectl、jq、yq 这三个工具。Bash 虽然不够优雅,但胜在到处都有、依赖少,适合运维同学快速上手。下面这个脚本是我在真实环境里用过的一个精简版本,注释写得很详细,你可以直接抄作业。

3.1 脚本功能清单

这个脚本会干这几件事:第一,扫描所有 Kustomization 对象,检查它们的同步间隔和挂起状态;第二,检查所有涉及 Flux 的 ClusterRoleBinding,看有没有滥用 cluster-admin;第三,扫描所有正在运行的 Pod,找有没有镜像标签是 latest 的。如果发现问题,它会输出一个 JSON 报告。

3.2 完整脚本

#!/usr/bin/env bash
# =============================================================
# flux-audit.sh
# 技术栈:Bash(依赖 kubectl、jq、yq)
# 用途:对生产环境中的 Flux 配置做一次基础审计
# 用法:./flux-audit.sh [namespace] (不传 namespace 则审计全集群)
# =============================================================

set -uo pipefail

# 审计目标命名空间,默认空字符串表示所有命名空间
NAMESPACE="${1:-}"

# 生成带时间戳的审计报告文件名
REPORT_FILE="flux-audit-report-$(date +%Y%m%d%H%M%S).json"

# 临时文件,用来收集审计发现
TMP_FINDINGS="/tmp/flux-audit-findings-$$.txt"
: > "$TMP_FINDINGS"

# -------------------------------------------------------------
# 辅助函数:往临时文件里写一条审计发现
# 参数1:级别(error/warn/info)
# 参数2:对象描述
# 参数3:具体信息
# -------------------------------------------------------------
add_finding() {
  local level="$1"
  local object="$2"
  local message="$3"
  # 一行一个 JSON 对象,最后统一合并
  echo "{\"level\":\"$level\",\"object\":\"$object\",\"message\":\"$message\"}" >> "$TMP_FINDINGS"
}

# -------------------------------------------------------------
# 第一步:获取 Kustomization 列表
# 使用 jq 把每个资源转成一行,方便循环处理
# -------------------------------------------------------------
if [ -n "$NAMESPACE" ]; then
  kustomizations=$(kubectl -n "$NAMESPACE" get kustomizations.kustomize.toolkit.fluxcd.io -o json 2>/dev/null)
else
  kustomizations=$(kubectl get kustomizations.kustomize.toolkit.fluxcd.io -A -o json 2>/dev/null)
fi

if [ -z "$kustomizations" ]; then
  echo "没有找到 Kustomization 资源,请检查 Flux 是否安装正常。" >&2
  exit 1
fi

# 利用进程替换而不是管道,避免 while 循环在子 shell 中执行导致变量丢失
while read -r k; do
  name=$(echo "$k" | jq -r '.metadata.name')
  ns=$(echo "$k" | jq -r '.metadata.namespace')
  obj="${ns}/${name}"
  echo "审计 Kustomization: $obj"

  # ---------- 检查同步间隔是否过短 ----------
  interval=$(echo "$k" | jq -r '.spec.interval // "5m"')
  # 简单把分钟转成秒
  if [[ "$interval" =~ ^([0-9]+)m$ ]]; then
    seconds=$(( ${BASH_REMATCH[1]} * 60 ))
  elif [[ "$interval" =~ ^([0-9]+)h$ ]]; then
    seconds=$(( ${BASH_REMATCH[1]} * 3600 ))
  else
    seconds=300
  fi
  if [ "$seconds" -lt 300 ]; then
    add_finding "warn" "$obj" "同步间隔 ${interval} 小于5分钟,建议至少设置为5m,避免频繁拉取Git仓库"
  fi

  # ---------- 检查是否被挂起 ----------
  suspend=$(echo "$k" | jq -r '.spec.suspend // false')
  if [ "$suspend" == "true" ]; then
    add_finding "info" "$obj" "此Kustomization被挂起,Flux不会同步它,请确认是否是有意为之"
  fi

  # ---------- 检查 sourceRef 是否显式声明 kind ----------
  source_kind=$(echo "$k" | jq -r '.spec.sourceRef.kind // ""')
  if [ -z "$source_kind" ]; then
    add_finding "warn" "$obj" "sourceRef 没有指定 kind,Flux 默认按 GitRepository 处理,建议显式写出,避免混淆"
  fi

done < <(echo "$kustomizations" | jq -c '.items[]')

# -------------------------------------------------------------
# 第二步:检查 Flux 相关主体的权限
# 找出所有 ClusterRoleBinding 中 subject 里带 flux 字样的绑定
# -------------------------------------------------------------
echo "审计 ClusterRoleBinding..."

while read -r crb; do
  crb_name=$(echo "$crb" | jq -r '.metadata.name')
  subjects=$(echo "$crb" | jq -r '.subjects[]? | select(.kind=="ServiceAccount") | (.namespace // "unknown") + "/" + .name' 2>/dev/null)
  if [ -z "$subjects" ]; then
    continue
  fi
  # 只关注和 flux 相关的 subject
  if echo "$subjects" | grep -qi "flux"; then
    role_ref=$(echo "$crb" | jq -r '.roleRef.name')
    if [ "$role_ref" == "cluster-admin" ]; then
      add_finding "error" "ClusterRoleBinding/${crb_name}" "Flux相关ServiceAccount绑定到了cluster-admin,风险极高,请改为最小权限角色"
    else
      add_finding "info" "ClusterRoleBinding/${crb_name}" "Flux相关主体绑定角色 ${role_ref},权限情况请结合集群策略确认"
    fi
  fi
done < <(kubectl get clusterrolebindings.rbac.authorization.k8s.io -o json | jq -c '.items[]')

# -------------------------------------------------------------
# 第三步:扫描所有运行中的 Pod 镜像,找出 latest 标签
# -------------------------------------------------------------
echo "扫描镜像标签..."
while read -r container; do
  image=$(echo "$container" | jq -r '.image // empty')
  if [ -z "$image" ]; then
    continue
  fi
  # 用正则匹配最后一个冒号后面是不是 latest 或没有标签
  if [[ "$image" =~ :latest$ ]] || [[ "$image" != *:* ]]; then
    add_finding "error" "Container" "镜像 ${image} 使用了 latest 或不带标签,生产环境请固定到具体版本"
  fi
done < <(kubectl get pods -A -o json | jq -c '.items[].spec.containers[]')

# -------------------------------------------------------------
# 汇总输出 JSON 报告
# -------------------------------------------------------------
echo "生成审计报告 ${REPORT_FILE}..."

# 读取临时文件中的所有行,用 jq 合成 JSON 数组
findings_json=$(jq -s '.' "$TMP_FINDINGS")

cat > "$REPORT_FILE" <<EOF
{
  "audit_time": "$(date -Iseconds)",
  "namespace_scope": "${NAMESPACE:-all}",
  "findings": $findings_json
}
EOF

# 清理临时文件
rm -f "$TMP_FINDINGS"

echo "审计完成,报告文件:${REPORT_FILE}"

3.3 运行脚本并查看结果

# 赋予执行权限
chmod +x flux-audit.sh

# 只审计 production 命名空间
./flux-audit.sh production

# 或者审计全集群(需要有足够的 RBAC 权限)
./flux-audit.sh

脚本跑完后,你会看到一个 flux-audit-report-时间戳.json 文件。用 cat 可以查看里面每一条审计发现,级别是 error 的就需要马上处理。

四、关联技术:用 Conftest 做更精细的规则校验

上面的 Bash 脚本适合做“扫描型”审计,但如果你想对 Git 仓库里的 YAML 文件做更结构化的合规检查,我强烈推荐你了解一个叫 Conftest 的开源工具。它专门用来测试配置文件的策略,用正则表达式或者 JSON Schema 来定义“什么是对的”。

比如,你可以用 Conftest 来校验所有 Deployment 的副本数不能低于 3:

# policy/deployment.rego
package main

deny[msg] {
  input.kind == "Deployment"
  input.spec.replicas != null
  input.spec.replicas < 3
  msg = sprintf("副本数必须大于等于3,当前是%v", [input.spec.replicas])
}

注意,这里示例技术栈变成了 Rego,和 Bash 脚本不一样了。所以我们在实际落地时,最好把 Bash 作为“定时扫描器”,把 Conftest 作为“合并请求里跑的检查”,两者各司其职,不算混用。但在我们这篇文章的示例维度,为了保持单纯,刚才的 Bash 脚本就足够你理解审计的核心思路了。

五、落地过程中最容易踩的坑

方案设计得很美好,真到了生产环境,总会有一些地方让你抓狂。我把自己摔过的坑整理出来,希望你能绕开。

5.1 别忘了 kubeconfig 的权限范围

审计脚本本身也是跑在某个 ServiceAccount 上的。如果你用个人电脑上的 kubeconfig 跑,那只能看到你个人有权的资源。到了生产环境,一定要给审计脚本单独建一个 ServiceAccount,并授予只读权限,比如 view 这种集群角色。否则,审计脚本会因为没权限而扫出一堆“空数据”,让你误以为环境很干净。

5.2 注意 Flux 的资源是集群级的还是命名空间级的

Flux 的 GitRepository 和 Kustomization 是集群级还是命名空间级,取决于你安装时的设置。有的版本默认是命名空间级的,有的支持集群级。你脚本里如果用了 -A 可能是安全的,但如果你只审计某一个命名空间,一定要确认 Flux 存储的那份 Kustomization 确实在这个命名空间里。否则你会白跑。

5.3 镜像 tag 检查不能只看 latest

我们脚本里简单判断了 latest,但生产环境里更常见的是有人写死了一个 commit 哈希,或者用了动态生成的 tag 比如 dev-20250331。这种 tag 虽然不是 latest,但同样不可控。你应该在规则层维护一份“允许镜像仓库列表”和“允许 tag 格式”,而不是只过滤 latest。

5.4 审计结果可能会产生告警风暴

如果你第一天部署这套审计,很可能被几千条告警淹没。这是因为过去积累的问题一次性暴露了。我建议你上线时先以 info 级别记录,不触发告警,跑一周看看噪音比例。然后再把规则逐步收紧,让告警变得更有意义。

5.5 审计脚本本身也要受审计

这听起来有点套娃,但很现实。审计脚本里如果有硬编码密钥,或者存放在不安全的 Git 仓库,那它本身就成了一个攻击入口。我建议把审计脚本放到受保护的仓库里,并且脚本里不要出现任何云厂商密钥。可以用 kubectl 从 Secret 中动态读取。

5.6 注意 Flux 默认的垃圾回收行为

Flux 会把 Git 里被删除的资源从集群中移除。如果审计规则没有考虑到这一点,你可能会发现某一天一个旧 Deployment 被 Flux 清了,而审计报告里只记录了“期望状态与集群状态一致”。所以审计时要额外加上“被删除资源是否有备份”的检查。

六、总结

给 Flux 做配置审计,看起来像是在愉快的 GitOps 流程里加了个“拦路虎”,但实际它保护的是我们最敏感的生产环境。一个好的审计方案,应该能自动发现偏差、权限过大、镜像失控等问题,并且能在错误同步发生之前发出预警,而不是等故障爆发后才去翻日志。

我在这篇文章里给出的 Bash 脚本只是一个起点,你可以根据自己公司的规范不断丰富规则。记住,审计的最终目的不是限制开发者的自由度,而是让所有人都能更放心地把改动交给 Flux。毕竟,在集群里翻车可比在 Git 里回滚难受多了。