Skip to content

Commit 66d3814

Browse files
committed
feat(tls): inject centrally managed TLS config into tekton-pipelines-webhook
Wire the OpenShift APIServer TLS profile (TLS_MIN_VERSION, TLS_CIPHER_SUITES, TLS_CURVE_PREFERENCES) into the tekton-pipelines-webhook Deployment to support PQC readiness (SRVKP-9614). Made-with: Cursor
1 parent e4eaabd commit 66d3814

9 files changed

Lines changed: 266 additions & 12 deletions

File tree

pkg/reconciler/kubernetes/tektoninstallerset/client/check.go

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ func verifyMeta(resourceKind, isType string, logger *zap.SugaredLogger, set v1al
135135
// Spec Hash Check
136136
logger.Debugf("%v/%v: spec hash check", resourceKind, isType)
137137

138-
expectedHash, err := hash.Compute(comp.GetSpec())
138+
expectedHash, err := hash.Compute(specHashInput(comp))
139139
if err != nil {
140140
return err
141141
}
@@ -149,3 +149,17 @@ func verifyMeta(resourceKind, isType string, logger *zap.SugaredLogger, set v1al
149149

150150
return nil
151151
}
152+
153+
// specHashInput is the canonical input for computing an InstallerSet's spec hash.
154+
// Including PlatformDataHashKey means that operator-injected platform config
155+
// (e.g. OpenShift APIServer TLS profile) causes InstallerSets to be refreshed
156+
// when that config changes, without requiring the component's spec to change.
157+
func specHashInput(comp v1alpha1.TektonComponent) interface{} {
158+
return struct {
159+
Spec interface{}
160+
PlatformData string
161+
}{
162+
Spec: comp.GetSpec(),
163+
PlatformData: comp.GetAnnotations()[v1alpha1.PlatformDataHashKey],
164+
}
165+
}

pkg/reconciler/kubernetes/tektoninstallerset/client/check_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ func buildTriggerComponent(disabled bool) *v1alpha1.TektonTrigger {
4949
}
5050

5151
func computeHash(comp *v1alpha1.TektonTrigger) string {
52-
h, err := hash.Compute(comp.GetSpec())
52+
h, err := hash.Compute(specHashInput(comp))
5353
if err != nil {
5454
panic("failed to compute hash: " + err.Error())
5555
}

pkg/reconciler/kubernetes/tektoninstallerset/client/create.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ func (i *InstallerSetClient) waitForStatus(ctx context.Context, set *v1alpha1.Te
131131
}
132132

133133
func (i *InstallerSetClient) makeInstallerSet(ctx context.Context, comp v1alpha1.TektonComponent, manifest *mf.Manifest, isName, isType string, customLabels map[string]string) (*v1alpha1.TektonInstallerSet, error) {
134-
specHash, err := hash.Compute(comp.GetSpec())
134+
specHash, err := hash.Compute(specHashInput(comp))
135135
if err != nil {
136136
return nil, err
137137
}

pkg/reconciler/kubernetes/tektoninstallerset/client/update.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ func (i *InstallerSetClient) updateSet(ctx context.Context, comp v1alpha1.Tekton
8888
return err
8989
}
9090

91-
specHash, err := hash.Compute(comp.GetSpec())
91+
specHash, err := hash.Compute(specHashInput(comp))
9292
if err != nil {
9393
return err
9494
}

pkg/reconciler/kubernetes/tektoninstallerset/client/update_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ func TestInstallerSetClient_Update(t *testing.T) {
4141
},
4242
}
4343

44-
expectedHash, err := hash.Compute(comp.GetSpec())
44+
expectedHash, err := hash.Compute(specHashInput(comp))
4545
assert.NilError(t, err)
4646

4747
tests := []struct {

pkg/reconciler/openshift/tektonpipeline/extension.go

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import (
2525
mf "github.com/manifestival/manifestival"
2626
"github.com/tektoncd/operator/pkg/apis/operator/v1alpha1"
2727
operatorclient "github.com/tektoncd/operator/pkg/client/injection/client"
28+
tektonConfiginformer "github.com/tektoncd/operator/pkg/client/injection/informers/operator/v1alpha1/tektonconfig"
2829
"github.com/tektoncd/operator/pkg/reconciler/common"
2930
"github.com/tektoncd/operator/pkg/reconciler/kubernetes/tektoninstallerset/client"
3031
occommon "github.com/tektoncd/operator/pkg/reconciler/openshift/common"
@@ -40,6 +41,8 @@ const (
4041

4142
tektonPipelinesControllerName = "tekton-pipelines-controller"
4243
tektonRemoteResolversControllerName = "tekton-pipelines-remote-resolvers"
44+
tektonPipelinesWebhookDeployment = "tekton-pipelines-webhook"
45+
webhookContainerName = "webhook"
4346
)
4447

4548
func OpenShiftExtension(ctx context.Context) common.Extension {
@@ -49,22 +52,25 @@ func OpenShiftExtension(ctx context.Context) common.Extension {
4952
logger.Fatal("Failed to find version from env")
5053
}
5154

52-
ext := openshiftExtension{
55+
ext := &openshiftExtension{
5356
// component version is used for metrics, passing a dummy
5457
// value through extension not going to affect execution
5558
installerSetClient: client.NewInstallerSetClient(operatorclient.Get(ctx).OperatorV1alpha1().TektonInstallerSets(),
5659
version, "pipelines-ext", v1alpha1.KindTektonPipeline, nil),
57-
kubeClientSet: kubeclient.Get(ctx),
60+
kubeClientSet: kubeclient.Get(ctx),
61+
tektonConfigLister: tektonConfiginformer.Get(ctx).Lister(),
5862
}
5963
return ext
6064
}
6165

6266
type openshiftExtension struct {
6367
installerSetClient *client.InstallerSetClient
6468
kubeClientSet kubernetes.Interface
69+
tektonConfigLister occommon.TektonConfigLister
70+
resolvedTLSConfig *occommon.TLSEnvVars
6571
}
6672

67-
func (oe openshiftExtension) Transformers(comp v1alpha1.TektonComponent) []mf.Transformer {
73+
func (oe *openshiftExtension) Transformers(comp v1alpha1.TektonComponent) []mf.Transformer {
6874
trns := []mf.Transformer{
6975
occommon.ApplyCABundlesToDeployment,
7076
occommon.RemoveRunAsUser(),
@@ -73,9 +79,28 @@ func (oe openshiftExtension) Transformers(comp v1alpha1.TektonComponent) []mf.Tr
7379
occommon.ApplyCABundlesForStatefulSet(tektonPipelinesControllerName),
7480
occommon.ApplyCABundlesForStatefulSet(tektonRemoteResolversControllerName),
7581
}
82+
83+
// Inject APIServer TLS profile env vars into the webhook so that it applies
84+
// the cluster-wide TLS version and cipher suite policy (PQC readiness).
85+
if oe.resolvedTLSConfig != nil {
86+
trns = append(trns, occommon.InjectTLSEnvVars(oe.resolvedTLSConfig, "Deployment", tektonPipelinesWebhookDeployment, []string{webhookContainerName}))
87+
}
88+
7689
return trns
7790
}
78-
func (oe openshiftExtension) PreReconcile(ctx context.Context, comp v1alpha1.TektonComponent) error {
91+
92+
func (oe *openshiftExtension) PreReconcile(ctx context.Context, comp v1alpha1.TektonComponent) error {
93+
logger := logging.FromContext(ctx)
94+
95+
resolvedTLS, err := occommon.ResolveCentralTLSToEnvVars(ctx, oe.tektonConfigLister)
96+
if err != nil {
97+
return err
98+
}
99+
oe.resolvedTLSConfig = resolvedTLS
100+
if oe.resolvedTLSConfig != nil {
101+
logger.Infof("Injecting central TLS config into webhook: MinVersion=%s", oe.resolvedTLSConfig.MinVersion)
102+
}
103+
79104
manifest, err := preManifest()
80105
if err != nil {
81106
return err
@@ -102,7 +127,7 @@ func (oe openshiftExtension) PreReconcile(ctx context.Context, comp v1alpha1.Tek
102127
return common.ReconcileTargetNamespace(ctx, labels, nil, comp, oe.kubeClientSet)
103128
}
104129

105-
func (oe openshiftExtension) PostReconcile(ctx context.Context, comp v1alpha1.TektonComponent) error {
130+
func (oe *openshiftExtension) PostReconcile(ctx context.Context, comp v1alpha1.TektonComponent) error {
106131
pipeline := comp.(*v1alpha1.TektonPipeline)
107132

108133
// Install monitoring if metrics is enabled
@@ -124,7 +149,7 @@ func (oe openshiftExtension) PostReconcile(ctx context.Context, comp v1alpha1.Te
124149

125150
return nil
126151
}
127-
func (oe openshiftExtension) Finalize(ctx context.Context, comp v1alpha1.TektonComponent) error {
152+
func (oe *openshiftExtension) Finalize(ctx context.Context, comp v1alpha1.TektonComponent) error {
128153
if err := oe.installerSetClient.CleanupPostSet(ctx); err != nil {
129154
return err
130155
}
@@ -134,7 +159,7 @@ func (oe openshiftExtension) Finalize(ctx context.Context, comp v1alpha1.TektonC
134159
return nil
135160
}
136161

137-
func (oe openshiftExtension) GetPlatformData() string {
162+
func (oe *openshiftExtension) GetPlatformData() string {
138163
return ""
139164
}
140165

Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
/*
2+
Copyright 2026 The Tekton Authors
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package tektonpipeline
18+
19+
import (
20+
"testing"
21+
22+
mf "github.com/manifestival/manifestival"
23+
"github.com/tektoncd/operator/pkg/apis/operator/v1alpha1"
24+
occommon "github.com/tektoncd/operator/pkg/reconciler/openshift/common"
25+
appsv1 "k8s.io/api/apps/v1"
26+
corev1 "k8s.io/api/core/v1"
27+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
28+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
29+
"k8s.io/apimachinery/pkg/runtime"
30+
)
31+
32+
// makeWebhookDeployment returns an unstructured webhook Deployment for transformer tests.
33+
func makeWebhookDeployment(t *testing.T) unstructured.Unstructured {
34+
t.Helper()
35+
36+
d := &appsv1.Deployment{
37+
ObjectMeta: metav1.ObjectMeta{
38+
Name: tektonPipelinesWebhookDeployment,
39+
Namespace: "tekton-pipelines",
40+
},
41+
Spec: appsv1.DeploymentSpec{
42+
Template: corev1.PodTemplateSpec{
43+
Spec: corev1.PodSpec{
44+
Containers: []corev1.Container{
45+
{Name: webhookContainerName},
46+
},
47+
},
48+
},
49+
},
50+
}
51+
obj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(d)
52+
if err != nil {
53+
t.Fatalf("failed to convert deployment to unstructured: %v", err)
54+
}
55+
u := unstructured.Unstructured{Object: obj}
56+
u.SetKind("Deployment")
57+
u.SetAPIVersion("apps/v1")
58+
return u
59+
}
60+
61+
func TestTransformers_NoTLSConfig(t *testing.T) {
62+
ext := &openshiftExtension{
63+
resolvedTLSConfig: nil,
64+
}
65+
66+
transformers := ext.Transformers(&v1alpha1.TektonPipeline{})
67+
68+
// Without TLS config, the base transformers are returned but TLS injection is not present.
69+
// Verify by applying transformers to a webhook deployment — env vars must remain empty.
70+
u := makeWebhookDeployment(t)
71+
manifest, err := mf.ManifestFrom(mf.Slice([]unstructured.Unstructured{u}))
72+
if err != nil {
73+
t.Fatalf("failed to build manifest: %v", err)
74+
}
75+
76+
transformed, err := manifest.Transform(transformers...)
77+
if err != nil {
78+
t.Fatalf("transform failed: %v", err)
79+
}
80+
81+
d := &appsv1.Deployment{}
82+
if err := runtime.DefaultUnstructuredConverter.FromUnstructured(transformed.Resources()[0].Object, d); err != nil {
83+
t.Fatalf("failed to convert back: %v", err)
84+
}
85+
for _, c := range d.Spec.Template.Spec.Containers {
86+
if c.Name != webhookContainerName {
87+
continue
88+
}
89+
for _, e := range c.Env {
90+
if e.Name == occommon.TLSMinVersionEnvVar || e.Name == occommon.TLSCipherSuitesEnvVar {
91+
t.Errorf("unexpected TLS env var %s set when resolvedTLSConfig is nil", e.Name)
92+
}
93+
}
94+
}
95+
}
96+
97+
func TestTransformers_WithTLSConfig_InjectsEnvVarsIntoWebhook(t *testing.T) {
98+
tlsConfig := &occommon.TLSEnvVars{
99+
MinVersion: "1.2",
100+
CipherSuites: "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,TLS_AES_128_GCM_SHA256",
101+
}
102+
ext := &openshiftExtension{
103+
resolvedTLSConfig: tlsConfig,
104+
}
105+
106+
transformers := ext.Transformers(&v1alpha1.TektonPipeline{})
107+
108+
u := makeWebhookDeployment(t)
109+
manifest, err := mf.ManifestFrom(mf.Slice([]unstructured.Unstructured{u}))
110+
if err != nil {
111+
t.Fatalf("failed to build manifest: %v", err)
112+
}
113+
114+
transformed, err := manifest.Transform(transformers...)
115+
if err != nil {
116+
t.Fatalf("transform failed: %v", err)
117+
}
118+
119+
d := &appsv1.Deployment{}
120+
if err := runtime.DefaultUnstructuredConverter.FromUnstructured(transformed.Resources()[0].Object, d); err != nil {
121+
t.Fatalf("failed to convert back: %v", err)
122+
}
123+
124+
envMap := map[string]string{}
125+
for _, c := range d.Spec.Template.Spec.Containers {
126+
if c.Name != webhookContainerName {
127+
continue
128+
}
129+
for _, e := range c.Env {
130+
envMap[e.Name] = e.Value
131+
}
132+
}
133+
134+
if got := envMap[occommon.TLSMinVersionEnvVar]; got != tlsConfig.MinVersion {
135+
t.Errorf("%s = %q, want %q", occommon.TLSMinVersionEnvVar, got, tlsConfig.MinVersion)
136+
}
137+
if got := envMap[occommon.TLSCipherSuitesEnvVar]; got != tlsConfig.CipherSuites {
138+
t.Errorf("%s = %q, want %q", occommon.TLSCipherSuitesEnvVar, got, tlsConfig.CipherSuites)
139+
}
140+
}
141+
142+
func TestTransformers_WithTLSConfig_DoesNotInjectIntoOtherDeployments(t *testing.T) {
143+
tlsConfig := &occommon.TLSEnvVars{
144+
MinVersion: "1.3",
145+
CipherSuites: "TLS_AES_128_GCM_SHA256",
146+
}
147+
ext := &openshiftExtension{
148+
resolvedTLSConfig: tlsConfig,
149+
}
150+
151+
transformers := ext.Transformers(&v1alpha1.TektonPipeline{})
152+
153+
// Use a different deployment name — TLS env vars must NOT be injected.
154+
d := &appsv1.Deployment{
155+
ObjectMeta: metav1.ObjectMeta{
156+
Name: tektonPipelinesControllerName,
157+
Namespace: "tekton-pipelines",
158+
},
159+
Spec: appsv1.DeploymentSpec{
160+
Template: corev1.PodTemplateSpec{
161+
Spec: corev1.PodSpec{
162+
Containers: []corev1.Container{
163+
{Name: "controller"},
164+
},
165+
},
166+
},
167+
},
168+
}
169+
obj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(d)
170+
if err != nil {
171+
t.Fatalf("failed to convert: %v", err)
172+
}
173+
u := unstructured.Unstructured{Object: obj}
174+
u.SetKind("Deployment")
175+
u.SetAPIVersion("apps/v1")
176+
177+
manifest, err := mf.ManifestFrom(mf.Slice([]unstructured.Unstructured{u}))
178+
if err != nil {
179+
t.Fatalf("failed to build manifest: %v", err)
180+
}
181+
182+
transformed, err := manifest.Transform(transformers...)
183+
if err != nil {
184+
t.Fatalf("transform failed: %v", err)
185+
}
186+
187+
result := &appsv1.Deployment{}
188+
if err := runtime.DefaultUnstructuredConverter.FromUnstructured(transformed.Resources()[0].Object, result); err != nil {
189+
t.Fatalf("failed to convert back: %v", err)
190+
}
191+
192+
for _, c := range result.Spec.Template.Spec.Containers {
193+
for _, e := range c.Env {
194+
if e.Name == occommon.TLSMinVersionEnvVar || e.Name == occommon.TLSCipherSuitesEnvVar {
195+
t.Errorf("unexpected TLS env var %s injected into non-webhook deployment", e.Name)
196+
}
197+
}
198+
}
199+
}

pkg/reconciler/shared/tektonconfig/pipeline/pipeline.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,16 @@ func UpdatePipeline(ctx context.Context, old *v1alpha1.TektonPipeline, new *v1al
133133
updated = true
134134
}
135135

136+
oldPlatformData := old.ObjectMeta.Annotations[v1alpha1.PlatformDataHashKey]
137+
newPlatformData := new.ObjectMeta.Annotations[v1alpha1.PlatformDataHashKey]
138+
if oldPlatformData != newPlatformData {
139+
if old.ObjectMeta.Annotations == nil {
140+
old.ObjectMeta.Annotations = map[string]string{}
141+
}
142+
old.ObjectMeta.Annotations[v1alpha1.PlatformDataHashKey] = newPlatformData
143+
updated = true
144+
}
145+
136146
if updated {
137147
_, err := clients.Update(ctx, old, metav1.UpdateOptions{})
138148
if err != nil {

pkg/reconciler/shared/tektonconfig/tektonconfig.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,12 @@ func (r *Reconciler) ReconcileKind(ctx context.Context, tc *v1alpha1.TektonConfi
178178

179179
// Ensure Pipeline CR
180180
tektonpipeline := pipeline.GetTektonPipelineCR(tc, r.operatorVersion)
181+
if platformData := r.extension.GetPlatformData(); platformData != "" {
182+
if tektonpipeline.Annotations == nil {
183+
tektonpipeline.Annotations = map[string]string{}
184+
}
185+
tektonpipeline.Annotations[v1alpha1.PlatformDataHashKey] = platformData
186+
}
181187
logger.Debug("Ensuring TektonPipeline CR exists")
182188
if _, err := pipeline.EnsureTektonPipelineExists(ctx, r.operatorClientSet.OperatorV1alpha1().TektonPipelines(), tektonpipeline); err != nil {
183189
errMsg := fmt.Sprintf("TektonPipeline: %s", err.Error())

0 commit comments

Comments
 (0)