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 或更换域名):
-
生成新的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
-
-
用 base64 编码的新 CA 证书更新
config/webhook_external.yaml和config/webhook_internal.yaml中的 'CABundle' 字段。 -
如果用 Helm 部署,对 Helm chart 做同样的修改。
5.1.2 公开受信任的证书
使用公开受信任的证书时,跳过上面的步骤。删除 config/webhook_external.yaml 和 config/webhook_internal.yaml 中的 "CABundle" 字段,并把域名设为你注册的域名。