From 6ce6b4a05d9fadd6afa803cecc8c7ab3efe9bd1f Mon Sep 17 00:00:00 2001 From: Michael Date: Sun, 5 May 2024 10:22:08 +0800 Subject: [PATCH] [zh] Add a blog: 2024-04-24-validating-admission-policy-ga/ --- .../index.md | 406 ++++++++++++++++++ .../webhook.go | 90 ++++ 2 files changed, 496 insertions(+) create mode 100644 content/zh-cn/blog/_posts/2024-04-24-validating-admission-policy-ga/index.md create mode 100644 content/zh-cn/blog/_posts/2024-04-24-validating-admission-policy-ga/webhook.go diff --git a/content/zh-cn/blog/_posts/2024-04-24-validating-admission-policy-ga/index.md b/content/zh-cn/blog/_posts/2024-04-24-validating-admission-policy-ga/index.md new file mode 100644 index 0000000000..15cb508866 --- /dev/null +++ b/content/zh-cn/blog/_posts/2024-04-24-validating-admission-policy-ga/index.md @@ -0,0 +1,406 @@ +--- +layout: blog +title: "Kubernetes 1.30:验证准入策略 ValidatingAdmissionPolicy 正式发布" +slug: validating-admission-policy-ga +date: 2024-04-24 +author: > + Jiahui Feng (Google) +translator: Michael Yao (DaoCloud) +--- + + + +我代表 Kubernetes 项目组成员,很高兴地宣布 ValidatingAdmissionPolicy 已经作为 Kubernetes 1.30 发布的一部分**正式发布**。 +如果你还不了解这个全新的声明式验证准入 Webhook 的替代方案, +请参阅有关这个新特性的[上一篇博文](/blog/2022/12/20/validating-admission-policies-alpha/)。 +如果你已经对 ValidatingAdmissionPolicy 有所了解并且想要尝试一下,那么现在是最好的时机。 + +让我们替换一个简单的 Webhook,体验一下 ValidatingAdmissionPolicy。 + + +## 准入 Webhook 示例 + +首先,让我们看一个简单 Webhook 的示例。以下是一个强制将 +`runAsNonRoot`、`readOnlyRootFilesystem`、`allowPrivilegeEscalation` 和 `privileged` 设置为最低权限值的 Webhook 代码片段。 + +```go +func verifyDeployment(deploy *appsv1.Deployment) error { + var errs []error + for i, c := range deploy.Spec.Template.Spec.Containers { + if c.Name == "" { + return fmt.Errorf("container %d has no name", i) + } + if c.SecurityContext == nil { + errs = append(errs, fmt.Errorf("container %q does not have SecurityContext", c.Name)) + } + if c.SecurityContext.RunAsNonRoot == nil || !*c.SecurityContext.RunAsNonRoot { + errs = append(errs, fmt.Errorf("container %q must set RunAsNonRoot to true in its SecurityContext", c.Name)) + } + if c.SecurityContext.ReadOnlyRootFilesystem == nil || !*c.SecurityContext.ReadOnlyRootFilesystem { + errs = append(errs, fmt.Errorf("container %q must set ReadOnlyRootFilesystem to true in its SecurityContext", c.Name)) + } + if c.SecurityContext.AllowPrivilegeEscalation != nil && *c.SecurityContext.AllowPrivilegeEscalation { + errs = append(errs, fmt.Errorf("container %q must NOT set AllowPrivilegeEscalation to true in its SecurityContext", c.Name)) + } + if c.SecurityContext.Privileged != nil && *c.SecurityContext.Privileged { + errs = append(errs, fmt.Errorf("container %q must NOT set Privileged to true in its SecurityContext", c.Name)) + } + } + return errors.NewAggregate(errs) +} +``` + + +查阅[什么是准入 Webhook?](/zh-cn/docs/reference/access-authn-authz/extensible-admission-controllers/#what-are-admission-webhooks), +或者查看这个 Webhook 的[完整代码](webhook.go)以便更好地理解下述演示。 + +## 策略 + +现在,让我们尝试使用 ValidatingAdmissionPolicy 来忠实地重新创建验证。 + +```yaml +apiVersion: admissionregistration.k8s.io/v1 +kind: ValidatingAdmissionPolicy +metadata: + name: "pod-security.policy.example.com" +spec: + failurePolicy: Fail + matchConstraints: + resourceRules: + - apiGroups: ["apps"] + apiVersions: ["v1"] + operations: ["CREATE", "UPDATE"] + resources: ["deployments"] + validations: + - expression: object.spec.template.spec.containers.all(c, has(c.securityContext) && has(c.securityContext.runAsNonRoot) && c.securityContext.runAsNonRoot) + message: 'all containers must set runAsNonRoot to true' + - expression: object.spec.template.spec.containers.all(c, has(c.securityContext) && has(c.securityContext.readOnlyRootFilesystem) && c.securityContext.readOnlyRootFilesystem) + message: 'all containers must set readOnlyRootFilesystem to true' + - expression: object.spec.template.spec.containers.all(c, !has(c.securityContext) || !has(c.securityContext.allowPrivilegeEscalation) || !c.securityContext.allowPrivilegeEscalation) + message: 'all containers must NOT set allowPrivilegeEscalation to true' + - expression: object.spec.template.spec.containers.all(c, !has(c.securityContext) || !has(c.securityContext.Privileged) || !c.securityContext.Privileged) + message: 'all containers must NOT set privileged to true' +``` + + +使用 `kubectl` 创建策略。很好,到目前为止没有任何问题。那我们获取此策略对象并查看其状态。 + +```shell +kubectl get -oyaml validatingadmissionpolicies/pod-security.policy.example.com +``` + +```yaml + status: + typeChecking: + expressionWarnings: + - fieldRef: spec.validations[3].expression + warning: | + apps/v1, Kind=Deployment: ERROR: :1:76: undefined field 'Privileged' + | object.spec.template.spec.containers.all(c, !has(c.securityContext) || !has(c.securityContext.Privileged) || !c.securityContext.Privileged) + | ...........................................................................^ + ERROR: :1:128: undefined field 'Privileged' + | object.spec.template.spec.containers.all(c, !has(c.securityContext) || !has(c.securityContext.Privileged) || !c.securityContext.Privileged) + | ...............................................................................................................................^ + +``` + + +系统根据所匹配的类别 `apps/v1.Deployment` 对策略执行了检查。 +查看 `fieldRef` 后,发现问题出现在第 3 个表达式上(索引从 0 开始)。 +有问题的表达式访问了一个未定义的 `Privileged` 字段。 +噢,看起来是一个复制粘贴错误。字段名应该是小写的。 + +```yaml +apiVersion: admissionregistration.k8s.io/v1 +kind: ValidatingAdmissionPolicy +metadata: + name: "pod-security.policy.example.com" +spec: + failurePolicy: Fail + matchConstraints: + resourceRules: + - apiGroups: ["apps"] + apiVersions: ["v1"] + operations: ["CREATE", "UPDATE"] + resources: ["deployments"] + validations: + - expression: object.spec.template.spec.containers.all(c, has(c.securityContext) && has(c.securityContext.runAsNonRoot) && c.securityContext.runAsNonRoot) + message: 'all containers must set runAsNonRoot to true' + - expression: object.spec.template.spec.containers.all(c, has(c.securityContext) && has(c.securityContext.readOnlyRootFilesystem) && c.securityContext.readOnlyRootFilesystem) + message: 'all containers must set readOnlyRootFilesystem to true' + - expression: object.spec.template.spec.containers.all(c, !has(c.securityContext) || !has(c.securityContext.allowPrivilegeEscalation) || !c.securityContext.allowPrivilegeEscalation) + message: 'all containers must NOT set allowPrivilegeEscalation to true' + - expression: object.spec.template.spec.containers.all(c, !has(c.securityContext) || !has(c.securityContext.privileged) || !c.securityContext.privileged) + message: 'all containers must NOT set privileged to true' +``` + + +再次检查状态,你应该看到所有警告都已被清除。 + +接下来,我们创建一个命名空间进行测试。 + +```shell +kubectl create namespace policy-test +``` + + +接下来,我将策略绑定到命名空间。但此时我将动作设置为 `Warn`, +这样此策略将打印出[警告](/zh-cn/blog/2020/09/03/warnings/)而不是拒绝请求。 +这对于在开发和自动化测试期间收集所有表达式的结果非常有用。 + +```yaml +apiVersion: admissionregistration.k8s.io/v1 +kind: ValidatingAdmissionPolicyBinding +metadata: + name: "pod-security.policy-binding.example.com" +spec: + policyName: "pod-security.policy.example.com" + validationActions: ["Warn"] + matchResources: + namespaceSelector: + matchLabels: + "kubernetes.io/metadata.name": "policy-test" +``` + + +测试策略的执行过程。 + +```shell +kubectl create -n policy-test -f- < +看起来很不错!策略和 Webhook 给出了等效的结果。 +又测试了其他几种情形后,当我们对策略有信心时,也许是时候进行一些清理工作了。 + +- 对于每个表达式,我们重复访问 `object.spec.template.spec.containers` 和每个 `securityContext`; +- 有一个检查某字段是否存在然后访问该字段的模式,这种模式看起来有点繁琐。 + + +幸运的是,自 Kubernetes 1.28 以来,我们对这两个问题都有了新的解决方案。 +变量组合(Variable Composition)允许我们将重复的子表达式提取到单独的变量中。 +Kubernetes 允许为 CEL 使用[可选库](https://github.com/google/cel-spec/wiki/proposal-246), +这些库非常适合处理可选的字段,你猜对了。 + +在了解了这两个特性后,让我们稍微重构一下此策略。 + +```yaml +apiVersion: admissionregistration.k8s.io/v1 +kind: ValidatingAdmissionPolicy +metadata: + name: "pod-security.policy.example.com" +spec: + failurePolicy: Fail + matchConstraints: + resourceRules: + - apiGroups: ["apps"] + apiVersions: ["v1"] + operations: ["CREATE", "UPDATE"] + resources: ["deployments"] + variables: + - name: containers + expression: object.spec.template.spec.containers + - name: securityContexts + expression: 'variables.containers.map(c, c.?securityContext)' + validations: + - expression: variables.securityContexts.all(c, c.?runAsNonRoot == optional.of(true)) + message: 'all containers must set runAsNonRoot to true' + - expression: variables.securityContexts.all(c, c.?readOnlyRootFilesystem == optional.of(true)) + message: 'all containers must set readOnlyRootFilesystem to true' + - expression: variables.securityContexts.all(c, c.?allowPrivilegeEscalation != optional.of(true)) + message: 'all containers must NOT set allowPrivilegeEscalation to true' + - expression: variables.securityContexts.all(c, c.?privileged != optional.of(true)) + message: 'all containers must NOT set privileged to true' +``` + + +策略现在更简洁、更易读。更新策略后,你应该看到它的功用与之前无异。 + +现在让我们将策略绑定从警告更改为实际拒绝验证失败的请求。 + +```yaml +apiVersion: admissionregistration.k8s.io/v1 +kind: ValidatingAdmissionPolicyBinding +metadata: + name: "pod-security.policy-binding.example.com" +spec: + policyName: "pod-security.policy.example.com" + validationActions: ["Deny"] + matchResources: + namespaceSelector: + matchLabels: + "kubernetes.io/metadata.name": "policy-test" +``` + + +最后,移除 Webhook。现在结果应该只包含来自策略的消息。 + +```shell +kubectl create -n policy-test -f- < +请注意,根据设计,此策略在第一个导致请求被拒绝的表达式之后停止处理。 +这与表达式只生成警告时的情况不同。 + + +## 设置监控 + +与 Webhook 不同,策略不是一个可以公开其自身指标的专用进程。 +相反,你可以使用源自 API 服务器的指标来代替。 + +以下是使用 Prometheus 查询语言执行一些常见监控任务的示例。 + +找到上述策略执行期间的 95 分位值: + +```text +histogram_quantile(0.95, sum(rate(apiserver_validating_admission_policy_check_duration_seconds_bucket{policy="pod-security.policy.example.com"}[5m])) by (le)) +``` + + +找到策略评估的速率: + +```text +rate(apiserver_validating_admission_policy_check_total{policy="pod-security.policy.example.com"}[5m]) +``` + + +你可以阅读[指标参考](/docs/reference/instrumentation/metrics/)了解有关上述指标的更多信息。 +ValidatingAdmissionPolicy 的指标目前处于 Alpha 阶段,随着稳定性在未来版本中的提升,将会有更多和更好的指标。 diff --git a/content/zh-cn/blog/_posts/2024-04-24-validating-admission-policy-ga/webhook.go b/content/zh-cn/blog/_posts/2024-04-24-validating-admission-policy-ga/webhook.go new file mode 100644 index 0000000000..3f0d1a8fae --- /dev/null +++ b/content/zh-cn/blog/_posts/2024-04-24-validating-admission-policy-ga/webhook.go @@ -0,0 +1,90 @@ +package main + +import ( + "encoding/json" + "flag" + "fmt" + "log" + "net/http" + + admissionv1 "k8s.io/api/admission/v1" + appsv1 "k8s.io/api/apps/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/errors" +) + +func verifyDeployment(deploy *appsv1.Deployment) error { + var errs []error + for i, c := range deploy.Spec.Template.Spec.Containers { + if c.Name == "" { + return fmt.Errorf("container %d has no name", i) + } + if c.SecurityContext == nil { + errs = append(errs, fmt.Errorf("container %q does not have SecurityContext", c.Name)) + } + if c.SecurityContext.RunAsNonRoot == nil || !*c.SecurityContext.RunAsNonRoot { + errs = append(errs, fmt.Errorf("container %q must set RunAsNonRoot to true in its SecurityContext", c.Name)) + } + if c.SecurityContext.ReadOnlyRootFilesystem == nil || !*c.SecurityContext.ReadOnlyRootFilesystem { + errs = append(errs, fmt.Errorf("container %q must set ReadOnlyRootFilesystem to true in its SecurityContext", c.Name)) + } + if c.SecurityContext.AllowPrivilegeEscalation != nil && *c.SecurityContext.AllowPrivilegeEscalation { + errs = append(errs, fmt.Errorf("container %q must NOT set AllowPrivilegeEscalation to true in its SecurityContext", c.Name)) + } + if c.SecurityContext.Privileged != nil && *c.SecurityContext.Privileged { + errs = append(errs, fmt.Errorf("container %q must NOT set Privileged to true in its SecurityContext", c.Name)) + } + } + return errors.NewAggregate(errs) +} + +func WebhookEnforceSecurePodConfiguration(rw http.ResponseWriter, req *http.Request) { + result := &admissionv1.AdmissionReview{Response: &admissionv1.AdmissionResponse{}} + err := func() error { + ar := new(admissionv1.AdmissionReview) + err := json.NewDecoder(req.Body).Decode(ar) + if err != nil { + return err + } + if ar.Request == nil { + return nil + } + result.TypeMeta = ar.TypeMeta + result.Response.UID = ar.Request.UID + if len(ar.Request.Object.Raw) == 0 { + return nil + } + deploy := new(appsv1.Deployment) + err = json.Unmarshal(ar.Request.Object.Raw, deploy) + if err != nil { + return err + } + return verifyDeployment(deploy) + }() + if err == nil { + result.Response.Allowed = true + } else { + result.Response.Allowed = false + result.Response.Result = &metav1.Status{ + Code: http.StatusForbidden, + Message: err.Error(), + } + } + err = json.NewEncoder(rw).Encode(result) + if err != nil { + log.Println(err) + } +} + +var _ http.HandlerFunc = WebhookEnforceSecurePodConfiguration + +func main() { + http.HandleFunc("/", WebhookEnforceSecurePodConfiguration) + + addr := flag.String("addr", ":8443", "address to listen on") + certFile := flag.String("cert", "cert.pem", "path to TLS certificate") + keyFile := flag.String("key", "key.pem", "path to TLS key") + flag.Parse() + + log.Fatalln(http.ListenAndServeTLS(*addr, *certFile, *keyFile, nil)) +}