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