跳转到主要内容

Kubernetes 准入 Webhook

概述​

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

基础示例​

示例:仅通过配置,拒绝使用特定 tag 镜像的 deployment:

模型:

[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)

策略:

p, "1.14.1",deny

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

Casbin K8s-Gatekeeper 有以下几个优点:

  • 使用简单——编写 ACL 配置,而不是编写大量代码
  • 支持热更新配置,无需重启插件
  • 灵活——可以对任意 Kubernetes 资源应用任意规则,配合 kubectl gatekeeper 使用
  • 简化了 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 资源执行操作的 HTTP 请求(例如创建或删除一个 deployment)。更多信息请参阅 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(这些 enforcer 由用户通过 kubectl 或我们提供的 Go 客户端创建和维护)。每个 enforcer 都包含一个 Casbin 模型和策略。准入请求会依次由每个 enforcer 进行评估,必须全部通过才会被接受。

(如果你还不熟悉 Casbin 的 enforcer、模型或策略,请参阅快速上手。)

例如,如果管理员想阻止 'nginx:1.14.1' 镜像,同时允许 'nginx:1.3.1',他们可以使用下面的模型和策略创建一个 enforcer(创建和配置的细节将在后续章节中介绍):

模型:

[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

只要在 [matchers] 部分保持一致地使用,'obj' 这个名字可以随意更改。

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 是一个 slice。

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​

如果想检查数组中所有元素是否满足某个条件(例如"所有元素都必须以 'aaa' 开头"),而又没有 for 循环可用,可以使用 accessWithWildcard 配合 map/slice 展开来实现。

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

支持使用多个通配符。例如,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 的表达式求值器会自动把数组展开为可变参数。利用这一特性来实现数组/slice/map 的展开,以下几个函数接受数组/slice:

  • 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(): 把整数转换为浮点数(因为比较运算需要 float 类型)。
  • 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 移到其他命名空间,或者更换域名):

  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. 更新 config/webhook_external.yaml 和 config/webhook_internal.yaml 中的 'CABundle' 字段,填入 base64 编码后的新 CA 证书。

  5. 对于 Helm 部署,请对 Helm chart 做相应的修改。

5.1.2 公开可信的证书​

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