|
| 1 | +apiVersion: templates.gatekeeper.sh/v1 |
| 2 | +kind: ConstraintTemplate |
| 3 | +metadata: |
| 4 | + name: k8spspselinuxv2 |
| 5 | + annotations: |
| 6 | + metadata.gatekeeper.sh/title: "SELinux V2" |
| 7 | + metadata.gatekeeper.sh/version: 1.1.0 |
| 8 | + description: >- |
| 9 | + Defines an allow-list of seLinuxOptions configurations for pod |
| 10 | + containers. Corresponds to a PodSecurityPolicy requiring SELinux configs. |
| 11 | + For more information, see |
| 12 | + https://kubernetes.io/docs/concepts/policy/pod-security-policy/#selinux |
| 13 | +spec: |
| 14 | + crd: |
| 15 | + spec: |
| 16 | + names: |
| 17 | + kind: K8sPSPSELinuxV2 |
| 18 | + validation: |
| 19 | + # Schema for the `parameters` field |
| 20 | + openAPIV3Schema: |
| 21 | + type: object |
| 22 | + description: >- |
| 23 | + Defines an allow-list of seLinuxOptions configurations for pod |
| 24 | + containers. Corresponds to a PodSecurityPolicy requiring SELinux configs. |
| 25 | + For more information, see |
| 26 | + https://kubernetes.io/docs/concepts/policy/pod-security-policy/#selinux |
| 27 | + properties: |
| 28 | + exemptImages: |
| 29 | + description: >- |
| 30 | + Any container that uses an image that matches an entry in this list will be excluded |
| 31 | + from enforcement. Prefix-matching can be signified with `*`. For example: `my-image-*`. |
| 32 | +
|
| 33 | + It is recommended that users use the fully-qualified Docker image name (e.g. start with a domain name) |
| 34 | + in order to avoid unexpectedly exempting images from an untrusted repository. |
| 35 | + type: array |
| 36 | + items: |
| 37 | + type: string |
| 38 | + allowedSELinuxOptions: |
| 39 | + type: array |
| 40 | + description: "An allow-list of SELinux options configurations." |
| 41 | + items: |
| 42 | + type: object |
| 43 | + description: "An allowed configuration of SELinux options for a pod container." |
| 44 | + properties: |
| 45 | + level: |
| 46 | + type: string |
| 47 | + description: "An SELinux level." |
| 48 | + role: |
| 49 | + type: string |
| 50 | + description: "An SELinux role." |
| 51 | + type: |
| 52 | + type: string |
| 53 | + description: "An SELinux type." |
| 54 | + user: |
| 55 | + type: string |
| 56 | + description: "An SELinux user." |
| 57 | + targets: |
| 58 | + - target: admission.k8s.gatekeeper.sh |
| 59 | + code: |
| 60 | + - engine: K8sNativeValidation |
| 61 | + source: |
| 62 | + variables: |
| 63 | + - name: notViolatingSELinuxOptions |
| 64 | + expression: | |
| 65 | + has(object.spec.securityContext) && has(object.spec.securityContext.seLinuxOptions) ? |
| 66 | + (has(variables.params.allowedSELinuxOptions) ? |
| 67 | + ( |
| 68 | + (has(variables.params.allowedSELinuxOptions.level) && has(object.spec.securityContext.seLinuxOptions.level) && (object.spec.securityContext.seLinuxOptions.level == variables.params.allowedSELinuxOptions.level)) && |
| 69 | + (has(variables.params.allowedSELinuxOptions.role) && has(object.spec.securityContext.seLinuxOptions.role) && (object.spec.securityContext.seLinuxOptions.role == variables.params.allowedSELinuxOptions.role)) && |
| 70 | + (has(variables.params.allowedSELinuxOptions.type) && has(object.spec.securityContext.seLinuxOptions.type) && (object.spec.securityContext.seLinuxOptions.type == variables.params.allowedSELinuxOptions.type)) && |
| 71 | + (has(variables.params.allowedSELinuxOptions.user) && has(object.spec.securityContext.seLinuxOptions.user) && (object.spec.securityContext.seLinuxOptions.user == variables.params.allowedSELinuxOptions.user)) |
| 72 | + ) : |
| 73 | + (!has(object.spec.securityContext.seLinuxOptions.level) && !has(object.spec.securityContext.seLinuxOptions.role) && !has(object.spec.securityContext.seLinuxOptions.type) && !has(object.spec.securityContext.seLinuxOptions.user))) |
| 74 | + : true |
| 75 | + - name: containers |
| 76 | + expression: 'has(object.spec.containers) ? object.spec.containers.filter(c, has(c.securityContext) && has(c.securityContext.seLinuxOptions)) : []' |
| 77 | + - name: initContainers |
| 78 | + expression: 'has(object.spec.initContainers) ? object.spec.initContainers.filter(c, has(c.securityContext) && has(c.securityContext.seLinuxOptions)) : []' |
| 79 | + - name: ephemeralContainers |
| 80 | + expression: 'has(object.spec.ephemeralContainers) ? object.spec.ephemeralContainers.filter(c, has(c.securityContext) && has(c.securityContext.seLinuxOptions)) : []' |
| 81 | + - name: exemptImagePrefixes |
| 82 | + expression: | |
| 83 | + !has(variables.params.exemptImages) ? [] : |
| 84 | + variables.params.exemptImages.filter(image, image.endsWith("*")).map(image, string(image).replace("*", "")) |
| 85 | + - name: exemptImageExplicit |
| 86 | + expression: | |
| 87 | + !has(variables.params.exemptImages) ? [] : |
| 88 | + variables.params.exemptImages.filter(image, !image.endsWith("*")) |
| 89 | + - name: exemptImages |
| 90 | + expression: | |
| 91 | + (variables.containers + variables.initContainers + variables.ephemeralContainers).filter(container, |
| 92 | + container.image in variables.exemptImageExplicit || |
| 93 | + variables.exemptImagePrefixes.exists(exemption, string(container.image).startsWith(exemption))) |
| 94 | + - name: badContainers |
| 95 | + expression: | |
| 96 | + (variables.containers + variables.initContainers + variables.ephemeralContainers).filter(c, !(c.image in variables.exemptImages) && (has(c.securityContext.seLinuxOptions) ? ( |
| 97 | + has(variables.params.allowedSELinuxOptions) ? |
| 98 | + ( |
| 99 | + !(has(variables.params.allowedSELinuxOptions.level) && has(c.securityContext.seLinuxOptions.level) && (c.securityContext.seLinuxOptions.level == variables.params.allowedSELinuxOptions.level)) || |
| 100 | + !(has(variables.params.allowedSELinuxOptions.role) && has(c.securityContext.seLinuxOptions.role) && (c.securityContext.seLinuxOptions.role == variables.params.allowedSELinuxOptions.role)) || |
| 101 | + !(has(variables.params.allowedSELinuxOptions.type) && has(c.securityContext.seLinuxOptions.type) && (c.securityContext.seLinuxOptions.type == variables.params.allowedSELinuxOptions.type)) || |
| 102 | + !(has(variables.params.allowedSELinuxOptions.user) && has(c.securityContext.seLinuxOptions.user) && (c.securityContext.seLinuxOptions.user == variables.params.allowedSELinuxOptions.user)) |
| 103 | + ) : (has(c.securityContext.seLinuxOptions.level) || has(c.securityContext.seLinuxOptions.role) || has(c.securityContext.seLinuxOptions.type) || has(c.securityContext.seLinuxOptions.user)) |
| 104 | + ) : false |
| 105 | + )) |
| 106 | + validations: |
| 107 | + - expression: '(has(request.operation) && request.operation == "UPDATE") || variables.notViolatingSELinuxOptions' |
| 108 | + messageExpression: '"SELinux options is not allowed, pod: " + object.metadata.name + ". Allowed options: " + variables.params.allowedSELinuxOptions' |
| 109 | + - expression: '(has(request.operation) && request.operation == "UPDATE") || size(variables.badContainers) == 0' |
| 110 | + messageExpression: '"SELinux options is not allowed, pod: " + object.metadata.name + ", container: " + variables.badContainers.map(c, c.name).join(", ")' |
| 111 | + - engine: Rego |
| 112 | + source: |
| 113 | + rego: | |
| 114 | + package k8spspselinux |
| 115 | +
|
| 116 | + import data.lib.exclude_update.is_update |
| 117 | + import data.lib.exempt_container.is_exempt |
| 118 | +
|
| 119 | + # Disallow top level custom SELinux options |
| 120 | + violation[{"msg": msg, "details": {}}] { |
| 121 | + # spec.securityContext.seLinuxOptions field is immutable. |
| 122 | + not is_update(input.review) |
| 123 | +
|
| 124 | + has_field(input.review.object.spec.securityContext, "seLinuxOptions") |
| 125 | + not input_seLinuxOptions_allowed(input.review.object.spec.securityContext.seLinuxOptions) |
| 126 | + msg := sprintf("SELinux options is not allowed, pod: %v. Allowed options: %v", [input.review.object.metadata.name, input.parameters.allowedSELinuxOptions]) |
| 127 | + } |
| 128 | + # Disallow container level custom SELinux options |
| 129 | + violation[{"msg": msg, "details": {}}] { |
| 130 | + # spec.containers.securityContext.seLinuxOptions field is immutable. |
| 131 | + not is_update(input.review) |
| 132 | +
|
| 133 | + c := input_security_context[_] |
| 134 | + not is_exempt(c) |
| 135 | + has_field(c.securityContext, "seLinuxOptions") |
| 136 | + not input_seLinuxOptions_allowed(c.securityContext.seLinuxOptions) |
| 137 | + msg := sprintf("SELinux options is not allowed, pod: %v, container: %v. Allowed options: %v", [input.review.object.metadata.name, c.name, input.parameters.allowedSELinuxOptions]) |
| 138 | + } |
| 139 | +
|
| 140 | + input_seLinuxOptions_allowed(options) { |
| 141 | + params := input.parameters.allowedSELinuxOptions[_] |
| 142 | + field_allowed("level", options, params) |
| 143 | + field_allowed("role", options, params) |
| 144 | + field_allowed("type", options, params) |
| 145 | + field_allowed("user", options, params) |
| 146 | + } |
| 147 | +
|
| 148 | + field_allowed(field, options, params) { |
| 149 | + params[field] == options[field] |
| 150 | + } |
| 151 | + field_allowed(field, options, _) { |
| 152 | + not has_field(options, field) |
| 153 | + } |
| 154 | +
|
| 155 | + input_security_context[c] { |
| 156 | + c := input.review.object.spec.containers[_] |
| 157 | + has_field(c.securityContext, "seLinuxOptions") |
| 158 | + } |
| 159 | + input_security_context[c] { |
| 160 | + c := input.review.object.spec.initContainers[_] |
| 161 | + has_field(c.securityContext, "seLinuxOptions") |
| 162 | + } |
| 163 | + input_security_context[c] { |
| 164 | + c := input.review.object.spec.ephemeralContainers[_] |
| 165 | + has_field(c.securityContext, "seLinuxOptions") |
| 166 | + } |
| 167 | +
|
| 168 | + # has_field returns whether an object has a field |
| 169 | + has_field(object, field) = true { |
| 170 | + object[field] |
| 171 | + } |
| 172 | + libs: |
| 173 | + - | |
| 174 | + package lib.exclude_update |
| 175 | +
|
| 176 | + is_update(review) { |
| 177 | + review.operation == "UPDATE" |
| 178 | + } |
| 179 | + - | |
| 180 | + package lib.exempt_container |
| 181 | +
|
| 182 | + is_exempt(container) { |
| 183 | + exempt_images := object.get(object.get(input, "parameters", {}), "exemptImages", []) |
| 184 | + img := container.image |
| 185 | + exemption := exempt_images[_] |
| 186 | + _matches_exemption(img, exemption) |
| 187 | + } |
| 188 | +
|
| 189 | + _matches_exemption(img, exemption) { |
| 190 | + not endswith(exemption, "*") |
| 191 | + exemption == img |
| 192 | + } |
| 193 | +
|
| 194 | + _matches_exemption(img, exemption) { |
| 195 | + endswith(exemption, "*") |
| 196 | + prefix := trim_suffix(exemption, "*") |
| 197 | + startswith(img, prefix) |
| 198 | + } |
0 commit comments