From def0032c7d6b60da6b3a343d94fc977d4e095a68 Mon Sep 17 00:00:00 2001 From: Dmytro Kozlov Date: Sat, 16 Apr 2022 15:07:07 +0300 Subject: [PATCH] lib/httpserver: added tlsCipherSuites flag (#2468) * lib/httpserver: added tlsCipherSuites flag * lib/httpserver: compare lower case strings * lib/httpserver: use EqualFold * lib/httpserver: used flagutil.NewArray, supported only strings cipher suites * lib/httpserver: updated flag description, added flag to documentation * Update lib/httpserver/httpserver.go Co-authored-by: Aliaksandr Valialkin --- README.md | 2 + lib/httpserver/httpserver.go | 34 ++++++++++++-- lib/httpserver/httpserver_test.go | 78 +++++++++++++++++++++++++++++++ 3 files changed, 111 insertions(+), 3 deletions(-) create mode 100644 lib/httpserver/httpserver_test.go diff --git a/README.md b/README.md index e3cbf9d6e..eeb767936 100644 --- a/README.md +++ b/README.md @@ -1923,6 +1923,8 @@ Pass `-help` to VictoriaMetrics in order to see the list of supported command-li Path to file with TLS certificate. Used only if -tls is set. Prefer ECDSA certs instead of RSA certs as RSA certs are slower. The provided certificate file is automatically re-read every second, so it can be dynamically updated -tlsKeyFile string Path to file with TLS key. Used only if -tls is set. The provided key file is automatically re-read every second, so it can be dynamically updated + -tlsCipherSuites + Cipher suites names for TLS encryption. For example, TLS_RSA_WITH_AES_128_CBC_SHA,TLS_RSA_WITH_AES_256_CBC_SHA. Used only if -tls flag is set -version Show VictoriaMetrics version ``` diff --git a/lib/httpserver/httpserver.go b/lib/httpserver/httpserver.go index 18a54fd6c..f343ef724 100644 --- a/lib/httpserver/httpserver.go +++ b/lib/httpserver/httpserver.go @@ -30,9 +30,10 @@ import ( ) var ( - tlsEnable = flag.Bool("tls", false, "Whether to enable TLS (aka HTTPS) for incoming requests. -tlsCertFile and -tlsKeyFile must be set if -tls is set") - tlsCertFile = flag.String("tlsCertFile", "", "Path to file with TLS certificate. Used only if -tls is set. Prefer ECDSA certs instead of RSA certs as RSA certs are slower. The provided certificate file is automatically re-read every second, so it can be dynamically updated") - tlsKeyFile = flag.String("tlsKeyFile", "", "Path to file with TLS key. Used only if -tls is set. The provided key file is automatically re-read every second, so it can be dynamically updated") + tlsEnable = flag.Bool("tls", false, "Whether to enable TLS (aka HTTPS) for incoming requests. -tlsCertFile and -tlsKeyFile must be set if -tls is set") + tlsCertFile = flag.String("tlsCertFile", "", "Path to file with TLS certificate. Used only if -tls is set. Prefer ECDSA certs instead of RSA certs as RSA certs are slower. The provided certificate file is automatically re-read every second, so it can be dynamically updated") + tlsKeyFile = flag.String("tlsKeyFile", "", "Path to file with TLS key. Used only if -tls is set. The provided key file is automatically re-read every second, so it can be dynamically updated") + tlsCipherSuites = flagutil.NewArray("tlsCipherSuites", "Cipher suites names for TLS encryption. For example, TLS_RSA_WITH_AES_128_CBC_SHA,TLS_RSA_WITH_AES_256_CBC_SHA. Used only if -tls flag is set") pathPrefix = flag.String("http.pathPrefix", "", "An optional prefix to add to all the paths handled by http server. For example, if '-http.pathPrefix=/foo/bar' is set, "+ "then all the http requests will be handled on '/foo/bar/*' paths. This may be useful for proxied requests. "+ @@ -100,10 +101,18 @@ func Serve(addr string, rh RequestHandler) { var certLock sync.Mutex var certDeadline uint64 var cert *tls.Certificate + var cipherSuites []uint16 c, err := tls.LoadX509KeyPair(*tlsCertFile, *tlsKeyFile) if err != nil { logger.Fatalf("cannot load TLS cert from tlsCertFile=%q, tlsKeyFile=%q: %s", *tlsCertFile, *tlsKeyFile, err) } + if len(*tlsCipherSuites) != 0 { + collectedCipherSuites, err := collectCipherSuites(*tlsCipherSuites) + if err != nil { + logger.Fatalf("cannot use TLS cipher suites from tlsCipherSuites=%q: %s", *tlsCipherSuites, err) + } + cipherSuites = collectedCipherSuites + } cert = &c cfg := &tls.Config{ MinVersion: tls.VersionTLS12, @@ -121,6 +130,7 @@ func Serve(addr string, rh RequestHandler) { } return cert, nil }, + CipherSuites: cipherSuites, } ln = tls.NewListener(ln, cfg) } @@ -687,3 +697,21 @@ func GetRequestURI(r *http.Request) string { } return requestURI + delimiter + queryArgs } + +func collectCipherSuites(definedCipherSuites []string) ([]uint16, error) { + var cipherSuites []uint16 + + supportedCipherSuites := tls.CipherSuites() + supportedCipherSuitesMap := make(map[string]uint16, len(supportedCipherSuites)) + for _, scf := range supportedCipherSuites { + supportedCipherSuitesMap[strings.ToLower(scf.Name)] = scf.ID + } + for _, gotSuite := range definedCipherSuites { + id, ok := supportedCipherSuitesMap[strings.ToLower(gotSuite)] + if !ok { + return nil, fmt.Errorf("got unsupported cipher suite name: %s", gotSuite) + } + cipherSuites = append(cipherSuites, id) + } + return cipherSuites, nil +} diff --git a/lib/httpserver/httpserver_test.go b/lib/httpserver/httpserver_test.go new file mode 100644 index 000000000..b87ec49f1 --- /dev/null +++ b/lib/httpserver/httpserver_test.go @@ -0,0 +1,78 @@ +package httpserver + +import ( + "reflect" + "testing" +) + +func Test_validateCipherSuites(t *testing.T) { + type args struct { + definedCipherSuites []string + } + tests := []struct { + name string + args args + want []uint16 + wantErr bool + }{ + { + name: "empty cipher suites", + args: args{definedCipherSuites: []string{}}, + want: nil, + }, + { + name: "got wrong string", + args: args{definedCipherSuites: []string{"word"}}, + want: nil, + wantErr: true, + }, + { + name: "got wrong number", + args: args{definedCipherSuites: []string{"123"}}, + want: nil, + wantErr: true, + }, + { + name: "got correct string cipher suite", + args: args{definedCipherSuites: []string{"TLS_RSA_WITH_AES_128_CBC_SHA", "TLS_RSA_WITH_AES_256_CBC_SHA"}}, + want: []uint16{0x2f, 0x35}, + wantErr: false, + }, + { + name: "got correct string with different cases (upper and lower) cipher suite", + args: args{definedCipherSuites: []string{"tls_rsa_with_aes_128_cbc_sha", "TLS_RSA_WITH_AES_256_CBC_SHA"}}, + want: []uint16{0x2f, 0x35}, + wantErr: false, + }, + { + name: "got correct number cipher suite", + args: args{definedCipherSuites: []string{"0x2f", "0x35"}}, + want: nil, + wantErr: true, + }, + { + name: "got insecure number cipher suite", + args: args{definedCipherSuites: []string{"0x0005", "0x000a"}}, + want: nil, + wantErr: true, + }, + { + name: "got insecure string cipher suite", + args: args{definedCipherSuites: []string{"TLS_ECDHE_ECDSA_WITH_RC4_128_SHA", "TLS_ECDHE_RSA_WITH_RC4_128_SHA"}}, + want: nil, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := collectCipherSuites(tt.args.definedCipherSuites) + if (err != nil) != tt.wantErr { + t.Errorf("collectCipherSuites() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("validateCipherSuites() got = %v, want %v", got, tt.want) + } + }) + } +}