From 79d6e2f08f71fcc4c51011067694bd7ace40cae9 Mon Sep 17 00:00:00 2001 From: Alain Atemnkeng <aatemnke@vt.edu> Date: Mon, 15 Apr 2024 07:29:01 +0000 Subject: [PATCH] Update 4 files - /Chart.yaml - /templates/constraint_image_admission_controller.yaml - /rego/image_admission_control/policy.rego - /rego/image_admission_control/policy_test.rego --- Chart.yaml | 2 +- rego/image_admission_control/policy.rego | 64 +++++++++++ rego/image_admission_control/policy_test.rego | 106 ++++++++++++++++++ ...constraint_image_admission_controller.yaml | 28 +---- 4 files changed, 173 insertions(+), 27 deletions(-) create mode 100644 rego/image_admission_control/policy.rego create mode 100644 rego/image_admission_control/policy_test.rego diff --git a/Chart.yaml b/Chart.yaml index 1842a25..48fb1ce 100644 --- a/Chart.yaml +++ b/Chart.yaml @@ -1,4 +1,4 @@ apiVersion: v2 name: constraint-templates -version: 1.5.5 +version: 1.5.9 appVersion: 1.0.0 diff --git a/rego/image_admission_control/policy.rego b/rego/image_admission_control/policy.rego new file mode 100644 index 0000000..5d72fb5 --- /dev/null +++ b/rego/image_admission_control/policy.rego @@ -0,0 +1,64 @@ +package k8simageprovenance + +# List of exactly allowed repositories +allowed_repos := { + "docker.io", + "quay.io", + "public.ecr.aws", + "gcr.io", + "ghcr.io", + "harbor.io", + "harbor.it.vt.edu", + "gitlab.com", + "code.vt.edu", + "registry-1.docker.io", + "602401143452.dkr.ecr.us-east-1.amazonaws.com", + "code.vt.edu:5005", + "docker.elastic.co", + "harbor.platform.it.vt.edu", + "registry.gitlab.com", + "registry.k8s.io" +} + +# Function to extract hostname from image +get_hostname(image) = hostname { + trace(sprintf("Checking hostname for image: %v", [image])) + contains(image, "/") + parts := split(image, "/") + # If the first part contains a '.', assume it's a hostname + contains(parts[0], ".") + hostname := parts[0] + trace(sprintf("Extracted hostname: %v", [hostname])) +} else = "docker.io" { # Default to Docker Hub if no hostname is identifiable + not contains(image, "/") + trace("Defaulting to docker.io (no hostname found)") +} else = "docker.io" { # Default if no '.' in the first part of the name + contains(image, "/") + parts := split(image, "/") + not contains(parts[0], ".") + trace("Defaulting to docker.io (no '.' in first part of hostname)") +} + +# Check if image is from an allowed repository +is_from_allowed_repo(image) { + trace(sprintf("Checking if image %v is from an allowed repo", [image])) + hostname := get_hostname(image) + allowed := [repo | repo := allowed_repos[_]; hostname == repo] + result := count(allowed) > 0 + trace(sprintf("Image %v allowed: %v", [image, result])) + result +} + +# Main violation logic for all container types +violation[{"msg": msg}] { + trace("Evaluating violation") + input.review.object.kind == "Pod" + # Check initContainers and ephemeralContainers alongside containers + container_types := ["containers", "initContainers", "ephemeralContainers"] + container_type := container_types[_] + container := input.review.object.spec[container_type][_] + trace(sprintf("Checking %v image: %v", [container_type, container.image])) + not is_from_allowed_repo(container.image) + msg := sprintf("Image %v in %v is not from an allowed repository. Please contact the platform team", [container.image, container_type]) + trace(sprintf("Violation found: %v", [msg])) +} diff --git a/rego/image_admission_control/policy_test.rego b/rego/image_admission_control/policy_test.rego new file mode 100644 index 0000000..340ab48 --- /dev/null +++ b/rego/image_admission_control/policy_test.rego @@ -0,0 +1,106 @@ +package k8simageprovenance + +# Helper function to create mock input for a Pod with different types of container images +create_input(image, container_type) = { + "review": { + "object": { + "kind": "Pod", + "spec": { + container_type: [ + { + "image": image + } + ] + } + } + } +} + +# Test with an allowed image repository +test_with_allowed_image_repository { + container_types := ["containers", "initContainers", "ephemeralContainers"] + container_type := container_types[_] + test_input := create_input("docker.io/nginx", container_type) + results := data.k8simageprovenance.violation with input as test_input + count(results) == 0 +} + +# Test with a disallowed image repository +test_with_disallowed_image_repository { + container_types := ["containers", "initContainers", "ephemeralContainers"] + container_type := container_types[_] + test_input := create_input("unlistedrepo.com/image", container_type) + results := data.k8simageprovenance.violation with input as test_input + count(results) == 1 +} + +# Test with no repository specified (defaults to Docker Hub) +test_with_default_docker_hub_image { + container_types := ["containers", "initContainers", "ephemeralContainers"] + container_type := container_types[_] + test_input := create_input("nginx", container_type) + results := data.k8simageprovenance.violation with input as test_input + count(results) == 0 +} + +# Test with multiple containers, mixed repositories +test_with_mixed_repositories { + test_input := { + "review": { + "object": { + "kind": "Pod", + "spec": { + "containers": [ + {"image": "docker.io/nginx"}, + {"image": "unlistedrepo.com/image"} + ], + "initContainers": [ + {"image": "docker.io/initnginx"} + ], + "ephemeralContainers": [ + {"image": "quay.io/tempnginx"} + ] + } + } + } + } + results := data.k8simageprovenance.violation with input as test_input + count(results) == 1 +} + +# Test with image from Docker Hub's default repository +test_with_docker_hub_default_repository { + container_types := ["containers", "initContainers", "ephemeralContainers"] + container_type := container_types[_] + test_input := create_input("alpine", container_type) + results := data.k8simageprovenance.violation with input as test_input + count(results) == 0 +} + +# Test with image from non-standard repository path +test_with_non_standard_repository_path { + container_types := ["containers", "initContainers", "ephemeralContainers"] + container_type := container_types[_] + test_input := create_input("docker.io/somepath/nginx", container_type) + results := data.k8simageprovenance.violation with input as test_input + count(results) == 0 +} + +# Separate tests for each container type with allowed images +test_with_allowed_image_containers { + test_input := create_input("docker.io/nginx", "containers") + results := data.k8simageprovenance.violation with input as test_input + count(results) == 0 +} + +test_with_allowed_image_init_containers { + test_input := create_input("docker.io/nginx", "initContainers") + results := data.k8simageprovenance.violation with input as test_input + count(results) == 0 +} + +test_with_allowed_image_ephemeral_containers { + test_input := create_input("docker.io/nginx", "ephemeralContainers") + results := data.k8simageprovenance.violation with input as test_input + count(results) == 0 +} diff --git a/templates/constraint_image_admission_controller.yaml b/templates/constraint_image_admission_controller.yaml index 132815c..7c51c30 100644 --- a/templates/constraint_image_admission_controller.yaml +++ b/templates/constraint_image_admission_controller.yaml @@ -10,32 +10,8 @@ spec: validation: openAPIV3Schema: type: object - properties: + properties: {} targets: - target: admission.k8s.gatekeeper.sh rego: | - package k8simageprovenance - - violation[{"msg": msg}] { - input.review.object.kind == "Pod" - container := input.review.object.spec.containers[_] - not startswith(container.image, "docker.io/") - not startswith(container.image, "quay.io/") - not startswith(container.image, "public.ecr.aws/") - not startswith(container.image, "gcr.io/") - not startswith(container.image, "ghcr.io/") - not startswith(container.image, "harbor.") - not startswith(container.image, "gitlab.") - msg := sprintf("Image %v is not from an allowed repository. Please contact the platform team", [container.image]) - } ---- -apiVersion: constraints.gatekeeper.sh/v1beta1 -kind: K8sImageProvenance -metadata: - name: image-provenance-constraint -spec: - enforcementAction: deny - match: - kinds: - - apiGroups: [""] - kinds: ["Pod"] +{{.Files.Get "rego/image_provenance/policy.rego" | indent 8 }} \ No newline at end of file -- GitLab