apiVersion: templates.gatekeeper.sh/v1beta1 kind: ConstraintTemplate metadata: name: k8spodhostfilesystem annotations: description: >- Controls usage of the host filesystem. Corresponds to the `allowedHostPaths` field in a PodSecurityPolicy. For more information, see https://kubernetes.io/docs/concepts/policy/pod-security-policy/#volumes-and-file-systems spec: crd: spec: names: kind: K8sPodHostFilesystem validation: # Schema for the `parameters` field openAPIV3Schema: type: object description: >- Controls usage of the host filesystem. Corresponds to the `allowedHostPaths` field in a PodSecurityPolicy. For more information, see https://kubernetes.io/docs/concepts/policy/pod-security-policy/#volumes-and-file-systems properties: allowedHostPaths: type: array description: "An array of hostpath objects, representing paths and read/write configuration." items: type: object properties: pathPrefix: type: string description: "The path prefix that the host volume must match." readOnly: type: boolean description: "when set to true, any container volumeMounts matching the pathPrefix must include `readOnly: true`." ops: type: array items: type: string errMsg: type: string targets: - target: admission.k8s.gatekeeper.sh libs: - | package lib.k8s.helpers constraint_ops = value { value := input.parameters.ops } review_operation = value { value := input.review.operation } review_metadata_labels = value { value := input.review.object.metadata.labels } review_spec_template_metadata_labels = value { value := input.review.object.spec.template.metadata.labels } deployment_containers = value { value := input.review.object.spec.template.spec.containers } deployment_init_containers = value { value := input.review.object.spec.template.spec.initContainers } deployment_pod = value { value := input.review.object.spec.template.spec } review_id = value { value := sprintf("%v/%v/%v", [ review_kind, review_namespace, review_name ]) } review_name = value { value := input.review.object.metadata.name } else = value { value := "NOT_FOUND" } review_namespace = value { value := input.review.object.metadata.namespace } else = value { value := "NOT_FOUND" } review_kind = value { value := input.review.kind.kind } else = value { value := "NOT_FOUND" } rego: | package k8spodhostfilesystem import data.lib.k8s.helpers as helpers violation[{"msg": msg, "details": {}}] { helpers.review_operation = helpers.constraint_ops[_] volume := input_hostpath_volumes[_] allowedPaths := get_allowed_paths(input) input_hostpath_violation(allowedPaths, volume) msg := sprintf("%v: HostPath volume %v is not allowed, pod: %v. Allowed path: %v, Resource ID (kind/ns/name): %v", [input.parameters.errMsg,volume,input.review.object.metadata.name,allowedPaths,helpers.review_id]) } input_hostpath_violation(allowedPaths, volume) { # An empty list means all host paths are blocked allowedPaths == [] } input_hostpath_violation(allowedPaths, volume) { not input_hostpath_allowed(allowedPaths, volume) } get_allowed_paths(arg) = out { not arg.parameters out = [] } get_allowed_paths(arg) = out { not arg.parameters.allowedHostPaths out = [] } get_allowed_paths(arg) = out { out = arg.parameters.allowedHostPaths } input_hostpath_allowed(allowedPaths, volume) { allowedHostPath := allowedPaths[_] path_matches(allowedHostPath.pathPrefix, volume.hostPath.path) not allowedHostPath.readOnly == true } input_hostpath_allowed(allowedPaths, volume) { allowedHostPath := allowedPaths[_] path_matches(allowedHostPath.pathPrefix, volume.hostPath.path) allowedHostPath.readOnly not writeable_input_volume_mounts(volume.name) } writeable_input_volume_mounts(volume_name) { container := input_containers[_] mount := container.volumeMounts[_] mount.name == volume_name not mount.readOnly } # This allows "/foo", "/foo/", "/foo/bar" etc., but # disallows "/fool", "/etc/foo" etc. path_matches(prefix, path) { a := path_array(prefix) b := path_array(path) prefix_matches(a, b) } path_array(p) = out { p != "/" out := split(trim(p, "/"), "/") } # This handles the special case for "/", since # split(trim("/", "/"), "/") == [""] path_array("/") = [] prefix_matches(a, b) { count(a) <= count(b) not any_not_equal_upto(a, b, count(a)) } any_not_equal_upto(a, b, n) { a[i] != b[i] i < n } input_hostpath_volumes[v] { v := input.review.object.spec.volumes[_] has_field(v, "hostPath") } # has_field returns whether an object has a field has_field(object, field) = true { object[field] } input_containers[c] { c := input.review.object.spec.containers[_] } input_containers[c] { c := input.review.object.spec.initContainers[_] }