diff --git a/pkg/cmd/operator/cmd.go b/pkg/cmd/operator/cmd.go index 7de13e8e3..489d9ee65 100644 --- a/pkg/cmd/operator/cmd.go +++ b/pkg/cmd/operator/cmd.go @@ -49,6 +49,7 @@ import ( "k8s.io/client-go/tools/clientcmd" "k8s.io/client-go/tools/leaderelection" "k8s.io/client-go/tools/leaderelection/resourcelock" + cliflag "k8s.io/component-base/cli/flag" "k8s.io/utils/clock" "sigs.k8s.io/controller-runtime/pkg/cache" "sigs.k8s.io/controller-runtime/pkg/client" @@ -84,8 +85,10 @@ const ( ) type ControllerManagerOptions struct { - LogLevel string - Kubeconfig string + LogLevel string + Kubeconfig string + TLSMinVersion string + TLSCipherSuites []string } func NewOperator() *cobra.Command { @@ -260,31 +263,13 @@ func NewOperator() *cobra.Command { return err } - // Retrieve the initial TLS Adherence policy for the TLS profile watcher - initialTLSAdherence, err := utiltls.FetchAPIServerTLSAdherencePolicy(ctx, k8sClient) + tlsConfig, initialTLSProfile, initialTLSAdherence, tlsOverrideFromFlags, err := resolveTLSConfig( + ctx, k8sClient, opts.TLSMinVersion, opts.TLSCipherSuites) if err != nil { - log.WithError(err).Error("failed to fetch initial TLS adherence") + log.WithError(err).Error("failed to resolve TLS configuration") return err } - // Retrieve the initial TLS profile to use for the TLS profile watcher - initialTLSProfile, err := utiltls.FetchAPIServerTLSProfile(ctx, k8sClient) - if err != nil { - log.WithError(err).Error("failed to fetch initial TLS profile") - return err - } - - tlsConfig := func(*tls.Config) {} - - if libgocrypto.ShouldHonorClusterTLSProfile(initialTLSAdherence) { - var unsupportedCiphers []string - // Create the TLS configuration function for the server endpoints. - tlsConfig, unsupportedCiphers = utiltls.NewTLSConfigFromProfile(initialTLSProfile) - if len(unsupportedCiphers) > 0 { - log.Infof("TLS configuration contains unsupported ciphers that will be ignored: %v", unsupportedCiphers) - } - } - // Create a new Cmd to provide shared dependencies and start components log.Info("setting up managers") mgr, err := manager.New(cfg, manager.Options{ @@ -327,28 +312,32 @@ func NewOperator() *cobra.Command { // Setup all Controllers log.Info("setting up controllers") - if err := controller.AddToManager(mgr, rootMgr, opts.Kubeconfig, coreClient); err != nil { + if err := controller.AddToManager(ctx, mgr, rootMgr, opts.Kubeconfig, coreClient, opts.TLSMinVersion, opts.TLSCipherSuites, tlsOverrideFromFlags); err != nil { log.WithError(err).Error("unable to register controllers to the manager") return err } - // Set up the TLS security profile watcher controller. - // This will trigger a graceful shutdown when the TLS profile changes. - if err := (&utiltls.SecurityProfileWatcher{ - Client: mgr.GetClient(), - InitialTLSProfileSpec: initialTLSProfile, - InitialTLSAdherencePolicy: initialTLSAdherence, - OnProfileChange: func(ctx context.Context, oldTLSProfileSpec, newTLSProfileSpec configv1.TLSProfileSpec) { - log.Infof("TLS profile changed from %v to %v, restarting operator", oldTLSProfileSpec, newTLSProfileSpec) - cancel() - }, - OnAdherencePolicyChange: func(ctx context.Context, oldTLSAdherencePolicy configv1.TLSAdherencePolicy, newTLSAdherencePolicy configv1.TLSAdherencePolicy) { - log.Infof("TLS adherence policy changed from %v to %v, restarting operator", oldTLSAdherencePolicy, newTLSAdherencePolicy) - cancel() - }, - }).SetupWithManager(mgr); err != nil { - log.WithError(err).Error("failed to set up TLS profile watcher") - return err + // When TLS is overridden via CLI flags, the watcher is not needed since + // we're not reading from apiservers.config.openshift.io/cluster. + if tlsOverrideFromFlags { + log.Info("TLS security profile watcher disabled because TLS is configured via CLI flags") + } else { + if err := (&utiltls.SecurityProfileWatcher{ + Client: mgr.GetClient(), + InitialTLSProfileSpec: initialTLSProfile, + InitialTLSAdherencePolicy: initialTLSAdherence, + OnProfileChange: func(ctx context.Context, oldTLSProfileSpec, newTLSProfileSpec configv1.TLSProfileSpec) { + log.Infof("TLS profile changed from %v to %v, restarting operator", oldTLSProfileSpec, newTLSProfileSpec) + cancel() + }, + OnAdherencePolicyChange: func(ctx context.Context, oldTLSAdherencePolicy configv1.TLSAdherencePolicy, newTLSAdherencePolicy configv1.TLSAdherencePolicy) { + log.Infof("TLS adherence policy changed from %v to %v, restarting operator", oldTLSAdherencePolicy, newTLSAdherencePolicy) + cancel() + }, + }).SetupWithManager(mgr); err != nil { + log.WithError(err).Error("failed to set up TLS profile watcher") + return err + } } // Start the managers @@ -456,6 +445,8 @@ func NewOperator() *cobra.Command { cmd.PersistentFlags().StringVar(&opts.LogLevel, "log-level", defaultLogLevel, "Log level (debug,info,warn,error,fatal)") cmd.PersistentFlags().StringVar(&opts.Kubeconfig, "kubeconfig", "", "Path to the kubeconfig to use.") + cmd.PersistentFlags().StringVar(&opts.TLSMinVersion, "tls-min-version", "", "Minimum TLS version supported. When set, overrides the cluster-wide TLS profile from APIServer CR. Possible values: VersionTLS10, VersionTLS11, VersionTLS12, VersionTLS13") + cmd.PersistentFlags().StringSliceVar(&opts.TLSCipherSuites, "tls-cipher-suites", nil, "Comma-separated list of cipher suites for the server. When set, overrides the cluster-wide TLS profile from APIServer CR. Values are Go crypto/tls cipher suite names.") cmd.PersistentFlags().AddGoFlagSet(flag.CommandLine) initializeGlog(cmd.PersistentFlags()) flag.CommandLine.Parse([]string{}) @@ -508,6 +499,71 @@ func (writer glogWriter) Write(data []byte) (n int, err error) { return len(data), nil } +func resolveTLSConfig(ctx context.Context, k8sClient client.Client, tlsMinVersion string, tlsCipherSuites []string) ( + func(*tls.Config), configv1.TLSProfileSpec, configv1.TLSAdherencePolicy, bool, error, +) { + if tlsMinVersion != "" || len(tlsCipherSuites) > 0 { + log.Info("TLS configuration overridden via CLI flags") + + var minVersion uint16 + if tlsMinVersion != "" { + var err error + minVersion, err = cliflag.TLSVersion(tlsMinVersion) + if err != nil { + return nil, configv1.TLSProfileSpec{}, "", false, + fmt.Errorf("invalid --tls-min-version value: %w", err) + } + } else { + minVersion = cliflag.DefaultTLSVersion() + } + + var cipherSuites []uint16 + if len(tlsCipherSuites) > 0 { + var err error + cipherSuites, err = cliflag.TLSCipherSuites(tlsCipherSuites) + if err != nil { + return nil, configv1.TLSProfileSpec{}, "", false, + fmt.Errorf("invalid --tls-cipher-suites value: %w", err) + } + } + + tlsConfigFunc := func(cfg *tls.Config) { + cfg.MinVersion = minVersion + if minVersion < tls.VersionTLS13 { + cfg.CipherSuites = cipherSuites + } else if len(tlsCipherSuites) > 0 { + log.Warning("TLS 1.3 cipher suites are not configurable in Go, ignoring --tls-cipher-suites") + } + } + + return tlsConfigFunc, configv1.TLSProfileSpec{}, "", true, nil + } + + initialTLSAdherence, err := utiltls.FetchAPIServerTLSAdherencePolicy(ctx, k8sClient) + if err != nil { + return nil, configv1.TLSProfileSpec{}, "", false, + fmt.Errorf("failed to fetch TLS adherence policy: %w", err) + } + + initialTLSProfile, err := utiltls.FetchAPIServerTLSProfile(ctx, k8sClient) + if err != nil { + return nil, configv1.TLSProfileSpec{}, "", false, + fmt.Errorf("failed to fetch TLS profile: %w", err) + } + + tlsConfigFunc := func(*tls.Config) {} + + if libgocrypto.ShouldHonorClusterTLSProfile(initialTLSAdherence) { + var unsupportedCiphers []string + tlsConfigFunc, unsupportedCiphers = utiltls.NewTLSConfigFromProfile(initialTLSProfile) + if len(unsupportedCiphers) > 0 { + log.Infof("TLS configuration contains unsupported ciphers that will be ignored: %v", unsupportedCiphers) + } + } + + return tlsConfigFunc, initialTLSProfile, initialTLSAdherence, false, nil +} + func terminateWhenProxyChanges(path string, cancel context.CancelFunc, done <-chan struct{}) { // read the contents of the configmap fileContents := map[string][]byte{} diff --git a/pkg/operator/controller.go b/pkg/operator/controller.go index 52089d7ea..9ae7c8076 100644 --- a/pkg/operator/controller.go +++ b/pkg/operator/controller.go @@ -17,6 +17,8 @@ limitations under the License. package controller import ( + "context" + configv1 "github.com/openshift/api/config/v1" awsactuator "github.com/openshift/cloud-credential-operator/pkg/aws/actuator" "github.com/openshift/cloud-credential-operator/pkg/azure" @@ -51,7 +53,6 @@ const ( func init() { AddToManagerFuncs = append(AddToManagerFuncs, metrics.Add) AddToManagerFuncs = append(AddToManagerFuncs, secretannotator.Add) - AddToManagerFuncs = append(AddToManagerFuncs, podidentity.Add) AddToManagerFuncs = append(AddToManagerFuncs, status.Add) AddToManagerFuncs = append(AddToManagerFuncs, loglevel.Add) AddToManagerFuncs = append(AddToManagerFuncs, cleanup.Add) @@ -65,13 +66,20 @@ var AddToManagerFuncs []func(manager.Manager, manager.Manager, string) error // AddToManagerWithActuatorFuncs is a list of functions to add all Controllers with Actuators to the Manager var AddToManagerWithActuatorFuncs []func(manager.Manager, manager.Manager, actuator.Actuator, configv1.PlatformType, corev1client.CoreV1Interface) error -// AddToManager adds all Controllers to the Manager -func AddToManager(m, rootM manager.Manager, explicitKubeconfig string, coreClient corev1client.CoreV1Interface) error { +// AddToManager adds all Controllers to the Manager with optional TLS CLI flag overrides. +// TODO: consolidate tlsMinVersion, tlsCipherSuites, tlsOverrideFromFlags into a struct +// if more TLS parameters are added — the bool is derivable from the flag values. +func AddToManager(ctx context.Context, m, rootM manager.Manager, explicitKubeconfig string, coreClient corev1client.CoreV1Interface, tlsMinVersion string, tlsCipherSuites []string, tlsOverrideFromFlags bool) error { for _, f := range AddToManagerFuncs { if err := f(m, rootM, explicitKubeconfig); err != nil { return err } } + + if err := podidentity.AddWithTLS(ctx, m, rootM, explicitKubeconfig, tlsMinVersion, tlsCipherSuites, tlsOverrideFromFlags); err != nil { + return err + } + for _, f := range AddToManagerWithActuatorFuncs { // Check for supported platform types, dummy if not found: // TODO: Use infrastructure type to determine this in future, it's not being populated yet: diff --git a/pkg/operator/podidentity/podidentitywebhook_controller.go b/pkg/operator/podidentity/podidentitywebhook_controller.go index 65d7d7e8f..09feb239a 100644 --- a/pkg/operator/podidentity/podidentitywebhook_controller.go +++ b/pkg/operator/podidentity/podidentitywebhook_controller.go @@ -149,7 +149,15 @@ func (c *podIdentityController) Start(ctx context.Context) error { return nil } +func AddWithTLS(ctx context.Context, mgr, _ manager.Manager, kubeconfig string, tlsMinVersion string, tlsCipherSuites []string, tlsOverrideFromFlags bool) error { + return addInternal(ctx, mgr, kubeconfig, tlsMinVersion, tlsCipherSuites, tlsOverrideFromFlags) +} + func Add(mgr, _ manager.Manager, kubeconfig string) error { + return addInternal(context.TODO(), mgr, kubeconfig, "", nil, false) +} + +func addInternal(ctx context.Context, mgr manager.Manager, kubeconfig string, tlsMinVersion string, tlsCipherSuites []string, tlsOverrideFromFlags bool) error { infraStatus, err := platform.GetInfraStatusUsingKubeconfig(kubeconfig) if err != nil { return err @@ -172,7 +180,6 @@ func Add(mgr, _ manager.Manager, kubeconfig string) error { log.WithField("controller", controllerName).Warn("Failed to get platform type") return nil } - ctx := context.TODO() logger := log.WithFields(log.Fields{"platform": platformType, "controller": controllerName}) config := mgr.GetConfig() @@ -224,18 +231,27 @@ func Add(mgr, _ manager.Manager, kubeconfig string) error { return err } - tlsAdherence, err := utiltls.FetchAPIServerTLSAdherencePolicy(ctx, k8sClient) - if err != nil { - return err - } + if tlsOverrideFromFlags { + logger.Info("Webhook TLS configuration overridden via CLI flags") + r.tlsProfileSpec = configv1.TLSProfileSpec{ + MinTLSVersion: configv1.TLSProtocolVersion(tlsMinVersion), + Ciphers: tlsCipherSuites, + } + r.tlsFromFlags = true + } else { + tlsAdherence, err := utiltls.FetchAPIServerTLSAdherencePolicy(ctx, k8sClient) + if err != nil { + return err + } - initialTLSProfile, err := utiltls.FetchAPIServerTLSProfile(ctx, k8sClient) - if err != nil { - return err - } + initialTLSProfile, err := utiltls.FetchAPIServerTLSProfile(ctx, k8sClient) + if err != nil { + return err + } - if libgocrypto.ShouldHonorClusterTLSProfile(tlsAdherence) { - r.tlsProfileSpec = initialTLSProfile + if libgocrypto.ShouldHonorClusterTLSProfile(tlsAdherence) { + r.tlsProfileSpec = initialTLSProfile + } } c, err := controller.New(controllerName, mgr, controller.Options{Reconciler: r}) @@ -321,6 +337,7 @@ type staticResourceReconciler struct { cache resourceapply.ResourceCache podIdentityType PodIdentityManifestSource tlsProfileSpec configv1.TLSProfileSpec + tlsFromFlags bool // degradedSince tracks when reconciliation errors started. On pod restart // this resets to zero; seedDegradedSince recovers it from the published // ClusterOperator Degraded condition so the grace period is not re-granted. @@ -501,10 +518,29 @@ func (r *staticResourceReconciler) ReconcileResources(ctx context.Context) (*app requestedDeployment.Spec.Template.Spec.Containers[0].Image = r.imagePullSpec - err = r.podIdentityType.ApplyDeploymentSubstitutionsInPlace(requestedDeployment, r.client, r.logger, r.tlsProfileSpec) - if err != nil { - r.logger.WithError(err).Error("error substituting Deployment") - return nil, err + if r.tlsFromFlags { + // CLI flags provide IANA cipher names, but ApplyDeploymentSubstitutionsInPlace + // runs OpenSSL-to-IANA conversion that would silently drop them. Pass an empty + // TLSProfileSpec so non-TLS substitutions still run, then inject TLS args directly. + err = r.podIdentityType.ApplyDeploymentSubstitutionsInPlace(requestedDeployment, r.client, r.logger, configv1.TLSProfileSpec{}) + if err != nil { + r.logger.WithError(err).Error("error substituting Deployment") + return nil, err + } + if r.tlsProfileSpec.MinTLSVersion != "" { + requestedDeployment.Spec.Template.Spec.Containers[0].Command = append(requestedDeployment.Spec.Template.Spec.Containers[0].Command, + fmt.Sprintf("--tls-min-version=%s", r.tlsProfileSpec.MinTLSVersion)) + } + if len(r.tlsProfileSpec.Ciphers) > 0 && r.tlsProfileSpec.MinTLSVersion != configv1.TLSProtocolVersion("VersionTLS13") { + requestedDeployment.Spec.Template.Spec.Containers[0].Command = append(requestedDeployment.Spec.Template.Spec.Containers[0].Command, + fmt.Sprintf("--tls-cipher-suites=%s", strings.Join(r.tlsProfileSpec.Ciphers, ","))) + } + } else { + err = r.podIdentityType.ApplyDeploymentSubstitutionsInPlace(requestedDeployment, r.client, r.logger, r.tlsProfileSpec) + if err != nil { + r.logger.WithError(err).Error("error substituting Deployment") + return nil, err + } } resultDeployment, modified, err := resourceapply.ApplyDeployment(ctx, r.clientset.AppsV1(), r.eventRecorder, requestedDeployment, r.deploymentGeneration)