From d59972f3a9cc09feb046758155c84653df3fc46f Mon Sep 17 00:00:00 2001 From: Max Smythe Date: Tue, 3 Sep 2024 19:12:12 -0700 Subject: [PATCH] Add CEL to K8sPSPCapabilities template (#535) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add CEL to K8sPSPCapabilities template Signed-off-by: Max Smythe * bump minor version Signed-off-by: Max Smythe --------- Signed-off-by: Max Smythe Co-authored-by: Jaydipkumar Arvindbhai Gabani Co-authored-by: Sertaç Özercan <852750+sozercan@users.noreply.github.com> --- .../capabilities/1.1.0/artifacthub-pkg.yml | 22 ++ .../capabilities/1.1.0/kustomization.yaml | 2 + .../samples/capabilities-demo/constraint.yaml | 14 + .../disallowed_ephemeral.yaml | 21 ++ .../capabilities-demo/example_allowed.yaml | 22 ++ .../capabilities-demo/example_disallowed.yaml | 21 ++ .../samples/capabilities-demo/update.yaml | 26 ++ .../capabilities/1.1.0/suite.yaml | 25 ++ .../capabilities/1.1.0/template.yaml | 224 ++++++++++++++ .../capabilities/template.yaml | 288 +++++++++++------- .../capabilities/constraint.tmpl | 22 +- src/pod-security-policy/capabilities/src.cel | 59 ++++ website/docs/validation/capabilities.md | 288 +++++++++++------- 13 files changed, 802 insertions(+), 232 deletions(-) create mode 100644 artifacthub/library/pod-security-policy/capabilities/1.1.0/artifacthub-pkg.yml create mode 100644 artifacthub/library/pod-security-policy/capabilities/1.1.0/kustomization.yaml create mode 100644 artifacthub/library/pod-security-policy/capabilities/1.1.0/samples/capabilities-demo/constraint.yaml create mode 100644 artifacthub/library/pod-security-policy/capabilities/1.1.0/samples/capabilities-demo/disallowed_ephemeral.yaml create mode 100644 artifacthub/library/pod-security-policy/capabilities/1.1.0/samples/capabilities-demo/example_allowed.yaml create mode 100644 artifacthub/library/pod-security-policy/capabilities/1.1.0/samples/capabilities-demo/example_disallowed.yaml create mode 100644 artifacthub/library/pod-security-policy/capabilities/1.1.0/samples/capabilities-demo/update.yaml create mode 100644 artifacthub/library/pod-security-policy/capabilities/1.1.0/suite.yaml create mode 100644 artifacthub/library/pod-security-policy/capabilities/1.1.0/template.yaml create mode 100644 src/pod-security-policy/capabilities/src.cel diff --git a/artifacthub/library/pod-security-policy/capabilities/1.1.0/artifacthub-pkg.yml b/artifacthub/library/pod-security-policy/capabilities/1.1.0/artifacthub-pkg.yml new file mode 100644 index 000000000..5523381c3 --- /dev/null +++ b/artifacthub/library/pod-security-policy/capabilities/1.1.0/artifacthub-pkg.yml @@ -0,0 +1,22 @@ +version: 1.1.0 +name: k8spspcapabilities +displayName: Capabilities +createdAt: "2024-05-29T23:37:22Z" +description: Controls Linux capabilities on containers. Corresponds to the `allowedCapabilities` and `requiredDropCapabilities` fields in a PodSecurityPolicy. For more information, see https://kubernetes.io/docs/concepts/policy/pod-security-policy/#capabilities +digest: 1b837e4add0952bb782bf0d0bc5e12ac0b0543ee4d23f88e9a282b531bfb8ff5 +license: Apache-2.0 +homeURL: https://open-policy-agent.github.io/gatekeeper-library/website/capabilities +keywords: + - gatekeeper + - open-policy-agent + - policies +readme: |- + # Capabilities + Controls Linux capabilities on containers. Corresponds to the `allowedCapabilities` and `requiredDropCapabilities` fields in a PodSecurityPolicy. For more information, see https://kubernetes.io/docs/concepts/policy/pod-security-policy/#capabilities +install: |- + ### Usage + ```shell + kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper-library/master/artifacthub/library/pod-security-policy/capabilities/1.1.0/template.yaml + ``` +provider: + name: Gatekeeper Library diff --git a/artifacthub/library/pod-security-policy/capabilities/1.1.0/kustomization.yaml b/artifacthub/library/pod-security-policy/capabilities/1.1.0/kustomization.yaml new file mode 100644 index 000000000..7d70d11b7 --- /dev/null +++ b/artifacthub/library/pod-security-policy/capabilities/1.1.0/kustomization.yaml @@ -0,0 +1,2 @@ +resources: + - template.yaml diff --git a/artifacthub/library/pod-security-policy/capabilities/1.1.0/samples/capabilities-demo/constraint.yaml b/artifacthub/library/pod-security-policy/capabilities/1.1.0/samples/capabilities-demo/constraint.yaml new file mode 100644 index 000000000..3f856082f --- /dev/null +++ b/artifacthub/library/pod-security-policy/capabilities/1.1.0/samples/capabilities-demo/constraint.yaml @@ -0,0 +1,14 @@ +apiVersion: constraints.gatekeeper.sh/v1beta1 +kind: K8sPSPCapabilities +metadata: + name: capabilities-demo +spec: + match: + kinds: + - apiGroups: [""] + kinds: ["Pod"] + namespaces: + - "default" + parameters: + allowedCapabilities: ["something"] + requiredDropCapabilities: ["must_drop"] diff --git a/artifacthub/library/pod-security-policy/capabilities/1.1.0/samples/capabilities-demo/disallowed_ephemeral.yaml b/artifacthub/library/pod-security-policy/capabilities/1.1.0/samples/capabilities-demo/disallowed_ephemeral.yaml new file mode 100644 index 000000000..5467c826e --- /dev/null +++ b/artifacthub/library/pod-security-policy/capabilities/1.1.0/samples/capabilities-demo/disallowed_ephemeral.yaml @@ -0,0 +1,21 @@ +apiVersion: v1 +kind: Pod +metadata: + name: opa-disallowed + labels: + owner: me.agilebank.demo +spec: + ephemeralContainers: + - name: opa + image: openpolicyagent/opa:0.9.2 + args: + - "run" + - "--server" + - "--addr=localhost:8080" + securityContext: + capabilities: + add: ["disallowedcapability"] + resources: + limits: + cpu: "100m" + memory: "30Mi" diff --git a/artifacthub/library/pod-security-policy/capabilities/1.1.0/samples/capabilities-demo/example_allowed.yaml b/artifacthub/library/pod-security-policy/capabilities/1.1.0/samples/capabilities-demo/example_allowed.yaml new file mode 100644 index 000000000..41bf6a0ed --- /dev/null +++ b/artifacthub/library/pod-security-policy/capabilities/1.1.0/samples/capabilities-demo/example_allowed.yaml @@ -0,0 +1,22 @@ +apiVersion: v1 +kind: Pod +metadata: + name: opa-allowed + labels: + owner: me.agilebank.demo +spec: + containers: + - name: opa + image: openpolicyagent/opa:0.9.2 + args: + - "run" + - "--server" + - "--addr=localhost:8080" + securityContext: + capabilities: + add: ["something"] + drop: ["must_drop", "another_one"] + resources: + limits: + cpu: "100m" + memory: "30Mi" diff --git a/artifacthub/library/pod-security-policy/capabilities/1.1.0/samples/capabilities-demo/example_disallowed.yaml b/artifacthub/library/pod-security-policy/capabilities/1.1.0/samples/capabilities-demo/example_disallowed.yaml new file mode 100644 index 000000000..fdd886189 --- /dev/null +++ b/artifacthub/library/pod-security-policy/capabilities/1.1.0/samples/capabilities-demo/example_disallowed.yaml @@ -0,0 +1,21 @@ +apiVersion: v1 +kind: Pod +metadata: + name: opa-disallowed + labels: + owner: me.agilebank.demo +spec: + containers: + - name: opa + image: openpolicyagent/opa:0.9.2 + args: + - "run" + - "--server" + - "--addr=localhost:8080" + securityContext: + capabilities: + add: ["disallowedcapability"] + resources: + limits: + cpu: "100m" + memory: "30Mi" \ No newline at end of file diff --git a/artifacthub/library/pod-security-policy/capabilities/1.1.0/samples/capabilities-demo/update.yaml b/artifacthub/library/pod-security-policy/capabilities/1.1.0/samples/capabilities-demo/update.yaml new file mode 100644 index 000000000..df8ea0070 --- /dev/null +++ b/artifacthub/library/pod-security-policy/capabilities/1.1.0/samples/capabilities-demo/update.yaml @@ -0,0 +1,26 @@ +kind: AdmissionReview +apiVersion: admission.k8s.io/v1beta1 +request: + operation: "UPDATE" + object: + apiVersion: v1 + kind: Pod + metadata: + name: opa-disallowed + labels: + owner: me.agilebank.demo + spec: + containers: + - name: opa + image: openpolicyagent/opa:0.9.2 + args: + - "run" + - "--server" + - "--addr=localhost:8080" + securityContext: + capabilities: + add: ["disallowedcapability"] + resources: + limits: + cpu: "100m" + memory: "30Mi" diff --git a/artifacthub/library/pod-security-policy/capabilities/1.1.0/suite.yaml b/artifacthub/library/pod-security-policy/capabilities/1.1.0/suite.yaml new file mode 100644 index 000000000..48c2fcb46 --- /dev/null +++ b/artifacthub/library/pod-security-policy/capabilities/1.1.0/suite.yaml @@ -0,0 +1,25 @@ +kind: Suite +apiVersion: test.gatekeeper.sh/v1alpha1 +metadata: + name: capabilities +tests: + - name: capabilities + template: template.yaml + constraint: samples/capabilities-demo/constraint.yaml + cases: + - name: example-disallowed + object: samples/capabilities-demo/example_disallowed.yaml + assertions: + - violations: yes + - name: example-allowed + object: samples/capabilities-demo/example_allowed.yaml + assertions: + - violations: no + - name: disallowed-ephemeral + object: samples/capabilities-demo/disallowed_ephemeral.yaml + assertions: + - violations: yes + - name: update + object: samples/capabilities-demo/update.yaml + assertions: + - violations: no diff --git a/artifacthub/library/pod-security-policy/capabilities/1.1.0/template.yaml b/artifacthub/library/pod-security-policy/capabilities/1.1.0/template.yaml new file mode 100644 index 000000000..a9766fd60 --- /dev/null +++ b/artifacthub/library/pod-security-policy/capabilities/1.1.0/template.yaml @@ -0,0 +1,224 @@ +apiVersion: templates.gatekeeper.sh/v1 +kind: ConstraintTemplate +metadata: + name: k8spspcapabilities + annotations: + metadata.gatekeeper.sh/title: "Capabilities" + metadata.gatekeeper.sh/version: 1.1.0 + description: >- + Controls Linux capabilities on containers. Corresponds to the + `allowedCapabilities` and `requiredDropCapabilities` fields in a + PodSecurityPolicy. For more information, see + https://kubernetes.io/docs/concepts/policy/pod-security-policy/#capabilities +spec: + crd: + spec: + names: + kind: K8sPSPCapabilities + validation: + # Schema for the `parameters` field + openAPIV3Schema: + type: object + description: >- + Controls Linux capabilities on containers. Corresponds to the + `allowedCapabilities` and `requiredDropCapabilities` fields in a + PodSecurityPolicy. For more information, see + https://kubernetes.io/docs/concepts/policy/pod-security-policy/#capabilities + properties: + exemptImages: + description: >- + Any container that uses an image that matches an entry in this list will be excluded + from enforcement. Prefix-matching can be signified with `*`. For example: `my-image-*`. + + It is recommended that users use the fully-qualified Docker image name (e.g. start with a domain name) + in order to avoid unexpectedly exempting images from an untrusted repository. + type: array + items: + type: string + allowedCapabilities: + type: array + description: "A list of Linux capabilities that can be added to a container." + items: + type: string + requiredDropCapabilities: + type: array + description: "A list of Linux capabilities that are required to be dropped from a container." + items: + type: string + targets: + - target: admission.k8s.gatekeeper.sh + code: + - engine: K8sNativeValidation + source: + variables: + - name: containers + expression: 'has(variables.anyObject.spec.containers) ? variables.anyObject.spec.containers : []' + - name: initContainers + expression: 'has(variables.anyObject.spec.initContainers) ? variables.anyObject.spec.initContainers : []' + - name: ephemeralContainers + expression: 'has(variables.anyObject.spec.ephemeralContainers) ? variables.anyObject.spec.ephemeralContainers : []' + - name: allContainers + expression: 'variables.containers + variables.initContainers + variables.ephemeralContainers' + - name: exemptImagePrefixes + expression: | + !has(variables.params.exemptImages) ? [] : + variables.params.exemptImages.filter(image, image.endsWith("*")).map(image, string(image).replace("*", "")) + - name: exemptImageExplicit + expression: | + !has(variables.params.exemptImages) ? [] : + variables.params.exemptImages.filter(image, !image.endsWith("*")) + - name: exemptImages + expression: | + (variables.containers + variables.initContainers + variables.ephemeralContainers).filter(container, + container.image in variables.exemptImageExplicit || + variables.exemptImagePrefixes.exists(exemption, string(container.image).startsWith(exemption)) + ).map(container, container.image) + - name: allowedCapabilities + expression: 'has(variables.params.allowedCapabilities) ? variables.params.allowedCapabilities : []' + - name: allCapabilitiesAllowed + expression: '"*" in variables.allowedCapabilities' + - name: disallowedCapabilitiesByContainer + expression: | + variables.allContainers.map(container, !(container.image in variables.exemptImages) && + !variables.allCapabilitiesAllowed && has(container.securityContext) && has(container.securityContext.capabilities) && has(container.securityContext.capabilities.add) && + container.securityContext.capabilities.add.exists(capability, !(capability in variables.allowedCapabilities)), + [container.name, dyn(container.securityContext.capabilities.add.filter(capability, !(capability in variables.allowedCapabilities)).join(", "))] + ) + - name: requiredDropCapabilities + expression: 'has(variables.params.requiredDropCapabilities) ? variables.params.requiredDropCapabilities : []' + - name: missingDropCapabilitiesByContainer + expression: | + variables.allContainers.map(container, !(container.image in variables.exemptImages) && + size(variables.requiredDropCapabilities) > 0 && ( + !has(container.securityContext) || !has(container.securityContext.capabilities) || !has(container.securityContext.capabilities.drop) || ( + !("all" in container.securityContext.capabilities.drop) && + variables.requiredDropCapabilities.exists(capability, !(capability in container.securityContext.capabilities.drop)) + ) + ), + [container.name, + !has(container.securityContext) ? variables.requiredDropCapabilities : + !has(container.securityContext.capabilities) ? variables.requiredDropCapabilities : + !has(container.securityContext.capabilities.drop) ? variables.requiredDropCapabilities : + variables.requiredDropCapabilities.filter(capability, !(capability in container.securityContext.capabilities.drop)) + ] + ) + validations: + - expression: '(has(request.operation) && request.operation == "UPDATE") || size(variables.disallowedCapabilitiesByContainer) == 0' + messageExpression: | + "containers have disallowed capabilities: " + variables.disallowedCapabilitiesByContainer.map(pair, "{container: " + pair[0] + ", capabilities: [" + pair[1] + "]}").join(", ") + - expression: '(has(request.operation) && request.operation == "UPDATE") || size(variables.missingDropCapabilitiesByContainer) == 0' + messageExpression: | + "containers are not dropping all required capabilities: " + variables.missingDropCapabilitiesByContainer.map(pair, "{container: " + pair[0] + ", capabilities: [" + pair[1].join(", ") + "]}").join(", ") + - engine: Rego + source: + rego: | + package capabilities + + import data.lib.exclude_update.is_update + import data.lib.exempt_container.is_exempt + + violation[{"msg": msg}] { + # spec.containers.securityContext.capabilities field is immutable. + not is_update(input.review) + + container := input.review.object.spec.containers[_] + not is_exempt(container) + has_disallowed_capabilities(container) + msg := sprintf("container <%v> has a disallowed capability. Allowed capabilities are %v", [container.name, get_default(input.parameters, "allowedCapabilities", "NONE")]) + } + + violation[{"msg": msg}] { + not is_update(input.review) + container := input.review.object.spec.containers[_] + not is_exempt(container) + missing_drop_capabilities(container) + msg := sprintf("container <%v> is not dropping all required capabilities. Container must drop all of %v or \"ALL\"", [container.name, input.parameters.requiredDropCapabilities]) + } + + + + violation[{"msg": msg}] { + not is_update(input.review) + container := input.review.object.spec.initContainers[_] + not is_exempt(container) + has_disallowed_capabilities(container) + msg := sprintf("init container <%v> has a disallowed capability. Allowed capabilities are %v", [container.name, get_default(input.parameters, "allowedCapabilities", "NONE")]) + } + + violation[{"msg": msg}] { + not is_update(input.review) + container := input.review.object.spec.initContainers[_] + not is_exempt(container) + missing_drop_capabilities(container) + msg := sprintf("init container <%v> is not dropping all required capabilities. Container must drop all of %v or \"ALL\"", [container.name, input.parameters.requiredDropCapabilities]) + } + + + + violation[{"msg": msg}] { + not is_update(input.review) + container := input.review.object.spec.ephemeralContainers[_] + not is_exempt(container) + has_disallowed_capabilities(container) + msg := sprintf("ephemeral container <%v> has a disallowed capability. Allowed capabilities are %v", [container.name, get_default(input.parameters, "allowedCapabilities", "NONE")]) + } + + violation[{"msg": msg}] { + not is_update(input.review) + container := input.review.object.spec.ephemeralContainers[_] + not is_exempt(container) + missing_drop_capabilities(container) + msg := sprintf("ephemeral container <%v> is not dropping all required capabilities. Container must drop all of %v or \"ALL\"", [container.name, input.parameters.requiredDropCapabilities]) + } + + + has_disallowed_capabilities(container) { + allowed := {c | c := lower(input.parameters.allowedCapabilities[_])} + not allowed["*"] + capabilities := {c | c := lower(container.securityContext.capabilities.add[_])} + + count(capabilities - allowed) > 0 + } + + missing_drop_capabilities(container) { + must_drop := {c | c := lower(input.parameters.requiredDropCapabilities[_])} + all := {"all"} + dropped := {c | c := lower(container.securityContext.capabilities.drop[_])} + + count(must_drop - dropped) > 0 + count(all - dropped) > 0 + } + + get_default(obj, param, _) := obj[param] + + get_default(obj, param, _default) := _default { + not obj[param] + not obj[param] == false + } + libs: + - | + package lib.exclude_update + + is_update(review) { + review.operation == "UPDATE" + } + - | + package lib.exempt_container + + is_exempt(container) { + exempt_images := object.get(object.get(input, "parameters", {}), "exemptImages", []) + img := container.image + exemption := exempt_images[_] + _matches_exemption(img, exemption) + } + + _matches_exemption(img, exemption) { + not endswith(exemption, "*") + exemption == img + } + + _matches_exemption(img, exemption) { + endswith(exemption, "*") + prefix := trim_suffix(exemption, "*") + startswith(img, prefix) + } diff --git a/library/pod-security-policy/capabilities/template.yaml b/library/pod-security-policy/capabilities/template.yaml index 2501a1538..a9766fd60 100644 --- a/library/pod-security-policy/capabilities/template.yaml +++ b/library/pod-security-policy/capabilities/template.yaml @@ -4,7 +4,7 @@ metadata: name: k8spspcapabilities annotations: metadata.gatekeeper.sh/title: "Capabilities" - metadata.gatekeeper.sh/version: 1.0.2 + metadata.gatekeeper.sh/version: 1.1.0 description: >- Controls Linux capabilities on containers. Corresponds to the `allowedCapabilities` and `requiredDropCapabilities` fields in a @@ -47,114 +47,178 @@ spec: type: string targets: - target: admission.k8s.gatekeeper.sh - rego: | - package capabilities - - import data.lib.exclude_update.is_update - import data.lib.exempt_container.is_exempt - - violation[{"msg": msg}] { - # spec.containers.securityContext.capabilities field is immutable. - not is_update(input.review) - - container := input.review.object.spec.containers[_] - not is_exempt(container) - has_disallowed_capabilities(container) - msg := sprintf("container <%v> has a disallowed capability. Allowed capabilities are %v", [container.name, get_default(input.parameters, "allowedCapabilities", "NONE")]) - } - - violation[{"msg": msg}] { - not is_update(input.review) - container := input.review.object.spec.containers[_] - not is_exempt(container) - missing_drop_capabilities(container) - msg := sprintf("container <%v> is not dropping all required capabilities. Container must drop all of %v or \"ALL\"", [container.name, input.parameters.requiredDropCapabilities]) - } - - - - violation[{"msg": msg}] { - not is_update(input.review) - container := input.review.object.spec.initContainers[_] - not is_exempt(container) - has_disallowed_capabilities(container) - msg := sprintf("init container <%v> has a disallowed capability. Allowed capabilities are %v", [container.name, get_default(input.parameters, "allowedCapabilities", "NONE")]) - } - - violation[{"msg": msg}] { - not is_update(input.review) - container := input.review.object.spec.initContainers[_] - not is_exempt(container) - missing_drop_capabilities(container) - msg := sprintf("init container <%v> is not dropping all required capabilities. Container must drop all of %v or \"ALL\"", [container.name, input.parameters.requiredDropCapabilities]) - } - - - - violation[{"msg": msg}] { - not is_update(input.review) - container := input.review.object.spec.ephemeralContainers[_] - not is_exempt(container) - has_disallowed_capabilities(container) - msg := sprintf("ephemeral container <%v> has a disallowed capability. Allowed capabilities are %v", [container.name, get_default(input.parameters, "allowedCapabilities", "NONE")]) - } - - violation[{"msg": msg}] { - not is_update(input.review) - container := input.review.object.spec.ephemeralContainers[_] - not is_exempt(container) - missing_drop_capabilities(container) - msg := sprintf("ephemeral container <%v> is not dropping all required capabilities. Container must drop all of %v or \"ALL\"", [container.name, input.parameters.requiredDropCapabilities]) - } - - - has_disallowed_capabilities(container) { - allowed := {c | c := lower(input.parameters.allowedCapabilities[_])} - not allowed["*"] - capabilities := {c | c := lower(container.securityContext.capabilities.add[_])} - - count(capabilities - allowed) > 0 - } - - missing_drop_capabilities(container) { - must_drop := {c | c := lower(input.parameters.requiredDropCapabilities[_])} - all := {"all"} - dropped := {c | c := lower(container.securityContext.capabilities.drop[_])} - - count(must_drop - dropped) > 0 - count(all - dropped) > 0 - } - - get_default(obj, param, _) := obj[param] - - get_default(obj, param, _default) := _default { - not obj[param] - not obj[param] == false - } - libs: - - | - package lib.exclude_update - - is_update(review) { - review.operation == "UPDATE" - } - - | - package lib.exempt_container - - is_exempt(container) { - exempt_images := object.get(object.get(input, "parameters", {}), "exemptImages", []) - img := container.image - exemption := exempt_images[_] - _matches_exemption(img, exemption) - } - - _matches_exemption(img, exemption) { - not endswith(exemption, "*") - exemption == img - } - - _matches_exemption(img, exemption) { - endswith(exemption, "*") - prefix := trim_suffix(exemption, "*") - startswith(img, prefix) - } + code: + - engine: K8sNativeValidation + source: + variables: + - name: containers + expression: 'has(variables.anyObject.spec.containers) ? variables.anyObject.spec.containers : []' + - name: initContainers + expression: 'has(variables.anyObject.spec.initContainers) ? variables.anyObject.spec.initContainers : []' + - name: ephemeralContainers + expression: 'has(variables.anyObject.spec.ephemeralContainers) ? variables.anyObject.spec.ephemeralContainers : []' + - name: allContainers + expression: 'variables.containers + variables.initContainers + variables.ephemeralContainers' + - name: exemptImagePrefixes + expression: | + !has(variables.params.exemptImages) ? [] : + variables.params.exemptImages.filter(image, image.endsWith("*")).map(image, string(image).replace("*", "")) + - name: exemptImageExplicit + expression: | + !has(variables.params.exemptImages) ? [] : + variables.params.exemptImages.filter(image, !image.endsWith("*")) + - name: exemptImages + expression: | + (variables.containers + variables.initContainers + variables.ephemeralContainers).filter(container, + container.image in variables.exemptImageExplicit || + variables.exemptImagePrefixes.exists(exemption, string(container.image).startsWith(exemption)) + ).map(container, container.image) + - name: allowedCapabilities + expression: 'has(variables.params.allowedCapabilities) ? variables.params.allowedCapabilities : []' + - name: allCapabilitiesAllowed + expression: '"*" in variables.allowedCapabilities' + - name: disallowedCapabilitiesByContainer + expression: | + variables.allContainers.map(container, !(container.image in variables.exemptImages) && + !variables.allCapabilitiesAllowed && has(container.securityContext) && has(container.securityContext.capabilities) && has(container.securityContext.capabilities.add) && + container.securityContext.capabilities.add.exists(capability, !(capability in variables.allowedCapabilities)), + [container.name, dyn(container.securityContext.capabilities.add.filter(capability, !(capability in variables.allowedCapabilities)).join(", "))] + ) + - name: requiredDropCapabilities + expression: 'has(variables.params.requiredDropCapabilities) ? variables.params.requiredDropCapabilities : []' + - name: missingDropCapabilitiesByContainer + expression: | + variables.allContainers.map(container, !(container.image in variables.exemptImages) && + size(variables.requiredDropCapabilities) > 0 && ( + !has(container.securityContext) || !has(container.securityContext.capabilities) || !has(container.securityContext.capabilities.drop) || ( + !("all" in container.securityContext.capabilities.drop) && + variables.requiredDropCapabilities.exists(capability, !(capability in container.securityContext.capabilities.drop)) + ) + ), + [container.name, + !has(container.securityContext) ? variables.requiredDropCapabilities : + !has(container.securityContext.capabilities) ? variables.requiredDropCapabilities : + !has(container.securityContext.capabilities.drop) ? variables.requiredDropCapabilities : + variables.requiredDropCapabilities.filter(capability, !(capability in container.securityContext.capabilities.drop)) + ] + ) + validations: + - expression: '(has(request.operation) && request.operation == "UPDATE") || size(variables.disallowedCapabilitiesByContainer) == 0' + messageExpression: | + "containers have disallowed capabilities: " + variables.disallowedCapabilitiesByContainer.map(pair, "{container: " + pair[0] + ", capabilities: [" + pair[1] + "]}").join(", ") + - expression: '(has(request.operation) && request.operation == "UPDATE") || size(variables.missingDropCapabilitiesByContainer) == 0' + messageExpression: | + "containers are not dropping all required capabilities: " + variables.missingDropCapabilitiesByContainer.map(pair, "{container: " + pair[0] + ", capabilities: [" + pair[1].join(", ") + "]}").join(", ") + - engine: Rego + source: + rego: | + package capabilities + + import data.lib.exclude_update.is_update + import data.lib.exempt_container.is_exempt + + violation[{"msg": msg}] { + # spec.containers.securityContext.capabilities field is immutable. + not is_update(input.review) + + container := input.review.object.spec.containers[_] + not is_exempt(container) + has_disallowed_capabilities(container) + msg := sprintf("container <%v> has a disallowed capability. Allowed capabilities are %v", [container.name, get_default(input.parameters, "allowedCapabilities", "NONE")]) + } + + violation[{"msg": msg}] { + not is_update(input.review) + container := input.review.object.spec.containers[_] + not is_exempt(container) + missing_drop_capabilities(container) + msg := sprintf("container <%v> is not dropping all required capabilities. Container must drop all of %v or \"ALL\"", [container.name, input.parameters.requiredDropCapabilities]) + } + + + + violation[{"msg": msg}] { + not is_update(input.review) + container := input.review.object.spec.initContainers[_] + not is_exempt(container) + has_disallowed_capabilities(container) + msg := sprintf("init container <%v> has a disallowed capability. Allowed capabilities are %v", [container.name, get_default(input.parameters, "allowedCapabilities", "NONE")]) + } + + violation[{"msg": msg}] { + not is_update(input.review) + container := input.review.object.spec.initContainers[_] + not is_exempt(container) + missing_drop_capabilities(container) + msg := sprintf("init container <%v> is not dropping all required capabilities. Container must drop all of %v or \"ALL\"", [container.name, input.parameters.requiredDropCapabilities]) + } + + + + violation[{"msg": msg}] { + not is_update(input.review) + container := input.review.object.spec.ephemeralContainers[_] + not is_exempt(container) + has_disallowed_capabilities(container) + msg := sprintf("ephemeral container <%v> has a disallowed capability. Allowed capabilities are %v", [container.name, get_default(input.parameters, "allowedCapabilities", "NONE")]) + } + + violation[{"msg": msg}] { + not is_update(input.review) + container := input.review.object.spec.ephemeralContainers[_] + not is_exempt(container) + missing_drop_capabilities(container) + msg := sprintf("ephemeral container <%v> is not dropping all required capabilities. Container must drop all of %v or \"ALL\"", [container.name, input.parameters.requiredDropCapabilities]) + } + + + has_disallowed_capabilities(container) { + allowed := {c | c := lower(input.parameters.allowedCapabilities[_])} + not allowed["*"] + capabilities := {c | c := lower(container.securityContext.capabilities.add[_])} + + count(capabilities - allowed) > 0 + } + + missing_drop_capabilities(container) { + must_drop := {c | c := lower(input.parameters.requiredDropCapabilities[_])} + all := {"all"} + dropped := {c | c := lower(container.securityContext.capabilities.drop[_])} + + count(must_drop - dropped) > 0 + count(all - dropped) > 0 + } + + get_default(obj, param, _) := obj[param] + + get_default(obj, param, _default) := _default { + not obj[param] + not obj[param] == false + } + libs: + - | + package lib.exclude_update + + is_update(review) { + review.operation == "UPDATE" + } + - | + package lib.exempt_container + + is_exempt(container) { + exempt_images := object.get(object.get(input, "parameters", {}), "exemptImages", []) + img := container.image + exemption := exempt_images[_] + _matches_exemption(img, exemption) + } + + _matches_exemption(img, exemption) { + not endswith(exemption, "*") + exemption == img + } + + _matches_exemption(img, exemption) { + endswith(exemption, "*") + prefix := trim_suffix(exemption, "*") + startswith(img, prefix) + } diff --git a/src/pod-security-policy/capabilities/constraint.tmpl b/src/pod-security-policy/capabilities/constraint.tmpl index 9ca6bed26..02c8abe32 100644 --- a/src/pod-security-policy/capabilities/constraint.tmpl +++ b/src/pod-security-policy/capabilities/constraint.tmpl @@ -4,7 +4,7 @@ metadata: name: k8spspcapabilities annotations: metadata.gatekeeper.sh/title: "Capabilities" - metadata.gatekeeper.sh/version: 1.0.2 + metadata.gatekeeper.sh/version: 1.1.0 description: >- Controls Linux capabilities on containers. Corresponds to the `allowedCapabilities` and `requiredDropCapabilities` fields in a @@ -47,10 +47,16 @@ spec: type: string targets: - target: admission.k8s.gatekeeper.sh - rego: | -{{ file.Read "src/pod-security-policy/capabilities/src.rego" | strings.Indent 8 | strings.TrimSuffix "\n" }} - libs: - - | -{{ file.Read "src/pod-security-policy/capabilities/lib_exclude_update.rego" | strings.Indent 10 | strings.TrimSuffix "\n" }} - - | -{{ file.Read "src/pod-security-policy/capabilities/lib_exempt_container.rego" | strings.Indent 10 | strings.TrimSuffix "\n" }} + code: + - engine: K8sNativeValidation + source: +{{ file.Read "src/pod-security-policy/capabilities/src.cel" | strings.Indent 10 | strings.TrimSuffix "\n" }} + - engine: Rego + source: + rego: | +{{ file.Read "src/pod-security-policy/capabilities/src.rego" | strings.Indent 12 | strings.TrimSuffix "\n" }} + libs: + - | +{{ file.Read "src/pod-security-policy/capabilities/lib_exclude_update.rego" | strings.Indent 12 | strings.TrimSuffix "\n" }} + - | +{{ file.Read "src/pod-security-policy/capabilities/lib_exempt_container.rego" | strings.Indent 12 | strings.TrimSuffix "\n" }} diff --git a/src/pod-security-policy/capabilities/src.cel b/src/pod-security-policy/capabilities/src.cel new file mode 100644 index 000000000..42c70ff6a --- /dev/null +++ b/src/pod-security-policy/capabilities/src.cel @@ -0,0 +1,59 @@ +variables: +- name: containers + expression: 'has(variables.anyObject.spec.containers) ? variables.anyObject.spec.containers : []' +- name: initContainers + expression: 'has(variables.anyObject.spec.initContainers) ? variables.anyObject.spec.initContainers : []' +- name: ephemeralContainers + expression: 'has(variables.anyObject.spec.ephemeralContainers) ? variables.anyObject.spec.ephemeralContainers : []' +- name: allContainers + expression: 'variables.containers + variables.initContainers + variables.ephemeralContainers' +- name: exemptImagePrefixes + expression: | + !has(variables.params.exemptImages) ? [] : + variables.params.exemptImages.filter(image, image.endsWith("*")).map(image, string(image).replace("*", "")) +- name: exemptImageExplicit + expression: | + !has(variables.params.exemptImages) ? [] : + variables.params.exemptImages.filter(image, !image.endsWith("*")) +- name: exemptImages + expression: | + (variables.containers + variables.initContainers + variables.ephemeralContainers).filter(container, + container.image in variables.exemptImageExplicit || + variables.exemptImagePrefixes.exists(exemption, string(container.image).startsWith(exemption)) + ).map(container, container.image) +- name: allowedCapabilities + expression: 'has(variables.params.allowedCapabilities) ? variables.params.allowedCapabilities : []' +- name: allCapabilitiesAllowed + expression: '"*" in variables.allowedCapabilities' +- name: disallowedCapabilitiesByContainer + expression: | + variables.allContainers.map(container, !(container.image in variables.exemptImages) && + !variables.allCapabilitiesAllowed && has(container.securityContext) && has(container.securityContext.capabilities) && has(container.securityContext.capabilities.add) && + container.securityContext.capabilities.add.exists(capability, !(capability in variables.allowedCapabilities)), + [container.name, dyn(container.securityContext.capabilities.add.filter(capability, !(capability in variables.allowedCapabilities)).join(", "))] + ) +- name: requiredDropCapabilities + expression: 'has(variables.params.requiredDropCapabilities) ? variables.params.requiredDropCapabilities : []' +- name: missingDropCapabilitiesByContainer + expression: | + variables.allContainers.map(container, !(container.image in variables.exemptImages) && + size(variables.requiredDropCapabilities) > 0 && ( + !has(container.securityContext) || !has(container.securityContext.capabilities) || !has(container.securityContext.capabilities.drop) || ( + !("all" in container.securityContext.capabilities.drop) && + variables.requiredDropCapabilities.exists(capability, !(capability in container.securityContext.capabilities.drop)) + ) + ), + [container.name, + !has(container.securityContext) ? variables.requiredDropCapabilities : + !has(container.securityContext.capabilities) ? variables.requiredDropCapabilities : + !has(container.securityContext.capabilities.drop) ? variables.requiredDropCapabilities : + variables.requiredDropCapabilities.filter(capability, !(capability in container.securityContext.capabilities.drop)) + ] + ) +validations: +- expression: '(has(request.operation) && request.operation == "UPDATE") || size(variables.disallowedCapabilitiesByContainer) == 0' + messageExpression: | + "containers have disallowed capabilities: " + variables.disallowedCapabilitiesByContainer.map(pair, "{container: " + pair[0] + ", capabilities: [" + pair[1] + "]}").join(", ") +- expression: '(has(request.operation) && request.operation == "UPDATE") || size(variables.missingDropCapabilitiesByContainer) == 0' + messageExpression: | + "containers are not dropping all required capabilities: " + variables.missingDropCapabilitiesByContainer.map(pair, "{container: " + pair[0] + ", capabilities: [" + pair[1].join(", ") + "]}").join(", ") \ No newline at end of file diff --git a/website/docs/validation/capabilities.md b/website/docs/validation/capabilities.md index 5048c6627..b119ae89b 100644 --- a/website/docs/validation/capabilities.md +++ b/website/docs/validation/capabilities.md @@ -16,7 +16,7 @@ metadata: name: k8spspcapabilities annotations: metadata.gatekeeper.sh/title: "Capabilities" - metadata.gatekeeper.sh/version: 1.0.2 + metadata.gatekeeper.sh/version: 1.1.0 description: >- Controls Linux capabilities on containers. Corresponds to the `allowedCapabilities` and `requiredDropCapabilities` fields in a @@ -59,117 +59,181 @@ spec: type: string targets: - target: admission.k8s.gatekeeper.sh - rego: | - package capabilities - - import data.lib.exclude_update.is_update - import data.lib.exempt_container.is_exempt - - violation[{"msg": msg}] { - # spec.containers.securityContext.capabilities field is immutable. - not is_update(input.review) - - container := input.review.object.spec.containers[_] - not is_exempt(container) - has_disallowed_capabilities(container) - msg := sprintf("container <%v> has a disallowed capability. Allowed capabilities are %v", [container.name, get_default(input.parameters, "allowedCapabilities", "NONE")]) - } - - violation[{"msg": msg}] { - not is_update(input.review) - container := input.review.object.spec.containers[_] - not is_exempt(container) - missing_drop_capabilities(container) - msg := sprintf("container <%v> is not dropping all required capabilities. Container must drop all of %v or \"ALL\"", [container.name, input.parameters.requiredDropCapabilities]) - } - - - - violation[{"msg": msg}] { - not is_update(input.review) - container := input.review.object.spec.initContainers[_] - not is_exempt(container) - has_disallowed_capabilities(container) - msg := sprintf("init container <%v> has a disallowed capability. Allowed capabilities are %v", [container.name, get_default(input.parameters, "allowedCapabilities", "NONE")]) - } - - violation[{"msg": msg}] { - not is_update(input.review) - container := input.review.object.spec.initContainers[_] - not is_exempt(container) - missing_drop_capabilities(container) - msg := sprintf("init container <%v> is not dropping all required capabilities. Container must drop all of %v or \"ALL\"", [container.name, input.parameters.requiredDropCapabilities]) - } - - - - violation[{"msg": msg}] { - not is_update(input.review) - container := input.review.object.spec.ephemeralContainers[_] - not is_exempt(container) - has_disallowed_capabilities(container) - msg := sprintf("ephemeral container <%v> has a disallowed capability. Allowed capabilities are %v", [container.name, get_default(input.parameters, "allowedCapabilities", "NONE")]) - } - - violation[{"msg": msg}] { - not is_update(input.review) - container := input.review.object.spec.ephemeralContainers[_] - not is_exempt(container) - missing_drop_capabilities(container) - msg := sprintf("ephemeral container <%v> is not dropping all required capabilities. Container must drop all of %v or \"ALL\"", [container.name, input.parameters.requiredDropCapabilities]) - } - - - has_disallowed_capabilities(container) { - allowed := {c | c := lower(input.parameters.allowedCapabilities[_])} - not allowed["*"] - capabilities := {c | c := lower(container.securityContext.capabilities.add[_])} - - count(capabilities - allowed) > 0 - } - - missing_drop_capabilities(container) { - must_drop := {c | c := lower(input.parameters.requiredDropCapabilities[_])} - all := {"all"} - dropped := {c | c := lower(container.securityContext.capabilities.drop[_])} - - count(must_drop - dropped) > 0 - count(all - dropped) > 0 - } - - get_default(obj, param, _) := obj[param] - - get_default(obj, param, _default) := _default { - not obj[param] - not obj[param] == false - } - libs: - - | - package lib.exclude_update - - is_update(review) { - review.operation == "UPDATE" - } - - | - package lib.exempt_container - - is_exempt(container) { - exempt_images := object.get(object.get(input, "parameters", {}), "exemptImages", []) - img := container.image - exemption := exempt_images[_] - _matches_exemption(img, exemption) - } - - _matches_exemption(img, exemption) { - not endswith(exemption, "*") - exemption == img - } - - _matches_exemption(img, exemption) { - endswith(exemption, "*") - prefix := trim_suffix(exemption, "*") - startswith(img, prefix) - } + code: + - engine: K8sNativeValidation + source: + variables: + - name: containers + expression: 'has(variables.anyObject.spec.containers) ? variables.anyObject.spec.containers : []' + - name: initContainers + expression: 'has(variables.anyObject.spec.initContainers) ? variables.anyObject.spec.initContainers : []' + - name: ephemeralContainers + expression: 'has(variables.anyObject.spec.ephemeralContainers) ? variables.anyObject.spec.ephemeralContainers : []' + - name: allContainers + expression: 'variables.containers + variables.initContainers + variables.ephemeralContainers' + - name: exemptImagePrefixes + expression: | + !has(variables.params.exemptImages) ? [] : + variables.params.exemptImages.filter(image, image.endsWith("*")).map(image, string(image).replace("*", "")) + - name: exemptImageExplicit + expression: | + !has(variables.params.exemptImages) ? [] : + variables.params.exemptImages.filter(image, !image.endsWith("*")) + - name: exemptImages + expression: | + (variables.containers + variables.initContainers + variables.ephemeralContainers).filter(container, + container.image in variables.exemptImageExplicit || + variables.exemptImagePrefixes.exists(exemption, string(container.image).startsWith(exemption)) + ).map(container, container.image) + - name: allowedCapabilities + expression: 'has(variables.params.allowedCapabilities) ? variables.params.allowedCapabilities : []' + - name: allCapabilitiesAllowed + expression: '"*" in variables.allowedCapabilities' + - name: disallowedCapabilitiesByContainer + expression: | + variables.allContainers.map(container, !(container.image in variables.exemptImages) && + !variables.allCapabilitiesAllowed && has(container.securityContext) && has(container.securityContext.capabilities) && has(container.securityContext.capabilities.add) && + container.securityContext.capabilities.add.exists(capability, !(capability in variables.allowedCapabilities)), + [container.name, dyn(container.securityContext.capabilities.add.filter(capability, !(capability in variables.allowedCapabilities)).join(", "))] + ) + - name: requiredDropCapabilities + expression: 'has(variables.params.requiredDropCapabilities) ? variables.params.requiredDropCapabilities : []' + - name: missingDropCapabilitiesByContainer + expression: | + variables.allContainers.map(container, !(container.image in variables.exemptImages) && + size(variables.requiredDropCapabilities) > 0 && ( + !has(container.securityContext) || !has(container.securityContext.capabilities) || !has(container.securityContext.capabilities.drop) || ( + !("all" in container.securityContext.capabilities.drop) && + variables.requiredDropCapabilities.exists(capability, !(capability in container.securityContext.capabilities.drop)) + ) + ), + [container.name, + !has(container.securityContext) ? variables.requiredDropCapabilities : + !has(container.securityContext.capabilities) ? variables.requiredDropCapabilities : + !has(container.securityContext.capabilities.drop) ? variables.requiredDropCapabilities : + variables.requiredDropCapabilities.filter(capability, !(capability in container.securityContext.capabilities.drop)) + ] + ) + validations: + - expression: '(has(request.operation) && request.operation == "UPDATE") || size(variables.disallowedCapabilitiesByContainer) == 0' + messageExpression: | + "containers have disallowed capabilities: " + variables.disallowedCapabilitiesByContainer.map(pair, "{container: " + pair[0] + ", capabilities: [" + pair[1] + "]}").join(", ") + - expression: '(has(request.operation) && request.operation == "UPDATE") || size(variables.missingDropCapabilitiesByContainer) == 0' + messageExpression: | + "containers are not dropping all required capabilities: " + variables.missingDropCapabilitiesByContainer.map(pair, "{container: " + pair[0] + ", capabilities: [" + pair[1].join(", ") + "]}").join(", ") + - engine: Rego + source: + rego: | + package capabilities + + import data.lib.exclude_update.is_update + import data.lib.exempt_container.is_exempt + + violation[{"msg": msg}] { + # spec.containers.securityContext.capabilities field is immutable. + not is_update(input.review) + + container := input.review.object.spec.containers[_] + not is_exempt(container) + has_disallowed_capabilities(container) + msg := sprintf("container <%v> has a disallowed capability. Allowed capabilities are %v", [container.name, get_default(input.parameters, "allowedCapabilities", "NONE")]) + } + + violation[{"msg": msg}] { + not is_update(input.review) + container := input.review.object.spec.containers[_] + not is_exempt(container) + missing_drop_capabilities(container) + msg := sprintf("container <%v> is not dropping all required capabilities. Container must drop all of %v or \"ALL\"", [container.name, input.parameters.requiredDropCapabilities]) + } + + + + violation[{"msg": msg}] { + not is_update(input.review) + container := input.review.object.spec.initContainers[_] + not is_exempt(container) + has_disallowed_capabilities(container) + msg := sprintf("init container <%v> has a disallowed capability. Allowed capabilities are %v", [container.name, get_default(input.parameters, "allowedCapabilities", "NONE")]) + } + + violation[{"msg": msg}] { + not is_update(input.review) + container := input.review.object.spec.initContainers[_] + not is_exempt(container) + missing_drop_capabilities(container) + msg := sprintf("init container <%v> is not dropping all required capabilities. Container must drop all of %v or \"ALL\"", [container.name, input.parameters.requiredDropCapabilities]) + } + + + + violation[{"msg": msg}] { + not is_update(input.review) + container := input.review.object.spec.ephemeralContainers[_] + not is_exempt(container) + has_disallowed_capabilities(container) + msg := sprintf("ephemeral container <%v> has a disallowed capability. Allowed capabilities are %v", [container.name, get_default(input.parameters, "allowedCapabilities", "NONE")]) + } + + violation[{"msg": msg}] { + not is_update(input.review) + container := input.review.object.spec.ephemeralContainers[_] + not is_exempt(container) + missing_drop_capabilities(container) + msg := sprintf("ephemeral container <%v> is not dropping all required capabilities. Container must drop all of %v or \"ALL\"", [container.name, input.parameters.requiredDropCapabilities]) + } + + + has_disallowed_capabilities(container) { + allowed := {c | c := lower(input.parameters.allowedCapabilities[_])} + not allowed["*"] + capabilities := {c | c := lower(container.securityContext.capabilities.add[_])} + + count(capabilities - allowed) > 0 + } + + missing_drop_capabilities(container) { + must_drop := {c | c := lower(input.parameters.requiredDropCapabilities[_])} + all := {"all"} + dropped := {c | c := lower(container.securityContext.capabilities.drop[_])} + + count(must_drop - dropped) > 0 + count(all - dropped) > 0 + } + + get_default(obj, param, _) := obj[param] + + get_default(obj, param, _default) := _default { + not obj[param] + not obj[param] == false + } + libs: + - | + package lib.exclude_update + + is_update(review) { + review.operation == "UPDATE" + } + - | + package lib.exempt_container + + is_exempt(container) { + exempt_images := object.get(object.get(input, "parameters", {}), "exemptImages", []) + img := container.image + exemption := exempt_images[_] + _matches_exemption(img, exemption) + } + + _matches_exemption(img, exemption) { + not endswith(exemption, "*") + exemption == img + } + + _matches_exemption(img, exemption) { + endswith(exemption, "*") + prefix := trim_suffix(exemption, "*") + startswith(img, prefix) + } ```