K8s 准入控制 Gatekeeper 策略配置实战

Gatekeeper 准入控制解决的是 Kubernetes 里「事后才发现」的问题:一个带 privileged: true 的 Pod 被 apply 上去,等到安全扫描出报告时,它可能已经挂载了宿主机根目录。准入控制(Admission Control)把校验提前到 API Server 写 etcd 之前——不合规的清单根本进不了集群。本文用 Open Policy Agent 的 Gatekeeper 实现层,把「禁止特权容器、禁止 hostPath、镜像必须有明确 tag、必须设置资源限制」写成可版本化的策略代码。

适用场景

  • 多团队共用集群,无法保证每个人都记得写 securityContext 与 resources。
  • 等保或内部安全基线要求「容器不得以特权模式运行」「镜像来源可控」,需要可审计的强制手段。
  • 已经启用 Pod Security Admission(PSA),但需要比 baseline/restricted 更细的自定义规则,例如镜像仓库白名单。
  • 需要先观察存量工作负载的违规情况,再决定何时从告警切到拦截。

前置条件

  • Kubernetes 1.25 及以上,集群管理员权限(可创建 CRD 与 ValidatingWebhookConfiguration)。
  • 已安装 Helm 3;集群能拉取 openpolicyagent/gatekeeper 镜像。
  • 确认控制面 API Server 到 Gatekeeper Service 的网络可达(webhook 超时会导致整个集群写入失败,务必先在测试集群演练)。
  • 已备份关键工作负载清单,用于策略上线后的回归。

原理说明

Gatekeeper 以动态准入控制器的形式运行:它向 API Server 注册一个 ValidatingWebhookConfiguration,所有匹配规则的写请求在落盘前会被转发到 Gatekeeper 的 webhook 端点。Gatekeeper 内部用 OPA 的 Rego 引擎求值,返回 allowed: true/false,以及拒绝原因。

它的对象模型分三层:

  • ConstraintTemplate:定义「一类策略」的参数结构与 Rego 逻辑,相当于策略函数。
  • Constraint:模板的实例,填写具体参数(如允许的镜像仓库列表)并指定作用范围。
  • Config/Audit:Gatekeeper 定期对存量资源做审计,把已存在的违规对象记录到 Constraint 的 status.violations,这让「先观察后拦截」成为可能。

enforcementAction 是灰度开关,取值 deny(拒绝)、warn(放行但在 kubectl 输出告警)、dryrun(仅记录违规)。生产落地路径应为 dryrun → warn → deny,每档至少观察一个发布周期。

性能上有一个容易踩的坑:webhook 处在所有写请求的同步路径上,Rego 求值慢或 Gatekeeper 副本不足,会直接拖慢甚至阻塞整个集群的 API 写入。实际表现为 kubectl apply 超时、控制器反复重试、Deployment 卡在创建中。因此约束的 match 范围要尽量收窄,避免对全集群所有资源生效;单条 Rego 只做字段比对这类轻量判断,不要在准入链路里做列表全量遍历或外部调用。上线前用 kubectl apply --dry-run=server 压测一批典型清单,观察延迟是否在可接受区间,这一步比事后回滚便宜得多。

操作步骤

1. 部署 Gatekeeper 并确认 webhook 就绪

helm repo add gatekeeper https://open-policy-agent.github.io/gatekeeper/charts
helm repo update

helm install gatekeeper gatekeeper/gatekeeper \
  --namespace gatekeeper-system --create-namespace \
  --set replicas=3 \
  --set auditInterval=60 \
  --set controllerManager.exemptNamespaces='{kube-system,gatekeeper-system,kube-node-lease}'

kubectl -n gatekeeper-system get pods
kubectl get validatingwebhookconfiguration gatekeeper-validating-webhook-configuration

replicas 必须大于 1,且 failurePolicy 保持默认 Ignore。若设为 Fail 且副本全挂,整个集群将无法创建任何资源,这是 Gatekeeper 最常见的事故形态。

2. 编写约束模板:禁止特权与危险能力

apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
  name: k8sdenyprivileged
spec:
  crd:
    spec:
      names:
        kind: K8sDenyPrivileged
      validation:
        openAPIV3Schema:
          type: object
          properties:
            allowCapabilities:
              type: array
              items:
                type: string
  targets:
    - target: admission.k8s.gatekeeper.sh
      rego: |
        package k8sdenyprivileged

        violation[{"msg": msg}] {
          c := input.review.object.spec.containers[_]
          c.securityContext.privileged == true
          msg := sprintf("容器 %v 不允许 privileged 模式", [c.name])
        }

        violation[{"msg": msg}] {
          c := input.review.object.spec.containers[_]
          cap := c.securityContext.capabilities.add[_]
          not allowed(cap)
          msg := sprintf("容器 %v 不允许添加能力 %v", [c.name, cap])
        }

        allowed(cap) {
          cap == input.parameters.allowCapabilities[_]
        }

        violation[{"msg": msg}] {
          c := input.review.object.spec.initContainers[_]
          c.securityContext.privileged == true
          msg := sprintf("初始化容器 %v 不允许 privileged 模式", [c.name])
        }

注意 initContainers 要单独写一条规则:只校验 spec.containers 是这类策略最典型的漏网之处。

3. 下发约束,先 dryrun 观察存量

apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sDenyPrivileged
metadata:
  name: deny-privileged-pods
spec:
  enforcementAction: dryrun
  match:
    kinds:
      - apiGroups: [""]
        kinds: ["Pod"]
      - apiGroups: ["apps"]
        kinds: ["Deployment", "StatefulSet", "DaemonSet"]
    excludedNamespaces:
      - kube-system
      - gatekeeper-system
      - monitoring
    scope: Namespaced
  parameters:
    allowCapabilities:
      - NET_BIND_SERVICE
      - CHOWN
kubectl apply -f template-deny-privileged.yaml
kubectl apply -f constraint-deny-privileged.yaml

# 查看审计发现的存量违规
kubectl get k8sdenyprivileged deny-privileged-pods \
  -o jsonpath='{.status.totalViolations}{"\n"}'
kubectl get k8sdenyprivileged deny-privileged-pods \
  -o jsonpath='{range .status.violations[*]}{.namespace}/{.name}{"\n"}{end}' | head -20

审计结果需要与业务方逐个确认。存量违规通常集中在监控组件(node-exporter 需要 hostPath)与日志采集(Fluent Bit 需要 hostPath 读 /var/log),这些应通过 excludedNamespaces 或参数白名单放行,而不是直接一刀切。

4. 增加镜像来源与标签策略

apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
  name: k8sallowedrepos
spec:
  crd:
    spec:
      names:
        kind: K8sAllowedRepos
      validation:
        openAPIV3Schema:
          type: object
          properties:
            repos:
              type: array
              items:
                type: string
  targets:
    - target: admission.k8s.gatekeeper.sh
      rego: |
        package k8sallowedrepos

        violation[{"msg": msg}] {
          c := input.review.object.spec.containers[_]
          not startswith_any(c.image)
          msg := sprintf("镜像 %v 不在允许的仓库列表中", [c.image])
        }

        startswith_any(image) {
          startswith(image, input.parameters.repos[_])
        }

        violation[{"msg": msg}] {
          c := input.review.object.spec.containers[_]
          endswith(c.image, ":latest")
          msg := sprintf("容器 %v 禁止使用 latest 标签,请指定固定版本", [c.name])
        }

        violation[{"msg": msg}] {
          c := input.review.object.spec.containers[_]
          not contains(c.image, ":")
          msg := sprintf("容器 %v 的镜像未指定 tag", [c.name])
        }
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sAllowedRepos
metadata:
  name: allow-internal-registry
spec:
  enforcementAction: warn
  match:
    kinds:
      - apiGroups: [""]
        kinds: ["Pod"]
    excludedNamespaces: ["kube-system", "gatekeeper-system"]
  parameters:
    repos:
      - "harbor.example.com/"
      - "registry.k8s.io/"

5. 灰度切换到 deny

# 先 warn 观察一周,确认 kubectl 已有告警且无业务误伤
kubectl patch k8sallowedrepos allow-internal-registry \
  --type merge -p '{"spec":{"enforcementAction":"deny"}}'

kubectl patch k8sdenyprivileged deny-privileged-pods \
  --type merge -p '{"spec":{"enforcementAction":"deny"}}'

配置验证

验证要覆盖「该拦的拦住」与「该放的放行」两个方向:

# 1) 特权容器应被拒绝
kubectl run badpriv --image=harbor.example.com/tools/busybox:1.36 \
  --overrides='{"spec":{"containers":[{"name":"badpriv","image":"harbor.example.com/tools/busybox:1.36","securityContext":{"privileged":true}}]}}'
# 预期:admission webhook "validation.gatekeeper.sh" denied the request

# 2) latest 标签应被拒绝
kubectl create deployment badtag --image=harbor.example.com/app/web:latest
# 预期:禁止使用 latest 标签

# 3) 合规清单应放行
kubectl run goodpod --image=harbor.example.com/tools/busybox:1.36 --restart=Never -- sleep 3600
kubectl get pod goodpod

# 4) 查看约束状态与审计时间
kubectl get constraints
kubectl get k8sallowedrepos allow-internal-registry -o yaml | grep -A5 status

若第 1、2 步未被拒绝,按顺序排查:kubectl get validatingwebhookconfiguration -o yaml 中的 rules 是否覆盖 CREATE/UPDATE、Constraint 的 match.kinds 是否与实际资源类型一致(Deployment 属于 apps 组,只写 Pod 不会拦到 Deployment)、以及目标命名空间是否在 excludedNamespaces 中。

常见问题

FAQ 1:策略下发后整个集群无法创建资源,怎么快速恢复?

先删 webhook 配置止血,再修策略,顺序不能反:

kubectl delete validatingwebhookconfiguration gatekeeper-validating-webhook-configuration
kubectl -n gatekeeper-system scale deploy gatekeeper-controller-manager --replicas=0
# 修复 Rego 或放宽 Constraint 后重新 apply,再由 helm 重新生成 webhook

根因通常是 Rego 写错导致求值异常(例如对不存在的字段直接取值),或 Constraint 的 match 范围过大把 kube-system 也纳入了。Rego 中访问可选字段应使用 c.securityContext.privileged 这样的路径判断,而不是强行取值,同时务必保留 exemptNamespaces。

FAQ 2:dryrun 与 audit 有什么区别,为什么 dryrun 也能看到违规?

dryrun 作用于新请求:违规请求被放行,但会被记录为 event,用 kubectl get events -A | grep dryrun 可查。audit 是 Gatekeeper 定时扫描存量对象,结果写到 Constraint 的 status.violations。两者互补:audit 告诉你集群里现在有多少不合规,dryrun 告诉你新提交的清单会不会踩线。上线前先用 audit 摸底,再用 dryrun 试运行,最后才切 deny。

FAQ 3:Gatekeeper 和 Pod Security Admission 需要同时启用吗?

建议同时启用,职责不同。PSA 是内置能力,按命名空间标签执行 baseline/restricted 三档标准,开销低、无需额外组件,适合兜底;Gatekeeper 适合表达 PSA 覆盖不到的组织级规则,例如镜像仓库白名单、必须带 owner 标签、禁止 hostPath 特定路径。两者叠加时注意拒绝信息会同时出现,排障时按 webhook 名称区分来源。

FAQ 4:策略误伤了 CI 流水线,能不能只对特定团队放行?

用 match.namespaces 或 excludedNamespaces 做范围收敛,配合命名空间标签选择器比硬编码名单更易维护。不建议为放行而放宽整条策略;更稳妥的做法是允许在 warn 档位下提交豁免申请,把例外记录在 Constraint 的注解里,形成可审计的例外台账。

总结

Gatekeeper 准入控制的价值不在「拦住一次 apply」,而在把安全基线从文档变成集群内的强制逻辑。落地顺序固定为:部署时保证 3 副本与 failurePolicy: Ignore,策略先行 dryrun 摸存量,audit 结果与业务确认豁免范围,warn 观察一周,最后切 deny。模板层面记得补齐 initContainers、ephemeralContainers 这类容易被忽略的容器类型,并给每条约束写明拒绝原因,让开发在报错信息里就能看懂怎么改。