Skip to content

Commit e4a4ced

Browse files
dkwon17claude
andcommitted
Honor cluster TLS profile only when tlsAdherence policy requires it
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent 5684f19 commit e4a4ced

12 files changed

Lines changed: 335 additions & 161 deletions

File tree

‎controllers/workspace/devworkspace_controller.go‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ type DevWorkspaceReconciler struct {
9696
// +kubebuilder:rbac:groups=rbac.authorization.k8s.io,resources=clusterroles;clusterrolebindings,verbs=get;list;watch;create;update
9797
// +kubebuilder:rbac:groups=rbac.authorization.k8s.io,resources=roles;rolebindings,verbs=get;list;watch;create;update;delete
9898
// +kubebuilder:rbac:groups=oauth.openshift.io,resources=oauthclients,verbs=get;list;watch;create;update;patch;delete;deletecollection
99+
// +kubebuilder:rbac:groups=config.openshift.io,resources=apiservers,verbs=get;list;watch
99100
// +kubebuilder:rbac:groups=monitoring.coreos.com,resources=servicemonitors,verbs=get;create
100101
// +kubebuilder:rbac:groups=config.openshift.io,resources=proxies,verbs=get,resourceNames=cluster
101102
// +kubebuilder:rbac:groups=apps,resourceNames=devworkspace-controller,resources=deployments/finalizers,verbs=update

‎deploy/deployment/kubernetes/combined.yaml‎

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎deploy/deployment/kubernetes/objects/devworkspace-controller-role.ClusterRole.yaml‎

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎deploy/deployment/openshift/combined.yaml‎

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎deploy/deployment/openshift/objects/devworkspace-controller-role.ClusterRole.yaml‎

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎deploy/templates/components/rbac/role.yaml‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,14 @@ rules:
133133
- patch
134134
- update
135135
- watch
136+
- apiGroups:
137+
- config.openshift.io
138+
resources:
139+
- apiservers
140+
verbs:
141+
- get
142+
- list
143+
- watch
136144
- apiGroups:
137145
- config.openshift.io
138146
resourceNames:

‎go.mod‎

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,10 @@ require (
1212
github.com/google/gofuzz v1.2.0
1313
github.com/google/uuid v1.6.0
1414
github.com/kevinburke/ssh_config v1.2.0
15-
github.com/onsi/ginkgo/v2 v2.27.4
16-
github.com/onsi/gomega v1.39.0
17-
github.com/openshift/api v0.0.0-20200205133042-34f0ec8dab87
15+
github.com/onsi/ginkgo/v2 v2.28.1
16+
github.com/onsi/gomega v1.39.1
17+
github.com/openshift/api v0.0.0-20260317165824-54a3998d81eb
18+
github.com/openshift/controller-runtime-common v0.0.0-20260428152732-64ee174f5e2e
1819
github.com/operator-framework/operator-lib v0.11.0
1920
github.com/prometheus/client_golang v1.23.2
2021
github.com/robfig/cron/v3 v3.0.0
@@ -60,11 +61,10 @@ require (
6061
github.com/go-openapi/jsonreference v0.20.2 // indirect
6162
github.com/go-openapi/swag v0.23.0 // indirect
6263
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
63-
github.com/gogo/protobuf v1.3.2 // indirect
6464
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
6565
github.com/google/cel-go v0.29.0 // indirect
6666
github.com/google/gnostic-models v0.7.0 // indirect
67-
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect
67+
github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83 // indirect
6868
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 // indirect
6969
github.com/hashicorp/errwrap v1.0.0 // indirect
7070
github.com/hashicorp/go-multierror v1.1.1 // indirect
@@ -78,6 +78,7 @@ require (
7878
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
7979
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
8080
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
81+
github.com/openshift/library-go v0.0.0-20260213153706-03f1709971c5 // indirect
8182
github.com/pjbgf/sha1cd v0.6.0 // indirect
8283
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
8384
github.com/prometheus/client_model v0.6.2 // indirect

‎go.sum‎

Lines changed: 12 additions & 149 deletions
Large diffs are not rendered by default.

‎main.go‎

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import (
3030
"github.com/devfile/devworkspace-operator/pkg/config"
3131
"github.com/devfile/devworkspace-operator/pkg/infrastructure"
3232
kubesync "github.com/devfile/devworkspace-operator/pkg/library/kubernetes"
33+
"github.com/devfile/devworkspace-operator/pkg/tlssetup"
3334
"github.com/devfile/devworkspace-operator/pkg/webhook"
3435
"github.com/devfile/devworkspace-operator/version"
3536

@@ -116,6 +117,13 @@ func main() {
116117
setupLog.Error(err, "failed to initialized Kubernetes objects decoder")
117118
}
118119

120+
serverTLS, err := tlssetup.BuildServerTLSOptions(
121+
context.Background(), ctrl.GetConfigOrDie(), scheme, setupLog)
122+
if err != nil {
123+
setupLog.Error(err, "failed to build TLS options for servers")
124+
os.Exit(1)
125+
}
126+
119127
cacheFunc, err := cache.GetCacheFunc()
120128
if err != nil {
121129
setupLog.Error(err, "failed to set up objects cache")
@@ -128,9 +136,11 @@ func main() {
128136
BindAddress: metricsAddr,
129137
FilterProvider: filters.WithAuthenticationAndAuthorization,
130138
SecureServing: true,
139+
TLSOpts: serverTLS.TLSOpts,
131140
},
132141
WebhookServer: ctrl_webhook.NewServer(ctrl_webhook.Options{
133-
Port: 9443,
142+
Port: 9443,
143+
TLSOpts: serverTLS.TLSOpts,
134144
}),
135145
HealthProbeBindAddress: ":6789",
136146
LeaderElection: enableLeaderElection,
@@ -220,6 +230,16 @@ func main() {
220230
setupLog.Error(err, "failed creating conversion webhook for DevWorkspaces v1alpha2")
221231
}
222232

233+
// On OpenShift, watch cluster TLS profile and restart if it changes.
234+
signalCtx := ctrl.SetupSignalHandler()
235+
ctx, cancelCtx := context.WithCancel(signalCtx)
236+
defer cancelCtx()
237+
238+
if err := tlssetup.RegisterSecurityProfileWatcher(mgr, serverTLS, cancelCtx, setupLog); err != nil {
239+
setupLog.Error(err, "unable to set up TLS security profile watcher")
240+
os.Exit(1)
241+
}
242+
223243
// Setup health check
224244
if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil {
225245
setupLog.Error(err, "Unable to set up health check")
@@ -233,7 +253,7 @@ func main() {
233253
}
234254

235255
setupLog.Info("starting manager")
236-
if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil {
256+
if err := mgr.Start(ctx); err != nil {
237257
setupLog.Error(err, "problem running manager")
238258
os.Exit(1)
239259
}

‎pkg/tlssetup/server_tls.go‎

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
//
2+
// Copyright (c) 2019-2026 Red Hat, Inc.
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
//
15+
16+
// Package tlssetup builds TLS options for controller-runtime servers from the
17+
// OpenShift APIServer TLS profile.
18+
package tlssetup
19+
20+
import (
21+
"context"
22+
"crypto/tls"
23+
24+
"github.com/go-logr/logr"
25+
configv1 "github.com/openshift/api/config/v1"
26+
ostls "github.com/openshift/controller-runtime-common/pkg/tls"
27+
k8sruntime "k8s.io/apimachinery/pkg/runtime"
28+
"k8s.io/client-go/rest"
29+
"sigs.k8s.io/controller-runtime/pkg/client"
30+
"sigs.k8s.io/controller-runtime/pkg/manager"
31+
32+
"github.com/devfile/devworkspace-operator/pkg/infrastructure"
33+
)
34+
35+
// ServerTLS holds TLS options and the initial APIServer profile/adherence policy for watching.
36+
type ServerTLS struct {
37+
TLSOpts []func(*tls.Config)
38+
InitialTLSProfileSpec configv1.TLSProfileSpec
39+
InitialTLSAdherencePolicy configv1.TLSAdherencePolicy
40+
}
41+
42+
// ShouldHonorClusterTLSProfile returns true when the component must honor the cluster
43+
// TLS profile. Unknown values return true for forward compatibility.
44+
func ShouldHonorClusterTLSProfile(adherence configv1.TLSAdherencePolicy) bool {
45+
switch adherence {
46+
case "", configv1.TLSAdherencePolicyLegacyAdheringComponentsOnly:
47+
return false
48+
default:
49+
// StrictAllComponents or unknown future value → honor the profile
50+
return true
51+
}
52+
}
53+
54+
// BuildServerTLSOptions fetches TLS settings from the OpenShift API server.
55+
// Only applies the cluster profile when the tlsAdherence policy requires it.
56+
func BuildServerTLSOptions(ctx context.Context, cfg *rest.Config, scheme *k8sruntime.Scheme, log logr.Logger) (ServerTLS, error) {
57+
var result ServerTLS
58+
59+
if !infrastructure.IsOpenShift() {
60+
log.Info("Not running on OpenShift; using Go default TLS configuration")
61+
return result, nil
62+
}
63+
64+
bootstrapClient, err := client.New(cfg, client.Options{Scheme: scheme})
65+
if err != nil {
66+
log.Error(err, "Failed to create bootstrap client for TLS profile fetch; using Go default TLS configuration")
67+
return result, nil
68+
}
69+
70+
profile, err := ostls.FetchAPIServerTLSProfile(ctx, bootstrapClient)
71+
if err != nil {
72+
log.Error(err, "Failed to fetch TLS profile from APIServer; using Go default TLS configuration")
73+
return result, nil
74+
}
75+
76+
adherence, err := ostls.FetchAPIServerTLSAdherencePolicy(ctx, bootstrapClient)
77+
if err != nil {
78+
log.Error(err, "Failed to fetch TLS adherence policy from APIServer; using Go default TLS configuration")
79+
return result, nil
80+
}
81+
82+
result.InitialTLSProfileSpec = profile
83+
result.InitialTLSAdherencePolicy = adherence
84+
85+
// Check if we should honor the cluster TLS profile
86+
if !ShouldHonorClusterTLSProfile(adherence) {
87+
log.Info("TLS adherence policy does not require strict adherence; using Go default TLS configuration",
88+
"policy", adherence)
89+
return result, nil
90+
}
91+
92+
// Apply the cluster TLS profile
93+
tlsConfigFn, unsupported := ostls.NewTLSConfigFromProfile(profile)
94+
if len(unsupported) > 0 {
95+
log.Info("TLS profile contains ciphers unsupported by Go; they will be ignored",
96+
"unsupportedCiphers", unsupported)
97+
}
98+
99+
result.TLSOpts = []func(*tls.Config){tlsConfigFn}
100+
101+
log.Info("Applying cluster TLS profile to metrics and webhook servers",
102+
"minTLSVersion", profile.MinTLSVersion,
103+
"cipherCount", len(profile.Ciphers),
104+
"adherencePolicy", adherence)
105+
106+
return result, nil
107+
}
108+
109+
// RegisterSecurityProfileWatcher watches the APIServer TLS profile and adherence policy.
110+
// Calls onCancel to trigger restart when either changes. No-op on non-OpenShift.
111+
func RegisterSecurityProfileWatcher(mgr manager.Manager, serverTLS ServerTLS, onCancel context.CancelFunc, log logr.Logger) error {
112+
if !infrastructure.IsOpenShift() {
113+
return nil
114+
}
115+
116+
// Only set up the watcher if we successfully fetched the initial profile
117+
if len(serverTLS.TLSOpts) == 0 {
118+
log.Info("Skipping TLS profile watcher (profile not applied)")
119+
return nil
120+
}
121+
122+
watcher := &ostls.SecurityProfileWatcher{
123+
Client: mgr.GetClient(),
124+
InitialTLSProfileSpec: serverTLS.InitialTLSProfileSpec,
125+
InitialTLSAdherencePolicy: serverTLS.InitialTLSAdherencePolicy,
126+
OnProfileChange: func(_ context.Context, old, new configv1.TLSProfileSpec) {
127+
log.Info("TLS security profile changed; initiating graceful restart",
128+
"oldMinTLSVersion", old.MinTLSVersion,
129+
"newMinTLSVersion", new.MinTLSVersion)
130+
onCancel()
131+
},
132+
OnAdherencePolicyChange: func(_ context.Context, old, new configv1.TLSAdherencePolicy) {
133+
log.Info("TLS adherence policy changed; initiating graceful restart",
134+
"old", old,
135+
"new", new)
136+
onCancel()
137+
},
138+
}
139+
140+
return watcher.SetupWithManager(mgr)
141+
}

0 commit comments

Comments
 (0)