|
| 1 | +------ |
| 2 | +layout: blog |
| 3 | +title: "Kubernetes 1.30: Validating Admission Policy Is Generally Available" |
| 4 | +slug: validating-admission-policy-ga |
| 5 | +date: 2024-04-01 |
| 6 | +canonicalUrl: https://www.k8s.dev/blog/2024/04/01/validating-admission-policy/ |
| 7 | +--- |
| 8 | + |
| 9 | +** Author **: Jiahui Feng (Google) |
| 10 | + |
| 11 | +We are excited to announce that Validating Admission Policy has reached its General Availability |
| 12 | +as part of Kubernetes 1.30 release. If you have not yet read about this new declarative alternative to |
| 13 | +validating admission webhooks, it may be interesting to read our |
| 14 | +[previous post](/blog/2022/12/20/validating-admission-policies-alpha/) about the new feature. |
| 15 | + |
| 16 | +If you have already heard about Validating Admission Policy and you are eager to try it out, there is no better way to |
| 17 | +start using it by replacing an existing webhook. |
| 18 | + |
| 19 | +# The Webhook |
| 20 | +First, let's take a look at an example of a webhook that can be a good candidate. Here is an excerpt from a webhook that |
| 21 | +enforce `runAsNonRoot`, `readOnlyRootFilesystem`, `allowPrivilegeEscalation`, and `privileged` to be set to the least permissive values. |
| 22 | + |
| 23 | +```go |
| 24 | +func verifyDeployment(deploy *appsv1.Deployment) error { |
| 25 | + for i, c := range deploy.Spec.Template.Spec.Containers { |
| 26 | + if c.Name == "" { |
| 27 | + return fmt.Errorf("container %d has no name", i) |
| 28 | + } |
| 29 | + if c.SecurityContext == nil { |
| 30 | + return fmt.Errorf("container %q does not have SecurityContext", c.Name) |
| 31 | + } |
| 32 | + if c.SecurityContext.RunAsNonRoot == nil || !*c.SecurityContext.RunAsNonRoot { |
| 33 | + return fmt.Errorf("container %q must set RunAsNonRoot to true in its SecurityContext", c.Name) |
| 34 | + } |
| 35 | + if c.SecurityContext.ReadOnlyRootFilesystem == nil || !*c.SecurityContext.ReadOnlyRootFilesystem { |
| 36 | + return fmt.Errorf("container %q must set ReadOnlyRootFilesystem to true in its SecurityContext", c.Name) |
| 37 | + } |
| 38 | + if c.SecurityContext.AllowPrivilegeEscalation != nil && *c.SecurityContext.AllowPrivilegeEscalation { |
| 39 | + return fmt.Errorf("container %q must NOT set AllowPrivilegeEscalation to true in its SecurityContext", c.Name) |
| 40 | + } |
| 41 | + if c.SecurityContext.Privileged != nil && *c.SecurityContext.Privileged { |
| 42 | + return fmt.Errorf("container %q must NOT set Privileged to true in its SecurityContext", c.Name) |
| 43 | + } |
| 44 | + } |
| 45 | + return nil |
| 46 | +} |
| 47 | +``` |
| 48 | + |
| 49 | +Check out [the doc](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#what-are-admission-webhooks) |
| 50 | +for a refresher on how admission webhooks work. Or, see the [full code](https://gist.github.com/jiahuif/2653f2ce41fe6a2e5739ea7cd76b182b) of this webhook to follow along this tutorial. |
| 51 | + |
| 52 | +# The Policy |
| 53 | +Now let's try to recreate the validation with a ValidatingAdmissionPolicy. |
| 54 | +```yaml |
| 55 | +apiVersion: admissionregistration.k8s.io/v1 |
| 56 | +kind: ValidatingAdmissionPolicy |
| 57 | +metadata: |
| 58 | + name: "pod-security.policy.example.com" |
| 59 | +spec: |
| 60 | + failurePolicy: Fail |
| 61 | + matchConstraints: |
| 62 | + resourceRules: |
| 63 | + - apiGroups: ["apps"] |
| 64 | + apiVersions: ["v1"] |
| 65 | + operations: ["CREATE", "UPDATE"] |
| 66 | + resources: ["deployments"] |
| 67 | + validations: |
| 68 | + - expression: object.spec.template.spec.containers.all(c, has(c.securityContext) && has(c.securityContext.runAsNonRoot) && c.securityContext.runAsNonRoot) |
| 69 | + message: 'all containers must set runAsNonRoot to true' |
| 70 | + - expression: object.spec.template.spec.containers.all(c, has(c.securityContext) && has(c.securityContext.readOnlyRootFilesystem) && c.securityContext.readOnlyRootFilesystem) |
| 71 | + message: 'all containers must set readOnlyRootFilesystem to true' |
| 72 | + - expression: object.spec.template.spec.containers.all(c, !has(c.securityContext) || !has(c.securityContext.allowPrivilegeEscalation) || !c.securityContext.allowPrivilegeEscalation) |
| 73 | + message: 'all containers must set allowPrivilegeEscalation to false' |
| 74 | + - expression: object.spec.template.spec.containers.all(c, !has(c.securityContext) || !has(c.securityContext.Privileged) || !c.securityContext.Privileged) |
| 75 | + message: 'all containers must set privileged to false' |
| 76 | +``` |
| 77 | +Create the policy with `kubectl`. Great, no complain so far. But let's take a look at its status. |
| 78 | +```yaml |
| 79 | + status: |
| 80 | + typeChecking: |
| 81 | + expressionWarnings: |
| 82 | + - fieldRef: spec.validations[3].expression |
| 83 | + warning: | |
| 84 | + apps/v1, Kind=Deployment: ERROR: <input>:1:76: undefined field 'Privileged' |
| 85 | + | object.spec.template.spec.containers.all(c, !has(c.securityContext) || !has(c.securityContext.Privileged) || !c.securityContext.Privileged) |
| 86 | + | ...........................................................................^ |
| 87 | + ERROR: <input>:1:128: undefined field 'Privileged' |
| 88 | + | object.spec.template.spec.containers.all(c, !has(c.securityContext) || !has(c.securityContext.Privileged) || !c.securityContext.Privileged) |
| 89 | + | ...............................................................................................................................^ |
| 90 | +
|
| 91 | +``` |
| 92 | +Ah, seems like a copy-paste error. Let's correct it real quick. |
| 93 | +```yaml |
| 94 | +apiVersion: admissionregistration.k8s.io/v1 |
| 95 | +kind: ValidatingAdmissionPolicy |
| 96 | +metadata: |
| 97 | + name: "pod-security.policy.example.com" |
| 98 | +spec: |
| 99 | + failurePolicy: Fail |
| 100 | + matchConstraints: |
| 101 | + resourceRules: |
| 102 | + - apiGroups: ["apps"] |
| 103 | + apiVersions: ["v1"] |
| 104 | + operations: ["CREATE", "UPDATE"] |
| 105 | + resources: ["deployments"] |
| 106 | + validations: |
| 107 | + - expression: object.spec.template.spec.containers.all(c, has(c.securityContext) && has(c.securityContext.runAsNonRoot) && c.securityContext.runAsNonRoot) |
| 108 | + message: 'all containers must set runAsNonRoot to true' |
| 109 | + - expression: object.spec.template.spec.containers.all(c, has(c.securityContext) && has(c.securityContext.readOnlyRootFilesystem) && c.securityContext.readOnlyRootFilesystem) |
| 110 | + message: 'all containers must set readOnlyRootFilesystem to true' |
| 111 | + - expression: object.spec.template.spec.containers.all(c, !has(c.securityContext) || !has(c.securityContext.allowPrivilegeEscalation) || !c.securityContext.allowPrivilegeEscalation) |
| 112 | + message: 'all containers must set allowPrivilegeEscalation to false' |
| 113 | + - expression: object.spec.template.spec.containers.all(c, !has(c.securityContext) || !has(c.securityContext.privileged) || !c.securityContext.privileged) |
| 114 | + message: 'all containers must set privileged to false' |
| 115 | +``` |
| 116 | +Check its status again, and you should see all warnings cleared. |
| 117 | + |
| 118 | +Next, let's create a namespace for our tests. |
| 119 | +```shell |
| 120 | +kubectl create namespace policy-test |
| 121 | +``` |
| 122 | +Then, bind the policy to the namespace. But at this point, we set the action to `Warn` |
| 123 | +so that the policy prints out warnings instead of rejecting the requests. |
| 124 | +```yaml |
| 125 | +apiVersion: admissionregistration.k8s.io/v1 |
| 126 | +kind: ValidatingAdmissionPolicyBinding |
| 127 | +metadata: |
| 128 | + name: "pod-security.policy-binding.example.com" |
| 129 | +spec: |
| 130 | + policyName: "pod-security.policy.example.com" |
| 131 | + validationActions: ["Warn"] |
| 132 | + matchResources: |
| 133 | + namespaceSelector: |
| 134 | + matchLabels: |
| 135 | + "kubernetes.io/metadata.name": "policy-test" |
| 136 | +``` |
| 137 | +Tests out policy enforcement. |
| 138 | +```shell |
| 139 | +kubectl create -n policy-test -f- <<EOF |
| 140 | +apiVersion: apps/v1 |
| 141 | +kind: Deployment |
| 142 | +metadata: |
| 143 | + labels: |
| 144 | + app: nginx |
| 145 | + name: nginx |
| 146 | +spec: |
| 147 | + selector: |
| 148 | + matchLabels: |
| 149 | + app: nginx |
| 150 | + template: |
| 151 | + metadata: |
| 152 | + labels: |
| 153 | + app: nginx |
| 154 | + spec: |
| 155 | + containers: |
| 156 | + - image: nginx |
| 157 | + name: nginx |
| 158 | + securityContext: |
| 159 | + privileged: true |
| 160 | + allowPrivilegeEscalation: true |
| 161 | +EOF |
| 162 | +``` |
| 163 | +```text |
| 164 | +Warning: Validation failed for ValidatingAdmissionPolicy 'pod-security.policy.example.com' with binding 'pod-security.policy-binding.example.com': all containers must set runAsNonRoot to true |
| 165 | +Warning: Validation failed for ValidatingAdmissionPolicy 'pod-security.policy.example.com' with binding 'pod-security.policy-binding.example.com': all containers must set readOnlyRootFilesystem to true |
| 166 | +Warning: Validation failed for ValidatingAdmissionPolicy 'pod-security.policy.example.com' with binding 'pod-security.policy-binding.example.com': all containers must set allowPrivilegeEscalation to false |
| 167 | +Warning: Validation failed for ValidatingAdmissionPolicy 'pod-security.policy.example.com' with binding 'pod-security.policy-binding.example.com': all containers must set privileged to false |
| 168 | +Error from server: error when creating "STDIN": admission webhook "webhook.example.com" denied the request: container "nginx" must set RunAsNonRoot to true in its SecurityContext |
| 169 | +``` |
| 170 | +Not quite the exact same behavior but good enough. After a few other cases, when we are confident with our policy, maybe it is time for some refactoring. |
| 171 | +We can extract repeated sub-expressions into their own variables. |
| 172 | +```yaml |
| 173 | +apiVersion: admissionregistration.k8s.io/v1 |
| 174 | +kind: ValidatingAdmissionPolicy |
| 175 | +metadata: |
| 176 | + name: "pod-security.policy.example.com" |
| 177 | +spec: |
| 178 | + failurePolicy: Fail |
| 179 | + matchConstraints: |
| 180 | + resourceRules: |
| 181 | + - apiGroups: ["apps"] |
| 182 | + apiVersions: ["v1"] |
| 183 | + operations: ["CREATE", "UPDATE"] |
| 184 | + resources: ["deployments"] |
| 185 | + variables: |
| 186 | + - name: containers |
| 187 | + expression: object.spec.template.spec.containers |
| 188 | + - name: securityContexts |
| 189 | + expression: 'variables.containers.map(c, has(c.securityContext) ? c.securityContext : {})' |
| 190 | + validations: |
| 191 | + - expression: variables.securityContexts.all(c, has(c.runAsNonRoot) && c.runAsNonRoot) |
| 192 | + message: 'all containers must set runAsNonRoot to true' |
| 193 | + - expression: variables.securityContexts.all(c, has(c.readOnlyRootFilesystem) && c.readOnlyRootFilesystem) |
| 194 | + message: 'all containers must set readOnlyRootFilesystem to true' |
| 195 | + - expression: variables.securityContexts.all(c, !has(c.allowPrivilegeEscalation) || !c.allowPrivilegeEscalation) |
| 196 | + message: 'all containers must set allowPrivilegeEscalation to false' |
| 197 | + - expression: variables.securityContexts.all(c, !has(c.privileged) || !c.privileged) |
| 198 | + message: 'all containers must set privileged to false' |
| 199 | +``` |
0 commit comments