diff --git a/gatekeeper-config/charts/Chart.yaml b/gatekeeper-config/charts/Chart.yaml index 0c1b9be95..06962a225 100644 --- a/gatekeeper-config/charts/Chart.yaml +++ b/gatekeeper-config/charts/Chart.yaml @@ -4,8 +4,8 @@ apiVersion: v2 description: Helm chart deploying OPA Gatekeeper ConstraintTemplates and Constraints for Kubernetes admission control. name: gatekeeper-config -version: 0.1.0 -appVersion: 0.1.0 +version: 0.2.0 +appVersion: 0.2.0 icon: https://raw.githubusercontent.com/open-policy-agent/gatekeeper/master/logo/gatekeeper-horizontal-color.png keywords: - gatekeeper diff --git a/gatekeeper-config/charts/templates/constraint-forbidden-clusterwide-objects.yaml b/gatekeeper-config/charts/templates/constraint-forbidden-clusterwide-objects.yaml new file mode 100644 index 000000000..1d6dedb08 --- /dev/null +++ b/gatekeeper-config/charts/templates/constraint-forbidden-clusterwide-objects.yaml @@ -0,0 +1,20 @@ +{{- if .Values.policies.forbiddenClusterwideObjects.enabled }} +# SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company and Greenhouse contributors +# SPDX-License-Identifier: Apache-2.0 +apiVersion: constraints.gatekeeper.sh/v1beta1 +kind: GkForbiddenClusterwideObjects +metadata: + name: forbidden-clusterwide-objects + labels: + severity: 'debug' +spec: + enforcementAction: {{ .Values.policies.forbiddenClusterwideObjects.enforcementAction }} + match: + kinds: + - apiGroups: ["admissionregistration.k8s.io"] + kinds: ["MutatingWebhookConfiguration", "ValidatingWebhookConfiguration"] + scope: Cluster + parameters: + allowedWebhooks: + {{- toYaml .Values.policies.forbiddenClusterwideObjects.allowedWebhooks | nindent 6 }} +{{- end }} diff --git a/gatekeeper-config/charts/templates/constraint-images-from-approved-registries.yaml b/gatekeeper-config/charts/templates/constraint-images-from-approved-registries.yaml new file mode 100644 index 000000000..4dbb1becd --- /dev/null +++ b/gatekeeper-config/charts/templates/constraint-images-from-approved-registries.yaml @@ -0,0 +1,24 @@ +{{- if .Values.policies.imagesFromApprovedRegistries.enabled }} +# SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company and Greenhouse contributors +# SPDX-License-Identifier: Apache-2.0 +apiVersion: constraints.gatekeeper.sh/v1beta1 +kind: GkImagesFromApprovedRegistries +metadata: + name: images-from-approved-registries + labels: + severity: 'warning' +spec: + enforcementAction: {{ .Values.policies.imagesFromApprovedRegistries.enforcementAction }} + match: + kinds: + - apiGroups: ["apps"] + kinds: ["Deployment", "DaemonSet", "StatefulSet", "ReplicaSet"] + - apiGroups: [""] + kinds: ["Pod"] + - apiGroups: ["batch"] + kinds: ["Job", "CronJob"] + scope: Namespaced + parameters: + allowedRegistries: + {{- toYaml .Values.policies.imagesFromApprovedRegistries.allowedRegistries | nindent 6 }} +{{- end }} diff --git a/gatekeeper-config/charts/templates/constraint-pci-forbidden-images.yaml b/gatekeeper-config/charts/templates/constraint-pci-forbidden-images.yaml new file mode 100644 index 000000000..04e250d68 --- /dev/null +++ b/gatekeeper-config/charts/templates/constraint-pci-forbidden-images.yaml @@ -0,0 +1,24 @@ +{{- if .Values.policies.pciForbiddenImages.enabled }} +# SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company and Greenhouse contributors +# SPDX-License-Identifier: Apache-2.0 +apiVersion: constraints.gatekeeper.sh/v1beta1 +kind: GkPCIForbiddenImages +metadata: + name: pci-forbidden-images + labels: + severity: 'error' +spec: + enforcementAction: {{ .Values.policies.pciForbiddenImages.enforcementAction }} + match: + kinds: + - apiGroups: ["apps"] + kinds: ["Deployment", "DaemonSet", "StatefulSet", "ReplicaSet"] + - apiGroups: [""] + kinds: ["Pod"] + - apiGroups: ["batch"] + kinds: ["Job", "CronJob"] + scope: Namespaced + parameters: + patterns: + {{- toYaml .Values.policies.pciForbiddenImages.patterns | nindent 6 }} +{{- end }} diff --git a/gatekeeper-config/charts/templates/constraint-pod-required-labels.yaml b/gatekeeper-config/charts/templates/constraint-pod-required-labels.yaml new file mode 100644 index 000000000..8cd50992a --- /dev/null +++ b/gatekeeper-config/charts/templates/constraint-pod-required-labels.yaml @@ -0,0 +1,24 @@ +{{- if .Values.policies.podRequiredLabels.enabled }} +# SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company and Greenhouse contributors +# SPDX-License-Identifier: Apache-2.0 +apiVersion: constraints.gatekeeper.sh/v1beta1 +kind: GkPodRequiredLabels +metadata: + name: pod-required-labels + labels: + severity: 'warning' +spec: + enforcementAction: {{ .Values.policies.podRequiredLabels.enforcementAction }} + match: + kinds: + - apiGroups: ["apps"] + kinds: ["Deployment", "DaemonSet", "StatefulSet", "ReplicaSet"] + - apiGroups: [""] + kinds: ["Pod"] + - apiGroups: ["batch"] + kinds: ["Job", "CronJob"] + scope: Namespaced + parameters: + requiredLabels: + {{- toYaml .Values.policies.podRequiredLabels.requiredLabels | nindent 6 }} +{{- end }} diff --git a/gatekeeper-config/charts/templates/constraint-pod-security-v2.yaml b/gatekeeper-config/charts/templates/constraint-pod-security-v2.yaml new file mode 100644 index 000000000..95814f982 --- /dev/null +++ b/gatekeeper-config/charts/templates/constraint-pod-security-v2.yaml @@ -0,0 +1,24 @@ +{{- if .Values.policies.podSecurityV2.enabled }} +# SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company and Greenhouse contributors +# SPDX-License-Identifier: Apache-2.0 +apiVersion: constraints.gatekeeper.sh/v1beta1 +kind: GkPodSecurityV2 +metadata: + name: pod-security-v2 + labels: + severity: 'debug' +spec: + enforcementAction: {{ .Values.policies.podSecurityV2.enforcementAction }} + match: + kinds: + - apiGroups: ["apps"] + kinds: ["Deployment", "DaemonSet", "StatefulSet", "ReplicaSet"] + - apiGroups: [""] + kinds: ["Pod"] + - apiGroups: ["batch"] + kinds: ["Job", "CronJob"] + scope: Namespaced + parameters: + allowlist: + {{- toYaml .Values.policies.podSecurityV2.allowlist | nindent 6 }} +{{- end }} diff --git a/gatekeeper-config/charts/templates/constrainttemplate-forbidden-clusterwide-objects.yaml b/gatekeeper-config/charts/templates/constrainttemplate-forbidden-clusterwide-objects.yaml new file mode 100644 index 000000000..8788629af --- /dev/null +++ b/gatekeeper-config/charts/templates/constrainttemplate-forbidden-clusterwide-objects.yaml @@ -0,0 +1,44 @@ +{{- if .Values.policies.forbiddenClusterwideObjects.enabled }} +# SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company and Greenhouse contributors +# SPDX-License-Identifier: Apache-2.0 +apiVersion: templates.gatekeeper.sh/v1 +kind: ConstraintTemplate +metadata: + name: gkforbiddenclusterwideobjects +spec: + crd: + spec: + names: + kind: GkForbiddenClusterwideObjects + validation: + openAPIV3Schema: + type: object + properties: + allowedWebhooks: + type: array + items: + description: name of an admission webhook that is allowed to exist + type: string + + targets: + - target: admission.k8s.gatekeeper.sh + code: + - engine: Rego + source: + version: "v1" + libs: + - | +{{ include "gatekeeper-config.lib.add_support_labels" . | indent 16 }} + rego: | + package forbiddenclusterwideobjects + + import data.lib.add_support_labels + + iro := input.review.object + + violation contains {"msg": add_support_labels.from_k8s_object(iro, msg)} if { + webhook := iro.webhooks[_] + not webhook.name in {n | n := input.parameters.allowedWebhooks[_]} + msg := sprintf("webhook %q does not match the configured allowlist", [webhook.name]) + } +{{- end }} diff --git a/gatekeeper-config/charts/templates/constrainttemplate-images-from-approved-registries.yaml b/gatekeeper-config/charts/templates/constrainttemplate-images-from-approved-registries.yaml new file mode 100644 index 000000000..b9eb40d65 --- /dev/null +++ b/gatekeeper-config/charts/templates/constrainttemplate-images-from-approved-registries.yaml @@ -0,0 +1,54 @@ +{{- if .Values.policies.imagesFromApprovedRegistries.enabled }} +# SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company and Greenhouse contributors +# SPDX-License-Identifier: Apache-2.0 +apiVersion: templates.gatekeeper.sh/v1 +kind: ConstraintTemplate +metadata: + name: gkimagesfromapprovedregistries +spec: + crd: + spec: + names: + kind: GkImagesFromApprovedRegistries + validation: + openAPIV3Schema: + type: object + properties: + allowedRegistries: + type: array + items: + description: allowed registry prefix + type: string + + targets: + - target: admission.k8s.gatekeeper.sh + code: + - engine: Rego + source: + version: "v1" + libs: + - | +{{ include "gatekeeper-config.lib.add_support_labels" . | indent 16 }} + - | +{{ include "gatekeeper-config.lib.traversal" . | indent 16 }} + rego: | + package imagesfromapprovedregistries + + import data.lib.add_support_labels + import data.lib.traversal + + iro := input.review.object + containers := traversal.find_container_specs(iro) + + violation contains {"msg": add_support_labels.from_k8s_object(iro, msg)} if { + container := containers[_] + image := container.image + not image_from_approved_registry(image) + msg := sprintf("container %q uses an image from an unapproved registry: %s", [container.name, image]) + } + + image_from_approved_registry(image) if { + prefix := input.parameters.allowedRegistries[_] + startswith(image, prefix) + } +{{- end }} diff --git a/gatekeeper-config/charts/templates/constrainttemplate-pci-forbidden-images.yaml b/gatekeeper-config/charts/templates/constrainttemplate-pci-forbidden-images.yaml new file mode 100644 index 000000000..020b707f1 --- /dev/null +++ b/gatekeeper-config/charts/templates/constrainttemplate-pci-forbidden-images.yaml @@ -0,0 +1,51 @@ +{{- if .Values.policies.pciForbiddenImages.enabled }} +# SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company and Greenhouse contributors +# SPDX-License-Identifier: Apache-2.0 +apiVersion: templates.gatekeeper.sh/v1 +kind: ConstraintTemplate +metadata: + name: gkpciforbiddenimages +spec: + crd: + spec: + names: + kind: GkPCIForbiddenImages + validation: + openAPIV3Schema: + type: object + properties: + patterns: + type: array + items: + description: regex pattern matched against the container image reference + type: string + + targets: + - target: admission.k8s.gatekeeper.sh + code: + - engine: Rego + source: + version: "v1" + libs: + - | +{{ include "gatekeeper-config.lib.add_support_labels" . | indent 16 }} + - | +{{ include "gatekeeper-config.lib.traversal" . | indent 16 }} + rego: | + package pciforbiddenimages + + import data.lib.add_support_labels + import data.lib.traversal + + iro := input.review.object + containers := traversal.find_container_specs(iro) + + violation contains {"msg": add_support_labels.from_k8s_object(iro, msg)} if { + container := containers[_] + + pattern := input.parameters.patterns[_] + regex.match(pattern, container.image) + + msg := sprintf("container %q uses forbidden image: %s", [container.name, container.image]) + } +{{- end }} diff --git a/gatekeeper-config/charts/templates/constrainttemplate-pod-required-labels.yaml b/gatekeeper-config/charts/templates/constrainttemplate-pod-required-labels.yaml new file mode 100644 index 000000000..9cda572bc --- /dev/null +++ b/gatekeeper-config/charts/templates/constrainttemplate-pod-required-labels.yaml @@ -0,0 +1,57 @@ +{{- if .Values.policies.podRequiredLabels.enabled }} +# SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company and Greenhouse contributors +# SPDX-License-Identifier: Apache-2.0 +apiVersion: templates.gatekeeper.sh/v1 +kind: ConstraintTemplate +metadata: + name: gkpodrequiredlabels +spec: + crd: + spec: + names: + kind: GkPodRequiredLabels + validation: + openAPIV3Schema: + type: object + properties: + requiredLabels: + type: array + items: + description: required label key + type: string + + targets: + - target: admission.k8s.gatekeeper.sh + code: + - engine: Rego + source: + version: "v1" + libs: + - | +{{ include "gatekeeper-config.lib.add_support_labels" . | indent 16 }} + - | +{{ include "gatekeeper-config.lib.traversal" . | indent 16 }} + rego: | + package podrequiredlabels + + import data.lib.add_support_labels + import data.lib.traversal + + iro := input.review.object + pod := traversal.find_pod(iro) + + # The labels that matter for ownership/routing are on the pod + # template metadata, not the workload metadata, so the lookup + # uses `pod` rather than `iro`. + # + # `object.get(..., null) == null` is the right idiom for "label + # missing": `not object.get(...)` is a no-op because null is a + # defined value (so `not null` is false, not true), which would + # make this rule silently never fire. + violation contains {"msg": add_support_labels.from_k8s_object(iro, msg)} if { + pod.isFound + labelKey := input.parameters.requiredLabels[_] + object.get(pod, ["metadata", "labels", labelKey], null) == null + msg := sprintf("pod is missing required label: %s", [labelKey]) + } +{{- end }} diff --git a/gatekeeper-config/charts/templates/constrainttemplate-pod-security-v2.yaml b/gatekeeper-config/charts/templates/constrainttemplate-pod-security-v2.yaml new file mode 100644 index 000000000..82e03ee66 --- /dev/null +++ b/gatekeeper-config/charts/templates/constrainttemplate-pod-security-v2.yaml @@ -0,0 +1,305 @@ +{{- if .Values.policies.podSecurityV2.enabled }} +# SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company and Greenhouse contributors +# SPDX-License-Identifier: Apache-2.0 +apiVersion: templates.gatekeeper.sh/v1 +kind: ConstraintTemplate +metadata: + name: gkpodsecurityv2 +spec: + crd: + spec: + names: + kind: GkPodSecurityV2 + validation: + openAPIV3Schema: + type: object + properties: + allowlist: + type: array + items: + description: positive allowlist entry + type: object + properties: + matchNamespace: + type: string + matchRepository: + type: string + justification: + type: string + mayUseHostNetwork: + type: boolean + mayUseHostPID: + type: boolean + mayUsePrivilegeEscalation: + type: boolean + mayBePrivileged: + type: boolean + mayUseCapabilities: + type: array + items: + description: capability + type: string + mayReadHostPathVolumes: + type: array + items: + description: host path of the volume + type: string + mayWriteHostPathVolumes: + type: array + items: + description: host path of the volume + type: string + + targets: + - target: admission.k8s.gatekeeper.sh + code: + - engine: Rego + source: + version: "v1" + libs: + - | +{{ include "gatekeeper-config.lib.add_support_labels" . | indent 16 }} + - | +{{ include "gatekeeper-config.lib.traversal" . | indent 16 }} + rego: | + package podsecurityv2 + + import data.lib.add_support_labels + import data.lib.traversal + + iro := input.review.object + pod := traversal.find_pod(iro) + containers := traversal.find_container_specs(iro) + + ######################################################################## + # allowlists: match pods that may use certain privileged features + # + # By default, everything is forbidden; + # positive allowlist entries from the config are evaluated at the end + + default isPodAllowedToUseHostNetwork := false + default isPodAllowedToUseHostPID := false + default isContainerAllowedToUsePrivilegeEscalation(container) := false + default isContainerAllowedToBePrivileged(container) := false + default isContainerAllowedToUseCapability(container, capability) := false + default isContainerAllowedToAccessHostPath(container, hostPath, readOnly) := false + + # Blanket allowance for readonly access to paths known not to contain credentials. + isContainerAllowedToAccessHostPath(container, hostPath, readOnly) if { + readOnly + normalizedHostPath := normalize_path(hostPath) + normalizedHostPath in ["/etc/machine-id", "/lib/modules"] + } + + ######################################################################## + # generate violations for all pods using privileged security features + # without being allowlisted + + violation contains {"msg": add_support_labels.from_k8s_object(iro, msg)} if { + pod.isFound + object.get(pod.spec, ["hostNetwork"], false) + not isPodAllowedToUseHostNetwork + msg := "pod is not allowed to set spec.hostNetwork = true" + } + + violation contains {"msg": add_support_labels.from_k8s_object(iro, msg)} if { + pod.isFound + object.get(pod.spec, ["hostPID"], false) + not isPodAllowedToUseHostPID + msg := "pod is not allowed to set spec.hostPID = true" + } + + violation contains {"msg": add_support_labels.from_k8s_object(iro, msg)} if { + container := containers[_] + object.get(container, ["securityContext", "allowPrivilegeEscalation"], false) + not isContainerAllowedToUsePrivilegeEscalation(container) + msg := sprintf("pod is not allowed to set spec.containers[%q].securityContext.allowPrivilegeEscalation = true (image = %q)", [container.name, extract_repository(container.image)]) + } + + violation contains {"msg": add_support_labels.from_k8s_object(iro, msg)} if { + container := containers[_] + object.get(container, ["securityContext", "privileged"], false) + not isContainerAllowedToBePrivileged(container) + msg := sprintf("pod is not allowed to set spec.containers[%q].securityContext.privileged = true (image = %q)", [container.name, extract_repository(container.image)]) + } + + violation contains {"msg": add_support_labels.from_k8s_object(iro, msg)} if { + container := containers[_] + capabilities := object.get(container, ["securityContext", "capabilities", "add"], []) + capability := capabilities[_] + not isContainerAllowedToUseCapability(container, capability) + msg := sprintf("pod is not allowed to set spec.containers[%q].securityContext.capabilities.add = [%q] (image = %q)", [container.name, capability, extract_repository(container.image)]) + } + + violation contains {"msg": add_support_labels.from_k8s_object(iro, msg)} if { + pod.isFound + volume := pod.spec.volumes[_] + hostPath := object.get(volume, ["hostPath", "path"], null) + hostPath != null + + container := containers[_] + volumeMount := container.volumeMounts[_] + volume.name == volumeMount.name + readOnly := object.get(volumeMount, ["readOnly"], false) + + not isContainerAllowedToAccessHostPath(container, hostPath, readOnly) + msg := sprintf("container %q in this pod is not allowed to mount hostPath volumes with path %q (readonly = %v) (image = %q)", [container.name, hostPath, readOnly, extract_repository(container.image)]) + } + + ######################################################################## + # evaluate allowlist entries in config + + isPodAllowedToUseHostNetwork if { + every container in containers { + some entry in input.parameters.allowlist + isMatchingContainer(entry, container) + entry.mayUseHostNetwork + } + } + + isPodAllowedToUseHostPID if { + every container in containers { + some entry in input.parameters.allowlist + isMatchingContainer(entry, container) + entry.mayUseHostPID + } + } + + isContainerAllowedToUsePrivilegeEscalation(container) if { + entry := input.parameters.allowlist[_] + isMatchingContainer(entry, container) + entry.mayUsePrivilegeEscalation + } + + isContainerAllowedToBePrivileged(container) if { + entry := input.parameters.allowlist[_] + isMatchingContainer(entry, container) + entry.mayBePrivileged + } + + isContainerAllowedToUseCapability(container, capability) if { + entry := input.parameters.allowlist[_] + isMatchingContainer(entry, container) + capability in entry.mayUseCapabilities + } + + isContainerAllowedToAccessHostPath(container, hostPath, readOnly) if { + entry := input.parameters.allowlist[_] + isMatchingContainer(entry, container) + readOnly + normalizedHostPath := normalize_path(hostPath) + some allowedPath in entry.mayReadHostPathVolumes + normalizedAllowedPath := normalize_path(allowedPath) + is_path_prefix(normalizedAllowedPath, normalizedHostPath) + } + + isContainerAllowedToAccessHostPath(container, hostPath, readOnly) if { + entry := input.parameters.allowlist[_] + isMatchingContainer(entry, container) + normalizedHostPath := normalize_path(hostPath) + some allowedPath in entry.mayWriteHostPathVolumes + normalizedAllowedPath := normalize_path(allowedPath) + is_path_prefix(normalizedAllowedPath, normalizedHostPath) + } + + ######################################################################## + # helper functions + + is_path_prefix(prefix, path) if { + prefix == "/" + } + + is_path_prefix(prefix, path) if { + path == prefix + } + + is_path_prefix(prefix, path) if { + prefix != "/" + startswith(path, sprintf("%s/", [prefix])) + } + + normalize_path(path) := result if { + path == "/" + result := path + } + + normalize_path(path) := result if { + path != "/" + endswith(path, "/") + result := substring(path, 0, count(path) - 1) + } + + normalize_path(path) := result if { + path != "/" + not endswith(path, "/") + result := path + } + + extract_repository(image) := result if { + no_digest := strip_digest(image) + no_registry := strip_registry(no_digest) + result := strip_tag(no_registry) + } + + strip_digest(image) := result if { + at_index := indexof(image, "@") + at_index == -1 + result := image + } + + strip_digest(image) := result if { + at_index := indexof(image, "@") + at_index != -1 + result := substring(image, 0, at_index) + } + + strip_registry(image) := result if { + has_registry_prefix(image) + parts := split(image, "/") + result := concat("/", array.slice(parts, 1, count(parts))) + } + + strip_registry(image) := result if { + not has_registry_prefix(image) + result := image + } + + has_registry_prefix(image) if { + slash_index := indexof(image, "/") + slash_index > 0 + prefix := substring(image, 0, slash_index) + contains(prefix, ".") + } + + has_registry_prefix(image) if { + slash_index := indexof(image, "/") + slash_index > 0 + prefix := substring(image, 0, slash_index) + contains(prefix, ":") + } + + strip_tag(s) := result if { + colon_index := indexof(s, ":") + colon_index == -1 + result := s + } + + strip_tag(s) := result if { + colon_index := indexof(s, ":") + colon_index != -1 + result := substring(s, 0, colon_index) + } + + isMatchingContainer(entry, container) if { + entry.matchNamespace != "*" + iro.metadata.namespace == entry.matchNamespace + repo := extract_repository(container.image) + repo == entry.matchRepository + } + + isMatchingContainer(entry, container) if { + entry.matchNamespace == "*" + repo := extract_repository(container.image) + repo == entry.matchRepository + } +{{- end }} diff --git a/gatekeeper-config/charts/values.yaml b/gatekeeper-config/charts/values.yaml index 2afebe0c8..7587dcc77 100644 --- a/gatekeeper-config/charts/values.yaml +++ b/gatekeeper-config/charts/values.yaml @@ -12,5 +12,30 @@ policies: enforcementAction: dryrun allowedNamePatterns: [] + forbiddenClusterwideObjects: + enabled: false + enforcementAction: dryrun + allowedWebhooks: [] + + imagesFromApprovedRegistries: + enabled: false + enforcementAction: dryrun + allowedRegistries: [] + + pciForbiddenImages: + enabled: false + enforcementAction: dryrun + patterns: [] + + podRequiredLabels: + enabled: false + enforcementAction: dryrun + requiredLabels: [] + + podSecurityV2: + enabled: false + enforcementAction: dryrun + allowlist: [] + tests: enabled: false diff --git a/gatekeeper-config/plugindefinition.yaml b/gatekeeper-config/plugindefinition.yaml index 510f9c976..2ef2b4cdd 100644 --- a/gatekeeper-config/plugindefinition.yaml +++ b/gatekeeper-config/plugindefinition.yaml @@ -6,7 +6,7 @@ kind: PluginDefinition metadata: name: gatekeeper-config spec: - version: 0.1.0 + version: 0.2.0 displayName: Gatekeeper Config description: | Deploys OPA Gatekeeper ConstraintTemplates and Constraints for Kubernetes @@ -17,7 +17,7 @@ spec: helmChart: name: gatekeeper-config repository: oci://ghcr.io/cloudoperators/greenhouse-extensions/charts - version: 0.1.0 + version: 0.2.0 options: - name: policies.highCpuRequests.enabled displayName: High CPU requests policy enabled @@ -55,3 +55,96 @@ spec: description: List of {namespace, namePattern} entries that exempt matching Pods from the unmanaged-pods policy. required: false type: list + + - name: policies.forbiddenClusterwideObjects.enabled + displayName: Forbidden clusterwide objects policy enabled + description: Enable the forbidden-clusterwide-objects policy. + default: false + required: false + type: bool + - name: policies.forbiddenClusterwideObjects.enforcementAction + displayName: Forbidden clusterwide objects enforcement action + description: Gatekeeper enforcementAction for forbidden-clusterwide-objects (dryrun, warn, or deny). + default: dryrun + required: false + type: string + - name: policies.forbiddenClusterwideObjects.allowedWebhooks + displayName: Allowed admission webhook names + description: Names of MutatingWebhookConfiguration and ValidatingWebhookConfiguration entries that may exist in the cluster. + required: false + type: list + + - name: policies.imagesFromApprovedRegistries.enabled + displayName: Images from approved registries policy enabled + description: Enable the images-from-approved-registries policy. + default: false + required: false + type: bool + - name: policies.imagesFromApprovedRegistries.enforcementAction + displayName: Images from approved registries enforcement action + description: Gatekeeper enforcementAction for images-from-approved-registries (dryrun, warn, or deny). + default: dryrun + required: false + type: string + - name: policies.imagesFromApprovedRegistries.allowedRegistries + displayName: Allowed registry prefixes + description: List of registry prefixes that container images may be pulled from. + required: false + type: list + + - name: policies.pciForbiddenImages.enabled + displayName: PCI forbidden images policy enabled + description: Enable the pci-forbidden-images policy. + default: false + required: false + type: bool + - name: policies.pciForbiddenImages.enforcementAction + displayName: PCI forbidden images enforcement action + description: Gatekeeper enforcementAction for pci-forbidden-images (dryrun, warn, or deny). + default: dryrun + required: false + type: string + - name: policies.pciForbiddenImages.patterns + displayName: Forbidden image regex patterns + description: Regex patterns matched against container image references. Matching images are flagged. + required: false + type: list + + - name: policies.podRequiredLabels.enabled + displayName: Pod required labels policy enabled + description: Enable the pod-required-labels policy. + default: false + required: false + type: bool + - name: policies.podRequiredLabels.enforcementAction + displayName: Pod required labels enforcement action + description: Gatekeeper enforcementAction for pod-required-labels (dryrun, warn, or deny). + default: dryrun + required: false + type: string + - name: policies.podRequiredLabels.requiredLabels + displayName: Required pod template labels + description: Label keys that must be present on every pod template (spec.template.metadata.labels). + required: false + type: list + + - name: policies.podSecurityV2.enabled + displayName: Pod security v2 policy enabled + description: Enable the pod-security-v2 policy. + default: false + required: false + type: bool + - name: policies.podSecurityV2.enforcementAction + displayName: Pod security v2 enforcement action + description: Gatekeeper enforcementAction for pod-security-v2 (dryrun, warn, or deny). + default: dryrun + required: false + type: string + - name: policies.podSecurityV2.allowlist + displayName: Pod security v2 allowlist entries + description: | + List of allowlist entries granting matching pods access to privileged features + (hostNetwork, hostPID, privileged, capabilities, hostPath volumes). Each entry + is keyed by {matchNamespace, matchRepository}. + required: false + type: list diff --git a/gatekeeper-config/tests/fixtures/forbidden-clusterwide-objects/mwc-allowed.yaml b/gatekeeper-config/tests/fixtures/forbidden-clusterwide-objects/mwc-allowed.yaml new file mode 100644 index 000000000..c273f4d36 --- /dev/null +++ b/gatekeeper-config/tests/fixtures/forbidden-clusterwide-objects/mwc-allowed.yaml @@ -0,0 +1,12 @@ +# SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company and Greenhouse contributors +# SPDX-License-Identifier: Apache-2.0 + +apiVersion: admissionregistration.k8s.io/v1 +kind: MutatingWebhookConfiguration +metadata: + name: allowed-mwc +webhooks: + - name: allowed.example.com + clientConfig: {} + sideEffects: None + admissionReviewVersions: ["v1"] diff --git a/gatekeeper-config/tests/fixtures/forbidden-clusterwide-objects/mwc-disallowed.yaml b/gatekeeper-config/tests/fixtures/forbidden-clusterwide-objects/mwc-disallowed.yaml new file mode 100644 index 000000000..d168356e1 --- /dev/null +++ b/gatekeeper-config/tests/fixtures/forbidden-clusterwide-objects/mwc-disallowed.yaml @@ -0,0 +1,12 @@ +# SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company and Greenhouse contributors +# SPDX-License-Identifier: Apache-2.0 + +apiVersion: admissionregistration.k8s.io/v1 +kind: MutatingWebhookConfiguration +metadata: + name: disallowed-mwc +webhooks: + - name: rogue.example.com + clientConfig: {} + sideEffects: None + admissionReviewVersions: ["v1"] diff --git a/gatekeeper-config/tests/fixtures/forbidden-clusterwide-objects/vwc-allowed.yaml b/gatekeeper-config/tests/fixtures/forbidden-clusterwide-objects/vwc-allowed.yaml new file mode 100644 index 000000000..10e2995d7 --- /dev/null +++ b/gatekeeper-config/tests/fixtures/forbidden-clusterwide-objects/vwc-allowed.yaml @@ -0,0 +1,12 @@ +# SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company and Greenhouse contributors +# SPDX-License-Identifier: Apache-2.0 + +apiVersion: admissionregistration.k8s.io/v1 +kind: ValidatingWebhookConfiguration +metadata: + name: allowed-vwc +webhooks: + - name: allowed.example.com + clientConfig: {} + sideEffects: None + admissionReviewVersions: ["v1"] diff --git a/gatekeeper-config/tests/fixtures/images-from-approved-registries/pod-approved.yaml b/gatekeeper-config/tests/fixtures/images-from-approved-registries/pod-approved.yaml new file mode 100644 index 000000000..84d74a7db --- /dev/null +++ b/gatekeeper-config/tests/fixtures/images-from-approved-registries/pod-approved.yaml @@ -0,0 +1,12 @@ +# SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company and Greenhouse contributors +# SPDX-License-Identifier: Apache-2.0 + +apiVersion: v1 +kind: Pod +metadata: + name: approved + namespace: default +spec: + containers: + - name: app + image: registry.k8s.io/pause:3.9 diff --git a/gatekeeper-config/tests/fixtures/images-from-approved-registries/pod-mixed-containers.yaml b/gatekeeper-config/tests/fixtures/images-from-approved-registries/pod-mixed-containers.yaml new file mode 100644 index 000000000..c2912eed9 --- /dev/null +++ b/gatekeeper-config/tests/fixtures/images-from-approved-registries/pod-mixed-containers.yaml @@ -0,0 +1,14 @@ +# SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company and Greenhouse contributors +# SPDX-License-Identifier: Apache-2.0 + +apiVersion: v1 +kind: Pod +metadata: + name: mixed + namespace: default +spec: + containers: + - name: ok + image: ghcr.io/cloudoperators/sample:v1 + - name: bad + image: docker.io/library/nginx:1.27 diff --git a/gatekeeper-config/tests/fixtures/images-from-approved-registries/pod-unapproved.yaml b/gatekeeper-config/tests/fixtures/images-from-approved-registries/pod-unapproved.yaml new file mode 100644 index 000000000..9635b8fe7 --- /dev/null +++ b/gatekeeper-config/tests/fixtures/images-from-approved-registries/pod-unapproved.yaml @@ -0,0 +1,12 @@ +# SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company and Greenhouse contributors +# SPDX-License-Identifier: Apache-2.0 + +apiVersion: v1 +kind: Pod +metadata: + name: unapproved + namespace: default +spec: + containers: + - name: app + image: docker.io/library/nginx:1.27 diff --git a/gatekeeper-config/tests/fixtures/pci-forbidden-images/pod-allowed.yaml b/gatekeeper-config/tests/fixtures/pci-forbidden-images/pod-allowed.yaml new file mode 100644 index 000000000..6509a37a5 --- /dev/null +++ b/gatekeeper-config/tests/fixtures/pci-forbidden-images/pod-allowed.yaml @@ -0,0 +1,12 @@ +# SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company and Greenhouse contributors +# SPDX-License-Identifier: Apache-2.0 + +apiVersion: v1 +kind: Pod +metadata: + name: alpine-pod + namespace: default +spec: + containers: + - name: app + image: alpine:3 diff --git a/gatekeeper-config/tests/fixtures/pci-forbidden-images/pod-busybox-flagged.yaml b/gatekeeper-config/tests/fixtures/pci-forbidden-images/pod-busybox-flagged.yaml new file mode 100644 index 000000000..7b5264379 --- /dev/null +++ b/gatekeeper-config/tests/fixtures/pci-forbidden-images/pod-busybox-flagged.yaml @@ -0,0 +1,12 @@ +# SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company and Greenhouse contributors +# SPDX-License-Identifier: Apache-2.0 + +apiVersion: v1 +kind: Pod +metadata: + name: busybox-pod + namespace: default +spec: + containers: + - name: shell + image: busybox:1.36 diff --git a/gatekeeper-config/tests/fixtures/pci-forbidden-images/pod-init-busybox-flagged.yaml b/gatekeeper-config/tests/fixtures/pci-forbidden-images/pod-init-busybox-flagged.yaml new file mode 100644 index 000000000..07dede50d --- /dev/null +++ b/gatekeeper-config/tests/fixtures/pci-forbidden-images/pod-init-busybox-flagged.yaml @@ -0,0 +1,15 @@ +# SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company and Greenhouse contributors +# SPDX-License-Identifier: Apache-2.0 + +apiVersion: v1 +kind: Pod +metadata: + name: init-busybox-pod + namespace: default +spec: + initContainers: + - name: init + image: registry.k8s.io/busybox:1.36 + containers: + - name: app + image: alpine:3 diff --git a/gatekeeper-config/tests/fixtures/pod-required-labels/deployment-template-missing.yaml b/gatekeeper-config/tests/fixtures/pod-required-labels/deployment-template-missing.yaml new file mode 100644 index 000000000..7431b552f --- /dev/null +++ b/gatekeeper-config/tests/fixtures/pod-required-labels/deployment-template-missing.yaml @@ -0,0 +1,19 @@ +# SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company and Greenhouse contributors +# SPDX-License-Identifier: Apache-2.0 + +apiVersion: apps/v1 +kind: Deployment +metadata: + name: workload-labeled-template-not + namespace: default + labels: + greenhouse.sap/owned-by: my-team +spec: + template: + metadata: + labels: + app: example + spec: + containers: + - name: app + image: alpine:3 diff --git a/gatekeeper-config/tests/fixtures/pod-required-labels/pod-missing-label.yaml b/gatekeeper-config/tests/fixtures/pod-required-labels/pod-missing-label.yaml new file mode 100644 index 000000000..68a1a32d8 --- /dev/null +++ b/gatekeeper-config/tests/fixtures/pod-required-labels/pod-missing-label.yaml @@ -0,0 +1,12 @@ +# SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company and Greenhouse contributors +# SPDX-License-Identifier: Apache-2.0 + +apiVersion: v1 +kind: Pod +metadata: + name: unlabeled + namespace: default +spec: + containers: + - name: app + image: alpine:3 diff --git a/gatekeeper-config/tests/fixtures/pod-required-labels/pod-with-label.yaml b/gatekeeper-config/tests/fixtures/pod-required-labels/pod-with-label.yaml new file mode 100644 index 000000000..469bdb86c --- /dev/null +++ b/gatekeeper-config/tests/fixtures/pod-required-labels/pod-with-label.yaml @@ -0,0 +1,14 @@ +# SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company and Greenhouse contributors +# SPDX-License-Identifier: Apache-2.0 + +apiVersion: v1 +kind: Pod +metadata: + name: labeled + namespace: default + labels: + greenhouse.sap/owned-by: my-team +spec: + containers: + - name: app + image: alpine:3 diff --git a/gatekeeper-config/tests/fixtures/pod-security-v2/pod-capability-violation.yaml b/gatekeeper-config/tests/fixtures/pod-security-v2/pod-capability-violation.yaml new file mode 100644 index 000000000..8bd06d79d --- /dev/null +++ b/gatekeeper-config/tests/fixtures/pod-security-v2/pod-capability-violation.yaml @@ -0,0 +1,15 @@ +# SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company and Greenhouse contributors +# SPDX-License-Identifier: Apache-2.0 + +apiVersion: v1 +kind: Pod +metadata: + name: cap-add + namespace: default +spec: + containers: + - name: app + image: alpine:3 + securityContext: + capabilities: + add: ["NET_ADMIN"] diff --git a/gatekeeper-config/tests/fixtures/pod-security-v2/pod-host-network-allowlisted-registry-port.yaml b/gatekeeper-config/tests/fixtures/pod-security-v2/pod-host-network-allowlisted-registry-port.yaml new file mode 100644 index 000000000..57f14e0d0 --- /dev/null +++ b/gatekeeper-config/tests/fixtures/pod-security-v2/pod-host-network-allowlisted-registry-port.yaml @@ -0,0 +1,13 @@ +# SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company and Greenhouse contributors +# SPDX-License-Identifier: Apache-2.0 + +apiVersion: v1 +kind: Pod +metadata: + name: host-net-allowed-port-registry + namespace: kube-system +spec: + hostNetwork: true + containers: + - name: app + image: localhost:5000/k8s/system-pod:v1 diff --git a/gatekeeper-config/tests/fixtures/pod-security-v2/pod-host-network-allowlisted.yaml b/gatekeeper-config/tests/fixtures/pod-security-v2/pod-host-network-allowlisted.yaml new file mode 100644 index 000000000..6f751d636 --- /dev/null +++ b/gatekeeper-config/tests/fixtures/pod-security-v2/pod-host-network-allowlisted.yaml @@ -0,0 +1,13 @@ +# SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company and Greenhouse contributors +# SPDX-License-Identifier: Apache-2.0 + +apiVersion: v1 +kind: Pod +metadata: + name: host-net-allowed + namespace: kube-system +spec: + hostNetwork: true + containers: + - name: app + image: ghcr.io/k8s/system-pod:v1 diff --git a/gatekeeper-config/tests/fixtures/pod-security-v2/pod-host-network-violation.yaml b/gatekeeper-config/tests/fixtures/pod-security-v2/pod-host-network-violation.yaml new file mode 100644 index 000000000..a2b58eddf --- /dev/null +++ b/gatekeeper-config/tests/fixtures/pod-security-v2/pod-host-network-violation.yaml @@ -0,0 +1,13 @@ +# SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company and Greenhouse contributors +# SPDX-License-Identifier: Apache-2.0 + +apiVersion: v1 +kind: Pod +metadata: + name: host-net + namespace: default +spec: + hostNetwork: true + containers: + - name: app + image: alpine:3 diff --git a/gatekeeper-config/tests/fixtures/pod-security-v2/pod-privileged-violation.yaml b/gatekeeper-config/tests/fixtures/pod-security-v2/pod-privileged-violation.yaml new file mode 100644 index 000000000..9f4fc5995 --- /dev/null +++ b/gatekeeper-config/tests/fixtures/pod-security-v2/pod-privileged-violation.yaml @@ -0,0 +1,14 @@ +# SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company and Greenhouse contributors +# SPDX-License-Identifier: Apache-2.0 + +apiVersion: v1 +kind: Pod +metadata: + name: privileged + namespace: default +spec: + containers: + - name: app + image: alpine:3 + securityContext: + privileged: true diff --git a/gatekeeper-config/tests/fixtures/pod-security-v2/pod-readonly-machine-id-pass.yaml b/gatekeeper-config/tests/fixtures/pod-security-v2/pod-readonly-machine-id-pass.yaml new file mode 100644 index 000000000..8ddea8c85 --- /dev/null +++ b/gatekeeper-config/tests/fixtures/pod-security-v2/pod-readonly-machine-id-pass.yaml @@ -0,0 +1,20 @@ +# SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company and Greenhouse contributors +# SPDX-License-Identifier: Apache-2.0 + +apiVersion: v1 +kind: Pod +metadata: + name: machine-id-reader + namespace: default +spec: + containers: + - name: app + image: alpine:3 + volumeMounts: + - name: machine-id + mountPath: /host/etc/machine-id + readOnly: true + volumes: + - name: machine-id + hostPath: + path: /etc/machine-id diff --git a/gatekeeper-config/tests/suite.yaml b/gatekeeper-config/tests/suite.yaml index 1c2277872..e4f53cf68 100644 --- a/gatekeeper-config/tests/suite.yaml +++ b/gatekeeper-config/tests/suite.yaml @@ -155,3 +155,110 @@ tests: assertions: - violations: 1 message: '^\{"owned_by":"none"\} >> pod is not owned by a managing construct$' + +- name: forbidden-clusterwide-objects + template: rendered/gatekeeper-config/templates/constrainttemplate-forbidden-clusterwide-objects.yaml + constraint: rendered/gatekeeper-config/templates/constraint-forbidden-clusterwide-objects.yaml + cases: + - name: mwc-allowed + object: fixtures/forbidden-clusterwide-objects/mwc-allowed.yaml + assertions: + - violations: 0 + - name: mwc-disallowed + object: fixtures/forbidden-clusterwide-objects/mwc-disallowed.yaml + assertions: + - violations: 1 + message: '^\{"owned_by":"none"\} >> webhook "rogue\.example\.com" does not match the configured allowlist$' + - name: vwc-allowed + object: fixtures/forbidden-clusterwide-objects/vwc-allowed.yaml + assertions: + - violations: 0 + +- name: images-from-approved-registries + template: rendered/gatekeeper-config/templates/constrainttemplate-images-from-approved-registries.yaml + constraint: rendered/gatekeeper-config/templates/constraint-images-from-approved-registries.yaml + cases: + - name: pod-approved + object: fixtures/images-from-approved-registries/pod-approved.yaml + assertions: + - violations: 0 + - name: pod-unapproved + object: fixtures/images-from-approved-registries/pod-unapproved.yaml + assertions: + - violations: 1 + message: '^\{"owned_by":"none"\} >> container "app" uses an image from an unapproved registry: docker\.io/library/nginx:1\.27$' + - name: pod-mixed-containers + object: fixtures/images-from-approved-registries/pod-mixed-containers.yaml + assertions: + - violations: 1 + message: '^\{"owned_by":"none"\} >> container "bad" uses an image from an unapproved registry: docker\.io/library/nginx:1\.27$' + +- name: pci-forbidden-images + template: rendered/gatekeeper-config/templates/constrainttemplate-pci-forbidden-images.yaml + constraint: rendered/gatekeeper-config/templates/constraint-pci-forbidden-images.yaml + cases: + - name: pod-busybox-flagged + object: fixtures/pci-forbidden-images/pod-busybox-flagged.yaml + assertions: + - violations: 1 + message: '^\{"owned_by":"none"\} >> container "shell" uses forbidden image: busybox:1\.36$' + - name: pod-init-busybox-flagged + object: fixtures/pci-forbidden-images/pod-init-busybox-flagged.yaml + assertions: + - violations: 1 + message: '^\{"owned_by":"none"\} >> container "init" uses forbidden image: registry\.k8s\.io/busybox:1\.36$' + - name: pod-allowed + object: fixtures/pci-forbidden-images/pod-allowed.yaml + assertions: + - violations: 0 + +- name: pod-required-labels + template: rendered/gatekeeper-config/templates/constrainttemplate-pod-required-labels.yaml + constraint: rendered/gatekeeper-config/templates/constraint-pod-required-labels.yaml + cases: + - name: pod-with-label + object: fixtures/pod-required-labels/pod-with-label.yaml + assertions: + - violations: 0 + - name: pod-missing-label + object: fixtures/pod-required-labels/pod-missing-label.yaml + assertions: + - violations: 1 + message: '^\{"owned_by":"none"\} >> pod is missing required label: greenhouse\.sap/owned-by$' + - name: deployment-template-missing-required-label + object: fixtures/pod-required-labels/deployment-template-missing.yaml + assertions: + - violations: 1 + message: '^\{"owned_by":"my-team"\} >> pod is missing required label: greenhouse\.sap/owned-by$' + +- name: pod-security-v2 + template: rendered/gatekeeper-config/templates/constrainttemplate-pod-security-v2.yaml + constraint: rendered/gatekeeper-config/templates/constraint-pod-security-v2.yaml + cases: + - name: pod-host-network-violation + object: fixtures/pod-security-v2/pod-host-network-violation.yaml + assertions: + - violations: 1 + message: '^\{"owned_by":"none"\} >> pod is not allowed to set spec\.hostNetwork = true$' + - name: pod-host-network-allowlisted + object: fixtures/pod-security-v2/pod-host-network-allowlisted.yaml + assertions: + - violations: 0 + - name: pod-host-network-allowlisted-registry-port + object: fixtures/pod-security-v2/pod-host-network-allowlisted-registry-port.yaml + assertions: + - violations: 0 + - name: pod-privileged-violation + object: fixtures/pod-security-v2/pod-privileged-violation.yaml + assertions: + - violations: 1 + message: '^\{"owned_by":"none"\} >> pod is not allowed to set spec\.containers\["app"\]\.securityContext\.privileged = true \(image = "alpine"\)$' + - name: pod-readonly-machine-id-pass + object: fixtures/pod-security-v2/pod-readonly-machine-id-pass.yaml + assertions: + - violations: 0 + - name: pod-capability-violation + object: fixtures/pod-security-v2/pod-capability-violation.yaml + assertions: + - violations: 1 + message: '^\{"owned_by":"none"\} >> pod is not allowed to set spec\.containers\["app"\]\.securityContext\.capabilities\.add = \["NET_ADMIN"\] \(image = "alpine"\)$' diff --git a/gatekeeper-config/tests/values-test.yaml b/gatekeeper-config/tests/values-test.yaml index 0c87770a7..b01ed7a51 100644 --- a/gatekeeper-config/tests/values-test.yaml +++ b/gatekeeper-config/tests/values-test.yaml @@ -11,5 +11,33 @@ policies: - namespace: default namePattern: "recycler-for-.*" + forbiddenClusterwideObjects: + enabled: true + allowedWebhooks: + - "allowed.example.com" + imagesFromApprovedRegistries: + enabled: true + allowedRegistries: + - "registry.k8s.io/" + - "ghcr.io/" + pciForbiddenImages: + enabled: true + patterns: + - "(^|/)busybox(:|@|$)" + podRequiredLabels: + enabled: true + requiredLabels: + - "greenhouse.sap/owned-by" + podSecurityV2: + enabled: true + allowlist: + - matchNamespace: kube-system + matchRepository: k8s/system-pod + mayUseHostNetwork: true + mayBePrivileged: true + mayUseCapabilities: ["NET_ADMIN"] + mayReadHostPathVolumes: [] + mayWriteHostPathVolumes: [] + tests: enabled: true