在 Kyverno 策略中使用来自 ConfigMap、Kubernetes API Server和 image registry 的数据。
变量部分讨论了变量如何帮助创建更智能和可重用的策略定义,并介绍了存储所有变量的规则 context 的概念。
本节提供关于在策略中使用来自 ConfigMap、Kubernetes API Server和 image registry 的数据的详细信息。
注意
为了提高安全性和性能,Kyverno 被设计为不允许连接到集群 Kubernetes API Server和 image registry以外的系统。使用单独的控制器从任何来源获取数据并将其存储在可在策略中有效使用的 ConfigMap 中。这种设计可以实现关注点分离和安全边界的实施。
来自 ConfigMap 的变量
Kubernetes 中的 ConfigMap 资源通常用作应用程序可以使用的配置详细信息的来源。这些数据可以以多种格式写入,存储在命名空间中,并且可以轻松访问。 Kyverno 支持使用 ConfigMap 作为变量的数据源。评估引用 ConfigMap 资源的策略时,会检查 ConfigMap 数据,以确保对 ConfigMap 的引用始终是动态的。如果 ConfigMap 更新,后续策略查找将在该时间点获取最新数据。
为了在 rule 中使用来自 ConfigMap 的数据,需要一个context。对于您希望使用 ConfigMap 中的数据的每个 rule,您必须定义一个 context。然后可以使用 JMESPath 表示法在策略 rule 中引用上下文数据。
查找 ConfigMap 值
在规则的 context 中定义的 ConfigMap 可以使用其在上下文中的唯一名称来引用。可以使用 JMESPath 样式表达式引用 ConfigMap 值。
{{ <context-name>.data.<key-name> }}
考虑这样一个简单的 ConfigMap 定义。
apiVersion: v1
kind: ConfigMap
metadata:
name: some-config-map
namespace: some-namespace
data:
env: production
要在 rule 内引用来自 ConfigMap 的值,请在 rule 内定义一个使用了一个或多个 ConfigMap 声明的 context。使用上面引用的示例 ConfigMap 片段,以下 rule 定义了一个按名称引用此特定 ConfigMap 的 context。
rules:
- name: example-lookup
# Define a context for the rule
context:
# A unique name for the ConfigMap
- name: dictionary
configMap:
# Name of the ConfigMap which will be looked up
name: some-config-map
# Namespace in which this ConfigMap is stored
namespace: some-namespace
基于上面的示例,我们现在可以使用 {{dictionary.data.env}} 引用 ConfigMap 值。在策略执行期间,该变量将替换为 production。
放入完整 ClusterPolicy 的上下文中,将 ConfigMap 作为变量引用如下所示。
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: cm-variable-example
annotations:
pod-policies.kyverno.io/autogen-controllers: DaemonSet,Deployment,StatefulSet
spec:
rules:
- name: example-configmap-lookup
context:
- name: dictionary
configMap:
name: some-config-map
namespace: some-namespace
match:
any:
- resources:
kinds:
- Pod
mutate:
patchStrategicMerge:
metadata:
labels:
my-environment-name: "{{dictionary.data.env}}"
在上面的 ClusterPolicy 中,一个 mutate 规则匹配所有传入的 Pod 资源,并为其添加一个名为 my-environment-name 的 label。因为我们已经定义了一个 context,它指向我们之前名为 some-config-map 的 ConfigMap,所以我们可以使用表达式 {{dictionary.data.env}} 来引用该值。一个新创建的 Pod 将会接收到 label my-environment-name=production。
注意:ConfigMap 名称和键可以包含 JMESPath 不支持的字符,例如“-”(减号或破折号)或“/”(斜杠)。要将这些字符计算为文字,请在 JMESPath 表达式的该部分添加双引号,如下所示:
{{ "<name>".data."<key>" }}
有关格式化问题的更多信息,请参阅 JMESPath 页面。
处理 ConfigMap 数组值
除了简单的字符串值之外,Kyverno 还能够使用 ConfigMap 中的数组值。
注意:自 Kyverno 1.7.0 起,将数组值存储在 YAML 块标量中已被删除。请改用 JSON 编码的字符串数组。
例如,假设您想在 ConfigMap 中定义允许的角色列表。Kyverno 策略可以引用此列表来拒绝注释中定义的角色的请求。
假设一个 ConfigMap,其内容为 YAML 多行值。
apiVersion: v1
kind: ConfigMap
metadata:
name: roles-dictionary
namespace: default
data:
allowed-roles: "[\"cluster-admin\", \"cluster-operator\", \"tenant-admin\"]"
注意:如前所述,某些字符必须转义以进行 JMESPath 处理。在这种情况下,反斜杠 ("") 字符用于转义双引号,这允许将 ConfigMap 数据存储为 JSON 数组。
现在数组数据保存在 allowed-roles 键中,下面是一个示例 ClusterPolicy,其中包含一个规则,如果名为 role 的注解的值不在允许列表中,则该规则会阻止部署:
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: cm-array-example
spec:
validationFailureAction: enforce
background: false
rules:
- name: validate-role-annotation
context:
- name: roles-dictionary
configMap:
name: roles-dictionary
namespace: default
match:
any:
- resources:
kinds:
- Deployment
validate:
message: "The role {{ request.object.metadata.annotations.role }} is not in the allowed list of roles: {{ \"roles-dictionary\".data.\"allowed-roles\" }}."
deny:
conditions:
any:
- key: "{{ request.object.metadata.annotations.role }}"
operator: NotIn
value: "{{ \"roles-dictionary\".data.\"allowed-roles\" }}"
如果在我们之前定义的名为 roles-dictionary 的 ConfigMap 的数组中找不到注解 role,则此规则拒绝新建 Deployment 的请求。
注意:您可能还会注意到,此示例在单个规则中使用了来自 AdmissionReview 和 ConfigMap 源的变量。这种组合可以证明在制定有用的策略方面非常强大和灵活。
创建此示例 ClusterPolicy 后,尝试创建注解 role=super-user 的新 Deployment 并测试结果。
apiVersion: apps/v1
kind: Deployment
metadata:
name: busybox
annotations:
role: super-user
labels:
app: busybox
spec:
replicas: 1
selector:
matchLabels:
app: busybox
template:
metadata:
labels:
app: busybox
spec:
containers:
- image: busybox:1.28
name: busybox
command: ["sleep", "9999"]
提交清单,看看 Kyverno 的反应。
$ kubectl create -f deploy.yaml
Error from server: error when creating "deploy.yaml": admission webhook "validate.kyverno.svc" denied the request:
resource Deployment/default/busybox was blocked due to the following policies
cm-array-example:
validate-role-annotation: 'The role super-user is not in the allowed list of roles: ["cluster-admin", "cluster-operator", "tenant-admin"].'
将注解 role 更改为 ConfigMap 中存在的值之一,例如 tenant-admin,允许创建 Deployment 资源。
来自 Kubernetes API Server调用的变量
Kubernetes 由允许查询和操作资源的声明性 API 提供支持。Kyverno 策略可以使用 Kubernetes API 来获取资源,甚至是资源类型的集合,以在策略中使用。此外,Kyverno 允许将 JMESPath(JSON 匹配表达式)应用于资源数据,以提取值并将其转换为易于在策略中使用的格式。
Kyverno Kubernetes API 调用与 kubectl 和其他 API 客户端一样工作,并且可以使用现有工具进行测试。
例如,下面是一个命令行,它使用 kubectl 获取命名空间中的 Pod 列表,然后将输出通过管道传输到 kyverno jp,以计算 Pod 的数量:
kubectl get --raw /api/v1/namespaces/kyverno/pods | kyverno jp "items | length(@)"
使用 kubectl get --raw 和 kyverno jp 命令来测试 API 调用。
Kyverno 中相应的 API 调用定义如下。使用一个变量 {{request.namespace}} 来使用被操作对象的 Namespace,然后同样用 JMESPath 获取 Namespace 中 Pod 的数量,并以变量 podCount 的形式存储到 context 中。
rules:
- name: example-api-call
context:
- name: podCount
apiCall:
urlPath: "/api/v1/namespaces/{{request.namespace}}/pods"
jmesPath: "items | length(@)"
URL路径
Kubernetes API 使用 group 和 version来组织资源。例如,资源类型 Deployment 的 API Group 是 apps, version 是 v1。
API 调用的 HTTP URL 路径基于group、version和资源类型,如下所示:
-
/apis/{GROUP}/{VERSION}/{RESOURCETYPE}:获取一个资源集合
-
/apis/{GROUP}/{VERSION}/{RESOURCETYPE}/{NAME}:获取一个资源
对于命名空间级别的资源,要通过名称获取特定资源或获取命名空间中的所有资源,还必须提供命名空间名称,如下所示:
-
/apis/{GROUP}/{VERSION}/namespaces/{NAMESPACE}/{RESOURCETYPE}:获取命名空间中的一组资源集合
-
/apis/{GROUP}/{VERSION}/namespaces/{NAMESPACE}/{RESOURCETYPE}/{NAME}:获取命名空间中的一个资源
对于历史资源,Kubernetes 核心 API 都在 /api/v1 下。例如,要查询所有命名空间资源,使用路径 /api/v1/namespaces。
在 v1.22 的 API 参考文档中定义了 Kubernetes API 组,也可以通过如下所示的 kubectl api-resources 命令检索:
$ kubectl api-resources
NAME SHORTNAMES APIGROUP NAMESPACED KIND
bindings true Binding
componentstatuses cs false ComponentStatus
configmaps cm true ConfigMap
endpoints ep true Endpoints
events ev true Event
limitranges limits true LimitRange
namespaces ns false Namespace
nodes no false Node
persistentvolumeclaims pvc true PersistentVolumeClaim
...
kubectl api-versions 命令打印出每个 API 组的可用版本。这是一个示例:
$ kubectl api-versions
admissionregistration.k8s.io/v1
admissionregistration.k8s.io/v1beta1
apiextensions.k8s.io/v1
apiextensions.k8s.io/v1beta1
apiregistration.k8s.io/v1
apiregistration.k8s.io/v1beta1
apps/v1
authentication.k8s.io/v1
authentication.k8s.io/v1beta1
authorization.k8s.io/v1
authorization.k8s.io/v1beta1
autoscaling/v1
autoscaling/v2beta1
autoscaling/v2beta2
batch/v1
...
您可以结合使用这些命令来查找资源的 URL 路径,如下所示:
要查找资源的 API 组和版本,请使用 kubectl api-resources 查找组,然后使用 kubectl api-versions 查找可用版本。
本示例查找 Deployment 资源组,然后查询版本:
kubectl api-resources | grep deploy
API 组显示在输出的第三列中。然后,您可以使用组名来查找版本:
kubectl api-versions | grep apps
其输出将是 apps/v1。旧版本的 Kubernetes(1.18 之前)将显示 apps/v1beta2。
处理集合
请求资源集合的 URL 路径上的 HTTP GET 的 API 服务器响应将是具有项目(资源)列表的对象。
这是一个获取所有命名空间资源的示例:
kubectl get --raw /api/v1/namespaces | jq
使用 jq 格式化输出以提高可读性。
这将返回一个 NamespaceList 对象,其属性 items 包含命名空间列表:
{
"kind": "NamespaceList",
"apiVersion": "v1",
"metadata": {
"selfLink": "/api/v1/namespaces",
"resourceVersion": "2009258"
},
"items": [
{
"metadata": {
"name": "default",
"selfLink": "/api/v1/namespaces/default",
"uid": "5011b5d5-abb7-4fef-93f9-8b5fa4b2eba9",
"resourceVersion": "155",
"creationTimestamp": "2021-01-19T20:20:37Z",
"managedFields": [
{
"manager": "kube-apiserver",
"operation": "Update",
"apiVersion": "v1",
"time": "2021-01-19T20:20:37Z",
"fieldsType": "FieldsV1",
"fieldsV1": {
"f:status": {
"f:phase": {}
}
}
}
]
},
"spec": {
"finalizers": [
"kubernetes"
]
},
"status": {
"phase": "Active"
}
},
...
要在 JMESPath 中处理此数据,请引用items。这是一个示例,它在所有命名空间资源中提取了一些元数据字段:
kubectl get --raw /api/v1/namespaces | kyverno jp "items[*].{name: metadata.name, creationTime: metadata.creationTimestamp}"
这将生成一个新的 JSON 对象列表,其中包含属性名称和创建时间。
[
{
"creationTimestamp": "2021-01-19T20:20:37Z",
"name": "default"
},
{
"creationTimestamp": "2021-01-19T20:20:36Z",
"name": "kube-node-lease"
},
...
要在列表中查找项目,您可以使用 JMESPath 过滤器。例如,此命令将按名称匹配命名空间:
kubectl get --raw /api/v1/namespaces | kyverno jp "items[?metadata.name == 'default'].{uid: metadata.uid, creationTimestamp: metadata.creationTimestamp}"
除了通配符和过滤器之外,JMESPath 还有许多其他强大的、有用的功能。请务必阅读 JMESPath 教程并尝试此处的 Kyverno JMESPath 页面之外的交互式示例。
示例策略:在命名空间中限制 LoadBalancer 类型的服务
这是一个完整的示例策略,它将限制每个命名空间只能有一个 LoadBalancer 类型的服务。
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: limits
spec:
validationFailureAction: enforce
rules:
- name: limit-lb-svc
match:
any:
- resources:
kinds:
- Service
context:
- name: serviceCount
apiCall:
urlPath: "/api/v1/namespaces/{{ request.namespace }}/services"
jmesPath: "items[?spec.type == 'LoadBalancer'] | length(@)"
preconditions:
any:
- key: "{{ request.operation }}"
operator: Equals
value: CREATE
validate:
message: "Only one LoadBalancer service is allowed per namespace"
deny:
conditions:
any:
- key: "{{ serviceCount }}"
operator: GreaterThan
value: 1
此示例策略检索命名空间中的 Service列表,并将 LoadBalancer 类型的 Service 计数,并存储在名为 serviceCount 的变量中。deny 规则用于确保计数不能超过 1。
来自 Image Registry 的变量
通过使用 imageRegistry context 类型,context也可以使用 OCI 镜像上的元数据。通过使用此外部数据源,Kyverno 策略可以根据作为传入资源的一部分出现的容器镜像的详细信息做出决策。
例如,如果您使用如下所示的 imageRegistry:
context:
- name: imageData
imageRegistry:
reference: "ghcr.io/kyverno/kyverno"
输出 imageData 变量将具有如下结构:
{
"image": "ghcr.io/kyverno/kyverno",
"resolvedImage": "ghcr.io/kyverno/kyverno@sha256:17bfcdf276ce2cec0236e069f0ad6b3536c653c73dbeba59405334c0d3b51ecb",
"registry": "ghcr.io",
"repository": "kyverno/kyverno",
"identifier": "latest",
"manifest": manifest,
"configData": config,
}
注意
imageData 代表了一个镜像在 registry 执行任何重定向并由 Kyverno 进行内部修改之后的“归一化”的视图(Kyverno 默认将一个空注册表设置为 docker.io 并将一个空标签设置为 latest)。最值得注意的是,这会影响托管在 Docker Hub 上的官方镜像。Docker Hub 上的官方镜像与其他镜像的区别在于它们的存储库以 library/ 为前缀,即使被拉取的镜像不包含它。例如,使用 python:slim 拉取 python 官方图像会导致设置 imageData 的以下字段:
{
"image": "docker.io/python:slim",
"resolvedImage": "index.docker.io/library/python@sha256:43705a7d3a22c5b954ed4bd8db073698522128cf2aaec07690a34aab59c65066",
"registry": "index.docker.io",
"repository": "library/python",
"identifier": "slim"
}
manifest 和 config 分别包含来自 crane manifest <image> 和 crane config <image> 的输出。
例如,可以检查给定图像的labels、entrypoint、volumes、history、layers、etc 等。使用工具 crane,显示镜像 ghcr.io/kyverno/kyverno:latest 的配置信息:
$ crane config ghcr.io/kyverno/kyverno:latest | jq
{
"architecture": "amd64",
"config": {
"User": "10001",
"Env": [
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
],
"Entrypoint": [
"./kyverno"
],
"WorkingDir": "/",
"Labels": {
"maintainer": "Kyverno"
},
"OnBuild": null
},
"created": "2022-02-04T08:57:38.818583756Z",
"history": [
{
"created": "2022-02-04T08:57:38.454742161Z",
"created_by": "LABEL maintainer=Kyverno",
"comment": "buildkit.dockerfile.v0",
"empty_layer": true
},
{
"created": "2022-02-04T08:57:38.454742161Z",
"created_by": "COPY /output/kyverno / # buildkit",
"comment": "buildkit.dockerfile.v0"
},
{
"created": "2022-02-04T08:57:38.802069102Z",
"created_by": "COPY /etc/passwd /etc/passwd # buildkit",
"comment": "buildkit.dockerfile.v0"
},
{
"created": "2022-02-04T08:57:38.818583756Z",
"created_by": "COPY /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ # buildkit",
"comment": "buildkit.dockerfile.v0"
},
{
"created": "2022-02-04T08:57:38.818583756Z",
"created_by": "USER 10001",
"comment": "buildkit.dockerfile.v0",
"empty_layer": true
},
{
"created": "2022-02-04T08:57:38.818583756Z",
"created_by": "ENTRYPOINT [\"./kyverno\"]",
"comment": "buildkit.dockerfile.v0",
"empty_layer": true
}
],
"os": "linux",
"rootfs": {
"type": "layers",
"diff_ids": [
"sha256:180b308b8730567d2d06a342148e1e9d274c8db84113077cfd0104a7e68db646",
"sha256:99187eab8264c714d0c260ae8b727c4d2bda3a9962635aaea67d04d0f8b0f466",
"sha256:26d825f3d198779c4990007ae907ba21e7c7b6213a7eb78d908122e435ec9958"
]
}
}
在上面的输出中,我们可以在 config.User 下看到运行这个容器的 Dockerfile 的 USER 声明是 10001。可以编写 Kyverno 策略来利用此信息并执行,例如,验证镜像的 USER 是非 root。
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: imageref-demo
spec:
validationFailureAction: enforce
rules:
- name: no-root-images
match:
any:
- resources:
kinds:
- Pod
preconditions:
all:
- key: "{{request.operation}}"
operator: NotEquals
value: DELETE
validate:
message: "Images run as root are not allowed."
foreach:
- list: "request.object.spec.containers"
context:
- name: imageData
imageRegistry:
reference: "{{ element.image }}"
deny:
conditions:
any:
- key: "{{ imageData.configData.config.User || ''}}"
operator: Equals
value: ""
在上面的示例策略中,已经编写了一个名为 imageData ,类型为 imageRegistry 的新 context。reference 键用于指示 Kyverno 存储镜像元数据的位置。其中 element 是 Pod 内的每个容器,因此 element.image 的容器镜像。然后可以在表达式中引用该值,例如在 deny.conditions 中通过键 {{ imageData.configData.config.User || ''}}。
使用示例“bad” Pod 来测试违反此政策的情况,如下所示,Pod 被阻止。
apiVersion: v1
kind: Pod
metadata:
name: badpod
spec:
containers:
- name: ubuntu
image: ubuntu:latest
$ kubectl apply -f bad.yaml
Error from server: error when creating "bad.yaml": admission webhook "validate.kyverno.svc-fail" denied the request:
resource Pod/default/badpod was blocked due to the following policies
imageref-demo:
no-root-images: 'validation failure: Images run as root are not allowed.'
相比之下,当使用“good”的 Pod 时,例如上面提到的 Kyverno 容器镜像,该资源是允许的。
apiVersion: v1
kind: Pod
metadata:
name: goodpod
spec:
containers:
- name: kyverno
image: ghcr.io/kyverno/kyverno:latest
$ kubectl apply -f good.yaml
pod/goodpod created
imageRegistry 上下文类型还有一个名为 jmesPath 的可选属性,能够应用一个 JMESPath 表达式,事先存储为 context 的值,并写入 imageRegistry 的返回的内容中。例如,下面的代码片段通过叠加其清单报告的镜像的所有组成层,将镜像的总大小存储在名为 imageSize 的 context 中(通过 crane manifest 可以得到各层的信息)。然后可以在稍后的表达式中评估 context 变量的值。
context:
- name: imageSize
imageRegistry:
reference: "{{ element.image }}"
# Note that we need to use `to_string` here to allow kyverno to treat it like a resource quantity of type memory
# the total size of an image as calculated by docker is the total sum of its layer sizes
jmesPath: "to_string(sum(manifest.layers[*].size))"
要访问存储在私有 registry 中的图像,请参阅使用私有 registry。
有关使用 imageRegistry con text的更多示例,请参阅示例页面。
网友评论