diff --git a/checks/kubernetes/pss/baseline/11_seccomp_profile_unconfined.rego b/checks/kubernetes/pss/baseline/11_seccomp_profile_unconfined.rego index 9e0606b7..aabf3692 100644 --- a/checks/kubernetes/pss/baseline/11_seccomp_profile_unconfined.rego +++ b/checks/kubernetes/pss/baseline/11_seccomp_profile_unconfined.rego @@ -27,49 +27,57 @@ package builtin.kubernetes.KSV104 import data.lib.kubernetes -import data.lib.utils -# getSeccompContainers returns all containers which have a seccomp -# profile set and is profile not set to "unconfined" -getSeccompContainers[container] { - some i - keys := [key | key := sprintf("%s/%s", [ - "container.seccomp.security.alpha.kubernetes.io", - kubernetes.containers[_].name, - ])] - seccomp := object.filter(kubernetes.annotations[_], keys) - val := seccomp[i] - val != "unconfined" - [a, c] := split(i, "/") - container = c +pod_seccomp_profile_path := ["securityContext", "seccompProfile", "type"] + +controller_seccomp_profile_path := ["spec", "securityContext", "seccompProfile", "type"] + +seccomp_annotation_key_prefix := "container.seccomp.security.alpha.kubernetes.io" + +container_seccomp_annotation_key(container_name) := sprintf("%s/%s", [seccomp_annotation_key_prefix, container_name]) + +container_seccomp_from_annotations(container) := profile { + annotation_key := container_seccomp_annotation_key(container.name) + profile := kubernetes.annotations[_][annotation_key] +} else := "" + +# containers_with_unconfined_seccomp_profile_type returns all containers which have a seccomp +# profile set and is profile set to "Unconfined" +containers_with_unconfined_seccomp_profile_type[name] { + seccomp := container_seccomp[_] + lower(seccomp.type) == "unconfined" + name := seccomp.container.name } -# getNoSeccompContainers returns all containers which do not have -# a seccomp profile specified or profile set to "unconfined" -getNoSeccompContainers[container] { - container := kubernetes.containers[_].name - not getSeccompContainers[container] +# containers_with_unconfined_seccomp_profile_type returns all containers that do not have +# a seccomp profile type specified, since the default is unconfined +# https://kubernetes.io/docs/tutorials/security/seccomp/#enable-the-use-of-runtimedefault-as-the-default-seccomp-profile-for-all-workloads +containers_with_unconfined_seccomp_profile_type[name] { + seccomp := container_seccomp[_] + seccomp.type == "" + name := seccomp.container.name } -# getContainersWithDisallowedSeccompProfileType returns all containers which have a seccomp -# profile set and is profile set to "Unconfined" -getContainersWithDisallowedSeccompProfileType[name] { +container_seccomp[{"container": container, "type": type}] { + kubernetes.is_pod container := kubernetes.containers[_] - type := container.securityContext.seccompProfile.type - type == "Unconfined" - name = container.name + profile := container_seccomp_from_annotations(container) + type := object.get(container, pod_seccomp_profile_path, profile) } -# getContainersWithDisallowedSeccompProfileType returns all containers which do not have -# a seccomp profile type specified -getContainersWithDisallowedSeccompProfileType[name] { - container := kubernetes.containers[_] - not container.securityContext.seccompProfile.type - name = container.name +container_seccomp[{"container": container, "type": type}] { + not kubernetes.is_pod + pod := kubernetes.pods[_] + container := kubernetes.pod_containers(pod)[_] + profile := container_seccomp_from_annotations(container) + + # the profile type specified in the template takes precedence over the annotation + tplSeccompProfile := object.get(pod, controller_seccomp_profile_path, profile) + type := object.get(container, pod_seccomp_profile_path, tplSeccompProfile) } deny[res] { - cause := getContainersWithDisallowedSeccompProfileType[_] - msg := kubernetes.format(sprintf("container %s of %s %s in %s namespace should specify a seccomp profile", [getNoSeccompContainers[_], lower(kubernetes.kind), kubernetes.name, kubernetes.namespace])) + cause := containers_with_unconfined_seccomp_profile_type[_] + msg := kubernetes.format(sprintf("container %q of %s %q in %q namespace should specify a seccomp profile", [cause, lower(kubernetes.kind), kubernetes.name, kubernetes.namespace])) res := result.new(msg, cause) } diff --git a/checks/kubernetes/pss/baseline/11_seccomp_profile_unconfined_test.rego b/checks/kubernetes/pss/baseline/11_seccomp_profile_unconfined_test.rego index 9afdf422..d13b8e08 100644 --- a/checks/kubernetes/pss/baseline/11_seccomp_profile_unconfined_test.rego +++ b/checks/kubernetes/pss/baseline/11_seccomp_profile_unconfined_test.rego @@ -13,11 +13,30 @@ test_container_seccomp_profile_unconfined_denied { "-c", "echo 'Hello' && sleep 1h", ], - "securityContext": {"seccompProfile": {"type": "RuntimeDefault"}}, + "securityContext": {"seccompProfile": {"type": "Unconfined"}}, }]}, } - count(r) == 0 + count(r) == 1 +} + +test_container_empty_seccomp_profile_unconfined_denied { + r := deny with input as { + "apiVersion": "v1", + "kind": "Pod", + "metadata": {"name": "hello-sysctls"}, + "spec": {"containers": [{ + "name": "hello", + "image": "busybox", + "command": [ + "sh", + "-c", + "echo 'Hello' && sleep 1h", + ], + }]}, + } + + count(r) == 1 } test_container_seccomp_profile_unconfined_allowed { @@ -41,3 +60,192 @@ test_container_seccomp_profile_unconfined_allowed { count(r) == 0 } + +test_deployment_seccomp_profile_unconfined_allowed { + r := deny with input as { + "apiVersion": "apps/v1", + "kind": "Deployment", + "metadata": { + "name": "mydeployment", + "namespace": "mynamespace", + }, + "spec": { + "selector": {"matchLabels": {"app": "myapp"}}, + "template": { + "metadata": {"labels": {"app": "myapp"}}, + "spec": { + "containers": [{ + "name": "container", + "image": "node:8-alpine", + }], + "securityContext": {"seccompProfile": {"type": "RuntimeDefault"}}, + }, + }, + }, + } + count(r) == 0 +} + +test_deployment_seccomp_profile_unconfined_denied { + r := deny with input as { + "apiVersion": "apps/v1", + "kind": "Deployment", + "metadata": { + "name": "mydeployment", + "namespace": "mynamespace", + }, + "spec": { + "selector": {"matchLabels": {"app": "myapp"}}, + "template": { + "metadata": {"labels": {"app": "myapp"}}, + "spec": { + "containers": [{ + "name": "container", + "image": "node:8-alpine", + }], + "securityContext": {"seccompProfile": {"type": "Unconfined"}}, + }, + }, + }, + } + count(r) == 1 +} + +test_deployment_override_seccomp_profile_unconfined_allowed { + r := deny with input as { + "apiVersion": "apps/v1", + "kind": "Deployment", + "metadata": { + "name": "mydeployment", + "namespace": "mynamespace", + }, + "spec": { + "selector": {"matchLabels": {"app": "myapp"}}, + "template": { + "metadata": {"labels": {"app": "myapp"}}, + "spec": { + "containers": [{ + "name": "container", + "image": "node:8-alpine", + "securityContext": {"seccompProfile": {"type": "RuntimeDefault"}}, + }], + "securityContext": {"seccompProfile": {"type": "Unconfined"}}, + }, + }, + }, + } + count(r) == 0 +} + +test_deployment_override_seccomp_profile_unconfined_deny { + r := deny with input as { + "apiVersion": "apps/v1", + "kind": "Deployment", + "metadata": { + "name": "mydeployment", + "namespace": "mynamespace", + }, + "spec": { + "selector": {"matchLabels": {"app": "myapp"}}, + "template": { + "metadata": {"labels": {"app": "myapp"}}, + "spec": { + "containers": [ + { + "name": "container", + "image": "node:8-alpine", + }, + { + "name": "container2", + "image": "node:8-alpine", + "securityContext": {"seccompProfile": {"type": "Unconfined"}}, + }, + ], + "securityContext": {"seccompProfile": {"type": "RuntimeDefault"}}, + }, + }, + }, + } + count(r) == 1 + contains(r[_].msg, "container2") +} + +test_cronjob_seccomp_profile_unconfined_denied { + r := deny with input as { + "apiVersion": "batch/v1", + "kind": "CronJob", + "metadata": { + "name": "mydeployment", + "namespace": "mynamespace", + }, + "spec": { + "schedule": "* * * * *", + "jobTemplate": {"spec": {"template": {"spec": {"containers": [{ + "name": "test-container", + "image": "node:8-alpine", + "securityContext": {"seccompProfile": {"type": "Unconfined"}}, + }]}}}}, + }, + } + count(r) == 1 +} + +test_pod_annotations_seccomp_profile_unconfined_denied { + r := deny with input as { + "apiVersion": "v1", + "kind": "Pod", + "metadata": { + "name": "mydeployment", + "annotations": {"container.seccomp.security.alpha.kubernetes.io/test-container": "unconfined"}, + }, + "spec": {"containers": [{ + "name": "test-container", + "image": "node:8-alpine", + }]}, + } + count(r) == 1 +} + +test_pod_annotations_seccomp_profile_unconfined_allowed { + r := deny with input as { + "apiVersion": "v1", + "kind": "Pod", + "metadata": { + "name": "mydeployment", + "annotations": {"container.seccomp.security.alpha.kubernetes.io/test-container": "runtime/default"}, + }, + "spec": {"containers": [{ + "name": "test-container", + "image": "node:8-alpine", + }]}, + } + count(r) == 0 +} + +test_deployment_annotations_seccomp_profile_unconfined_allowed { + r := deny with input as { + "apiVersion": "apps/v1", + "kind": "Deployment", + "metadata": { + "name": "mydeployment", + "namespace": "default", + }, + "spec": { + "selector": {"matchLabels": {"app": "myapp"}}, + "template": { + "metadata": { + "labels": {"app": "myapp"}, + "annotations": {"container.seccomp.security.alpha.kubernetes.io/test-container": "unconfined"}, + }, + "spec": { + "containers": [{ + "name": "test-container", + "image": "node:8-alpine", + }], + "securityContext": {"seccompProfile": {"type": "RuntimeDefault"}}, + }, + }, + }, + } + count(r) == 0 +}