From d2541d261b59419405a281998fc4d201cced8525 Mon Sep 17 00:00:00 2001 From: Simon Baird Date: Tue, 10 Mar 2026 12:12:02 -0400 Subject: [PATCH 1/5] Support running nginx TUF inside test cluster The goal is to be able to have a working TUF root in the Kind cluster so we can use it in a Tekton task during acceptance tests. Also, expose some env vars that we'll need in upcoming Tekton task acceptance tests. The way this is done is that we run nginx in the cluster, but with files that are extracted (using a bash script added in this commit) from the existing wiremock data. Beware we now have two different ways of stubbing/mocking a TUF instance, one new way that works in the cluster and can be use when testing Tekton tasks, and one old way that works directly in a test container and uses wiremock. (In case it's not obvious, there was a lot of heavy lifing done by Claude here.) Ref: https://issues.redhat.com/browse/EC-1695 Co-authored-by: Claude Code --- .gitignore | 6 +- Makefile | 25 +++- acceptance/kubernetes/kind/kind.go | 2 +- acceptance/kubernetes/kind/kubernetes.go | 15 ++ acceptance/kubernetes/kubernetes.go | 6 + acceptance/tuf/tuf.go | 1 + acceptance/wiremock/wiremock.go | 32 +++- hack/test/kustomization.yaml | 1 + hack/tuf/create-tuf-files.sh | 98 +++++++++++++ hack/tuf/kustomization.yaml | 27 ++++ hack/tuf/namespace.yaml | 21 +++ hack/tuf/tuf.yaml | 177 +++++++++++++++++++++++ 12 files changed, 401 insertions(+), 10 deletions(-) create mode 100755 hack/tuf/create-tuf-files.sh create mode 100644 hack/tuf/kustomization.yaml create mode 100644 hack/tuf/namespace.yaml create mode 100644 hack/tuf/tuf.yaml diff --git a/.gitignore b/.gitignore index 1e919fc0b..63ad2a1c7 100644 --- a/.gitignore +++ b/.gitignore @@ -182,4 +182,8 @@ hack/**/charts benchmark/*/data.tar.gz # JUnit file output from acceptance tests -junit-acceptance.xml \ No newline at end of file +junit-acceptance.xml + +# Generated TUF ConfigMaps (now created just-in-time) +hack/tuf/tuf-files-configmap.yaml +hack/tuf/tuf-root-configmap.yaml diff --git a/Makefile b/Makefile index a15d5d0fd..dac54bfde 100644 --- a/Makefile +++ b/Makefile @@ -118,6 +118,26 @@ test: ## Run all unit tests ACCEPTANCE_TIMEOUT:=20m .ONESHELL: .SHELLFLAGS=-e -c + +# Generate TUF ConfigMaps if they don't exist or source files are newer +hack/tuf/tuf-files-configmap.yaml: acceptance/wiremock/recordings/tuf/__files/* acceptance/wiremock/recordings/tuf/mappings/* acceptance/tuf/root.json hack/tuf/create-tuf-files.sh + @echo "Generating TUF ConfigMaps..." + @hack/tuf/create-tuf-files.sh + +# The root ConfigMap is generated at the same time as the files ConfigMap +hack/tuf/tuf-root-configmap.yaml: hack/tuf/tuf-files-configmap.yaml + +.PHONY: tuf-yaml-clean +tuf-yaml-clean: ## Remove generated TUF ConfigMap YAML files + @echo "Cleaning TUF ConfigMap YAML files..." + @rm -f hack/tuf/tuf-files-configmap.yaml hack/tuf/tuf-root-configmap.yaml + +.PHONY: tuf-yaml-refresh +tuf-yaml-refresh: tuf-yaml-clean tuf-yaml ## Force regeneration of TUF ConfigMap YAML files + +.PHONY: tuf-yaml +tuf-yaml: hack/tuf/tuf-files-configmap.yaml hack/tuf/tuf-root-configmap.yaml ## Generate TUF ConfigMap YAML files for acceptance tests + .PHONY: acceptance acceptance: ## Run all acceptance tests @@ -130,13 +150,14 @@ acceptance: ## Run all acceptance tests cp -R . "$$ACCEPTANCE_WORKDIR"; \ cd "$$ACCEPTANCE_WORKDIR" && \ $(MAKE) build && \ + $(MAKE) tuf-yaml && \ export GOCOVERDIR="$${ACCEPTANCE_WORKDIR}/coverage"; \ cd acceptance && go test -timeout $(ACCEPTANCE_TIMEOUT) ./... ; go tool covdata textfmt -i=$${GOCOVERDIR} -o="$(ROOT_DIR)/coverage-acceptance.out" # Add @focus above the feature you're hacking on to use this # (Mainly for use with the feature-% target below) .PHONY: focus-acceptance -focus-acceptance: build ## Run acceptance tests with @focus tag +focus-acceptance: build tuf-yaml ## Run acceptance tests with @focus tag @cd acceptance && go test . -args -tags=@focus # Uses sed hackery to insert a @focus tag and then remove it afterwards. @@ -157,7 +178,7 @@ feature_%: ## Run acceptance tests for a single feature file, e.g. make feature_ fi # (Replace spaces with underscores in the scenario name.) -scenario_%: build ## Run acceptance tests for a single scenario, e.g. make scenario_inline_policy +scenario_%: build tuf-yaml ## Run acceptance tests for a single scenario, e.g. make scenario_inline_policy @cd acceptance && go test -test.run 'TestFeatures/$*' benchmark/%/data.tar.gz: diff --git a/acceptance/kubernetes/kind/kind.go b/acceptance/kubernetes/kind/kind.go index abf104542..146a1a9cd 100644 --- a/acceptance/kubernetes/kind/kind.go +++ b/acceptance/kubernetes/kind/kind.go @@ -166,7 +166,7 @@ func Start(givenCtx context.Context) (ctx context.Context, kCluster types.Cluste var port int if port, err = freeport.GetFreePort(); err != nil { - logger.Errorf("Unable to determine a free port: %v", err) + logger.Errorf("Unable to determine a free port for registry: %v", err) return } else { kCluster.registryPort = int32(port) //nolint:gosec // G115 - ports shouldn't be larger than int32 diff --git a/acceptance/kubernetes/kind/kubernetes.go b/acceptance/kubernetes/kind/kubernetes.go index 1752d60cf..3f3e413dd 100644 --- a/acceptance/kubernetes/kind/kubernetes.go +++ b/acceptance/kubernetes/kind/kubernetes.go @@ -39,7 +39,9 @@ import ( "github.com/conforma/cli/acceptance/crypto" "github.com/conforma/cli/acceptance/kubernetes/types" "github.com/conforma/cli/acceptance/kustomize" + "github.com/conforma/cli/acceptance/rekor" "github.com/conforma/cli/acceptance/testenv" + "github.com/conforma/cli/acceptance/wiremock" ) // createPolicyObject creates the EnterpriseContractPolicy object with the given @@ -254,6 +256,19 @@ func stringParam(ctx context.Context, name, value string, t *testState) pipeline vars["BUILD_SNAPSHOT_DIGEST"] = t.snapshotDigest } + // Add TUF and certificate variables for keyless verification + // For Tekton tasks, always use the cluster-internal TUF service + vars["TUF"] = "http://tuf.tuf-service.svc.cluster.local:8080" + vars["CERT_IDENTITY"] = "https://kubernetes.io/namespaces/default/serviceaccounts/default" + vars["CERT_ISSUER"] = "https://kubernetes.default.svc.cluster.local" + + // Only set REKOR variable if stub rekord was started + if wiremock.IsRunning(ctx) { + if rekorURL, err := rekor.StubRekor(ctx); err == nil { + vars["REKOR"] = rekorURL + } + } + publicKeys := crypto.PublicKeysFrom(ctx) for name, key := range publicKeys { vars[fmt.Sprintf("%s_PUBLIC_KEY", name)] = key diff --git a/acceptance/kubernetes/kubernetes.go b/acceptance/kubernetes/kubernetes.go index eb5507a84..c1c8b2ffa 100644 --- a/acceptance/kubernetes/kubernetes.go +++ b/acceptance/kubernetes/kubernetes.go @@ -36,6 +36,7 @@ import ( "github.com/conforma/cli/acceptance/kubernetes/stub" "github.com/conforma/cli/acceptance/kubernetes/types" "github.com/conforma/cli/acceptance/registry" + "github.com/conforma/cli/acceptance/rekor" "github.com/conforma/cli/acceptance/snaps" "github.com/conforma/cli/acceptance/testenv" ) @@ -366,6 +367,11 @@ func taskLogsShouldMatchTheSnapshot(ctx context.Context, stepName string) error vars["EC_VERSION"] = v + // Add Rekor URL for snapshot normalization + if rekorURL, err := rekor.StubRekor(ctx); err == nil { + vars["REKOR"] = rekorURL + } + for _, step := range info.Steps { if step.Name == stepName { return snaps.MatchSnapshot(ctx, step.Name, step.Logs, vars) diff --git a/acceptance/tuf/tuf.go b/acceptance/tuf/tuf.go index c3cc5946d..fd978640c 100644 --- a/acceptance/tuf/tuf.go +++ b/acceptance/tuf/tuf.go @@ -100,6 +100,7 @@ func initializeRoot(ctx context.Context) (context.Context, error) { } // Stub returns the `http://host:port` of the stubbed TUF. +// This is used for CLI context (external WireMock) func Stub(ctx context.Context) (string, error) { endpoint, err := wiremock.Endpoint(ctx) if err != nil { diff --git a/acceptance/wiremock/wiremock.go b/acceptance/wiremock/wiremock.go index 08f7a94b2..2301072a8 100644 --- a/acceptance/wiremock/wiremock.go +++ b/acceptance/wiremock/wiremock.go @@ -187,6 +187,11 @@ func (c *client) UnmatchedRequests() ([]unmatchedRequest, error) { // StartWiremock starts the WireMock instance if one is not already running func StartWiremock(ctx context.Context) (context.Context, error) { + return StartWiremockOnPort(ctx, 0) // 0 means use any available port +} + +// StartWiremockOnPort starts the WireMock instance on a specific port if one is not already running +func StartWiremockOnPort(ctx context.Context, hostPort int) (context.Context, error) { var state *wiremockState ctx, err := testenv.SetupState(ctx, &state) if err != nil { @@ -204,9 +209,18 @@ func StartWiremock(ctx context.Context) (context.Context, error) { return ctx, err } + var exposedPorts []string + if hostPort > 0 { + // Use specific port + exposedPorts = []string{fmt.Sprintf("0.0.0.0:%d:8080/tcp", hostPort), "0.0.0.0::8443/tcp"} + } else { + // Use any available port + exposedPorts = []string{"0.0.0.0::8080/tcp", "0.0.0.0::8443/tcp"} + } + req := testenv.TestContainersRequest(ctx, testcontainers.ContainerRequest{ Image: wireMockImage, - ExposedPorts: []string{"0.0.0.0::8080/tcp", "0.0.0.0::8443/tcp"}, + ExposedPorts: exposedPorts, WaitingFor: wait.ForHTTP("/__admin/mappings").WithPort("8080/tcp"), Binds: []string{fmt.Sprintf("%s:/recordings:z", recordings)}, Cmd: []string{ @@ -225,12 +239,18 @@ func StartWiremock(ctx context.Context) (context.Context, error) { return ctx, fmt.Errorf("unable to run GenericContainer: %v", err) } - port, err := w.MappedPort(ctx, "8080/tcp") - if err != nil { - return ctx, err + var url string + if hostPort > 0 { + // Use the specific port that was requested + url = fmt.Sprintf("http://localhost:%d", hostPort) + } else { + // Get the dynamically assigned port + port, err := w.MappedPort(ctx, "8080/tcp") + if err != nil { + return ctx, err + } + url = fmt.Sprintf("http://localhost:%d", port.Int()) } - - url := fmt.Sprintf("http://localhost:%d", port.Int()) state.URL = url return ctx, nil diff --git a/hack/test/kustomization.yaml b/hack/test/kustomization.yaml index 9d61c5bc4..b6eabfbb8 100644 --- a/hack/test/kustomization.yaml +++ b/hack/test/kustomization.yaml @@ -22,4 +22,5 @@ resources: - ../tekton - ../ecc - ../registry + - ../tuf - ../work diff --git a/hack/tuf/create-tuf-files.sh b/hack/tuf/create-tuf-files.sh new file mode 100755 index 000000000..c98bac8bb --- /dev/null +++ b/hack/tuf/create-tuf-files.sh @@ -0,0 +1,98 @@ +#!/bin/bash +# Copyright The Conforma Contributors +# +# 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. +# +# SPDX-License-Identifier: Apache-2.0 + + +# Script to create TUF ConfigMaps with exact file content +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" +WIREMOCK_TUF_DIR="$PROJECT_ROOT/acceptance/wiremock/recordings/tuf" + +# Extract file names from wiremock mapping files +extract_body_files() { + local files_dir="$WIREMOCK_TUF_DIR/__files" + local mappings_dir="$WIREMOCK_TUF_DIR/mappings" + + # Check if directories exist + if [[ ! -d "$files_dir" ]]; then + echo "Error: WireMock files directory not found: $files_dir" >&2 + exit 1 + fi + + if [[ ! -d "$mappings_dir" ]]; then + echo "Error: WireMock mappings directory not found: $mappings_dir" >&2 + exit 1 + fi + + local body_files=() + + # Extract bodyFileName from each mapping file using jq + while IFS= read -r -d '' mapping_file; do + if [[ -f "$mapping_file" ]]; then + local body_file + body_file=$(jq -r '.response.bodyFileName // empty' "$mapping_file") + if [[ -n "$body_file" && -f "$files_dir/$body_file" ]]; then + # Skip files that aren't useful for TUF (like root.json which we handle separately) + if [[ "$body_file" != "root.json" ]]; then + body_files+=("$body_file") + fi + fi + fi + done < <(find "$mappings_dir" -name "mapping-*.json" -print0) + + printf '%s\n' "${body_files[@]}" +} + +# Build kubectl command with dynamic file list +build_kubectl_command() { + local files_dir="$WIREMOCK_TUF_DIR/__files" + local cmd_args=("kubectl" "create" "configmap" "tuf-files" "--namespace=tuf-service") + + local body_files + readarray -t body_files < <(extract_body_files) + + if [[ ${#body_files[@]} -eq 0 ]]; then + echo "Error: No TUF body files found in wiremock recordings" >&2 + exit 1 + fi + + echo "Found ${#body_files[@]} TUF files to include in ConfigMap:" >&2 + + for body_file in "${body_files[@]}"; do + echo " - $body_file" >&2 + # Use the filename as the key in the ConfigMap + cmd_args+=("--from-file=$body_file=$files_dir/$body_file") + done + + cmd_args+=("--dry-run=client" "-o" "yaml" "--validate=false") + + printf '%s\n' "${cmd_args[@]}" +} + +# Create ConfigMap YAML files +echo "Creating TUF files ConfigMap YAML..." >&2 +readarray -t kubectl_cmd < <(build_kubectl_command) +"${kubectl_cmd[@]}" > "$SCRIPT_DIR/tuf-files-configmap.yaml" + +echo "Creating TUF root ConfigMap YAML..." >&2 +kubectl create configmap tuf-root-data \ + --namespace=tuf-service \ + --from-file=root.json="$PROJECT_ROOT/acceptance/tuf/root.json" \ + --dry-run=client -o yaml --validate=false > "$SCRIPT_DIR/tuf-root-configmap.yaml" + +echo "TUF ConfigMaps YAML files created successfully" diff --git a/hack/tuf/kustomization.yaml b/hack/tuf/kustomization.yaml new file mode 100644 index 000000000..1e1fecb24 --- /dev/null +++ b/hack/tuf/kustomization.yaml @@ -0,0 +1,27 @@ +# Copyright The Conforma Contributors +# +# 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. +# +# SPDX-License-Identifier: Apache-2.0 + +--- +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +resources: + - namespace.yaml + - tuf.yaml + # These two files are generated using `make tuf-yaml` from wiremock data. + # See also the hack/tuf/create-tuf-files.sh script in this directory. + - tuf-files-configmap.yaml + - tuf-root-configmap.yaml diff --git a/hack/tuf/namespace.yaml b/hack/tuf/namespace.yaml new file mode 100644 index 000000000..bde5be034 --- /dev/null +++ b/hack/tuf/namespace.yaml @@ -0,0 +1,21 @@ +# Copyright The Conforma Contributors +# +# 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. +# +# SPDX-License-Identifier: Apache-2.0 + +--- +apiVersion: v1 +kind: Namespace +metadata: + name: tuf-service diff --git a/hack/tuf/tuf.yaml b/hack/tuf/tuf.yaml new file mode 100644 index 000000000..10d3d6de7 --- /dev/null +++ b/hack/tuf/tuf.yaml @@ -0,0 +1,177 @@ +# Copyright The Conforma Contributors +# +# 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. +# +# SPDX-License-Identifier: Apache-2.0 + +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: tuf-port-number + namespace: tuf-service +data: + PORT: "8080" +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: tuf-server + namespace: tuf-service + labels: + app.kubernetes.io/name: tuf-server +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: tuf-server + template: + metadata: + labels: + app.kubernetes.io/name: tuf-server + spec: + initContainers: + - name: setup-tuf-data + image: busybox:1.35 + command: + - sh + - -c + - | + cp /tuf-files/* /tuf-data/ + + # Create the versioned metadata files that TUF expects + cp /tuf-data/body-timestamp.json-*.json /tuf-data/timestamp.json + cp /tuf-data/body-1.snapshot.json-*.json /tuf-data/1.snapshot.json + cp /tuf-data/body-1.targets.json-*.json /tuf-data/1.targets.json + + # Create root.json from embedded content + cp /tuf-root/root.json /tuf-data/root.json + + # Create targets directory and copy target files with hash-based names dynamically + mkdir -p /tuf-data/targets + + # Process all target files using patterns instead of hardcoded names + for file in /tuf-data/body-targets-*; do + [ -f "$file" ] || continue + + # Extract the original filename by removing the body-targets- prefix and the -HASH.ext suffix + basename_file=$(basename "$file") + + # Pattern: body-targets-.-. + # We want to extract .. + # Handle different patterns by removing the suffix intelligently + without_prefix=$(echo "$basename_file" | sed 's/^body-targets-//') + + # Special case for tsa_intermedi -> tsa_intermediate_0.crt + if echo "$without_prefix" | grep -q "tsa_intermedi-"; then + target_name=$(echo "$without_prefix" | sed 's/tsa_intermedi-.*/tsa_intermediate_0.crt.pem/') + # For files ending with .-HASH.ext, remove the .-HASH part + elif echo "$without_prefix" | grep -q '\.-[A-Za-z0-9]*\.'; then + target_name=$(echo "$without_prefix" | sed 's/\.-[A-Za-z0-9]*\./\./') + # For files ending with -HASH.ext, remove the -HASH part but keep the extension + elif echo "$without_prefix" | grep -q '\-[A-Za-z0-9]*\.[a-z]*$'; then + # Extract everything before -HASH, then add the extension + ext=$(echo "$without_prefix" | sed 's/.*\.//') + base=$(echo "$without_prefix" | sed 's/\(.*\)-[A-Za-z0-9]*\.[a-z]*$/\1/') + target_name="$base.$ext" + else + # Fallback: remove -HASH.ext and add the extension + ext=$(echo "$basename_file" | sed 's/.*\.//') + base=$(echo "$without_prefix" | sed 's/-[A-Za-z0-9]*\.[a-z]*$//') + target_name="$base.$ext" + fi + + if [ -n "$target_name" ]; then + cp "$file" "/tuf-data/targets/$target_name" + echo "Copied $basename_file -> targets/$target_name" + else + echo "Warning: Could not parse filename pattern for $basename_file" + fi + done + + ls -la /tuf-data/ + ls -la /tuf-data/targets/ + volumeMounts: + - name: tuf-files + mountPath: /tuf-files + readOnly: true + - name: tuf-root + mountPath: /tuf-root + readOnly: true + - name: tuf-data + mountPath: /tuf-data + containers: + - name: tuf-server + image: docker.io/nginx:1.25-alpine + ports: + - name: http + containerPort: 8080 + env: + - name: NGINX_PORT + value: "8080" + volumeMounts: + - name: tuf-data + mountPath: /usr/share/nginx/html + readOnly: true + - name: nginx-config + mountPath: /etc/nginx/conf.d/default.conf + subPath: nginx.conf + volumes: + - name: tuf-files + configMap: + name: tuf-files + - name: tuf-root + configMap: + name: tuf-root-data + - name: tuf-data + emptyDir: {} + - name: nginx-config + configMap: + name: tuf-nginx-config +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: tuf-nginx-config + namespace: tuf-service +data: + nginx.conf: | + server { + listen 8080; + server_name localhost; + root /usr/share/nginx/html; + + location / { + try_files $uri $uri/ =404; + add_header Access-Control-Allow-Origin *; + add_header Access-Control-Allow-Methods "GET, OPTIONS"; + add_header Access-Control-Allow-Headers "Content-Type"; + } + } +--- +apiVersion: v1 +kind: Service +metadata: + labels: + app.kubernetes.io/name: tuf-server + name: tuf + namespace: tuf-service +spec: + ports: + - name: http + port: 8080 + protocol: TCP + targetPort: 8080 + selector: + app.kubernetes.io/name: tuf-server + type: ClusterIP From 9de6ed6ec54dd28d3222fae5aa9affdb5863881e Mon Sep 17 00:00:00 2001 From: Simon Baird Date: Tue, 10 Mar 2026 13:43:33 -0400 Subject: [PATCH 2/5] Support creating ConfigMaps in acceptance tests Also namespaces, since we want the ConfigMap in a particular namespace. An RBAC is created also so the ConfigMap is readable by every service account. This will be used in the acceptance test added in an upcoming commit. Ref: https://issues.redhat.com/browse/EC-1695 Co-authored-by: Claude Code --- acceptance/kubernetes/kind/kubernetes.go | 133 +++++++++++++++++++++++ acceptance/kubernetes/kubernetes.go | 22 ++++ acceptance/kubernetes/stub/stub.go | 8 ++ acceptance/kubernetes/types/types.go | 2 + 4 files changed, 165 insertions(+) diff --git a/acceptance/kubernetes/kind/kubernetes.go b/acceptance/kubernetes/kind/kubernetes.go index 3f3e413dd..dc8dcb5f4 100644 --- a/acceptance/kubernetes/kind/kubernetes.go +++ b/acceptance/kubernetes/kind/kubernetes.go @@ -32,6 +32,8 @@ import ( pipeline "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1" tekton "github.com/tektoncd/pipeline/pkg/client/clientset/versioned/typed/pipeline/v1" v1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" @@ -191,6 +193,137 @@ func (k *kindCluster) CreateNamedSnapshot(ctx context.Context, name string, spec return k.createSnapshot(ctx, snapshot) } +// CreateConfigMap creates a ConfigMap with the given name and namespace with the provided content +// Also creates necessary RBAC permissions for cross-namespace access +func (k *kindCluster) CreateConfigMap(ctx context.Context, name, namespace, content string) error { + var data map[string]string + + // Parse JSON content and extract individual fields as ConfigMap data keys + if strings.HasPrefix(strings.TrimSpace(content), "{") { + // Parse JSON content + var jsonData map[string]interface{} + if err := json.Unmarshal([]byte(content), &jsonData); err != nil { + return fmt.Errorf("failed to parse JSON content: %w", err) + } + + // Convert to string map for ConfigMap data + data = make(map[string]string) + for key, value := range jsonData { + if value != nil { + data[key] = fmt.Sprintf("%v", value) + } + } + } else { + // For non-JSON content, store as-is under a single key + data = map[string]string{ + "content": content, + } + } + + configMap := &v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + Data: data, + } + + // Create the ConfigMap (or update if it already exists) + if _, err := k.client.CoreV1().ConfigMaps(namespace).Create(ctx, configMap, metav1.CreateOptions{}); err != nil { + if apierrors.IsAlreadyExists(err) { + // ConfigMap exists, so get the existing one to retrieve its ResourceVersion + existing, err := k.client.CoreV1().ConfigMaps(namespace).Get(ctx, name, metav1.GetOptions{}) + if err != nil { + return fmt.Errorf("failed to get existing ConfigMap: %w", err) + } + // Set the ResourceVersion from the existing ConfigMap + configMap.ResourceVersion = existing.ResourceVersion + // Now update with the proper ResourceVersion + if _, err := k.client.CoreV1().ConfigMaps(namespace).Update(ctx, configMap, metav1.UpdateOptions{}); err != nil { + return fmt.Errorf("failed to update existing ConfigMap: %w", err) + } + } else { + return err + } + } + + // Create RBAC permissions for cross-namespace ConfigMap access + // This allows any service account to read ConfigMaps from any namespace + if err := k.ensureConfigMapRBAC(ctx); err != nil { + return fmt.Errorf("failed to create RBAC permissions: %w", err) + } + + return nil +} + +// ensureConfigMapRBAC creates necessary RBAC permissions for ConfigMap access across namespaces +func (k *kindCluster) ensureConfigMapRBAC(ctx context.Context) error { + // Create ClusterRole for ConfigMap reading (idempotent) + clusterRole := &rbacv1.ClusterRole{ + ObjectMeta: metav1.ObjectMeta{ + Name: "acceptance-configmap-reader", + }, + Rules: []rbacv1.PolicyRule{ + { + APIGroups: []string{""}, + Resources: []string{"configmaps"}, + Verbs: []string{"get", "list"}, + }, + }, + } + + if _, err := k.client.RbacV1().ClusterRoles().Create(ctx, clusterRole, metav1.CreateOptions{}); err != nil { + // Ignore error if ClusterRole already exists + if !strings.Contains(err.Error(), "already exists") { + return fmt.Errorf("failed to create ClusterRole: %w", err) + } + } + + // Create ClusterRoleBinding for all service accounts (idempotent) + clusterRoleBinding := &rbacv1.ClusterRoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "acceptance-configmap-reader-binding", + }, + RoleRef: rbacv1.RoleRef{ + APIGroup: "rbac.authorization.k8s.io", + Kind: "ClusterRole", + Name: "acceptance-configmap-reader", + }, + Subjects: []rbacv1.Subject{ + { + Kind: "Group", + Name: "system:serviceaccounts", + APIGroup: "rbac.authorization.k8s.io", + }, + }, + } + + if _, err := k.client.RbacV1().ClusterRoleBindings().Create(ctx, clusterRoleBinding, metav1.CreateOptions{}); err != nil { + // Ignore error if ClusterRoleBinding already exists + if !strings.Contains(err.Error(), "already exists") { + return fmt.Errorf("failed to create ClusterRoleBinding: %w", err) + } + } + + return nil +} + +// CreateNamedNamespace creates a namespace with the specified name +func (k *kindCluster) CreateNamedNamespace(ctx context.Context, name string) error { + _, err := k.client.CoreV1().Namespaces().Create(ctx, &v1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + }, + }, metav1.CreateOptions{}) + + // Ignore error if namespace already exists + if err != nil && strings.Contains(err.Error(), "already exists") { + return nil + } + + return err +} + // CreateNamespace creates a randomly-named namespace for the test to execute in // and stores it in the test context func (k *kindCluster) CreateNamespace(ctx context.Context) (context.Context, error) { diff --git a/acceptance/kubernetes/kubernetes.go b/acceptance/kubernetes/kubernetes.go index c1c8b2ffa..eddbe56e7 100644 --- a/acceptance/kubernetes/kubernetes.go +++ b/acceptance/kubernetes/kubernetes.go @@ -175,6 +175,26 @@ func createNamedSnapshot(ctx context.Context, name string, specification *godog. return c.cluster.CreateNamedSnapshot(ctx, name, specification.Content) } +func createConfigMap(ctx context.Context, name, namespace string, content *godog.DocString) error { + c := testenv.FetchState[ClusterState](ctx) + + if err := mustBeUp(ctx, *c); err != nil { + return err + } + + return c.cluster.CreateConfigMap(ctx, name, namespace, content.Content) +} + +func createNamedNamespace(ctx context.Context, name string) error { + c := testenv.FetchState[ClusterState](ctx) + + if err := mustBeUp(ctx, *c); err != nil { + return err + } + + return c.cluster.CreateNamedNamespace(ctx, name) +} + func createNamedSnapshotWithManyComponents(ctx context.Context, name string, amount int, key string) (context.Context, error) { c := testenv.FetchState[ClusterState](ctx) @@ -499,6 +519,8 @@ func AddStepsTo(sc *godog.ScenarioContext) { sc.Step(`^the task results should match the snapshot$`, taskResultsShouldMatchTheSnapshot) sc.Step(`^the task result "([^"]*)" should equal "([^"]*)"$`, taskResultShouldEqual) sc.Step(`^policy configuration named "([^"]*)" with (\d+) policy sources from "([^"]*)"(?:, patched with)$`, createNamedPolicyWithManySources) + sc.Step(`^a namespace named "([^"]*)" exists$`, createNamedNamespace) + sc.Step(`^a ConfigMap "([^"]*)" in namespace "([^"]*)" with content:$`, createConfigMap) // stop usage of the cluster once a test is done, godog will call this // function on failure and on the last step, so more than once if the // failure is not on the last step and once if there was no failure or the diff --git a/acceptance/kubernetes/stub/stub.go b/acceptance/kubernetes/stub/stub.go index e5c61e065..ca3100a2f 100644 --- a/acceptance/kubernetes/stub/stub.go +++ b/acceptance/kubernetes/stub/stub.go @@ -135,6 +135,14 @@ func (s stubCluster) CreateNamedSnapshot(ctx context.Context, name string, speci }))).WithHeaders(map[string]string{"Content-Type": "application/json"}).WithStatus(200))) } +func (s stubCluster) CreateConfigMap(_ context.Context, _, _, _ string) error { + return errors.New("ConfigMap creation is not supported when using the stub Kubernetes") +} + +func (s stubCluster) CreateNamedNamespace(_ context.Context, _ string) error { + return errors.New("Named namespace creation is not supported when using the stub Kubernetes") +} + func (s stubCluster) CreatePolicy(_ context.Context, _ string) error { return errors.New("use `Given policy configuration named \"\" with specification` when using the stub Kubernetes") } diff --git a/acceptance/kubernetes/types/types.go b/acceptance/kubernetes/types/types.go index c747a5144..4c4dd62d8 100644 --- a/acceptance/kubernetes/types/types.go +++ b/acceptance/kubernetes/types/types.go @@ -31,6 +31,8 @@ type Cluster interface { AwaitUntilTaskIsDone(context.Context) (bool, error) TaskInfo(context.Context) (*TaskInfo, error) CreateNamedSnapshot(context.Context, string, string) error + CreateNamedNamespace(context.Context, string) error + CreateConfigMap(context.Context, string, string, string) error Registry(context.Context) (string, error) BuildSnapshotArtifact(context.Context, string) (context.Context, error) } From 62613e84aceed7ca7307b68bc5e8d5a725a9c29d Mon Sep 17 00:00:00 2001 From: Simon Baird Date: Fri, 13 Mar 2026 19:55:58 -0400 Subject: [PATCH 3/5] Reduce policy repetition in feature file I got tired of looking at the repeated policies in task_validate_image.feature. This technique could perhaps be extended to other feature files, but I don't want to do that right now. Unrelated to, but created while working on the PR for... Ref: https://issues.redhat.com/browse/EC-1695 Co-authored-by: Claude Code --- acceptance/kubernetes/kubernetes.go | 54 +++++++++- features/task_validate_image.feature | 154 +++------------------------ 2 files changed, 65 insertions(+), 143 deletions(-) diff --git a/acceptance/kubernetes/kubernetes.go b/acceptance/kubernetes/kubernetes.go index eddbe56e7..380f618df 100644 --- a/acceptance/kubernetes/kubernetes.go +++ b/acceptance/kubernetes/kubernetes.go @@ -122,7 +122,6 @@ func buildSnapshotArtifact(ctx context.Context, specification *godog.DocString) } return c.cluster.BuildSnapshotArtifact(ctx, specification.Content) - } func createNamedPolicy(ctx context.Context, name string, specification *godog.DocString) error { @@ -498,6 +497,56 @@ func stepEnvVarShouldBe(ctx context.Context, stepName, envName, want string) err return fmt.Errorf("step %q not found when looking for the %q env var", stepName, envName) } +// createBasicPolicyWithKnownKey creates a basic policy with ${known_PUBLIC_KEY} +// Avoid some repetition in feature files that use the same policy repeatedly +func createBasicPolicyWithKnownKey(ctx context.Context) error { + policyContent := `{"publicKey": ${known_PUBLIC_KEY}}` + return createPolicy(ctx, &godog.DocString{Content: policyContent}) +} + +// createSLSAProvenancePolicy creates a policy for SLSA provenance checking +// Avoid some repetition in feature files that use the same policy repeatedly +func createSLSAProvenancePolicy(ctx context.Context) error { + policyContent := `{ + "sources": [ + { + "policy": [ + "github.com/conforma/policy//policy/release?ref=0de5461c14413484575e63e96ddb514d8ab954b5", + "github.com/conforma/policy//policy/lib?ref=0de5461c14413484575e63e96ddb514d8ab954b5" + ], + "config": { + "include": [ + "slsa_provenance_available" + ] + } + } + ] +}` + return createPolicy(ctx, &godog.DocString{Content: policyContent}) +} + +// createGoldenContainerPolicy creates the hardcoded policy used in golden container scenarios +// Avoid some repetition in feature files that use the same policy repeatedly +func createGoldenContainerPolicy(ctx context.Context) error { + policyContent := `{ + "publicKey": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAERhr8Zj4dZW67zucg8fDr11M4lmRp\nzN6SIcIjkvH39siYg1DkCoa2h2xMUZ10ecbM3/ECqvBV55YwQ2rcIEa7XQ==\n-----END PUBLIC KEY-----", + "sources": [ + { + "policy": [ + "github.com/conforma/policy//policy/release?ref=0de5461c14413484575e63e96ddb514d8ab954b5", + "github.com/conforma/policy//policy/lib?ref=0de5461c14413484575e63e96ddb514d8ab954b5" + ], + "config": { + "include": [ + "slsa_provenance_available" + ] + } + } + ] +}` + return createPolicy(ctx, &godog.DocString{Content: policyContent}) +} + // AddStepsTo adds cluster-related steps to the context func AddStepsTo(sc *godog.ScenarioContext) { sc.Step(`^a stub cluster running$`, startAndSetupState(stub.Start)) @@ -506,6 +555,9 @@ func AddStepsTo(sc *godog.ScenarioContext) { sc.Step(`^a snapshot artifact with content:$`, buildSnapshotArtifact) sc.Step(`^policy configuration named "([^"]*)" with specification$`, createNamedPolicy) sc.Step(`^a cluster policy with content:$`, createPolicy) + sc.Step(`^a basic policy with known public key$`, createBasicPolicyWithKnownKey) + sc.Step(`^a policy for SLSA provenance checking$`, createSLSAProvenancePolicy) + sc.Step(`^a golden container policy$`, createGoldenContainerPolicy) sc.Step(`^version ([\d.]+) of the task named "([^"]*)" is run with parameters:$`, runTask) sc.Step(`^version ([\d.]+) of the task named "([^"]*)" with workspace "([^"]*)" is run with parameters:$`, runTaskWithWorkspace) sc.Step(`^the task should succeed$`, theTaskShouldSucceed) diff --git a/features/task_validate_image.feature b/features/task_validate_image.feature index de205d33b..3dbb2beb0 100644 --- a/features/task_validate_image.feature +++ b/features/task_validate_image.feature @@ -9,25 +9,7 @@ Feature: Verify Enterprise Contract Tekton Tasks Scenario: Golden container image Given a working namespace - Given a cluster policy with content: - ``` - { - "publicKey": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAERhr8Zj4dZW67zucg8fDr11M4lmRp\nzN6SIcIjkvH39siYg1DkCoa2h2xMUZ10ecbM3/ECqvBV55YwQ2rcIEa7XQ==\n-----END PUBLIC KEY-----", - "sources": [ - { - "policy": [ - "github.com/conforma/policy//policy/release?ref=0de5461c14413484575e63e96ddb514d8ab954b5", - "github.com/conforma/policy//policy/lib?ref=0de5461c14413484575e63e96ddb514d8ab954b5" - ], - "config": { - "include": [ - "slsa_provenance_available" - ] - } - } - ] - } - ``` + Given a golden container policy When version 0.1 of the task named "verify-enterprise-contract" is run with parameters: | IMAGES | {"components": [{"containerImage": "quay.io/hacbs-contract-demo/golden-container@sha256:e76a4ae9dd8a52a0d191fd34ca133af5b4f2609536d32200a4a40a09fdc93a0d"}]} | | POLICY_CONFIGURATION | ${NAMESPACE}/${POLICY_NAME} | @@ -40,25 +22,7 @@ Feature: Verify Enterprise Contract Tekton Tasks Scenario: Extra rule data provided to task Given a working namespace - Given a cluster policy with content: - ``` - { - "publicKey": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAERhr8Zj4dZW67zucg8fDr11M4lmRp\nzN6SIcIjkvH39siYg1DkCoa2h2xMUZ10ecbM3/ECqvBV55YwQ2rcIEa7XQ==\n-----END PUBLIC KEY-----", - "sources": [ - { - "policy": [ - "github.com/conforma/policy//policy/release", - "github.com/conforma/policy//policy/lib" - ], - "config": { - "include": [ - "slsa_provenance_available" - ] - } - } - ] - } - ``` + Given a golden container policy When version 0.1 of the task named "verify-enterprise-contract" is run with parameters: | IMAGES | {"components": [{"containerImage": "quay.io/hacbs-contract-demo/golden-container@sha256:e76a4ae9dd8a52a0d191fd34ca133af5b4f2609536d32200a4a40a09fdc93a0d"}]} | | POLICY_CONFIGURATION | ${NAMESPACE}/${POLICY_NAME} | @@ -71,25 +35,7 @@ Feature: Verify Enterprise Contract Tekton Tasks Scenario: Initialize TUF succeeds Given a working namespace - Given a cluster policy with content: - ``` - { - "publicKey": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAERhr8Zj4dZW67zucg8fDr11M4lmRp\nzN6SIcIjkvH39siYg1DkCoa2h2xMUZ10ecbM3/ECqvBV55YwQ2rcIEa7XQ==\n-----END PUBLIC KEY-----", - "sources": [ - { - "policy": [ - "github.com/conforma/policy//policy/release", - "github.com/conforma/policy//policy/lib" - ], - "config": { - "include": [ - "slsa_provenance_available" - ] - } - } - ] - } - ``` + Given a golden container policy When version 0.1 of the task named "verify-enterprise-contract" is run with parameters: | IMAGES | {"components": [{"containerImage": "quay.io/hacbs-contract-demo/golden-container@sha256:e76a4ae9dd8a52a0d191fd34ca133af5b4f2609536d32200a4a40a09fdc93a0d"}]} | | POLICY_CONFIGURATION | ${NAMESPACE}/${POLICY_NAME} | @@ -103,25 +49,7 @@ Feature: Verify Enterprise Contract Tekton Tasks Scenario: Initialize TUF fails Given a working namespace - Given a cluster policy with content: - ``` - { - "publicKey": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAERhr8Zj4dZW67zucg8fDr11M4lmRp\nzN6SIcIjkvH39siYg1DkCoa2h2xMUZ10ecbM3/ECqvBV55YwQ2rcIEa7XQ==\n-----END PUBLIC KEY-----", - "sources": [ - { - "policy": [ - "github.com/conforma/policy//policy/release", - "github.com/conforma/policy//policy/lib" - ], - "config": { - "include": [ - "slsa_provenance_available" - ] - } - } - ] - } - ``` + Given a golden container policy When version 0.1 of the task named "verify-enterprise-contract" is run with parameters: | IMAGES | {"components": [{"containerImage": "quay.io/hacbs-contract-demo/golden-container@sha256:e76a4ae9dd8a52a0d191fd34ca133af5b4f2609536d32200a4a40a09fdc93a0d"}]} | | POLICY_CONFIGURATION | ${NAMESPACE}/${POLICY_NAME} | @@ -206,12 +134,7 @@ Feature: Verify Enterprise Contract Tekton Tasks Scenario: Non strict with failures Given a working namespace And a key pair named "known" - And a cluster policy with content: - ``` - { - "publicKey": ${known_PUBLIC_KEY} - } - ``` + And a basic policy with known public key When version 0.1 of the task named "verify-enterprise-contract" is run with parameters: | IMAGES | {"components": [{"containerImage": "${REGISTRY}/acceptance/does-not-exist"}]} | | POLICY_CONFIGURATION | ${NAMESPACE}/${POLICY_NAME} | @@ -224,12 +147,7 @@ Feature: Verify Enterprise Contract Tekton Tasks Scenario: Strict with failures Given a working namespace And a key pair named "known" - And a cluster policy with content: - ``` - { - "publicKey": ${known_PUBLIC_KEY} - } - ``` + And a basic policy with known public key When version 0.1 of the task named "verify-enterprise-contract" is run with parameters: | IMAGES | {"components": [{"containerImage": "${REGISTRY}/acceptance/does-not-exist"}]} | | POLICY_CONFIGURATION | ${NAMESPACE}/${POLICY_NAME} | @@ -245,12 +163,7 @@ Feature: Verify Enterprise Contract Tekton Tasks And an image named "acceptance/okayish" And a valid image signature of "acceptance/okayish" image signed by the "known" key And a valid attestation of "acceptance/okayish" signed by the "known" key - And a cluster policy with content: - ``` - { - "publicKey": ${known_PUBLIC_KEY} - } - ``` + And a basic policy with known public key When version 0.1 of the task named "verify-enterprise-contract" is run with parameters: | IMAGES | {"components": [{"containerImage": "${REGISTRY}/acceptance/okayish"}]} | | POLICY_CONFIGURATION | ${NAMESPACE}/${POLICY_NAME} | @@ -269,10 +182,7 @@ Feature: Verify Enterprise Contract Tekton Tasks And an image named "acceptance/info" And a valid image signature of "acceptance/info" image signed by the "known" key And a valid attestation of "acceptance/info" signed by the "known" key - And a cluster policy with content: - ``` - {"publicKey": ${known_PUBLIC_KEY}} - ``` + And a basic policy with known public key When version 0.1 of the task named "verify-enterprise-contract" is run with parameters: | IMAGES | {"components": [{"containerImage": "${REGISTRY}/acceptance/info"}]} | | POLICY_CONFIGURATION | ${NAMESPACE}/${POLICY_NAME} | @@ -288,10 +198,7 @@ Feature: Verify Enterprise Contract Tekton Tasks And an image named "acceptance/effective-time" And a valid image signature of "acceptance/effective-time" image signed by the "known" key And a valid attestation of "acceptance/effective-time" signed by the "known" key - And a cluster policy with content: - ``` - {"publicKey": ${known_PUBLIC_KEY}} - ``` + And a basic policy with known public key When version 0.1 of the task named "verify-enterprise-contract" is run with parameters: | IMAGES | {"components": [{"containerImage": "${REGISTRY}/acceptance/effective-time"}]} | | POLICY_CONFIGURATION | ${NAMESPACE}/${POLICY_NAME} | @@ -306,10 +213,7 @@ Feature: Verify Enterprise Contract Tekton Tasks And an image named "acceptance/ssl-cert-dir" And a valid image signature of "acceptance/ssl-cert-dir" image signed by the "known" key And a valid attestation of "acceptance/ssl-cert-dir" signed by the "known" key - And a cluster policy with content: - ``` - {"publicKey": ${known_PUBLIC_KEY}} - ``` + And a basic policy with known public key When version 0.1 of the task named "verify-enterprise-contract" is run with parameters: | IMAGES | {"components": [{"containerImage": "${REGISTRY}/acceptance/ssl-cert-dir"}]} | | POLICY_CONFIGURATION | ${NAMESPACE}/${POLICY_NAME} | @@ -350,24 +254,7 @@ Feature: Verify Enterprise Contract Tekton Tasks # Confirm we can verify the signatures on a keylessly signed image signed with cosign v2 Scenario: Keyless signing verification cosign v2 style Given a working namespace - Given a cluster policy with content: - ``` - { - "sources": [ - { - "policy": [ - "github.com/conforma/policy//policy/release?ref=0de5461c14413484575e63e96ddb514d8ab954b5", - "github.com/conforma/policy//policy/lib?ref=0de5461c14413484575e63e96ddb514d8ab954b5" - ], - "config": { - "include": [ - "slsa_provenance_available" - ] - } - } - ] - } - ``` + Given a policy for SLSA provenance checking When version 0.1 of the task named "verify-enterprise-contract" is run with parameters: | IMAGES | {"components": [{"containerImage": "quay.io/conforma/test:keyless_v2@sha256:03a10dff06ae364ef9727d562e7077b135b00c7a978e571c4354519e6d0f23b8"}]} | | POLICY_CONFIGURATION | ${NAMESPACE}/${POLICY_NAME} | @@ -383,24 +270,7 @@ Feature: Verify Enterprise Contract Tekton Tasks # Confirm we can verify the signatures on a keylessly signed image signed with cosign v3 Scenario: Keyless signing verification cosign v3 style Given a working namespace - Given a cluster policy with content: - ``` - { - "sources": [ - { - "policy": [ - "github.com/conforma/policy//policy/release?ref=0de5461c14413484575e63e96ddb514d8ab954b5", - "github.com/conforma/policy//policy/lib?ref=0de5461c14413484575e63e96ddb514d8ab954b5" - ], - "config": { - "include": [ - "slsa_provenance_available" - ] - } - } - ] - } - ``` + Given a policy for SLSA provenance checking When version 0.1 of the task named "verify-enterprise-contract" is run with parameters: | IMAGES | {"components": [{"containerImage": "quay.io/conforma/test:keyless_v3@sha256:712ca3a7fcd41fe6b3e6f434a31f738743b6c31f1d81ad458502d6b0239a8903"}]} | | POLICY_CONFIGURATION | ${NAMESPACE}/${POLICY_NAME} | From 9dde86139a1773a906df634f5f544a7274724db7 Mon Sep 17 00:00:00 2001 From: Simon Baird Date: Wed, 18 Mar 2026 14:44:59 -0400 Subject: [PATCH 4/5] Improvements for task keyless signing support - Add acceptance tests that use the local test image instead of the "real" image in quay. This was enabled by the previous commit related to running nginx TUF in the cluster. - Add support for regexp style params keyless signature verification params - Minor tweak to the handling of ignore rekor so we don't ignore it when doing keyless. - Minor improvements/tidying with the task param logic Note: I was trying to maintain several smaller commits, but it became difficult after some (possibly unrelated) local acceptance test problems. Ref: https://redhat.atlassian.net/browse/EC-1695 --- acceptance/image/image.go | 10 +- .../pages/verify-conforma-konflux-ta.adoc | 2 + .../pages/verify-enterprise-contract.adoc | 2 + .../__snapshots__/task_validate_image.snap | 101 +++++++++++++++++- features/task_validate_image.feature | 36 +++++++ .../0.1/verify-conforma-konflux-ta.yaml | 56 +++++++++- .../0.1/verify-enterprise-contract.yaml | 56 +++++++++- 7 files changed, 251 insertions(+), 12 deletions(-) diff --git a/acceptance/image/image.go b/acceptance/image/image.go index 30774fc43..6328cf66a 100644 --- a/acceptance/image/image.go +++ b/acceptance/image/image.go @@ -761,6 +761,11 @@ func createAndPushKeylessImage(ctx context.Context, imageName string) (context.C continue } + // Skip empty signatures (DSSE envelopes have empty signature annotations) + if signature == "" { + continue + } + sig := Signature{ Signature: signature, } @@ -995,7 +1000,10 @@ func RawAttestationSignaturesFrom(ctx context.Context) map[string]string { ret := map[string]string{} for ref, signature := range state.AttestationSignatures { - ret[fmt.Sprintf("ATTESTATION_SIGNATURE_%s", ref)] = signature.Signature + // Only add the variable if the signature is not empty to avoid polluting snapshots + if signature.Signature != "" { + ret[fmt.Sprintf("ATTESTATION_SIGNATURE_%s", ref)] = signature.Signature + } } return ret diff --git a/docs/modules/ROOT/pages/verify-conforma-konflux-ta.adoc b/docs/modules/ROOT/pages/verify-conforma-konflux-ta.adoc index 844d2c63d..c4ef66d46 100644 --- a/docs/modules/ROOT/pages/verify-conforma-konflux-ta.adoc +++ b/docs/modules/ROOT/pages/verify-conforma-konflux-ta.adoc @@ -25,6 +25,8 @@ You can also specify a policy configuration using a git url, e.g. *REKOR_HOST* (`string`):: Rekor host for transparency log lookups *CERTIFICATE_IDENTITY* (`string`):: Expected identity in the signing certificate for keyless verification. This should be the email or URI that was used when signing. You should provide both CERTIFICATE_OIDC_ISSUER and CERTIFICATE_IDENTITY for keyless verification. The PUBLIC_KEY param will be ignored if this is provided. *CERTIFICATE_OIDC_ISSUER* (`string`):: Expected OIDC issuer in the signing certificate for keyless verification. This should match the issuer that provided the identity token used for signing. You should provide both CERTIFICATE_OIDC_ISSUER and CERTIFICATE_IDENTITY for keyless verification. The PUBLIC_KEY param will be ignored if this is provided. +*CERTIFICATE_IDENTITY_REGEXP* (`string`):: Similar to CERTIFICATE_IDENTITY but the value is a regexp that will be matched. Note that CERTIFICATE_IDENTITY takes precendence over this if both are present. +*CERTIFICATE_OIDC_ISSUER_REGEXP* (`string`):: Similar to CERTIFICATE_OIDC_ISSUER but a regexp that will be matched. Note that CERTIFICATE_OIDC_ISSUER takes precendence over this if both are present. *IGNORE_REKOR* (`string`):: Skip Rekor transparency log checks during validation. + *Default*: `false` diff --git a/docs/modules/ROOT/pages/verify-enterprise-contract.adoc b/docs/modules/ROOT/pages/verify-enterprise-contract.adoc index 2a0a9c25a..e96d85832 100644 --- a/docs/modules/ROOT/pages/verify-enterprise-contract.adoc +++ b/docs/modules/ROOT/pages/verify-enterprise-contract.adoc @@ -36,6 +36,8 @@ You can also specify a policy configuration using a git url, e.g. *REKOR_HOST* (`string`):: Rekor host for transparency log lookups *CERTIFICATE_IDENTITY* (`string`):: Expected identity in the signing certificate for keyless verification. This should be the email or URI that was used when signing. You should provide both CERTIFICATE_OIDC_ISSUER and CERTIFICATE_IDENTITY for keyless verification. The PUBLIC_KEY param will be ignored if this is provided. *CERTIFICATE_OIDC_ISSUER* (`string`):: Expected OIDC issuer in the signing certificate for keyless verification. This should match the issuer that provided the identity token used for signing. You should provide both CERTIFICATE_OIDC_ISSUER and CERTIFICATE_IDENTITY for keyless verification. The PUBLIC_KEY param will be ignored if this is provided. +*CERTIFICATE_IDENTITY_REGEXP* (`string`):: Similar to CERTIFICATE_IDENTITY but the value is a regexp that will be matched. Note that CERTIFICATE_IDENTITY takes precendence over this if both are present. +*CERTIFICATE_OIDC_ISSUER_REGEXP* (`string`):: Similar to CERTIFICATE_OIDC_ISSUER but a regexp that will be matched. Note that CERTIFICATE_OIDC_ISSUER takes precendence over this if both are present. *IGNORE_REKOR* (`string`):: Skip Rekor transparency log checks during validation. + *Default*: `false` diff --git a/features/__snapshots__/task_validate_image.snap b/features/__snapshots__/task_validate_image.snap index 91a7a9e69..68cdf45b6 100755 --- a/features/__snapshots__/task_validate_image.snap +++ b/features/__snapshots__/task_validate_image.snap @@ -63,7 +63,7 @@ Error: Get "http://tuf.invalid/root.json": dial tcp: lookup tuf.invalid on 10.96 --- [Initialize TUF succeeds:initialize-tuf - 1] -${TIMESTAMP} INFO Step was skipped due to when expressions were evaluated to false. +WARNING: Could not fetch signing_config.json from the TUF mirror (encountered error: getting info for target "signing_config.v0.2.json": target signing_config.v0.2.json not found). It is recommended to use a signing config file rather than provide service URLs when signing. --- @@ -453,3 +453,102 @@ true "TEST_OUTPUT": "{\"timestamp\":\"${TIMESTAMP}\",\"namespace\":\"\",\"successes\":5,\"failures\":0,\"warnings\":0,\"result\":\"SUCCESS\"}\n" } --- + +[Keyless signing verification with local test image:detailed-report - 1] +Success: true +Result: SUCCESS +Violations: 0, Warnings: 0, Successes: 5 +Component: +ImageRef: ${REGISTRY}/acceptance/ec-happy-day-keyless@sha256:${REGISTRY_acceptance/ec-happy-day-keyless:latest_DIGEST} + + +--- + +[Keyless signing verification with local test image:results - 1] +{ + "TEST_OUTPUT": "{\"timestamp\":\"${TIMESTAMP}\",\"namespace\":\"\",\"successes\":5,\"failures\":0,\"warnings\":0,\"result\":\"SUCCESS\"}\n" +} +--- + +[Keyless signing verification with local test image using regexp params:detailed-report - 1] +Success: true +Result: SUCCESS +Violations: 0, Warnings: 0, Successes: 5 +Component: +ImageRef: ${REGISTRY}/acceptance/ec-happy-day-keyless@sha256:${REGISTRY_acceptance/ec-happy-day-keyless:latest_DIGEST} + + +--- + +[Keyless signing verification with local test image using regexp params:results - 1] +{ + "TEST_OUTPUT": "{\"timestamp\":\"${TIMESTAMP}\",\"namespace\":\"\",\"successes\":5,\"failures\":0,\"warnings\":0,\"result\":\"SUCCESS\"}\n" +} +--- + +[Collect keyless signing parameters when there is a malformed ConfigMap:collect-signing-params - 1] +Reading ConfigMap konflux-info/cluster-config-3 +ConfigMap found, extracting keyless signing parameters +enableKeylessSigning is not set, using default empty values +results.keylessSigningEnabled: false +results.defaultOIDCIssuer: +results.rekorExternalUrl: +results.fulcioExternalUrl: +results.tufExternalUrl: +results.buildIdentity: +results.buildIdentityRegexp: + +--- + +[Collect keyless signing parameters when the namespace does not exist:collect-signing-params - 1] +Reading ConfigMap doesnt-exist-namespace/whatever +ConfigMap not found, using default empty values +results.keylessSigningEnabled: false +results.defaultOIDCIssuer: +results.rekorExternalUrl: +results.fulcioExternalUrl: +results.tufExternalUrl: +results.buildIdentity: +results.buildIdentityRegexp: + +--- + +[Collect keyless signing parameters when the ConfigMap does not exist:collect-signing-params - 1] +Reading ConfigMap konflux-info/doesnt-exist-config +ConfigMap not found, using default empty values +results.keylessSigningEnabled: false +results.defaultOIDCIssuer: +results.rekorExternalUrl: +results.fulcioExternalUrl: +results.tufExternalUrl: +results.buildIdentity: +results.buildIdentityRegexp: + +--- + +[Collect keyless signing parameters from ConfigMap with keyless signing disabled:collect-signing-params - 1] +Reading ConfigMap konflux-info/cluster-config-2 +ConfigMap found, extracting keyless signing parameters +enableKeylessSigning is not set, using default empty values +results.keylessSigningEnabled: false +results.defaultOIDCIssuer: +results.rekorExternalUrl: +results.fulcioExternalUrl: +results.tufExternalUrl: +results.buildIdentity: +results.buildIdentityRegexp: + +--- + +[Collect keyless signing parameters from ConfigMap:collect-signing-params - 1] +Reading ConfigMap konflux-info/cluster-config +ConfigMap found, extracting keyless signing parameters +results.keylessSigningEnabled: true +results.defaultOIDCIssuer: https://kubernetes.default.svc.cluster.local +results.rekorExternalUrl: https://rekor.example.com +results.fulcioExternalUrl: https://fulcio.example.com +results.tufExternalUrl: https://tuf.example.com +results.buildIdentity: https://kubernetes.io/namespaces/openshift-pipelines/serviceaccounts/tekton-chains-controller +results.buildIdentityRegexp: ^https://konflux-ci.dev/.*$ + +--- diff --git a/features/task_validate_image.feature b/features/task_validate_image.feature index 3dbb2beb0..568f1e9d9 100644 --- a/features/task_validate_image.feature +++ b/features/task_validate_image.feature @@ -282,3 +282,39 @@ Feature: Verify Enterprise Contract Tekton Tasks Then the task should succeed And the task logs for step "report-json" should match the snapshot And the task results should match the snapshot + + Scenario: Keyless signing verification with local test image + Given a working namespace + Given a signed and attested keyless image named "acceptance/ec-happy-day-keyless" + Given a initialized tuf root + Given a policy for SLSA provenance checking + When version 0.1 of the task named "verify-enterprise-contract" is run with parameters: + | IMAGES | {"components": [{"containerImage": "${REGISTRY}/acceptance/ec-happy-day-keyless"}]} | + | POLICY_CONFIGURATION | ${NAMESPACE}/${POLICY_NAME} | + | CERTIFICATE_IDENTITY | https://kubernetes.io/namespaces/default/serviceaccounts/default | + | CERTIFICATE_OIDC_ISSUER | https://kubernetes.default.svc.cluster.local | + | TUF_MIRROR | ${TUF} | + | REKOR_HOST | ${REKOR} | + | IGNORE_REKOR | false | + | STRICT | true | + Then the task should succeed + And the task logs for step "detailed-report" should match the snapshot + And the task results should match the snapshot + + Scenario: Keyless signing verification with local test image using regexp params + Given a working namespace + Given a signed and attested keyless image named "acceptance/ec-happy-day-keyless" + Given a initialized tuf root + Given a policy for SLSA provenance checking + When version 0.1 of the task named "verify-enterprise-contract" is run with parameters: + | IMAGES | {"components": [{"containerImage": "${REGISTRY}/acceptance/ec-happy-day-keyless"}]} | + | POLICY_CONFIGURATION | ${NAMESPACE}/${POLICY_NAME} | + | CERTIFICATE_IDENTITY_REGEXP | ^https://kubernetes\.io/namespaces/.*/serviceaccounts/.*$ | + | CERTIFICATE_OIDC_ISSUER_REGEXP | ^https://kubernetes\.default\.svc\.cluster\.local$ | + | TUF_MIRROR | ${TUF} | + | REKOR_HOST | ${REKOR} | + | IGNORE_REKOR | false | + | STRICT | true | + Then the task should succeed + And the task logs for step "detailed-report" should match the snapshot + And the task results should match the snapshot diff --git a/tasks/verify-conforma-konflux-ta/0.1/verify-conforma-konflux-ta.yaml b/tasks/verify-conforma-konflux-ta/0.1/verify-conforma-konflux-ta.yaml index 8d06266ef..271c12724 100644 --- a/tasks/verify-conforma-konflux-ta/0.1/verify-conforma-konflux-ta.yaml +++ b/tasks/verify-conforma-konflux-ta/0.1/verify-conforma-konflux-ta.yaml @@ -83,6 +83,20 @@ spec: keyless verification. The PUBLIC_KEY param will be ignored if this is provided. default: "" + - name: CERTIFICATE_IDENTITY_REGEXP + type: string + description: >- + Similar to CERTIFICATE_IDENTITY but the value is a regexp that will be matched. + Note that CERTIFICATE_IDENTITY takes precendence over this if both are present. + default: "" + + - name: CERTIFICATE_OIDC_ISSUER_REGEXP + type: string + description: >- + Similar to CERTIFICATE_OIDC_ISSUER but a regexp that will be matched. Note that + CERTIFICATE_OIDC_ISSUER takes precendence over this if both are present. + default: "" + - name: IGNORE_REKOR type: string description: >- @@ -318,20 +332,48 @@ spec: # To keep bash logic as thin as possible we deliberately don't sanitize # these params. If something is wrong or missing let Conforma handle it. - if [ -n "${CERTIFICATE_IDENTITY}" ] || [ -n "${CERTIFICATE_OIDC_ISSUER}" ]; then + + if [ -n "${CERTIFICATE_IDENTITY}" ] || \ + [ -n "${CERTIFICATE_OIDC_ISSUER}" ] || \ + [ -n "${CERTIFICATE_IDENTITY_REGEXP}" ] || \ + [ -n "${CERTIFICATE_OIDC_ISSUER_REGEXP}" ]; then + # If *any* of the above are non-empty assume the intention is to + # try keyless verification + + if [ -n "${CERTIFICATE_IDENTITY}" ]; then + cmd_args+=( + --certificate-identity="${CERTIFICATE_IDENTITY}" + ) + elif [ -n "${CERTIFICATE_IDENTITY_REGEXP}" ]; then + cmd_args+=( + --certificate-identity-regexp="${CERTIFICATE_IDENTITY_REGEXP}" + ) + fi + + if [ -n "${CERTIFICATE_OIDC_ISSUER}" ]; then + cmd_args+=( + --certificate-oidc-issuer="${CERTIFICATE_OIDC_ISSUER}" + ) + elif [ -n "${CERTIFICATE_OIDC_ISSUER_REGEXP}" ]; then + cmd_args+=( + --certificate-oidc-issuer-regexp="${CERTIFICATE_OIDC_ISSUER_REGEXP}" + ) + fi + + # Force --ignore-rekor to false since we need rekor cmd_args+=( - --certificate-identity="${CERTIFICATE_IDENTITY}" - --certificate-oidc-issuer="${CERTIFICATE_OIDC_ISSUER}" + --ignore-rekor=false ) - elif [ -n "${PUBLIC_KEY}" ]; then + else + # Assume traditional signing secret verification cmd_args+=( --public-key="${PUBLIC_KEY}" + --ignore-rekor="${IGNORE_REKOR}" ) fi cmd_args+=( --rekor-url="${REKOR_HOST}" - --ignore-rekor="${IGNORE_REKOR}" --workers="${WORKERS}" --info="${INFO}" --timeout=0 @@ -407,6 +449,10 @@ spec: value: "$(params.CERTIFICATE_IDENTITY)" - name: CERTIFICATE_OIDC_ISSUER value: "$(params.CERTIFICATE_OIDC_ISSUER)" + - name: CERTIFICATE_IDENTITY_REGEXP + value: "$(params.CERTIFICATE_IDENTITY_REGEXP)" + - name: CERTIFICATE_OIDC_ISSUER_REGEXP + value: "$(params.CERTIFICATE_OIDC_ISSUER_REGEXP)" - name: REKOR_HOST value: "$(params.REKOR_HOST)" - name: IGNORE_REKOR diff --git a/tasks/verify-enterprise-contract/0.1/verify-enterprise-contract.yaml b/tasks/verify-enterprise-contract/0.1/verify-enterprise-contract.yaml index f30b43485..d9845c694 100644 --- a/tasks/verify-enterprise-contract/0.1/verify-enterprise-contract.yaml +++ b/tasks/verify-enterprise-contract/0.1/verify-enterprise-contract.yaml @@ -90,6 +90,20 @@ spec: keyless verification. The PUBLIC_KEY param will be ignored if this is provided. default: "" + - name: CERTIFICATE_IDENTITY_REGEXP + type: string + description: >- + Similar to CERTIFICATE_IDENTITY but the value is a regexp that will be matched. + Note that CERTIFICATE_IDENTITY takes precendence over this if both are present. + default: "" + + - name: CERTIFICATE_OIDC_ISSUER_REGEXP + type: string + description: >- + Similar to CERTIFICATE_OIDC_ISSUER but a regexp that will be matched. Note that + CERTIFICATE_OIDC_ISSUER takes precendence over this if both are present. + default: "" + - name: IGNORE_REKOR type: string description: >- @@ -270,20 +284,48 @@ spec: # To keep bash logic as thin as possible we deliberately don't sanitize # these params. If something is wrong or missing let Conforma handle it. - if [ -n "${CERTIFICATE_IDENTITY}" ] || [ -n "${CERTIFICATE_OIDC_ISSUER}" ]; then + + if [ -n "${CERTIFICATE_IDENTITY}" ] || \ + [ -n "${CERTIFICATE_OIDC_ISSUER}" ] || \ + [ -n "${CERTIFICATE_IDENTITY_REGEXP}" ] || \ + [ -n "${CERTIFICATE_OIDC_ISSUER_REGEXP}" ]; then + # If *any* of the above are non-empty assume the intention is to + # try keyless verification + + if [ -n "${CERTIFICATE_IDENTITY}" ]; then + cmd_args+=( + --certificate-identity="${CERTIFICATE_IDENTITY}" + ) + elif [ -n "${CERTIFICATE_IDENTITY_REGEXP}" ]; then + cmd_args+=( + --certificate-identity-regexp="${CERTIFICATE_IDENTITY_REGEXP}" + ) + fi + + if [ -n "${CERTIFICATE_OIDC_ISSUER}" ]; then + cmd_args+=( + --certificate-oidc-issuer="${CERTIFICATE_OIDC_ISSUER}" + ) + elif [ -n "${CERTIFICATE_OIDC_ISSUER_REGEXP}" ]; then + cmd_args+=( + --certificate-oidc-issuer-regexp="${CERTIFICATE_OIDC_ISSUER_REGEXP}" + ) + fi + + # Force --ignore-rekor to false since we need rekor cmd_args+=( - --certificate-identity="${CERTIFICATE_IDENTITY}" - --certificate-oidc-issuer="${CERTIFICATE_OIDC_ISSUER}" + --ignore-rekor=false ) - elif [ -n "${PUBLIC_KEY}" ]; then + else + # Assume traditional signing secret verification cmd_args+=( --public-key="${PUBLIC_KEY}" + --ignore-rekor="${IGNORE_REKOR}" ) fi cmd_args+=( --rekor-url="${REKOR_HOST}" - --ignore-rekor="${IGNORE_REKOR}" --workers="${WORKERS}" --info="${INFO}" --timeout=0 @@ -313,6 +355,10 @@ spec: value: "$(params.CERTIFICATE_IDENTITY)" - name: CERTIFICATE_OIDC_ISSUER value: "$(params.CERTIFICATE_OIDC_ISSUER)" + - name: CERTIFICATE_IDENTITY_REGEXP + value: "$(params.CERTIFICATE_IDENTITY_REGEXP)" + - name: CERTIFICATE_OIDC_ISSUER_REGEXP + value: "$(params.CERTIFICATE_OIDC_ISSUER_REGEXP)" - name: REKOR_HOST value: "$(params.REKOR_HOST)" - name: IGNORE_REKOR From 142a144ffaa8d765a8872bc9d6efe402c1c80514 Mon Sep 17 00:00:00 2001 From: Simon Baird Date: Wed, 18 Mar 2026 14:59:40 -0400 Subject: [PATCH 5/5] Add new task to collect keyless signing params Ref: https://redhat.atlassian.net/browse/EC-1695 Co-authored-by: Claude Code --- .../pages/collect-keyless-signing-params.adoc | 41 +++++ docs/modules/ROOT/partials/tasks_nav.adoc | 1 + .../__snapshots__/task_validate_image.snap | 67 +++++++ features/task_validate_image.feature | 118 +++++++++++++ .../0.1/collect-keyless-signing-params.yaml | 167 ++++++++++++++++++ 5 files changed, 394 insertions(+) create mode 100644 docs/modules/ROOT/pages/collect-keyless-signing-params.adoc create mode 100644 tasks/collect-keyless-signing-params/0.1/collect-keyless-signing-params.yaml diff --git a/docs/modules/ROOT/pages/collect-keyless-signing-params.adoc b/docs/modules/ROOT/pages/collect-keyless-signing-params.adoc new file mode 100644 index 000000000..bbdab3267 --- /dev/null +++ b/docs/modules/ROOT/pages/collect-keyless-signing-params.adoc @@ -0,0 +1,41 @@ += collect-keyless-signing-params + +Version: 0.1 + +== Synopsis + +Tekton task to collect Konflux configuration parameters related to +keyless signing using cosign. The task attempts to read the "cluster-config" +ConfigMap in the "konflux-info" namespace to extract signing parameters. + +In case the ConfigMap is not found, the task will output empty strings for all parameters, +allowing the pipeline to continue without signing parameters. + + +== Params +[horizontal] + +*configMapName* (`string`):: The name of the ConfigMap to read signing parameters from ++ +*Default*: `cluster-config` +*configMapNamespace* (`string`):: The namespace where the ConfigMap is located ++ +*Default*: `konflux-info` + +== Results + +[horizontal] +*defaultOIDCIssuer*:: A default OIDC issuer URL to be used for signing. + +*rekorExternalUrl*:: The external URL of the Rekor transparency log. + +*fulcioExternalUrl*:: The external URL of the Fulcio certificate authority. + +*tufExternalUrl*:: The external URL of the TUF repository. + +*buildIdentity*:: The build identity from the OIDC token claims, if applicable. + +*buildIdentityRegexp*:: A regular expression to extract build identity from the OIDC token claims, if applicable. + +*keylessSigningEnabled*:: A flag indicating whether keyless signing is enabled based on the presence of signing parameters. + diff --git a/docs/modules/ROOT/partials/tasks_nav.adoc b/docs/modules/ROOT/partials/tasks_nav.adoc index 6ba57f992..77e80365b 100644 --- a/docs/modules/ROOT/partials/tasks_nav.adoc +++ b/docs/modules/ROOT/partials/tasks_nav.adoc @@ -1,4 +1,5 @@ * xref:tasks.adoc[Tekton Tasks] +** xref:collect-keyless-signing-params.adoc[collect-keyless-signing-params] ** xref:verify-conforma-konflux-ta.adoc[verify-conforma-konflux-ta] ** xref:verify-conforma-vsa-release-ta.adoc[verify-conforma-vsa-release-ta] ** xref:verify-enterprise-contract.adoc[verify-enterprise-contract] diff --git a/features/__snapshots__/task_validate_image.snap b/features/__snapshots__/task_validate_image.snap index 68cdf45b6..192e7a358 100755 --- a/features/__snapshots__/task_validate_image.snap +++ b/features/__snapshots__/task_validate_image.snap @@ -552,3 +552,70 @@ results.buildIdentity: https://kubernetes.io/namespaces/openshift-pipelines/serv results.buildIdentityRegexp: ^https://konflux-ci.dev/.*$ --- + +[Collect keyless signing parameters when there is a malformed ConfigMap:collect-signing-params - 1] +Reading ConfigMap konflux-info/cluster-config-3 +ConfigMap found, extracting keyless signing parameters +enableKeylessSigning is not set, using default empty values +results.keylessSigningEnabled: false +results.defaultOIDCIssuer: +results.rekorExternalUrl: +results.fulcioExternalUrl: +results.tufExternalUrl: +results.buildIdentity: +results.buildIdentityRegexp: + +--- + +[Collect keyless signing parameters when the namespace does not exist:collect-signing-params - 1] +Reading ConfigMap doesnt-exist-namespace/whatever +ConfigMap not found, using default empty values +results.keylessSigningEnabled: false +results.defaultOIDCIssuer: +results.rekorExternalUrl: +results.fulcioExternalUrl: +results.tufExternalUrl: +results.buildIdentity: +results.buildIdentityRegexp: + +--- + +[Collect keyless signing parameters when the ConfigMap does not exist:collect-signing-params - 1] +Reading ConfigMap konflux-info/doesnt-exist-config +ConfigMap not found, using default empty values +results.keylessSigningEnabled: false +results.defaultOIDCIssuer: +results.rekorExternalUrl: +results.fulcioExternalUrl: +results.tufExternalUrl: +results.buildIdentity: +results.buildIdentityRegexp: + +--- + +[Collect keyless signing parameters from ConfigMap with keyless signing disabled:collect-signing-params - 1] +Reading ConfigMap konflux-info/cluster-config-2 +ConfigMap found, extracting keyless signing parameters +enableKeylessSigning is not set, using default empty values +results.keylessSigningEnabled: false +results.defaultOIDCIssuer: +results.rekorExternalUrl: +results.fulcioExternalUrl: +results.tufExternalUrl: +results.buildIdentity: +results.buildIdentityRegexp: + +--- + +[Collect keyless signing parameters from ConfigMap:collect-signing-params - 1] +Reading ConfigMap konflux-info/cluster-config +ConfigMap found, extracting keyless signing parameters +results.keylessSigningEnabled: true +results.defaultOIDCIssuer: https://kubernetes.default.svc.cluster.local +results.rekorExternalUrl: https://rekor.example.com +results.fulcioExternalUrl: https://fulcio.example.com +results.tufExternalUrl: https://tuf.example.com +results.buildIdentity: https://kubernetes.io/namespaces/openshift-pipelines/serviceaccounts/tekton-chains-controller +results.buildIdentityRegexp: ^https://konflux-ci.dev/.*$ + +--- diff --git a/features/task_validate_image.feature b/features/task_validate_image.feature index 568f1e9d9..acdce54d2 100644 --- a/features/task_validate_image.feature +++ b/features/task_validate_image.feature @@ -318,3 +318,121 @@ Feature: Verify Enterprise Contract Tekton Tasks Then the task should succeed And the task logs for step "detailed-report" should match the snapshot And the task results should match the snapshot + + Scenario: Collect keyless signing parameters from ConfigMap + Given a working namespace + And a namespace named "konflux-info" exists + # Note: These scenarios might run in parallel so let's use a different config map + # for each scenario so we don't have to worry about them clashing with each other + And a ConfigMap "cluster-config" in namespace "konflux-info" with content: + """ + { + "defaultOIDCIssuer": "https://kubernetes.default.svc.cluster.local", + "rekorExternalUrl": "https://rekor.example.com", + "fulcioExternalUrl": "https://fulcio.example.com", + "tufExternalUrl": "https://tuf.example.com", + "buildIdentity": "https://kubernetes.io/namespaces/openshift-pipelines/serviceaccounts/tekton-chains-controller", + "buildIdentityRegexp": "^https://konflux-ci.dev/.*$", + "enableKeylessSigning": "true" + } + """ + When version 0.1 of the task named "collect-keyless-signing-params" is run with parameters: + | configMapName | cluster-config | + Then the task should succeed + And the task logs for step "collect-signing-params" should match the snapshot + And the task result "defaultOIDCIssuer" should equal "https://kubernetes.default.svc.cluster.local" + And the task result "rekorExternalUrl" should equal "https://rekor.example.com" + And the task result "fulcioExternalUrl" should equal "https://fulcio.example.com" + And the task result "tufExternalUrl" should equal "https://tuf.example.com" + And the task result "buildIdentity" should equal "https://kubernetes.io/namespaces/openshift-pipelines/serviceaccounts/tekton-chains-controller" + And the task result "buildIdentityRegexp" should equal "^https://konflux-ci.dev/.*$" + And the task result "keylessSigningEnabled" should equal "true" + + Scenario: Collect keyless signing parameters from ConfigMap with keyless signing disabled + Given a working namespace + And a namespace named "konflux-info" exists + # Note: These scenarios might run in parallel so let's use a different config map + # for each scenario so we don't have to worry about them clashing with each other + And a ConfigMap "cluster-config-2" in namespace "konflux-info" with content: + """ + { + "defaultOIDCIssuer": "https://kubernetes.default.svc.cluster.local", + "rekorExternalUrl": "https://rekor.example.com", + "fulcioExternalUrl": "https://fulcio.example.com", + "tufExternalUrl": "https://tuf.example.com", + "buildIdentity": "https://kubernetes.io/namespaces/openshift-pipelines/serviceaccounts/tekton-chains-controller", + "buildIdentityRegexp": "^https://konflux-ci.dev/.*$", + "enableKeylessSigning": "false" + } + """ + When version 0.1 of the task named "collect-keyless-signing-params" is run with parameters: + | configMapName | cluster-config-2 | + Then the task should succeed + And the task logs for step "collect-signing-params" should match the snapshot + And the task result "defaultOIDCIssuer" should equal "" + And the task result "rekorExternalUrl" should equal "" + And the task result "fulcioExternalUrl" should equal "" + And the task result "tufExternalUrl" should equal "" + And the task result "buildIdentity" should equal "" + And the task result "buildIdentityRegexp" should equal "" + And the task result "keylessSigningEnabled" should equal "false" + + Scenario: Collect keyless signing parameters when there is a malformed ConfigMap + Given a working namespace + And a namespace named "konflux-info" exists + # Note: These scenarios might run in parallel so let's use a different config map + # for each scenario so we don't have to worry about them clashing with each other + And a ConfigMap "cluster-config-3" in namespace "konflux-info" with content: + """ + {"foo": "bar"} + """ + When version 0.1 of the task named "collect-keyless-signing-params" is run with parameters: + | configMapName | cluster-config-3 | + Then the task should succeed + And the task logs for step "collect-signing-params" should match the snapshot + And the task result "defaultOIDCIssuer" should equal "" + And the task result "rekorExternalUrl" should equal "" + And the task result "fulcioExternalUrl" should equal "" + And the task result "tufExternalUrl" should equal "" + And the task result "buildIdentity" should equal "" + And the task result "buildIdentityRegexp" should equal "" + And the task result "keylessSigningEnabled" should equal "false" + + Scenario: Collect keyless signing parameters when the ConfigMap does not exist + Given a working namespace + And a namespace named "konflux-info" exists + # Note: These scenarios might run in parallel so let's use a different config map + # for each scenario so we don't have to worry about them clashing with each other. + # Creating a config map deliberately so we are sure the rbac is created. (I might + # be wrong but I think it could matter if this secenario runs before any of the + # others.) + And a ConfigMap "cluster-config-4" in namespace "konflux-info" with content: + """ + {"foo": "bar"} + """ + When version 0.1 of the task named "collect-keyless-signing-params" is run with parameters: + | configMapNamespace | konflux-info | + | configMapName | doesnt-exist-config | + Then the task should succeed + And the task logs for step "collect-signing-params" should match the snapshot + And the task result "defaultOIDCIssuer" should equal "" + And the task result "rekorExternalUrl" should equal "" + And the task result "fulcioExternalUrl" should equal "" + And the task result "tufExternalUrl" should equal "" + And the task result "buildIdentityRegexp" should equal "" + And the task result "keylessSigningEnabled" should equal "false" + + Scenario: Collect keyless signing parameters when the namespace does not exist + Given a working namespace + When version 0.1 of the task named "collect-keyless-signing-params" is run with parameters: + | configMapNamespace | doesnt-exist-namespace | + | configMapName | whatever | + Then the task should succeed + And the task logs for step "collect-signing-params" should match the snapshot + And the task result "defaultOIDCIssuer" should equal "" + And the task result "rekorExternalUrl" should equal "" + And the task result "fulcioExternalUrl" should equal "" + And the task result "tufExternalUrl" should equal "" + And the task result "buildIdentity" should equal "" + And the task result "buildIdentityRegexp" should equal "" + And the task result "keylessSigningEnabled" should equal "false" diff --git a/tasks/collect-keyless-signing-params/0.1/collect-keyless-signing-params.yaml b/tasks/collect-keyless-signing-params/0.1/collect-keyless-signing-params.yaml new file mode 100644 index 000000000..5b9f353d9 --- /dev/null +++ b/tasks/collect-keyless-signing-params/0.1/collect-keyless-signing-params.yaml @@ -0,0 +1,167 @@ +--- +# Copyright The Conforma Contributors +# +# 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. +# +# SPDX-License-Identifier: Apache-2.0 + +apiVersion: tekton.dev/v1 +kind: Task +metadata: + name: collect-keyless-signing-params + annotations: + tekton.dev/displayName: Collect Keyless Signing Parameters + tekton.dev/pipelines.minVersion: "0.12.1" + tekton.dev/tags: ec, keyless, signing, configuration + labels: + app.kubernetes.io/version: "0.1" + +spec: + description: | + Tekton task to collect Konflux configuration parameters related to + keyless signing using cosign. The task attempts to read the "cluster-config" + ConfigMap in the "konflux-info" namespace to extract signing parameters. + + In case the ConfigMap is not found, the task will output empty strings for all parameters, + allowing the pipeline to continue without signing parameters. + + params: + - name: configMapName + type: string + description: The name of the ConfigMap to read signing parameters from + default: cluster-config + + - name: configMapNamespace + type: string + description: The namespace where the ConfigMap is located + default: konflux-info + + results: + - name: defaultOIDCIssuer + type: string + description: | + A default OIDC issuer URL to be used for signing. + + - name: rekorExternalUrl + type: string + description: | + The external URL of the Rekor transparency log. + + - name: fulcioExternalUrl + type: string + description: | + The external URL of the Fulcio certificate authority. + + - name: tufExternalUrl + type: string + description: | + The external URL of the TUF repository. + + - name: buildIdentity + type: string + description: | + The build identity from the OIDC token claims, if applicable. + + - name: buildIdentityRegexp + type: string + description: | + A regular expression to extract build identity from the OIDC token claims, if applicable. + + - name: keylessSigningEnabled + type: string + description: | + A flag indicating whether keyless signing is enabled based on the presence of signing parameters. + + stepTemplate: + securityContext: + runAsUser: 1001 + + steps: + - name: collect-signing-params + image: quay.io/conforma/cli:latest + computeResources: + limits: + memory: 128Mi + cpu: 50m + requests: + memory: 128Mi + cpu: 50m + script: | + #!/bin/bash + set -euo pipefail + + # Default value is "false" + keylessSigningEnabled="false" + + # Default values are empty strings + defaultOIDCIssuer="" + rekorExternalUrl="" + fulcioExternalUrl="" + tufExternalUrl="" + buildIdentity="" + buildIdentityRegexp="" + + # Read from the ConfigMap + echo "Reading ConfigMap ${configMapNamespace}/${configMapName}" + KFLX_CONFIG_PATH='/tmp/cluster-config.json' + if kubectl get configmap "${configMapName}" -n "${configMapNamespace}" -o json --ignore-not-found > "${KFLX_CONFIG_PATH}"; then + if [ -s "${KFLX_CONFIG_PATH}" ]; then + echo "ConfigMap found, extracting keyless signing parameters" + + # First we read "keylessSigningEnabled" + keylessSigningEnabled=$(jq -r '.data.enableKeylessSigning // "false"' "$KFLX_CONFIG_PATH") + + if [ "$keylessSigningEnabled" = "true" ]; then + # If that is set to "true" then read the other values + defaultOIDCIssuer=$(jq -r '.data.defaultOIDCIssuer // ""' "$KFLX_CONFIG_PATH") + rekorExternalUrl=$(jq -r '.data.rekorExternalUrl // ""' "$KFLX_CONFIG_PATH") + fulcioExternalUrl=$(jq -r '.data.fulcioExternalUrl // ""' "$KFLX_CONFIG_PATH") + tufExternalUrl=$(jq -r '.data.tufExternalUrl // ""' "$KFLX_CONFIG_PATH") + buildIdentity=$(jq -r '.data.buildIdentity // ""' "$KFLX_CONFIG_PATH") + buildIdentityRegexp=$(jq -r '.data.buildIdentityRegexp // ""' "$KFLX_CONFIG_PATH") + else + # Otherwise we ignore the rest of the ConfigMap + echo "enableKeylessSigning is not set, using default empty values" + fi + else + # Because we used --ignore-not-found this doesn't produce an error + echo "ConfigMap not found, using default empty values" + fi + else + # Some error other than "not found" + # (Stderr from kubectl should be visible in the task log.) + echo "Problem reading ConfigMap, using default empty values" + fi + + # Write to task results + echo -n "$keylessSigningEnabled" > "$(results.keylessSigningEnabled.path)" + echo -n "$defaultOIDCIssuer" > "$(results.defaultOIDCIssuer.path)" + echo -n "$rekorExternalUrl" > "$(results.rekorExternalUrl.path)" + echo -n "$fulcioExternalUrl" > "$(results.fulcioExternalUrl.path)" + echo -n "$tufExternalUrl" > "$(results.tufExternalUrl.path)" + echo -n "$buildIdentity" > "$(results.buildIdentity.path)" + echo -n "$buildIdentityRegexp" > "$(results.buildIdentityRegexp.path)" + + # Output for troubleshooting/debugging + echo "results.keylessSigningEnabled: $keylessSigningEnabled" + echo "results.defaultOIDCIssuer: $defaultOIDCIssuer" + echo "results.rekorExternalUrl: $rekorExternalUrl" + echo "results.fulcioExternalUrl: $fulcioExternalUrl" + echo "results.tufExternalUrl: $tufExternalUrl" + echo "results.buildIdentity: $buildIdentity" + echo "results.buildIdentityRegexp: $buildIdentityRegexp" + env: + - name: configMapNamespace + value: "$(params.configMapNamespace)" + - name: configMapName + value: "$(params.configMapName)"