diff --git a/drivers/storage/portworx/component/pvccontroller.go b/drivers/storage/portworx/component/pvccontroller.go index 503734f35..c2a352a15 100644 --- a/drivers/storage/portworx/component/pvccontroller.go +++ b/drivers/storage/portworx/component/pvccontroller.go @@ -329,6 +329,18 @@ func (c *pvcController) createDeployment( command = append(command, "--secure-port="+AksPVCControllerSecurePort) } + if min, err := pxutil.GetTLSMinVersion(cluster); err != nil { + return err + } else if min != "" { + command = append(command, "--tls-min-version="+min) + } + + if cs, err := pxutil.GetTLSCipherSuites(cluster); err != nil { + return err + } else if cs != "" { + command = append(command, "--tls-cipher-suites="+cs) + } + existingDeployment := &appsv1.Deployment{} err = c.k8sClient.Get( context.TODO(), diff --git a/drivers/storage/portworx/util/util.go b/drivers/storage/portworx/util/util.go index 1fa1a9d8f..f52c5ccfe 100644 --- a/drivers/storage/portworx/util/util.go +++ b/drivers/storage/portworx/util/util.go @@ -2,6 +2,7 @@ package util import ( "context" + "crypto/tls" "crypto/x509" "encoding/base64" "fmt" @@ -148,6 +149,10 @@ const ( AnnotationSCCPriority = pxAnnotationPrefix + "/scc-priority" // AnnotationFACDTopology is added when FACD topology was successfully installed on a *new* cluster (it's blocked for existing clusters) AnnotationFACDTopology = pxAnnotationPrefix + "/facd-topology" + // AnnotationServerTLSMinVersion sets up TLS-servers w/ requested TLS as minimal version + AnnotationServerTLSMinVersion = pxAnnotationPrefix + "/tls-min-version" + // AnnotationServerTLSCipherSuites sets up TLS-servers w/ requested cipher suites + AnnotationServerTLSCipherSuites = pxAnnotationPrefix + "/tls-cipher-suites" // EnvKeyPXImage key for the environment variable that specifies Portworx image EnvKeyPXImage = "PX_IMAGE" @@ -1471,3 +1476,63 @@ func SetTelemetryCertOwnerRef( return k8sClient.Update(context.TODO(), secret) } + +// GetTLSMinVersion gets requested TLS version and validates it +func GetTLSMinVersion(cluster *corev1.StorageCluster) (string, error) { + req := cluster.Annotations[AnnotationServerTLSMinVersion] + if req == "" { + return "", nil + } + req = strings.Trim(req, " \t") + + switch strings.ToUpper(req) { + case "VERSIONTLS10": + return "VersionTLS10", nil + case "VERSIONTLS11": + return "VersionTLS11", nil + case "VERSIONTLS12": + return "VersionTLS12", nil + case "VERSIONTLS13": + return "VersionTLS13", nil + } + + return "", fmt.Errorf("invalid TLS version: expected one of VersionTLS1{0..3}, got %s", req) +} + +// GetTLSCipherSuites gets requested TLS ciphers suites and validates it +// - RETURN: the normalized comma-separated list of cipher suites, or error if requested unknown cipher +func GetTLSCipherSuites(cluster *corev1.StorageCluster) (string, error) { + req := cluster.Annotations[AnnotationServerTLSCipherSuites] + if req == "" { + return "", nil + } + + csMap := make(map[string]bool) + for _, c := range tls.CipherSuites() { + csMap[c.Name] = true + } + icsMap := make(map[string]bool) + for _, c := range tls.InsecureCipherSuites() { + icsMap[c.Name] = true + } + + req = strings.ToUpper(req) + parts := strings.FieldsFunc(req, func(r rune) bool { + return r == ' ' || r == '\t' || r == ':' || r == ';' || r == ',' + }) + outList := make([]string, 0, len(parts)) + + for _, p := range parts { + if p == "" { + // nop.. + } else if _, has := csMap[p]; has { + outList = append(outList, p) + } else if _, has = icsMap[p]; has { + logrus.Warnf("Requested insecure cipher suite %s", p) + outList = append(outList, p) + } else { + return "", fmt.Errorf("unknown cipher suite %s", p) + } + } + return strings.Join(outList, ","), nil +} diff --git a/drivers/storage/portworx/util/util_test.go b/drivers/storage/portworx/util/util_test.go index ba980357c..3f24ed547 100644 --- a/drivers/storage/portworx/util/util_test.go +++ b/drivers/storage/portworx/util/util_test.go @@ -481,3 +481,100 @@ func stringPtr(val string) *string { func boolPtr(val bool) *bool { return &val } + +func TestGetTLSMinVersion(t *testing.T) { + cluster := &corev1.StorageCluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: "px-cluster", + Namespace: "kube-system", + Annotations: make(map[string]string), + }, + Spec: corev1.StorageClusterSpec{}, + } + + ret, err := GetTLSMinVersion(cluster) + assert.NoError(t, err) + assert.Empty(t, ret) + + data := []struct { + input string + expectOut string + expectErr bool + }{ + {"", "", false}, + {"VersionTLS10", "VersionTLS10", false}, + {" VersionTLS10", "VersionTLS10", false}, + {"VersionTLS10\t", "VersionTLS10", false}, + {" VersionTLS11 ", "VersionTLS11", false}, + {"VersionTLS12", "VersionTLS12", false}, + {"VersionTLS13", "VersionTLS13", false}, + {"versiontls13", "VersionTLS13", false}, + {"VERSIONTLS13", "VersionTLS13", false}, + {"VersionTLS14", "", true}, + {"VersionTLS3", "", true}, + } + for i, td := range data { + cluster.ObjectMeta.Annotations[AnnotationServerTLSMinVersion] = td.input + ret, err = GetTLSMinVersion(cluster) + if td.expectErr { + assert.Error(t, err, "Expecting error for #%d / %v", i+1, td) + } else { + assert.NoError(t, err, "Expecting NO error for #%d / %v", i+1, td) + assert.Equal(t, td.expectOut, ret, "Unexpected result for #%d / %v", i+1, td) + } + } +} + +func TestGetTLSCipherSuites(t *testing.T) { + cluster := &corev1.StorageCluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: "px-cluster", + Namespace: "kube-system", + Annotations: make(map[string]string), + }, + Spec: corev1.StorageClusterSpec{}, + } + + ret, err := GetTLSCipherSuites(cluster) + assert.NoError(t, err) + assert.Empty(t, ret) + + data := []struct { + input string + expectOut string + expectErr bool + }{ + {"", "", false}, + { + "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", + false, + }, + { + "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", + false, + }, + { + " TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 :: TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 \t", + "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", + false, + }, + { + "tls_ecdhe_ecdsa_with_aes_256_gcm_sha384 tls_ecdhe_rsa_with_aes_256_gcm_sha384 tls_ecdhe_rsa_with_aes_128_gcm_sha256", + "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", + false, + }, + {"x,y", "", true}, + } + for i, td := range data { + cluster.ObjectMeta.Annotations[AnnotationServerTLSCipherSuites] = td.input + ret, err = GetTLSCipherSuites(cluster) + if td.expectErr { + assert.Error(t, err, "Expecting error for #%d / %v", i+1, td) + } else { + assert.NoError(t, err, "Expecting NO error for #%d / %v", i+1, td) + assert.Equal(t, td.expectOut, ret, "Unexpected result for #%d / %v", i+1, td) + } + } +}