From 1f105aa2de4670b8cd548f6625e7c89d7e7fc409 Mon Sep 17 00:00:00 2001 From: Rustam Samigullin <89222124+rsamigullin@users.noreply.github.com> Date: Mon, 16 Dec 2024 13:36:05 -0500 Subject: [PATCH] Addding new Safeguards (#441) --- .../container-allowed-images/constraint.yaml | 12 + .../container-allowed-images/template.yaml | 43 +++ .../container-enforce-probes/constraint.yaml | 13 + .../container-enforce-probes/template.yaml | 90 ++++++ .../container-resource-limits/constraint.yaml | 14 + .../container-resource-limits/template.yaml | 263 ++++++++++++++++++ .../constraint.yaml | 11 + .../template.yaml | 64 +++++ .../constraint.yaml | 11 + .../template.yaml | 111 ++++++++ .../pod-enforce-antiaffinity/constraint.yaml | 9 + .../pod-enforce-antiaffinity/template.yaml | 25 ++ .../v1.0.0/restricted-taints/constraint.yaml | 11 + .../v1.0.0/restricted-taints/template.yaml | 43 +++ .../unique-service-selectors/constraint.yaml | 9 + .../unique-service-selectors/template.yaml | 70 +++++ .../container-allowed-images/constraint.yaml | 12 + .../container-allowed-images/template.yaml | 43 +++ .../container-enforce-probes/constraint.yaml | 13 + .../container-enforce-probes/template.yaml | 90 ++++++ .../container-resource-limits/constraint.yaml | 14 + .../container-resource-limits/template.yaml | 263 ++++++++++++++++++ .../constraint.yaml | 11 + .../template.yaml | 64 +++++ .../constraint.yaml | 11 + .../template.yaml | 111 ++++++++ .../pod-enforce-antiaffinity/constraint.yaml | 9 + .../pod-enforce-antiaffinity/template.yaml | 25 ++ .../v2.0.0/restricted-taints/constraint.yaml | 11 + .../v2.0.0/restricted-taints/template.yaml | 43 +++ .../unique-service-selectors/constraint.yaml | 9 + .../unique-service-selectors/template.yaml | 70 +++++ 32 files changed, 1598 insertions(+) create mode 100755 pkg/safeguards/lib/manifests/v1.0.0/container-allowed-images/constraint.yaml create mode 100755 pkg/safeguards/lib/manifests/v1.0.0/container-allowed-images/template.yaml create mode 100755 pkg/safeguards/lib/manifests/v1.0.0/container-enforce-probes/constraint.yaml create mode 100755 pkg/safeguards/lib/manifests/v1.0.0/container-enforce-probes/template.yaml create mode 100755 pkg/safeguards/lib/manifests/v1.0.0/container-resource-limits/constraint.yaml create mode 100755 pkg/safeguards/lib/manifests/v1.0.0/container-resource-limits/template.yaml create mode 100755 pkg/safeguards/lib/manifests/v1.0.0/container-restricted-image-pulls/constraint.yaml create mode 100755 pkg/safeguards/lib/manifests/v1.0.0/container-restricted-image-pulls/template.yaml create mode 100755 pkg/safeguards/lib/manifests/v1.0.0/disallowed-bad-pod-disruption-budgets/constraint.yaml create mode 100755 pkg/safeguards/lib/manifests/v1.0.0/disallowed-bad-pod-disruption-budgets/template.yaml create mode 100755 pkg/safeguards/lib/manifests/v1.0.0/pod-enforce-antiaffinity/constraint.yaml create mode 100755 pkg/safeguards/lib/manifests/v1.0.0/pod-enforce-antiaffinity/template.yaml create mode 100755 pkg/safeguards/lib/manifests/v1.0.0/restricted-taints/constraint.yaml create mode 100755 pkg/safeguards/lib/manifests/v1.0.0/restricted-taints/template.yaml create mode 100755 pkg/safeguards/lib/manifests/v1.0.0/unique-service-selectors/constraint.yaml create mode 100755 pkg/safeguards/lib/manifests/v1.0.0/unique-service-selectors/template.yaml create mode 100755 pkg/safeguards/lib/manifests/v2.0.0/container-allowed-images/constraint.yaml create mode 100755 pkg/safeguards/lib/manifests/v2.0.0/container-allowed-images/template.yaml create mode 100755 pkg/safeguards/lib/manifests/v2.0.0/container-enforce-probes/constraint.yaml create mode 100755 pkg/safeguards/lib/manifests/v2.0.0/container-enforce-probes/template.yaml create mode 100755 pkg/safeguards/lib/manifests/v2.0.0/container-resource-limits/constraint.yaml create mode 100755 pkg/safeguards/lib/manifests/v2.0.0/container-resource-limits/template.yaml create mode 100755 pkg/safeguards/lib/manifests/v2.0.0/container-restricted-image-pulls/constraint.yaml create mode 100755 pkg/safeguards/lib/manifests/v2.0.0/container-restricted-image-pulls/template.yaml create mode 100755 pkg/safeguards/lib/manifests/v2.0.0/disallowed-bad-pod-disruption-budgets/constraint.yaml create mode 100755 pkg/safeguards/lib/manifests/v2.0.0/disallowed-bad-pod-disruption-budgets/template.yaml create mode 100755 pkg/safeguards/lib/manifests/v2.0.0/pod-enforce-antiaffinity/constraint.yaml create mode 100755 pkg/safeguards/lib/manifests/v2.0.0/pod-enforce-antiaffinity/template.yaml create mode 100755 pkg/safeguards/lib/manifests/v2.0.0/restricted-taints/constraint.yaml create mode 100755 pkg/safeguards/lib/manifests/v2.0.0/restricted-taints/template.yaml create mode 100755 pkg/safeguards/lib/manifests/v2.0.0/unique-service-selectors/constraint.yaml create mode 100755 pkg/safeguards/lib/manifests/v2.0.0/unique-service-selectors/template.yaml diff --git a/pkg/safeguards/lib/manifests/v1.0.0/container-allowed-images/constraint.yaml b/pkg/safeguards/lib/manifests/v1.0.0/container-allowed-images/constraint.yaml new file mode 100755 index 00000000..28d614ab --- /dev/null +++ b/pkg/safeguards/lib/manifests/v1.0.0/container-allowed-images/constraint.yaml @@ -0,0 +1,12 @@ +apiVersion: constraints.gatekeeper.sh/v1beta1 +kind: K8sAzureV2ContainerAllowedImages +metadata: + name: v2-container-allowed-images +spec: + match: + kinds: + - apiGroups: [""] + kinds: ["Pod"] + parameters: + imageRegex: .* + excludedContainers: [] \ No newline at end of file diff --git a/pkg/safeguards/lib/manifests/v1.0.0/container-allowed-images/template.yaml b/pkg/safeguards/lib/manifests/v1.0.0/container-allowed-images/template.yaml new file mode 100755 index 00000000..1a8de381 --- /dev/null +++ b/pkg/safeguards/lib/manifests/v1.0.0/container-allowed-images/template.yaml @@ -0,0 +1,43 @@ +apiVersion: templates.gatekeeper.sh/v1beta1 +kind: ConstraintTemplate +metadata: + name: k8sazurev2containerallowedimages +spec: + crd: + spec: + names: + kind: K8sAzureV2ContainerAllowedImages + validation: + # Schema for the `parameters` field + openAPIV3Schema: + properties: + imageRegex: + type: string + excludedContainers: + type: array + items: + type: string + targets: + - target: admission.k8s.gatekeeper.sh + rego: | + package k8sazurev2containerallowedimages + + violation[{"msg": msg}] { + container := input_containers[_] + not input_container_excluded(container.name) + not regex.match(input.parameters.imageRegex, container.image) + msg := sprintf("Container image %v for container %v has not been allowed.", [container.image, container.name]) + } + + input_containers[c] { + c := input.review.object.spec.containers[_] + } + input_containers[c] { + c := input.review.object.spec.initContainers[_] + } + input_containers[c] { + c := input.review.object.spec.ephemeralContainers[_] + } + input_container_excluded(field) { + field == input.parameters.excludedContainers[_] + } diff --git a/pkg/safeguards/lib/manifests/v1.0.0/container-enforce-probes/constraint.yaml b/pkg/safeguards/lib/manifests/v1.0.0/container-enforce-probes/constraint.yaml new file mode 100755 index 00000000..a6223f32 --- /dev/null +++ b/pkg/safeguards/lib/manifests/v1.0.0/container-enforce-probes/constraint.yaml @@ -0,0 +1,13 @@ +apiVersion: constraints.gatekeeper.sh/v1beta1 +kind: K8sAzureV2ContainerEnforceProbes +metadata: + name: v2-container-enforce-probes +spec: + match: + kinds: + - apiGroups: [""] + kinds: ["Pod"] + parameters: + enforceProbes : ["readinessProbe","livenessProbe"] + excludedContainers: [] + excludedImages: [] \ No newline at end of file diff --git a/pkg/safeguards/lib/manifests/v1.0.0/container-enforce-probes/template.yaml b/pkg/safeguards/lib/manifests/v1.0.0/container-enforce-probes/template.yaml new file mode 100755 index 00000000..537b7b18 --- /dev/null +++ b/pkg/safeguards/lib/manifests/v1.0.0/container-enforce-probes/template.yaml @@ -0,0 +1,90 @@ +apiVersion: templates.gatekeeper.sh/v1beta1 +kind: ConstraintTemplate +metadata: + name: k8sazurev2containerenforceprobes +spec: + crd: + spec: + names: + kind: K8sAzureV2ContainerEnforceProbes + validation: + openAPIV3Schema: + properties: + enforceProbes: + type: array + items: + type: string + excludedContainers: + type: array + items: + type: string + excludedImages: + 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 excluding images from an untrusted repository. + type: array + items: + type: string + targets: + - target: admission.k8s.gatekeeper.sh + rego: | + package k8sazurev2containerenforceprobes + + import data.lib.exclude_container_image.is_excluded + + # Rule: + ## Parameter enforceProbes is one string array that will define which kinds of probes to be enforced for all the containers (init container excludes). The allowed values could be livenessProbes and readinessProbes for now + ## Once certain probe is enforces, e.g. livenessProbes, the policy will check on all containers(except init) if they have livenessProbes field. Besides, the probes should at least have defined one of the probe_types, "tcpSocket", "httpGet" or "exec" + + probe_type_set = probe_types { + probe_types := {type | type := ["tcpSocket", "httpGet", "exec"][_]} + } + violation[{"msg": msg}] { + container := input_containers[_] + not input_container_excluded(container.name) + not is_excluded(container) + probe := input.parameters.enforceProbes[_] + probe_is_missing(container, probe) + msg := get_violation_message(container, input.review, probe) + } + probe_is_missing(ctr, probe) = true { + not ctr[probe] + } + probe_is_missing(ctr, probe) = true { + probe_field_empty(ctr, probe) + } + probe_field_empty(ctr, probe) = true { + probe_fields := {field | ctr[probe][field]} + diff_fields := probe_type_set - probe_fields + count(diff_fields) == count(probe_type_set) + } + get_violation_message(container, review, probe) = msg { + msg := sprintf("Container <%v> in your Pod <%v> has no <%v>. Required probes: %v", [container.name, review.object.metadata.name, probe, input.parameters.enforceProbes]) + } + + input_containers[c] { + c := input.review.object.spec.containers[_] + } + input_container_excluded(field) { + field == input.parameters.excludedContainers[_] + } + libs: + - | + package lib.exclude_container_image + is_excluded(container) { + exclude_images := object.get(object.get(input, "parameters", {}), "excludedImages", []) + img := container.image + exclusion := exclude_images[_] + _matches_exclusion(img, exclusion) + } + _matches_exclusion(img, exclusion) { + not endswith(exclusion, "*") + exclusion == img + } + _matches_exclusion(img, exclusion) { + endswith(exclusion, "*") + prefix := trim_suffix(exclusion, "*") + startswith(img, prefix) + } \ No newline at end of file diff --git a/pkg/safeguards/lib/manifests/v1.0.0/container-resource-limits/constraint.yaml b/pkg/safeguards/lib/manifests/v1.0.0/container-resource-limits/constraint.yaml new file mode 100755 index 00000000..521c0da3 --- /dev/null +++ b/pkg/safeguards/lib/manifests/v1.0.0/container-resource-limits/constraint.yaml @@ -0,0 +1,14 @@ +apiVersion: constraints.gatekeeper.sh/v1beta1 +kind: K8sAzureV3ContainerLimits +metadata: + name: v3-container-limits +spec: + match: + kinds: + - apiGroups: [""] + kinds: ["Pod"] + parameters: + cpuLimit : "200m" + memoryLimit: "1Gi" + excludedContainers: [] + excludedImages: [] \ No newline at end of file diff --git a/pkg/safeguards/lib/manifests/v1.0.0/container-resource-limits/template.yaml b/pkg/safeguards/lib/manifests/v1.0.0/container-resource-limits/template.yaml new file mode 100755 index 00000000..aa710ace --- /dev/null +++ b/pkg/safeguards/lib/manifests/v1.0.0/container-resource-limits/template.yaml @@ -0,0 +1,263 @@ +apiVersion: templates.gatekeeper.sh/v1beta1 +kind: ConstraintTemplate +metadata: + name: k8sazurev3containerlimits +spec: + crd: + spec: + names: + kind: K8sAzureV3ContainerLimits + validation: + # Schema for the `parameters` field + openAPIV3Schema: + properties: + cpuLimit: + type: string + memoryLimit: + type: string + excludedContainers: + type: array + items: + type: string + excludedImages: + 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 excluding images from an untrusted repository. + type: array + items: + type: string + targets: + - target: admission.k8s.gatekeeper.sh + rego: | + package k8sazurev3containerlimits + + import data.lib.exclude_container_image.is_excluded + + missing(obj, field) = true { + not obj[field] + } + + missing(obj, field) = true { + obj[field] == "" + } + + canonify_cpu(orig) = new { + is_number(orig) + new := orig * 1000 + } + + canonify_cpu(orig) = new { + not is_number(orig) + endswith(orig, "m") + new := to_number(replace(orig, "m", "")) + } + + canonify_cpu(orig) = new { + not is_number(orig) + not endswith(orig, "m") + regex.match("^[0-9]+(\\.[0-9]+)?$", orig) + new := to_number(orig) * 1000 + } + + # 10 ** 21 + mem_multiple("E") = 1000000000000000000000 { true } + + # 10 ** 18 + mem_multiple("P") = 1000000000000000000 { true } + + # 10 ** 15 + mem_multiple("T") = 1000000000000000 { true } + + # 10 ** 12 + mem_multiple("G") = 1000000000000 { true } + + # 10 ** 9 + mem_multiple("M") = 1000000000 { true } + + # 10 ** 6 + mem_multiple("k") = 1000000 { true } + + # 10 ** 3 + mem_multiple("") = 1000 { true } + + # Kubernetes accepts millibyte precision when it probably shouldn't. + # https://github.com/kubernetes/kubernetes/issues/28741 + # 10 ** 0 + mem_multiple("m") = 1 { true } + + # 1000 * 2 ** 10 + mem_multiple("Ki") = 1024000 { true } + + # 1000 * 2 ** 20 + mem_multiple("Mi") = 1048576000 { true } + + # 1000 * 2 ** 30 + mem_multiple("Gi") = 1073741824000 { true } + + # 1000 * 2 ** 40 + mem_multiple("Ti") = 1099511627776000 { true } + + # 1000 * 2 ** 50 + mem_multiple("Pi") = 1125899906842624000 { true } + + # 1000 * 2 ** 60 + mem_multiple("Ei") = 1152921504606846976000 { true } + + get_suffix(mem) = suffix { + not is_string(mem) + suffix := "" + } + + get_suffix(mem) = suffix { + is_string(mem) + count(mem) > 0 + suffix := substring(mem, count(mem) - 1, -1) + mem_multiple(suffix) + } + + get_suffix(mem) = suffix { + is_string(mem) + count(mem) > 1 + suffix := substring(mem, count(mem) - 2, -1) + mem_multiple(suffix) + } + + get_suffix(mem) = suffix { + is_string(mem) + count(mem) > 1 + not mem_multiple(substring(mem, count(mem) - 1, -1)) + not mem_multiple(substring(mem, count(mem) - 2, -1)) + suffix := "" + } + + get_suffix(mem) = suffix { + is_string(mem) + count(mem) == 1 + not mem_multiple(substring(mem, count(mem) - 1, -1)) + suffix := "" + } + + get_suffix(mem) = suffix { + is_string(mem) + count(mem) == 0 + suffix := "" + } + + canonify_mem(orig) = new { + is_number(orig) + new := orig * 1000 + } + + canonify_mem(orig) = new { + not is_number(orig) + suffix := get_suffix(orig) + raw := replace(orig, suffix, "") + regex.match("^[0-9]+(\\.[0-9]+)?$", raw) + new := to_number(raw) * mem_multiple(suffix) + } + + violation[{"msg": msg}] { + general_violation[{"msg": msg, "field": "containers"}] + } + + violation[{"msg": msg}] { + general_violation[{"msg": msg, "field": "initContainers"}] + } + + general_violation[{"msg": msg, "field": field}] { + container := input.review.object.spec[field][_] + not input_container_excluded(container.name) + not is_excluded(container) + cpu_orig := container.resources.limits.cpu + not canonify_cpu(cpu_orig) + msg := sprintf("container <%v> cpu limit <%v> could not be parsed", [container.name, cpu_orig]) + } + + general_violation[{"msg": msg, "field": field}] { + container := input.review.object.spec[field][_] + not input_container_excluded(container.name) + not is_excluded(container) + mem_orig := container.resources.limits.memory + not canonify_mem(mem_orig) + msg := sprintf("container <%v> memory limit <%v> could not be parsed", [container.name, mem_orig]) + } + + general_violation[{"msg": msg, "field": field}] { + container := input.review.object.spec[field][_] + not input_container_excluded(container.name) + not is_excluded(container) + not container.resources + msg := sprintf("container <%v> has no resource limits", [container.name]) + } + + general_violation[{"msg": msg, "field": field}] { + container := input.review.object.spec[field][_] + not input_container_excluded(container.name) + not is_excluded(container) + not container.resources.limits + msg := sprintf("container <%v> has no resource limits", [container.name]) + } + + general_violation[{"msg": msg, "field": field}] { + container := input.review.object.spec[field][_] + not input_container_excluded(container.name) + not is_excluded(container) + missing(container.resources.limits, "cpu") + msg := sprintf("container <%v> has no cpu limit", [container.name]) + } + + general_violation[{"msg": msg, "field": field}] { + container := input.review.object.spec[field][_] + not input_container_excluded(container.name) + not is_excluded(container) + missing(container.resources.limits, "memory") + msg := sprintf("container <%v> has no memory limit", [container.name]) + } + + general_violation[{"msg": msg, "field": field}] { + container := input.review.object.spec[field][_] + not input_container_excluded(container.name) + not is_excluded(container) + cpu_orig := container.resources.limits.cpu + cpu := canonify_cpu(cpu_orig) + max_cpu_orig := input.parameters.cpuLimit + max_cpu := canonify_cpu(max_cpu_orig) + cpu > max_cpu + msg := sprintf("container <%v> cpu limit <%v> is higher than the maximum allowed of <%v>", [container.name, cpu_orig, max_cpu_orig]) + } + + general_violation[{"msg": msg, "field": field}] { + container := input.review.object.spec[field][_] + not input_container_excluded(container.name) + not is_excluded(container) + mem_orig := container.resources.limits.memory + mem := canonify_mem(mem_orig) + max_mem_orig := input.parameters.memoryLimit + max_mem := canonify_mem(max_mem_orig) + mem > max_mem + msg := sprintf("container <%v> memory limit <%v> is higher than the maximum allowed of <%v>", [container.name, mem_orig, max_mem_orig]) + } + + input_container_excluded(field) { + field == input.parameters.excludedContainers[_] + } + libs: + - | + package lib.exclude_container_image + is_excluded(container) { + exclude_images := object.get(object.get(input, "parameters", {}), "excludedImages", []) + img := container.image + exclusion := exclude_images[_] + _matches_exclusion(img, exclusion) + } + _matches_exclusion(img, exclusion) { + not endswith(exclusion, "*") + exclusion == img + } + _matches_exclusion(img, exclusion) { + endswith(exclusion, "*") + prefix := trim_suffix(exclusion, "*") + startswith(img, prefix) + } diff --git a/pkg/safeguards/lib/manifests/v1.0.0/container-restricted-image-pulls/constraint.yaml b/pkg/safeguards/lib/manifests/v1.0.0/container-restricted-image-pulls/constraint.yaml new file mode 100755 index 00000000..99dc6b0a --- /dev/null +++ b/pkg/safeguards/lib/manifests/v1.0.0/container-restricted-image-pulls/constraint.yaml @@ -0,0 +1,11 @@ +apiVersion: constraints.gatekeeper.sh/v1beta1 +kind: K8sAzureV1ContainerRestrictedImagePulls +metadata: + name: v1-container-restricted-image-pulls +spec: + match: + kinds: + - apiGroups: [""] + kinds: ["Pod"] + parameters: + excludedImages: [] \ No newline at end of file diff --git a/pkg/safeguards/lib/manifests/v1.0.0/container-restricted-image-pulls/template.yaml b/pkg/safeguards/lib/manifests/v1.0.0/container-restricted-image-pulls/template.yaml new file mode 100755 index 00000000..75cc86ea --- /dev/null +++ b/pkg/safeguards/lib/manifests/v1.0.0/container-restricted-image-pulls/template.yaml @@ -0,0 +1,64 @@ +apiVersion: templates.gatekeeper.sh/v1beta1 +kind: ConstraintTemplate +metadata: + name: k8sazurev1containerrestrictedimagepulls +spec: + crd: + spec: + names: + kind: K8sAzureV1ContainerRestrictedImagePulls + validation: + # Schema for the `parameters` field + openAPIV3Schema: + properties: + excludedImages: + type: array + items: + type: string + targets: + - target: admission.k8s.gatekeeper.sh + rego: | + package k8sazurev1containerrestrictedimagepulls + + has_key(object, key) { + _ = object[key] + } + + violation[{"msg": msg}] { + container := input_containers[_] + not is_excluded(container) + not has_key(input.review.object.spec, "imagePullSecrets") + namespace := input.review.namespace + pod := input.review.object.metadata.name + msg := sprintf("%s in %s does not have imagePullSecrets. Unauthenticated image pulls are not recommended.", [pod, namespace]) + } + + input_containers[c] { + c := input.review.object.spec.containers[_] + } + + input_containers[c] { + c := input.review.object.spec.initContainers[_] + } + + input_containers[c] { + c := input.review.object.spec.ephemeralContainers[_] + } + + is_excluded(container) { + exclude_images := object.get(object.get(input, "parameters", {}), "excludedImages", []) + img := container.image + exclusion := exclude_images[_] + matches_exclusion(img, exclusion) + } + + matches_exclusion(img, exclusion) { + not endswith(exclusion, "*") + exclusion == img + } + + matches_exclusion(img, exclusion) { + endswith(exclusion, "*") + prefix := trim_suffix(exclusion, "*") + startswith(img, prefix) + } diff --git a/pkg/safeguards/lib/manifests/v1.0.0/disallowed-bad-pod-disruption-budgets/constraint.yaml b/pkg/safeguards/lib/manifests/v1.0.0/disallowed-bad-pod-disruption-budgets/constraint.yaml new file mode 100755 index 00000000..b7cdf7da --- /dev/null +++ b/pkg/safeguards/lib/manifests/v1.0.0/disallowed-bad-pod-disruption-budgets/constraint.yaml @@ -0,0 +1,11 @@ +apiVersion: constraints.gatekeeper.sh/v1beta1 +kind: K8sAzureV1DisallowedBadPodDisruptionBudgets +metadata: + name: v1-disallowed-bad-pod-disruption-budgets +spec: + match: + kinds: + - apiGroups: ["apps"] + kinds: ["Deployment", "ReplicaSet", "StatefulSet"] + - apiGroups: ["policy"] + kinds: ["PodDisruptionBudget"] \ No newline at end of file diff --git a/pkg/safeguards/lib/manifests/v1.0.0/disallowed-bad-pod-disruption-budgets/template.yaml b/pkg/safeguards/lib/manifests/v1.0.0/disallowed-bad-pod-disruption-budgets/template.yaml new file mode 100755 index 00000000..57be9db9 --- /dev/null +++ b/pkg/safeguards/lib/manifests/v1.0.0/disallowed-bad-pod-disruption-budgets/template.yaml @@ -0,0 +1,111 @@ +apiVersion: templates.gatekeeper.sh/v1beta1 +kind: ConstraintTemplate +metadata: + name: k8sazurev1disallowedbadpoddisruptionbudgets + annotations: + metadata.gatekeeper.sh/title: "Pod Disruption Budget" + metadata.gatekeeper.sh/version: 1.0.0 + metadata.gatekeeper.sh/requires-sync-data: | + "[ + [ + { + "groups": ["policy"], + "versions": ["v1"], + "kinds": ["PodDisruptionBudget"] + }, + { + "groups": ["apps"], + "versions": ["v1"], + "kinds": ["StatefulSet", "ReplicaSet", "Deployment"] + } + ] + ]" + description: Prevents customers from applying bad Pod Disruption Budgets +spec: + crd: + spec: + names: + kind: K8sAzureV1DisallowedBadPodDisruptionBudgets + targets: + - target: admission.k8s.gatekeeper.sh + rego: | + package k8sazurev1disallowedbadpoddisruptionbudgets + + violation[{"msg": msg}] { + input.review.kind.kind == "PodDisruptionBudget" + pdb := input.review.object + not valid_pdb_max_unavailable(pdb) + msg := sprintf( + "PodDisruptionBudget <%s> has maxUnavailable of 0, only positive integers are allowed for maxUnavailable", + [pdb.metadata.name] + ) + } + + violation[{"msg": msg}] { + obj := input.review.object + pdb := data.inventory.namespace[obj.metadata.namespace]["policy/v1"].PodDisruptionBudget[_] + obj.spec.selector.matchLabels == pdb.spec.selector.matchLabels + not valid_pdb_max_unavailable(pdb) + msg := sprintf( + "%s <%s> has been selected by PodDisruptionBudget <%s> but has maxUnavailable of 0, only positive integers are allowed for maxUnavailable", + [obj.kind, obj.metadata.name, pdb.metadata.name] + ) + } + + violation[{"msg": msg}] { + obj := input.review.object + pdb := data.inventory.namespace[obj.metadata.namespace]["policy/v1"].PodDisruptionBudget[_] + obj.spec.selector.matchLabels == pdb.spec.selector.matchLabels + not valid_pdb_min_available(obj, pdb) + msg := sprintf("%s <%s> has %d replica(s) but PodDisruptionBudget <%s> has minAvailable of %d, only positive integers less than %d are allowed for minAvailable", + [obj.kind, obj.metadata.name, obj.spec.replicas, pdb.metadata.name, pdb.spec.minAvailable, obj.spec.replicas]) + } + + violation[{"msg":msg}] { + input.review.kind.kind == "PodDisruptionBudget" + pdb := input.review.object + + matchingDeploys := {x | x.spec.selector.matchLabels == pdb.spec.selector.matchLabels; x = data.inventory.namespace[pdb.metadata.namespace]["apps/v1"].Deployment[_]} + deploy := matchingDeploys[_] + not valid_pdb_min_available(deploy, pdb) + + msg := sprintf("PodDisruptionBudget %s specifies minAvailable of %d, but matching Deployment %s has %d replicas. minAvailable should be less than %d",[pdb.metadata.name,pdb.spec.minAvailable,deploy.metadata.name,deploy.spec.replicas,deploy.spec.replicas]) + } + + violation[{"msg":msg}] { + input.review.kind.kind == "PodDisruptionBudget" + pdb := input.review.object + + matchingSS := {x | x.spec.selector.matchLabels == pdb.spec.selector.matchLabels; x = data.inventory.namespace[pdb.metadata.namespace]["apps/v1"].StatefulSet[_]} + ss := matchingSS[_] + not valid_pdb_min_available(ss, pdb) + + msg := sprintf("PodDisruptionBudget %s specifies minAvailable of %d, but matching StatefulSet %s has %d replicas. minAvailable should be less than %d",[pdb.metadata.name,pdb.spec.minAvailable,ss.metadata.name,ss.spec.replicas,ss.spec.replicas]) + } + + violation[{"msg":msg}] { + input.review.kind.kind == "PodDisruptionBudget" + pdb := input.review.object + + matchingRS := {x | x.spec.selector.matchLabels == pdb.spec.selector.matchLabels; x = data.inventory.namespace[pdb.metadata.namespace]["apps/v1"].ReplicaSet[_]} + rs := matchingRS[_] + not valid_pdb_min_available(rs, pdb) + + msg := sprintf("PodDisruptionBudget %s specifies minAvailable of %d, but matching ReplicaSet %s has %d replicas. minAvailable should be less than %d",[pdb.metadata.name,pdb.spec.minAvailable,rs.metadata.name,rs.spec.replicas,rs.spec.replicas]) + } + + valid_pdb_min_available(obj, pdb) { + # default to -1 if minAvailable is not set so valid_pdb_min_available is always true + # for objects with >= 0 replicas. If minAvailable defaults to >= 0, objects with + # replicas field might violate this constraint if they are equal to the default set here + min_available := object.get(pdb.spec, "minAvailable", -1) + obj.spec.replicas > min_available + } + + valid_pdb_max_unavailable(pdb) { + # default to 1 if maxUnavailable is not set so valid_pdb_max_unavailable always returns true. + # If maxUnavailable defaults to 0, it violates this constraint because all pods needs to be + # available and no pods can be evicted voluntarily + max_unavailable := object.get(pdb.spec, "maxUnavailable", 1) + max_unavailable > 0 + } \ No newline at end of file diff --git a/pkg/safeguards/lib/manifests/v1.0.0/pod-enforce-antiaffinity/constraint.yaml b/pkg/safeguards/lib/manifests/v1.0.0/pod-enforce-antiaffinity/constraint.yaml new file mode 100755 index 00000000..a2e037dc --- /dev/null +++ b/pkg/safeguards/lib/manifests/v1.0.0/pod-enforce-antiaffinity/constraint.yaml @@ -0,0 +1,9 @@ +apiVersion: constraints.gatekeeper.sh/v1beta1 +kind: K8sAzureV1AntiAffinityRules +metadata: + name: v1-multiple-replicas-need-anti-affinity +spec: + match: + kinds: + - apiGroups: ["apps"] + kinds: ["Deployment","StatefulSet","ReplicationController","ReplicaSet"] \ No newline at end of file diff --git a/pkg/safeguards/lib/manifests/v1.0.0/pod-enforce-antiaffinity/template.yaml b/pkg/safeguards/lib/manifests/v1.0.0/pod-enforce-antiaffinity/template.yaml new file mode 100755 index 00000000..6e654588 --- /dev/null +++ b/pkg/safeguards/lib/manifests/v1.0.0/pod-enforce-antiaffinity/template.yaml @@ -0,0 +1,25 @@ +apiVersion: templates.gatekeeper.sh/v1beta1 +kind: ConstraintTemplate +metadata: + name: k8sazurev1antiaffinityrules + annotations: + description: Requires deployments with multiple replicas have pod anti affinity rules +spec: + crd: + spec: + names: + kind: K8sAzureV1AntiAffinityRules + targets: + - target: admission.k8s.gatekeeper.sh + rego: | + package k8sazurev1antiaffinityrules + + missing_affinity(obj) { + not obj.affinity.podAntiAffinity + } + + violation[{"msg": msg}] { + input.review.object.spec.replicas > 1 + missing_affinity(input.review.object.spec.template.spec) + msg := sprintf("%s with %d replicas should have pod anti-affinity rules set to avoid disruptions due to nodes crashing", [input.review.kind.kind, input.review.object.spec.replicas]) + } \ No newline at end of file diff --git a/pkg/safeguards/lib/manifests/v1.0.0/restricted-taints/constraint.yaml b/pkg/safeguards/lib/manifests/v1.0.0/restricted-taints/constraint.yaml new file mode 100755 index 00000000..1ac015a6 --- /dev/null +++ b/pkg/safeguards/lib/manifests/v1.0.0/restricted-taints/constraint.yaml @@ -0,0 +1,11 @@ +apiVersion: constraints.gatekeeper.sh/v1beta1 +kind: K8sAzureV1ReservedTaints +metadata: + name: v1-system-reserved-taints +spec: + match: + kinds: + - apiGroups: [""] + kinds: ["Node"] + parameters: + reservedTaints: ["CriticalAddonsOnly"] \ No newline at end of file diff --git a/pkg/safeguards/lib/manifests/v1.0.0/restricted-taints/template.yaml b/pkg/safeguards/lib/manifests/v1.0.0/restricted-taints/template.yaml new file mode 100755 index 00000000..ee17e48e --- /dev/null +++ b/pkg/safeguards/lib/manifests/v1.0.0/restricted-taints/template.yaml @@ -0,0 +1,43 @@ +apiVersion: templates.gatekeeper.sh/v1beta1 +kind: ConstraintTemplate +metadata: + name: k8sazurev1reservedtaints + annotations: + description: Restricts the CriticalAddonsOnly taint to just the system pool +spec: + crd: + spec: + names: + kind: K8sAzureV1ReservedTaints + validation: + openAPIV3Schema: + type: object + properties: + reservedTaints: + type: array + items: + type: string + targets: + - target: admission.k8s.gatekeeper.sh + rego: | + package k8sazurev1reservedtaints + + is_system_pool(node) { + node.metadata.labels["kubernetes.azure.com/mode"] == "system" + } + + is_system_pool(node) { + node.metadata.labels["kubernetes.azure.com/mode"] == "System" + } + + violation[{"msg": msg}] { + node := input.review.object + # did the customer try to add a taint with key "CriticalAddonsOnly" to a non-system pool? + taints := {x | x = node.spec.taints[_].key} + not is_system_pool(node) + taint := taints[_] + restrictedTaint := input.parameters.reservedTaints[_] + regex.match(restrictedTaint,taint) + + msg := sprintf("Taint with key <%s> is reserved for the system pool only",[taint]) + } \ No newline at end of file diff --git a/pkg/safeguards/lib/manifests/v1.0.0/unique-service-selectors/constraint.yaml b/pkg/safeguards/lib/manifests/v1.0.0/unique-service-selectors/constraint.yaml new file mode 100755 index 00000000..695fde13 --- /dev/null +++ b/pkg/safeguards/lib/manifests/v1.0.0/unique-service-selectors/constraint.yaml @@ -0,0 +1,9 @@ +apiVersion: constraints.gatekeeper.sh/v1beta1 +kind: K8sAzureV1UniqueServiceSelector +metadata: + name: v1-unique-service-selector +spec: + match: + kinds: + - apiGroups: [""] + kinds: ["Service"] \ No newline at end of file diff --git a/pkg/safeguards/lib/manifests/v1.0.0/unique-service-selectors/template.yaml b/pkg/safeguards/lib/manifests/v1.0.0/unique-service-selectors/template.yaml new file mode 100755 index 00000000..9a97ac5e --- /dev/null +++ b/pkg/safeguards/lib/manifests/v1.0.0/unique-service-selectors/template.yaml @@ -0,0 +1,70 @@ +apiVersion: templates.gatekeeper.sh/v1beta1 +kind: ConstraintTemplate +metadata: + name: k8sazurev1uniqueserviceselector + annotations: + metadata.gatekeeper.sh/title: "Unique Service Selectors" + metadata.gatekeeper.sh/version: 1.0.0 + metadata.gatekeeper.sh/requires-sync-data: | + "[ + [ + { + "groups": [""], + "versions": ["v1"], + "kinds": ["Service"] + } + ] + ]" + description: >- + Requires Services to have unique selectors within a namespace. + Selectors are considered the same if they have identical keys and values. + Selectors may share a key/value pair so long as there is at least one + distinct key/value pair between them. + + https://kubernetes.io/docs/concepts/services-networking/service/#defining-a-service +spec: + crd: + spec: + names: + kind: K8sAzureV1UniqueServiceSelector + targets: + - target: admission.k8s.gatekeeper.sh + rego: | + package k8sazurev1uniqueserviceselector + + make_apiversion(kind) = apiVersion { + g := kind.group + v := kind.version + g != "" + apiVersion = sprintf("%s/%s", [g, v]) + } + + make_apiversion(kind) = apiVersion { + kind.group == "" + apiVersion = kind.version + } + + identical(obj, review) { + obj.metadata.namespace == review.object.metadata.namespace + obj.metadata.name == review.object.metadata.name + obj.kind == review.kind.kind + obj.apiVersion == make_apiversion(review.kind) + } + + flatten_selector(obj) = flattened { + selectors := [s | s = concat(":", [key, val]); val = obj.spec.selector[key]] + flattened := concat(",", sort(selectors)) + } + + violation[{"msg": msg}] { + input.review.kind.kind == "Service" + input.review.kind.version == "v1" + input.review.kind.group == "" + input_namespace := input.review.object.metadata.namespace + input_selector := flatten_selector(input.review.object) + other := data.inventory.namespace[input_namespace]["v1"].Service[_] + not identical(other, input.review) + other_selector := flatten_selector(other) + input_selector == other_selector + msg := sprintf("same selector as service <%s> in namespace <%s>", [other.metadata.name, input_namespace]) + } \ No newline at end of file diff --git a/pkg/safeguards/lib/manifests/v2.0.0/container-allowed-images/constraint.yaml b/pkg/safeguards/lib/manifests/v2.0.0/container-allowed-images/constraint.yaml new file mode 100755 index 00000000..28d614ab --- /dev/null +++ b/pkg/safeguards/lib/manifests/v2.0.0/container-allowed-images/constraint.yaml @@ -0,0 +1,12 @@ +apiVersion: constraints.gatekeeper.sh/v1beta1 +kind: K8sAzureV2ContainerAllowedImages +metadata: + name: v2-container-allowed-images +spec: + match: + kinds: + - apiGroups: [""] + kinds: ["Pod"] + parameters: + imageRegex: .* + excludedContainers: [] \ No newline at end of file diff --git a/pkg/safeguards/lib/manifests/v2.0.0/container-allowed-images/template.yaml b/pkg/safeguards/lib/manifests/v2.0.0/container-allowed-images/template.yaml new file mode 100755 index 00000000..1a8de381 --- /dev/null +++ b/pkg/safeguards/lib/manifests/v2.0.0/container-allowed-images/template.yaml @@ -0,0 +1,43 @@ +apiVersion: templates.gatekeeper.sh/v1beta1 +kind: ConstraintTemplate +metadata: + name: k8sazurev2containerallowedimages +spec: + crd: + spec: + names: + kind: K8sAzureV2ContainerAllowedImages + validation: + # Schema for the `parameters` field + openAPIV3Schema: + properties: + imageRegex: + type: string + excludedContainers: + type: array + items: + type: string + targets: + - target: admission.k8s.gatekeeper.sh + rego: | + package k8sazurev2containerallowedimages + + violation[{"msg": msg}] { + container := input_containers[_] + not input_container_excluded(container.name) + not regex.match(input.parameters.imageRegex, container.image) + msg := sprintf("Container image %v for container %v has not been allowed.", [container.image, container.name]) + } + + input_containers[c] { + c := input.review.object.spec.containers[_] + } + input_containers[c] { + c := input.review.object.spec.initContainers[_] + } + input_containers[c] { + c := input.review.object.spec.ephemeralContainers[_] + } + input_container_excluded(field) { + field == input.parameters.excludedContainers[_] + } diff --git a/pkg/safeguards/lib/manifests/v2.0.0/container-enforce-probes/constraint.yaml b/pkg/safeguards/lib/manifests/v2.0.0/container-enforce-probes/constraint.yaml new file mode 100755 index 00000000..a6223f32 --- /dev/null +++ b/pkg/safeguards/lib/manifests/v2.0.0/container-enforce-probes/constraint.yaml @@ -0,0 +1,13 @@ +apiVersion: constraints.gatekeeper.sh/v1beta1 +kind: K8sAzureV2ContainerEnforceProbes +metadata: + name: v2-container-enforce-probes +spec: + match: + kinds: + - apiGroups: [""] + kinds: ["Pod"] + parameters: + enforceProbes : ["readinessProbe","livenessProbe"] + excludedContainers: [] + excludedImages: [] \ No newline at end of file diff --git a/pkg/safeguards/lib/manifests/v2.0.0/container-enforce-probes/template.yaml b/pkg/safeguards/lib/manifests/v2.0.0/container-enforce-probes/template.yaml new file mode 100755 index 00000000..537b7b18 --- /dev/null +++ b/pkg/safeguards/lib/manifests/v2.0.0/container-enforce-probes/template.yaml @@ -0,0 +1,90 @@ +apiVersion: templates.gatekeeper.sh/v1beta1 +kind: ConstraintTemplate +metadata: + name: k8sazurev2containerenforceprobes +spec: + crd: + spec: + names: + kind: K8sAzureV2ContainerEnforceProbes + validation: + openAPIV3Schema: + properties: + enforceProbes: + type: array + items: + type: string + excludedContainers: + type: array + items: + type: string + excludedImages: + 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 excluding images from an untrusted repository. + type: array + items: + type: string + targets: + - target: admission.k8s.gatekeeper.sh + rego: | + package k8sazurev2containerenforceprobes + + import data.lib.exclude_container_image.is_excluded + + # Rule: + ## Parameter enforceProbes is one string array that will define which kinds of probes to be enforced for all the containers (init container excludes). The allowed values could be livenessProbes and readinessProbes for now + ## Once certain probe is enforces, e.g. livenessProbes, the policy will check on all containers(except init) if they have livenessProbes field. Besides, the probes should at least have defined one of the probe_types, "tcpSocket", "httpGet" or "exec" + + probe_type_set = probe_types { + probe_types := {type | type := ["tcpSocket", "httpGet", "exec"][_]} + } + violation[{"msg": msg}] { + container := input_containers[_] + not input_container_excluded(container.name) + not is_excluded(container) + probe := input.parameters.enforceProbes[_] + probe_is_missing(container, probe) + msg := get_violation_message(container, input.review, probe) + } + probe_is_missing(ctr, probe) = true { + not ctr[probe] + } + probe_is_missing(ctr, probe) = true { + probe_field_empty(ctr, probe) + } + probe_field_empty(ctr, probe) = true { + probe_fields := {field | ctr[probe][field]} + diff_fields := probe_type_set - probe_fields + count(diff_fields) == count(probe_type_set) + } + get_violation_message(container, review, probe) = msg { + msg := sprintf("Container <%v> in your Pod <%v> has no <%v>. Required probes: %v", [container.name, review.object.metadata.name, probe, input.parameters.enforceProbes]) + } + + input_containers[c] { + c := input.review.object.spec.containers[_] + } + input_container_excluded(field) { + field == input.parameters.excludedContainers[_] + } + libs: + - | + package lib.exclude_container_image + is_excluded(container) { + exclude_images := object.get(object.get(input, "parameters", {}), "excludedImages", []) + img := container.image + exclusion := exclude_images[_] + _matches_exclusion(img, exclusion) + } + _matches_exclusion(img, exclusion) { + not endswith(exclusion, "*") + exclusion == img + } + _matches_exclusion(img, exclusion) { + endswith(exclusion, "*") + prefix := trim_suffix(exclusion, "*") + startswith(img, prefix) + } \ No newline at end of file diff --git a/pkg/safeguards/lib/manifests/v2.0.0/container-resource-limits/constraint.yaml b/pkg/safeguards/lib/manifests/v2.0.0/container-resource-limits/constraint.yaml new file mode 100755 index 00000000..cc85fd96 --- /dev/null +++ b/pkg/safeguards/lib/manifests/v2.0.0/container-resource-limits/constraint.yaml @@ -0,0 +1,14 @@ +apiVersion: constraints.gatekeeper.sh/v1beta1 +kind: K8sAzureV3ContainerLimits +metadata: + name: v3-container-limits +spec: + match: + kinds: + - apiGroups: [""] + kinds: ["Pod"] + parameters: + cpuLimit : "2" + memoryLimit: "1Gi" + excludedContainers: [] + excludedImages: [] \ No newline at end of file diff --git a/pkg/safeguards/lib/manifests/v2.0.0/container-resource-limits/template.yaml b/pkg/safeguards/lib/manifests/v2.0.0/container-resource-limits/template.yaml new file mode 100755 index 00000000..aa710ace --- /dev/null +++ b/pkg/safeguards/lib/manifests/v2.0.0/container-resource-limits/template.yaml @@ -0,0 +1,263 @@ +apiVersion: templates.gatekeeper.sh/v1beta1 +kind: ConstraintTemplate +metadata: + name: k8sazurev3containerlimits +spec: + crd: + spec: + names: + kind: K8sAzureV3ContainerLimits + validation: + # Schema for the `parameters` field + openAPIV3Schema: + properties: + cpuLimit: + type: string + memoryLimit: + type: string + excludedContainers: + type: array + items: + type: string + excludedImages: + 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 excluding images from an untrusted repository. + type: array + items: + type: string + targets: + - target: admission.k8s.gatekeeper.sh + rego: | + package k8sazurev3containerlimits + + import data.lib.exclude_container_image.is_excluded + + missing(obj, field) = true { + not obj[field] + } + + missing(obj, field) = true { + obj[field] == "" + } + + canonify_cpu(orig) = new { + is_number(orig) + new := orig * 1000 + } + + canonify_cpu(orig) = new { + not is_number(orig) + endswith(orig, "m") + new := to_number(replace(orig, "m", "")) + } + + canonify_cpu(orig) = new { + not is_number(orig) + not endswith(orig, "m") + regex.match("^[0-9]+(\\.[0-9]+)?$", orig) + new := to_number(orig) * 1000 + } + + # 10 ** 21 + mem_multiple("E") = 1000000000000000000000 { true } + + # 10 ** 18 + mem_multiple("P") = 1000000000000000000 { true } + + # 10 ** 15 + mem_multiple("T") = 1000000000000000 { true } + + # 10 ** 12 + mem_multiple("G") = 1000000000000 { true } + + # 10 ** 9 + mem_multiple("M") = 1000000000 { true } + + # 10 ** 6 + mem_multiple("k") = 1000000 { true } + + # 10 ** 3 + mem_multiple("") = 1000 { true } + + # Kubernetes accepts millibyte precision when it probably shouldn't. + # https://github.com/kubernetes/kubernetes/issues/28741 + # 10 ** 0 + mem_multiple("m") = 1 { true } + + # 1000 * 2 ** 10 + mem_multiple("Ki") = 1024000 { true } + + # 1000 * 2 ** 20 + mem_multiple("Mi") = 1048576000 { true } + + # 1000 * 2 ** 30 + mem_multiple("Gi") = 1073741824000 { true } + + # 1000 * 2 ** 40 + mem_multiple("Ti") = 1099511627776000 { true } + + # 1000 * 2 ** 50 + mem_multiple("Pi") = 1125899906842624000 { true } + + # 1000 * 2 ** 60 + mem_multiple("Ei") = 1152921504606846976000 { true } + + get_suffix(mem) = suffix { + not is_string(mem) + suffix := "" + } + + get_suffix(mem) = suffix { + is_string(mem) + count(mem) > 0 + suffix := substring(mem, count(mem) - 1, -1) + mem_multiple(suffix) + } + + get_suffix(mem) = suffix { + is_string(mem) + count(mem) > 1 + suffix := substring(mem, count(mem) - 2, -1) + mem_multiple(suffix) + } + + get_suffix(mem) = suffix { + is_string(mem) + count(mem) > 1 + not mem_multiple(substring(mem, count(mem) - 1, -1)) + not mem_multiple(substring(mem, count(mem) - 2, -1)) + suffix := "" + } + + get_suffix(mem) = suffix { + is_string(mem) + count(mem) == 1 + not mem_multiple(substring(mem, count(mem) - 1, -1)) + suffix := "" + } + + get_suffix(mem) = suffix { + is_string(mem) + count(mem) == 0 + suffix := "" + } + + canonify_mem(orig) = new { + is_number(orig) + new := orig * 1000 + } + + canonify_mem(orig) = new { + not is_number(orig) + suffix := get_suffix(orig) + raw := replace(orig, suffix, "") + regex.match("^[0-9]+(\\.[0-9]+)?$", raw) + new := to_number(raw) * mem_multiple(suffix) + } + + violation[{"msg": msg}] { + general_violation[{"msg": msg, "field": "containers"}] + } + + violation[{"msg": msg}] { + general_violation[{"msg": msg, "field": "initContainers"}] + } + + general_violation[{"msg": msg, "field": field}] { + container := input.review.object.spec[field][_] + not input_container_excluded(container.name) + not is_excluded(container) + cpu_orig := container.resources.limits.cpu + not canonify_cpu(cpu_orig) + msg := sprintf("container <%v> cpu limit <%v> could not be parsed", [container.name, cpu_orig]) + } + + general_violation[{"msg": msg, "field": field}] { + container := input.review.object.spec[field][_] + not input_container_excluded(container.name) + not is_excluded(container) + mem_orig := container.resources.limits.memory + not canonify_mem(mem_orig) + msg := sprintf("container <%v> memory limit <%v> could not be parsed", [container.name, mem_orig]) + } + + general_violation[{"msg": msg, "field": field}] { + container := input.review.object.spec[field][_] + not input_container_excluded(container.name) + not is_excluded(container) + not container.resources + msg := sprintf("container <%v> has no resource limits", [container.name]) + } + + general_violation[{"msg": msg, "field": field}] { + container := input.review.object.spec[field][_] + not input_container_excluded(container.name) + not is_excluded(container) + not container.resources.limits + msg := sprintf("container <%v> has no resource limits", [container.name]) + } + + general_violation[{"msg": msg, "field": field}] { + container := input.review.object.spec[field][_] + not input_container_excluded(container.name) + not is_excluded(container) + missing(container.resources.limits, "cpu") + msg := sprintf("container <%v> has no cpu limit", [container.name]) + } + + general_violation[{"msg": msg, "field": field}] { + container := input.review.object.spec[field][_] + not input_container_excluded(container.name) + not is_excluded(container) + missing(container.resources.limits, "memory") + msg := sprintf("container <%v> has no memory limit", [container.name]) + } + + general_violation[{"msg": msg, "field": field}] { + container := input.review.object.spec[field][_] + not input_container_excluded(container.name) + not is_excluded(container) + cpu_orig := container.resources.limits.cpu + cpu := canonify_cpu(cpu_orig) + max_cpu_orig := input.parameters.cpuLimit + max_cpu := canonify_cpu(max_cpu_orig) + cpu > max_cpu + msg := sprintf("container <%v> cpu limit <%v> is higher than the maximum allowed of <%v>", [container.name, cpu_orig, max_cpu_orig]) + } + + general_violation[{"msg": msg, "field": field}] { + container := input.review.object.spec[field][_] + not input_container_excluded(container.name) + not is_excluded(container) + mem_orig := container.resources.limits.memory + mem := canonify_mem(mem_orig) + max_mem_orig := input.parameters.memoryLimit + max_mem := canonify_mem(max_mem_orig) + mem > max_mem + msg := sprintf("container <%v> memory limit <%v> is higher than the maximum allowed of <%v>", [container.name, mem_orig, max_mem_orig]) + } + + input_container_excluded(field) { + field == input.parameters.excludedContainers[_] + } + libs: + - | + package lib.exclude_container_image + is_excluded(container) { + exclude_images := object.get(object.get(input, "parameters", {}), "excludedImages", []) + img := container.image + exclusion := exclude_images[_] + _matches_exclusion(img, exclusion) + } + _matches_exclusion(img, exclusion) { + not endswith(exclusion, "*") + exclusion == img + } + _matches_exclusion(img, exclusion) { + endswith(exclusion, "*") + prefix := trim_suffix(exclusion, "*") + startswith(img, prefix) + } diff --git a/pkg/safeguards/lib/manifests/v2.0.0/container-restricted-image-pulls/constraint.yaml b/pkg/safeguards/lib/manifests/v2.0.0/container-restricted-image-pulls/constraint.yaml new file mode 100755 index 00000000..99dc6b0a --- /dev/null +++ b/pkg/safeguards/lib/manifests/v2.0.0/container-restricted-image-pulls/constraint.yaml @@ -0,0 +1,11 @@ +apiVersion: constraints.gatekeeper.sh/v1beta1 +kind: K8sAzureV1ContainerRestrictedImagePulls +metadata: + name: v1-container-restricted-image-pulls +spec: + match: + kinds: + - apiGroups: [""] + kinds: ["Pod"] + parameters: + excludedImages: [] \ No newline at end of file diff --git a/pkg/safeguards/lib/manifests/v2.0.0/container-restricted-image-pulls/template.yaml b/pkg/safeguards/lib/manifests/v2.0.0/container-restricted-image-pulls/template.yaml new file mode 100755 index 00000000..75cc86ea --- /dev/null +++ b/pkg/safeguards/lib/manifests/v2.0.0/container-restricted-image-pulls/template.yaml @@ -0,0 +1,64 @@ +apiVersion: templates.gatekeeper.sh/v1beta1 +kind: ConstraintTemplate +metadata: + name: k8sazurev1containerrestrictedimagepulls +spec: + crd: + spec: + names: + kind: K8sAzureV1ContainerRestrictedImagePulls + validation: + # Schema for the `parameters` field + openAPIV3Schema: + properties: + excludedImages: + type: array + items: + type: string + targets: + - target: admission.k8s.gatekeeper.sh + rego: | + package k8sazurev1containerrestrictedimagepulls + + has_key(object, key) { + _ = object[key] + } + + violation[{"msg": msg}] { + container := input_containers[_] + not is_excluded(container) + not has_key(input.review.object.spec, "imagePullSecrets") + namespace := input.review.namespace + pod := input.review.object.metadata.name + msg := sprintf("%s in %s does not have imagePullSecrets. Unauthenticated image pulls are not recommended.", [pod, namespace]) + } + + input_containers[c] { + c := input.review.object.spec.containers[_] + } + + input_containers[c] { + c := input.review.object.spec.initContainers[_] + } + + input_containers[c] { + c := input.review.object.spec.ephemeralContainers[_] + } + + is_excluded(container) { + exclude_images := object.get(object.get(input, "parameters", {}), "excludedImages", []) + img := container.image + exclusion := exclude_images[_] + matches_exclusion(img, exclusion) + } + + matches_exclusion(img, exclusion) { + not endswith(exclusion, "*") + exclusion == img + } + + matches_exclusion(img, exclusion) { + endswith(exclusion, "*") + prefix := trim_suffix(exclusion, "*") + startswith(img, prefix) + } diff --git a/pkg/safeguards/lib/manifests/v2.0.0/disallowed-bad-pod-disruption-budgets/constraint.yaml b/pkg/safeguards/lib/manifests/v2.0.0/disallowed-bad-pod-disruption-budgets/constraint.yaml new file mode 100755 index 00000000..b7cdf7da --- /dev/null +++ b/pkg/safeguards/lib/manifests/v2.0.0/disallowed-bad-pod-disruption-budgets/constraint.yaml @@ -0,0 +1,11 @@ +apiVersion: constraints.gatekeeper.sh/v1beta1 +kind: K8sAzureV1DisallowedBadPodDisruptionBudgets +metadata: + name: v1-disallowed-bad-pod-disruption-budgets +spec: + match: + kinds: + - apiGroups: ["apps"] + kinds: ["Deployment", "ReplicaSet", "StatefulSet"] + - apiGroups: ["policy"] + kinds: ["PodDisruptionBudget"] \ No newline at end of file diff --git a/pkg/safeguards/lib/manifests/v2.0.0/disallowed-bad-pod-disruption-budgets/template.yaml b/pkg/safeguards/lib/manifests/v2.0.0/disallowed-bad-pod-disruption-budgets/template.yaml new file mode 100755 index 00000000..57be9db9 --- /dev/null +++ b/pkg/safeguards/lib/manifests/v2.0.0/disallowed-bad-pod-disruption-budgets/template.yaml @@ -0,0 +1,111 @@ +apiVersion: templates.gatekeeper.sh/v1beta1 +kind: ConstraintTemplate +metadata: + name: k8sazurev1disallowedbadpoddisruptionbudgets + annotations: + metadata.gatekeeper.sh/title: "Pod Disruption Budget" + metadata.gatekeeper.sh/version: 1.0.0 + metadata.gatekeeper.sh/requires-sync-data: | + "[ + [ + { + "groups": ["policy"], + "versions": ["v1"], + "kinds": ["PodDisruptionBudget"] + }, + { + "groups": ["apps"], + "versions": ["v1"], + "kinds": ["StatefulSet", "ReplicaSet", "Deployment"] + } + ] + ]" + description: Prevents customers from applying bad Pod Disruption Budgets +spec: + crd: + spec: + names: + kind: K8sAzureV1DisallowedBadPodDisruptionBudgets + targets: + - target: admission.k8s.gatekeeper.sh + rego: | + package k8sazurev1disallowedbadpoddisruptionbudgets + + violation[{"msg": msg}] { + input.review.kind.kind == "PodDisruptionBudget" + pdb := input.review.object + not valid_pdb_max_unavailable(pdb) + msg := sprintf( + "PodDisruptionBudget <%s> has maxUnavailable of 0, only positive integers are allowed for maxUnavailable", + [pdb.metadata.name] + ) + } + + violation[{"msg": msg}] { + obj := input.review.object + pdb := data.inventory.namespace[obj.metadata.namespace]["policy/v1"].PodDisruptionBudget[_] + obj.spec.selector.matchLabels == pdb.spec.selector.matchLabels + not valid_pdb_max_unavailable(pdb) + msg := sprintf( + "%s <%s> has been selected by PodDisruptionBudget <%s> but has maxUnavailable of 0, only positive integers are allowed for maxUnavailable", + [obj.kind, obj.metadata.name, pdb.metadata.name] + ) + } + + violation[{"msg": msg}] { + obj := input.review.object + pdb := data.inventory.namespace[obj.metadata.namespace]["policy/v1"].PodDisruptionBudget[_] + obj.spec.selector.matchLabels == pdb.spec.selector.matchLabels + not valid_pdb_min_available(obj, pdb) + msg := sprintf("%s <%s> has %d replica(s) but PodDisruptionBudget <%s> has minAvailable of %d, only positive integers less than %d are allowed for minAvailable", + [obj.kind, obj.metadata.name, obj.spec.replicas, pdb.metadata.name, pdb.spec.minAvailable, obj.spec.replicas]) + } + + violation[{"msg":msg}] { + input.review.kind.kind == "PodDisruptionBudget" + pdb := input.review.object + + matchingDeploys := {x | x.spec.selector.matchLabels == pdb.spec.selector.matchLabels; x = data.inventory.namespace[pdb.metadata.namespace]["apps/v1"].Deployment[_]} + deploy := matchingDeploys[_] + not valid_pdb_min_available(deploy, pdb) + + msg := sprintf("PodDisruptionBudget %s specifies minAvailable of %d, but matching Deployment %s has %d replicas. minAvailable should be less than %d",[pdb.metadata.name,pdb.spec.minAvailable,deploy.metadata.name,deploy.spec.replicas,deploy.spec.replicas]) + } + + violation[{"msg":msg}] { + input.review.kind.kind == "PodDisruptionBudget" + pdb := input.review.object + + matchingSS := {x | x.spec.selector.matchLabels == pdb.spec.selector.matchLabels; x = data.inventory.namespace[pdb.metadata.namespace]["apps/v1"].StatefulSet[_]} + ss := matchingSS[_] + not valid_pdb_min_available(ss, pdb) + + msg := sprintf("PodDisruptionBudget %s specifies minAvailable of %d, but matching StatefulSet %s has %d replicas. minAvailable should be less than %d",[pdb.metadata.name,pdb.spec.minAvailable,ss.metadata.name,ss.spec.replicas,ss.spec.replicas]) + } + + violation[{"msg":msg}] { + input.review.kind.kind == "PodDisruptionBudget" + pdb := input.review.object + + matchingRS := {x | x.spec.selector.matchLabels == pdb.spec.selector.matchLabels; x = data.inventory.namespace[pdb.metadata.namespace]["apps/v1"].ReplicaSet[_]} + rs := matchingRS[_] + not valid_pdb_min_available(rs, pdb) + + msg := sprintf("PodDisruptionBudget %s specifies minAvailable of %d, but matching ReplicaSet %s has %d replicas. minAvailable should be less than %d",[pdb.metadata.name,pdb.spec.minAvailable,rs.metadata.name,rs.spec.replicas,rs.spec.replicas]) + } + + valid_pdb_min_available(obj, pdb) { + # default to -1 if minAvailable is not set so valid_pdb_min_available is always true + # for objects with >= 0 replicas. If minAvailable defaults to >= 0, objects with + # replicas field might violate this constraint if they are equal to the default set here + min_available := object.get(pdb.spec, "minAvailable", -1) + obj.spec.replicas > min_available + } + + valid_pdb_max_unavailable(pdb) { + # default to 1 if maxUnavailable is not set so valid_pdb_max_unavailable always returns true. + # If maxUnavailable defaults to 0, it violates this constraint because all pods needs to be + # available and no pods can be evicted voluntarily + max_unavailable := object.get(pdb.spec, "maxUnavailable", 1) + max_unavailable > 0 + } \ No newline at end of file diff --git a/pkg/safeguards/lib/manifests/v2.0.0/pod-enforce-antiaffinity/constraint.yaml b/pkg/safeguards/lib/manifests/v2.0.0/pod-enforce-antiaffinity/constraint.yaml new file mode 100755 index 00000000..a2e037dc --- /dev/null +++ b/pkg/safeguards/lib/manifests/v2.0.0/pod-enforce-antiaffinity/constraint.yaml @@ -0,0 +1,9 @@ +apiVersion: constraints.gatekeeper.sh/v1beta1 +kind: K8sAzureV1AntiAffinityRules +metadata: + name: v1-multiple-replicas-need-anti-affinity +spec: + match: + kinds: + - apiGroups: ["apps"] + kinds: ["Deployment","StatefulSet","ReplicationController","ReplicaSet"] \ No newline at end of file diff --git a/pkg/safeguards/lib/manifests/v2.0.0/pod-enforce-antiaffinity/template.yaml b/pkg/safeguards/lib/manifests/v2.0.0/pod-enforce-antiaffinity/template.yaml new file mode 100755 index 00000000..6e654588 --- /dev/null +++ b/pkg/safeguards/lib/manifests/v2.0.0/pod-enforce-antiaffinity/template.yaml @@ -0,0 +1,25 @@ +apiVersion: templates.gatekeeper.sh/v1beta1 +kind: ConstraintTemplate +metadata: + name: k8sazurev1antiaffinityrules + annotations: + description: Requires deployments with multiple replicas have pod anti affinity rules +spec: + crd: + spec: + names: + kind: K8sAzureV1AntiAffinityRules + targets: + - target: admission.k8s.gatekeeper.sh + rego: | + package k8sazurev1antiaffinityrules + + missing_affinity(obj) { + not obj.affinity.podAntiAffinity + } + + violation[{"msg": msg}] { + input.review.object.spec.replicas > 1 + missing_affinity(input.review.object.spec.template.spec) + msg := sprintf("%s with %d replicas should have pod anti-affinity rules set to avoid disruptions due to nodes crashing", [input.review.kind.kind, input.review.object.spec.replicas]) + } \ No newline at end of file diff --git a/pkg/safeguards/lib/manifests/v2.0.0/restricted-taints/constraint.yaml b/pkg/safeguards/lib/manifests/v2.0.0/restricted-taints/constraint.yaml new file mode 100755 index 00000000..1ac015a6 --- /dev/null +++ b/pkg/safeguards/lib/manifests/v2.0.0/restricted-taints/constraint.yaml @@ -0,0 +1,11 @@ +apiVersion: constraints.gatekeeper.sh/v1beta1 +kind: K8sAzureV1ReservedTaints +metadata: + name: v1-system-reserved-taints +spec: + match: + kinds: + - apiGroups: [""] + kinds: ["Node"] + parameters: + reservedTaints: ["CriticalAddonsOnly"] \ No newline at end of file diff --git a/pkg/safeguards/lib/manifests/v2.0.0/restricted-taints/template.yaml b/pkg/safeguards/lib/manifests/v2.0.0/restricted-taints/template.yaml new file mode 100755 index 00000000..ee17e48e --- /dev/null +++ b/pkg/safeguards/lib/manifests/v2.0.0/restricted-taints/template.yaml @@ -0,0 +1,43 @@ +apiVersion: templates.gatekeeper.sh/v1beta1 +kind: ConstraintTemplate +metadata: + name: k8sazurev1reservedtaints + annotations: + description: Restricts the CriticalAddonsOnly taint to just the system pool +spec: + crd: + spec: + names: + kind: K8sAzureV1ReservedTaints + validation: + openAPIV3Schema: + type: object + properties: + reservedTaints: + type: array + items: + type: string + targets: + - target: admission.k8s.gatekeeper.sh + rego: | + package k8sazurev1reservedtaints + + is_system_pool(node) { + node.metadata.labels["kubernetes.azure.com/mode"] == "system" + } + + is_system_pool(node) { + node.metadata.labels["kubernetes.azure.com/mode"] == "System" + } + + violation[{"msg": msg}] { + node := input.review.object + # did the customer try to add a taint with key "CriticalAddonsOnly" to a non-system pool? + taints := {x | x = node.spec.taints[_].key} + not is_system_pool(node) + taint := taints[_] + restrictedTaint := input.parameters.reservedTaints[_] + regex.match(restrictedTaint,taint) + + msg := sprintf("Taint with key <%s> is reserved for the system pool only",[taint]) + } \ No newline at end of file diff --git a/pkg/safeguards/lib/manifests/v2.0.0/unique-service-selectors/constraint.yaml b/pkg/safeguards/lib/manifests/v2.0.0/unique-service-selectors/constraint.yaml new file mode 100755 index 00000000..695fde13 --- /dev/null +++ b/pkg/safeguards/lib/manifests/v2.0.0/unique-service-selectors/constraint.yaml @@ -0,0 +1,9 @@ +apiVersion: constraints.gatekeeper.sh/v1beta1 +kind: K8sAzureV1UniqueServiceSelector +metadata: + name: v1-unique-service-selector +spec: + match: + kinds: + - apiGroups: [""] + kinds: ["Service"] \ No newline at end of file diff --git a/pkg/safeguards/lib/manifests/v2.0.0/unique-service-selectors/template.yaml b/pkg/safeguards/lib/manifests/v2.0.0/unique-service-selectors/template.yaml new file mode 100755 index 00000000..9a97ac5e --- /dev/null +++ b/pkg/safeguards/lib/manifests/v2.0.0/unique-service-selectors/template.yaml @@ -0,0 +1,70 @@ +apiVersion: templates.gatekeeper.sh/v1beta1 +kind: ConstraintTemplate +metadata: + name: k8sazurev1uniqueserviceselector + annotations: + metadata.gatekeeper.sh/title: "Unique Service Selectors" + metadata.gatekeeper.sh/version: 1.0.0 + metadata.gatekeeper.sh/requires-sync-data: | + "[ + [ + { + "groups": [""], + "versions": ["v1"], + "kinds": ["Service"] + } + ] + ]" + description: >- + Requires Services to have unique selectors within a namespace. + Selectors are considered the same if they have identical keys and values. + Selectors may share a key/value pair so long as there is at least one + distinct key/value pair between them. + + https://kubernetes.io/docs/concepts/services-networking/service/#defining-a-service +spec: + crd: + spec: + names: + kind: K8sAzureV1UniqueServiceSelector + targets: + - target: admission.k8s.gatekeeper.sh + rego: | + package k8sazurev1uniqueserviceselector + + make_apiversion(kind) = apiVersion { + g := kind.group + v := kind.version + g != "" + apiVersion = sprintf("%s/%s", [g, v]) + } + + make_apiversion(kind) = apiVersion { + kind.group == "" + apiVersion = kind.version + } + + identical(obj, review) { + obj.metadata.namespace == review.object.metadata.namespace + obj.metadata.name == review.object.metadata.name + obj.kind == review.kind.kind + obj.apiVersion == make_apiversion(review.kind) + } + + flatten_selector(obj) = flattened { + selectors := [s | s = concat(":", [key, val]); val = obj.spec.selector[key]] + flattened := concat(",", sort(selectors)) + } + + violation[{"msg": msg}] { + input.review.kind.kind == "Service" + input.review.kind.version == "v1" + input.review.kind.group == "" + input_namespace := input.review.object.metadata.namespace + input_selector := flatten_selector(input.review.object) + other := data.inventory.namespace[input_namespace]["v1"].Service[_] + not identical(other, input.review) + other_selector := flatten_selector(other) + input_selector == other_selector + msg := sprintf("same selector as service <%s> in namespace <%s>", [other.metadata.name, input_namespace]) + } \ No newline at end of file