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 移到其他命名空间,或者更换域名):
-
生成新的 CA:
-
生成 CA 私钥:
openssl genrsa -des3 -out ca.key 2048 -
去除密码保护:
openssl rsa -in ca.key -out ca.key
-
-
生成 webhook 服务端私钥:
openssl genrsa -des3 -out server.key 2048
openssl rsa -in server.key -out server.key -
用 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
-
-
更新
config/webhook_external.yaml和config/webhook_internal.yaml中的 'CABundle' 字段,填入 base64 编码后的新 CA 证书。 -
对于 Helm 部署,请对 Helm chart 做相应的修改。
5.1.2 公开可信的证书
使用公开可信的证书时,可以跳过上述步骤。删除 config/webhook_external.yaml 和 config/webhook_internal.yaml 中的 "CABundle" 字段,并把域名设置为你注册的域名即可。