跳转至主要内容

Admission Webhook for K8s

1. Casbin K8s-Gatekeeper的概述和文档

Casbin K8s-GateKeeper是一个将Casbin集成为访问控制工具的Kubernetes准入webhook。 通过使用Casbin K8s-GateKeeper,您可以建立灵活的规则来授权或拦截对K8s资源的任何操作,无需编写任何代码,只需几行Casbin模型和策略的声明性配置,这些配置是Casbin ACL(访问控制列表)语言的一部分。

Casbin K8s-GateKeeper由Casbin社区开发和维护。 该项目的仓库可在此处获取:https://github.com/casbin/k8s-gatekeeper

0.1 一个简单的例子

例如,您不需要编写任何代码,只需使用以下几行配置就可以实现这个功能:“禁止在任何部署中使用带有某些指定标签的图像”:

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)

And Policy:

p, "1.14.1",deny

这些都是普通的Casbin ACL语言。 假设你已经阅读了关于它们的章节,理解起来会非常容易。

Casbin K8s-Gatekeeper有以下优点:

  • 易于使用。 编写几行ACL远比编写大量代码要好。
  • 它允许热更新配置。 您不需要关闭整个插件来修改配置。
  • 它是灵活的。 可以在任何K8s资源上制定任意规则,可以使用kubectl gatekeeper进行探索。
  • 它简化了K8s准入webhook的实现,这是非常复杂的。 你不需要知道K8s准入webhook是什么,也不需要知道如何为它编写代码。 你需要做的只是知道你想要约束的资源,然后编写Casbin ACL。 大家都知道K8s是复杂的,但是通过使用Casbin K8s-Gatekeeper,你的时间可以得到节省。
  • 它由Casbin社区维护。 如果你对这个插件有任何疑问,或者在尝试使用时遇到任何问题,随时联系我们。

1.1 Casbin K8s-Gatekeeper是如何工作的?

K8s-Gatekeeper是一个K8s的准入webhook,它使用Casbin应用任意用户定义的访问控制规则,以帮助防止管理员不希望的任何K8s操作。

Casbin是一个强大且高效的开源访问控制库。 它提供了基于各种访问控制模型进行授权的支持。 有关Casbin的更多详细信息,请参见概述

K8s中的准入webhooks是接收'准入请求'并对其进行处理的HTTP回调。 特别地,K8s-Gatekeeper是一种特殊类型的准入webhook:'ValidatingAdmissionWebhook',它可以决定是否接受或拒绝这个准入请求。 至于准入请求,它们是描述在K8s指定资源上的操作的HTTP请求(例如,创建/删除部署)。 关于准入webhooks的更多信息,请参阅K8s官方文档

1.2 一个说明其工作方式的例子

例如,当有人想要创建一个包含运行nginx的pod的部署(使用kubectl或K8s客户端),K8s将生成一个准入请求,如果将其转换为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可以检测到存储在K8s的etcd中的所有Casbin执行器,这些执行器是由用户创建和维护的(通过kubectl或我们提供的Go客户端)。 每个执行器都包含一个Casbin模型和一个Casbin策略。 准入请求将由每个执行器逐一处理,只有通过所有执行器的请求才能被这个K8s-Gatekeeper接受。

(如果你不明白什么是Casbin执行器、模型或策略,请参阅这个文档:入门)。

例如,出于某种原因,管理员想要禁止'image:nginx:1.14.1'的出现,而允许'nginx:1.3.1'。 可以创建包含以下规则和策略的执行器(我们将在后面的章节中解释如何创建执行器,这些模型和策略是什么,以及如何编写它们)。

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

Policy:

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

通过创建包含上述模型和策略的执行器,之前的准入请求将被这个执行器拒绝,这意味着K8s不会创建这个部署。

2 安装K8s-gatekeeper

有三种可用的安装K8s-gatekeeper的方法:外部webhook,内部webhook和Helm。

备注

注意:这些方法仅供用户尝试K8s-gatekeeper使用,并不安全。 如果你希望在生产环境中使用它,请确保你阅读了第5章。 高级设置并在安装前做出任何必要的修改。

2.1 内部webhook

2.1.1 第一步:构建镜像

对于内部webhook方法,webhook本身将作为Kubernetes内的一个服务实现。 要创建必要的服务和部署,你需要构建一个K8s-gatekeeper的镜像。 你可以通过运行以下命令来构建你自己的镜像:

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

这个命令将创建一个名为'k8s-gatekeeper:latest'的本地镜像。

备注

注意:如果你正在使用minikube,请在运行'docker build'之前执行eval $(minikube -p minikube docker-env)

2.1.2 第二步:为K8s-gatekeeper设置服务和部署

运行以下命令:

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

这将开始运行K8s-gatekeeper,你可以通过运行kubectl get pods来确认这一点。

2.1.3 第三步:为K8s-gatekeeper安装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 将像访问常规网站一样访问 K8s-gatekeeper。 Kubernetes 有一个强制要求,即 admission webhook 必须是 HTTPS。 为了尝试 K8s-gatekeeper,我们提供了一套证书和私钥(尽管这不安全)。 如果你更喜欢使用自己的证书,请参考第5章。 高级设置 以获取调整证书和私钥的说明。

我们提供的证书是为 'webhook.domain.local' 发布的。 因此,修改主机(例如,/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 安装 K8s-gatekeeper

2.3.1 第一步:构建镜像

请参考第2.1.1章

2.3.2 Helm 安装

运行命令 helm install k8sgatekeeper ./k8sgatekeeper

3. 尝试 K8s-gatekeeper

3.1 创建 Casbin 模型和策略

你有两种方法来创建模型和策略:通过 kubectl 或通过我们提供的 go-client。

3.1.1 通过 kubectl 创建/更新 Casbin 模型和策略

在 K8s-gatekeeper 中,Casbin 模型存储在一个名为 'CasbinModel' 的 CRD 资源中。 其定义位于 config/auth.casbin.org_casbinmodels.yaml 中。

example/allowed_repo/model.yaml 中有示例。 注意以下字段:

  • metadata.name:模型的名称。 此名称必须与与此模型相关的 CasbinPolicy 对象的名称相同,以便 K8s-gatekeeper 可以将它们配对并创建一个执行器。
  • spec.enable:如果此字段设置为“false”,则将忽略此模型(以及与此模型相关的 CasbinPolicy 对象)。
  • spec.modelText:包含 Casbin 模型的模型文本的字符串。

Casbin 策略存储在另一个名为 'CasbinPolicy' 的 CRD 资源中,其定义可以在 config/auth.casbin.org_casbinpolicies.yaml 中找到。

example/allowed_repo/policy.yaml 中有示例。 请注意以下字段:

  • metadata.name:策略的名称。 此名称必须与与此策略相关的CasbinModel对象的名称相同,以便K8s-gatekeeper可以将它们配对并创建一个执行器。
  • spec.policyItem:包含Casbin模型的策略文本的字符串。

创建自己的CasbinModel和CasbinPolicy文件后,使用以下命令应用它们:

kubectl apply -f <filename>

一旦创建了一对CasbinModel和CasbinPolicy,K8s-gatekeeper将能够在5秒内检测到它。

3.1.2 通过我们提供的go-client创建/更新Casbin模型和策略

我们理解可能会有一些情况,不方便直接在K8s集群的节点上使用shell执行命令,比如当你正在为你的公司构建一个自动化的云平台。 因此,我们开发了一个go-client来创建和维护CasbinModel和CasbinPolicy。

go-client库位于pkg/client

client.go中,我们提供了一个创建客户端的函数。

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

externalClient参数决定了K8s-gatekeeper是在K8s集群内部运行还是在外部运行。

model.go中,我们提供了各种函数来创建、删除和修改CasbinModel。 你可以在model_test.go中找到如何使用这些接口。

policy.go中,我们提供了各种函数来创建、删除和修改CasbiPolicy。 你可以在policy_test.go中找到如何使用这些接口。

3.1.2 尝试K8s-gatekeeper是否工作

假设你已经在example/allowed_repo中创建了精确的模型和策略。 现在,尝试以下命令:

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

你应该发现K8s会拒绝这个请求,并提到webhook是拒绝这个请求的原因。 然而,当你尝试应用example/allowed_repo/testcase/approve_2.yaml时,它将被接受。

4. 如何使用K8s-gatekeeper编写模型和策略

首先,确保你熟悉Casbin模型和策略的基本语法。 如果你不熟悉,请先阅读入门部分。 在本章中,我们假设你已经理解了Casbin模型和策略是什么。

4.1 模型的请求定义

当K8s-gatekeeper正在授权一个请求时,输入总是一个对象:Admission Request的Go对象。 这意味着执行器总是这样使用的:

ok, err := enforcer.Enforce(admission)

其中admission是一个由K8s的官方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 中的表达式求值器不支持在映射或数组(切片)中进行索引,也不支持数组的扩展。 因此,K8s-gatekeeper 提供了各种 'Casbin 函数' 作为扩展来实现这些功能。 如果您发现这些扩展仍无法满足您的需求,随时可以提出问题,或创建一个拉取请求。

如果您不熟悉 Casbin 函数,可以参考 Function 以获取更多信息。

以下是扩展函数:

4.2.1 扩展函数

4.2.1.1 access

Access 用于解决 Casbin 不支持在映射或数组中进行索引的问题。 示例 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 是一个映射,Value() 是一个没有参数并返回单个值的简单函数。

4.2.1.2 accessWithWildcard

有时,您可能有这样的需求:数组中的所有元素都必须有一个前缀 "aaa"。 然而,Casbin 不支持 for 循环。 通过 accessWithWildcard 和 "map/slice expansion" 功能,您可以轻松实现这样的需求。

例如,假设 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 的表达式求值器中,当一个参数是数组时,它将被自动扩展为一个变长参数。 利用这个特性来支持数组/切片/映射的扩展,我们还集成了几个接受数组/切片作为参数的函数:

  • contain(): 接受多个参数并返回任何参数(除最后一个参数外)是否等于最后一个参数。
  • split(a,b,c...,sep,index): 返回一个包含 [splits(a,sep)[index], splits(b,sep)[index], splits(a,sep)[index], ...] 的切片。
  • len(): 返回可变长度参数的长度。
  • matchRegex(a,b,c...,regex): 返回所有给定参数(abc,...) 是否匹配给定的正则表达式。

这是 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"],因为 splits 支持可变长度参数并对每个元素执行 splits 操作,所以将选择并返回索引 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 的声明时,类型为 XXX 的对象)。
  • IsNil(): 返回参数是否为 nil。

5. 高级设置

5.1 关于证书

在 Kubernetes(k8s)中,webhook 必须使用 HTTPS 是强制性的。 有两种方法可以实现这一点:

  • 使用自签名证书(此存储库中的示例使用此方法)
  • 使用普通证书

5.1.1 自签名证书

使用自签名证书意味着发行证书的证书颁发机构(CA)不是众所周知的 CA。 因此,你必须让 k8s 知道这个 CA。

目前,此存储库中的示例使用自制的 CA,其私钥和证书分别存储在 config/certificate/ca.crtconfig/certificate/ca.key 中。 webhook 的证书是 config/certificate/server.crt,由自制的 CA 颁发。 此证书的域名是 "webhook.domain.local"(用于外部 webhook)和 "casbin-webhook-svc.default.svc"(用于内部 webhook)。

通过 webhook 配置文件将 CA 的信息传递给 k8s。 config/webhook_external.yamlconfig/webhook_internal.yaml 都有一个名为 "CABundle" 的字段,其中包含 CA 证书的 base64 编码字符串。

如果你需要更改证书/域名(例如,如果你想将此 webhook 放入 k8s 的另一个命名空间,同时使用内部 webhook,或者如果你想在使用外部 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=<The domain you want>

        注意:如果您决定修改服务名称,请用您自己服务的真实服务名称替换'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. 替换'CABundle'字段:config/webhook_external.yamlconfig/webhook_internal.yaml都有一个名为"CABundle"的字段,其中包含CA的证书的base64编码字符串。 用新证书更新此字段。

  5. 如果您正在使用helm,需要对helm图表应用类似的更改。

5.1.2 合法证书

如果您使用合法证书,您不需要经历所有这些程序。 移除config/webhook_external.yamlconfig/webhook_internal.yaml中的"CABundle"字段,并将这些文件中的域更改为您拥有的域。