From a8ce46571f98048e744ca9f333eec7e4564a01cb Mon Sep 17 00:00:00 2001 From: Jawed khelil Date: Thu, 30 Apr 2026 12:03:44 +0200 Subject: [PATCH 1/2] feat(tls): inject centrally managed TLS config into tekton-triggers-webhook Wire the OpenShift APIServer TLS profile (TLS_MIN_VERSION, TLS_CIPHER_SUITES, TLS_CURVE_PREFERENCES) into the tekton-triggers-webhook Deployment to support PQC readiness (SRVKP-9615). Made-with: Cursor --- .../openshift/tektontrigger/extension.go | 50 ++++- .../openshift/tektontrigger/extension_test.go | 197 ++++++++++++++++++ .../shared/tektonconfig/tektonconfig.go | 6 + .../shared/tektonconfig/trigger/trigger.go | 10 + 4 files changed, 255 insertions(+), 8 deletions(-) create mode 100644 pkg/reconciler/openshift/tektontrigger/extension_test.go diff --git a/pkg/reconciler/openshift/tektontrigger/extension.go b/pkg/reconciler/openshift/tektontrigger/extension.go index 5903c162a7..1b53d36c49 100644 --- a/pkg/reconciler/openshift/tektontrigger/extension.go +++ b/pkg/reconciler/openshift/tektontrigger/extension.go @@ -21,12 +21,19 @@ import ( mf "github.com/manifestival/manifestival" "github.com/tektoncd/operator/pkg/apis/operator/v1alpha1" + tektonConfiginformer "github.com/tektoncd/operator/pkg/client/injection/informers/operator/v1alpha1/tektonconfig" "github.com/tektoncd/operator/pkg/reconciler/common" "github.com/tektoncd/operator/pkg/reconciler/kubernetes/tektontrigger" occommon "github.com/tektoncd/operator/pkg/reconciler/openshift/common" + "knative.dev/pkg/logging" "knative.dev/pkg/ptr" ) +const ( + tektonTriggersWebhookDeployment = "tekton-triggers-webhook" + webhookContainerName = "webhook" +) + // triggersProperties holds fields for configuring runAsUser and runAsGroup. type triggersProperties struct { DefaultRunAsUser *string `json:"default-run-as-user,omitempty"` @@ -44,30 +51,57 @@ var triggersData = triggersProperties{ } func OpenShiftExtension(ctx context.Context) common.Extension { - return openshiftExtension{} + return &openshiftExtension{ + tektonConfigLister: tektonConfiginformer.Get(ctx).Lister(), + } } -type openshiftExtension struct{} +type openshiftExtension struct { + tektonConfigLister occommon.TektonConfigLister + resolvedTLSConfig *occommon.TLSEnvVars +} -func (oe openshiftExtension) Transformers(comp v1alpha1.TektonComponent) []mf.Transformer { - return []mf.Transformer{ +func (oe *openshiftExtension) Transformers(comp v1alpha1.TektonComponent) []mf.Transformer { + trns := []mf.Transformer{ occommon.RemoveRunAsUser(), occommon.RemoveRunAsGroup(), occommon.ApplyCABundlesToDeployment, common.AddConfigMapValues(tektontrigger.ConfigDefaults, triggersData), replaceDeploymentArgs("-el-events", "enable"), } + + // Inject APIServer TLS profile env vars into the webhook so that it applies + // the cluster-wide TLS version and cipher suite policy (PQC readiness). + if oe.resolvedTLSConfig != nil { + trns = append(trns, occommon.InjectTLSEnvVars(oe.resolvedTLSConfig, "Deployment", tektonTriggersWebhookDeployment, []string{webhookContainerName})) + } + + return trns } -func (oe openshiftExtension) PreReconcile(ctx context.Context, tc v1alpha1.TektonComponent) error { + +func (oe *openshiftExtension) PreReconcile(ctx context.Context, tc v1alpha1.TektonComponent) error { + logger := logging.FromContext(ctx) + + resolvedTLS, err := occommon.ResolveCentralTLSToEnvVars(ctx, oe.tektonConfigLister) + if err != nil { + return err + } + oe.resolvedTLSConfig = resolvedTLS + if oe.resolvedTLSConfig != nil { + logger.Infof("Injecting central TLS config into triggers webhook: MinVersion=%s", oe.resolvedTLSConfig.MinVersion) + } + return nil } -func (oe openshiftExtension) PostReconcile(context.Context, v1alpha1.TektonComponent) error { + +func (oe *openshiftExtension) PostReconcile(context.Context, v1alpha1.TektonComponent) error { return nil } -func (oe openshiftExtension) Finalize(context.Context, v1alpha1.TektonComponent) error { + +func (oe *openshiftExtension) Finalize(context.Context, v1alpha1.TektonComponent) error { return nil } -func (oe openshiftExtension) GetPlatformData() string { +func (oe *openshiftExtension) GetPlatformData() string { return "" } diff --git a/pkg/reconciler/openshift/tektontrigger/extension_test.go b/pkg/reconciler/openshift/tektontrigger/extension_test.go new file mode 100644 index 0000000000..6547693a80 --- /dev/null +++ b/pkg/reconciler/openshift/tektontrigger/extension_test.go @@ -0,0 +1,197 @@ +/* +Copyright 2026 The Tekton 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. +*/ + +package tektontrigger + +import ( + "testing" + + mf "github.com/manifestival/manifestival" + "github.com/tektoncd/operator/pkg/apis/operator/v1alpha1" + occommon "github.com/tektoncd/operator/pkg/reconciler/openshift/common" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" +) + +// makeTriggersWebhookDeployment returns an unstructured triggers webhook Deployment for transformer tests. +func makeTriggersWebhookDeployment(t *testing.T) unstructured.Unstructured { + t.Helper() + + d := &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: tektonTriggersWebhookDeployment, + Namespace: "openshift-pipelines", + }, + Spec: appsv1.DeploymentSpec{ + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + {Name: webhookContainerName}, + }, + }, + }, + }, + } + obj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(d) + if err != nil { + t.Fatalf("failed to convert deployment to unstructured: %v", err) + } + u := unstructured.Unstructured{Object: obj} + u.SetKind("Deployment") + u.SetAPIVersion("apps/v1") + return u +} + +func TestTriggersTransformers_NoTLSConfig(t *testing.T) { + ext := &openshiftExtension{ + resolvedTLSConfig: nil, + } + + transformers := ext.Transformers(&v1alpha1.TektonTrigger{}) + + u := makeTriggersWebhookDeployment(t) + manifest, err := mf.ManifestFrom(mf.Slice([]unstructured.Unstructured{u})) + if err != nil { + t.Fatalf("failed to build manifest: %v", err) + } + + transformed, err := manifest.Transform(transformers...) + if err != nil { + t.Fatalf("transform failed: %v", err) + } + + d := &appsv1.Deployment{} + if err := runtime.DefaultUnstructuredConverter.FromUnstructured(transformed.Resources()[0].Object, d); err != nil { + t.Fatalf("failed to convert back: %v", err) + } + for _, c := range d.Spec.Template.Spec.Containers { + if c.Name != webhookContainerName { + continue + } + for _, e := range c.Env { + if e.Name == occommon.TLSMinVersionEnvVar || e.Name == occommon.TLSCipherSuitesEnvVar { + t.Errorf("unexpected TLS env var %s set when resolvedTLSConfig is nil", e.Name) + } + } + } +} + +func TestTriggersTransformers_WithTLSConfig_InjectsEnvVarsIntoWebhook(t *testing.T) { + tlsConfig := &occommon.TLSEnvVars{ + MinVersion: "1.2", + CipherSuites: "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,TLS_AES_128_GCM_SHA256", + } + ext := &openshiftExtension{ + resolvedTLSConfig: tlsConfig, + } + + transformers := ext.Transformers(&v1alpha1.TektonTrigger{}) + + u := makeTriggersWebhookDeployment(t) + manifest, err := mf.ManifestFrom(mf.Slice([]unstructured.Unstructured{u})) + if err != nil { + t.Fatalf("failed to build manifest: %v", err) + } + + transformed, err := manifest.Transform(transformers...) + if err != nil { + t.Fatalf("transform failed: %v", err) + } + + d := &appsv1.Deployment{} + if err := runtime.DefaultUnstructuredConverter.FromUnstructured(transformed.Resources()[0].Object, d); err != nil { + t.Fatalf("failed to convert back: %v", err) + } + + envMap := map[string]string{} + for _, c := range d.Spec.Template.Spec.Containers { + if c.Name != webhookContainerName { + continue + } + for _, e := range c.Env { + envMap[e.Name] = e.Value + } + } + + if got := envMap[occommon.TLSMinVersionEnvVar]; got != tlsConfig.MinVersion { + t.Errorf("%s = %q, want %q", occommon.TLSMinVersionEnvVar, got, tlsConfig.MinVersion) + } + if got := envMap[occommon.TLSCipherSuitesEnvVar]; got != tlsConfig.CipherSuites { + t.Errorf("%s = %q, want %q", occommon.TLSCipherSuitesEnvVar, got, tlsConfig.CipherSuites) + } +} + +func TestTriggersTransformers_WithTLSConfig_DoesNotInjectIntoOtherDeployments(t *testing.T) { + tlsConfig := &occommon.TLSEnvVars{ + MinVersion: "1.3", + CipherSuites: "TLS_AES_128_GCM_SHA256", + } + ext := &openshiftExtension{ + resolvedTLSConfig: tlsConfig, + } + + transformers := ext.Transformers(&v1alpha1.TektonTrigger{}) + + // Use a different deployment name — TLS env vars must NOT be injected. + d := &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: "tekton-triggers-controller", + Namespace: "openshift-pipelines", + }, + Spec: appsv1.DeploymentSpec{ + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + {Name: "controller"}, + }, + }, + }, + }, + } + obj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(d) + if err != nil { + t.Fatalf("failed to convert: %v", err) + } + u := unstructured.Unstructured{Object: obj} + u.SetKind("Deployment") + u.SetAPIVersion("apps/v1") + + manifest, err := mf.ManifestFrom(mf.Slice([]unstructured.Unstructured{u})) + if err != nil { + t.Fatalf("failed to build manifest: %v", err) + } + + transformed, err := manifest.Transform(transformers...) + if err != nil { + t.Fatalf("transform failed: %v", err) + } + + result := &appsv1.Deployment{} + if err := runtime.DefaultUnstructuredConverter.FromUnstructured(transformed.Resources()[0].Object, result); err != nil { + t.Fatalf("failed to convert back: %v", err) + } + + for _, c := range result.Spec.Template.Spec.Containers { + for _, e := range c.Env { + if e.Name == occommon.TLSMinVersionEnvVar || e.Name == occommon.TLSCipherSuitesEnvVar { + t.Errorf("unexpected TLS env var %s injected into non-webhook deployment", e.Name) + } + } + } +} diff --git a/pkg/reconciler/shared/tektonconfig/tektonconfig.go b/pkg/reconciler/shared/tektonconfig/tektonconfig.go index 4189669c99..b9550856c1 100644 --- a/pkg/reconciler/shared/tektonconfig/tektonconfig.go +++ b/pkg/reconciler/shared/tektonconfig/tektonconfig.go @@ -260,6 +260,12 @@ func (r *Reconciler) ReconcileKind(ctx context.Context, tc *v1alpha1.TektonConfi // Ensure Pipeline Trigger if !tc.Spec.Trigger.Disabled && (tc.Spec.Profile == v1alpha1.ProfileAll || tc.Spec.Profile == v1alpha1.ProfileBasic) { tektontrigger := trigger.GetTektonTriggerCR(tc, r.operatorVersion) + if platformData := r.extension.GetPlatformData(); platformData != "" { + if tektontrigger.Annotations == nil { + tektontrigger.Annotations = map[string]string{} + } + tektontrigger.Annotations[v1alpha1.PlatformDataHashKey] = platformData + } logger.Debug("Ensuring TektonTrigger CR exists") if _, err := trigger.EnsureTektonTriggerExists(ctx, r.operatorClientSet.OperatorV1alpha1().TektonTriggers(), tektontrigger); err != nil { errMsg := fmt.Sprintf("TektonTrigger: %s", err.Error()) diff --git a/pkg/reconciler/shared/tektonconfig/trigger/trigger.go b/pkg/reconciler/shared/tektonconfig/trigger/trigger.go index 252fe58865..01525ddb1a 100644 --- a/pkg/reconciler/shared/tektonconfig/trigger/trigger.go +++ b/pkg/reconciler/shared/tektonconfig/trigger/trigger.go @@ -127,6 +127,16 @@ func UpdateTrigger(ctx context.Context, old *v1alpha1.TektonTrigger, new *v1alph updated = true } + oldPlatformData := old.ObjectMeta.Annotations[v1alpha1.PlatformDataHashKey] + newPlatformData := new.ObjectMeta.Annotations[v1alpha1.PlatformDataHashKey] + if oldPlatformData != newPlatformData { + if old.ObjectMeta.Annotations == nil { + old.ObjectMeta.Annotations = map[string]string{} + } + old.ObjectMeta.Annotations[v1alpha1.PlatformDataHashKey] = newPlatformData + updated = true + } + if updated { _, err := clients.Update(ctx, old, metav1.UpdateOptions{}) if err != nil { From 8f5ab37888ec2eda2f90b92f96a24c5221807b40 Mon Sep 17 00:00:00 2001 From: Jawed khelil Date: Mon, 11 May 2026 11:43:49 +0200 Subject: [PATCH 2/2] feat(tls): inject TLS env vars into triggers core interceptors Extend the OpenShift TLS profile injection to also cover the tekton-triggers-core-interceptors Deployment, in addition to the existing tekton-triggers-webhook target. Both deployments now receive TLS_MIN_VERSION, TLS_CIPHER_SUITES, and TLS_CURVE_PREFERENCES from the centrally resolved APIServer TLS profile, ensuring cluster-wide PQC-readiness policy is enforced consistently across all Triggers components (SRVKP-9615). Container name confirmed from the live cluster: tekton-triggers-core-interceptors (openshift-pipelines namespace) Signed-off-by: Jawed khelil Assisted-by: Claude Sonnet 4.6 (via Cursor) Co-authored-by: Cursor --- .../openshift/tektontrigger/extension.go | 13 ++- .../openshift/tektontrigger/extension_test.go | 94 ++++++++++++++++++- 2 files changed, 99 insertions(+), 8 deletions(-) diff --git a/pkg/reconciler/openshift/tektontrigger/extension.go b/pkg/reconciler/openshift/tektontrigger/extension.go index 1b53d36c49..9d397ba510 100644 --- a/pkg/reconciler/openshift/tektontrigger/extension.go +++ b/pkg/reconciler/openshift/tektontrigger/extension.go @@ -32,6 +32,8 @@ import ( const ( tektonTriggersWebhookDeployment = "tekton-triggers-webhook" webhookContainerName = "webhook" + tektonTriggersCoreInterceptors = "tekton-triggers-core-interceptors" + coreInterceptorsContainerName = "tekton-triggers-core-interceptors" ) // triggersProperties holds fields for configuring runAsUser and runAsGroup. @@ -70,10 +72,13 @@ func (oe *openshiftExtension) Transformers(comp v1alpha1.TektonComponent) []mf.T replaceDeploymentArgs("-el-events", "enable"), } - // Inject APIServer TLS profile env vars into the webhook so that it applies - // the cluster-wide TLS version and cipher suite policy (PQC readiness). + // Inject APIServer TLS profile env vars into the webhook and core interceptors + // so that both apply the cluster-wide TLS version and cipher suite policy (PQC readiness). if oe.resolvedTLSConfig != nil { - trns = append(trns, occommon.InjectTLSEnvVars(oe.resolvedTLSConfig, "Deployment", tektonTriggersWebhookDeployment, []string{webhookContainerName})) + trns = append(trns, + occommon.InjectTLSEnvVars(oe.resolvedTLSConfig, "Deployment", tektonTriggersWebhookDeployment, []string{webhookContainerName}), + occommon.InjectTLSEnvVars(oe.resolvedTLSConfig, "Deployment", tektonTriggersCoreInterceptors, []string{coreInterceptorsContainerName}), + ) } return trns @@ -88,7 +93,7 @@ func (oe *openshiftExtension) PreReconcile(ctx context.Context, tc v1alpha1.Tekt } oe.resolvedTLSConfig = resolvedTLS if oe.resolvedTLSConfig != nil { - logger.Infof("Injecting central TLS config into triggers webhook: MinVersion=%s", oe.resolvedTLSConfig.MinVersion) + logger.Infof("Injecting central TLS config into triggers webhook and core interceptors: MinVersion=%s", oe.resolvedTLSConfig.MinVersion) } return nil diff --git a/pkg/reconciler/openshift/tektontrigger/extension_test.go b/pkg/reconciler/openshift/tektontrigger/extension_test.go index 6547693a80..ec06c590ae 100644 --- a/pkg/reconciler/openshift/tektontrigger/extension_test.go +++ b/pkg/reconciler/openshift/tektontrigger/extension_test.go @@ -29,20 +29,20 @@ import ( "k8s.io/apimachinery/pkg/runtime" ) -// makeTriggersWebhookDeployment returns an unstructured triggers webhook Deployment for transformer tests. -func makeTriggersWebhookDeployment(t *testing.T) unstructured.Unstructured { +// makeDeployment returns an unstructured Deployment with the given name and container name. +func makeDeployment(t *testing.T, deploymentName, containerName string) unstructured.Unstructured { t.Helper() d := &appsv1.Deployment{ ObjectMeta: metav1.ObjectMeta{ - Name: tektonTriggersWebhookDeployment, + Name: deploymentName, Namespace: "openshift-pipelines", }, Spec: appsv1.DeploymentSpec{ Template: corev1.PodTemplateSpec{ Spec: corev1.PodSpec{ Containers: []corev1.Container{ - {Name: webhookContainerName}, + {Name: containerName}, }, }, }, @@ -58,6 +58,16 @@ func makeTriggersWebhookDeployment(t *testing.T) unstructured.Unstructured { return u } +// makeTriggersWebhookDeployment returns an unstructured triggers webhook Deployment for transformer tests. +func makeTriggersWebhookDeployment(t *testing.T) unstructured.Unstructured { + return makeDeployment(t, tektonTriggersWebhookDeployment, webhookContainerName) +} + +// makeCoreInterceptorsDeployment returns an unstructured core interceptors Deployment for transformer tests. +func makeCoreInterceptorsDeployment(t *testing.T) unstructured.Unstructured { + return makeDeployment(t, tektonTriggersCoreInterceptors, coreInterceptorsContainerName) +} + func TestTriggersTransformers_NoTLSConfig(t *testing.T) { ext := &openshiftExtension{ resolvedTLSConfig: nil, @@ -137,6 +147,82 @@ func TestTriggersTransformers_WithTLSConfig_InjectsEnvVarsIntoWebhook(t *testing } } +func TestTriggersTransformers_WithTLSConfig_InjectsEnvVarsIntoCoreInterceptors(t *testing.T) { + tlsConfig := &occommon.TLSEnvVars{ + MinVersion: "1.2", + CipherSuites: "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,TLS_AES_128_GCM_SHA256", + } + ext := &openshiftExtension{ + resolvedTLSConfig: tlsConfig, + } + + transformers := ext.Transformers(&v1alpha1.TektonTrigger{}) + + u := makeCoreInterceptorsDeployment(t) + manifest, err := mf.ManifestFrom(mf.Slice([]unstructured.Unstructured{u})) + if err != nil { + t.Fatalf("failed to build manifest: %v", err) + } + + transformed, err := manifest.Transform(transformers...) + if err != nil { + t.Fatalf("transform failed: %v", err) + } + + d := &appsv1.Deployment{} + if err := runtime.DefaultUnstructuredConverter.FromUnstructured(transformed.Resources()[0].Object, d); err != nil { + t.Fatalf("failed to convert back: %v", err) + } + + envMap := map[string]string{} + for _, c := range d.Spec.Template.Spec.Containers { + if c.Name != coreInterceptorsContainerName { + continue + } + for _, e := range c.Env { + envMap[e.Name] = e.Value + } + } + + if got := envMap[occommon.TLSMinVersionEnvVar]; got != tlsConfig.MinVersion { + t.Errorf("%s = %q, want %q", occommon.TLSMinVersionEnvVar, got, tlsConfig.MinVersion) + } + if got := envMap[occommon.TLSCipherSuitesEnvVar]; got != tlsConfig.CipherSuites { + t.Errorf("%s = %q, want %q", occommon.TLSCipherSuitesEnvVar, got, tlsConfig.CipherSuites) + } +} + +func TestTriggersTransformers_NoTLSConfig_DoesNotInjectIntoCoreInterceptors(t *testing.T) { + ext := &openshiftExtension{ + resolvedTLSConfig: nil, + } + + transformers := ext.Transformers(&v1alpha1.TektonTrigger{}) + + u := makeCoreInterceptorsDeployment(t) + manifest, err := mf.ManifestFrom(mf.Slice([]unstructured.Unstructured{u})) + if err != nil { + t.Fatalf("failed to build manifest: %v", err) + } + + transformed, err := manifest.Transform(transformers...) + if err != nil { + t.Fatalf("transform failed: %v", err) + } + + d := &appsv1.Deployment{} + if err := runtime.DefaultUnstructuredConverter.FromUnstructured(transformed.Resources()[0].Object, d); err != nil { + t.Fatalf("failed to convert back: %v", err) + } + for _, c := range d.Spec.Template.Spec.Containers { + for _, e := range c.Env { + if e.Name == occommon.TLSMinVersionEnvVar || e.Name == occommon.TLSCipherSuitesEnvVar { + t.Errorf("unexpected TLS env var %s set when resolvedTLSConfig is nil", e.Name) + } + } + } +} + func TestTriggersTransformers_WithTLSConfig_DoesNotInjectIntoOtherDeployments(t *testing.T) { tlsConfig := &occommon.TLSEnvVars{ MinVersion: "1.3",