diff --git a/.github/workflows/kind-cluster-resource-names.yaml b/.github/workflows/kind-cluster-resource-names.yaml new file mode 100644 index 000000000..8cac4310f --- /dev/null +++ b/.github/workflows/kind-cluster-resource-names.yaml @@ -0,0 +1,93 @@ +# Copyright 2024 The Sigstore Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: Test policy-controller with --resource-names flag + +on: + pull_request: + branches: [ 'main', 'release-*' ] + +defaults: + run: + shell: bash + +permissions: read-all + +jobs: + resource-names-flag-test: + name: Test --resource-names flag functionality + runs-on: ubuntu-latest + + strategy: + fail-fast: false # Keep running if one leg fails. + matrix: + k8s-version: + - v1.31.x + - v1.32.x + - v1.33.x + - v1.34.x + + env: + KO_DOCKER_REPO: "registry.local:5000/policy-controller" + SCAFFOLDING_RELEASE_VERSION: "v0.7.27" + GO111MODULE: on + GOFLAGS: -ldflags=-s -ldflags=-w + KOCACHE: ~/ko + + steps: + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + - uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0 + with: + go-version-file: './go.mod' + check-latest: true + + # will use the latest release available for ko + - uses: ko-build/setup-ko@d006021bd0c28d1ce33a07e7943d48b079944c8d # v0.9 + + - uses: imranismail/setup-kustomize@2ba527d4d055ab63514ba50a99456fc35684947f # v2.1.0 + + - name: Install yq + uses: mikefarah/yq@065b200af9851db0d5132f50bc10b1406ea5c0a8 # v4.50.1 + + - name: Setup mirror + uses: chainguard-dev/actions/setup-mirror@main + with: + mirror: mirror.gcr.io + + - uses: sigstore/cosign-installer@3454372f43399081ed03b604cb2d021dabca52bb # v3.8.2 + + - name: Install cluster + sigstore + uses: sigstore/scaffolding/actions/setup@main + with: + k8s-version: ${{ matrix.k8s-version }} + version: ${{ env.SCAFFOLDING_RELEASE_VERSION }} + + - name: Install policy-controller + env: + GIT_HASH: ${{ github.sha }} + GIT_VERSION: ci + LDFLAGS: "" + POLICY_CONTROLLER_YAML: test/kustomize-resource-names/policy-controller-e2e.yaml + KO_PREFIX: registry.local:5000/policy-controller + POLICY_CONTROLLER_ARCHS: linux/amd64 + run: | + make ko-policy-controller + + - name: Run --resource-names flag tests + run: | + ./test/e2e_test_resource_names_flag.sh + + - name: Collect diagnostics + if: ${{ failure() }} + uses: chainguard-dev/actions/kind-diag@6b6aeacf5f8f4854707392e7708773a7f510b611 # main diff --git a/cmd/webhook/main.go b/cmd/webhook/main.go index 6f84894fd..0c98da819 100644 --- a/cmd/webhook/main.go +++ b/cmd/webhook/main.go @@ -21,6 +21,7 @@ import ( "fmt" "log" "os" + "strings" "time" policyduckv1beta1 "github.com/sigstore/policy-controller/pkg/apis/duck/v1beta1" @@ -60,7 +61,13 @@ import ( cwebhook "github.com/sigstore/policy-controller/pkg/webhook" ) +const ( + defaultResourceNames = "pods,replicasets,deployments,statefulsets,daemonsets,jobs,cronjobs" +) + var ( + validResourceTypes = []string{"pods", "replicasets", "deployments", "statefulsets", "daemonsets", "jobs", "cronjobs"} + // webhookName holds the name of the validating and mutating webhook // configuration resources dispatching admission requests to policy-controller. // It is also the name of the webhook which is injected by the controller @@ -103,6 +110,10 @@ var ( // trustrootResyncPeriod holds the interval which the TrustRoot will resync // This is essential for triggering a reconcile update for potentially stale TUF metadata. trustrootResyncPeriod = flag.Duration("trustroot-resync-period", 24*time.Hour, "The resync period for ClusterImagePolicies. The default is 24h.") + + // resourceNames holds the comma-separated list of Kubernetes resources to monitor. + // https://github.com/sigstore/policy-controller/issues/1388 + resourceNames = flag.String("resource-names", defaultResourceNames, "Comma-separated list of Kubernetes resources to monitor. WARNING: If parent resources (e.g., Deployments, ReplicaSets) are not monitored, users will not receive policy violation feedback when creating them, even though the Pods they create may violate policies.") ) func main() { @@ -118,6 +129,13 @@ func main() { flag.Parse() + // Parse and validate resource names + resourceList := parseResourceNames(*resourceNames) + types = buildTypesMap(resourceList, *resourceNames != defaultResourceNames) + + // Update ValidResourceNames to match the configured resources + common.ValidResourceNames = sets.NewString(resourceList...) + // If TUF has been disabled do not try to set it up. if !*disableTUF { // If they provided an alternate TUF root file to use, read it here. @@ -139,11 +157,6 @@ func main() { ctx = clusterimagepolicy.ToContext(ctx, *policyResyncPeriod) ctx = pctuf.ToContext(ctx, *trustrootResyncPeriod) - // This must match the set of resources we configure in - // cmd/webhook/main.go in the "types" map. - common.ValidResourceNames = sets.NewString("replicasets", "deployments", - "pods", "cronjobs", "jobs", "statefulsets", "daemonsets") - v := version.GetVersionInfo() vJSON, _ := v.JSONString() log.Printf("%v", vJSON) @@ -199,17 +212,77 @@ func (c *crdEphemeralContainers) SupportedVerbs() []admissionregistrationv1.Oper } } -var types = map[schema.GroupVersionKind]resourcesemantics.GenericCRD{ - corev1.SchemeGroupVersion.WithKind("Pod"): &crdEphemeralContainers{GenericCRD: &duckv1.Pod{}}, +// types holds the mapping of Kubernetes resource types to their CRD handlers. +// This is initialized in main() based on the --resource-names flag. +var types map[schema.GroupVersionKind]resourcesemantics.GenericCRD + +// parseResourceNames parses a comma-separated list of resource names and returns a cleaned slice. +func parseResourceNames(input string) []string { + parts := strings.Split(input, ",") + result := make([]string, 0, len(parts)) + for _, part := range parts { + trimmed := strings.TrimSpace(strings.ToLower(part)) + if trimmed != "" { + result = append(result, trimmed) + } + } + return result +} + +// buildTypesMap constructs the types map based on the provided resource names. +// Resource names must be in lowercase plural form (e.g., "pods", "deployments"). +func buildTypesMap(resourceNames []string, isCustom bool) map[schema.GroupVersionKind]resourcesemantics.GenericCRD { + typesMap := make(map[schema.GroupVersionKind]resourcesemantics.GenericCRD) + var validResources, invalidResources []string + + for _, name := range resourceNames { + switch name { + case "pods": + typesMap[corev1.SchemeGroupVersion.WithKind("Pod")] = &crdEphemeralContainers{GenericCRD: &duckv1.Pod{}} + validResources = append(validResources, name) + case "replicasets": + typesMap[appsv1.SchemeGroupVersion.WithKind("ReplicaSet")] = &crdNoStatusUpdatesOrDeletes{GenericCRD: &policyduckv1beta1.PodScalable{}} + validResources = append(validResources, name) + case "deployments": + typesMap[appsv1.SchemeGroupVersion.WithKind("Deployment")] = &crdNoStatusUpdatesOrDeletes{GenericCRD: &policyduckv1beta1.PodScalable{}} + validResources = append(validResources, name) + case "statefulsets": + typesMap[appsv1.SchemeGroupVersion.WithKind("StatefulSet")] = &crdNoStatusUpdatesOrDeletes{GenericCRD: &policyduckv1beta1.PodScalable{}} + validResources = append(validResources, name) + case "daemonsets": + typesMap[appsv1.SchemeGroupVersion.WithKind("DaemonSet")] = &crdNoStatusUpdatesOrDeletes{GenericCRD: &duckv1.WithPod{}} + validResources = append(validResources, name) + case "jobs": + typesMap[batchv1.SchemeGroupVersion.WithKind("Job")] = &crdNoStatusUpdatesOrDeletes{GenericCRD: &duckv1.WithPod{}} + validResources = append(validResources, name) + case "cronjobs": + typesMap[batchv1.SchemeGroupVersion.WithKind("CronJob")] = &crdNoStatusUpdatesOrDeletes{GenericCRD: &duckv1.CronJob{}} + typesMap[batchv1beta1.SchemeGroupVersion.WithKind("CronJob")] = &crdNoStatusUpdatesOrDeletes{GenericCRD: &duckv1.CronJob{}} + validResources = append(validResources, name) + default: + invalidResources = append(invalidResources, name) + } + } - appsv1.SchemeGroupVersion.WithKind("ReplicaSet"): &crdNoStatusUpdatesOrDeletes{GenericCRD: &policyduckv1beta1.PodScalable{}}, - appsv1.SchemeGroupVersion.WithKind("Deployment"): &crdNoStatusUpdatesOrDeletes{GenericCRD: &policyduckv1beta1.PodScalable{}}, - appsv1.SchemeGroupVersion.WithKind("StatefulSet"): &crdNoStatusUpdatesOrDeletes{GenericCRD: &policyduckv1beta1.PodScalable{}}, - appsv1.SchemeGroupVersion.WithKind("DaemonSet"): &crdNoStatusUpdatesOrDeletes{GenericCRD: &duckv1.WithPod{}}, - batchv1.SchemeGroupVersion.WithKind("Job"): &crdNoStatusUpdatesOrDeletes{GenericCRD: &duckv1.WithPod{}}, + if len(invalidResources) > 0 { + log.Printf("WARNING: Invalid resource names will be ignored: %v. Valid options: %v", invalidResources, validResourceTypes) + } + + if len(validResources) == 0 { + log.Fatalf("No valid resources specified. At least one resource type must be monitored. Valid options: %v", validResourceTypes) + } + + if isCustom { + log.Printf("WARNING: Using custom resource names may result in poor user experience. "+ + "If parent resources (e.g., Deployments, ReplicaSets) are not monitored, users will not receive policy violation feedback "+ + "when creating those resources, even though the Pods they create may violate policies. "+ + "It is recommended to monitor all resource types unless you have a specific reason to exclude some. "+ + "Current monitored resources: %v", validResources) + } else { + log.Printf("Monitoring resources: %v", validResources) + } - batchv1.SchemeGroupVersion.WithKind("CronJob"): &crdNoStatusUpdatesOrDeletes{GenericCRD: &duckv1.CronJob{}}, - batchv1beta1.SchemeGroupVersion.WithKind("CronJob"): &crdNoStatusUpdatesOrDeletes{GenericCRD: &duckv1.CronJob{}}, + return typesMap } var typesCIP = map[schema.GroupVersionKind]resourcesemantics.GenericCRD{ diff --git a/test/e2e_test_resource_names_flag.sh b/test/e2e_test_resource_names_flag.sh new file mode 100755 index 000000000..e10bf038e --- /dev/null +++ b/test/e2e_test_resource_names_flag.sh @@ -0,0 +1,247 @@ +#!/usr/bin/env bash +# +# Copyright 2024 The Sigstore Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# E2E test for the --resource-names flag functionality. +# +# This test validates selective resource monitoring by configuring the policy +# controller to monitor only ReplicaSets and DaemonSets, then verifying: +# 1. Unmonitored resources (Pods, Deployments) are allowed without policy checks +# 2. Monitored resources (ReplicaSets, DaemonSets) are properly validated +# 3. Signed images are allowed for monitored resources + +set -ex + +if [[ -z "${KO_DOCKER_REPO}" ]]; then + echo "Must specify env variable KO_DOCKER_REPO" + exit 1 +fi + +# Variables +export NS=demo-resource-names +export demoimage="ghcr.io/sigstore/timestamp-server@sha256:dcf2f3a640bfb0a5d17aabafb34b407fe4403363c715718ab305a62b3606540d" + +echo "==========================================" +echo "Testing custom resource monitoring" +echo "==========================================" + +# Step 1: Deploy policy controller with ONLY ReplicaSets and DaemonSets monitored +echo '::group:: Deploy policy-controller with custom resources (replicasets,daemonsets only)' +kubectl apply -f <(kustomize build ./test/kustomize-resource-names) +kubectl rollout status --timeout 5m --namespace cosign-system deployments/webhook +echo '::endgroup::' + +# Step 2: Verify webhook configuration +echo '::group:: Verify webhook is configured for custom resources' +# Wait for webhook configuration to be updated with custom resources +echo "Waiting for webhook configuration to be updated..." +for i in {1..30}; do + if kubectl get validatingwebhookconfiguration policy.sigstore.dev -o yaml | grep -q "replicasets"; then + echo "replicasets found in webhook configuration" + break + fi + echo "Attempt $i/30: Waiting for webhook configuration update..." + sleep 2 +done + +# Check that webhook rules include replicasets and daemonsets +kubectl get validatingwebhookconfiguration policy.sigstore.dev -o yaml | grep -q "replicasets" || { + echo "ERROR: replicasets not found in webhook configuration" + kubectl get validatingwebhookconfiguration policy.sigstore.dev -o yaml + exit 1 +} +kubectl get validatingwebhookconfiguration policy.sigstore.dev -o yaml | grep -q "daemonsets" || { + echo "ERROR: daemonsets not found in webhook configuration" + kubectl get validatingwebhookconfiguration policy.sigstore.dev -o yaml + exit 1 +} +echo "Webhook correctly configured for ReplicaSet and DaemonSet" +echo '::endgroup::' + +# Step 3: Create test namespace +echo '::group:: Create and label test namespace' +kubectl create namespace ${NS} +kubectl label namespace ${NS} policy.sigstore.dev/include=true +echo '::endgroup::' + +# Step 4: Deploy ClusterImagePolicy +echo '::group:: Deploy ClusterImagePolicy requiring signatures' +cat <