diff --git a/artifacthub/library/pod-security-policy/apparmor/1.1.0/artifacthub-pkg.yml b/artifacthub/library/pod-security-policy/apparmor/1.1.0/artifacthub-pkg.yml
new file mode 100644
index 000000000..2212230ed
--- /dev/null
+++ b/artifacthub/library/pod-security-policy/apparmor/1.1.0/artifacthub-pkg.yml
@@ -0,0 +1,22 @@
+version: 1.1.0
+name: k8spspapparmor
+displayName: App Armor
+createdAt: "2024-05-29T23:39:01Z"
+description: Configures an allow-list of AppArmor profiles for use by containers. This corresponds to specific annotations applied to a PodSecurityPolicy. For information on AppArmor, see https://kubernetes.io/docs/tutorials/clusters/apparmor/
+digest: d77b1285f65085153e6a6e6ac86dc32195591df467c3162abe8cc6c806cab69a
+license: Apache-2.0
+homeURL: https://open-policy-agent.github.io/gatekeeper-library/website/apparmor
+keywords:
+ - gatekeeper
+ - open-policy-agent
+ - policies
+readme: |-
+ # App Armor
+ Configures an allow-list of AppArmor profiles for use by containers. This corresponds to specific annotations applied to a PodSecurityPolicy. For information on AppArmor, see https://kubernetes.io/docs/tutorials/clusters/apparmor/
+install: |-
+ ### Usage
+ ```shell
+ kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper-library/master/artifacthub/library/pod-security-policy/apparmor/1.1.0/template.yaml
+ ```
+provider:
+ name: Gatekeeper Library
diff --git a/artifacthub/library/pod-security-policy/apparmor/1.1.0/kustomization.yaml b/artifacthub/library/pod-security-policy/apparmor/1.1.0/kustomization.yaml
new file mode 100644
index 000000000..7d70d11b7
--- /dev/null
+++ b/artifacthub/library/pod-security-policy/apparmor/1.1.0/kustomization.yaml
@@ -0,0 +1,2 @@
+resources:
+ - template.yaml
diff --git a/artifacthub/library/pod-security-policy/apparmor/1.1.0/samples/psp-apparmor/constraint.yaml b/artifacthub/library/pod-security-policy/apparmor/1.1.0/samples/psp-apparmor/constraint.yaml
new file mode 100644
index 000000000..cf9b9d3d1
--- /dev/null
+++ b/artifacthub/library/pod-security-policy/apparmor/1.1.0/samples/psp-apparmor/constraint.yaml
@@ -0,0 +1,12 @@
+apiVersion: constraints.gatekeeper.sh/v1beta1
+kind: K8sPSPAppArmor
+metadata:
+ name: psp-apparmor
+spec:
+ match:
+ kinds:
+ - apiGroups: [""]
+ kinds: ["Pod"]
+ parameters:
+ allowedProfiles:
+ - localhost/custom
diff --git a/artifacthub/library/pod-security-policy/apparmor/1.1.0/samples/psp-apparmor/disallowed_ephemeral.yaml b/artifacthub/library/pod-security-policy/apparmor/1.1.0/samples/psp-apparmor/disallowed_ephemeral.yaml
new file mode 100644
index 000000000..cd6fc5f81
--- /dev/null
+++ b/artifacthub/library/pod-security-policy/apparmor/1.1.0/samples/psp-apparmor/disallowed_ephemeral.yaml
@@ -0,0 +1,13 @@
+apiVersion: v1
+kind: Pod
+metadata:
+ name: nginx-apparmor-disallowed
+ annotations:
+ # apparmor.security.beta.kubernetes.io/pod: unconfined # runtime/default
+ container.apparmor.security.beta.kubernetes.io/nginx: unconfined
+ labels:
+ app: nginx-apparmor
+spec:
+ ephemeralContainers:
+ - name: nginx
+ image: nginx
diff --git a/artifacthub/library/pod-security-policy/apparmor/1.1.0/samples/psp-apparmor/example_allowed.yaml b/artifacthub/library/pod-security-policy/apparmor/1.1.0/samples/psp-apparmor/example_allowed.yaml
new file mode 100644
index 000000000..204fa00b5
--- /dev/null
+++ b/artifacthub/library/pod-security-policy/apparmor/1.1.0/samples/psp-apparmor/example_allowed.yaml
@@ -0,0 +1,13 @@
+apiVersion: v1
+kind: Pod
+metadata:
+ name: nginx-apparmor-allowed
+ annotations:
+ # apparmor.security.beta.kubernetes.io/pod: unconfined # runtime/default
+ container.apparmor.security.beta.kubernetes.io/nginx: localhost/custom
+ labels:
+ app: nginx-apparmor
+spec:
+ containers:
+ - name: nginx
+ image: nginx
diff --git a/artifacthub/library/pod-security-policy/apparmor/1.1.0/samples/psp-apparmor/example_allowed_container.yaml b/artifacthub/library/pod-security-policy/apparmor/1.1.0/samples/psp-apparmor/example_allowed_container.yaml
new file mode 100644
index 000000000..3d28665d5
--- /dev/null
+++ b/artifacthub/library/pod-security-policy/apparmor/1.1.0/samples/psp-apparmor/example_allowed_container.yaml
@@ -0,0 +1,14 @@
+apiVersion: v1
+kind: Pod
+metadata:
+ name: nginx-apparmor-allowed
+ labels:
+ app: nginx-apparmor
+spec:
+ containers:
+ - name: nginx
+ image: nginx
+ securityContext:
+ appArmorProfile:
+ type: "Localhost"
+ localhostProfile: "custom"
diff --git a/artifacthub/library/pod-security-policy/apparmor/1.1.0/samples/psp-apparmor/example_allowed_override.yaml b/artifacthub/library/pod-security-policy/apparmor/1.1.0/samples/psp-apparmor/example_allowed_override.yaml
new file mode 100644
index 000000000..171694985
--- /dev/null
+++ b/artifacthub/library/pod-security-policy/apparmor/1.1.0/samples/psp-apparmor/example_allowed_override.yaml
@@ -0,0 +1,17 @@
+apiVersion: v1
+kind: Pod
+metadata:
+ name: nginx-apparmor-allowed
+ labels:
+ app: nginx-apparmor
+spec:
+ securityContext:
+ appArmorProfile:
+ type: "Unconfined"
+ containers:
+ - name: nginx
+ image: nginx
+ securityContext:
+ appArmorProfile:
+ type: "Localhost"
+ localhostProfile: "custom"
diff --git a/artifacthub/library/pod-security-policy/apparmor/1.1.0/samples/psp-apparmor/example_allowed_pod.yaml b/artifacthub/library/pod-security-policy/apparmor/1.1.0/samples/psp-apparmor/example_allowed_pod.yaml
new file mode 100644
index 000000000..6f71d6b3c
--- /dev/null
+++ b/artifacthub/library/pod-security-policy/apparmor/1.1.0/samples/psp-apparmor/example_allowed_pod.yaml
@@ -0,0 +1,14 @@
+apiVersion: v1
+kind: Pod
+metadata:
+ name: nginx-apparmor-allowed
+ labels:
+ app: nginx-apparmor
+spec:
+ securityContext:
+ appArmorProfile:
+ type: "Localhost"
+ localhostProfile: "custom"
+ containers:
+ - name: nginx
+ image: nginx
diff --git a/artifacthub/library/pod-security-policy/apparmor/1.1.0/samples/psp-apparmor/example_disallowed.yaml b/artifacthub/library/pod-security-policy/apparmor/1.1.0/samples/psp-apparmor/example_disallowed.yaml
new file mode 100644
index 000000000..8ffdaf768
--- /dev/null
+++ b/artifacthub/library/pod-security-policy/apparmor/1.1.0/samples/psp-apparmor/example_disallowed.yaml
@@ -0,0 +1,13 @@
+apiVersion: v1
+kind: Pod
+metadata:
+ name: nginx-apparmor-disallowed
+ annotations:
+ # apparmor.security.beta.kubernetes.io/pod: unconfined # runtime/default
+ container.apparmor.security.beta.kubernetes.io/nginx: unconfined
+ labels:
+ app: nginx-apparmor
+spec:
+ containers:
+ - name: nginx
+ image: nginx
diff --git a/artifacthub/library/pod-security-policy/apparmor/1.1.0/samples/psp-apparmor/example_disallowed_no_profile.yaml b/artifacthub/library/pod-security-policy/apparmor/1.1.0/samples/psp-apparmor/example_disallowed_no_profile.yaml
new file mode 100644
index 000000000..4e99cd25d
--- /dev/null
+++ b/artifacthub/library/pod-security-policy/apparmor/1.1.0/samples/psp-apparmor/example_disallowed_no_profile.yaml
@@ -0,0 +1,10 @@
+apiVersion: v1
+kind: Pod
+metadata:
+ name: nginx-apparmor-disallowed
+ labels:
+ app: nginx-apparmor
+spec:
+ containers:
+ - name: nginx
+ image: nginx
diff --git a/artifacthub/library/pod-security-policy/apparmor/1.1.0/samples/psp-apparmor/example_disallowed_override.yaml b/artifacthub/library/pod-security-policy/apparmor/1.1.0/samples/psp-apparmor/example_disallowed_override.yaml
new file mode 100644
index 000000000..aa4c98301
--- /dev/null
+++ b/artifacthub/library/pod-security-policy/apparmor/1.1.0/samples/psp-apparmor/example_disallowed_override.yaml
@@ -0,0 +1,17 @@
+apiVersion: v1
+kind: Pod
+metadata:
+ name: nginx-apparmor-allowed
+ labels:
+ app: nginx-apparmor
+spec:
+ securityContext:
+ appArmorProfile:
+ type: "Localhost"
+ localhostProfile: "custom"
+ containers:
+ - name: nginx
+ image: nginx
+ securityContext:
+ appArmorProfile:
+ type: "Unconfined"
diff --git a/artifacthub/library/pod-security-policy/apparmor/1.1.0/suite.yaml b/artifacthub/library/pod-security-policy/apparmor/1.1.0/suite.yaml
new file mode 100644
index 000000000..861048ba9
--- /dev/null
+++ b/artifacthub/library/pod-security-policy/apparmor/1.1.0/suite.yaml
@@ -0,0 +1,41 @@
+kind: Suite
+apiVersion: test.gatekeeper.sh/v1alpha1
+metadata:
+ name: apparmor
+tests:
+ - name: apparmor
+ template: template.yaml
+ constraint: samples/psp-apparmor/constraint.yaml
+ cases:
+ - name: example-allowed
+ object: samples/psp-apparmor/example_allowed.yaml
+ assertions:
+ - violations: no
+ - name: example-allowed-container
+ object: samples/psp-apparmor/example_allowed_container.yaml
+ assertions:
+ - violations: no
+ - name: example-allowed-pod
+ object: samples/psp-apparmor/example_allowed_pod.yaml
+ assertions:
+ - violations: no
+ - name: example-allowed-override
+ object: samples/psp-apparmor/example_allowed_override.yaml
+ assertions:
+ - violations: no
+ - name: example-disallowed
+ object: samples/psp-apparmor/example_disallowed.yaml
+ assertions:
+ - violations: yes
+ - name: example-disallowed-override
+ object: samples/psp-apparmor/example_disallowed_override.yaml
+ assertions:
+ - violations: yes
+ - name: example-disallowed-no-profile
+ object: samples/psp-apparmor/example_disallowed_no_profile.yaml
+ assertions:
+ - violations: yes
+ - name: disallowed-ephemeral
+ object: samples/psp-apparmor/disallowed_ephemeral.yaml
+ assertions:
+ - violations: yes
diff --git a/artifacthub/library/pod-security-policy/apparmor/1.1.0/template.yaml b/artifacthub/library/pod-security-policy/apparmor/1.1.0/template.yaml
new file mode 100644
index 000000000..63aa13f68
--- /dev/null
+++ b/artifacthub/library/pod-security-policy/apparmor/1.1.0/template.yaml
@@ -0,0 +1,218 @@
+apiVersion: templates.gatekeeper.sh/v1
+kind: ConstraintTemplate
+metadata:
+ name: k8spspapparmor
+ annotations:
+ metadata.gatekeeper.sh/title: "App Armor"
+ metadata.gatekeeper.sh/version: 1.1.0
+ description: >-
+ Configures an allow-list of AppArmor profiles for use by containers.
+ This corresponds to specific annotations applied to a PodSecurityPolicy.
+ For information on AppArmor, see
+ https://kubernetes.io/docs/tutorials/clusters/apparmor/
+spec:
+ crd:
+ spec:
+ names:
+ kind: K8sPSPAppArmor
+ validation:
+ # Schema for the `parameters` field
+ openAPIV3Schema:
+ type: object
+ description: >-
+ Configures an allow-list of AppArmor profiles for use by containers.
+ This corresponds to specific annotations applied to a PodSecurityPolicy.
+ For information on AppArmor, see
+ https://kubernetes.io/docs/tutorials/clusters/apparmor/
+ 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:
+ description: "An array of AppArmor profiles. Examples: `runtime/default`, `unconfined`."
+ type: array
+ 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: podAppArmor
+ expression: 'has(variables.anyObject.spec.securityContext) && has(variables.anyObject.spec.securityContext.appArmorProfile) ? variables.anyObject.spec.securityContext.appArmorProfile : null'
+ - name: canonicalPodAppArmor
+ expression: |
+ variables.podAppArmor == null ? "runtime/default" :
+ variables.podAppArmor.type == "RuntimeDefault" ? "runtime/default" :
+ variables.podAppArmor.type == "Unconfined" ? "unconfined" :
+ variables.podAppArmor.type == "Localhost" ? "localhost/" + variables.podAppArmor.localhostProfile : ""
+ # break this mapping up by container type (regular/init/ephemeral) to avoid problems with name collisions,
+ # which may be a problem when running shift-left (no K8s API server to enforce uniqueness of container names)
+ - name: appArmorByContainer
+ expression: |
+ variables.containers.map(container, [container.name,
+ has(container.securityContext) && has(container.securityContext.appArmorProfile) ?
+ (container.securityContext.appArmorProfile.type == "RuntimeDefault" ? "runtime/default" :
+ container.securityContext.appArmorProfile.type == "Unconfined" ? "unconfined" :
+ container.securityContext.appArmorProfile.type == "Localhost" ? "localhost/" + container.securityContext.appArmorProfile.localhostProfile : "") :
+ has(variables.anyObject.metadata.annotations) && ("container.apparmor.security.beta.kubernetes.io/" + container.name) in variables.anyObject.metadata.annotations ?
+ variables.anyObject.metadata.annotations["container.apparmor.security.beta.kubernetes.io/" + container.name] :
+ variables.canonicalPodAppArmor
+ ])
+ - name: appArmorByInitContainer
+ expression: |
+ variables.initContainers.map(container, [container.name,
+ has(container.securityContext) && has(container.securityContext.appArmorProfile) ?
+ (container.securityContext.appArmorProfile.type == "RuntimeDefault" ? "runtime/default" :
+ container.securityContext.appArmorProfile.type == "Unconfined" ? "unconfined" :
+ container.securityContext.appArmorProfile.type == "Localhost" ? "localhost/" + container.securityContext.appArmorProfile.localhostProfile : "") :
+ has(variables.anyObject.metadata.annotations) && ("container.apparmor.security.beta.kubernetes.io/" + container.name) in variables.anyObject.metadata.annotations ?
+ variables.anyObject.metadata.annotations["container.apparmor.security.beta.kubernetes.io/" + container.name] :
+ variables.canonicalPodAppArmor
+ ])
+ - name: appArmorByEphemeralContainer
+ expression: |
+ variables.ephemeralContainers.map(container, [container.name,
+ has(container.securityContext) && has(container.securityContext.appArmorProfile) ?
+ (container.securityContext.appArmorProfile.type == "RuntimeDefault" ? "runtime/default" :
+ container.securityContext.appArmorProfile.type == "Unconfined" ? "unconfined" :
+ container.securityContext.appArmorProfile.type == "Localhost" ? "localhost/" + container.securityContext.appArmorProfile.localhostProfile : "") :
+ has(variables.anyObject.metadata.annotations) && ("container.apparmor.security.beta.kubernetes.io/" + container.name) in variables.anyObject.metadata.annotations ?
+ variables.anyObject.metadata.annotations["container.apparmor.security.beta.kubernetes.io/" + container.name] :
+ variables.canonicalPodAppArmor
+ ])
+ - 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)
+ validations:
+ - expression: |
+ variables.containers.all(container,
+ (container.image in variables.exemptImages) ||
+ variables.appArmorByContainer.exists(pair, pair[0] == container.name && pair[1] in variables.params.allowedProfiles)
+ )
+ messageExpression: '"AppArmor profile is not allowed. Allowed Profiles: " + variables.params.allowedProfiles.join(", ")'
+ - expression: |
+ variables.initContainers.all(container,
+ (container.image in variables.exemptImages) ||
+ variables.appArmorByInitContainer.exists(pair, pair[0] == container.name && pair[1] in variables.params.allowedProfiles)
+ )
+ messageExpression: '"AppArmor profile is not allowed. Allowed Profiles: " + variables.params.allowedProfiles.join(", ")'
+ - expression: |
+ variables.ephemeralContainers.all(container,
+ (container.image in variables.exemptImages) ||
+ variables.appArmorByEphemeralContainer.exists(pair, pair[0] == container.name && pair[1] in variables.params.allowedProfiles)
+ )
+ messageExpression: '"AppArmor profile is not allowed. Allowed Profiles: " + variables.params.allowedProfiles.join(", ")'
+ - engine: Rego
+ source:
+ rego: |
+ package k8spspapparmor
+
+ import data.lib.exempt_container.is_exempt
+
+ violation[{"msg": msg, "details": {}}] {
+ container := input_containers[_]
+ not is_exempt(container)
+ not input_apparmor_allowed(input.review.object, container)
+ msg := sprintf("AppArmor profile is not allowed, pod: %v, container: %v. Allowed profiles: %v", [input.review.object.metadata.name, container.name, input.parameters.allowedProfiles])
+ }
+
+ input_apparmor_allowed(pod, container) {
+ get_apparmor_profile(pod, container) == input.parameters.allowedProfiles[_]
+ }
+
+ input_containers[c] {
+ c := input.review.object.spec.containers[_]
+ }
+ input_containers[c] {
+ c := input.review.object.spec.initContainers[_]
+ }
+ input_containers[c] {
+ c := input.review.object.spec.ephemeralContainers[_]
+ }
+
+ get_apparmor_profile(_, container) = out {
+ profile := object.get(container, ["securityContext", "appArmorProfile"], null)
+ profile != null
+ out := canonicalize_apparmor_profile(profile)
+ }
+
+ get_apparmor_profile(pod, container) = out {
+ profile := object.get(container, ["securityContext", "appArmorProfile"], null)
+ profile == null
+ out := pod.metadata.annotations[sprintf("container.apparmor.security.beta.kubernetes.io/%v", [container.name])]
+ }
+
+ get_apparmor_profile(pod, container) = out {
+ profile := object.get(container, ["securityContext", "appArmorProfile"], null)
+ profile == null
+ not pod.metadata.annotations[sprintf("container.apparmor.security.beta.kubernetes.io/%v", [container.name])]
+ out := canonicalize_apparmor_profile(object.get(pod, ["spec", "securityContext", "appArmorProfile"], null))
+ }
+
+ canonicalize_apparmor_profile(profile) = out {
+ profile.type == "RuntimeDefault"
+ out := "runtime/default"
+ }
+
+ canonicalize_apparmor_profile(profile) = out {
+ profile.type == "Unconfined"
+ out := "unconfined"
+ }
+
+ canonicalize_apparmor_profile(profile) = out {
+ profile.type = "Localhost"
+ out := sprintf("localhost/%s", [profile.localhostProfile])
+ }
+
+ canonicalize_apparmor_profile(profile) = out {
+ profile == null
+ out := "runtime/default"
+ }
+ 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/apparmor/samples/psp-apparmor/constraint.yaml b/library/pod-security-policy/apparmor/samples/psp-apparmor/constraint.yaml
index 7024f1b2e..cf9b9d3d1 100644
--- a/library/pod-security-policy/apparmor/samples/psp-apparmor/constraint.yaml
+++ b/library/pod-security-policy/apparmor/samples/psp-apparmor/constraint.yaml
@@ -9,4 +9,4 @@ spec:
kinds: ["Pod"]
parameters:
allowedProfiles:
- - runtime/default
+ - localhost/custom
diff --git a/library/pod-security-policy/apparmor/samples/psp-apparmor/example_allowed.yaml b/library/pod-security-policy/apparmor/samples/psp-apparmor/example_allowed.yaml
index 661f2f00d..204fa00b5 100644
--- a/library/pod-security-policy/apparmor/samples/psp-apparmor/example_allowed.yaml
+++ b/library/pod-security-policy/apparmor/samples/psp-apparmor/example_allowed.yaml
@@ -4,7 +4,7 @@ metadata:
name: nginx-apparmor-allowed
annotations:
# apparmor.security.beta.kubernetes.io/pod: unconfined # runtime/default
- container.apparmor.security.beta.kubernetes.io/nginx: runtime/default
+ container.apparmor.security.beta.kubernetes.io/nginx: localhost/custom
labels:
app: nginx-apparmor
spec:
diff --git a/library/pod-security-policy/apparmor/samples/psp-apparmor/example_allowed_container.yaml b/library/pod-security-policy/apparmor/samples/psp-apparmor/example_allowed_container.yaml
new file mode 100644
index 000000000..3d28665d5
--- /dev/null
+++ b/library/pod-security-policy/apparmor/samples/psp-apparmor/example_allowed_container.yaml
@@ -0,0 +1,14 @@
+apiVersion: v1
+kind: Pod
+metadata:
+ name: nginx-apparmor-allowed
+ labels:
+ app: nginx-apparmor
+spec:
+ containers:
+ - name: nginx
+ image: nginx
+ securityContext:
+ appArmorProfile:
+ type: "Localhost"
+ localhostProfile: "custom"
diff --git a/library/pod-security-policy/apparmor/samples/psp-apparmor/example_allowed_override.yaml b/library/pod-security-policy/apparmor/samples/psp-apparmor/example_allowed_override.yaml
new file mode 100644
index 000000000..171694985
--- /dev/null
+++ b/library/pod-security-policy/apparmor/samples/psp-apparmor/example_allowed_override.yaml
@@ -0,0 +1,17 @@
+apiVersion: v1
+kind: Pod
+metadata:
+ name: nginx-apparmor-allowed
+ labels:
+ app: nginx-apparmor
+spec:
+ securityContext:
+ appArmorProfile:
+ type: "Unconfined"
+ containers:
+ - name: nginx
+ image: nginx
+ securityContext:
+ appArmorProfile:
+ type: "Localhost"
+ localhostProfile: "custom"
diff --git a/library/pod-security-policy/apparmor/samples/psp-apparmor/example_allowed_pod.yaml b/library/pod-security-policy/apparmor/samples/psp-apparmor/example_allowed_pod.yaml
new file mode 100644
index 000000000..6f71d6b3c
--- /dev/null
+++ b/library/pod-security-policy/apparmor/samples/psp-apparmor/example_allowed_pod.yaml
@@ -0,0 +1,14 @@
+apiVersion: v1
+kind: Pod
+metadata:
+ name: nginx-apparmor-allowed
+ labels:
+ app: nginx-apparmor
+spec:
+ securityContext:
+ appArmorProfile:
+ type: "Localhost"
+ localhostProfile: "custom"
+ containers:
+ - name: nginx
+ image: nginx
diff --git a/library/pod-security-policy/apparmor/samples/psp-apparmor/example_disallowed_no_profile.yaml b/library/pod-security-policy/apparmor/samples/psp-apparmor/example_disallowed_no_profile.yaml
new file mode 100644
index 000000000..4e99cd25d
--- /dev/null
+++ b/library/pod-security-policy/apparmor/samples/psp-apparmor/example_disallowed_no_profile.yaml
@@ -0,0 +1,10 @@
+apiVersion: v1
+kind: Pod
+metadata:
+ name: nginx-apparmor-disallowed
+ labels:
+ app: nginx-apparmor
+spec:
+ containers:
+ - name: nginx
+ image: nginx
diff --git a/library/pod-security-policy/apparmor/samples/psp-apparmor/example_disallowed_override.yaml b/library/pod-security-policy/apparmor/samples/psp-apparmor/example_disallowed_override.yaml
new file mode 100644
index 000000000..aa4c98301
--- /dev/null
+++ b/library/pod-security-policy/apparmor/samples/psp-apparmor/example_disallowed_override.yaml
@@ -0,0 +1,17 @@
+apiVersion: v1
+kind: Pod
+metadata:
+ name: nginx-apparmor-allowed
+ labels:
+ app: nginx-apparmor
+spec:
+ securityContext:
+ appArmorProfile:
+ type: "Localhost"
+ localhostProfile: "custom"
+ containers:
+ - name: nginx
+ image: nginx
+ securityContext:
+ appArmorProfile:
+ type: "Unconfined"
diff --git a/library/pod-security-policy/apparmor/suite.yaml b/library/pod-security-policy/apparmor/suite.yaml
index a75ebe1eb..861048ba9 100644
--- a/library/pod-security-policy/apparmor/suite.yaml
+++ b/library/pod-security-policy/apparmor/suite.yaml
@@ -11,10 +11,30 @@ tests:
object: samples/psp-apparmor/example_allowed.yaml
assertions:
- violations: no
+ - name: example-allowed-container
+ object: samples/psp-apparmor/example_allowed_container.yaml
+ assertions:
+ - violations: no
+ - name: example-allowed-pod
+ object: samples/psp-apparmor/example_allowed_pod.yaml
+ assertions:
+ - violations: no
+ - name: example-allowed-override
+ object: samples/psp-apparmor/example_allowed_override.yaml
+ assertions:
+ - violations: no
- name: example-disallowed
object: samples/psp-apparmor/example_disallowed.yaml
assertions:
- violations: yes
+ - name: example-disallowed-override
+ object: samples/psp-apparmor/example_disallowed_override.yaml
+ assertions:
+ - violations: yes
+ - name: example-disallowed-no-profile
+ object: samples/psp-apparmor/example_disallowed_no_profile.yaml
+ assertions:
+ - violations: yes
- name: disallowed-ephemeral
object: samples/psp-apparmor/disallowed_ephemeral.yaml
assertions:
diff --git a/library/pod-security-policy/apparmor/template.yaml b/library/pod-security-policy/apparmor/template.yaml
index babad1318..63aa13f68 100644
--- a/library/pod-security-policy/apparmor/template.yaml
+++ b/library/pod-security-policy/apparmor/template.yaml
@@ -4,7 +4,7 @@ metadata:
name: k8spspapparmor
annotations:
metadata.gatekeeper.sh/title: "App Armor"
- metadata.gatekeeper.sh/version: 1.0.0
+ metadata.gatekeeper.sh/version: 1.1.0
description: >-
Configures an allow-list of AppArmor profiles for use by containers.
This corresponds to specific annotations applied to a PodSecurityPolicy.
@@ -42,58 +42,177 @@ spec:
type: string
targets:
- target: admission.k8s.gatekeeper.sh
- rego: |
- package k8spspapparmor
-
- import data.lib.exempt_container.is_exempt
-
- violation[{"msg": msg, "details": {}}] {
- metadata := input.review.object.metadata
- container := input_containers[_]
- not is_exempt(container)
- not input_apparmor_allowed(container, metadata)
- msg := sprintf("AppArmor profile is not allowed, pod: %v, container: %v. Allowed profiles: %v", [input.review.object.metadata.name, container.name, input.parameters.allowedProfiles])
- }
-
- input_apparmor_allowed(container, metadata) {
- get_annotation_for(container, metadata) == input.parameters.allowedProfiles[_]
- }
-
- input_containers[c] {
- c := input.review.object.spec.containers[_]
- }
- input_containers[c] {
- c := input.review.object.spec.initContainers[_]
- }
- input_containers[c] {
- c := input.review.object.spec.ephemeralContainers[_]
- }
-
- get_annotation_for(container, metadata) = out {
- out = metadata.annotations[sprintf("container.apparmor.security.beta.kubernetes.io/%v", [container.name])]
- }
- get_annotation_for(container, metadata) = out {
- not metadata.annotations[sprintf("container.apparmor.security.beta.kubernetes.io/%v", [container.name])]
- out = "runtime/default"
- }
- 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: podAppArmor
+ expression: 'has(variables.anyObject.spec.securityContext) && has(variables.anyObject.spec.securityContext.appArmorProfile) ? variables.anyObject.spec.securityContext.appArmorProfile : null'
+ - name: canonicalPodAppArmor
+ expression: |
+ variables.podAppArmor == null ? "runtime/default" :
+ variables.podAppArmor.type == "RuntimeDefault" ? "runtime/default" :
+ variables.podAppArmor.type == "Unconfined" ? "unconfined" :
+ variables.podAppArmor.type == "Localhost" ? "localhost/" + variables.podAppArmor.localhostProfile : ""
+ # break this mapping up by container type (regular/init/ephemeral) to avoid problems with name collisions,
+ # which may be a problem when running shift-left (no K8s API server to enforce uniqueness of container names)
+ - name: appArmorByContainer
+ expression: |
+ variables.containers.map(container, [container.name,
+ has(container.securityContext) && has(container.securityContext.appArmorProfile) ?
+ (container.securityContext.appArmorProfile.type == "RuntimeDefault" ? "runtime/default" :
+ container.securityContext.appArmorProfile.type == "Unconfined" ? "unconfined" :
+ container.securityContext.appArmorProfile.type == "Localhost" ? "localhost/" + container.securityContext.appArmorProfile.localhostProfile : "") :
+ has(variables.anyObject.metadata.annotations) && ("container.apparmor.security.beta.kubernetes.io/" + container.name) in variables.anyObject.metadata.annotations ?
+ variables.anyObject.metadata.annotations["container.apparmor.security.beta.kubernetes.io/" + container.name] :
+ variables.canonicalPodAppArmor
+ ])
+ - name: appArmorByInitContainer
+ expression: |
+ variables.initContainers.map(container, [container.name,
+ has(container.securityContext) && has(container.securityContext.appArmorProfile) ?
+ (container.securityContext.appArmorProfile.type == "RuntimeDefault" ? "runtime/default" :
+ container.securityContext.appArmorProfile.type == "Unconfined" ? "unconfined" :
+ container.securityContext.appArmorProfile.type == "Localhost" ? "localhost/" + container.securityContext.appArmorProfile.localhostProfile : "") :
+ has(variables.anyObject.metadata.annotations) && ("container.apparmor.security.beta.kubernetes.io/" + container.name) in variables.anyObject.metadata.annotations ?
+ variables.anyObject.metadata.annotations["container.apparmor.security.beta.kubernetes.io/" + container.name] :
+ variables.canonicalPodAppArmor
+ ])
+ - name: appArmorByEphemeralContainer
+ expression: |
+ variables.ephemeralContainers.map(container, [container.name,
+ has(container.securityContext) && has(container.securityContext.appArmorProfile) ?
+ (container.securityContext.appArmorProfile.type == "RuntimeDefault" ? "runtime/default" :
+ container.securityContext.appArmorProfile.type == "Unconfined" ? "unconfined" :
+ container.securityContext.appArmorProfile.type == "Localhost" ? "localhost/" + container.securityContext.appArmorProfile.localhostProfile : "") :
+ has(variables.anyObject.metadata.annotations) && ("container.apparmor.security.beta.kubernetes.io/" + container.name) in variables.anyObject.metadata.annotations ?
+ variables.anyObject.metadata.annotations["container.apparmor.security.beta.kubernetes.io/" + container.name] :
+ variables.canonicalPodAppArmor
+ ])
+ - 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)
+ validations:
+ - expression: |
+ variables.containers.all(container,
+ (container.image in variables.exemptImages) ||
+ variables.appArmorByContainer.exists(pair, pair[0] == container.name && pair[1] in variables.params.allowedProfiles)
+ )
+ messageExpression: '"AppArmor profile is not allowed. Allowed Profiles: " + variables.params.allowedProfiles.join(", ")'
+ - expression: |
+ variables.initContainers.all(container,
+ (container.image in variables.exemptImages) ||
+ variables.appArmorByInitContainer.exists(pair, pair[0] == container.name && pair[1] in variables.params.allowedProfiles)
+ )
+ messageExpression: '"AppArmor profile is not allowed. Allowed Profiles: " + variables.params.allowedProfiles.join(", ")'
+ - expression: |
+ variables.ephemeralContainers.all(container,
+ (container.image in variables.exemptImages) ||
+ variables.appArmorByEphemeralContainer.exists(pair, pair[0] == container.name && pair[1] in variables.params.allowedProfiles)
+ )
+ messageExpression: '"AppArmor profile is not allowed. Allowed Profiles: " + variables.params.allowedProfiles.join(", ")'
+ - engine: Rego
+ source:
+ rego: |
+ package k8spspapparmor
+
+ import data.lib.exempt_container.is_exempt
+
+ violation[{"msg": msg, "details": {}}] {
+ container := input_containers[_]
+ not is_exempt(container)
+ not input_apparmor_allowed(input.review.object, container)
+ msg := sprintf("AppArmor profile is not allowed, pod: %v, container: %v. Allowed profiles: %v", [input.review.object.metadata.name, container.name, input.parameters.allowedProfiles])
+ }
+
+ input_apparmor_allowed(pod, container) {
+ get_apparmor_profile(pod, container) == input.parameters.allowedProfiles[_]
+ }
+
+ input_containers[c] {
+ c := input.review.object.spec.containers[_]
+ }
+ input_containers[c] {
+ c := input.review.object.spec.initContainers[_]
+ }
+ input_containers[c] {
+ c := input.review.object.spec.ephemeralContainers[_]
+ }
+
+ get_apparmor_profile(_, container) = out {
+ profile := object.get(container, ["securityContext", "appArmorProfile"], null)
+ profile != null
+ out := canonicalize_apparmor_profile(profile)
+ }
+
+ get_apparmor_profile(pod, container) = out {
+ profile := object.get(container, ["securityContext", "appArmorProfile"], null)
+ profile == null
+ out := pod.metadata.annotations[sprintf("container.apparmor.security.beta.kubernetes.io/%v", [container.name])]
+ }
+
+ get_apparmor_profile(pod, container) = out {
+ profile := object.get(container, ["securityContext", "appArmorProfile"], null)
+ profile == null
+ not pod.metadata.annotations[sprintf("container.apparmor.security.beta.kubernetes.io/%v", [container.name])]
+ out := canonicalize_apparmor_profile(object.get(pod, ["spec", "securityContext", "appArmorProfile"], null))
+ }
+
+ canonicalize_apparmor_profile(profile) = out {
+ profile.type == "RuntimeDefault"
+ out := "runtime/default"
+ }
+
+ canonicalize_apparmor_profile(profile) = out {
+ profile.type == "Unconfined"
+ out := "unconfined"
+ }
+
+ canonicalize_apparmor_profile(profile) = out {
+ profile.type = "Localhost"
+ out := sprintf("localhost/%s", [profile.localhostProfile])
+ }
+
+ canonicalize_apparmor_profile(profile) = out {
+ profile == null
+ out := "runtime/default"
+ }
+ 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/apparmor/constraint.tmpl b/src/pod-security-policy/apparmor/constraint.tmpl
index 7951cb5ef..3bf86f42b 100644
--- a/src/pod-security-policy/apparmor/constraint.tmpl
+++ b/src/pod-security-policy/apparmor/constraint.tmpl
@@ -4,7 +4,7 @@ metadata:
name: k8spspapparmor
annotations:
metadata.gatekeeper.sh/title: "App Armor"
- metadata.gatekeeper.sh/version: 1.0.0
+ metadata.gatekeeper.sh/version: 1.1.0
description: >-
Configures an allow-list of AppArmor profiles for use by containers.
This corresponds to specific annotations applied to a PodSecurityPolicy.
@@ -42,8 +42,15 @@ spec:
type: string
targets:
- target: admission.k8s.gatekeeper.sh
- rego: |
-{{ file.Read "src/pod-security-policy/apparmor/src.rego" | strings.Indent 8 | strings.TrimSuffix "\n" }}
- libs:
- - |
-{{ file.Read "src/pod-security-policy/apparmor/lib_exempt_container.rego" | strings.Indent 10 | strings.TrimSuffix "\n" }}
+ code:
+ - engine: K8sNativeValidation
+ source:
+{{ file.Read "src/pod-security-policy/apparmor/src.cel" | strings.Indent 10 | strings.TrimSuffix "\n" }}
+ - engine: Rego
+ source:
+ rego: |
+{{ file.Read "src/pod-security-policy/apparmor/src.rego" | strings.Indent 12 | strings.TrimSuffix "\n" }}
+ libs:
+ - |
+{{ file.Read "src/pod-security-policy/apparmor/lib_exempt_container.rego" | strings.Indent 12 | strings.TrimSuffix "\n" }}
+
diff --git a/src/pod-security-policy/apparmor/src.cel b/src/pod-security-policy/apparmor/src.cel
new file mode 100644
index 000000000..25539abca
--- /dev/null
+++ b/src/pod-security-policy/apparmor/src.cel
@@ -0,0 +1,83 @@
+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: podAppArmor
+ expression: 'has(variables.anyObject.spec.securityContext) && has(variables.anyObject.spec.securityContext.appArmorProfile) ? variables.anyObject.spec.securityContext.appArmorProfile : null'
+- name: canonicalPodAppArmor
+ expression: |
+ variables.podAppArmor == null ? "runtime/default" :
+ variables.podAppArmor.type == "RuntimeDefault" ? "runtime/default" :
+ variables.podAppArmor.type == "Unconfined" ? "unconfined" :
+ variables.podAppArmor.type == "Localhost" ? "localhost/" + variables.podAppArmor.localhostProfile : ""
+# break this mapping up by container type (regular/init/ephemeral) to avoid problems with name collisions,
+# which may be a problem when running shift-left (no K8s API server to enforce uniqueness of container names)
+- name: appArmorByContainer
+ expression: |
+ variables.containers.map(container, [container.name,
+ has(container.securityContext) && has(container.securityContext.appArmorProfile) ?
+ (container.securityContext.appArmorProfile.type == "RuntimeDefault" ? "runtime/default" :
+ container.securityContext.appArmorProfile.type == "Unconfined" ? "unconfined" :
+ container.securityContext.appArmorProfile.type == "Localhost" ? "localhost/" + container.securityContext.appArmorProfile.localhostProfile : "") :
+ has(variables.anyObject.metadata.annotations) && ("container.apparmor.security.beta.kubernetes.io/" + container.name) in variables.anyObject.metadata.annotations ?
+ variables.anyObject.metadata.annotations["container.apparmor.security.beta.kubernetes.io/" + container.name] :
+ variables.canonicalPodAppArmor
+ ])
+- name: appArmorByInitContainer
+ expression: |
+ variables.initContainers.map(container, [container.name,
+ has(container.securityContext) && has(container.securityContext.appArmorProfile) ?
+ (container.securityContext.appArmorProfile.type == "RuntimeDefault" ? "runtime/default" :
+ container.securityContext.appArmorProfile.type == "Unconfined" ? "unconfined" :
+ container.securityContext.appArmorProfile.type == "Localhost" ? "localhost/" + container.securityContext.appArmorProfile.localhostProfile : "") :
+ has(variables.anyObject.metadata.annotations) && ("container.apparmor.security.beta.kubernetes.io/" + container.name) in variables.anyObject.metadata.annotations ?
+ variables.anyObject.metadata.annotations["container.apparmor.security.beta.kubernetes.io/" + container.name] :
+ variables.canonicalPodAppArmor
+ ])
+- name: appArmorByEphemeralContainer
+ expression: |
+ variables.ephemeralContainers.map(container, [container.name,
+ has(container.securityContext) && has(container.securityContext.appArmorProfile) ?
+ (container.securityContext.appArmorProfile.type == "RuntimeDefault" ? "runtime/default" :
+ container.securityContext.appArmorProfile.type == "Unconfined" ? "unconfined" :
+ container.securityContext.appArmorProfile.type == "Localhost" ? "localhost/" + container.securityContext.appArmorProfile.localhostProfile : "") :
+ has(variables.anyObject.metadata.annotations) && ("container.apparmor.security.beta.kubernetes.io/" + container.name) in variables.anyObject.metadata.annotations ?
+ variables.anyObject.metadata.annotations["container.apparmor.security.beta.kubernetes.io/" + container.name] :
+ variables.canonicalPodAppArmor
+ ])
+- 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)
+validations:
+- expression: |
+ variables.containers.all(container,
+ (container.image in variables.exemptImages) ||
+ variables.appArmorByContainer.exists(pair, pair[0] == container.name && pair[1] in variables.params.allowedProfiles)
+ )
+ messageExpression: '"AppArmor profile is not allowed. Allowed Profiles: " + variables.params.allowedProfiles.join(", ")'
+- expression: |
+ variables.initContainers.all(container,
+ (container.image in variables.exemptImages) ||
+ variables.appArmorByInitContainer.exists(pair, pair[0] == container.name && pair[1] in variables.params.allowedProfiles)
+ )
+ messageExpression: '"AppArmor profile is not allowed. Allowed Profiles: " + variables.params.allowedProfiles.join(", ")'
+- expression: |
+ variables.ephemeralContainers.all(container,
+ (container.image in variables.exemptImages) ||
+ variables.appArmorByEphemeralContainer.exists(pair, pair[0] == container.name && pair[1] in variables.params.allowedProfiles)
+ )
+ messageExpression: '"AppArmor profile is not allowed. Allowed Profiles: " + variables.params.allowedProfiles.join(", ")'
\ No newline at end of file
diff --git a/src/pod-security-policy/apparmor/src.rego b/src/pod-security-policy/apparmor/src.rego
index 613df8030..ef4e3fb76 100644
--- a/src/pod-security-policy/apparmor/src.rego
+++ b/src/pod-security-policy/apparmor/src.rego
@@ -3,15 +3,14 @@ package k8spspapparmor
import data.lib.exempt_container.is_exempt
violation[{"msg": msg, "details": {}}] {
- metadata := input.review.object.metadata
container := input_containers[_]
not is_exempt(container)
- not input_apparmor_allowed(container, metadata)
+ not input_apparmor_allowed(input.review.object, container)
msg := sprintf("AppArmor profile is not allowed, pod: %v, container: %v. Allowed profiles: %v", [input.review.object.metadata.name, container.name, input.parameters.allowedProfiles])
}
-input_apparmor_allowed(container, metadata) {
- get_annotation_for(container, metadata) == input.parameters.allowedProfiles[_]
+input_apparmor_allowed(pod, container) {
+ get_apparmor_profile(pod, container) == input.parameters.allowedProfiles[_]
}
input_containers[c] {
@@ -24,10 +23,41 @@ input_containers[c] {
c := input.review.object.spec.ephemeralContainers[_]
}
-get_annotation_for(container, metadata) = out {
- out = metadata.annotations[sprintf("container.apparmor.security.beta.kubernetes.io/%v", [container.name])]
+get_apparmor_profile(_, container) = out {
+ profile := object.get(container, ["securityContext", "appArmorProfile"], null)
+ profile != null
+ out := canonicalize_apparmor_profile(profile)
}
-get_annotation_for(container, metadata) = out {
- not metadata.annotations[sprintf("container.apparmor.security.beta.kubernetes.io/%v", [container.name])]
- out = "runtime/default"
+
+get_apparmor_profile(pod, container) = out {
+ profile := object.get(container, ["securityContext", "appArmorProfile"], null)
+ profile == null
+ out := pod.metadata.annotations[sprintf("container.apparmor.security.beta.kubernetes.io/%v", [container.name])]
+}
+
+get_apparmor_profile(pod, container) = out {
+ profile := object.get(container, ["securityContext", "appArmorProfile"], null)
+ profile == null
+ not pod.metadata.annotations[sprintf("container.apparmor.security.beta.kubernetes.io/%v", [container.name])]
+ out := canonicalize_apparmor_profile(object.get(pod, ["spec", "securityContext", "appArmorProfile"], null))
+}
+
+canonicalize_apparmor_profile(profile) = out {
+ profile.type == "RuntimeDefault"
+ out := "runtime/default"
+}
+
+canonicalize_apparmor_profile(profile) = out {
+ profile.type == "Unconfined"
+ out := "unconfined"
+}
+
+canonicalize_apparmor_profile(profile) = out {
+ profile.type = "Localhost"
+ out := sprintf("localhost/%s", [profile.localhostProfile])
+}
+
+canonicalize_apparmor_profile(profile) = out {
+ profile == null
+ out := "runtime/default"
}
diff --git a/website/docs/validation/apparmor.md b/website/docs/validation/apparmor.md
index c1e2c16d8..6ed740ddf 100644
--- a/website/docs/validation/apparmor.md
+++ b/website/docs/validation/apparmor.md
@@ -16,7 +16,7 @@ metadata:
name: k8spspapparmor
annotations:
metadata.gatekeeper.sh/title: "App Armor"
- metadata.gatekeeper.sh/version: 1.0.0
+ metadata.gatekeeper.sh/version: 1.1.0
description: >-
Configures an allow-list of AppArmor profiles for use by containers.
This corresponds to specific annotations applied to a PodSecurityPolicy.
@@ -54,61 +54,180 @@ spec:
type: string
targets:
- target: admission.k8s.gatekeeper.sh
- rego: |
- package k8spspapparmor
-
- import data.lib.exempt_container.is_exempt
-
- violation[{"msg": msg, "details": {}}] {
- metadata := input.review.object.metadata
- container := input_containers[_]
- not is_exempt(container)
- not input_apparmor_allowed(container, metadata)
- msg := sprintf("AppArmor profile is not allowed, pod: %v, container: %v. Allowed profiles: %v", [input.review.object.metadata.name, container.name, input.parameters.allowedProfiles])
- }
-
- input_apparmor_allowed(container, metadata) {
- get_annotation_for(container, metadata) == input.parameters.allowedProfiles[_]
- }
-
- input_containers[c] {
- c := input.review.object.spec.containers[_]
- }
- input_containers[c] {
- c := input.review.object.spec.initContainers[_]
- }
- input_containers[c] {
- c := input.review.object.spec.ephemeralContainers[_]
- }
-
- get_annotation_for(container, metadata) = out {
- out = metadata.annotations[sprintf("container.apparmor.security.beta.kubernetes.io/%v", [container.name])]
- }
- get_annotation_for(container, metadata) = out {
- not metadata.annotations[sprintf("container.apparmor.security.beta.kubernetes.io/%v", [container.name])]
- out = "runtime/default"
- }
- 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: podAppArmor
+ expression: 'has(variables.anyObject.spec.securityContext) && has(variables.anyObject.spec.securityContext.appArmorProfile) ? variables.anyObject.spec.securityContext.appArmorProfile : null'
+ - name: canonicalPodAppArmor
+ expression: |
+ variables.podAppArmor == null ? "runtime/default" :
+ variables.podAppArmor.type == "RuntimeDefault" ? "runtime/default" :
+ variables.podAppArmor.type == "Unconfined" ? "unconfined" :
+ variables.podAppArmor.type == "Localhost" ? "localhost/" + variables.podAppArmor.localhostProfile : ""
+ # break this mapping up by container type (regular/init/ephemeral) to avoid problems with name collisions,
+ # which may be a problem when running shift-left (no K8s API server to enforce uniqueness of container names)
+ - name: appArmorByContainer
+ expression: |
+ variables.containers.map(container, [container.name,
+ has(container.securityContext) && has(container.securityContext.appArmorProfile) ?
+ (container.securityContext.appArmorProfile.type == "RuntimeDefault" ? "runtime/default" :
+ container.securityContext.appArmorProfile.type == "Unconfined" ? "unconfined" :
+ container.securityContext.appArmorProfile.type == "Localhost" ? "localhost/" + container.securityContext.appArmorProfile.localhostProfile : "") :
+ has(variables.anyObject.metadata.annotations) && ("container.apparmor.security.beta.kubernetes.io/" + container.name) in variables.anyObject.metadata.annotations ?
+ variables.anyObject.metadata.annotations["container.apparmor.security.beta.kubernetes.io/" + container.name] :
+ variables.canonicalPodAppArmor
+ ])
+ - name: appArmorByInitContainer
+ expression: |
+ variables.initContainers.map(container, [container.name,
+ has(container.securityContext) && has(container.securityContext.appArmorProfile) ?
+ (container.securityContext.appArmorProfile.type == "RuntimeDefault" ? "runtime/default" :
+ container.securityContext.appArmorProfile.type == "Unconfined" ? "unconfined" :
+ container.securityContext.appArmorProfile.type == "Localhost" ? "localhost/" + container.securityContext.appArmorProfile.localhostProfile : "") :
+ has(variables.anyObject.metadata.annotations) && ("container.apparmor.security.beta.kubernetes.io/" + container.name) in variables.anyObject.metadata.annotations ?
+ variables.anyObject.metadata.annotations["container.apparmor.security.beta.kubernetes.io/" + container.name] :
+ variables.canonicalPodAppArmor
+ ])
+ - name: appArmorByEphemeralContainer
+ expression: |
+ variables.ephemeralContainers.map(container, [container.name,
+ has(container.securityContext) && has(container.securityContext.appArmorProfile) ?
+ (container.securityContext.appArmorProfile.type == "RuntimeDefault" ? "runtime/default" :
+ container.securityContext.appArmorProfile.type == "Unconfined" ? "unconfined" :
+ container.securityContext.appArmorProfile.type == "Localhost" ? "localhost/" + container.securityContext.appArmorProfile.localhostProfile : "") :
+ has(variables.anyObject.metadata.annotations) && ("container.apparmor.security.beta.kubernetes.io/" + container.name) in variables.anyObject.metadata.annotations ?
+ variables.anyObject.metadata.annotations["container.apparmor.security.beta.kubernetes.io/" + container.name] :
+ variables.canonicalPodAppArmor
+ ])
+ - 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)
+ validations:
+ - expression: |
+ variables.containers.all(container,
+ (container.image in variables.exemptImages) ||
+ variables.appArmorByContainer.exists(pair, pair[0] == container.name && pair[1] in variables.params.allowedProfiles)
+ )
+ messageExpression: '"AppArmor profile is not allowed. Allowed Profiles: " + variables.params.allowedProfiles.join(", ")'
+ - expression: |
+ variables.initContainers.all(container,
+ (container.image in variables.exemptImages) ||
+ variables.appArmorByInitContainer.exists(pair, pair[0] == container.name && pair[1] in variables.params.allowedProfiles)
+ )
+ messageExpression: '"AppArmor profile is not allowed. Allowed Profiles: " + variables.params.allowedProfiles.join(", ")'
+ - expression: |
+ variables.ephemeralContainers.all(container,
+ (container.image in variables.exemptImages) ||
+ variables.appArmorByEphemeralContainer.exists(pair, pair[0] == container.name && pair[1] in variables.params.allowedProfiles)
+ )
+ messageExpression: '"AppArmor profile is not allowed. Allowed Profiles: " + variables.params.allowedProfiles.join(", ")'
+ - engine: Rego
+ source:
+ rego: |
+ package k8spspapparmor
+
+ import data.lib.exempt_container.is_exempt
+
+ violation[{"msg": msg, "details": {}}] {
+ container := input_containers[_]
+ not is_exempt(container)
+ not input_apparmor_allowed(input.review.object, container)
+ msg := sprintf("AppArmor profile is not allowed, pod: %v, container: %v. Allowed profiles: %v", [input.review.object.metadata.name, container.name, input.parameters.allowedProfiles])
+ }
+
+ input_apparmor_allowed(pod, container) {
+ get_apparmor_profile(pod, container) == input.parameters.allowedProfiles[_]
+ }
+
+ input_containers[c] {
+ c := input.review.object.spec.containers[_]
+ }
+ input_containers[c] {
+ c := input.review.object.spec.initContainers[_]
+ }
+ input_containers[c] {
+ c := input.review.object.spec.ephemeralContainers[_]
+ }
+
+ get_apparmor_profile(_, container) = out {
+ profile := object.get(container, ["securityContext", "appArmorProfile"], null)
+ profile != null
+ out := canonicalize_apparmor_profile(profile)
+ }
+
+ get_apparmor_profile(pod, container) = out {
+ profile := object.get(container, ["securityContext", "appArmorProfile"], null)
+ profile == null
+ out := pod.metadata.annotations[sprintf("container.apparmor.security.beta.kubernetes.io/%v", [container.name])]
+ }
+
+ get_apparmor_profile(pod, container) = out {
+ profile := object.get(container, ["securityContext", "appArmorProfile"], null)
+ profile == null
+ not pod.metadata.annotations[sprintf("container.apparmor.security.beta.kubernetes.io/%v", [container.name])]
+ out := canonicalize_apparmor_profile(object.get(pod, ["spec", "securityContext", "appArmorProfile"], null))
+ }
+
+ canonicalize_apparmor_profile(profile) = out {
+ profile.type == "RuntimeDefault"
+ out := "runtime/default"
+ }
+
+ canonicalize_apparmor_profile(profile) = out {
+ profile.type == "Unconfined"
+ out := "unconfined"
+ }
+
+ canonicalize_apparmor_profile(profile) = out {
+ profile.type = "Localhost"
+ out := sprintf("localhost/%s", [profile.localhostProfile])
+ }
+
+ canonicalize_apparmor_profile(profile) = out {
+ profile == null
+ out := "runtime/default"
+ }
+ 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)
+ }
+
```
@@ -135,7 +254,7 @@ spec:
kinds: ["Pod"]
parameters:
allowedProfiles:
- - runtime/default
+ - localhost/custom
```
@@ -157,7 +276,7 @@ metadata:
name: nginx-apparmor-allowed
annotations:
# apparmor.security.beta.kubernetes.io/pod: unconfined # runtime/default
- container.apparmor.security.beta.kubernetes.io/nginx: runtime/default
+ container.apparmor.security.beta.kubernetes.io/nginx: localhost/custom
labels:
app: nginx-apparmor
spec:
@@ -173,6 +292,93 @@ Usage
kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper-library/master/library/pod-security-policy/apparmor/samples/psp-apparmor/example_allowed.yaml
```
+
+
+example-allowed-container
+
+```yaml
+apiVersion: v1
+kind: Pod
+metadata:
+ name: nginx-apparmor-allowed
+ labels:
+ app: nginx-apparmor
+spec:
+ containers:
+ - name: nginx
+ image: nginx
+ securityContext:
+ appArmorProfile:
+ type: "Localhost"
+ localhostProfile: "custom"
+
+```
+
+Usage
+
+```shell
+kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper-library/master/library/pod-security-policy/apparmor/samples/psp-apparmor/example_allowed_container.yaml
+```
+
+
+
+example-allowed-pod
+
+```yaml
+apiVersion: v1
+kind: Pod
+metadata:
+ name: nginx-apparmor-allowed
+ labels:
+ app: nginx-apparmor
+spec:
+ securityContext:
+ appArmorProfile:
+ type: "Localhost"
+ localhostProfile: "custom"
+ containers:
+ - name: nginx
+ image: nginx
+
+```
+
+Usage
+
+```shell
+kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper-library/master/library/pod-security-policy/apparmor/samples/psp-apparmor/example_allowed_pod.yaml
+```
+
+
+
+example-allowed-override
+
+```yaml
+apiVersion: v1
+kind: Pod
+metadata:
+ name: nginx-apparmor-allowed
+ labels:
+ app: nginx-apparmor
+spec:
+ securityContext:
+ appArmorProfile:
+ type: "Unconfined"
+ containers:
+ - name: nginx
+ image: nginx
+ securityContext:
+ appArmorProfile:
+ type: "Localhost"
+ localhostProfile: "custom"
+
+```
+
+Usage
+
+```shell
+kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper-library/master/library/pod-security-policy/apparmor/samples/psp-apparmor/example_allowed_override.yaml
+```
+
example-disallowed
@@ -200,6 +406,61 @@ Usage
kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper-library/master/library/pod-security-policy/apparmor/samples/psp-apparmor/example_disallowed.yaml
```
+
+
+example-disallowed-override
+
+```yaml
+apiVersion: v1
+kind: Pod
+metadata:
+ name: nginx-apparmor-allowed
+ labels:
+ app: nginx-apparmor
+spec:
+ securityContext:
+ appArmorProfile:
+ type: "Localhost"
+ localhostProfile: "custom"
+ containers:
+ - name: nginx
+ image: nginx
+ securityContext:
+ appArmorProfile:
+ type: "Unconfined"
+
+```
+
+Usage
+
+```shell
+kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper-library/master/library/pod-security-policy/apparmor/samples/psp-apparmor/example_disallowed_override.yaml
+```
+
+
+
+example-disallowed-no-profile
+
+```yaml
+apiVersion: v1
+kind: Pod
+metadata:
+ name: nginx-apparmor-disallowed
+ labels:
+ app: nginx-apparmor
+spec:
+ containers:
+ - name: nginx
+ image: nginx
+
+```
+
+Usage
+
+```shell
+kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper-library/master/library/pod-security-policy/apparmor/samples/psp-apparmor/example_disallowed_no_profile.yaml
+```
+
disallowed-ephemeral