Skip to content

Commit 7ab710f

Browse files
committed
ValidatingAdmissionPolicy GA blog post.
1 parent 839a202 commit 7ab710f

File tree

1 file changed

+199
-0
lines changed
  • content/en/blog/_posts/2024-04-01-validating-admission-policy-ga

1 file changed

+199
-0
lines changed
Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
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

Comments
 (0)