From b0e5fc4b93c8809f5e7fdfbf9651fc17124ea176 Mon Sep 17 00:00:00 2001 From: Shweta Padubidri Date: Fri, 8 May 2026 13:40:01 -0400 Subject: [PATCH] COO-1819:fix: Add TLS min version and cipher suite configuration support Signed-off-by: Shweta Padubidri --- cmd/plugin-backend.go | 83 ++++++++++++++++++----- cmd/plugin-backend_test.go | 130 +++++++++++++++++++++++++++++++++++++ go.mod | 7 +- go.sum | 19 ++++-- pkg/server.go | 18 ++++- 5 files changed, 234 insertions(+), 23 deletions(-) create mode 100644 cmd/plugin-backend_test.go diff --git a/cmd/plugin-backend.go b/cmd/plugin-backend.go index f2214d4..656b771 100644 --- a/cmd/plugin-backend.go +++ b/cmd/plugin-backend.go @@ -1,25 +1,30 @@ package main import ( + "crypto/tls" "flag" + "fmt" "os" "strconv" "strings" server "github.com/openshift/troubleshooting-panel-console-plugin/pkg" "github.com/sirupsen/logrus" + k8sapiflag "k8s.io/component-base/cli/flag" ) var ( - portArg = flag.Int("port", 0, "server port to listen on (default: 9443)") - certArg = flag.String("cert", "", "cert file path to enable TLS (disabled by default)") - keyArg = flag.String("key", "", "private key file path to enable TLS (disabled by default)") - featuresArg = flag.String("features", "", "enabled features, comma separated") - staticPathArg = flag.String("static-path", "", "static files path to serve frontend (default: './web/dist')") - configPathArg = flag.String("config-path", "", "config files path (default: './config')") - pluginConfigArg = flag.String("plugin-config-path", "", "plugin yaml configuration") - logLevelArg = flag.String("log-level", logrus.InfoLevel.String(), "verbosity of logs\noptions: ['panic', 'fatal', 'error', 'warn', 'info', 'debug', 'trace']\n'trace' level will log all incoming requests\n(default 'error')") - log = logrus.WithField("module", "main") + portArg = flag.Int("port", 0, "server port to listen on (default: 9443)") + certArg = flag.String("cert", "", "cert file path to enable TLS (disabled by default)") + keyArg = flag.String("key", "", "private key file path to enable TLS (disabled by default)") + tlsMinVersionArg = flag.String("tls-min-version", "", "minimum TLS version (e.g., VersionTLS12, VersionTLS13)") + tlsCipherSuitesArg = flag.String("tls-cipher-suites", "", "comma-separated list of cipher suites for TLS 1.0-1.2 (ignored for TLS 1.3)") + featuresArg = flag.String("features", "", "enabled features, comma separated") + staticPathArg = flag.String("static-path", "", "static files path to serve frontend (default: './web/dist')") + configPathArg = flag.String("config-path", "", "config files path (default: './config')") + pluginConfigArg = flag.String("plugin-config-path", "", "plugin yaml configuration") + logLevelArg = flag.String("log-level", logrus.InfoLevel.String(), "verbosity of logs\noptions: ['panic', 'fatal', 'error', 'warn', 'info', 'debug', 'trace']\n'trace' level will log all incoming requests\n(default 'error')") + log = logrus.WithField("module", "main") ) func main() { @@ -28,6 +33,8 @@ func main() { port := mergeEnvValueInt("PORT", *portArg, 9443) cert := mergeEnvValue("CERT_FILE_PATH", *certArg, "") key := mergeEnvValue("PRIVATE_KEY_FILE_PATH", *keyArg, "") + tlsMinVersion := mergeEnvValue("TLS_MIN_VERSION", *tlsMinVersionArg, "") + tlsCipherSuites := mergeEnvValue("TLS_CIPHER_SUITES", *tlsCipherSuitesArg, "") features := mergeEnvValue("TROUBLESHOOTING_PANEL_CONSOLE_PLUGIN_FEATURES", *featuresArg, "") staticPath := mergeEnvValue("TROUBLESHOOTING_PANEL_CONSOLE_PLUGIN_STATIC_PATH", *staticPathArg, "opt/app-root/web/dist") configPath := mergeEnvValue("TROUBLESHOOTING_PANEL_CONSOLE_PLUGIN_MANIFEST_CONFIG_PATH", *configPathArg, "opt/app-root/web/dist") @@ -49,13 +56,25 @@ func main() { log.Infof("enabled features: %+q\n", featuresList) + tlsMinVer, err := parseTLSVersion(tlsMinVersion) + if err != nil { + log.WithError(err).Fatal("Invalid TLS minimum version") + } + + tlsCipherSuitesList, err := parseCipherSuites(tlsCipherSuites) + if err != nil { + log.WithError(err).Fatal("Invalid TLS cipher suites") + } + server.Start(&server.Config{ - Port: port, - CertFile: cert, - PrivateKeyFile: key, - Features: featuresSet, - StaticPath: staticPath, - ConfigPath: configPath, + Port: port, + CertFile: cert, + PrivateKeyFile: key, + TLSMinVersion: tlsMinVer, + TLSCipherSuites: tlsCipherSuitesList, + Features: featuresSet, + StaticPath: staticPath, + ConfigPath: configPath, PluginConfigPath: pluginConfigPath, }) } @@ -88,3 +107,37 @@ func mergeEnvValueInt(key string, arg int, defaultValue int) int { return defaultValue } + +func parseTLSVersion(version string) (uint16, error) { + if version == "" { + return 0, nil + } + tlsVersion, err := k8sapiflag.TLSVersion(version) + if err != nil { + return 0, err + } + // Reject TLS 1.0 and 1.1 as they are deprecated per RFC 8996 + if tlsVersion != 0 && tlsVersion < tls.VersionTLS12 { + return 0, fmt.Errorf("TLS versions below 1.2 are not supported (got %s); minimum allowed is VersionTLS12", version) + } + return tlsVersion, nil +} + +func parseCipherSuites(cipherSuitesStr string) ([]uint16, error) { + if cipherSuitesStr == "" { + return nil, nil + } + + cipherSuiteNames := strings.Split(cipherSuitesStr, ",") + // Trim whitespace from each cipher suite name + trimmed := make([]string, 0, len(cipherSuiteNames)) + for _, name := range cipherSuiteNames { + if t := strings.TrimSpace(name); t != "" { + trimmed = append(trimmed, t) + } + } + if len(trimmed) == 0 { + return nil, nil + } + return k8sapiflag.TLSCipherSuites(trimmed) +} diff --git a/cmd/plugin-backend_test.go b/cmd/plugin-backend_test.go new file mode 100644 index 0000000..a8cd5e0 --- /dev/null +++ b/cmd/plugin-backend_test.go @@ -0,0 +1,130 @@ +package main + +import ( + "crypto/tls" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestParseTLSVersion(t *testing.T) { + tests := []struct { + name string + version string + expected uint16 + expectError bool + }{ + { + name: "Empty string returns 0", + version: "", + expected: 0, + expectError: false, + }, + { + name: "Valid TLS 1.2", + version: "VersionTLS12", + expected: tls.VersionTLS12, + expectError: false, + }, + { + name: "Valid TLS 1.3", + version: "VersionTLS13", + expected: tls.VersionTLS13, + expectError: false, + }, + { + name: "Invalid version", + version: "InvalidVersion", + expected: 0, + expectError: true, + }, + { + name: "TLS 1.0 rejected", + version: "VersionTLS10", + expected: 0, + expectError: true, + }, + { + name: "TLS 1.1 rejected", + version: "VersionTLS11", + expected: 0, + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := parseTLSVersion(tt.version) + if tt.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.expected, result) + } + }) + } +} + +func TestParseCipherSuites(t *testing.T) { + tests := []struct { + name string + input string + expected []uint16 + expectError bool + }{ + { + name: "Empty string returns nil", + input: "", + expected: nil, + expectError: false, + }, + { + name: "Single valid cipher suite", + input: "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", + expected: []uint16{tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256}, + expectError: false, + }, + { + name: "Multiple valid cipher suites", + input: "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", + expected: []uint16{ + tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, + }, + expectError: false, + }, + { + name: "Valid cipher suites with spaces", + input: "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, TLS_AES_128_GCM_SHA256", + expected: []uint16{ + tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + tls.TLS_AES_128_GCM_SHA256, + }, + expectError: false, + }, + { + name: "Invalid cipher suite", + input: "INVALID_CIPHER", + expected: nil, + expectError: true, + }, + { + name: "Mixed valid and invalid", + input: "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,INVALID_CIPHER", + expected: nil, + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := parseCipherSuites(tt.input) + if tt.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.expected, result) + } + }) + } +} diff --git a/go.mod b/go.mod index dcef84e..1dbbb5c 100644 --- a/go.mod +++ b/go.mod @@ -3,15 +3,17 @@ module github.com/openshift/troubleshooting-panel-console-plugin go 1.24.0 require ( - github.com/evanphx/json-patch v0.5.2 + github.com/evanphx/json-patch v4.12.0+incompatible github.com/gorilla/handlers v1.5.2 github.com/gorilla/mux v1.8.1 + github.com/openshift/library-go v0.0.0-20240412173449-eb2f24c36528 github.com/sirupsen/logrus v1.9.3 github.com/stretchr/testify v1.9.0 gopkg.in/yaml.v2 v2.4.0 k8s.io/api v0.31.1 k8s.io/apiserver v0.31.1 k8s.io/client-go v0.31.1 + k8s.io/component-base v0.31.1 ) require ( @@ -31,6 +33,7 @@ require ( github.com/google/go-cmp v0.6.0 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/google/uuid v1.6.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/mailru/easyjson v0.7.7 // indirect @@ -39,6 +42,8 @@ require ( github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/spf13/cobra v1.8.1 // indirect + github.com/spf13/pflag v1.0.5 // indirect github.com/x448/float16 v0.8.4 // indirect golang.org/x/net v0.34.0 // indirect golang.org/x/oauth2 v0.25.0 // indirect diff --git a/go.sum b/go.sum index 804fe2a..116a6b0 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,4 @@ +github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -5,8 +6,8 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= -github.com/evanphx/json-patch v0.5.2 h1:xVCHIVMUu1wtM/VkR9jVZ45N3FhZfYMMYGorLCR8P3k= -github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ= +github.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH5pOlLGNtQ5lPWQu84= +github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= @@ -46,7 +47,8 @@ github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyE github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= -github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= @@ -71,8 +73,10 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/onsi/ginkgo/v2 v2.19.0 h1:9Cnnf7UHo57Hy3k6/m5k3dRfGTMXGvxhHFvkDTCTpvA= github.com/onsi/ginkgo/v2 v2.19.0/go.mod h1:rlwLi9PilAFJ8jCg9UE1QP6VBpd6/xj3SRC0d6TU0To= -github.com/onsi/gomega v1.19.0 h1:4ieX6qQjPP/BfC3mpsAtIGGlxTWPeA3Inl/7DtXw1tw= -github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= +github.com/onsi/gomega v1.33.1 h1:dsYjIxxSR755MDmKVsaFQTE22ChNBcuuTWgkUDSubOk= +github.com/onsi/gomega v1.33.1/go.mod h1:U4R44UsT+9eLIaYRB2a5qajjtQYn0hauxvRm16AVYg0= +github.com/openshift/library-go v0.0.0-20240412173449-eb2f24c36528 h1:vnLKZUSW1aPv7Pd6+QYjDUU+/8z2MSBacU38cAlNMPA= +github.com/openshift/library-go v0.0.0-20240412173449-eb2f24c36528/go.mod h1:m/HsttSi90vSixwoy5mPUBHcZid2YRw/QbsLErLxF9s= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -80,8 +84,11 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= +github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -163,6 +170,8 @@ k8s.io/apiserver v0.31.1 h1:Sars5ejQDCRBY5f7R3QFHdqN3s61nhkpaX8/k1iEw1c= k8s.io/apiserver v0.31.1/go.mod h1:lzDhpeToamVZJmmFlaLwdYZwd7zB+WYRYIboqA1kGxM= k8s.io/client-go v0.31.1 h1:f0ugtWSbWpxHR7sjVpQwuvw9a3ZKLXX0u0itkFXufb0= k8s.io/client-go v0.31.1/go.mod h1:sKI8871MJN2OyeqRlmA4W4KM9KBdBUpDLu/43eGemCg= +k8s.io/component-base v0.31.1 h1:UpOepcrX3rQ3ab5NB6g5iP0tvsgJWzxTyAo20sgYSy8= +k8s.io/component-base v0.31.1/go.mod h1:WGeaw7t/kTsqpVTaCoVEtillbqAhF2/JgvO0LDOMa0w= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 h1:BZqlfIlq5YbRMFko6/PM7FjZpUb45WallggurYhKGag= diff --git a/pkg/server.go b/pkg/server.go index c5c87f9..a1ce8bb 100644 --- a/pkg/server.go +++ b/pkg/server.go @@ -12,6 +12,7 @@ import ( "github.com/gorilla/handlers" "github.com/gorilla/mux" + oscrypto "github.com/openshift/library-go/pkg/crypto" "github.com/sirupsen/logrus" "gopkg.in/yaml.v2" v1 "k8s.io/api/core/v1" @@ -26,6 +27,8 @@ type Config struct { Port int CertFile string PrivateKeyFile string + TLSMinVersion uint16 + TLSCipherSuites []uint16 Features map[string]bool StaticPath string ConfigPath string @@ -51,8 +54,19 @@ func Start(cfg *Config) { router, pluginConfig := setupRoutes(cfg) router.Use(corsHeaderMiddleware()) - tlsConfig := &tls.Config{ - MinVersion: tls.VersionTLS12, + tlsConfig := oscrypto.SecureTLSConfig(&tls.Config{}) + + if cfg.TLSMinVersion != 0 { + tlsConfig.MinVersion = cfg.TLSMinVersion + } + // Note: CipherSuites only applies to TLS 1.0-1.2. TLS 1.3 cipher suites are + // non-configurable in Go's crypto/tls package and will use secure defaults. + if len(cfg.TLSCipherSuites) > 0 { + tlsConfig.CipherSuites = cfg.TLSCipherSuites + // Warn if cipher suites are configured with TLS 1.3 minimum, as they won't apply + if cfg.TLSMinVersion >= tls.VersionTLS13 { + log.Warn("TLS cipher suites are configured but will be ignored with TLS 1.3; TLS 1.3 uses non-configurable cipher suites") + } } timeout := 30 * time.Second