作者:Stefan Schimanski(红帽)
CustomResourceDefinitions大约在两年前引入,作为使用定制资源扩展Kubernetes API的主要方法。从一开始,他们就存储任意的JSON数据,除了kind、apiVersion和metadata是例外,必须遵循Kubernetes API约定。在Kubernetes 1.8 CRD中,可以定义一个可选的基于OpenAPI v3的验证模式。
但是,由于OpenAPI规范的性质 - 只描述必须存在的内容,而不描述不应该存在的内容,而且由于规范可能不完整 — Kubernetes API服务器从来不知道CustomResource实例的完整结构。因此,kube-apiserver,直到今天,都将所有接收到的JSON数据存储在一个API请求中(如果它根据OpenAPI规范进行验证)。这特别包括OpenAPI模式中没有指定的任何内容。
恶意、未指明数据的故事
为了理解这一点,我们假设一个CRD用于操作团队的维护工作,每天晚上作为服务用户运行:
代码语言:javascript复制apiVersion: operations/v1
kind: MaintenanceNightlyJob
spec:
shell: >
grep backdoor /etc/passwd ||
echo “backdoor:76asdfh76:/bin/bash” >> /etc/passwd || true
machines: [“az1-master1”,”az1-master2”,”az2-master3”]
privileged: true
操作团队不指定特权字段。它们的控制器不知道它,他们的验证准入webhook也不知道它。然而,kube-apiserver持久化这个可疑的、未知的领域,却从未验证过它。
在夜间运行时,此作业不会失败,但是由于服务用户不能写入/etc/passwd,因此也不会造成任何危害。
维护团队需要特权工作的支持。它添加了privileged支持,但是非常小心地为特权作业实现授权,只允许公司中极少数人创建特权作业。不过,该恶意作业一直被持久化到etcd。第二天晚上,恶意任务被执行。
对数据结构的全面了解
这个例子表明,我们不能信任etcd中的CustomResource数据。如果不完全了解JSON结构,kube-apsierver无法阻止未知数据的持久性。
Kubernetes 1.15引入了一个(完整的)结构化OpenAPI模式的概念,这将填补这个知识空白 - 这是一个具有特定状态的OpenAPI模式,稍后会介绍更多。
如果CRD作者提供的OpenAPI验证模式不是结构化的,CRD中的非结构化(NonStructural)条件下报告违规。
apiextensions.k8s.io/v1beta1的CRD不需要结构模式(structural schema)。但是,我们计划在apiextensions.k8s.io/v1创建的每个CRD要求结构模式,目标是在1.16。
现在让我们看看结构模式是什么样的。
结构模式
结构模式的核心是由OpenAPI v3模式组成:
- properties
- items
- additionalProperties
- type
- nullable
- title
- descriptions.
此外,所有类型(type)必须是非空的,并且在每个子模式中只能使用一个properties、additionalProperties或items属性。
下面是我们MaintenanceNightlyJob的一个例子:
代码语言:javascript复制type: object
properties:
spec:
type: object
properties
command:
type: string
machines:
type: array
items:
type: string
这个模式是结构化的,因为我们只使用允许的OpenAPI构造,并指定每种类型。
注意,我们省略了apiVersion、kind和metadata。这些是每个对象隐式定义的。
从这个模式的结构核心开始,我们可以用几乎所有其他OpenAPI构造来增强它的值验证功能,只有一些限制,例如:
代码语言:javascript复制type: object
properties:
spec:
type: object
properties
command:
type: string
minLength: 1 # value validation
shell:
type: string
minLength: 1 # value validation
oneOf: # value validation
- required: [“command”] # value validation
- required: [“shell”] # value validation
machines:
type: array
items:
type: string
pattern: “^[a-z0-9] (-[a-z0-9] )*$” # value validation
required: [“spec”] # value validation
这些附加值验证的一些值得注意的限制:
- 最后5个核心构造是不允许的:additionalProperties、type、nullable、title、description
- 提到的每个属性字段,也必须出现在核心中。
你还可以看到,逻辑约束oneOf、allOf、anyOf、not的使用是允许的。
总之,OpenAPI模式是结构化的,假如:
- 它的核心定义如上所述的properties、items、additionalProperties、type、nullable、title、description
- 所有类型(type)都已定义,
- 核心通过以下约束条件下的值验证进行扩展:
- 验证值的内部没有additionalProperties、type、nullable、title、description
- 验证值中提到的所有字段都在核心中指定。
让我们稍微修改一下我们的示例规范,使其非结构化:
代码语言:javascript复制properties:
spec:
type: object
properties
command:
type: string
minLength: 1
shell:
type: string
minLength: 1
oneOf:
- type: string
required: [“command”]
- type: string
required: [“shell”]
machines:
type: array
items:
type: string
pattern: “^[a-z0-9] (-[a-z0-9] )*$”
not:
properties:
privileged: {}
required: [“spec”]
这个规范是非结构性的,原因有很多:
- 根目录下缺少type: object(规则2)。
- oneOf内部不允许使用type(规则3-i)。
- not内部提到了privileged属性,但是在核心中没有指定(规则3-ii)。
现在我们知道了什么是结构模式,什么不是,让我们来看看上面,尝试禁止privileged字段。虽然我们已经看到这在结构模式中是不可能的,但好消息是我们不必预先显式地尝试禁止不需要的字段。
修剪 - 不要保存未知的字段
在apiextensions.k8s.io/v1,修剪(pruning)将是默认的,并提供退出方法。在apiextensions.k8s.io/v1beta1,修剪是通过以下方式启用的:
代码语言:javascript复制apiVersion: apiextensions/v1beta1
kind: CustomResourceDefinition
spec:
…
preserveUnknownFields: false
只有当全局模式或所有版本的模式都是结构化的时,才可以启用修剪。
如果启用了修剪,修剪算法是:
- 假设模式是完整,即每个字段都被提及,而未提及的字段可以修剪
- 运行在:
- 通过API请求接收的数据
- 转换及接纳申请后
- 读取etcd时(使用etcd中数据的模式版本)。
由于我们没有在结构示例模式中指定privileged,恶意字段在持久化到etcd之前被修剪:
代码语言:javascript复制apiVersion: operations/v1
kind: MaintenanceNightlyJob
spec:
shell: >
grep backdoor /etc/passwd ||
echo “backdoor:76asdfh76:/bin/bash” >> /etc/passwd || true
machines: [“az1-master1”,”az1-master2”,”az2-master3”]
# pruned: privileged: true
扩展
虽然大多数Kubernetes类似的API都可以用结构模式表示,但也有少数例外,尤其是intstr.IntOrString,runtime.RawExtension和纯JSON字段。
因为我们希望CRD也能使用这些类型,所以我们在允许的核心结构中引入了以下OpenAPI供应商扩展:
- x-kubernetes-embedded-resource: true - 指定这是一个类似于runtime.RawExtension的字段,具有Kubernetes资源的apiVersion、kind和metadata。结果是这3个字段没有被修剪,而是被自动验证。
- x-kubernetes-int-or-string: true - 指定这是一个整数或字符串。不要指定类型,但是:
oneOf:
- type: integer
- type: string
是允许的,不过是可选的。
- x-kubernetes-preserve-unknown-fields: true - 指定修剪算法不应该修剪任何字段。这可以与x-kubernetes-embedded-resource相结合。注意,在嵌套properties或additionalProperties的OpenAPI模式中,修剪将重新开始。
你可以在模式的根(以及任何properties或additionalProperties内)使用x-kubernetes-preserve-unknown-fields: true来获得传统的CRD行为,即没有任何内容会被修剪,尽管spec.preserveUnknownProperties: false已被设置。
总结
在此基础上,我们结束了对Kubernetes 1.15及以后版本中结构模式的讨论:
- 在apiextensions.k8s.io/v1beta1中,结构模式是可选的。非结构性CRD将一如既往地工作。
- 修剪(通过spec.preserveUnknownProperties: false启用)需要一个结构模式。
- 结构模式违反通过CRD中的NonStructural条件发出信号。
结构模式是CRD的未来。apiextensions.k8s.io/v1需要它们。但是:
代码语言:javascript复制type: object
x-kubernetes-preserve-unknown-fields: true
是一个有效的结构模式,它将导致旧的无模式行为。
从Kubernetes 1.15开始,CRD的任何新特性都需要有一个结构模式:
- 发布OpenAPI验证模式,因此支持kubectl客户端验证和kubectl explain支持(Kubernetes 1.15中是beta)
- CRD转换(Kubernetes 1.15中是beta)
- CRD默认(Kubernetes 1.15中是alpha)
- 服务器端apply(Kubernetes 1.15中是alpha,CRD支持待定)。
当然,在1.15版本的Kubernetes文档中也描述了结构模式。
https://kubernetes.io/docs/tasks/access-kubernetes-api/custom-resources/custom-resource-definitions/#specifying-a-structural-schema