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 这类容易被忽略的容器类型,并给每条约束写明拒绝原因,让开发在报错信息里就能看懂怎么改。