多租户K8s集群域名归属管控及Gatekeeper规则配置排障
问题背景
- 现有多租户Kubernetes集群,内部部署搭载负载均衡的Nginx反向代理,泛域名
*.example.com已解析指向该负载均衡IP地址 - 集群内命名空间按所属用户划分为项目A、项目B两个分组,已通过NetworkPolicy实现跨项目通信隔离,仅开放所有命名空间到Nginx所在命名空间、反向代理的访问权限
- 核心访问管控需求:带有
project=a标签的命名空间内的服务,仅允许使用my-service.project-a.example.com格式的域名访问,禁止使用my-service.project-b.example.com、my-service.example.com等不符合项目归属的域名 - 已在GKE集群通过Helm部署Gatekeeper,尝试实现Ingress Host格式校验:要求对应命名空间内创建的Ingress域名必须匹配
*.{namespace-project-label}.example.com格式,编写的资源配置如下:
apiVersion: templates.gatekeeper.sh/v1 kind: ConstraintTemplate metadata: name: k8srequiredingress spec: crd: spec: names: kind: K8sRequiredIngress validation: openAPIV3Schema: type: object properties: labels: type: array items: type: string targets: - target: admission.k8s.gatekeeper.sh rego: | package k8srequiredingress operations := {"CREATE", "UPDATE"} ns := input.review.object.metadata.namespace violation[{"msg": msg, "details": {"missing_labels": missing}}] { input.request.kind.kind == "Ingress" not data.kubernetes.namespaces[ns].labels.project msg := sprintf("Ingress denied as namespace '%v' is missing 'project' label", [ns]) } violation[{"msg": msg, "details": {"missing_labels": missing}}] { input.request.kind.kind == "Ingress" operations[input.request.operation] host := input.request.object.spec.rules[_].host project := data.kubernetes.namespaces[ns].labels.project not fqdn_matches(host, project) msg := sprintf("invalid ingress host %v, has to be of the form *.%v.example.com", [host, project]) } fqdn_matches(str, pattern) { str_parts := split(str, ".") count(str_parts) == 4 str_parts[1] == pattern } --- apiVersion: constraints.gatekeeper.sh/v1beta1 kind: K8sRequiredIngress metadata: name: ns-must-have-gk spec: match: kinds: - apiGroups: [""] kinds: ["Ingress"] --- apiVersion: config.gatekeeper.sh/v1alpha1 kind: Config metadata: name: config namespace: "gatekeeper-system" spec: sync: syncOnly: - group: "" version: "v1" kind: "Namespace"
- 部署上述配置时持续返回报错:
kubectl apply -f constraint_template.yaml Error from server: error when creating "constraint_template.yaml": admission webhook "validation.gatekeeper.sh" denied the request: invalid ConstraintTemplate: invalid data references: check refs failed on module {template}: errors (2): disallowed ref data.kubernetes.namespaces[ns].labels.project disallowed ref data.kubernetes.namespaces[ns].labels.project
报错原因
报错由三个配置问题共同导致:
- 动态索引缓存被安全规则拦截:Gatekeeper默认禁止在Rego规则中直接用动态变量作为键,索引本地同步缓存的集群资源。配置中
data.kubernetes.namespaces[ns].labels.project的ns是从准入请求动态获取的变量,直接作为键索引命名空间列表,触发内置校验拒绝。 - 缓存引用路径错误:
data.kubernetes是Gatekeeper极早期版本的资源缓存路径,新版本统一将同步的集群资源放在data.inventory路径下,旧路径默认不生效。 - 资源匹配规则错误:Ingress资源不属于核心API组(
""),Constraint中配置的apiGroups参数错误,即使模板校验通过,也无法匹配到实际的Ingress资源。另外Rego中取请求对象的路径有误,Gatekeeper封装后的准入请求对象存放在input.review路径下,而非原生AdmissionReview的input.request路径。
修复方案
1. 修正ConstraintTemplate配置
将动态索引命名空间的逻辑改为遍历匹配,修正缓存路径和请求对象路径,补全域名后缀校验逻辑,修正后的配置如下:
apiVersion: templates.gatekeeper.sh/v1 kind: ConstraintTemplate metadata: name: k8srequiredingress spec: crd: spec: names: kind: K8sRequiredIngress validation: openAPIV3Schema: type: object properties: rootDomain: type: string targets: - target: admission.k8s.gatekeeper.sh rego: | package k8srequiredingress operations := {"CREATE", "UPDATE"} ns := input.review.object.metadata.namespace # 遍历同步的命名空间缓存,匹配当前请求所属命名空间(替代动态键索引) ns_obj := data.inventory.cluster["v1"].Namespace[_] ns_obj.metadata.name == ns project := ns_obj.metadata.labels.project root_domain := input.parameters.rootDomain # 校验命名空间必须存在project标签 violation[{"msg": msg}] { input.review.kind.kind == "Ingress" input.review.kind.group == "networking.k8s.io" not project msg := sprintf("Ingress denied as namespace '%v' is missing required 'project' label", [ns]) } # 校验所有Ingress域名符合规则 violation[{"msg": msg}] { input.review.kind.kind == "Ingress" input.review.kind.group == "networking.k8s.io" operations[input.review.operation] project host := input.review.object.spec.rules[_].host not fqdn_matches(host, project, root_domain) msg := sprintf("Invalid ingress host '%v', must follow format *.%v.%v", [host, project, root_domain]) } # 域名匹配逻辑:拆分后段数匹配,项目标识、根域名符合预期 fqdn_matches(host, expected_project, root_domain) { domain_parts := split(root_domain, ".") host_parts := split(host, ".") # 计算总段数:服务名 + 项目标识 + 根域名段数 count(host_parts) == count(domain_parts) + 2 # 校验项目标识位置正确 host_parts[1] == expected_project # 校验根域名后缀匹配 suffix := array.slice(host_parts, 2, count(host_parts)) suffix == domain_parts }
2. 修正Constraint匹配规则
将apiGroups改为Ingress实际所属的networking.k8s.io,同时传入根域名参数:
apiVersion: constraints.gatekeeper.sh/v1beta1 kind: K8sRequiredIngress metadata: name: require-ingress-host-match-project spec: match: kinds: - apiGroups: ["networking.k8s.io"] kinds: ["Ingress"] parameters: rootDomain: "example.com"
3. 确认Config同步配置
原有Namespace同步配置无需修改,部署后等待Gatekeeper完成命名空间缓存同步即可正常生效。
补充落地方案
仅靠Gatekeeper准入校验属于第一道管控防线,建议增加两层兜底避免规则被绕过:
- 在Nginx反向代理层新增校验规则,判断请求Host头与后端服务所在命名空间的project标签是否匹配,不符合规则的请求直接返回403
- 限制普通用户创建LoadBalancer/NodePort类型Service的权限,所有对外暴露的服务必须通过指定的Nginx Ingress出口,避免用户绕开反向代理直接暴露服务
- 若使用ExternalDNS做域名自动管理,可给每个项目命名空间配置固定的域名后缀注解,强制自动生成的DNS记录符合命名规则,减少人工配置错误
内容的提问来源于stack exchange,提问作者tobias
相关产品推荐
相关产品推荐

