diff --git a/artifacthub/library/pod-security-policy/seccomp/1.1.0/artifacthub-pkg.yml b/artifacthub/library/pod-security-policy/seccomp/1.1.0/artifacthub-pkg.yml
new file mode 100644
index 000000000..318f91b02
--- /dev/null
+++ b/artifacthub/library/pod-security-policy/seccomp/1.1.0/artifacthub-pkg.yml
@@ -0,0 +1,22 @@
+version: 1.1.0
+name: k8spspseccomp
+displayName: Seccomp
+createdAt: "2024-06-03T13:44:11Z"
+description: Controls the seccomp profile used by containers. Corresponds to the `seccomp.security.alpha.kubernetes.io/allowedProfileNames` annotation on a PodSecurityPolicy. For more information, see https://kubernetes.io/docs/concepts/policy/pod-security-policy/#seccomp
+digest: 483941dab0df9cb51189b131e309bf927928b69b46ed51986d2f51e30fe758af
+license: Apache-2.0
+homeURL: https://open-policy-agent.github.io/gatekeeper-library/website/seccomp
+keywords:
+ - gatekeeper
+ - open-policy-agent
+ - policies
+readme: |-
+ # Seccomp
+ Controls the seccomp profile used by containers. Corresponds to the `seccomp.security.alpha.kubernetes.io/allowedProfileNames` annotation on a PodSecurityPolicy. For more information, see https://kubernetes.io/docs/concepts/policy/pod-security-policy/#seccomp
+install: |-
+ ### Usage
+ ```shell
+ kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper-library/master/artifacthub/library/pod-security-policy/seccomp/1.1.0/template.yaml
+ ```
+provider:
+ name: Gatekeeper Library
diff --git a/artifacthub/library/pod-security-policy/seccomp/1.1.0/kustomization.yaml b/artifacthub/library/pod-security-policy/seccomp/1.1.0/kustomization.yaml
new file mode 100644
index 000000000..7d70d11b7
--- /dev/null
+++ b/artifacthub/library/pod-security-policy/seccomp/1.1.0/kustomization.yaml
@@ -0,0 +1,2 @@
+resources:
+ - template.yaml
diff --git a/artifacthub/library/pod-security-policy/seccomp/1.1.0/samples/psp-seccomp/constraint.yaml b/artifacthub/library/pod-security-policy/seccomp/1.1.0/samples/psp-seccomp/constraint.yaml
new file mode 100644
index 000000000..0d28be8d9
--- /dev/null
+++ b/artifacthub/library/pod-security-policy/seccomp/1.1.0/samples/psp-seccomp/constraint.yaml
@@ -0,0 +1,15 @@
+apiVersion: constraints.gatekeeper.sh/v1beta1
+kind: K8sPSPSeccomp
+metadata:
+ name: psp-seccomp
+spec:
+ match:
+ kinds:
+ - apiGroups: [""]
+ kinds: ["Pod"]
+ parameters:
+ exemptImages:
+ - nginx-exempt
+ allowedProfiles:
+ - runtime/default
+ - localhost/profile.json
\ No newline at end of file
diff --git a/artifacthub/library/pod-security-policy/seccomp/1.1.0/samples/psp-seccomp/disallowed_ephemeral.yaml b/artifacthub/library/pod-security-policy/seccomp/1.1.0/samples/psp-seccomp/disallowed_ephemeral.yaml
new file mode 100644
index 000000000..1555d700e
--- /dev/null
+++ b/artifacthub/library/pod-security-policy/seccomp/1.1.0/samples/psp-seccomp/disallowed_ephemeral.yaml
@@ -0,0 +1,12 @@
+apiVersion: v1
+kind: Pod
+metadata:
+ name: nginx-seccomp-disallowed
+ annotations:
+ container.seccomp.security.alpha.kubernetes.io/nginx: unconfined
+ labels:
+ app: nginx-seccomp
+spec:
+ ephemeralContainers:
+ - name: nginx
+ image: nginx
diff --git a/artifacthub/library/pod-security-policy/seccomp/1.1.0/samples/psp-seccomp/example_allowed.yaml b/artifacthub/library/pod-security-policy/seccomp/1.1.0/samples/psp-seccomp/example_allowed.yaml
new file mode 100644
index 000000000..2ff43d307
--- /dev/null
+++ b/artifacthub/library/pod-security-policy/seccomp/1.1.0/samples/psp-seccomp/example_allowed.yaml
@@ -0,0 +1,12 @@
+apiVersion: v1
+kind: Pod
+metadata:
+ name: nginx-seccomp-allowed
+ annotations:
+ container.seccomp.security.alpha.kubernetes.io/nginx: runtime/default
+ labels:
+ app: nginx-seccomp
+spec:
+ containers:
+ - name: nginx
+ image: nginx
diff --git a/artifacthub/library/pod-security-policy/seccomp/1.1.0/samples/psp-seccomp/example_allowed2.yaml b/artifacthub/library/pod-security-policy/seccomp/1.1.0/samples/psp-seccomp/example_allowed2.yaml
new file mode 100644
index 000000000..f8766e774
--- /dev/null
+++ b/artifacthub/library/pod-security-policy/seccomp/1.1.0/samples/psp-seccomp/example_allowed2.yaml
@@ -0,0 +1,12 @@
+apiVersion: v1
+kind: Pod
+metadata:
+ name: nginx-seccomp-allowed2
+ annotations:
+ seccomp.security.alpha.kubernetes.io/pod: runtime/default
+ labels:
+ app: nginx-seccomp
+spec:
+ containers:
+ - name: nginx
+ image: nginx
diff --git a/artifacthub/library/pod-security-policy/seccomp/1.1.0/samples/psp-seccomp/example_allowed_exempt_image.yaml b/artifacthub/library/pod-security-policy/seccomp/1.1.0/samples/psp-seccomp/example_allowed_exempt_image.yaml
new file mode 100644
index 000000000..d5f42987d
--- /dev/null
+++ b/artifacthub/library/pod-security-policy/seccomp/1.1.0/samples/psp-seccomp/example_allowed_exempt_image.yaml
@@ -0,0 +1,13 @@
+apiVersion: v1
+kind: Pod
+metadata:
+ name: nginx-seccomp-disallowed
+ labels:
+ app: nginx-seccomp
+spec:
+ containers:
+ - name: nginx
+ image: nginx-exempt
+ securityContext:
+ seccompProfile:
+ type: Unconfined
diff --git a/artifacthub/library/pod-security-policy/seccomp/1.1.0/samples/psp-seccomp/example_allowed_localhost.yaml b/artifacthub/library/pod-security-policy/seccomp/1.1.0/samples/psp-seccomp/example_allowed_localhost.yaml
new file mode 100644
index 000000000..856f3217b
--- /dev/null
+++ b/artifacthub/library/pod-security-policy/seccomp/1.1.0/samples/psp-seccomp/example_allowed_localhost.yaml
@@ -0,0 +1,14 @@
+apiVersion: v1
+kind: Pod
+metadata:
+ name: nginx-seccomp-allowed-localhost
+ labels:
+ app: nginx-seccomp
+spec:
+ containers:
+ - name: nginx
+ image: nginx
+ securityContext:
+ seccompProfile:
+ type: Localhost
+ localhostProfile: profile.json
diff --git a/artifacthub/library/pod-security-policy/seccomp/1.1.0/samples/psp-seccomp/example_disallowed.yaml b/artifacthub/library/pod-security-policy/seccomp/1.1.0/samples/psp-seccomp/example_disallowed.yaml
new file mode 100644
index 000000000..8e94ca7e6
--- /dev/null
+++ b/artifacthub/library/pod-security-policy/seccomp/1.1.0/samples/psp-seccomp/example_disallowed.yaml
@@ -0,0 +1,12 @@
+apiVersion: v1
+kind: Pod
+metadata:
+ name: nginx-seccomp-disallowed
+ annotations:
+ container.seccomp.security.alpha.kubernetes.io/nginx: unconfined
+ labels:
+ app: nginx-seccomp
+spec:
+ containers:
+ - name: nginx
+ image: nginx
diff --git a/artifacthub/library/pod-security-policy/seccomp/1.1.0/samples/psp-seccomp/example_disallowed2.yaml b/artifacthub/library/pod-security-policy/seccomp/1.1.0/samples/psp-seccomp/example_disallowed2.yaml
new file mode 100644
index 000000000..6008d8f72
--- /dev/null
+++ b/artifacthub/library/pod-security-policy/seccomp/1.1.0/samples/psp-seccomp/example_disallowed2.yaml
@@ -0,0 +1,12 @@
+apiVersion: v1
+kind: Pod
+metadata:
+ name: nginx-seccomp-disallowed2
+ annotations:
+ seccomp.security.alpha.kubernetes.io/pod: unconfined
+ labels:
+ app: nginx-seccomp
+spec:
+ containers:
+ - name: nginx
+ image: nginx
diff --git a/artifacthub/library/pod-security-policy/seccomp/1.1.0/samples/psp-seccomp/example_disallowed_localhost.yaml b/artifacthub/library/pod-security-policy/seccomp/1.1.0/samples/psp-seccomp/example_disallowed_localhost.yaml
new file mode 100644
index 000000000..c566f7ca4
--- /dev/null
+++ b/artifacthub/library/pod-security-policy/seccomp/1.1.0/samples/psp-seccomp/example_disallowed_localhost.yaml
@@ -0,0 +1,14 @@
+apiVersion: v1
+kind: Pod
+metadata:
+ name: nginx-seccomp-disallowed-localhost
+ labels:
+ app: nginx-seccomp
+spec:
+ containers:
+ - name: nginx
+ image: nginx
+ securityContext:
+ seccompProfile:
+ type: Localhost
+ localhostProfile: profile.log
diff --git a/artifacthub/library/pod-security-policy/seccomp/1.1.0/suite.yaml b/artifacthub/library/pod-security-policy/seccomp/1.1.0/suite.yaml
new file mode 100644
index 000000000..0347d33fb
--- /dev/null
+++ b/artifacthub/library/pod-security-policy/seccomp/1.1.0/suite.yaml
@@ -0,0 +1,45 @@
+kind: Suite
+apiVersion: test.gatekeeper.sh/v1alpha1
+metadata:
+ name: seccomp
+tests:
+- name: default-seccomp-required
+ template: template.yaml
+ constraint: samples/psp-seccomp/constraint.yaml
+ cases:
+ - name: example-disallowed-global
+ object: samples/psp-seccomp/example_disallowed2.yaml
+ assertions:
+ - violations: 1
+ message: "Seccomp profile 'unconfined' is not allowed for container 'nginx'. Found at: annotation seccomp.security.alpha.kubernetes.io/pod"
+ - name: example-disallowed-container
+ object: samples/psp-seccomp/example_disallowed.yaml
+ assertions:
+ - violations: 1
+ message: "Seccomp profile 'unconfined' is not allowed for container 'nginx'. Found at: annotation container.seccomp.security.alpha.kubernetes.io/nginx"
+ - name: example-allowed-container
+ object: samples/psp-seccomp/example_allowed.yaml
+ assertions:
+ - violations: no
+ - name: example-allowed-global
+ object: samples/psp-seccomp/example_allowed2.yaml
+ assertions:
+ - violations: no
+ - name: disallowed-ephemeral
+ object: samples/psp-seccomp/disallowed_ephemeral.yaml
+ assertions:
+ - violations: 1
+ message: "Seccomp profile 'unconfined' is not allowed for container 'nginx'. Found at: annotation container.seccomp.security.alpha.kubernetes.io/nginx"
+ - name: example-allowed-container-exempt-image
+ object: samples/psp-seccomp/example_allowed_exempt_image.yaml
+ assertions:
+ - violations: no
+ - name: example-allowed-container-localhost-profile
+ object: samples/psp-seccomp/example_allowed_localhost.yaml
+ assertions:
+ - violations: no
+ - name: example-disallowed-container-localhost-profile
+ object: samples/psp-seccomp/example_disallowed_localhost.yaml
+ assertions:
+ - violations: 1
+ message: "Seccomp profile 'localhost/profile.log' is not allowed for container 'nginx'. Found at: container securityContext."
\ No newline at end of file
diff --git a/artifacthub/library/pod-security-policy/seccomp/1.1.0/template.yaml b/artifacthub/library/pod-security-policy/seccomp/1.1.0/template.yaml
new file mode 100644
index 000000000..d1b676d97
--- /dev/null
+++ b/artifacthub/library/pod-security-policy/seccomp/1.1.0/template.yaml
@@ -0,0 +1,398 @@
+apiVersion: templates.gatekeeper.sh/v1
+kind: ConstraintTemplate
+metadata:
+ name: k8spspseccomp
+ annotations:
+ metadata.gatekeeper.sh/title: "Seccomp"
+ metadata.gatekeeper.sh/version: 1.1.0
+ description: >-
+ Controls the seccomp profile used by containers. Corresponds to the
+ `seccomp.security.alpha.kubernetes.io/allowedProfileNames` annotation on
+ a PodSecurityPolicy. For more information, see
+ https://kubernetes.io/docs/concepts/policy/pod-security-policy/#seccomp
+spec:
+ crd:
+ spec:
+ names:
+ kind: K8sPSPSeccomp
+ validation:
+ # Schema for the `parameters` field
+ openAPIV3Schema:
+ type: object
+ description: >-
+ Controls the seccomp profile used by containers. Corresponds to the
+ `seccomp.security.alpha.kubernetes.io/allowedProfileNames` annotation on
+ a PodSecurityPolicy. For more information, see
+ https://kubernetes.io/docs/concepts/policy/pod-security-policy/#seccomp
+ properties:
+ exemptImages:
+ description: >-
+ Any container that uses an image that matches an entry in this list will be excluded
+ from enforcement. Prefix-matching can be signified with `*`. For example: `my-image-*`.
+
+ It is recommended that users use the fully-qualified Docker image name (e.g. start with a domain name)
+ in order to avoid unexpectedly exempting images from an untrusted repository.
+ type: array
+ items:
+ type: string
+ allowedProfiles:
+ type: array
+ description: >-
+ An array of allowed profile values for seccomp on Pods/Containers.
+
+ Can use the annotation naming scheme: `runtime/default`, `docker/default`, `unconfined` and/or
+ `localhost/some-profile.json`. The item `localhost/*` will allow any localhost based profile.
+
+ Can also use the securityContext naming scheme: `RuntimeDefault`, `Unconfined`
+ and/or `Localhost`. For securityContext `Localhost`, use the parameter `allowedLocalhostProfiles`
+ to list the allowed profile JSON files.
+
+ The policy code will translate between the two schemes so it is not necessary to use both.
+
+ Putting a `*` in this array allows all Profiles to be used.
+
+ This field is required since with an empty list this policy will block all workloads.
+ items:
+ type: string
+ allowedLocalhostFiles:
+ type: array
+ description: >-
+ When using securityContext naming scheme for seccomp and including `Localhost` this array holds
+ the allowed profile JSON files.
+
+ Putting a `*` in this array will allows all JSON files to be used.
+
+ This field is required to allow `Localhost` in securityContext as with an empty list it will block.
+ items:
+ type: string
+ targets:
+ - target: admission.k8s.gatekeeper.sh
+ code:
+ - engine: K8sNativeValidation
+ source:
+ variables:
+ - name: containers
+ expression: 'has(variables.anyObject.spec.containers) ? variables.anyObject.spec.containers : []'
+ - name: initContainers
+ expression: 'has(variables.anyObject.spec.initContainers) ? variables.anyObject.spec.initContainers : []'
+ - name: ephemeralContainers
+ expression: 'has(variables.anyObject.spec.ephemeralContainers) ? variables.anyObject.spec.ephemeralContainers : []'
+ - name: allowAllProfiles
+ expression: |
+ has(variables.params.allowedProfiles) && variables.params.allowedProfiles.exists(profile, profile == "*")
+ - name: exemptImagePrefixes
+ expression: |
+ !has(variables.params.exemptImages) ? [] :
+ variables.params.exemptImages.filter(image, image.endsWith("*")).map(image, string(image).replace("*", ""))
+ - name: exemptImageExplicit
+ expression: |
+ !has(variables.params.exemptImages) ? [] :
+ variables.params.exemptImages.filter(image, !image.endsWith("*"))
+ - name: exemptImages
+ expression: |
+ (variables.containers + variables.initContainers + variables.ephemeralContainers).filter(container,
+ container.image in variables.exemptImageExplicit ||
+ variables.exemptImagePrefixes.exists(exemption, string(container.image).startsWith(exemption))).map(container, container.image)
+ - name: unverifiedContainers
+ expression: |
+ (variables.containers + variables.initContainers + variables.ephemeralContainers).filter(container,
+ !variables.allowAllProfiles &&
+ !(container.image in variables.exemptImages))
+ - name: inputAllowedProfiles
+ expression: |
+ !has(variables.params.allowedProfiles) ? [] : variables.params.allowedProfiles
+ - name: allowedLocalhostFiles
+ expression: |
+ has(variables.params.allowedLocalhostFiles) ? variables.params.allowedLocalhostFiles : []
+ - name: allowedProfilesTranslation
+ expression: |
+ (variables.inputAllowedProfiles.filter(profile,
+ profile != "Localhost").map(profile, profile == "Unconfined" ? "unconfined" : profile)) +
+ (variables.inputAllowedProfiles.exists(profile, profile == "RuntimeDefault") ? ["runtime/default", "docker/default"] : [])
+ - name: allowSecurityContextLocalhost
+ expression: |
+ variables.inputAllowedProfiles.exists(profile, profile == "Localhost")
+ - name: derivedAllowedLocalhostFiles
+ expression: |
+ variables.allowSecurityContextLocalhost ? variables.params.allowedLocalhostFiles.map(file, "localhost/" + file) : []
+ - name: localhostWildcardAllowed
+ expression: |
+ variables.inputAllowedProfiles.exists(profile, profile == "localhost/*") || variables.derivedAllowedLocalhostFiles.exists(profile, profile == "localhost/*")
+ - name: allowedProfiles
+ expression: |
+ (variables.allowedProfilesTranslation + variables.derivedAllowedLocalhostFiles)
+ - name: hasPodSeccomp
+ expression: |
+ has(variables.anyObject.spec.securityContext) && has(variables.anyObject.spec.securityContext.seccompProfile)
+ - name: hasPodAnnotations
+ expression: |
+ has(variables.anyObject.metadata.annotations) && ("seccomp.security.alpha.kubernetes.io/pod" in variables.anyObject.metadata.annotations)
+ - name: podAnnotationsProfiles
+ expression: |
+ variables.unverifiedContainers.filter(container,
+ !(has(container.securityContext) && has(container.securityContext.seccompProfile)) &&
+ !(has(variables.anyObject.metadata.annotations) && (("container.seccomp.security.alpha.kubernetes.io/" + container.name) in variables.anyObject.metadata.annotations)) &&
+ !variables.hasPodSeccomp &&
+ variables.hasPodAnnotations
+ ).map(container, {
+ "container" : container.name,
+ "profile" : variables.anyObject.metadata.annotations["seccomp.security.alpha.kubernetes.io/pod"],
+ "file" : dyn(""),
+ "location" : dyn("annotation seccomp.security.alpha.kubernetes.io/pod"),
+ })
+ - name: containerAnnotationsProfiles
+ expression: |
+ variables.unverifiedContainers.filter(container,
+ !(has(container.securityContext) && has(container.securityContext.seccompProfile)) &&
+ !variables.hasPodSeccomp &&
+ has(variables.anyObject.metadata.annotations) && (("container.seccomp.security.alpha.kubernetes.io/" + container.name) in variables.anyObject.metadata.annotations)
+ ).map(container, {
+ "container" : container.name,
+ "profile" : variables.anyObject.metadata.annotations["container.seccomp.security.alpha.kubernetes.io/" + container.name],
+ "file" : dyn(""),
+ "location" : dyn("annotation container.seccomp.security.alpha.kubernetes.io/" + container.name),
+ })
+ - name: podLocalHostProfile
+ expression: |
+ variables.hasPodSeccomp && has(variables.anyObject.spec.securityContext.seccompProfile.localhostProfile) ? variables.anyObject.spec.securityContext.seccompProfile.localhostProfile : ""
+ - name: canonicalPodSecurityContextProfile
+ expression: |
+ has(variables.hasPodSeccomp) && has(variables.anyObject.spec.securityContext.seccompProfile.type) ?
+ (variables.anyObject.spec.securityContext.seccompProfile.type == "RuntimeDefault" ? (
+ variables.allowedProfiles.exists(profile, profile == "runtime/default") ? "runtime/default" : variables.allowedProfiles.exists(profile, profile == "docker/default") ? "docker/default" : "runtime/default") :
+ variables.anyObject.spec.securityContext.seccompProfile.type == "Unconfined" ? "unconfined" : variables.anyObject.spec.securityContext.seccompProfile.type == "Localhost" ? "localhost/" + variables.podLocalHostProfile : "")
+ : ""
+ - name: podSecurityContextProfiles
+ expression: |
+ variables.unverifiedContainers.filter(container,
+ !(has(container.securityContext) && has(container.securityContext.seccompProfile)) &&
+ variables.hasPodSeccomp
+ ).map(container, {
+ "container" : container.name,
+ "profile" : dyn(variables.canonicalPodSecurityContextProfile),
+ "file" : variables.podLocalHostProfile,
+ "location" : dyn("pod securityContext"),
+ })
+ - name: containerSecurityContextProfiles
+ expression: |
+ variables.unverifiedContainers.filter(container,
+ has(container.securityContext) && has(container.securityContext.seccompProfile)
+ ).map(container, {
+ "container" : container.name,
+ "profile" : dyn(has(container.securityContext.seccompProfile.type) ? (container.securityContext.seccompProfile.type == "RuntimeDefault" ? (
+ variables.allowedProfiles.exists(profile, profile == "runtime/default") ? "runtime/default" : variables.allowedProfiles.exists(profile, profile == "docker/default") ? "docker/default" : "runtime/default") :
+ container.securityContext.seccompProfile.type == "Unconfined" ? "unconfined" : container.securityContext.seccompProfile.type == "Localhost" ? "localhost/" + container.securityContext.seccompProfile.localhostProfile : "")
+ : ""),
+ "file" : has(container.securityContext.seccompProfile.localhostProfile) ? container.securityContext.seccompProfile.localhostProfile : dyn(""),
+ "location" : dyn("container securityContext"),
+ })
+ - name: containerProfilesMissing
+ expression: |
+ variables.unverifiedContainers.filter(container,
+ !(has(container.securityContext) && has(container.securityContext.seccompProfile)) &&
+ !(has(variables.anyObject.metadata.annotations) && (("container.seccomp.security.alpha.kubernetes.io/" + container.name) in variables.anyObject.metadata.annotations)) &&
+ !variables.hasPodSeccomp &&
+ !variables.hasPodAnnotations
+ ).map(container, {
+ "container" : container.name,
+ "profile" : dyn("not configured"),
+ "file" : dyn(""),
+ "location" : dyn("no explicit profile found"),
+ })
+ - name: allContainerProfiles
+ expression: |
+ variables.podAnnotationsProfiles + variables.containerAnnotationsProfiles + variables.podSecurityContextProfiles + variables.containerSecurityContextProfiles + variables.containerProfilesMissing
+ - name: badContainerProfiles
+ expression: |
+ variables.allContainerProfiles.filter(badContainerProfile,
+ !((badContainerProfile.profile in variables.allowedProfiles) || (badContainerProfile.profile.startsWith("localhost/") && variables.localhostWildcardAllowed))
+ ).map(badProfile, "Seccomp profile '" + badProfile.profile + "' is not allowed for container '" + badProfile.container + "'. Found at: " + badProfile.location + ". Allowed profiles: " + variables.allowedProfiles.join(", "))
+ validations:
+ - expression: 'size(variables.badContainerProfiles) == 0'
+ messageExpression: |
+ variables.badContainerProfiles.join(", ")
+ - engine: Rego
+ source:
+ rego: |
+ package k8spspseccomp
+
+ import data.lib.exempt_container.is_exempt
+
+ container_annotation_key_prefix = "container.seccomp.security.alpha.kubernetes.io/"
+
+ pod_annotation_key = "seccomp.security.alpha.kubernetes.io/pod"
+
+ violation[{"msg": msg}] {
+ not input_wildcard_allowed_profiles
+ allowed_profiles := get_allowed_profiles
+ container := input_containers[name]
+ not is_exempt(container)
+ result := get_profile(container)
+ not allowed_profile(result.profile, result.file, allowed_profiles)
+ msg := get_message(result.profile, result.file, name, result.location, allowed_profiles)
+ }
+
+ get_message(profile, _, name, location, allowed_profiles) = message {
+ message := sprintf("Seccomp profile '%v' is not allowed for container '%v'. Found at: %v. Allowed profiles: %v", [profile, name, location, allowed_profiles])
+ }
+
+ input_wildcard_allowed_profiles {
+ input.parameters.allowedProfiles[_] == "*"
+ }
+
+ input_wildcard_allowed_files {
+ input.parameters.allowedLocalhostFiles[_] == "*"
+ }
+
+ input_wildcard_allowed_files {
+ "localhost/*" == input.parameters.allowedProfiles[_]
+ }
+
+ # Simple allowed Profiles
+ allowed_profile(profile, _, allowed) {
+ not startswith(profile, "localhost/")
+ profile == allowed[_]
+ }
+
+ # annotation localhost with wildcard
+ allowed_profile(profile, _, allowed) {
+ "localhost/*" == allowed[_]
+ startswith(profile, "localhost/")
+ }
+
+ # annotation localhost without wildcard
+ allowed_profile(profile, _, allowed) {
+ startswith(profile, "localhost/")
+ profile == allowed[_]
+ }
+
+ # The profiles explicitly in the list
+ get_allowed_profiles[allowed] {
+ allowed := input.parameters.allowedProfiles[_]
+ }
+
+ # Seccomp Localhost to annotation translation
+ get_allowed_profiles[allowed] {
+ profile := input.parameters.allowedProfiles[_]
+ not contains(profile, "/")
+ file := object.get(input.parameters, "allowedLocalhostFiles", [])[_]
+ allowed := canonicalize_seccomp_profile({"type": profile, "localhostProfile": file}, "")[_]
+ }
+
+ # Container profile as defined in pod annotation
+ get_profile(container) = {"profile": profile, "file": "", "location": location} {
+ not has_securitycontext_container(container)
+ not has_annotation(get_container_annotation_key(container.name))
+ not has_securitycontext_pod
+ profile := input.review.object.metadata.annotations[pod_annotation_key]
+ location := sprintf("annotation %v", [pod_annotation_key])
+ }
+
+ # Container profile as defined in container annotation
+ get_profile(container) = {"profile": profile, "file": "", "location": location} {
+ not has_securitycontext_container(container)
+ not has_securitycontext_pod
+ container_annotation := get_container_annotation_key(container.name)
+ has_annotation(container_annotation)
+ profile := input.review.object.metadata.annotations[container_annotation]
+ location := sprintf("annotation %v", [container_annotation])
+ }
+
+ # Container profile as defined in pods securityContext
+ get_profile(container) = {"profile": profile, "file": file, "location": location} {
+ not has_securitycontext_container(container)
+ profile := canonicalize_seccomp_profile(input.review.object.spec.securityContext.seccompProfile, canonicalize_runtime_default_profile)[_]
+ file := object.get(input.review.object.spec.securityContext.seccompProfile, "localhostProfile", "")
+ location := "pod securityContext"
+ }
+
+ # Container profile as defined in containers securityContext
+ get_profile(container) = {"profile": profile, "file": file, "location": location} {
+ has_securitycontext_container(container)
+ profile := canonicalize_seccomp_profile(container.securityContext.seccompProfile, canonicalize_runtime_default_profile)[_]
+ file := object.get(container.securityContext.seccompProfile, "localhostProfile", "")
+ location := "container securityContext"
+ }
+
+ # Container profile missing
+ get_profile(container) = {"profile": "not configured", "file": "", "location": "no explicit profile found"} {
+ not has_securitycontext_container(container)
+ not has_securitycontext_pod
+ not has_annotation(get_container_annotation_key(container.name))
+ not has_annotation(pod_annotation_key)
+ }
+
+ has_annotation(annotation) {
+ input.review.object.metadata.annotations[annotation]
+ }
+
+ has_securitycontext_pod {
+ input.review.object.spec.securityContext.seccompProfile
+ }
+
+ has_securitycontext_container(container) {
+ container.securityContext.seccompProfile
+ }
+
+ get_container_annotation_key(name) = annotation {
+ annotation := concat("", [container_annotation_key_prefix, name])
+ }
+
+ input_containers[container.name] = container {
+ container := input.review.object.spec.containers[_]
+ }
+
+ input_containers[container.name] = container {
+ container := input.review.object.spec.initContainers[_]
+ }
+
+ input_containers[container.name] = container {
+ container := input.review.object.spec.ephemeralContainers[_]
+ }
+
+ canonicalize_runtime_default_profile() = out {
+ "runtime/default" == input.parameters.allowedProfiles[_]
+ out := "runtime/default"
+ } else = out {
+ "docker/default" == input.parameters.allowedProfiles[_]
+ out := "docker/default"
+ } else = out {
+ out := "runtime/default"
+ }
+
+ canonicalize_seccomp_profile(profile, def) = out {
+ profile.type == "RuntimeDefault"
+ def == ""
+ out := ["runtime/default", "docker/default"]
+ } else = out {
+ profile.type == "RuntimeDefault"
+ def != ""
+ out := [def]
+ } else = out {
+ profile.type == "Localhost"
+ out := [sprintf("localhost/%s", [profile.localhostProfile])]
+ } else = out {
+ profile.type == "Unconfined"
+ out := ["unconfined"]
+ }
+ libs:
+ - |
+ package lib.exempt_container
+
+ is_exempt(container) {
+ exempt_images := object.get(object.get(input, "parameters", {}), "exemptImages", [])
+ img := container.image
+ exemption := exempt_images[_]
+ _matches_exemption(img, exemption)
+ }
+
+ _matches_exemption(img, exemption) {
+ not endswith(exemption, "*")
+ exemption == img
+ }
+
+ _matches_exemption(img, exemption) {
+ endswith(exemption, "*")
+ prefix := trim_suffix(exemption, "*")
+ startswith(img, prefix)
+ }
diff --git a/artifacthub/library/pod-security-policy/seccompv2/1.0.0/artifacthub-pkg.yml b/artifacthub/library/pod-security-policy/seccompv2/1.0.0/artifacthub-pkg.yml
new file mode 100644
index 000000000..f573badaf
--- /dev/null
+++ b/artifacthub/library/pod-security-policy/seccompv2/1.0.0/artifacthub-pkg.yml
@@ -0,0 +1,22 @@
+version: 1.0.0
+name: k8spspseccompv2
+displayName: Seccomp V2
+createdAt: "2024-09-05T01:36:31Z"
+description: Controls the seccomp profile used by containers. Corresponds to the `securityContext.seccompProfile` field.
+digest: c6dbfe96ca7a4be156bee4bf42aef07a1424127fd9dc7d222b4e934c1919811f
+license: Apache-2.0
+homeURL: https://open-policy-agent.github.io/gatekeeper-library/website/seccompv2
+keywords:
+ - gatekeeper
+ - open-policy-agent
+ - policies
+readme: |-
+ # Seccomp V2
+ Controls the seccomp profile used by containers. Corresponds to the `securityContext.seccompProfile` field.
+install: |-
+ ### Usage
+ ```shell
+ kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper-library/master/artifacthub/library/pod-security-policy/seccompv2/1.0.0/template.yaml
+ ```
+provider:
+ name: Gatekeeper Library
diff --git a/artifacthub/library/pod-security-policy/seccompv2/1.0.0/kustomization.yaml b/artifacthub/library/pod-security-policy/seccompv2/1.0.0/kustomization.yaml
new file mode 100644
index 000000000..7d70d11b7
--- /dev/null
+++ b/artifacthub/library/pod-security-policy/seccompv2/1.0.0/kustomization.yaml
@@ -0,0 +1,2 @@
+resources:
+ - template.yaml
diff --git a/artifacthub/library/pod-security-policy/seccompv2/1.0.0/samples/psp-seccomp/constraint.yaml b/artifacthub/library/pod-security-policy/seccompv2/1.0.0/samples/psp-seccomp/constraint.yaml
new file mode 100644
index 000000000..a85141492
--- /dev/null
+++ b/artifacthub/library/pod-security-policy/seccompv2/1.0.0/samples/psp-seccomp/constraint.yaml
@@ -0,0 +1,17 @@
+apiVersion: constraints.gatekeeper.sh/v1beta1
+kind: K8sPSPSeccompV2
+metadata:
+ name: psp-seccomp
+spec:
+ match:
+ kinds:
+ - apiGroups: [""]
+ kinds: ["Pod"]
+ parameters:
+ exemptImages:
+ - nginx-exempt
+ allowedProfiles:
+ - RuntimeDefault
+ - Localhost
+ allowedLocalhostFiles:
+ - "*"
diff --git a/artifacthub/library/pod-security-policy/seccompv2/1.0.0/samples/psp-seccomp/disallowed_ephemeral.yaml b/artifacthub/library/pod-security-policy/seccompv2/1.0.0/samples/psp-seccomp/disallowed_ephemeral.yaml
new file mode 100644
index 000000000..4b32243a9
--- /dev/null
+++ b/artifacthub/library/pod-security-policy/seccompv2/1.0.0/samples/psp-seccomp/disallowed_ephemeral.yaml
@@ -0,0 +1,10 @@
+apiVersion: v1
+kind: Pod
+metadata:
+ name: nginx-seccomp-disallowed
+ labels:
+ app: nginx-seccomp
+spec:
+ ephemeralContainers:
+ - name: nginx
+ image: nginx
diff --git a/artifacthub/library/pod-security-policy/seccompv2/1.0.0/samples/psp-seccomp/example_allowed.yaml b/artifacthub/library/pod-security-policy/seccompv2/1.0.0/samples/psp-seccomp/example_allowed.yaml
new file mode 100644
index 000000000..65dfc5be8
--- /dev/null
+++ b/artifacthub/library/pod-security-policy/seccompv2/1.0.0/samples/psp-seccomp/example_allowed.yaml
@@ -0,0 +1,13 @@
+apiVersion: v1
+kind: Pod
+metadata:
+ name: nginx-seccomp-allowed
+ labels:
+ app: nginx-seccomp
+spec:
+ containers:
+ - name: nginx
+ image: nginx
+ securityContext:
+ seccompProfile:
+ type: RuntimeDefault
diff --git a/artifacthub/library/pod-security-policy/seccompv2/1.0.0/samples/psp-seccomp/example_allowed_exempt_image.yaml b/artifacthub/library/pod-security-policy/seccompv2/1.0.0/samples/psp-seccomp/example_allowed_exempt_image.yaml
new file mode 100644
index 000000000..d5f42987d
--- /dev/null
+++ b/artifacthub/library/pod-security-policy/seccompv2/1.0.0/samples/psp-seccomp/example_allowed_exempt_image.yaml
@@ -0,0 +1,13 @@
+apiVersion: v1
+kind: Pod
+metadata:
+ name: nginx-seccomp-disallowed
+ labels:
+ app: nginx-seccomp
+spec:
+ containers:
+ - name: nginx
+ image: nginx-exempt
+ securityContext:
+ seccompProfile:
+ type: Unconfined
diff --git a/artifacthub/library/pod-security-policy/seccompv2/1.0.0/samples/psp-seccomp/example_allowed_localhost.yaml b/artifacthub/library/pod-security-policy/seccompv2/1.0.0/samples/psp-seccomp/example_allowed_localhost.yaml
new file mode 100644
index 000000000..856f3217b
--- /dev/null
+++ b/artifacthub/library/pod-security-policy/seccompv2/1.0.0/samples/psp-seccomp/example_allowed_localhost.yaml
@@ -0,0 +1,14 @@
+apiVersion: v1
+kind: Pod
+metadata:
+ name: nginx-seccomp-allowed-localhost
+ labels:
+ app: nginx-seccomp
+spec:
+ containers:
+ - name: nginx
+ image: nginx
+ securityContext:
+ seccompProfile:
+ type: Localhost
+ localhostProfile: profile.json
diff --git a/artifacthub/library/pod-security-policy/seccompv2/1.0.0/samples/psp-seccomp/example_disallowed.yaml b/artifacthub/library/pod-security-policy/seccompv2/1.0.0/samples/psp-seccomp/example_disallowed.yaml
new file mode 100644
index 000000000..40c115b86
--- /dev/null
+++ b/artifacthub/library/pod-security-policy/seccompv2/1.0.0/samples/psp-seccomp/example_disallowed.yaml
@@ -0,0 +1,13 @@
+apiVersion: v1
+kind: Pod
+metadata:
+ name: nginx-seccomp-disallowed
+ labels:
+ app: nginx-seccomp
+spec:
+ containers:
+ - name: nginx
+ image: nginx
+ securityContext:
+ seccompProfile:
+ type: Unconfined
diff --git a/artifacthub/library/pod-security-policy/seccompv2/1.0.0/samples/psp-seccomp/example_disallowed2.yaml b/artifacthub/library/pod-security-policy/seccompv2/1.0.0/samples/psp-seccomp/example_disallowed2.yaml
new file mode 100644
index 000000000..e08463f18
--- /dev/null
+++ b/artifacthub/library/pod-security-policy/seccompv2/1.0.0/samples/psp-seccomp/example_disallowed2.yaml
@@ -0,0 +1,13 @@
+apiVersion: v1
+kind: Pod
+metadata:
+ name: nginx-seccomp-disallowed2
+ labels:
+ app: nginx-seccomp
+spec:
+ securityContext:
+ seccompProfile:
+ type: Unconfined
+ containers:
+ - name: nginx
+ image: nginx
diff --git a/artifacthub/library/pod-security-policy/seccompv2/1.0.0/suite.yaml b/artifacthub/library/pod-security-policy/seccompv2/1.0.0/suite.yaml
new file mode 100644
index 000000000..f9daa264d
--- /dev/null
+++ b/artifacthub/library/pod-security-policy/seccompv2/1.0.0/suite.yaml
@@ -0,0 +1,36 @@
+kind: Suite
+apiVersion: test.gatekeeper.sh/v1alpha1
+metadata:
+ name: seccomp
+tests:
+- name: default-seccomp-required
+ template: template.yaml
+ constraint: samples/psp-seccomp/constraint.yaml
+ cases:
+ - name: example-disallowed-global
+ object: samples/psp-seccomp/example_disallowed2.yaml
+ assertions:
+ - violations: 1
+ message: "Seccomp profile 'Unconfined' is not allowed for container 'nginx'. Found at: pod securityContext."
+ - name: example-disallowed-container
+ object: samples/psp-seccomp/example_disallowed.yaml
+ assertions:
+ - violations: 1
+ message: "Seccomp profile 'Unconfined' is not allowed for container 'nginx'. Found at: container securityContext."
+ - name: example-allowed-container
+ object: samples/psp-seccomp/example_allowed.yaml
+ assertions:
+ - violations: no
+ - name: example-allowed-container
+ object: samples/psp-seccomp/example_allowed_localhost.yaml
+ assertions:
+ - violations: no
+ - name: example-allowed-container-exempt-image
+ object: samples/psp-seccomp/example_allowed_exempt_image.yaml
+ assertions:
+ - violations: no
+ - name: disallowed-ephemeral
+ object: samples/psp-seccomp/disallowed_ephemeral.yaml
+ assertions:
+ - violations: 1
+ message: "Seccomp profile 'not configured' is not allowed for container 'nginx'. Found at: no explicit profile found"
diff --git a/artifacthub/library/pod-security-policy/seccompv2/1.0.0/template.yaml b/artifacthub/library/pod-security-policy/seccompv2/1.0.0/template.yaml
new file mode 100644
index 000000000..ce0a53202
--- /dev/null
+++ b/artifacthub/library/pod-security-policy/seccompv2/1.0.0/template.yaml
@@ -0,0 +1,301 @@
+apiVersion: templates.gatekeeper.sh/v1
+kind: ConstraintTemplate
+metadata:
+ name: k8spspseccompv2
+ annotations:
+ metadata.gatekeeper.sh/title: "Seccomp V2"
+ metadata.gatekeeper.sh/version: 1.0.0
+ description: >-
+ Controls the seccomp profile used by containers. Corresponds to the
+ `securityContext.seccompProfile` field. Security contexts from the annotation is not considered as Kubernetes no longer reads security contexts from the annotation.
+spec:
+ crd:
+ spec:
+ names:
+ kind: K8sPSPSeccompV2
+ validation:
+ # Schema for the `parameters` field
+ openAPIV3Schema:
+ type: object
+ description: >-
+ Controls the seccomp profile used by containers. Corresponds to the
+ `securityContext.seccompProfile` field. Security contexts from the annotation is not considered as Kubernetes no longer reads security contexts from the annotation.
+ properties:
+ exemptImages:
+ description: >-
+ Any container that uses an image that matches an entry in this list will be excluded
+ from enforcement. Prefix-matching can be signified with `*`. For example: `my-image-*`.
+
+ It is recommended that users use the fully-qualified Docker image name (e.g. start with a domain name)
+ in order to avoid unexpectedly exempting images from an untrusted repository.
+ type: array
+ items:
+ type: string
+ allowedProfiles:
+ type: array
+ description: >-
+ An array of allowed profile values for seccomp on Pods/Containers.
+
+ Can use the securityContext naming scheme: `RuntimeDefault`, `Unconfined`
+ and/or `Localhost`. For securityContext `Localhost`, use the parameter `allowedLocalhostFiles`
+ to list the allowed profile JSON files.
+
+ The policy code will translate between the two schemes so it is not necessary to use both.
+
+ Putting a `*` in this array allows all Profiles to be used.
+
+ This field is required since with an empty list this policy will block all workloads.
+ items:
+ type: string
+ allowedLocalhostFiles:
+ type: array
+ description: >-
+ When using securityContext naming scheme for seccomp and including `Localhost` this array holds
+ the allowed profile JSON files.
+
+ Putting a `*` in this array will allows all JSON files to be used.
+
+ This field is required to allow `Localhost` in securityContext as with an empty list it will block.
+ items:
+ type: string
+ targets:
+ - target: admission.k8s.gatekeeper.sh
+ code:
+ - engine: K8sNativeValidation
+ source:
+ variables:
+ - name: containers
+ expression: 'has(variables.anyObject.spec.containers) ? variables.anyObject.spec.containers : []'
+ - name: initContainers
+ expression: 'has(variables.anyObject.spec.initContainers) ? variables.anyObject.spec.initContainers : []'
+ - name: ephemeralContainers
+ expression: 'has(variables.anyObject.spec.ephemeralContainers) ? variables.anyObject.spec.ephemeralContainers : []'
+ - name: allowAllProfiles
+ expression: |
+ has(variables.params.allowedProfiles) && variables.params.allowedProfiles.exists(profile, profile == "*")
+ - name: exemptImagePrefixes
+ expression: |
+ !has(variables.params.exemptImages) ? [] :
+ variables.params.exemptImages.filter(image, image.endsWith("*")).map(image, string(image).replace("*", ""))
+ - name: exemptImageExplicit
+ expression: |
+ !has(variables.params.exemptImages) ? [] :
+ variables.params.exemptImages.filter(image, !image.endsWith("*"))
+ - name: exemptImages
+ expression: |
+ (variables.containers + variables.initContainers + variables.ephemeralContainers).filter(container,
+ container.image in variables.exemptImageExplicit ||
+ variables.exemptImagePrefixes.exists(exemption, string(container.image).startsWith(exemption))).map(container, container.image)
+ - name: unverifiedContainers
+ expression: |
+ (variables.containers + variables.initContainers + variables.ephemeralContainers).filter(container,
+ !variables.allowAllProfiles &&
+ !(container.image in variables.exemptImages))
+ - name: inputNonLocalHostProfiles
+ expression: |
+ variables.params.allowedProfiles.filter(profile, profile != "Localhost").map(profile, {"type": profile})
+ - name: inputLocalHostProfiles
+ expression: |
+ variables.params.allowedProfiles.exists(profile, profile == "Localhost") ? variables.params.allowedLocalhostFiles.map(file, {"type": "Localhost", "localHostProfile": string(file)}) : []
+ - name: inputAllowedProfiles
+ expression: |
+ variables.inputNonLocalHostProfiles + variables.inputLocalHostProfiles
+ - name: hasPodSeccomp
+ expression: |
+ has(variables.anyObject.spec.securityContext) && has(variables.anyObject.spec.securityContext.seccompProfile)
+ - name: podLocalHostProfile
+ expression: |
+ variables.hasPodSeccomp && has(variables.anyObject.spec.securityContext.seccompProfile.localhostProfile) ? variables.anyObject.spec.securityContext.seccompProfile.localhostProfile : ""
+ - name: podSecurityContextProfileType
+ expression: |
+ has(variables.hasPodSeccomp) && has(variables.anyObject.spec.securityContext.seccompProfile.type) ? variables.anyObject.spec.securityContext.seccompProfile.type
+ : ""
+ - name: podSecurityContextProfiles
+ expression: |
+ variables.unverifiedContainers.filter(container,
+ !(has(container.securityContext) && has(container.securityContext.seccompProfile)) &&
+ variables.hasPodSeccomp
+ ).map(container, {
+ "container" : container.name,
+ "profile" : dyn(variables.podSecurityContextProfileType),
+ "file" : variables.podLocalHostProfile,
+ "location" : dyn("pod securityContext"),
+ })
+ - name: containerSecurityContextProfiles
+ expression: |
+ variables.unverifiedContainers.filter(container,
+ has(container.securityContext) && has(container.securityContext.seccompProfile)
+ ).map(container, {
+ "container" : container.name,
+ "profile" : dyn(container.securityContext.seccompProfile.type),
+ "file" : has(container.securityContext.seccompProfile.localhostProfile) ? container.securityContext.seccompProfile.localhostProfile : dyn(""),
+ "location" : dyn("container securityContext"),
+ })
+ - name: containerProfilesMissing
+ expression: |
+ variables.unverifiedContainers.filter(container,
+ !(has(container.securityContext) && has(container.securityContext.seccompProfile)) &&
+ !variables.hasPodSeccomp
+ ).map(container, {
+ "container" : container.name,
+ "profile" : dyn("not configured"),
+ "file" : dyn(""),
+ "location" : dyn("no explicit profile found"),
+ })
+ - name: allContainerProfiles
+ expression: |
+ variables.podSecurityContextProfiles + variables.containerSecurityContextProfiles + variables.containerProfilesMissing
+ - name: badContainerProfilesWithoutFiles
+ expression: |
+ variables.allContainerProfiles.filter(container,
+ container.profile != "Localhost" &&
+ !variables.inputAllowedProfiles.exists(profile, profile.type == container.profile)
+ ).map(badProfile, "Seccomp profile '" + badProfile.profile + "' is not allowed for container '" + badProfile.container + "'. Found at: " + badProfile.location + ". Allowed profiles: " + variables.inputAllowedProfiles.map(profile, "{\"type\": \"" + profile.type + "\"" + (has(profile.localHostProfile) ? ", \"localHostProfile\": \"" + profile.localHostProfile + "\"}" : "}")).join(", "))
+ - name: badContainerProfilesWithFiles
+ expression: |
+ variables.allContainerProfiles.filter(container,
+ container.profile == "Localhost" &&
+ !variables.inputAllowedProfiles.exists(profile, profile.type == "Localhost" && (has(profile.localHostProfile) && (profile.localHostProfile == container.file || profile.localHostProfile == "*")))
+ ).map(badProfile, "Seccomp profile '" + badProfile.profile + "' With file '" + badProfile.file + "' is not allowed for container '" + badProfile.container + "'. Found at: " + badProfile.location + ". Allowed profiles: " + variables.inputAllowedProfiles.map(profile, "{\"type\": \"" + profile.type + "\"" + (has(profile.localHostProfile) ? ", \"localHostProfile\": \"" + profile.localHostProfile + "\"}" : "}")).join(", "))
+ validations:
+ - expression: 'size(variables.badContainerProfilesWithoutFiles) == 0'
+ messageExpression: |
+ variables.badContainerProfilesWithoutFiles.join(", ")
+ - expression: 'size(variables.badContainerProfilesWithFiles) == 0'
+ messageExpression: |
+ variables.badContainerProfilesWithFiles.join(", ")
+ - engine: Rego
+ source:
+ rego: |
+ package k8spspseccomp
+
+ import data.lib.exempt_container.is_exempt
+
+ violation[{"msg": msg}] {
+ not input_wildcard_allowed_profiles
+ allowed_profiles := get_allowed_profiles
+ container := input_containers[name]
+ not is_exempt(container)
+ result := get_profile(container)
+ not allowed_profile(result.profile, result.file, allowed_profiles)
+ msg := get_message(result.profile, result.file, name, result.location, allowed_profiles)
+ }
+
+ get_message(profile, _, name, location, allowed_profiles) = message {
+ profile != "Localhost"
+ message := sprintf("Seccomp profile '%v' is not allowed for container '%v'. Found at: %v. Allowed profiles: %v", [profile, name, location, allowed_profiles])
+ }
+
+ get_message(profile, file, name, location, allowed_profiles) = message {
+ profile == "Localhost"
+ message := sprintf("Seccomp profile '%v' with file '%v' is not allowed for container '%v'. Found at: %v. Allowed profiles: %v", [profile, file, name, location, allowed_profiles])
+ }
+
+ input_wildcard_allowed_profiles {
+ input.parameters.allowedProfiles[_] == "*"
+ }
+
+ input_wildcard_allowed_files {
+ input.parameters.allowedLocalhostFiles[_] == "*"
+ }
+
+ allowed_profile(_, _, _) {
+ input_wildcard_allowed_profiles
+ }
+
+ allowed_profile(profile, _, _) {
+ profile == "Localhost"
+ input_wildcard_allowed_files
+ }
+
+ # Simple allowed Profiles
+ allowed_profile(profile, _, allowed) {
+ profile != "Localhost"
+ allow_profile = allowed[_]
+ profile == allow_profile.type
+ }
+
+ # annotation localhost without wildcard
+ allowed_profile(profile, file, allowed) {
+ profile == "Localhost"
+ allow_profile = allowed[_]
+ allow_profile.type == "Localhost"
+ file == allow_profile.localHostProfile
+ }
+
+ # The profiles explicitly in the list
+ get_allowed_profiles[allowed] {
+ profile := input.parameters.allowedProfiles[_]
+ profile != "Localhost"
+ allowed := {"type": profile}
+ }
+
+ get_allowed_profiles[allowed] {
+ profile := input.parameters.allowedProfiles[_]
+ profile == "Localhost"
+ file := object.get(input.parameters, "allowedLocalhostFiles", [""])[_]
+ allowed := {"type": "Localhost", "localHostProfile": file}
+ }
+
+ # Container profile as defined in containers securityContext
+ get_profile(container) = {"profile": profile, "file": file, "location": location} {
+ has_securitycontext_container(container)
+ profile := container.securityContext.seccompProfile.type
+ file := object.get(container.securityContext.seccompProfile, "localhostProfile", "")
+ location := "container securityContext"
+ }
+
+ # Container profile as defined in pods securityContext
+ get_profile(container) = {"profile": profile, "file": file, "location": location} {
+ not has_securitycontext_container(container)
+ profile := input.review.object.spec.securityContext.seccompProfile.type
+ file := object.get(input.review.object.spec.securityContext.seccompProfile, "localhostProfile", "")
+ location := "pod securityContext"
+ }
+
+ # Container profile missing
+ get_profile(container) = {"profile": "not configured", "file": "", "location": "no explicit profile found"} {
+ not has_securitycontext_container(container)
+ not has_securitycontext_pod
+ }
+
+ has_securitycontext_pod {
+ input.review.object.spec.securityContext.seccompProfile
+ }
+
+ has_securitycontext_container(container) {
+ container.securityContext.seccompProfile
+ }
+
+ input_containers[container.name] = container {
+ container := input.review.object.spec.containers[_]
+ }
+
+ input_containers[container.name] = container {
+ container := input.review.object.spec.initContainers[_]
+ }
+
+ input_containers[container.name] = container {
+ container := input.review.object.spec.ephemeralContainers[_]
+ }
+ libs:
+ - |
+ package lib.exempt_container
+
+ is_exempt(container) {
+ exempt_images := object.get(object.get(input, "parameters", {}), "exemptImages", [])
+ img := container.image
+ exemption := exempt_images[_]
+ _matches_exemption(img, exemption)
+ }
+
+ _matches_exemption(img, exemption) {
+ not endswith(exemption, "*")
+ exemption == img
+ }
+
+ _matches_exemption(img, exemption) {
+ endswith(exemption, "*")
+ prefix := trim_suffix(exemption, "*")
+ startswith(img, prefix)
+ }
diff --git a/library/pod-security-policy/kustomization.yaml b/library/pod-security-policy/kustomization.yaml
index 63d1d6d44..75e0bd1cf 100644
--- a/library/pod-security-policy/kustomization.yaml
+++ b/library/pod-security-policy/kustomization.yaml
@@ -17,3 +17,4 @@ resources:
- selinux
- users
- volumes
+- seccompv2
diff --git a/library/pod-security-policy/seccomp/samples/psp-seccomp/constraint.yaml b/library/pod-security-policy/seccomp/samples/psp-seccomp/constraint.yaml
index d26af154e..0d28be8d9 100644
--- a/library/pod-security-policy/seccomp/samples/psp-seccomp/constraint.yaml
+++ b/library/pod-security-policy/seccomp/samples/psp-seccomp/constraint.yaml
@@ -8,6 +8,8 @@ spec:
- apiGroups: [""]
kinds: ["Pod"]
parameters:
+ exemptImages:
+ - nginx-exempt
allowedProfiles:
- runtime/default
- - docker/default
+ - localhost/profile.json
\ No newline at end of file
diff --git a/library/pod-security-policy/seccomp/samples/psp-seccomp/example_allowed_exempt_image.yaml b/library/pod-security-policy/seccomp/samples/psp-seccomp/example_allowed_exempt_image.yaml
new file mode 100644
index 000000000..d5f42987d
--- /dev/null
+++ b/library/pod-security-policy/seccomp/samples/psp-seccomp/example_allowed_exempt_image.yaml
@@ -0,0 +1,13 @@
+apiVersion: v1
+kind: Pod
+metadata:
+ name: nginx-seccomp-disallowed
+ labels:
+ app: nginx-seccomp
+spec:
+ containers:
+ - name: nginx
+ image: nginx-exempt
+ securityContext:
+ seccompProfile:
+ type: Unconfined
diff --git a/library/pod-security-policy/seccomp/samples/psp-seccomp/example_allowed_localhost.yaml b/library/pod-security-policy/seccomp/samples/psp-seccomp/example_allowed_localhost.yaml
new file mode 100644
index 000000000..856f3217b
--- /dev/null
+++ b/library/pod-security-policy/seccomp/samples/psp-seccomp/example_allowed_localhost.yaml
@@ -0,0 +1,14 @@
+apiVersion: v1
+kind: Pod
+metadata:
+ name: nginx-seccomp-allowed-localhost
+ labels:
+ app: nginx-seccomp
+spec:
+ containers:
+ - name: nginx
+ image: nginx
+ securityContext:
+ seccompProfile:
+ type: Localhost
+ localhostProfile: profile.json
diff --git a/library/pod-security-policy/seccomp/samples/psp-seccomp/example_disallowed_localhost.yaml b/library/pod-security-policy/seccomp/samples/psp-seccomp/example_disallowed_localhost.yaml
new file mode 100644
index 000000000..c566f7ca4
--- /dev/null
+++ b/library/pod-security-policy/seccomp/samples/psp-seccomp/example_disallowed_localhost.yaml
@@ -0,0 +1,14 @@
+apiVersion: v1
+kind: Pod
+metadata:
+ name: nginx-seccomp-disallowed-localhost
+ labels:
+ app: nginx-seccomp
+spec:
+ containers:
+ - name: nginx
+ image: nginx
+ securityContext:
+ seccompProfile:
+ type: Localhost
+ localhostProfile: profile.log
diff --git a/library/pod-security-policy/seccomp/suite.yaml b/library/pod-security-policy/seccomp/suite.yaml
index 62336b26e..0347d33fb 100644
--- a/library/pod-security-policy/seccomp/suite.yaml
+++ b/library/pod-security-policy/seccomp/suite.yaml
@@ -30,3 +30,16 @@ tests:
assertions:
- violations: 1
message: "Seccomp profile 'unconfined' is not allowed for container 'nginx'. Found at: annotation container.seccomp.security.alpha.kubernetes.io/nginx"
+ - name: example-allowed-container-exempt-image
+ object: samples/psp-seccomp/example_allowed_exempt_image.yaml
+ assertions:
+ - violations: no
+ - name: example-allowed-container-localhost-profile
+ object: samples/psp-seccomp/example_allowed_localhost.yaml
+ assertions:
+ - violations: no
+ - name: example-disallowed-container-localhost-profile
+ object: samples/psp-seccomp/example_disallowed_localhost.yaml
+ assertions:
+ - violations: 1
+ message: "Seccomp profile 'localhost/profile.log' is not allowed for container 'nginx'. Found at: container securityContext."
\ No newline at end of file
diff --git a/library/pod-security-policy/seccomp/template.yaml b/library/pod-security-policy/seccomp/template.yaml
index d252a1ace..d1b676d97 100644
--- a/library/pod-security-policy/seccomp/template.yaml
+++ b/library/pod-security-policy/seccomp/template.yaml
@@ -4,7 +4,7 @@ metadata:
name: k8spspseccomp
annotations:
metadata.gatekeeper.sh/title: "Seccomp"
- metadata.gatekeeper.sh/version: 1.0.1
+ metadata.gatekeeper.sh/version: 1.1.0
description: >-
Controls the seccomp profile used by containers. Corresponds to the
`seccomp.security.alpha.kubernetes.io/allowedProfileNames` annotation on
@@ -67,215 +67,332 @@ spec:
type: string
targets:
- target: admission.k8s.gatekeeper.sh
- rego: |
- package k8spspseccomp
-
- import data.lib.exempt_container.is_exempt
-
- container_annotation_key_prefix = "container.seccomp.security.alpha.kubernetes.io/"
-
- pod_annotation_key = "seccomp.security.alpha.kubernetes.io/pod"
-
- naming_translation = {
- # securityContext -> annotation
- "RuntimeDefault": ["runtime/default", "docker/default"],
- "Unconfined": ["unconfined"],
- "Localhost": ["localhost"],
- # annotation -> securityContext
- "runtime/default": ["RuntimeDefault"],
- "docker/default": ["RuntimeDefault"],
- "unconfined": ["Unconfined"],
- "localhost": ["Localhost"],
- }
-
- violation[{"msg": msg}] {
- not input_wildcard_allowed_profiles
- allowed_profiles := get_allowed_profiles
- container := input_containers[name]
- not is_exempt(container)
- result := get_profile(container)
- not allowed_profile(result.profile, result.file, allowed_profiles)
- msg := get_message(result.profile, result.file, name, result.location, allowed_profiles)
- }
-
- get_message(profile, _, name, location, allowed_profiles) = message {
- not profile == "Localhost"
- message := sprintf("Seccomp profile '%v' is not allowed for container '%v'. Found at: %v. Allowed profiles: %v", [profile, name, location, allowed_profiles])
- }
-
- get_message(profile, file, name, location, allowed_profiles) = message {
- profile == "Localhost"
- message := sprintf("Seccomp profile '%v' with file '%v' is not allowed for container '%v'. Found at: %v. Allowed profiles: %v", [profile, file, name, location, allowed_profiles])
- }
-
- input_wildcard_allowed_profiles {
- input.parameters.allowedProfiles[_] == "*"
- }
-
- input_wildcard_allowed_files {
- input.parameters.allowedLocalhostFiles[_] == "*"
- }
-
- input_wildcard_allowed_files {
- "localhost/*" == input.parameters.allowedProfiles[_]
- }
-
- # Simple allowed Profiles
- allowed_profile(profile, _, allowed) {
- not startswith(lower(profile), "localhost")
- profile == allowed[_]
- }
-
- # seccomp Localhost without wildcard
- allowed_profile(profile, file, allowed) {
- profile == "Localhost"
- not input_wildcard_allowed_files
- profile == allowed[_]
- allowed_files := {x | x := object.get(input.parameters, "allowedLocalhostFiles", [])[_]} | get_annotation_localhost_files
- file == allowed_files[_]
- }
-
- # seccomp Localhost with wildcard
- allowed_profile(profile, _, allowed) {
- profile == "Localhost"
- input_wildcard_allowed_files
- profile == allowed[_]
- }
-
- # annotation localhost with wildcard
- allowed_profile(profile, _, allowed) {
- "localhost/*" == allowed[_]
- startswith(profile, "localhost/")
- }
-
- # annotation localhost without wildcard
- allowed_profile(profile, _, allowed) {
- startswith(profile, "localhost/")
- profile == allowed[_]
- }
-
- # Localhost files from annotation scheme
- get_annotation_localhost_files[file] {
- profile := input.parameters.allowedProfiles[_]
- startswith(profile, "localhost/")
- file := replace(profile, "localhost/", "")
- }
-
- # The profiles explicitly in the list
- get_allowed_profiles[allowed] {
- allowed := input.parameters.allowedProfiles[_]
- }
-
- # The simply translated profiles
- get_allowed_profiles[allowed] {
- profile := input.parameters.allowedProfiles[_]
- not startswith(lower(profile), "localhost")
- allowed := naming_translation[profile][_]
- }
-
- # Seccomp Localhost to annotation translation
- get_allowed_profiles[allowed] {
- profile := input.parameters.allowedProfiles[_]
- profile == "Localhost"
- file := object.get(input.parameters, "allowedLocalhostFiles", [])[_]
- allowed := sprintf("%v/%v", [naming_translation[profile][_], file])
- }
-
- # Annotation localhost to Seccomp translation
- get_allowed_profiles[allowed] {
- profile := input.parameters.allowedProfiles[_]
- startswith(profile, "localhost")
- allowed := naming_translation.localhost[_]
- }
-
- # Container profile as defined in pod annotation
- get_profile(container) = {"profile": profile, "file": "", "location": location} {
- not has_securitycontext_container(container)
- not has_annotation(get_container_annotation_key(container.name))
- not has_securitycontext_pod
- profile := input.review.object.metadata.annotations[pod_annotation_key]
- location := sprintf("annotation %v", [pod_annotation_key])
- }
-
- # Container profile as defined in container annotation
- get_profile(container) = {"profile": profile, "file": "", "location": location} {
- not has_securitycontext_container(container)
- not has_securitycontext_pod
- container_annotation := get_container_annotation_key(container.name)
- has_annotation(container_annotation)
- profile := input.review.object.metadata.annotations[container_annotation]
- location := sprintf("annotation %v", [container_annotation])
- }
-
- # Container profile as defined in pods securityContext
- get_profile(container) = {"profile": profile, "file": file, "location": location} {
- not has_securitycontext_container(container)
- profile := input.review.object.spec.securityContext.seccompProfile.type
- file := object.get(input.review.object.spec.securityContext.seccompProfile, "localhostProfile", "")
- location := "pod securityContext"
- }
-
- # Container profile as defined in containers securityContext
- get_profile(container) = {"profile": profile, "file": file, "location": location} {
- has_securitycontext_container(container)
- profile := container.securityContext.seccompProfile.type
- file := object.get(container.securityContext.seccompProfile, "localhostProfile", "")
- location := "container securityContext"
- }
-
- # Container profile missing
- get_profile(container) = {"profile": "not configured", "file": "", "location": "no explicit profile found"} {
- not has_annotation(get_container_annotation_key(container.name))
- not has_annotation(pod_annotation_key)
- not has_securitycontext_pod
- not has_securitycontext_container(container)
- }
-
- has_annotation(annotation) {
- input.review.object.metadata.annotations[annotation]
- }
-
- has_securitycontext_pod {
- input.review.object.spec.securityContext.seccompProfile
- }
-
- has_securitycontext_container(container) {
- container.securityContext.seccompProfile
- }
-
- get_container_annotation_key(name) = annotation {
- annotation := concat("", [container_annotation_key_prefix, name])
- }
-
- input_containers[container.name] = container {
- container := input.review.object.spec.containers[_]
- }
-
- input_containers[container.name] = container {
- container := input.review.object.spec.initContainers[_]
- }
-
- input_containers[container.name] = container {
- container := input.review.object.spec.ephemeralContainers[_]
- }
- libs:
- - |
- package lib.exempt_container
-
- is_exempt(container) {
- exempt_images := object.get(object.get(input, "parameters", {}), "exemptImages", [])
- img := container.image
- exemption := exempt_images[_]
- _matches_exemption(img, exemption)
- }
-
- _matches_exemption(img, exemption) {
- not endswith(exemption, "*")
- exemption == img
- }
-
- _matches_exemption(img, exemption) {
- endswith(exemption, "*")
- prefix := trim_suffix(exemption, "*")
- startswith(img, prefix)
- }
+ code:
+ - engine: K8sNativeValidation
+ source:
+ variables:
+ - name: containers
+ expression: 'has(variables.anyObject.spec.containers) ? variables.anyObject.spec.containers : []'
+ - name: initContainers
+ expression: 'has(variables.anyObject.spec.initContainers) ? variables.anyObject.spec.initContainers : []'
+ - name: ephemeralContainers
+ expression: 'has(variables.anyObject.spec.ephemeralContainers) ? variables.anyObject.spec.ephemeralContainers : []'
+ - name: allowAllProfiles
+ expression: |
+ has(variables.params.allowedProfiles) && variables.params.allowedProfiles.exists(profile, profile == "*")
+ - name: exemptImagePrefixes
+ expression: |
+ !has(variables.params.exemptImages) ? [] :
+ variables.params.exemptImages.filter(image, image.endsWith("*")).map(image, string(image).replace("*", ""))
+ - name: exemptImageExplicit
+ expression: |
+ !has(variables.params.exemptImages) ? [] :
+ variables.params.exemptImages.filter(image, !image.endsWith("*"))
+ - name: exemptImages
+ expression: |
+ (variables.containers + variables.initContainers + variables.ephemeralContainers).filter(container,
+ container.image in variables.exemptImageExplicit ||
+ variables.exemptImagePrefixes.exists(exemption, string(container.image).startsWith(exemption))).map(container, container.image)
+ - name: unverifiedContainers
+ expression: |
+ (variables.containers + variables.initContainers + variables.ephemeralContainers).filter(container,
+ !variables.allowAllProfiles &&
+ !(container.image in variables.exemptImages))
+ - name: inputAllowedProfiles
+ expression: |
+ !has(variables.params.allowedProfiles) ? [] : variables.params.allowedProfiles
+ - name: allowedLocalhostFiles
+ expression: |
+ has(variables.params.allowedLocalhostFiles) ? variables.params.allowedLocalhostFiles : []
+ - name: allowedProfilesTranslation
+ expression: |
+ (variables.inputAllowedProfiles.filter(profile,
+ profile != "Localhost").map(profile, profile == "Unconfined" ? "unconfined" : profile)) +
+ (variables.inputAllowedProfiles.exists(profile, profile == "RuntimeDefault") ? ["runtime/default", "docker/default"] : [])
+ - name: allowSecurityContextLocalhost
+ expression: |
+ variables.inputAllowedProfiles.exists(profile, profile == "Localhost")
+ - name: derivedAllowedLocalhostFiles
+ expression: |
+ variables.allowSecurityContextLocalhost ? variables.params.allowedLocalhostFiles.map(file, "localhost/" + file) : []
+ - name: localhostWildcardAllowed
+ expression: |
+ variables.inputAllowedProfiles.exists(profile, profile == "localhost/*") || variables.derivedAllowedLocalhostFiles.exists(profile, profile == "localhost/*")
+ - name: allowedProfiles
+ expression: |
+ (variables.allowedProfilesTranslation + variables.derivedAllowedLocalhostFiles)
+ - name: hasPodSeccomp
+ expression: |
+ has(variables.anyObject.spec.securityContext) && has(variables.anyObject.spec.securityContext.seccompProfile)
+ - name: hasPodAnnotations
+ expression: |
+ has(variables.anyObject.metadata.annotations) && ("seccomp.security.alpha.kubernetes.io/pod" in variables.anyObject.metadata.annotations)
+ - name: podAnnotationsProfiles
+ expression: |
+ variables.unverifiedContainers.filter(container,
+ !(has(container.securityContext) && has(container.securityContext.seccompProfile)) &&
+ !(has(variables.anyObject.metadata.annotations) && (("container.seccomp.security.alpha.kubernetes.io/" + container.name) in variables.anyObject.metadata.annotations)) &&
+ !variables.hasPodSeccomp &&
+ variables.hasPodAnnotations
+ ).map(container, {
+ "container" : container.name,
+ "profile" : variables.anyObject.metadata.annotations["seccomp.security.alpha.kubernetes.io/pod"],
+ "file" : dyn(""),
+ "location" : dyn("annotation seccomp.security.alpha.kubernetes.io/pod"),
+ })
+ - name: containerAnnotationsProfiles
+ expression: |
+ variables.unverifiedContainers.filter(container,
+ !(has(container.securityContext) && has(container.securityContext.seccompProfile)) &&
+ !variables.hasPodSeccomp &&
+ has(variables.anyObject.metadata.annotations) && (("container.seccomp.security.alpha.kubernetes.io/" + container.name) in variables.anyObject.metadata.annotations)
+ ).map(container, {
+ "container" : container.name,
+ "profile" : variables.anyObject.metadata.annotations["container.seccomp.security.alpha.kubernetes.io/" + container.name],
+ "file" : dyn(""),
+ "location" : dyn("annotation container.seccomp.security.alpha.kubernetes.io/" + container.name),
+ })
+ - name: podLocalHostProfile
+ expression: |
+ variables.hasPodSeccomp && has(variables.anyObject.spec.securityContext.seccompProfile.localhostProfile) ? variables.anyObject.spec.securityContext.seccompProfile.localhostProfile : ""
+ - name: canonicalPodSecurityContextProfile
+ expression: |
+ has(variables.hasPodSeccomp) && has(variables.anyObject.spec.securityContext.seccompProfile.type) ?
+ (variables.anyObject.spec.securityContext.seccompProfile.type == "RuntimeDefault" ? (
+ variables.allowedProfiles.exists(profile, profile == "runtime/default") ? "runtime/default" : variables.allowedProfiles.exists(profile, profile == "docker/default") ? "docker/default" : "runtime/default") :
+ variables.anyObject.spec.securityContext.seccompProfile.type == "Unconfined" ? "unconfined" : variables.anyObject.spec.securityContext.seccompProfile.type == "Localhost" ? "localhost/" + variables.podLocalHostProfile : "")
+ : ""
+ - name: podSecurityContextProfiles
+ expression: |
+ variables.unverifiedContainers.filter(container,
+ !(has(container.securityContext) && has(container.securityContext.seccompProfile)) &&
+ variables.hasPodSeccomp
+ ).map(container, {
+ "container" : container.name,
+ "profile" : dyn(variables.canonicalPodSecurityContextProfile),
+ "file" : variables.podLocalHostProfile,
+ "location" : dyn("pod securityContext"),
+ })
+ - name: containerSecurityContextProfiles
+ expression: |
+ variables.unverifiedContainers.filter(container,
+ has(container.securityContext) && has(container.securityContext.seccompProfile)
+ ).map(container, {
+ "container" : container.name,
+ "profile" : dyn(has(container.securityContext.seccompProfile.type) ? (container.securityContext.seccompProfile.type == "RuntimeDefault" ? (
+ variables.allowedProfiles.exists(profile, profile == "runtime/default") ? "runtime/default" : variables.allowedProfiles.exists(profile, profile == "docker/default") ? "docker/default" : "runtime/default") :
+ container.securityContext.seccompProfile.type == "Unconfined" ? "unconfined" : container.securityContext.seccompProfile.type == "Localhost" ? "localhost/" + container.securityContext.seccompProfile.localhostProfile : "")
+ : ""),
+ "file" : has(container.securityContext.seccompProfile.localhostProfile) ? container.securityContext.seccompProfile.localhostProfile : dyn(""),
+ "location" : dyn("container securityContext"),
+ })
+ - name: containerProfilesMissing
+ expression: |
+ variables.unverifiedContainers.filter(container,
+ !(has(container.securityContext) && has(container.securityContext.seccompProfile)) &&
+ !(has(variables.anyObject.metadata.annotations) && (("container.seccomp.security.alpha.kubernetes.io/" + container.name) in variables.anyObject.metadata.annotations)) &&
+ !variables.hasPodSeccomp &&
+ !variables.hasPodAnnotations
+ ).map(container, {
+ "container" : container.name,
+ "profile" : dyn("not configured"),
+ "file" : dyn(""),
+ "location" : dyn("no explicit profile found"),
+ })
+ - name: allContainerProfiles
+ expression: |
+ variables.podAnnotationsProfiles + variables.containerAnnotationsProfiles + variables.podSecurityContextProfiles + variables.containerSecurityContextProfiles + variables.containerProfilesMissing
+ - name: badContainerProfiles
+ expression: |
+ variables.allContainerProfiles.filter(badContainerProfile,
+ !((badContainerProfile.profile in variables.allowedProfiles) || (badContainerProfile.profile.startsWith("localhost/") && variables.localhostWildcardAllowed))
+ ).map(badProfile, "Seccomp profile '" + badProfile.profile + "' is not allowed for container '" + badProfile.container + "'. Found at: " + badProfile.location + ". Allowed profiles: " + variables.allowedProfiles.join(", "))
+ validations:
+ - expression: 'size(variables.badContainerProfiles) == 0'
+ messageExpression: |
+ variables.badContainerProfiles.join(", ")
+ - engine: Rego
+ source:
+ rego: |
+ package k8spspseccomp
+
+ import data.lib.exempt_container.is_exempt
+
+ container_annotation_key_prefix = "container.seccomp.security.alpha.kubernetes.io/"
+
+ pod_annotation_key = "seccomp.security.alpha.kubernetes.io/pod"
+
+ violation[{"msg": msg}] {
+ not input_wildcard_allowed_profiles
+ allowed_profiles := get_allowed_profiles
+ container := input_containers[name]
+ not is_exempt(container)
+ result := get_profile(container)
+ not allowed_profile(result.profile, result.file, allowed_profiles)
+ msg := get_message(result.profile, result.file, name, result.location, allowed_profiles)
+ }
+
+ get_message(profile, _, name, location, allowed_profiles) = message {
+ message := sprintf("Seccomp profile '%v' is not allowed for container '%v'. Found at: %v. Allowed profiles: %v", [profile, name, location, allowed_profiles])
+ }
+
+ input_wildcard_allowed_profiles {
+ input.parameters.allowedProfiles[_] == "*"
+ }
+
+ input_wildcard_allowed_files {
+ input.parameters.allowedLocalhostFiles[_] == "*"
+ }
+
+ input_wildcard_allowed_files {
+ "localhost/*" == input.parameters.allowedProfiles[_]
+ }
+
+ # Simple allowed Profiles
+ allowed_profile(profile, _, allowed) {
+ not startswith(profile, "localhost/")
+ profile == allowed[_]
+ }
+
+ # annotation localhost with wildcard
+ allowed_profile(profile, _, allowed) {
+ "localhost/*" == allowed[_]
+ startswith(profile, "localhost/")
+ }
+
+ # annotation localhost without wildcard
+ allowed_profile(profile, _, allowed) {
+ startswith(profile, "localhost/")
+ profile == allowed[_]
+ }
+
+ # The profiles explicitly in the list
+ get_allowed_profiles[allowed] {
+ allowed := input.parameters.allowedProfiles[_]
+ }
+
+ # Seccomp Localhost to annotation translation
+ get_allowed_profiles[allowed] {
+ profile := input.parameters.allowedProfiles[_]
+ not contains(profile, "/")
+ file := object.get(input.parameters, "allowedLocalhostFiles", [])[_]
+ allowed := canonicalize_seccomp_profile({"type": profile, "localhostProfile": file}, "")[_]
+ }
+
+ # Container profile as defined in pod annotation
+ get_profile(container) = {"profile": profile, "file": "", "location": location} {
+ not has_securitycontext_container(container)
+ not has_annotation(get_container_annotation_key(container.name))
+ not has_securitycontext_pod
+ profile := input.review.object.metadata.annotations[pod_annotation_key]
+ location := sprintf("annotation %v", [pod_annotation_key])
+ }
+
+ # Container profile as defined in container annotation
+ get_profile(container) = {"profile": profile, "file": "", "location": location} {
+ not has_securitycontext_container(container)
+ not has_securitycontext_pod
+ container_annotation := get_container_annotation_key(container.name)
+ has_annotation(container_annotation)
+ profile := input.review.object.metadata.annotations[container_annotation]
+ location := sprintf("annotation %v", [container_annotation])
+ }
+
+ # Container profile as defined in pods securityContext
+ get_profile(container) = {"profile": profile, "file": file, "location": location} {
+ not has_securitycontext_container(container)
+ profile := canonicalize_seccomp_profile(input.review.object.spec.securityContext.seccompProfile, canonicalize_runtime_default_profile)[_]
+ file := object.get(input.review.object.spec.securityContext.seccompProfile, "localhostProfile", "")
+ location := "pod securityContext"
+ }
+
+ # Container profile as defined in containers securityContext
+ get_profile(container) = {"profile": profile, "file": file, "location": location} {
+ has_securitycontext_container(container)
+ profile := canonicalize_seccomp_profile(container.securityContext.seccompProfile, canonicalize_runtime_default_profile)[_]
+ file := object.get(container.securityContext.seccompProfile, "localhostProfile", "")
+ location := "container securityContext"
+ }
+
+ # Container profile missing
+ get_profile(container) = {"profile": "not configured", "file": "", "location": "no explicit profile found"} {
+ not has_securitycontext_container(container)
+ not has_securitycontext_pod
+ not has_annotation(get_container_annotation_key(container.name))
+ not has_annotation(pod_annotation_key)
+ }
+
+ has_annotation(annotation) {
+ input.review.object.metadata.annotations[annotation]
+ }
+
+ has_securitycontext_pod {
+ input.review.object.spec.securityContext.seccompProfile
+ }
+
+ has_securitycontext_container(container) {
+ container.securityContext.seccompProfile
+ }
+
+ get_container_annotation_key(name) = annotation {
+ annotation := concat("", [container_annotation_key_prefix, name])
+ }
+
+ input_containers[container.name] = container {
+ container := input.review.object.spec.containers[_]
+ }
+
+ input_containers[container.name] = container {
+ container := input.review.object.spec.initContainers[_]
+ }
+
+ input_containers[container.name] = container {
+ container := input.review.object.spec.ephemeralContainers[_]
+ }
+
+ canonicalize_runtime_default_profile() = out {
+ "runtime/default" == input.parameters.allowedProfiles[_]
+ out := "runtime/default"
+ } else = out {
+ "docker/default" == input.parameters.allowedProfiles[_]
+ out := "docker/default"
+ } else = out {
+ out := "runtime/default"
+ }
+
+ canonicalize_seccomp_profile(profile, def) = out {
+ profile.type == "RuntimeDefault"
+ def == ""
+ out := ["runtime/default", "docker/default"]
+ } else = out {
+ profile.type == "RuntimeDefault"
+ def != ""
+ out := [def]
+ } else = out {
+ profile.type == "Localhost"
+ out := [sprintf("localhost/%s", [profile.localhostProfile])]
+ } else = out {
+ profile.type == "Unconfined"
+ out := ["unconfined"]
+ }
+ libs:
+ - |
+ package lib.exempt_container
+
+ is_exempt(container) {
+ exempt_images := object.get(object.get(input, "parameters", {}), "exemptImages", [])
+ img := container.image
+ exemption := exempt_images[_]
+ _matches_exemption(img, exemption)
+ }
+
+ _matches_exemption(img, exemption) {
+ not endswith(exemption, "*")
+ exemption == img
+ }
+
+ _matches_exemption(img, exemption) {
+ endswith(exemption, "*")
+ prefix := trim_suffix(exemption, "*")
+ startswith(img, prefix)
+ }
diff --git a/library/pod-security-policy/seccompv2/kustomization.yaml b/library/pod-security-policy/seccompv2/kustomization.yaml
new file mode 100644
index 000000000..7d70d11b7
--- /dev/null
+++ b/library/pod-security-policy/seccompv2/kustomization.yaml
@@ -0,0 +1,2 @@
+resources:
+ - template.yaml
diff --git a/library/pod-security-policy/seccompv2/samples/psp-seccomp/constraint.yaml b/library/pod-security-policy/seccompv2/samples/psp-seccomp/constraint.yaml
new file mode 100644
index 000000000..a85141492
--- /dev/null
+++ b/library/pod-security-policy/seccompv2/samples/psp-seccomp/constraint.yaml
@@ -0,0 +1,17 @@
+apiVersion: constraints.gatekeeper.sh/v1beta1
+kind: K8sPSPSeccompV2
+metadata:
+ name: psp-seccomp
+spec:
+ match:
+ kinds:
+ - apiGroups: [""]
+ kinds: ["Pod"]
+ parameters:
+ exemptImages:
+ - nginx-exempt
+ allowedProfiles:
+ - RuntimeDefault
+ - Localhost
+ allowedLocalhostFiles:
+ - "*"
diff --git a/library/pod-security-policy/seccompv2/samples/psp-seccomp/disallowed_ephemeral.yaml b/library/pod-security-policy/seccompv2/samples/psp-seccomp/disallowed_ephemeral.yaml
new file mode 100644
index 000000000..4b32243a9
--- /dev/null
+++ b/library/pod-security-policy/seccompv2/samples/psp-seccomp/disallowed_ephemeral.yaml
@@ -0,0 +1,10 @@
+apiVersion: v1
+kind: Pod
+metadata:
+ name: nginx-seccomp-disallowed
+ labels:
+ app: nginx-seccomp
+spec:
+ ephemeralContainers:
+ - name: nginx
+ image: nginx
diff --git a/library/pod-security-policy/seccompv2/samples/psp-seccomp/example_allowed.yaml b/library/pod-security-policy/seccompv2/samples/psp-seccomp/example_allowed.yaml
new file mode 100644
index 000000000..65dfc5be8
--- /dev/null
+++ b/library/pod-security-policy/seccompv2/samples/psp-seccomp/example_allowed.yaml
@@ -0,0 +1,13 @@
+apiVersion: v1
+kind: Pod
+metadata:
+ name: nginx-seccomp-allowed
+ labels:
+ app: nginx-seccomp
+spec:
+ containers:
+ - name: nginx
+ image: nginx
+ securityContext:
+ seccompProfile:
+ type: RuntimeDefault
diff --git a/library/pod-security-policy/seccompv2/samples/psp-seccomp/example_allowed_exempt_image.yaml b/library/pod-security-policy/seccompv2/samples/psp-seccomp/example_allowed_exempt_image.yaml
new file mode 100644
index 000000000..d5f42987d
--- /dev/null
+++ b/library/pod-security-policy/seccompv2/samples/psp-seccomp/example_allowed_exempt_image.yaml
@@ -0,0 +1,13 @@
+apiVersion: v1
+kind: Pod
+metadata:
+ name: nginx-seccomp-disallowed
+ labels:
+ app: nginx-seccomp
+spec:
+ containers:
+ - name: nginx
+ image: nginx-exempt
+ securityContext:
+ seccompProfile:
+ type: Unconfined
diff --git a/library/pod-security-policy/seccompv2/samples/psp-seccomp/example_allowed_localhost.yaml b/library/pod-security-policy/seccompv2/samples/psp-seccomp/example_allowed_localhost.yaml
new file mode 100644
index 000000000..856f3217b
--- /dev/null
+++ b/library/pod-security-policy/seccompv2/samples/psp-seccomp/example_allowed_localhost.yaml
@@ -0,0 +1,14 @@
+apiVersion: v1
+kind: Pod
+metadata:
+ name: nginx-seccomp-allowed-localhost
+ labels:
+ app: nginx-seccomp
+spec:
+ containers:
+ - name: nginx
+ image: nginx
+ securityContext:
+ seccompProfile:
+ type: Localhost
+ localhostProfile: profile.json
diff --git a/library/pod-security-policy/seccompv2/samples/psp-seccomp/example_disallowed.yaml b/library/pod-security-policy/seccompv2/samples/psp-seccomp/example_disallowed.yaml
new file mode 100644
index 000000000..40c115b86
--- /dev/null
+++ b/library/pod-security-policy/seccompv2/samples/psp-seccomp/example_disallowed.yaml
@@ -0,0 +1,13 @@
+apiVersion: v1
+kind: Pod
+metadata:
+ name: nginx-seccomp-disallowed
+ labels:
+ app: nginx-seccomp
+spec:
+ containers:
+ - name: nginx
+ image: nginx
+ securityContext:
+ seccompProfile:
+ type: Unconfined
diff --git a/library/pod-security-policy/seccompv2/samples/psp-seccomp/example_disallowed2.yaml b/library/pod-security-policy/seccompv2/samples/psp-seccomp/example_disallowed2.yaml
new file mode 100644
index 000000000..e08463f18
--- /dev/null
+++ b/library/pod-security-policy/seccompv2/samples/psp-seccomp/example_disallowed2.yaml
@@ -0,0 +1,13 @@
+apiVersion: v1
+kind: Pod
+metadata:
+ name: nginx-seccomp-disallowed2
+ labels:
+ app: nginx-seccomp
+spec:
+ securityContext:
+ seccompProfile:
+ type: Unconfined
+ containers:
+ - name: nginx
+ image: nginx
diff --git a/library/pod-security-policy/seccompv2/suite.yaml b/library/pod-security-policy/seccompv2/suite.yaml
new file mode 100644
index 000000000..f9daa264d
--- /dev/null
+++ b/library/pod-security-policy/seccompv2/suite.yaml
@@ -0,0 +1,36 @@
+kind: Suite
+apiVersion: test.gatekeeper.sh/v1alpha1
+metadata:
+ name: seccomp
+tests:
+- name: default-seccomp-required
+ template: template.yaml
+ constraint: samples/psp-seccomp/constraint.yaml
+ cases:
+ - name: example-disallowed-global
+ object: samples/psp-seccomp/example_disallowed2.yaml
+ assertions:
+ - violations: 1
+ message: "Seccomp profile 'Unconfined' is not allowed for container 'nginx'. Found at: pod securityContext."
+ - name: example-disallowed-container
+ object: samples/psp-seccomp/example_disallowed.yaml
+ assertions:
+ - violations: 1
+ message: "Seccomp profile 'Unconfined' is not allowed for container 'nginx'. Found at: container securityContext."
+ - name: example-allowed-container
+ object: samples/psp-seccomp/example_allowed.yaml
+ assertions:
+ - violations: no
+ - name: example-allowed-container
+ object: samples/psp-seccomp/example_allowed_localhost.yaml
+ assertions:
+ - violations: no
+ - name: example-allowed-container-exempt-image
+ object: samples/psp-seccomp/example_allowed_exempt_image.yaml
+ assertions:
+ - violations: no
+ - name: disallowed-ephemeral
+ object: samples/psp-seccomp/disallowed_ephemeral.yaml
+ assertions:
+ - violations: 1
+ message: "Seccomp profile 'not configured' is not allowed for container 'nginx'. Found at: no explicit profile found"
diff --git a/library/pod-security-policy/seccompv2/template.yaml b/library/pod-security-policy/seccompv2/template.yaml
new file mode 100644
index 000000000..ce0a53202
--- /dev/null
+++ b/library/pod-security-policy/seccompv2/template.yaml
@@ -0,0 +1,301 @@
+apiVersion: templates.gatekeeper.sh/v1
+kind: ConstraintTemplate
+metadata:
+ name: k8spspseccompv2
+ annotations:
+ metadata.gatekeeper.sh/title: "Seccomp V2"
+ metadata.gatekeeper.sh/version: 1.0.0
+ description: >-
+ Controls the seccomp profile used by containers. Corresponds to the
+ `securityContext.seccompProfile` field. Security contexts from the annotation is not considered as Kubernetes no longer reads security contexts from the annotation.
+spec:
+ crd:
+ spec:
+ names:
+ kind: K8sPSPSeccompV2
+ validation:
+ # Schema for the `parameters` field
+ openAPIV3Schema:
+ type: object
+ description: >-
+ Controls the seccomp profile used by containers. Corresponds to the
+ `securityContext.seccompProfile` field. Security contexts from the annotation is not considered as Kubernetes no longer reads security contexts from the annotation.
+ properties:
+ exemptImages:
+ description: >-
+ Any container that uses an image that matches an entry in this list will be excluded
+ from enforcement. Prefix-matching can be signified with `*`. For example: `my-image-*`.
+
+ It is recommended that users use the fully-qualified Docker image name (e.g. start with a domain name)
+ in order to avoid unexpectedly exempting images from an untrusted repository.
+ type: array
+ items:
+ type: string
+ allowedProfiles:
+ type: array
+ description: >-
+ An array of allowed profile values for seccomp on Pods/Containers.
+
+ Can use the securityContext naming scheme: `RuntimeDefault`, `Unconfined`
+ and/or `Localhost`. For securityContext `Localhost`, use the parameter `allowedLocalhostFiles`
+ to list the allowed profile JSON files.
+
+ The policy code will translate between the two schemes so it is not necessary to use both.
+
+ Putting a `*` in this array allows all Profiles to be used.
+
+ This field is required since with an empty list this policy will block all workloads.
+ items:
+ type: string
+ allowedLocalhostFiles:
+ type: array
+ description: >-
+ When using securityContext naming scheme for seccomp and including `Localhost` this array holds
+ the allowed profile JSON files.
+
+ Putting a `*` in this array will allows all JSON files to be used.
+
+ This field is required to allow `Localhost` in securityContext as with an empty list it will block.
+ items:
+ type: string
+ targets:
+ - target: admission.k8s.gatekeeper.sh
+ code:
+ - engine: K8sNativeValidation
+ source:
+ variables:
+ - name: containers
+ expression: 'has(variables.anyObject.spec.containers) ? variables.anyObject.spec.containers : []'
+ - name: initContainers
+ expression: 'has(variables.anyObject.spec.initContainers) ? variables.anyObject.spec.initContainers : []'
+ - name: ephemeralContainers
+ expression: 'has(variables.anyObject.spec.ephemeralContainers) ? variables.anyObject.spec.ephemeralContainers : []'
+ - name: allowAllProfiles
+ expression: |
+ has(variables.params.allowedProfiles) && variables.params.allowedProfiles.exists(profile, profile == "*")
+ - name: exemptImagePrefixes
+ expression: |
+ !has(variables.params.exemptImages) ? [] :
+ variables.params.exemptImages.filter(image, image.endsWith("*")).map(image, string(image).replace("*", ""))
+ - name: exemptImageExplicit
+ expression: |
+ !has(variables.params.exemptImages) ? [] :
+ variables.params.exemptImages.filter(image, !image.endsWith("*"))
+ - name: exemptImages
+ expression: |
+ (variables.containers + variables.initContainers + variables.ephemeralContainers).filter(container,
+ container.image in variables.exemptImageExplicit ||
+ variables.exemptImagePrefixes.exists(exemption, string(container.image).startsWith(exemption))).map(container, container.image)
+ - name: unverifiedContainers
+ expression: |
+ (variables.containers + variables.initContainers + variables.ephemeralContainers).filter(container,
+ !variables.allowAllProfiles &&
+ !(container.image in variables.exemptImages))
+ - name: inputNonLocalHostProfiles
+ expression: |
+ variables.params.allowedProfiles.filter(profile, profile != "Localhost").map(profile, {"type": profile})
+ - name: inputLocalHostProfiles
+ expression: |
+ variables.params.allowedProfiles.exists(profile, profile == "Localhost") ? variables.params.allowedLocalhostFiles.map(file, {"type": "Localhost", "localHostProfile": string(file)}) : []
+ - name: inputAllowedProfiles
+ expression: |
+ variables.inputNonLocalHostProfiles + variables.inputLocalHostProfiles
+ - name: hasPodSeccomp
+ expression: |
+ has(variables.anyObject.spec.securityContext) && has(variables.anyObject.spec.securityContext.seccompProfile)
+ - name: podLocalHostProfile
+ expression: |
+ variables.hasPodSeccomp && has(variables.anyObject.spec.securityContext.seccompProfile.localhostProfile) ? variables.anyObject.spec.securityContext.seccompProfile.localhostProfile : ""
+ - name: podSecurityContextProfileType
+ expression: |
+ has(variables.hasPodSeccomp) && has(variables.anyObject.spec.securityContext.seccompProfile.type) ? variables.anyObject.spec.securityContext.seccompProfile.type
+ : ""
+ - name: podSecurityContextProfiles
+ expression: |
+ variables.unverifiedContainers.filter(container,
+ !(has(container.securityContext) && has(container.securityContext.seccompProfile)) &&
+ variables.hasPodSeccomp
+ ).map(container, {
+ "container" : container.name,
+ "profile" : dyn(variables.podSecurityContextProfileType),
+ "file" : variables.podLocalHostProfile,
+ "location" : dyn("pod securityContext"),
+ })
+ - name: containerSecurityContextProfiles
+ expression: |
+ variables.unverifiedContainers.filter(container,
+ has(container.securityContext) && has(container.securityContext.seccompProfile)
+ ).map(container, {
+ "container" : container.name,
+ "profile" : dyn(container.securityContext.seccompProfile.type),
+ "file" : has(container.securityContext.seccompProfile.localhostProfile) ? container.securityContext.seccompProfile.localhostProfile : dyn(""),
+ "location" : dyn("container securityContext"),
+ })
+ - name: containerProfilesMissing
+ expression: |
+ variables.unverifiedContainers.filter(container,
+ !(has(container.securityContext) && has(container.securityContext.seccompProfile)) &&
+ !variables.hasPodSeccomp
+ ).map(container, {
+ "container" : container.name,
+ "profile" : dyn("not configured"),
+ "file" : dyn(""),
+ "location" : dyn("no explicit profile found"),
+ })
+ - name: allContainerProfiles
+ expression: |
+ variables.podSecurityContextProfiles + variables.containerSecurityContextProfiles + variables.containerProfilesMissing
+ - name: badContainerProfilesWithoutFiles
+ expression: |
+ variables.allContainerProfiles.filter(container,
+ container.profile != "Localhost" &&
+ !variables.inputAllowedProfiles.exists(profile, profile.type == container.profile)
+ ).map(badProfile, "Seccomp profile '" + badProfile.profile + "' is not allowed for container '" + badProfile.container + "'. Found at: " + badProfile.location + ". Allowed profiles: " + variables.inputAllowedProfiles.map(profile, "{\"type\": \"" + profile.type + "\"" + (has(profile.localHostProfile) ? ", \"localHostProfile\": \"" + profile.localHostProfile + "\"}" : "}")).join(", "))
+ - name: badContainerProfilesWithFiles
+ expression: |
+ variables.allContainerProfiles.filter(container,
+ container.profile == "Localhost" &&
+ !variables.inputAllowedProfiles.exists(profile, profile.type == "Localhost" && (has(profile.localHostProfile) && (profile.localHostProfile == container.file || profile.localHostProfile == "*")))
+ ).map(badProfile, "Seccomp profile '" + badProfile.profile + "' With file '" + badProfile.file + "' is not allowed for container '" + badProfile.container + "'. Found at: " + badProfile.location + ". Allowed profiles: " + variables.inputAllowedProfiles.map(profile, "{\"type\": \"" + profile.type + "\"" + (has(profile.localHostProfile) ? ", \"localHostProfile\": \"" + profile.localHostProfile + "\"}" : "}")).join(", "))
+ validations:
+ - expression: 'size(variables.badContainerProfilesWithoutFiles) == 0'
+ messageExpression: |
+ variables.badContainerProfilesWithoutFiles.join(", ")
+ - expression: 'size(variables.badContainerProfilesWithFiles) == 0'
+ messageExpression: |
+ variables.badContainerProfilesWithFiles.join(", ")
+ - engine: Rego
+ source:
+ rego: |
+ package k8spspseccomp
+
+ import data.lib.exempt_container.is_exempt
+
+ violation[{"msg": msg}] {
+ not input_wildcard_allowed_profiles
+ allowed_profiles := get_allowed_profiles
+ container := input_containers[name]
+ not is_exempt(container)
+ result := get_profile(container)
+ not allowed_profile(result.profile, result.file, allowed_profiles)
+ msg := get_message(result.profile, result.file, name, result.location, allowed_profiles)
+ }
+
+ get_message(profile, _, name, location, allowed_profiles) = message {
+ profile != "Localhost"
+ message := sprintf("Seccomp profile '%v' is not allowed for container '%v'. Found at: %v. Allowed profiles: %v", [profile, name, location, allowed_profiles])
+ }
+
+ get_message(profile, file, name, location, allowed_profiles) = message {
+ profile == "Localhost"
+ message := sprintf("Seccomp profile '%v' with file '%v' is not allowed for container '%v'. Found at: %v. Allowed profiles: %v", [profile, file, name, location, allowed_profiles])
+ }
+
+ input_wildcard_allowed_profiles {
+ input.parameters.allowedProfiles[_] == "*"
+ }
+
+ input_wildcard_allowed_files {
+ input.parameters.allowedLocalhostFiles[_] == "*"
+ }
+
+ allowed_profile(_, _, _) {
+ input_wildcard_allowed_profiles
+ }
+
+ allowed_profile(profile, _, _) {
+ profile == "Localhost"
+ input_wildcard_allowed_files
+ }
+
+ # Simple allowed Profiles
+ allowed_profile(profile, _, allowed) {
+ profile != "Localhost"
+ allow_profile = allowed[_]
+ profile == allow_profile.type
+ }
+
+ # annotation localhost without wildcard
+ allowed_profile(profile, file, allowed) {
+ profile == "Localhost"
+ allow_profile = allowed[_]
+ allow_profile.type == "Localhost"
+ file == allow_profile.localHostProfile
+ }
+
+ # The profiles explicitly in the list
+ get_allowed_profiles[allowed] {
+ profile := input.parameters.allowedProfiles[_]
+ profile != "Localhost"
+ allowed := {"type": profile}
+ }
+
+ get_allowed_profiles[allowed] {
+ profile := input.parameters.allowedProfiles[_]
+ profile == "Localhost"
+ file := object.get(input.parameters, "allowedLocalhostFiles", [""])[_]
+ allowed := {"type": "Localhost", "localHostProfile": file}
+ }
+
+ # Container profile as defined in containers securityContext
+ get_profile(container) = {"profile": profile, "file": file, "location": location} {
+ has_securitycontext_container(container)
+ profile := container.securityContext.seccompProfile.type
+ file := object.get(container.securityContext.seccompProfile, "localhostProfile", "")
+ location := "container securityContext"
+ }
+
+ # Container profile as defined in pods securityContext
+ get_profile(container) = {"profile": profile, "file": file, "location": location} {
+ not has_securitycontext_container(container)
+ profile := input.review.object.spec.securityContext.seccompProfile.type
+ file := object.get(input.review.object.spec.securityContext.seccompProfile, "localhostProfile", "")
+ location := "pod securityContext"
+ }
+
+ # Container profile missing
+ get_profile(container) = {"profile": "not configured", "file": "", "location": "no explicit profile found"} {
+ not has_securitycontext_container(container)
+ not has_securitycontext_pod
+ }
+
+ has_securitycontext_pod {
+ input.review.object.spec.securityContext.seccompProfile
+ }
+
+ has_securitycontext_container(container) {
+ container.securityContext.seccompProfile
+ }
+
+ input_containers[container.name] = container {
+ container := input.review.object.spec.containers[_]
+ }
+
+ input_containers[container.name] = container {
+ container := input.review.object.spec.initContainers[_]
+ }
+
+ input_containers[container.name] = container {
+ container := input.review.object.spec.ephemeralContainers[_]
+ }
+ libs:
+ - |
+ package lib.exempt_container
+
+ is_exempt(container) {
+ exempt_images := object.get(object.get(input, "parameters", {}), "exemptImages", [])
+ img := container.image
+ exemption := exempt_images[_]
+ _matches_exemption(img, exemption)
+ }
+
+ _matches_exemption(img, exemption) {
+ not endswith(exemption, "*")
+ exemption == img
+ }
+
+ _matches_exemption(img, exemption) {
+ endswith(exemption, "*")
+ prefix := trim_suffix(exemption, "*")
+ startswith(img, prefix)
+ }
diff --git a/src/pod-security-policy/seccomp/constraint.tmpl b/src/pod-security-policy/seccomp/constraint.tmpl
index 7a5c95538..5fdd6d074 100644
--- a/src/pod-security-policy/seccomp/constraint.tmpl
+++ b/src/pod-security-policy/seccomp/constraint.tmpl
@@ -4,7 +4,7 @@ metadata:
name: k8spspseccomp
annotations:
metadata.gatekeeper.sh/title: "Seccomp"
- metadata.gatekeeper.sh/version: 1.0.1
+ metadata.gatekeeper.sh/version: 1.1.0
description: >-
Controls the seccomp profile used by containers. Corresponds to the
`seccomp.security.alpha.kubernetes.io/allowedProfileNames` annotation on
@@ -67,8 +67,14 @@ spec:
type: string
targets:
- target: admission.k8s.gatekeeper.sh
- rego: |
-{{ file.Read "src/pod-security-policy/seccomp/src.rego" | strings.Indent 8 | strings.TrimSuffix "\n" }}
- libs:
- - |
-{{ file.Read "src/pod-security-policy/seccomp/lib_exempt_container.rego" | strings.Indent 10 | strings.TrimSuffix "\n" }}
+ code:
+ - engine: K8sNativeValidation
+ source:
+{{ file.Read "src/pod-security-policy/seccomp/src.cel" | strings.Indent 10 | strings.TrimSuffix "\n" }}
+ - engine: Rego
+ source:
+ rego: |
+{{ file.Read "src/pod-security-policy/seccomp/src.rego" | strings.Indent 12 | strings.TrimSuffix "\n" }}
+ libs:
+ - |
+{{ file.Read "src/pod-security-policy/seccomp/lib_exempt_container.rego" | strings.Indent 14 | strings.TrimSuffix "\n" }}
diff --git a/src/pod-security-policy/seccomp/src.cel b/src/pod-security-policy/seccomp/src.cel
new file mode 100644
index 000000000..bdb8b218b
--- /dev/null
+++ b/src/pod-security-policy/seccomp/src.cel
@@ -0,0 +1,141 @@
+variables:
+- name: containers
+ expression: 'has(variables.anyObject.spec.containers) ? variables.anyObject.spec.containers : []'
+- name: initContainers
+ expression: 'has(variables.anyObject.spec.initContainers) ? variables.anyObject.spec.initContainers : []'
+- name: ephemeralContainers
+ expression: 'has(variables.anyObject.spec.ephemeralContainers) ? variables.anyObject.spec.ephemeralContainers : []'
+- name: allowAllProfiles
+ expression: |
+ has(variables.params.allowedProfiles) && variables.params.allowedProfiles.exists(profile, profile == "*")
+- name: exemptImagePrefixes
+ expression: |
+ !has(variables.params.exemptImages) ? [] :
+ variables.params.exemptImages.filter(image, image.endsWith("*")).map(image, string(image).replace("*", ""))
+- name: exemptImageExplicit
+ expression: |
+ !has(variables.params.exemptImages) ? [] :
+ variables.params.exemptImages.filter(image, !image.endsWith("*"))
+- name: exemptImages
+ expression: |
+ (variables.containers + variables.initContainers + variables.ephemeralContainers).filter(container,
+ container.image in variables.exemptImageExplicit ||
+ variables.exemptImagePrefixes.exists(exemption, string(container.image).startsWith(exemption))).map(container, container.image)
+- name: unverifiedContainers
+ expression: |
+ (variables.containers + variables.initContainers + variables.ephemeralContainers).filter(container,
+ !variables.allowAllProfiles &&
+ !(container.image in variables.exemptImages))
+- name: inputAllowedProfiles
+ expression: |
+ !has(variables.params.allowedProfiles) ? [] : variables.params.allowedProfiles
+- name: allowedLocalhostFiles
+ expression: |
+ has(variables.params.allowedLocalhostFiles) ? variables.params.allowedLocalhostFiles : []
+- name: allowedProfilesTranslation
+ expression: |
+ (variables.inputAllowedProfiles.filter(profile,
+ profile != "Localhost").map(profile, profile == "Unconfined" ? "unconfined" : profile)) +
+ (variables.inputAllowedProfiles.exists(profile, profile == "RuntimeDefault") ? ["runtime/default", "docker/default"] : [])
+- name: allowSecurityContextLocalhost
+ expression: |
+ variables.inputAllowedProfiles.exists(profile, profile == "Localhost")
+- name: derivedAllowedLocalhostFiles
+ expression: |
+ variables.allowSecurityContextLocalhost ? variables.params.allowedLocalhostFiles.map(file, "localhost/" + file) : []
+- name: localhostWildcardAllowed
+ expression: |
+ variables.inputAllowedProfiles.exists(profile, profile == "localhost/*") || variables.derivedAllowedLocalhostFiles.exists(profile, profile == "localhost/*")
+- name: allowedProfiles
+ expression: |
+ (variables.allowedProfilesTranslation + variables.derivedAllowedLocalhostFiles)
+- name: hasPodSeccomp
+ expression: |
+ has(variables.anyObject.spec.securityContext) && has(variables.anyObject.spec.securityContext.seccompProfile)
+- name: hasPodAnnotations
+ expression: |
+ has(variables.anyObject.metadata.annotations) && ("seccomp.security.alpha.kubernetes.io/pod" in variables.anyObject.metadata.annotations)
+- name: podAnnotationsProfiles
+ expression: |
+ variables.unverifiedContainers.filter(container,
+ !(has(container.securityContext) && has(container.securityContext.seccompProfile)) &&
+ !(has(variables.anyObject.metadata.annotations) && (("container.seccomp.security.alpha.kubernetes.io/" + container.name) in variables.anyObject.metadata.annotations)) &&
+ !variables.hasPodSeccomp &&
+ variables.hasPodAnnotations
+ ).map(container, {
+ "container" : container.name,
+ "profile" : variables.anyObject.metadata.annotations["seccomp.security.alpha.kubernetes.io/pod"],
+ "file" : dyn(""),
+ "location" : dyn("annotation seccomp.security.alpha.kubernetes.io/pod"),
+ })
+- name: containerAnnotationsProfiles
+ expression: |
+ variables.unverifiedContainers.filter(container,
+ !(has(container.securityContext) && has(container.securityContext.seccompProfile)) &&
+ !variables.hasPodSeccomp &&
+ has(variables.anyObject.metadata.annotations) && (("container.seccomp.security.alpha.kubernetes.io/" + container.name) in variables.anyObject.metadata.annotations)
+ ).map(container, {
+ "container" : container.name,
+ "profile" : variables.anyObject.metadata.annotations["container.seccomp.security.alpha.kubernetes.io/" + container.name],
+ "file" : dyn(""),
+ "location" : dyn("annotation container.seccomp.security.alpha.kubernetes.io/" + container.name),
+ })
+- name: podLocalHostProfile
+ expression: |
+ variables.hasPodSeccomp && has(variables.anyObject.spec.securityContext.seccompProfile.localhostProfile) ? variables.anyObject.spec.securityContext.seccompProfile.localhostProfile : ""
+- name: canonicalPodSecurityContextProfile
+ expression: |
+ has(variables.hasPodSeccomp) && has(variables.anyObject.spec.securityContext.seccompProfile.type) ?
+ (variables.anyObject.spec.securityContext.seccompProfile.type == "RuntimeDefault" ? (
+ variables.allowedProfiles.exists(profile, profile == "runtime/default") ? "runtime/default" : variables.allowedProfiles.exists(profile, profile == "docker/default") ? "docker/default" : "runtime/default") :
+ variables.anyObject.spec.securityContext.seccompProfile.type == "Unconfined" ? "unconfined" : variables.anyObject.spec.securityContext.seccompProfile.type == "Localhost" ? "localhost/" + variables.podLocalHostProfile : "")
+ : ""
+- name: podSecurityContextProfiles
+ expression: |
+ variables.unverifiedContainers.filter(container,
+ !(has(container.securityContext) && has(container.securityContext.seccompProfile)) &&
+ variables.hasPodSeccomp
+ ).map(container, {
+ "container" : container.name,
+ "profile" : dyn(variables.canonicalPodSecurityContextProfile),
+ "file" : variables.podLocalHostProfile,
+ "location" : dyn("pod securityContext"),
+ })
+- name: containerSecurityContextProfiles
+ expression: |
+ variables.unverifiedContainers.filter(container,
+ has(container.securityContext) && has(container.securityContext.seccompProfile)
+ ).map(container, {
+ "container" : container.name,
+ "profile" : dyn(has(container.securityContext.seccompProfile.type) ? (container.securityContext.seccompProfile.type == "RuntimeDefault" ? (
+ variables.allowedProfiles.exists(profile, profile == "runtime/default") ? "runtime/default" : variables.allowedProfiles.exists(profile, profile == "docker/default") ? "docker/default" : "runtime/default") :
+ container.securityContext.seccompProfile.type == "Unconfined" ? "unconfined" : container.securityContext.seccompProfile.type == "Localhost" ? "localhost/" + container.securityContext.seccompProfile.localhostProfile : "")
+ : ""),
+ "file" : has(container.securityContext.seccompProfile.localhostProfile) ? container.securityContext.seccompProfile.localhostProfile : dyn(""),
+ "location" : dyn("container securityContext"),
+ })
+- name: containerProfilesMissing
+ expression: |
+ variables.unverifiedContainers.filter(container,
+ !(has(container.securityContext) && has(container.securityContext.seccompProfile)) &&
+ !(has(variables.anyObject.metadata.annotations) && (("container.seccomp.security.alpha.kubernetes.io/" + container.name) in variables.anyObject.metadata.annotations)) &&
+ !variables.hasPodSeccomp &&
+ !variables.hasPodAnnotations
+ ).map(container, {
+ "container" : container.name,
+ "profile" : dyn("not configured"),
+ "file" : dyn(""),
+ "location" : dyn("no explicit profile found"),
+ })
+- name: allContainerProfiles
+ expression: |
+ variables.podAnnotationsProfiles + variables.containerAnnotationsProfiles + variables.podSecurityContextProfiles + variables.containerSecurityContextProfiles + variables.containerProfilesMissing
+- name: badContainerProfiles
+ expression: |
+ variables.allContainerProfiles.filter(badContainerProfile,
+ !((badContainerProfile.profile in variables.allowedProfiles) || (badContainerProfile.profile.startsWith("localhost/") && variables.localhostWildcardAllowed))
+ ).map(badProfile, "Seccomp profile '" + badProfile.profile + "' is not allowed for container '" + badProfile.container + "'. Found at: " + badProfile.location + ". Allowed profiles: " + variables.allowedProfiles.join(", "))
+validations:
+- expression: 'size(variables.badContainerProfiles) == 0'
+ messageExpression: |
+ variables.badContainerProfiles.join(", ")
diff --git a/src/pod-security-policy/seccomp/src.rego b/src/pod-security-policy/seccomp/src.rego
index 61185ddb9..d06a271d1 100644
--- a/src/pod-security-policy/seccomp/src.rego
+++ b/src/pod-security-policy/seccomp/src.rego
@@ -6,18 +6,6 @@ container_annotation_key_prefix = "container.seccomp.security.alpha.kubernetes.i
pod_annotation_key = "seccomp.security.alpha.kubernetes.io/pod"
-naming_translation = {
- # securityContext -> annotation
- "RuntimeDefault": ["runtime/default", "docker/default"],
- "Unconfined": ["unconfined"],
- "Localhost": ["localhost"],
- # annotation -> securityContext
- "runtime/default": ["RuntimeDefault"],
- "docker/default": ["RuntimeDefault"],
- "unconfined": ["Unconfined"],
- "localhost": ["Localhost"],
-}
-
violation[{"msg": msg}] {
not input_wildcard_allowed_profiles
allowed_profiles := get_allowed_profiles
@@ -29,15 +17,9 @@ violation[{"msg": msg}] {
}
get_message(profile, _, name, location, allowed_profiles) = message {
- not profile == "Localhost"
message := sprintf("Seccomp profile '%v' is not allowed for container '%v'. Found at: %v. Allowed profiles: %v", [profile, name, location, allowed_profiles])
}
-get_message(profile, file, name, location, allowed_profiles) = message {
- profile == "Localhost"
- message := sprintf("Seccomp profile '%v' with file '%v' is not allowed for container '%v'. Found at: %v. Allowed profiles: %v", [profile, file, name, location, allowed_profiles])
-}
-
input_wildcard_allowed_profiles {
input.parameters.allowedProfiles[_] == "*"
}
@@ -52,23 +34,7 @@ input_wildcard_allowed_files {
# Simple allowed Profiles
allowed_profile(profile, _, allowed) {
- not startswith(lower(profile), "localhost")
- profile == allowed[_]
-}
-
-# seccomp Localhost without wildcard
-allowed_profile(profile, file, allowed) {
- profile == "Localhost"
- not input_wildcard_allowed_files
- profile == allowed[_]
- allowed_files := {x | x := object.get(input.parameters, "allowedLocalhostFiles", [])[_]} | get_annotation_localhost_files
- file == allowed_files[_]
-}
-
-# seccomp Localhost with wildcard
-allowed_profile(profile, _, allowed) {
- profile == "Localhost"
- input_wildcard_allowed_files
+ not startswith(profile, "localhost/")
profile == allowed[_]
}
@@ -84,38 +50,17 @@ allowed_profile(profile, _, allowed) {
profile == allowed[_]
}
-# Localhost files from annotation scheme
-get_annotation_localhost_files[file] {
- profile := input.parameters.allowedProfiles[_]
- startswith(profile, "localhost/")
- file := replace(profile, "localhost/", "")
-}
-
# The profiles explicitly in the list
get_allowed_profiles[allowed] {
allowed := input.parameters.allowedProfiles[_]
}
-# The simply translated profiles
-get_allowed_profiles[allowed] {
- profile := input.parameters.allowedProfiles[_]
- not startswith(lower(profile), "localhost")
- allowed := naming_translation[profile][_]
-}
-
# Seccomp Localhost to annotation translation
get_allowed_profiles[allowed] {
profile := input.parameters.allowedProfiles[_]
- profile == "Localhost"
+ not contains(profile, "/")
file := object.get(input.parameters, "allowedLocalhostFiles", [])[_]
- allowed := sprintf("%v/%v", [naming_translation[profile][_], file])
-}
-
-# Annotation localhost to Seccomp translation
-get_allowed_profiles[allowed] {
- profile := input.parameters.allowedProfiles[_]
- startswith(profile, "localhost")
- allowed := naming_translation.localhost[_]
+ allowed := canonicalize_seccomp_profile({"type": profile, "localhostProfile": file}, "")[_]
}
# Container profile as defined in pod annotation
@@ -140,7 +85,7 @@ get_profile(container) = {"profile": profile, "file": "", "location": location}
# Container profile as defined in pods securityContext
get_profile(container) = {"profile": profile, "file": file, "location": location} {
not has_securitycontext_container(container)
- profile := input.review.object.spec.securityContext.seccompProfile.type
+ profile := canonicalize_seccomp_profile(input.review.object.spec.securityContext.seccompProfile, canonicalize_runtime_default_profile)[_]
file := object.get(input.review.object.spec.securityContext.seccompProfile, "localhostProfile", "")
location := "pod securityContext"
}
@@ -148,17 +93,17 @@ get_profile(container) = {"profile": profile, "file": file, "location": location
# Container profile as defined in containers securityContext
get_profile(container) = {"profile": profile, "file": file, "location": location} {
has_securitycontext_container(container)
- profile := container.securityContext.seccompProfile.type
+ profile := canonicalize_seccomp_profile(container.securityContext.seccompProfile, canonicalize_runtime_default_profile)[_]
file := object.get(container.securityContext.seccompProfile, "localhostProfile", "")
location := "container securityContext"
}
# Container profile missing
get_profile(container) = {"profile": "not configured", "file": "", "location": "no explicit profile found"} {
+ not has_securitycontext_container(container)
+ not has_securitycontext_pod
not has_annotation(get_container_annotation_key(container.name))
not has_annotation(pod_annotation_key)
- not has_securitycontext_pod
- not has_securitycontext_container(container)
}
has_annotation(annotation) {
@@ -188,3 +133,29 @@ input_containers[container.name] = container {
input_containers[container.name] = container {
container := input.review.object.spec.ephemeralContainers[_]
}
+
+canonicalize_runtime_default_profile() = out {
+ "runtime/default" == input.parameters.allowedProfiles[_]
+ out := "runtime/default"
+} else = out {
+ "docker/default" == input.parameters.allowedProfiles[_]
+ out := "docker/default"
+} else = out {
+ out := "runtime/default"
+}
+
+canonicalize_seccomp_profile(profile, def) = out {
+ profile.type == "RuntimeDefault"
+ def == ""
+ out := ["runtime/default", "docker/default"]
+} else = out {
+ profile.type == "RuntimeDefault"
+ def != ""
+ out := [def]
+} else = out {
+ profile.type == "Localhost"
+ out := [sprintf("localhost/%s", [profile.localhostProfile])]
+} else = out {
+ profile.type == "Unconfined"
+ out := ["unconfined"]
+}
diff --git a/src/pod-security-policy/seccomp/src_test.rego b/src/pod-security-policy/seccomp/src_test.rego
index 161afa359..74bba46c9 100644
--- a/src/pod-security-policy/seccomp/src_test.rego
+++ b/src/pod-security-policy/seccomp/src_test.rego
@@ -417,7 +417,7 @@ test_input_both_seccomp_pod_context_container_annotation_multiple_mixed {
test_translation_seccomp_allowed_annotation_all {
inp := {"parameters": input_parameters_annotation}
output := get_allowed_profiles with input as inp
- output == allowed_full_translated
+ output == allowed_full_translated_annotation_style
}
test_translation_seccomp_allowed_context_all {
@@ -645,3 +645,11 @@ allowed_full_translated = {
"RuntimeDefault", "docker/default", "runtime/default",
"Unconfined", "unconfined",
}
+
+allowed_full_translated_annotation_style = {
+ "runtime/default",
+ "docker/default",
+ "localhost/profile1.json",
+ "localhost/profile2.json",
+ "unconfined",
+}
diff --git a/src/pod-security-policy/seccompv2/constraint.tmpl b/src/pod-security-policy/seccompv2/constraint.tmpl
new file mode 100644
index 000000000..d33f99ea4
--- /dev/null
+++ b/src/pod-security-policy/seccompv2/constraint.tmpl
@@ -0,0 +1,73 @@
+apiVersion: templates.gatekeeper.sh/v1
+kind: ConstraintTemplate
+metadata:
+ name: k8spspseccompv2
+ annotations:
+ metadata.gatekeeper.sh/title: "Seccomp V2"
+ metadata.gatekeeper.sh/version: 1.0.0
+ description: >-
+ Controls the seccomp profile used by containers. Corresponds to the
+ `securityContext.seccompProfile` field. Security contexts from the annotation is not considered as Kubernetes no longer reads security contexts from the annotation.
+spec:
+ crd:
+ spec:
+ names:
+ kind: K8sPSPSeccompV2
+ validation:
+ # Schema for the `parameters` field
+ openAPIV3Schema:
+ type: object
+ description: >-
+ Controls the seccomp profile used by containers. Corresponds to the
+ `securityContext.seccompProfile` field. Security contexts from the annotation is not considered as Kubernetes no longer reads security contexts from the annotation.
+ properties:
+ exemptImages:
+ description: >-
+ Any container that uses an image that matches an entry in this list will be excluded
+ from enforcement. Prefix-matching can be signified with `*`. For example: `my-image-*`.
+
+ It is recommended that users use the fully-qualified Docker image name (e.g. start with a domain name)
+ in order to avoid unexpectedly exempting images from an untrusted repository.
+ type: array
+ items:
+ type: string
+ allowedProfiles:
+ type: array
+ description: >-
+ An array of allowed profile values for seccomp on Pods/Containers.
+
+ Can use the securityContext naming scheme: `RuntimeDefault`, `Unconfined`
+ and/or `Localhost`. For securityContext `Localhost`, use the parameter `allowedLocalhostFiles`
+ to list the allowed profile JSON files.
+
+ The policy code will translate between the two schemes so it is not necessary to use both.
+
+ Putting a `*` in this array allows all Profiles to be used.
+
+ This field is required since with an empty list this policy will block all workloads.
+ items:
+ type: string
+ allowedLocalhostFiles:
+ type: array
+ description: >-
+ When using securityContext naming scheme for seccomp and including `Localhost` this array holds
+ the allowed profile JSON files.
+
+ Putting a `*` in this array will allows all JSON files to be used.
+
+ This field is required to allow `Localhost` in securityContext as with an empty list it will block.
+ items:
+ type: string
+ targets:
+ - target: admission.k8s.gatekeeper.sh
+ code:
+ - engine: K8sNativeValidation
+ source:
+{{ file.Read "src/pod-security-policy/seccompv2/src.cel" | strings.Indent 10 | strings.TrimSuffix "\n" }}
+ - engine: Rego
+ source:
+ rego: |
+{{ file.Read "src/pod-security-policy/seccompv2/src.rego" | strings.Indent 12 | strings.TrimSuffix "\n" }}
+ libs:
+ - |
+{{ file.Read "src/pod-security-policy/seccompv2/lib_exempt_container.rego" | strings.Indent 14 | strings.TrimSuffix "\n" }}
diff --git a/src/pod-security-policy/seccompv2/lib_exempt_container.rego b/src/pod-security-policy/seccompv2/lib_exempt_container.rego
new file mode 100644
index 000000000..c483416be
--- /dev/null
+++ b/src/pod-security-policy/seccompv2/lib_exempt_container.rego
@@ -0,0 +1,19 @@
+package lib.exempt_container
+
+is_exempt(container) {
+ exempt_images := object.get(object.get(input, "parameters", {}), "exemptImages", [])
+ img := container.image
+ exemption := exempt_images[_]
+ _matches_exemption(img, exemption)
+}
+
+_matches_exemption(img, exemption) {
+ not endswith(exemption, "*")
+ exemption == img
+}
+
+_matches_exemption(img, exemption) {
+ endswith(exemption, "*")
+ prefix := trim_suffix(exemption, "*")
+ startswith(img, prefix)
+}
diff --git a/src/pod-security-policy/seccompv2/src.cel b/src/pod-security-policy/seccompv2/src.cel
new file mode 100644
index 000000000..c2847062a
--- /dev/null
+++ b/src/pod-security-policy/seccompv2/src.cel
@@ -0,0 +1,101 @@
+variables:
+- name: containers
+ expression: 'has(variables.anyObject.spec.containers) ? variables.anyObject.spec.containers : []'
+- name: initContainers
+ expression: 'has(variables.anyObject.spec.initContainers) ? variables.anyObject.spec.initContainers : []'
+- name: ephemeralContainers
+ expression: 'has(variables.anyObject.spec.ephemeralContainers) ? variables.anyObject.spec.ephemeralContainers : []'
+- name: allowAllProfiles
+ expression: |
+ has(variables.params.allowedProfiles) && variables.params.allowedProfiles.exists(profile, profile == "*")
+- name: exemptImagePrefixes
+ expression: |
+ !has(variables.params.exemptImages) ? [] :
+ variables.params.exemptImages.filter(image, image.endsWith("*")).map(image, string(image).replace("*", ""))
+- name: exemptImageExplicit
+ expression: |
+ !has(variables.params.exemptImages) ? [] :
+ variables.params.exemptImages.filter(image, !image.endsWith("*"))
+- name: exemptImages
+ expression: |
+ (variables.containers + variables.initContainers + variables.ephemeralContainers).filter(container,
+ container.image in variables.exemptImageExplicit ||
+ variables.exemptImagePrefixes.exists(exemption, string(container.image).startsWith(exemption))).map(container, container.image)
+- name: unverifiedContainers
+ expression: |
+ (variables.containers + variables.initContainers + variables.ephemeralContainers).filter(container,
+ !variables.allowAllProfiles &&
+ !(container.image in variables.exemptImages))
+- name: inputNonLocalHostProfiles
+ expression: |
+ variables.params.allowedProfiles.filter(profile, profile != "Localhost").map(profile, {"type": profile})
+- name: inputLocalHostProfiles
+ expression: |
+ variables.params.allowedProfiles.exists(profile, profile == "Localhost") ? variables.params.allowedLocalhostFiles.map(file, {"type": "Localhost", "localHostProfile": string(file)}) : []
+- name: inputAllowedProfiles
+ expression: |
+ variables.inputNonLocalHostProfiles + variables.inputLocalHostProfiles
+- name: hasPodSeccomp
+ expression: |
+ has(variables.anyObject.spec.securityContext) && has(variables.anyObject.spec.securityContext.seccompProfile)
+- name: podLocalHostProfile
+ expression: |
+ variables.hasPodSeccomp && has(variables.anyObject.spec.securityContext.seccompProfile.localhostProfile) ? variables.anyObject.spec.securityContext.seccompProfile.localhostProfile : ""
+- name: podSecurityContextProfileType
+ expression: |
+ has(variables.hasPodSeccomp) && has(variables.anyObject.spec.securityContext.seccompProfile.type) ? variables.anyObject.spec.securityContext.seccompProfile.type
+ : ""
+- name: podSecurityContextProfiles
+ expression: |
+ variables.unverifiedContainers.filter(container,
+ !(has(container.securityContext) && has(container.securityContext.seccompProfile)) &&
+ variables.hasPodSeccomp
+ ).map(container, {
+ "container" : container.name,
+ "profile" : dyn(variables.podSecurityContextProfileType),
+ "file" : variables.podLocalHostProfile,
+ "location" : dyn("pod securityContext"),
+ })
+- name: containerSecurityContextProfiles
+ expression: |
+ variables.unverifiedContainers.filter(container,
+ has(container.securityContext) && has(container.securityContext.seccompProfile)
+ ).map(container, {
+ "container" : container.name,
+ "profile" : dyn(container.securityContext.seccompProfile.type),
+ "file" : has(container.securityContext.seccompProfile.localhostProfile) ? container.securityContext.seccompProfile.localhostProfile : dyn(""),
+ "location" : dyn("container securityContext"),
+ })
+- name: containerProfilesMissing
+ expression: |
+ variables.unverifiedContainers.filter(container,
+ !(has(container.securityContext) && has(container.securityContext.seccompProfile)) &&
+ !variables.hasPodSeccomp
+ ).map(container, {
+ "container" : container.name,
+ "profile" : dyn("not configured"),
+ "file" : dyn(""),
+ "location" : dyn("no explicit profile found"),
+ })
+- name: allContainerProfiles
+ expression: |
+ variables.podSecurityContextProfiles + variables.containerSecurityContextProfiles + variables.containerProfilesMissing
+- name: badContainerProfilesWithoutFiles
+ expression: |
+ variables.allContainerProfiles.filter(container,
+ container.profile != "Localhost" &&
+ !variables.inputAllowedProfiles.exists(profile, profile.type == container.profile)
+ ).map(badProfile, "Seccomp profile '" + badProfile.profile + "' is not allowed for container '" + badProfile.container + "'. Found at: " + badProfile.location + ". Allowed profiles: " + variables.inputAllowedProfiles.map(profile, "{\"type\": \"" + profile.type + "\"" + (has(profile.localHostProfile) ? ", \"localHostProfile\": \"" + profile.localHostProfile + "\"}" : "}")).join(", "))
+- name: badContainerProfilesWithFiles
+ expression: |
+ variables.allContainerProfiles.filter(container,
+ container.profile == "Localhost" &&
+ !variables.inputAllowedProfiles.exists(profile, profile.type == "Localhost" && (has(profile.localHostProfile) && (profile.localHostProfile == container.file || profile.localHostProfile == "*")))
+ ).map(badProfile, "Seccomp profile '" + badProfile.profile + "' With file '" + badProfile.file + "' is not allowed for container '" + badProfile.container + "'. Found at: " + badProfile.location + ". Allowed profiles: " + variables.inputAllowedProfiles.map(profile, "{\"type\": \"" + profile.type + "\"" + (has(profile.localHostProfile) ? ", \"localHostProfile\": \"" + profile.localHostProfile + "\"}" : "}")).join(", "))
+validations:
+- expression: 'size(variables.badContainerProfilesWithoutFiles) == 0'
+ messageExpression: |
+ variables.badContainerProfilesWithoutFiles.join(", ")
+- expression: 'size(variables.badContainerProfilesWithFiles) == 0'
+ messageExpression: |
+ variables.badContainerProfilesWithFiles.join(", ")
diff --git a/src/pod-security-policy/seccompv2/src.rego b/src/pod-security-policy/seccompv2/src.rego
new file mode 100644
index 000000000..eee2ee037
--- /dev/null
+++ b/src/pod-security-policy/seccompv2/src.rego
@@ -0,0 +1,111 @@
+package k8spspseccomp
+
+import data.lib.exempt_container.is_exempt
+
+violation[{"msg": msg}] {
+ not input_wildcard_allowed_profiles
+ allowed_profiles := get_allowed_profiles
+ container := input_containers[name]
+ not is_exempt(container)
+ result := get_profile(container)
+ not allowed_profile(result.profile, result.file, allowed_profiles)
+ msg := get_message(result.profile, result.file, name, result.location, allowed_profiles)
+}
+
+get_message(profile, _, name, location, allowed_profiles) = message {
+ profile != "Localhost"
+ message := sprintf("Seccomp profile '%v' is not allowed for container '%v'. Found at: %v. Allowed profiles: %v", [profile, name, location, allowed_profiles])
+}
+
+get_message(profile, file, name, location, allowed_profiles) = message {
+ profile == "Localhost"
+ message := sprintf("Seccomp profile '%v' with file '%v' is not allowed for container '%v'. Found at: %v. Allowed profiles: %v", [profile, file, name, location, allowed_profiles])
+}
+
+input_wildcard_allowed_profiles {
+ input.parameters.allowedProfiles[_] == "*"
+}
+
+input_wildcard_allowed_files {
+ input.parameters.allowedLocalhostFiles[_] == "*"
+}
+
+allowed_profile(_, _, _) {
+ input_wildcard_allowed_profiles
+}
+
+allowed_profile(profile, _, _) {
+ profile == "Localhost"
+ input_wildcard_allowed_files
+}
+
+# Simple allowed Profiles
+allowed_profile(profile, _, allowed) {
+ profile != "Localhost"
+ allow_profile = allowed[_]
+ profile == allow_profile.type
+}
+
+# annotation localhost without wildcard
+allowed_profile(profile, file, allowed) {
+ profile == "Localhost"
+ allow_profile = allowed[_]
+ allow_profile.type == "Localhost"
+ file == allow_profile.localHostProfile
+}
+
+# The profiles explicitly in the list
+get_allowed_profiles[allowed] {
+ profile := input.parameters.allowedProfiles[_]
+ profile != "Localhost"
+ allowed := {"type": profile}
+}
+
+get_allowed_profiles[allowed] {
+ profile := input.parameters.allowedProfiles[_]
+ profile == "Localhost"
+ file := object.get(input.parameters, "allowedLocalhostFiles", [""])[_]
+ allowed := {"type": "Localhost", "localHostProfile": file}
+}
+
+# Container profile as defined in containers securityContext
+get_profile(container) = {"profile": profile, "file": file, "location": location} {
+ has_securitycontext_container(container)
+ profile := container.securityContext.seccompProfile.type
+ file := object.get(container.securityContext.seccompProfile, "localhostProfile", "")
+ location := "container securityContext"
+}
+
+# Container profile as defined in pods securityContext
+get_profile(container) = {"profile": profile, "file": file, "location": location} {
+ not has_securitycontext_container(container)
+ profile := input.review.object.spec.securityContext.seccompProfile.type
+ file := object.get(input.review.object.spec.securityContext.seccompProfile, "localhostProfile", "")
+ location := "pod securityContext"
+}
+
+# Container profile missing
+get_profile(container) = {"profile": "not configured", "file": "", "location": "no explicit profile found"} {
+ not has_securitycontext_container(container)
+ not has_securitycontext_pod
+}
+
+has_securitycontext_pod {
+ input.review.object.spec.securityContext.seccompProfile
+}
+
+has_securitycontext_container(container) {
+ container.securityContext.seccompProfile
+}
+
+input_containers[container.name] = container {
+ container := input.review.object.spec.containers[_]
+}
+
+input_containers[container.name] = container {
+ container := input.review.object.spec.initContainers[_]
+}
+
+input_containers[container.name] = container {
+ container := input.review.object.spec.ephemeralContainers[_]
+}
diff --git a/src/pod-security-policy/seccompv2/src_test.rego b/src/pod-security-policy/seccompv2/src_test.rego
new file mode 100644
index 000000000..372815b36
--- /dev/null
+++ b/src/pod-security-policy/seccompv2/src_test.rego
@@ -0,0 +1,361 @@
+package k8spspseccomp
+
+# securityContext based seccomp with containers
+
+test_input_seccomp_allowed_in_list {
+ inp := {"review": get_object({}, context_runtimedefault, single_container, {}), "parameters": input_parameter_in_list}
+ results := violation with input as inp
+ count(results) == 0
+}
+
+test_input_seccomp_allowed_all {
+ inp := {"review": get_object({}, context_runtimedefault, single_container, {}), "parameters": input_parameters_wildcard}
+ results := violation with input as inp
+ count(results) == 0
+}
+
+test_input_seccomp_container_allowed_all {
+ inp := {"review": get_object({}, {}, single_container_sc, {}), "parameters": input_parameters_wildcard}
+ results := violation with input as inp
+ count(results) == 0
+}
+
+test_input_seccomp_container_allowed_in_list {
+ inp := {"review": get_object({}, {}, single_container_sc, {}), "parameters": input_parameter_in_list}
+ results := violation with input as inp
+ count(results) == 0
+}
+
+test_input_seccomp_containers_allowed_in_list {
+ inp := {"review": get_object({}, {}, multiple_containers_sc, {}), "parameters": input_parameter_in_list}
+ results := violation with input as inp
+ count(results) == 0
+}
+
+test_input_seccomp_containers_allowed_in_list_localhost {
+ inp := {"review": get_object({}, {}, single_container_sc_localhost, {}), "parameters": input_parameters_in_list_locahost_file}
+ results := violation with input as inp
+ count(results) == 0
+}
+
+test_input_seccomp_containers_allowed_in_list_multiple {
+ inp := {"review": get_object({}, {}, multiple_containers_sc_mixed, {}), "parameters": input_parameters_in_list}
+ results := violation with input as inp
+ count(results) == 0
+}
+
+test_input_seccomp_not_allowed_not_in_list {
+ inp := {"review": get_object({}, context_runtimedefault, single_container, {}), "parameters": input_parameters_not_in_list}
+ results := violation with input as inp
+ count(results) == 1
+}
+
+test_input_seccomp_empty_parameters {
+ inp := {"review": get_object({}, context_runtimedefault, single_container, {}), "parameters": input_parameters_empty}
+ results := violation with input as inp
+ count(results) == 1
+}
+
+test_input_seccomp_pod_localhost_allowed_wrong_file {
+ inp := {"review": get_object({}, context_localhost, single_container, {}), "parameters": input_parameters_sc}
+ results := violation with input as inp
+ count(results) == 1
+}
+
+test_input_seccomp_pod_localhost_allowed_no_specified_file {
+ inp := {"review": get_object({}, context_localhost, single_container, {}), "parameters": input_parameters_sc_localhost_no_file}
+ results := violation with input as inp
+ count(results) == 1
+}
+
+test_input_seccomp_containers_mixed {
+ inp := {"review": get_object({}, {}, multiple_containers_sc_mixed, {}), "parameters": input_parameter_in_list}
+ results := violation with input as inp
+ count(results) == 1
+}
+
+test_input_seccomp_containers_mixed_missing {
+ inp := {"review": get_object({}, {}, multiple_containers_sc_missing, {}), "parameters": input_parameter_in_list}
+ results := violation with input as inp
+ count(results) == 1
+}
+
+test_input_seccomp_container_not_allowed_not_in_list {
+ inp := {"review": get_object({}, {}, single_container_sc, {}), "parameters": input_parameters_not_in_list}
+ results := violation with input as inp
+ count(results) == 1
+}
+
+test_input_seccomp_containers_not_allowed_not_in_list {
+ inp := {"review": get_object({}, {}, multiple_containers_sc, {}), "parameters": input_parameters_not_in_list}
+ results := violation with input as inp
+ count(results) == 2
+}
+
+test_input_seccomp_not_allowed_multiple_not_configured {
+ inp := {"review": get_object({}, {}, multiple_containers, {}), "parameters": input_parameter_in_list}
+ results := violation with input as inp
+ count(results) == 2
+}
+
+# securityContext based seccomp with pod
+
+test_input_seccomp_pod_multiple_allowed_all {
+ inp := {"review": get_object({}, context_runtimedefault, multiple_containers, {}), "parameters": input_parameters_wildcard}
+ results := violation with input as inp
+ count(results) == 0
+}
+
+test_input_seccomp_pod_localhost_allowed_both_wildcard_file {
+ inp := {"review": get_object({}, context_localhost, single_container, {}), "parameters": input_parameters_localhost_wildcard_both}
+ results := violation with input as inp
+ count(results) == 0
+}
+
+test_input_seccomp_pod_container {
+ inp := {"review": get_object({}, context_runtimedefault, multiple_containers_sc_missing, {}), "parameters": input_parameter_in_list}
+ results := violation with input as inp
+ count(results) == 0
+}
+
+test_input_seccomp_pod_container_both_allowed {
+ inp := {"review": get_object({}, context_runtimedefault, multiple_containers_sc_missing, {}), "parameters": input_parameters_in_list}
+ results := violation with input as inp
+ count(results) == 0
+}
+
+test_input_seccomp_pod_container_mixed_not_allowed_but_exempt {
+ inp := {"review": get_object({}, context_runtimedefault, single_container, {}), "parameters": input_parameters_exempt}
+ results := violation with input as inp
+ count(results) == 0
+}
+
+test_input_seccomp_pod_multiple_allowed_in_list {
+ inp := {"review": get_object({}, context_runtimedefault, multiple_containers, {}), "parameters": input_parameter_in_list}
+ results := violation with input as inp
+ count(results) == 0
+}
+
+test_input_seccomp_pod_localhost_allowed_wildcard_file {
+ inp := {"review": get_object({}, context_localhost, single_container, {}), "parameters": input_parameters_sc_localhost_wildcard_file}
+ results := violation with input as inp
+ count(results) == 0
+}
+
+test_input_seccomp_pod_multiple_empty_parameters {
+ inp := {"review": get_object({}, context_runtimedefault, multiple_containers, {}), "parameters": input_parameters_empty}
+ results := violation with input as inp
+ count(results) == 2
+}
+
+test_input_seccomp_pod_multiple_not_allowed_not_in_list {
+ inp := {"review": get_object({}, context_runtimedefault, multiple_containers, {}), "parameters": input_parameters_not_in_list}
+ results := violation with input as inp
+ count(results) == 2
+}
+
+test_input_seccomp_pod_container_not_allowed {
+ inp := {"review": get_object({}, context_runtimedefault, multiple_containers_sc_missing, {}), "parameters": input_parameters_not_in_list}
+ results := violation with input as inp
+ count(results) == 2
+}
+
+test_input_seccomp_pod_container_mixed_allowed {
+ inp := {"review": get_object({}, context_localhost, multiple_containers_sc_missing, {}), "parameters": input_parameter_in_list}
+ results := violation with input as inp
+ count(results) == 1
+}
+
+test_input_seccomp_pod_container_mixed_not_allowed {
+ inp := {"review": get_object({}, context_localhost, multiple_containers_sc_missing, {}), "parameters": input_parameters_not_in_list}
+ results := violation with input as inp
+ count(results) == 2
+}
+
+# securityContext based seccomp with init containers
+test_input_seccomp_pod_initcontainer_both_allowed {
+ inp := {"review": get_object({}, context_runtimedefault, {}, multiple_containers_sc_missing), "parameters": input_parameters_in_list}
+ results := violation with input as inp
+ count(results) == 0
+}
+
+test_input_seccomp_pod_initcontainer_mixed_allowed {
+ inp := {"review": get_object({}, context_localhost, {}, multiple_containers_sc_missing), "parameters": input_parameter_in_list}
+ results := violation with input as inp
+ count(results) == 1
+}
+
+test_input_seccomp_pod_initcontainer_mixed_not_allowed {
+ inp := {"review": get_object({}, context_localhost, {}, multiple_containers_sc_missing), "parameters": input_parameters_not_in_list}
+ results := violation with input as inp
+ count(results) == 2
+}
+
+# Localhost seccomp profile build
+
+test_translation_seccomp_allowed_context_localhost_wildcard_file {
+ inp := {"parameters": input_parameters_localhost_wildcard_both}
+ output := get_allowed_profiles with input as inp
+ output == {{"type": "Localhost", "localHostProfile": "*"}}
+}
+
+test_translation_seccomp_allowed_context_localhost_no_file {
+ inp := {"parameters": input_parameters_sc_localhost_no_file}
+ output := get_allowed_profiles with input as inp
+ output == {{"localHostProfile": "", "type": "Localhost"}}
+}
+
+test_translation_seccomp_allowed_context_localhost_with_file {
+ inp := {"parameters": input_parameters_sc_localhost_with_file}
+ output := get_allowed_profiles with input as inp
+ output == {{"type": "Localhost", "localHostProfile": "profile.json"}}
+}
+
+test_translation_seccomp_allowed_context_mixed {
+ inp := {"parameters": input_parameters_in_list}
+ output := get_allowed_profiles with input as inp
+ output == {{"type": "Localhost", "localHostProfile": "profile.json"}, {"type": "RuntimeDefault"}}
+}
+
+# Create Review Object
+get_object(annotations, podcontext, containers, initcontainers) = {"object": {
+ "metadata": {
+ "name": "nginx",
+ "annotations": annotations,
+ },
+ "spec": {
+ "containers": containers,
+ "initContainers": initcontainers,
+ "securityContext": podcontext,
+ },
+}}
+
+# Test Containers
+single_container = [{
+ "name": "nginx",
+ "image": "nginx",
+}]
+
+multiple_containers = [
+ {
+ "name": "nginx",
+ "image": "nginx",
+ },
+ {
+ "name": "nginx2",
+ "image": "nginx",
+ },
+]
+
+single_container_sc = [{
+ "name": "nginx",
+ "image": "nginx",
+ "securityContext": context_runtimedefault,
+}]
+
+single_container_sc_localhost = [{
+ "name": "nginx",
+ "image": "nginx",
+ "securityContext": context_localhost,
+}]
+
+multiple_containers_sc = [
+ {
+ "name": "nginx",
+ "image": "nginx",
+ "securityContext": context_runtimedefault,
+ },
+ {
+ "name": "nginx2",
+ "image": "nginx",
+ "securityContext": context_runtimedefault,
+ },
+]
+
+multiple_containers_sc_mixed = [
+ {
+ "name": "nginx",
+ "image": "nginx",
+ "securityContext": context_runtimedefault,
+ },
+ {
+ "name": "nginx2",
+ "image": "nginx",
+ "securityContext": context_localhost,
+ },
+]
+
+multiple_containers_sc_missing = [
+ {
+ "name": "nginx",
+ "image": "nginx",
+ "securityContext": context_runtimedefault,
+ },
+ {
+ "name": "nginx2",
+ "image": "nginx",
+ },
+]
+
+# Test securityContexts
+context_localhost = {"seccompProfile": {"type": "Localhost", "localhostProfile": "profile.json"}}
+
+context_runtimedefault = {"seccompProfile": {"type": "RuntimeDefault"}}
+
+# Test Parameters
+input_parameters_empty = {"allowedProfiles": []}
+
+input_parameters_wildcard = {"allowedProfiles": ["*"]}
+
+input_parameter_in_list = {"allowedProfiles": [
+ "RuntimeDefault",
+]}
+
+input_parameters_in_list = {
+ "allowedProfiles": [
+ "RuntimeDefault",
+ "Localhost",
+ ],
+ "allowedLocalhostFiles": ["profile.json"],
+}
+
+input_parameters_in_list_locahost_file = {
+ "allowedProfiles": [
+ "Localhost",
+ ],
+ "allowedLocalhostFiles": ["profile.json"],
+}
+
+input_parameters_not_in_list = {"allowedProfiles": [
+ "Unconfined",
+]}
+
+input_parameters_exempt = {
+ "exemptImages": ["nginx"],
+ "allowedProfiles": ["Unconfined"],
+}
+
+input_parameters_sc = {
+ "allowedProfiles": [
+ "RuntimeDefault",
+ "Localhost",
+ "Unconfined",
+ ],
+ "allowedLocalhostFiles": [
+ "profile1.json",
+ "profile2.json",
+ ],
+}
+
+input_parameters_sc_localhost_no_file = {
+ "allowedProfiles": ["Localhost"],
+}
+
+input_parameters_localhost_wildcard_both = {"allowedProfiles": ["Localhost"], "allowedLocalhostFiles": ["*"]}
+
+input_parameters_sc_localhost_wildcard_file = {
+ "allowedProfiles": ["Localhost"],
+ "allowedLocalhostFiles": ["*"],
+}
+
+input_parameters_sc_localhost_with_file = {"allowedProfiles": ["Localhost"], "allowedLocalhostFiles": ["profile.json"]}
\ No newline at end of file
diff --git a/website/docs/validation/seccomp.md b/website/docs/validation/seccomp.md
index d73218666..c56cc549e 100644
--- a/website/docs/validation/seccomp.md
+++ b/website/docs/validation/seccomp.md
@@ -16,7 +16,7 @@ metadata:
name: k8spspseccomp
annotations:
metadata.gatekeeper.sh/title: "Seccomp"
- metadata.gatekeeper.sh/version: 1.0.1
+ metadata.gatekeeper.sh/version: 1.1.0
description: >-
Controls the seccomp profile used by containers. Corresponds to the
`seccomp.security.alpha.kubernetes.io/allowedProfileNames` annotation on
@@ -79,218 +79,335 @@ spec:
type: string
targets:
- target: admission.k8s.gatekeeper.sh
- rego: |
- package k8spspseccomp
-
- import data.lib.exempt_container.is_exempt
-
- container_annotation_key_prefix = "container.seccomp.security.alpha.kubernetes.io/"
-
- pod_annotation_key = "seccomp.security.alpha.kubernetes.io/pod"
-
- naming_translation = {
- # securityContext -> annotation
- "RuntimeDefault": ["runtime/default", "docker/default"],
- "Unconfined": ["unconfined"],
- "Localhost": ["localhost"],
- # annotation -> securityContext
- "runtime/default": ["RuntimeDefault"],
- "docker/default": ["RuntimeDefault"],
- "unconfined": ["Unconfined"],
- "localhost": ["Localhost"],
- }
-
- violation[{"msg": msg}] {
- not input_wildcard_allowed_profiles
- allowed_profiles := get_allowed_profiles
- container := input_containers[name]
- not is_exempt(container)
- result := get_profile(container)
- not allowed_profile(result.profile, result.file, allowed_profiles)
- msg := get_message(result.profile, result.file, name, result.location, allowed_profiles)
- }
-
- get_message(profile, _, name, location, allowed_profiles) = message {
- not profile == "Localhost"
- message := sprintf("Seccomp profile '%v' is not allowed for container '%v'. Found at: %v. Allowed profiles: %v", [profile, name, location, allowed_profiles])
- }
-
- get_message(profile, file, name, location, allowed_profiles) = message {
- profile == "Localhost"
- message := sprintf("Seccomp profile '%v' with file '%v' is not allowed for container '%v'. Found at: %v. Allowed profiles: %v", [profile, file, name, location, allowed_profiles])
- }
-
- input_wildcard_allowed_profiles {
- input.parameters.allowedProfiles[_] == "*"
- }
-
- input_wildcard_allowed_files {
- input.parameters.allowedLocalhostFiles[_] == "*"
- }
-
- input_wildcard_allowed_files {
- "localhost/*" == input.parameters.allowedProfiles[_]
- }
-
- # Simple allowed Profiles
- allowed_profile(profile, _, allowed) {
- not startswith(lower(profile), "localhost")
- profile == allowed[_]
- }
-
- # seccomp Localhost without wildcard
- allowed_profile(profile, file, allowed) {
- profile == "Localhost"
- not input_wildcard_allowed_files
- profile == allowed[_]
- allowed_files := {x | x := object.get(input.parameters, "allowedLocalhostFiles", [])[_]} | get_annotation_localhost_files
- file == allowed_files[_]
- }
-
- # seccomp Localhost with wildcard
- allowed_profile(profile, _, allowed) {
- profile == "Localhost"
- input_wildcard_allowed_files
- profile == allowed[_]
- }
-
- # annotation localhost with wildcard
- allowed_profile(profile, _, allowed) {
- "localhost/*" == allowed[_]
- startswith(profile, "localhost/")
- }
-
- # annotation localhost without wildcard
- allowed_profile(profile, _, allowed) {
- startswith(profile, "localhost/")
- profile == allowed[_]
- }
-
- # Localhost files from annotation scheme
- get_annotation_localhost_files[file] {
- profile := input.parameters.allowedProfiles[_]
- startswith(profile, "localhost/")
- file := replace(profile, "localhost/", "")
- }
-
- # The profiles explicitly in the list
- get_allowed_profiles[allowed] {
- allowed := input.parameters.allowedProfiles[_]
- }
-
- # The simply translated profiles
- get_allowed_profiles[allowed] {
- profile := input.parameters.allowedProfiles[_]
- not startswith(lower(profile), "localhost")
- allowed := naming_translation[profile][_]
- }
-
- # Seccomp Localhost to annotation translation
- get_allowed_profiles[allowed] {
- profile := input.parameters.allowedProfiles[_]
- profile == "Localhost"
- file := object.get(input.parameters, "allowedLocalhostFiles", [])[_]
- allowed := sprintf("%v/%v", [naming_translation[profile][_], file])
- }
-
- # Annotation localhost to Seccomp translation
- get_allowed_profiles[allowed] {
- profile := input.parameters.allowedProfiles[_]
- startswith(profile, "localhost")
- allowed := naming_translation.localhost[_]
- }
-
- # Container profile as defined in pod annotation
- get_profile(container) = {"profile": profile, "file": "", "location": location} {
- not has_securitycontext_container(container)
- not has_annotation(get_container_annotation_key(container.name))
- not has_securitycontext_pod
- profile := input.review.object.metadata.annotations[pod_annotation_key]
- location := sprintf("annotation %v", [pod_annotation_key])
- }
-
- # Container profile as defined in container annotation
- get_profile(container) = {"profile": profile, "file": "", "location": location} {
- not has_securitycontext_container(container)
- not has_securitycontext_pod
- container_annotation := get_container_annotation_key(container.name)
- has_annotation(container_annotation)
- profile := input.review.object.metadata.annotations[container_annotation]
- location := sprintf("annotation %v", [container_annotation])
- }
-
- # Container profile as defined in pods securityContext
- get_profile(container) = {"profile": profile, "file": file, "location": location} {
- not has_securitycontext_container(container)
- profile := input.review.object.spec.securityContext.seccompProfile.type
- file := object.get(input.review.object.spec.securityContext.seccompProfile, "localhostProfile", "")
- location := "pod securityContext"
- }
-
- # Container profile as defined in containers securityContext
- get_profile(container) = {"profile": profile, "file": file, "location": location} {
- has_securitycontext_container(container)
- profile := container.securityContext.seccompProfile.type
- file := object.get(container.securityContext.seccompProfile, "localhostProfile", "")
- location := "container securityContext"
- }
-
- # Container profile missing
- get_profile(container) = {"profile": "not configured", "file": "", "location": "no explicit profile found"} {
- not has_annotation(get_container_annotation_key(container.name))
- not has_annotation(pod_annotation_key)
- not has_securitycontext_pod
- not has_securitycontext_container(container)
- }
-
- has_annotation(annotation) {
- input.review.object.metadata.annotations[annotation]
- }
-
- has_securitycontext_pod {
- input.review.object.spec.securityContext.seccompProfile
- }
-
- has_securitycontext_container(container) {
- container.securityContext.seccompProfile
- }
-
- get_container_annotation_key(name) = annotation {
- annotation := concat("", [container_annotation_key_prefix, name])
- }
-
- input_containers[container.name] = container {
- container := input.review.object.spec.containers[_]
- }
-
- input_containers[container.name] = container {
- container := input.review.object.spec.initContainers[_]
- }
-
- input_containers[container.name] = container {
- container := input.review.object.spec.ephemeralContainers[_]
- }
- libs:
- - |
- package lib.exempt_container
-
- is_exempt(container) {
- exempt_images := object.get(object.get(input, "parameters", {}), "exemptImages", [])
- img := container.image
- exemption := exempt_images[_]
- _matches_exemption(img, exemption)
- }
-
- _matches_exemption(img, exemption) {
- not endswith(exemption, "*")
- exemption == img
- }
-
- _matches_exemption(img, exemption) {
- endswith(exemption, "*")
- prefix := trim_suffix(exemption, "*")
- startswith(img, prefix)
- }
+ code:
+ - engine: K8sNativeValidation
+ source:
+ variables:
+ - name: containers
+ expression: 'has(variables.anyObject.spec.containers) ? variables.anyObject.spec.containers : []'
+ - name: initContainers
+ expression: 'has(variables.anyObject.spec.initContainers) ? variables.anyObject.spec.initContainers : []'
+ - name: ephemeralContainers
+ expression: 'has(variables.anyObject.spec.ephemeralContainers) ? variables.anyObject.spec.ephemeralContainers : []'
+ - name: allowAllProfiles
+ expression: |
+ has(variables.params.allowedProfiles) && variables.params.allowedProfiles.exists(profile, profile == "*")
+ - name: exemptImagePrefixes
+ expression: |
+ !has(variables.params.exemptImages) ? [] :
+ variables.params.exemptImages.filter(image, image.endsWith("*")).map(image, string(image).replace("*", ""))
+ - name: exemptImageExplicit
+ expression: |
+ !has(variables.params.exemptImages) ? [] :
+ variables.params.exemptImages.filter(image, !image.endsWith("*"))
+ - name: exemptImages
+ expression: |
+ (variables.containers + variables.initContainers + variables.ephemeralContainers).filter(container,
+ container.image in variables.exemptImageExplicit ||
+ variables.exemptImagePrefixes.exists(exemption, string(container.image).startsWith(exemption))).map(container, container.image)
+ - name: unverifiedContainers
+ expression: |
+ (variables.containers + variables.initContainers + variables.ephemeralContainers).filter(container,
+ !variables.allowAllProfiles &&
+ !(container.image in variables.exemptImages))
+ - name: inputAllowedProfiles
+ expression: |
+ !has(variables.params.allowedProfiles) ? [] : variables.params.allowedProfiles
+ - name: allowedLocalhostFiles
+ expression: |
+ has(variables.params.allowedLocalhostFiles) ? variables.params.allowedLocalhostFiles : []
+ - name: allowedProfilesTranslation
+ expression: |
+ (variables.inputAllowedProfiles.filter(profile,
+ profile != "Localhost").map(profile, profile == "Unconfined" ? "unconfined" : profile)) +
+ (variables.inputAllowedProfiles.exists(profile, profile == "RuntimeDefault") ? ["runtime/default", "docker/default"] : [])
+ - name: allowSecurityContextLocalhost
+ expression: |
+ variables.inputAllowedProfiles.exists(profile, profile == "Localhost")
+ - name: derivedAllowedLocalhostFiles
+ expression: |
+ variables.allowSecurityContextLocalhost ? variables.params.allowedLocalhostFiles.map(file, "localhost/" + file) : []
+ - name: localhostWildcardAllowed
+ expression: |
+ variables.inputAllowedProfiles.exists(profile, profile == "localhost/*") || variables.derivedAllowedLocalhostFiles.exists(profile, profile == "localhost/*")
+ - name: allowedProfiles
+ expression: |
+ (variables.allowedProfilesTranslation + variables.derivedAllowedLocalhostFiles)
+ - name: hasPodSeccomp
+ expression: |
+ has(variables.anyObject.spec.securityContext) && has(variables.anyObject.spec.securityContext.seccompProfile)
+ - name: hasPodAnnotations
+ expression: |
+ has(variables.anyObject.metadata.annotations) && ("seccomp.security.alpha.kubernetes.io/pod" in variables.anyObject.metadata.annotations)
+ - name: podAnnotationsProfiles
+ expression: |
+ variables.unverifiedContainers.filter(container,
+ !(has(container.securityContext) && has(container.securityContext.seccompProfile)) &&
+ !(has(variables.anyObject.metadata.annotations) && (("container.seccomp.security.alpha.kubernetes.io/" + container.name) in variables.anyObject.metadata.annotations)) &&
+ !variables.hasPodSeccomp &&
+ variables.hasPodAnnotations
+ ).map(container, {
+ "container" : container.name,
+ "profile" : variables.anyObject.metadata.annotations["seccomp.security.alpha.kubernetes.io/pod"],
+ "file" : dyn(""),
+ "location" : dyn("annotation seccomp.security.alpha.kubernetes.io/pod"),
+ })
+ - name: containerAnnotationsProfiles
+ expression: |
+ variables.unverifiedContainers.filter(container,
+ !(has(container.securityContext) && has(container.securityContext.seccompProfile)) &&
+ !variables.hasPodSeccomp &&
+ has(variables.anyObject.metadata.annotations) && (("container.seccomp.security.alpha.kubernetes.io/" + container.name) in variables.anyObject.metadata.annotations)
+ ).map(container, {
+ "container" : container.name,
+ "profile" : variables.anyObject.metadata.annotations["container.seccomp.security.alpha.kubernetes.io/" + container.name],
+ "file" : dyn(""),
+ "location" : dyn("annotation container.seccomp.security.alpha.kubernetes.io/" + container.name),
+ })
+ - name: podLocalHostProfile
+ expression: |
+ variables.hasPodSeccomp && has(variables.anyObject.spec.securityContext.seccompProfile.localhostProfile) ? variables.anyObject.spec.securityContext.seccompProfile.localhostProfile : ""
+ - name: canonicalPodSecurityContextProfile
+ expression: |
+ has(variables.hasPodSeccomp) && has(variables.anyObject.spec.securityContext.seccompProfile.type) ?
+ (variables.anyObject.spec.securityContext.seccompProfile.type == "RuntimeDefault" ? (
+ variables.allowedProfiles.exists(profile, profile == "runtime/default") ? "runtime/default" : variables.allowedProfiles.exists(profile, profile == "docker/default") ? "docker/default" : "runtime/default") :
+ variables.anyObject.spec.securityContext.seccompProfile.type == "Unconfined" ? "unconfined" : variables.anyObject.spec.securityContext.seccompProfile.type == "Localhost" ? "localhost/" + variables.podLocalHostProfile : "")
+ : ""
+ - name: podSecurityContextProfiles
+ expression: |
+ variables.unverifiedContainers.filter(container,
+ !(has(container.securityContext) && has(container.securityContext.seccompProfile)) &&
+ variables.hasPodSeccomp
+ ).map(container, {
+ "container" : container.name,
+ "profile" : dyn(variables.canonicalPodSecurityContextProfile),
+ "file" : variables.podLocalHostProfile,
+ "location" : dyn("pod securityContext"),
+ })
+ - name: containerSecurityContextProfiles
+ expression: |
+ variables.unverifiedContainers.filter(container,
+ has(container.securityContext) && has(container.securityContext.seccompProfile)
+ ).map(container, {
+ "container" : container.name,
+ "profile" : dyn(has(container.securityContext.seccompProfile.type) ? (container.securityContext.seccompProfile.type == "RuntimeDefault" ? (
+ variables.allowedProfiles.exists(profile, profile == "runtime/default") ? "runtime/default" : variables.allowedProfiles.exists(profile, profile == "docker/default") ? "docker/default" : "runtime/default") :
+ container.securityContext.seccompProfile.type == "Unconfined" ? "unconfined" : container.securityContext.seccompProfile.type == "Localhost" ? "localhost/" + container.securityContext.seccompProfile.localhostProfile : "")
+ : ""),
+ "file" : has(container.securityContext.seccompProfile.localhostProfile) ? container.securityContext.seccompProfile.localhostProfile : dyn(""),
+ "location" : dyn("container securityContext"),
+ })
+ - name: containerProfilesMissing
+ expression: |
+ variables.unverifiedContainers.filter(container,
+ !(has(container.securityContext) && has(container.securityContext.seccompProfile)) &&
+ !(has(variables.anyObject.metadata.annotations) && (("container.seccomp.security.alpha.kubernetes.io/" + container.name) in variables.anyObject.metadata.annotations)) &&
+ !variables.hasPodSeccomp &&
+ !variables.hasPodAnnotations
+ ).map(container, {
+ "container" : container.name,
+ "profile" : dyn("not configured"),
+ "file" : dyn(""),
+ "location" : dyn("no explicit profile found"),
+ })
+ - name: allContainerProfiles
+ expression: |
+ variables.podAnnotationsProfiles + variables.containerAnnotationsProfiles + variables.podSecurityContextProfiles + variables.containerSecurityContextProfiles + variables.containerProfilesMissing
+ - name: badContainerProfiles
+ expression: |
+ variables.allContainerProfiles.filter(badContainerProfile,
+ !((badContainerProfile.profile in variables.allowedProfiles) || (badContainerProfile.profile.startsWith("localhost/") && variables.localhostWildcardAllowed))
+ ).map(badProfile, "Seccomp profile '" + badProfile.profile + "' is not allowed for container '" + badProfile.container + "'. Found at: " + badProfile.location + ". Allowed profiles: " + variables.allowedProfiles.join(", "))
+ validations:
+ - expression: 'size(variables.badContainerProfiles) == 0'
+ messageExpression: |
+ variables.badContainerProfiles.join(", ")
+ - engine: Rego
+ source:
+ rego: |
+ package k8spspseccomp
+
+ import data.lib.exempt_container.is_exempt
+
+ container_annotation_key_prefix = "container.seccomp.security.alpha.kubernetes.io/"
+
+ pod_annotation_key = "seccomp.security.alpha.kubernetes.io/pod"
+
+ violation[{"msg": msg}] {
+ not input_wildcard_allowed_profiles
+ allowed_profiles := get_allowed_profiles
+ container := input_containers[name]
+ not is_exempt(container)
+ result := get_profile(container)
+ not allowed_profile(result.profile, result.file, allowed_profiles)
+ msg := get_message(result.profile, result.file, name, result.location, allowed_profiles)
+ }
+
+ get_message(profile, _, name, location, allowed_profiles) = message {
+ message := sprintf("Seccomp profile '%v' is not allowed for container '%v'. Found at: %v. Allowed profiles: %v", [profile, name, location, allowed_profiles])
+ }
+
+ input_wildcard_allowed_profiles {
+ input.parameters.allowedProfiles[_] == "*"
+ }
+
+ input_wildcard_allowed_files {
+ input.parameters.allowedLocalhostFiles[_] == "*"
+ }
+
+ input_wildcard_allowed_files {
+ "localhost/*" == input.parameters.allowedProfiles[_]
+ }
+
+ # Simple allowed Profiles
+ allowed_profile(profile, _, allowed) {
+ not startswith(profile, "localhost/")
+ profile == allowed[_]
+ }
+
+ # annotation localhost with wildcard
+ allowed_profile(profile, _, allowed) {
+ "localhost/*" == allowed[_]
+ startswith(profile, "localhost/")
+ }
+
+ # annotation localhost without wildcard
+ allowed_profile(profile, _, allowed) {
+ startswith(profile, "localhost/")
+ profile == allowed[_]
+ }
+
+ # The profiles explicitly in the list
+ get_allowed_profiles[allowed] {
+ allowed := input.parameters.allowedProfiles[_]
+ }
+
+ # Seccomp Localhost to annotation translation
+ get_allowed_profiles[allowed] {
+ profile := input.parameters.allowedProfiles[_]
+ not contains(profile, "/")
+ file := object.get(input.parameters, "allowedLocalhostFiles", [])[_]
+ allowed := canonicalize_seccomp_profile({"type": profile, "localhostProfile": file}, "")[_]
+ }
+
+ # Container profile as defined in pod annotation
+ get_profile(container) = {"profile": profile, "file": "", "location": location} {
+ not has_securitycontext_container(container)
+ not has_annotation(get_container_annotation_key(container.name))
+ not has_securitycontext_pod
+ profile := input.review.object.metadata.annotations[pod_annotation_key]
+ location := sprintf("annotation %v", [pod_annotation_key])
+ }
+
+ # Container profile as defined in container annotation
+ get_profile(container) = {"profile": profile, "file": "", "location": location} {
+ not has_securitycontext_container(container)
+ not has_securitycontext_pod
+ container_annotation := get_container_annotation_key(container.name)
+ has_annotation(container_annotation)
+ profile := input.review.object.metadata.annotations[container_annotation]
+ location := sprintf("annotation %v", [container_annotation])
+ }
+
+ # Container profile as defined in pods securityContext
+ get_profile(container) = {"profile": profile, "file": file, "location": location} {
+ not has_securitycontext_container(container)
+ profile := canonicalize_seccomp_profile(input.review.object.spec.securityContext.seccompProfile, canonicalize_runtime_default_profile)[_]
+ file := object.get(input.review.object.spec.securityContext.seccompProfile, "localhostProfile", "")
+ location := "pod securityContext"
+ }
+
+ # Container profile as defined in containers securityContext
+ get_profile(container) = {"profile": profile, "file": file, "location": location} {
+ has_securitycontext_container(container)
+ profile := canonicalize_seccomp_profile(container.securityContext.seccompProfile, canonicalize_runtime_default_profile)[_]
+ file := object.get(container.securityContext.seccompProfile, "localhostProfile", "")
+ location := "container securityContext"
+ }
+
+ # Container profile missing
+ get_profile(container) = {"profile": "not configured", "file": "", "location": "no explicit profile found"} {
+ not has_securitycontext_container(container)
+ not has_securitycontext_pod
+ not has_annotation(get_container_annotation_key(container.name))
+ not has_annotation(pod_annotation_key)
+ }
+
+ has_annotation(annotation) {
+ input.review.object.metadata.annotations[annotation]
+ }
+
+ has_securitycontext_pod {
+ input.review.object.spec.securityContext.seccompProfile
+ }
+
+ has_securitycontext_container(container) {
+ container.securityContext.seccompProfile
+ }
+
+ get_container_annotation_key(name) = annotation {
+ annotation := concat("", [container_annotation_key_prefix, name])
+ }
+
+ input_containers[container.name] = container {
+ container := input.review.object.spec.containers[_]
+ }
+
+ input_containers[container.name] = container {
+ container := input.review.object.spec.initContainers[_]
+ }
+
+ input_containers[container.name] = container {
+ container := input.review.object.spec.ephemeralContainers[_]
+ }
+
+ canonicalize_runtime_default_profile() = out {
+ "runtime/default" == input.parameters.allowedProfiles[_]
+ out := "runtime/default"
+ } else = out {
+ "docker/default" == input.parameters.allowedProfiles[_]
+ out := "docker/default"
+ } else = out {
+ out := "runtime/default"
+ }
+
+ canonicalize_seccomp_profile(profile, def) = out {
+ profile.type == "RuntimeDefault"
+ def == ""
+ out := ["runtime/default", "docker/default"]
+ } else = out {
+ profile.type == "RuntimeDefault"
+ def != ""
+ out := [def]
+ } else = out {
+ profile.type == "Localhost"
+ out := [sprintf("localhost/%s", [profile.localhostProfile])]
+ } else = out {
+ profile.type == "Unconfined"
+ out := ["unconfined"]
+ }
+ libs:
+ - |
+ package lib.exempt_container
+
+ is_exempt(container) {
+ exempt_images := object.get(object.get(input, "parameters", {}), "exemptImages", [])
+ img := container.image
+ exemption := exempt_images[_]
+ _matches_exemption(img, exemption)
+ }
+
+ _matches_exemption(img, exemption) {
+ not endswith(exemption, "*")
+ exemption == img
+ }
+
+ _matches_exemption(img, exemption) {
+ endswith(exemption, "*")
+ prefix := trim_suffix(exemption, "*")
+ startswith(img, prefix)
+ }
```
@@ -316,10 +433,11 @@ spec:
- apiGroups: [""]
kinds: ["Pod"]
parameters:
+ exemptImages:
+ - nginx-exempt
allowedProfiles:
- runtime/default
- - docker/default
-
+ - localhost/profile.json
```
Usage
@@ -459,6 +577,89 @@ Usage
kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper-library/master/library/pod-security-policy/seccomp/samples/psp-seccomp/disallowed_ephemeral.yaml
```
+
+
+example-allowed-container-exempt-image
+
+```yaml
+apiVersion: v1
+kind: Pod
+metadata:
+ name: nginx-seccomp-disallowed
+ labels:
+ app: nginx-seccomp
+spec:
+ containers:
+ - name: nginx
+ image: nginx-exempt
+ securityContext:
+ seccompProfile:
+ type: Unconfined
+
+```
+
+Usage
+
+```shell
+kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper-library/master/library/pod-security-policy/seccomp/samples/psp-seccomp/example_allowed_exempt_image.yaml
+```
+
+
+
+example-allowed-container-localhost-profile
+
+```yaml
+apiVersion: v1
+kind: Pod
+metadata:
+ name: nginx-seccomp-allowed-localhost
+ labels:
+ app: nginx-seccomp
+spec:
+ containers:
+ - name: nginx
+ image: nginx
+ securityContext:
+ seccompProfile:
+ type: Localhost
+ localhostProfile: profile.json
+
+```
+
+Usage
+
+```shell
+kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper-library/master/library/pod-security-policy/seccomp/samples/psp-seccomp/example_allowed_localhost.yaml
+```
+
+
+
+example-disallowed-container-localhost-profile
+
+```yaml
+apiVersion: v1
+kind: Pod
+metadata:
+ name: nginx-seccomp-disallowed-localhost
+ labels:
+ app: nginx-seccomp
+spec:
+ containers:
+ - name: nginx
+ image: nginx
+ securityContext:
+ seccompProfile:
+ type: Localhost
+ localhostProfile: profile.log
+
+```
+
+Usage
+
+```shell
+kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper-library/master/library/pod-security-policy/seccomp/samples/psp-seccomp/example_disallowed_localhost.yaml
+```
+
diff --git a/website/docs/validation/seccompv2.md b/website/docs/validation/seccompv2.md
new file mode 100644
index 000000000..f93abdb3b
--- /dev/null
+++ b/website/docs/validation/seccompv2.md
@@ -0,0 +1,519 @@
+---
+id: seccompv2
+title: Seccomp V2
+---
+
+# Seccomp V2
+
+## Description
+Controls the seccomp profile used by containers. Corresponds to the `securityContext.seccompProfile` field. Security contexts from the annotation is not considered as Kubernetes no longer reads security contexts from the annotation.
+
+## Template
+```yaml
+apiVersion: templates.gatekeeper.sh/v1
+kind: ConstraintTemplate
+metadata:
+ name: k8spspseccompv2
+ annotations:
+ metadata.gatekeeper.sh/title: "Seccomp V2"
+ metadata.gatekeeper.sh/version: 1.0.0
+ description: >-
+ Controls the seccomp profile used by containers. Corresponds to the
+ `securityContext.seccompProfile` field. Security contexts from the annotation is not considered as Kubernetes no longer reads security contexts from the annotation.
+spec:
+ crd:
+ spec:
+ names:
+ kind: K8sPSPSeccompV2
+ validation:
+ # Schema for the `parameters` field
+ openAPIV3Schema:
+ type: object
+ description: >-
+ Controls the seccomp profile used by containers. Corresponds to the
+ `securityContext.seccompProfile` field. Security contexts from the annotation is not considered as Kubernetes no longer reads security contexts from the annotation.
+ properties:
+ exemptImages:
+ description: >-
+ Any container that uses an image that matches an entry in this list will be excluded
+ from enforcement. Prefix-matching can be signified with `*`. For example: `my-image-*`.
+
+ It is recommended that users use the fully-qualified Docker image name (e.g. start with a domain name)
+ in order to avoid unexpectedly exempting images from an untrusted repository.
+ type: array
+ items:
+ type: string
+ allowedProfiles:
+ type: array
+ description: >-
+ An array of allowed profile values for seccomp on Pods/Containers.
+
+ Can use the securityContext naming scheme: `RuntimeDefault`, `Unconfined`
+ and/or `Localhost`. For securityContext `Localhost`, use the parameter `allowedLocalhostFiles`
+ to list the allowed profile JSON files.
+
+ The policy code will translate between the two schemes so it is not necessary to use both.
+
+ Putting a `*` in this array allows all Profiles to be used.
+
+ This field is required since with an empty list this policy will block all workloads.
+ items:
+ type: string
+ allowedLocalhostFiles:
+ type: array
+ description: >-
+ When using securityContext naming scheme for seccomp and including `Localhost` this array holds
+ the allowed profile JSON files.
+
+ Putting a `*` in this array will allows all JSON files to be used.
+
+ This field is required to allow `Localhost` in securityContext as with an empty list it will block.
+ items:
+ type: string
+ targets:
+ - target: admission.k8s.gatekeeper.sh
+ code:
+ - engine: K8sNativeValidation
+ source:
+ variables:
+ - name: containers
+ expression: 'has(variables.anyObject.spec.containers) ? variables.anyObject.spec.containers : []'
+ - name: initContainers
+ expression: 'has(variables.anyObject.spec.initContainers) ? variables.anyObject.spec.initContainers : []'
+ - name: ephemeralContainers
+ expression: 'has(variables.anyObject.spec.ephemeralContainers) ? variables.anyObject.spec.ephemeralContainers : []'
+ - name: allowAllProfiles
+ expression: |
+ has(variables.params.allowedProfiles) && variables.params.allowedProfiles.exists(profile, profile == "*")
+ - name: exemptImagePrefixes
+ expression: |
+ !has(variables.params.exemptImages) ? [] :
+ variables.params.exemptImages.filter(image, image.endsWith("*")).map(image, string(image).replace("*", ""))
+ - name: exemptImageExplicit
+ expression: |
+ !has(variables.params.exemptImages) ? [] :
+ variables.params.exemptImages.filter(image, !image.endsWith("*"))
+ - name: exemptImages
+ expression: |
+ (variables.containers + variables.initContainers + variables.ephemeralContainers).filter(container,
+ container.image in variables.exemptImageExplicit ||
+ variables.exemptImagePrefixes.exists(exemption, string(container.image).startsWith(exemption))).map(container, container.image)
+ - name: unverifiedContainers
+ expression: |
+ (variables.containers + variables.initContainers + variables.ephemeralContainers).filter(container,
+ !variables.allowAllProfiles &&
+ !(container.image in variables.exemptImages))
+ - name: inputNonLocalHostProfiles
+ expression: |
+ variables.params.allowedProfiles.filter(profile, profile != "Localhost").map(profile, {"type": profile})
+ - name: inputLocalHostProfiles
+ expression: |
+ variables.params.allowedProfiles.exists(profile, profile == "Localhost") ? variables.params.allowedLocalhostFiles.map(file, {"type": "Localhost", "localHostProfile": string(file)}) : []
+ - name: inputAllowedProfiles
+ expression: |
+ variables.inputNonLocalHostProfiles + variables.inputLocalHostProfiles
+ - name: hasPodSeccomp
+ expression: |
+ has(variables.anyObject.spec.securityContext) && has(variables.anyObject.spec.securityContext.seccompProfile)
+ - name: podLocalHostProfile
+ expression: |
+ variables.hasPodSeccomp && has(variables.anyObject.spec.securityContext.seccompProfile.localhostProfile) ? variables.anyObject.spec.securityContext.seccompProfile.localhostProfile : ""
+ - name: podSecurityContextProfileType
+ expression: |
+ has(variables.hasPodSeccomp) && has(variables.anyObject.spec.securityContext.seccompProfile.type) ? variables.anyObject.spec.securityContext.seccompProfile.type
+ : ""
+ - name: podSecurityContextProfiles
+ expression: |
+ variables.unverifiedContainers.filter(container,
+ !(has(container.securityContext) && has(container.securityContext.seccompProfile)) &&
+ variables.hasPodSeccomp
+ ).map(container, {
+ "container" : container.name,
+ "profile" : dyn(variables.podSecurityContextProfileType),
+ "file" : variables.podLocalHostProfile,
+ "location" : dyn("pod securityContext"),
+ })
+ - name: containerSecurityContextProfiles
+ expression: |
+ variables.unverifiedContainers.filter(container,
+ has(container.securityContext) && has(container.securityContext.seccompProfile)
+ ).map(container, {
+ "container" : container.name,
+ "profile" : dyn(container.securityContext.seccompProfile.type),
+ "file" : has(container.securityContext.seccompProfile.localhostProfile) ? container.securityContext.seccompProfile.localhostProfile : dyn(""),
+ "location" : dyn("container securityContext"),
+ })
+ - name: containerProfilesMissing
+ expression: |
+ variables.unverifiedContainers.filter(container,
+ !(has(container.securityContext) && has(container.securityContext.seccompProfile)) &&
+ !variables.hasPodSeccomp
+ ).map(container, {
+ "container" : container.name,
+ "profile" : dyn("not configured"),
+ "file" : dyn(""),
+ "location" : dyn("no explicit profile found"),
+ })
+ - name: allContainerProfiles
+ expression: |
+ variables.podSecurityContextProfiles + variables.containerSecurityContextProfiles + variables.containerProfilesMissing
+ - name: badContainerProfilesWithoutFiles
+ expression: |
+ variables.allContainerProfiles.filter(container,
+ container.profile != "Localhost" &&
+ !variables.inputAllowedProfiles.exists(profile, profile.type == container.profile)
+ ).map(badProfile, "Seccomp profile '" + badProfile.profile + "' is not allowed for container '" + badProfile.container + "'. Found at: " + badProfile.location + ". Allowed profiles: " + variables.inputAllowedProfiles.map(profile, "{\"type\": \"" + profile.type + "\"" + (has(profile.localHostProfile) ? ", \"localHostProfile\": \"" + profile.localHostProfile + "\"}" : "}")).join(", "))
+ - name: badContainerProfilesWithFiles
+ expression: |
+ variables.allContainerProfiles.filter(container,
+ container.profile == "Localhost" &&
+ !variables.inputAllowedProfiles.exists(profile, profile.type == "Localhost" && (has(profile.localHostProfile) && (profile.localHostProfile == container.file || profile.localHostProfile == "*")))
+ ).map(badProfile, "Seccomp profile '" + badProfile.profile + "' With file '" + badProfile.file + "' is not allowed for container '" + badProfile.container + "'. Found at: " + badProfile.location + ". Allowed profiles: " + variables.inputAllowedProfiles.map(profile, "{\"type\": \"" + profile.type + "\"" + (has(profile.localHostProfile) ? ", \"localHostProfile\": \"" + profile.localHostProfile + "\"}" : "}")).join(", "))
+ validations:
+ - expression: 'size(variables.badContainerProfilesWithoutFiles) == 0'
+ messageExpression: |
+ variables.badContainerProfilesWithoutFiles.join(", ")
+ - expression: 'size(variables.badContainerProfilesWithFiles) == 0'
+ messageExpression: |
+ variables.badContainerProfilesWithFiles.join(", ")
+ - engine: Rego
+ source:
+ rego: |
+ package k8spspseccomp
+
+ import data.lib.exempt_container.is_exempt
+
+ violation[{"msg": msg}] {
+ not input_wildcard_allowed_profiles
+ allowed_profiles := get_allowed_profiles
+ container := input_containers[name]
+ not is_exempt(container)
+ result := get_profile(container)
+ not allowed_profile(result.profile, result.file, allowed_profiles)
+ msg := get_message(result.profile, result.file, name, result.location, allowed_profiles)
+ }
+
+ get_message(profile, _, name, location, allowed_profiles) = message {
+ profile != "Localhost"
+ message := sprintf("Seccomp profile '%v' is not allowed for container '%v'. Found at: %v. Allowed profiles: %v", [profile, name, location, allowed_profiles])
+ }
+
+ get_message(profile, file, name, location, allowed_profiles) = message {
+ profile == "Localhost"
+ message := sprintf("Seccomp profile '%v' with file '%v' is not allowed for container '%v'. Found at: %v. Allowed profiles: %v", [profile, file, name, location, allowed_profiles])
+ }
+
+ input_wildcard_allowed_profiles {
+ input.parameters.allowedProfiles[_] == "*"
+ }
+
+ input_wildcard_allowed_files {
+ input.parameters.allowedLocalhostFiles[_] == "*"
+ }
+
+ allowed_profile(_, _, _) {
+ input_wildcard_allowed_profiles
+ }
+
+ allowed_profile(profile, _, _) {
+ profile == "Localhost"
+ input_wildcard_allowed_files
+ }
+
+ # Simple allowed Profiles
+ allowed_profile(profile, _, allowed) {
+ profile != "Localhost"
+ allow_profile = allowed[_]
+ profile == allow_profile.type
+ }
+
+ # annotation localhost without wildcard
+ allowed_profile(profile, file, allowed) {
+ profile == "Localhost"
+ allow_profile = allowed[_]
+ allow_profile.type == "Localhost"
+ file == allow_profile.localHostProfile
+ }
+
+ # The profiles explicitly in the list
+ get_allowed_profiles[allowed] {
+ profile := input.parameters.allowedProfiles[_]
+ profile != "Localhost"
+ allowed := {"type": profile}
+ }
+
+ get_allowed_profiles[allowed] {
+ profile := input.parameters.allowedProfiles[_]
+ profile == "Localhost"
+ file := object.get(input.parameters, "allowedLocalhostFiles", [""])[_]
+ allowed := {"type": "Localhost", "localHostProfile": file}
+ }
+
+ # Container profile as defined in containers securityContext
+ get_profile(container) = {"profile": profile, "file": file, "location": location} {
+ has_securitycontext_container(container)
+ profile := container.securityContext.seccompProfile.type
+ file := object.get(container.securityContext.seccompProfile, "localhostProfile", "")
+ location := "container securityContext"
+ }
+
+ # Container profile as defined in pods securityContext
+ get_profile(container) = {"profile": profile, "file": file, "location": location} {
+ not has_securitycontext_container(container)
+ profile := input.review.object.spec.securityContext.seccompProfile.type
+ file := object.get(input.review.object.spec.securityContext.seccompProfile, "localhostProfile", "")
+ location := "pod securityContext"
+ }
+
+ # Container profile missing
+ get_profile(container) = {"profile": "not configured", "file": "", "location": "no explicit profile found"} {
+ not has_securitycontext_container(container)
+ not has_securitycontext_pod
+ }
+
+ has_securitycontext_pod {
+ input.review.object.spec.securityContext.seccompProfile
+ }
+
+ has_securitycontext_container(container) {
+ container.securityContext.seccompProfile
+ }
+
+ input_containers[container.name] = container {
+ container := input.review.object.spec.containers[_]
+ }
+
+ input_containers[container.name] = container {
+ container := input.review.object.spec.initContainers[_]
+ }
+
+ input_containers[container.name] = container {
+ container := input.review.object.spec.ephemeralContainers[_]
+ }
+ libs:
+ - |
+ package lib.exempt_container
+
+ is_exempt(container) {
+ exempt_images := object.get(object.get(input, "parameters", {}), "exemptImages", [])
+ img := container.image
+ exemption := exempt_images[_]
+ _matches_exemption(img, exemption)
+ }
+
+ _matches_exemption(img, exemption) {
+ not endswith(exemption, "*")
+ exemption == img
+ }
+
+ _matches_exemption(img, exemption) {
+ endswith(exemption, "*")
+ prefix := trim_suffix(exemption, "*")
+ startswith(img, prefix)
+ }
+
+```
+
+### Usage
+```shell
+kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper-library/master/library/pod-security-policy/seccompv2/template.yaml
+```
+## Examples
+
+default-seccomp-required
+
+
+constraint
+
+```yaml
+apiVersion: constraints.gatekeeper.sh/v1beta1
+kind: K8sPSPSeccompV2
+metadata:
+ name: psp-seccomp
+spec:
+ match:
+ kinds:
+ - apiGroups: [""]
+ kinds: ["Pod"]
+ parameters:
+ exemptImages:
+ - nginx-exempt
+ allowedProfiles:
+ - RuntimeDefault
+ - Localhost
+ allowedLocalhostFiles:
+ - "*"
+
+```
+
+Usage
+
+```shell
+kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper-library/master/library/pod-security-policy/seccompv2/samples/psp-seccomp/constraint.yaml
+```
+
+
+
+
+example-disallowed-global
+
+```yaml
+apiVersion: v1
+kind: Pod
+metadata:
+ name: nginx-seccomp-disallowed2
+ labels:
+ app: nginx-seccomp
+spec:
+ securityContext:
+ seccompProfile:
+ type: Unconfined
+ containers:
+ - name: nginx
+ image: nginx
+
+```
+
+Usage
+
+```shell
+kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper-library/master/library/pod-security-policy/seccompv2/samples/psp-seccomp/example_disallowed2.yaml
+```
+
+
+
+example-disallowed-container
+
+```yaml
+apiVersion: v1
+kind: Pod
+metadata:
+ name: nginx-seccomp-disallowed
+ labels:
+ app: nginx-seccomp
+spec:
+ containers:
+ - name: nginx
+ image: nginx
+ securityContext:
+ seccompProfile:
+ type: Unconfined
+
+```
+
+Usage
+
+```shell
+kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper-library/master/library/pod-security-policy/seccompv2/samples/psp-seccomp/example_disallowed.yaml
+```
+
+
+
+example-allowed-container
+
+```yaml
+apiVersion: v1
+kind: Pod
+metadata:
+ name: nginx-seccomp-allowed
+ labels:
+ app: nginx-seccomp
+spec:
+ containers:
+ - name: nginx
+ image: nginx
+ securityContext:
+ seccompProfile:
+ type: RuntimeDefault
+
+```
+
+Usage
+
+```shell
+kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper-library/master/library/pod-security-policy/seccompv2/samples/psp-seccomp/example_allowed.yaml
+```
+
+
+
+example-allowed-container
+
+```yaml
+apiVersion: v1
+kind: Pod
+metadata:
+ name: nginx-seccomp-allowed-localhost
+ labels:
+ app: nginx-seccomp
+spec:
+ containers:
+ - name: nginx
+ image: nginx
+ securityContext:
+ seccompProfile:
+ type: Localhost
+ localhostProfile: profile.json
+
+```
+
+Usage
+
+```shell
+kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper-library/master/library/pod-security-policy/seccompv2/samples/psp-seccomp/example_allowed_localhost.yaml
+```
+
+
+
+example-allowed-container-exempt-image
+
+```yaml
+apiVersion: v1
+kind: Pod
+metadata:
+ name: nginx-seccomp-disallowed
+ labels:
+ app: nginx-seccomp
+spec:
+ containers:
+ - name: nginx
+ image: nginx-exempt
+ securityContext:
+ seccompProfile:
+ type: Unconfined
+
+```
+
+Usage
+
+```shell
+kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper-library/master/library/pod-security-policy/seccompv2/samples/psp-seccomp/example_allowed_exempt_image.yaml
+```
+
+
+
+disallowed-ephemeral
+
+```yaml
+apiVersion: v1
+kind: Pod
+metadata:
+ name: nginx-seccomp-disallowed
+ labels:
+ app: nginx-seccomp
+spec:
+ ephemeralContainers:
+ - name: nginx
+ image: nginx
+
+```
+
+Usage
+
+```shell
+kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper-library/master/library/pod-security-policy/seccompv2/samples/psp-seccomp/disallowed_ephemeral.yaml
+```
+
+
+
+
+
\ No newline at end of file
diff --git a/website/sidebars.js b/website/sidebars.js
index a14f9496c..5710b4a6e 100644
--- a/website/sidebars.js
+++ b/website/sidebars.js
@@ -68,6 +68,7 @@ module.exports = {
'validation/proc-mount',
'validation/read-only-root-filesystem',
'validation/seccomp',
+ 'validation/seccompv2',
'validation/selinux',
'validation/users',
'validation/volumes',