跳转至主要内容

Kubernetes Admission Webhook

概述​

Casbin K8s-Gatekeeper 是一个使用 Casbin 做授权的 Kubernetes 准入 webhook。你以声明式的方式定义模型和策略,允许或拒绝对任意 Kubernetes 资源的操作,webhook 中无需编写自定义代码。由 Casbin 社区维护:github.com/apache/casbin-k8s-gatekeeper。

基本示例​

示例:只用配置就禁止使用特定标签镜像的 deployment:

Model:

[request_definition]
r = obj

[policy_definition]
p = obj,eft

[policy_effect]
e = !some(where (p.eft == deny))

[matchers]
m = r.obj.Request.Namespace == "default" && r.obj.Request.Resource.Resource =="deployments" && \
contain(split(accessWithWildcard(${OBJECT}.Spec.Template.Spec.Containers , "*", "Image"),":",1) , p.obj)

Policy:

p, "1.14.1",deny

这里使用标准的 Casbin ACL 语言,读过前面的入门章节应该很容易理解。

Casbin K8s-Gatekeeper 有以下优点:

  • 易于使用:写 ACL 配置,而不是大量代码
  • 支持在线更新配置,无需重启插件
  • 灵活:用 kubectl gatekeeper 对任意 Kubernetes 资源应用任意规则
  • 简化了 Kubernetes 准入 webhook 的实现:不需要了解 webhook 内部原理,也不需要编写 webhook 代码。只需定义约束并编写 Casbin ACL。
  • 由社区维护:有问题或建议请联系我们

1.1 Casbin K8s-Gatekeeper 的工作原理​

K8s-Gatekeeper 是一个 Kubernetes 准入 webhook,使用 Casbin 实施自定义的访问控制规则,阻止对 Kubernetes 资源的不期望的操作。

Casbin 是一个高效的开源访问控制库,支持多种授权模型。详见 概览。

Kubernetes 中的准入 webhook 是接收并处理准入请求的 HTTP 回调。 K8s-Gatekeeper 是一个 ValidatingAdmissionWebhook,负责接受或拒绝准入请求。准入请求是描述对 Kubernetes 资源进行操作(例如创建或删除 deployment)的 HTTP 请求。更多信息见 Kubernetes 文档。

1.2 示例流程​

当有人(通过 kubectl 或 Kubernetes 客户端)创建一个包含 nginx pod 的 deployment 时,Kubernetes 会生成如下的准入请求(YAML 格式):

apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
spec:
selector:
matchLabels:
app: nginx
replicas: 1
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.14.1
ports:
- containerPort: 80

这个请求会经过多层中间件,其中包括 K8s-Gatekeeper。 K8s-Gatekeeper 会找出 Kubernetes etcd 中存储的所有 Casbin enforcer(由用户通过 kubectl 或提供的 Go 客户端创建和维护)。每个 enforcer 包含一个 Casbin 模型和策略。准入请求依次由每个 enforcer 评估,必须通过所有 enforcer 才会被接受。

(如果你不熟悉 Casbin 的 enforcer、模型或策略,见 快速开始。)

例如,管理员想禁止 'nginx:1.14.1' 镜像、同时允许 'nginx:1.3.1',可以用这样的模型和策略创建一个 enforcer(创建和配置的细节在后面的章节中介绍):

Model:

[request_definition]
r = obj

[policy_definition]
p = obj,eft

[policy_effect]
e = !some(where (p.eft == deny))

[matchers]
m = r.obj.Request.Namespace == "default" && r.obj.Request.Resource.Resource =="deployments" && \
access(r.obj.Request.Object.Object.Spec.Template.Spec.Containers , 0, "Image") == p.obj

策略:

p, "nginx:1.13.1",allow
p, "nginx:1.14.1",deny

用这个模型和策略创建 enforcer 后,上面的准入请求会被拒绝,Kubernetes 就不会创建这个 deployment。

2. 安装 K8s-gatekeeper​

有三种安装方式:外部 webhook、内部 webhook 和 Helm。

备注

这些安装方式仅用于评估试用。生产环境部署前,请阅读 第 5 章高级设置,并在安装前做好必要的安全调整。

2.1 内部 webhook​

2.1.1 第 1 步:构建镜像​

以内部 webhook 方式部署时,K8s-gatekeeper 作为 Kubernetes 服务运行。构建镜像:

docker build --target webhook -t k8s-gatekeeper .

这会生成一个名为 'k8s-gatekeeper:latest' 的本地镜像。

备注

minikube 用户在 'docker build' 之前先运行 eval $(minikube -p minikube docker-env)。

2.1.2 第 2 步:部署服务和资源​

运行以下命令:

kubectl apply -f config/rbac.yaml
kubectl apply -f config/webhook_deployment.yaml
kubectl apply -f config/webhook_internal.yaml

用 kubectl get pods 验证部署。

2.1.3 第 3 步:安装 CRD 资源​

安装自定义资源定义:

kubectl apply -f config/auth.casbin.org_casbinmodels.yaml 
kubectl apply -f config/auth.casbin.org_casbinpolicies.yaml

2.2 外部 webhook​

以外部 webhook 方式部署时,K8s-gatekeeper 在 Kubernetes 外部运行。 Kubernetes 要求准入 webhook 使用 HTTPS。我们提供了测试用的证书和私钥(不适合生产环境)。使用自定义证书,见 第 5 章高级设置。

提供的证书签发给 'webhook.domain.local'。修改 hosts 文件(例如 /etc/hosts),把 'webhook.domain.local' 指向运行 K8s-gatekeeper 的 IP 地址。

执行:

go mod tidy
go mod vendor
go run cmd/webhook/main.go
kubectl apply -f config/auth.casbin.org_casbinmodels.yaml
kubectl apply -f config/auth.casbin.org_casbinpolicies.yaml
kubectl apply -f config/webhook_external.yaml

2.3 通过 Helm 安装​

2.3.1 第 1 步:构建镜像​

见 第 2.1.1 节。

2.3.2 用 Helm 安装​

运行:helm install k8sgatekeeper ./k8sgatekeeper

3. 使用 K8s-gatekeeper​

3.1 创建 Casbin 模型和策略​

可以用 kubectl 或提供的 Go 客户端创建模型和策略。

3.1.1 通过 kubectl 创建 / 更新​

在 K8s-gatekeeper 中,Casbin 模型以 'CasbinModel' CRD 资源的形式存储。定义在 config/auth.casbin.org_casbinmodels.yaml 中。

示例见 example/allowed_repo/model.yaml。重要字段:

  • metadata.name:模型名称。必须与关联的 CasbinPolicy 对象同名,K8s-gatekeeper 才能正确配对。
  • spec.enable:设为 "false" 可以禁用这个模型及其关联的策略。
  • spec.modelText:包含 Casbin 模型定义的字符串。

Casbin 策略以 'CasbinPolicy' CRD 资源的形式存储,定义在 config/auth.casbin.org_casbinpolicies.yaml 中。

示例见 example/allowed_repo/policy.yaml。重要字段:

  • metadata.name:策略名称。必须与关联的 CasbinModel 对象同名。
  • spec.policyItem:包含 Casbin 策略定义的字符串。

应用你的 CasbinModel 和 CasbinPolicy 文件:

kubectl apply -f <filename>

K8s-gatekeeper 会在 5 秒内发现新的 CasbinModel/CasbinPolicy 对。

3.1.2 通过 Go 客户端创建 / 更新​

在不方便直接用 shell 访问集群节点的场景下(例如构建自动化的云平台),我们提供了一个 Go 客户端来管理 CasbinModel 和 CasbinPolicy 资源。

Go 客户端库在 pkg/client 中。

在 client.go 中,用这个函数创建客户端:

func NewK8sGateKeeperClient(externalClient bool) (*K8sGateKeeperClient, error) 

externalClient 参数表示 K8s-gatekeeper 运行在 Kubernetes 集群内部还是外部。

model.go 中提供了创建、删除和修改 CasbinModel 的函数。用法示例见 model_test.go。

policy.go 中提供了创建、删除和修改 CasbinPolicy 的函数。用法示例见 policy_test.go。

3.1.2 测试 K8s-gatekeeper​

用 example/allowed_repo 创建模型和策略后,这样测试:

kubectl apply -f example/allowed_repo/testcase/reject_1.yaml

Kubernetes 应当拒绝这个请求,并说明拒绝原因来自 webhook。而应用 example/allowed_repo/testcase/approve_2.yaml 应当成功。

4. 为 K8s-gatekeeper 编写模型和策略​

继续之前,请确保你已经了解 Casbin 的模型和策略语法。如果还不了解,先阅读 快速开始。本章假设你已经熟悉 Casbin 的模型和策略。

4.1 请求定义​

K8s-gatekeeper 评估请求时,输入始终是一个 AdmissionReview Go 对象。 enforcer 的用法如下:

ok, err := enforcer.Enforce(admission)

其中 admission 是 Kubernetes 官方 Go API "k8s.io/api/admission/v1" 中的 AdmissionReview 对象。结构体定义见:https://github.com/kubernetes/api/blob/master/admission/v1/types.go。更多文档:https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#webhook-request-and-response。

对于 K8s-gatekeeper 的模型,request_definition 应始终采用以下格式:

    [request_definition]
r = obj

名称 'obj' 可以修改,只要在 [matchers] 部分中保持一致即可。

4.2 模型匹配器​

用 Casbin 的 ABAC 功能编写规则。不过,Casbin 的表达式求值器原生不支持 map/数组下标访问和数组展开。 K8s-gatekeeper 提供了扩展函数来解决这个问题。如果需要更多功能,请提 issue 或提交 pull request。

Casbin 函数的背景知识见 函数。

扩展函数:

4.2.1 扩展函数​

4.2.1.1 access​

access 函数支持 map 和数组的下标访问。见 example/allowed_repo/model.yaml:

[matchers]
m = r.obj.Request.Namespace == "default" && r.obj.Request.Resource.Resource =="deployments" && \
access(r.obj.Request.Object.Object.Spec.Template.Spec.Containers , 0, "Image") == p.obj

这里 access(r.obj.Request.Object.Object.Spec.Template.Spec.Containers , 0, "Image") 等价于 r.obj.Request.Object.Object.Spec.Template.Spec.Containers[0].Image,其中 r.obj.Request.Object.Object.Spec.Template.Spec.Containers 是一个切片。

access 还可以调用返回单个值的无参函数。见 example/container_resource_limit/model.yaml:

[matchers]
m = r.obj.Request.Namespace == "default" && r.obj.Request.Resource.Resource =="deployments" && \
parseFloat(access(r.obj.Request.Object.Object.Spec.Template.Spec.Containers , 0, "Resources","Limits","cpu","Value")) >= parseFloat(p.cpu) && \
parseFloat(access(r.obj.Request.Object.Object.Spec.Template.Spec.Containers , 0, "Resources","Limits","memory","Value")) >= parseFloat(p.memory)

这里 access(r.obj.Request.Object.Object.Spec.Template.Spec.Containers , 0, "Resources","Limits","cpu","Value") 等于 r.obj.Request.Object.Object.Spec.Template.Spec.Containers[0].Resources.Limits["cpu"].Value(),其中 r.obj.Request.Object.Object.Spec.Template.Spec.Containers[0].Resources.Limits 是一个 map,Value() 是返回单个值的无参函数。

4.2.1.2 accessWithWildcard​

要在不用 for 循环的情况下检查数组所有元素的条件(例如“所有元素都必须以 'aaa' 开头”),使用带 map/切片展开的 accessWithWildcard。

如果 a.b.c 是数组 [aaa,bbb,ccc,ddd,eee],那么 accessWithWildcard(a,"b","c","*") 返回切片 [aaa,bbb,ccc,ddd,eee]。通配符 * 会展开切片。

支持多个通配符。例如 accessWithWildcard(a,"b","c","*","*") 返回 [a.b.c[0][0], a.b.c[0][1], ..., a.b.c[1][0], a.b.c[1][1], ...]。

4.2.1.3 可变长参数函数​

Casbin 的表达式求值器会自动把数组展开为可变长参数。利用这一点做数组 / 切片 / map 展开,以下几个函数可以接受数组 / 切片:

  • contain():接受多个参数,返回除最后一个外是否有参数等于最后一个参数。
  • split(a,b,c...,sep,index):返回 [splits(a,sep)[index], splits(b,sep)[index], splits(c,sep)[index], ...]。
  • len():返回可变长参数的个数。
  • matchRegex(a,b,c...,regex):返回所有参数(a、b、c……)是否都匹配该正则。

来自 example/disallowed_tag/model.yaml 的示例:

    [matchers]
m = r.obj.Request.Namespace == "default" && r.obj.Request.Resource.Resource =="deployments" && \
contain(split(accessWithWildcard(r.obj.Request.Object.Object.Spec.Template.Spec.Containers , "*", "Image"),":",1) , p.obj)

如果 accessWithWildcard(r.obj.Request.Object.Object.Spec.Template.Spec.Containers , "*", "Image") 返回 ["a:b", "c:d", "e:f", "g:h"],split 会借助可变长参数逐个处理每个元素,从每个元素中取下标 1。因此 split(accessWithWildcard(r.obj.Request.Object.Object.Spec.Template.Spec.Containers , "*", "Image"),":",1) 返回 ["b","d","f","h"]。最后,contain(split(accessWithWildcard(r.obj.Request.Object.Object.Spec.Template.Spec.Containers , "*", "Image"),":",1) , p.obj) 检查 p.obj 是否在 ["b","d","f","h"] 中。

4.2.1.2 类型转换函数​

  • ParseFloat():把整数转换为浮点数(比较需要浮点类型)。
  • ToString():把对象转换为字符串。对象的底层类型必须是字符串(例如 type XXX string)。
  • IsNil(): 返回参数是否为 nil。

5. 高级设置​

5.1 证书配置​

Kubernetes 要求 webhook 使用 HTTPS。有两种选择:

  • 自签名证书(本仓库示例使用这种)
  • 公开受信任的证书

5.1.1 自签名证书​

自签名证书使用一个不被公开认可的证书颁发机构(CA)。必须配置 Kubernetes 信任这个 CA。

仓库示例使用一个自定义 CA,其私钥和证书保存在 config/certificate/ca.key 和 config/certificate/ca.crt 中。 webhook 证书 config/certificate/server.crt 由这个 CA 为域名 "webhook.domain.local"(外部 webhook)和 "casbin-webhook-svc.default.svc"(内部 webhook)签发。

CA 信息通过 webhook 配置文件传给 Kubernetes。 config/webhook_external.yaml 和 config/webhook_internal.yaml 中都有一个 "CABundle" 字段,内容是 base64 编码的 CA 证书。

要更换证书 / 域名(例如把 webhook 移到另一个 namespace 或更换域名):

  1. 生成新的CA:

    • 生成 CA 私钥:

      openssl genrsa -des3 -out ca.key 2048
    • 去掉密码保护:

      openssl rsa -in ca.key -out ca.key
  2. 生成 webhook 服务器私钥:

    openssl genrsa -des3 -out server.key 2048
    openssl rsa -in server.key -out server.key
  3. 用 CA 签发 webhook 证书:

    • 复制系统的 OpenSSL 配置文件(用 openssl version -a 查找,通常是 openssl.cnf)。

    • 修改配置文件:

      • 在 [req] 部分加上:req_extensions = v3_req

      • 在 [v3_req] 部分加上:subjectAltName = @alt_names

      • 在末尾追加:

        [alt_names]
        DNS.2=<Your desired domain>

        注意:如果改了服务名,把 'casbin-webhook-svc.default.svc' 换成你实际的服务名。

    • 生成证书请求:

      openssl req -new -nodes -keyout server.key -out server.csr -config openssl.cnf
    • 用 CA 签发证书:

      openssl x509 -req -days 3650 -in server.csr -out server.crt -CA ca.crt -CAkey ca.key -CAcreateserial -extensions v3_req -extensions SAN -extfile openssl.cnf
  4. 用 base64 编码的新 CA 证书更新 config/webhook_external.yaml 和 config/webhook_internal.yaml 中的 'CABundle' 字段。

  5. 如果用 Helm 部署,对 Helm chart 做同样的修改。

5.1.2 公开受信任的证书​

使用公开受信任的证书时,跳过上面的步骤。删除 config/webhook_external.yaml 和 config/webhook_internal.yaml 中的 "CABundle" 字段,并把域名设为你注册的域名。