diff --git a/CHANGELOG.md b/CHANGELOG.md index 2bb2f1598..6cd06ad38 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Features - [#2154](https://github.com/influxdata/kapacitor/pull/2154): Add ability to skip ssl verification with an alert post node. Thanks @itsHabib! +- [#2193](https://github.com/influxdata/kapacitor/issues/2193): Add TLS configuration options. ### Bugfixes diff --git a/etc/kapacitor/kapacitor.conf b/etc/kapacitor/kapacitor.conf index 0f6f1fc04..20401bcfb 100644 --- a/etc/kapacitor/kapacitor.conf +++ b/etc/kapacitor/kapacitor.conf @@ -29,6 +29,24 @@ default-retention-policy = "" ### Use a separate private key location. # https-private-key = "" +[tls] + # Determines the available set of cipher suites. See https://golang.org/pkg/crypto/tls/#pkg-constants + # for a list of available ciphers, which depends on the version of Go (use the query + # SHOW DIAGNOSTICS to see the version of Go used to build Kapacitor). If not specified, uses + # the default settings from Go's crypto/tls package. + # ciphers = [ + # "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305", + # "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", + # ] + + # Minimum version of the tls protocol that will be negotiated. If not specified, uses the + # default settings from Go's crypto/tls package. + # min-version = "tls1.2" + + # Maximum version of the tls protocol that will be negotiated. If not specified, uses the + # default settings from Go's crypto/tls package. + # max-version = "tls1.2" + [config-override] # Enable/Disable the service for overridding configuration via the HTTP API. enabled = true diff --git a/integrations/helpers_test.go b/integrations/helpers_test.go index 282c2c600..d7b73a0fd 100644 --- a/integrations/helpers_test.go +++ b/integrations/helpers_test.go @@ -1,6 +1,7 @@ package integrations import ( + "crypto/tls" "errors" "fmt" "net/http" @@ -23,7 +24,7 @@ func newHTTPDService() *httpd.Service { config := httpd.NewConfig() config.BindAddress = ":0" // Choose port dynamically config.LogEnabled = testing.Verbose() - httpService := httpd.NewService(config, "localhost", diagService.NewHTTPDHandler()) + httpService := httpd.NewService(config, "localhost", new(tls.Config), diagService.NewHTTPDHandler()) err := httpService.Open() if err != nil { panic(err) diff --git a/server/config.go b/server/config.go index 11578a1c1..36a2f6a58 100644 --- a/server/config.go +++ b/server/config.go @@ -57,6 +57,7 @@ import ( "github.com/influxdata/kapacitor/services/udf" "github.com/influxdata/kapacitor/services/udp" "github.com/influxdata/kapacitor/services/victorops" + "github.com/influxdata/kapacitor/tlsconfig" "github.com/pkg/errors" "github.com/influxdata/influxdb/services/collectd" @@ -75,6 +76,7 @@ type Config struct { InfluxDB []influxdb.Config `toml:"influxdb" override:"influxdb,element-key=name"` Logging diagnostic.Config `toml:"logging"` ConfigOverride config.Config `toml:"config-override"` + TLS tlsconfig.Config `toml:"tls"` // Input services Graphite []graphite.Config `toml:"graphite"` @@ -147,6 +149,7 @@ func NewConfig() *Config { c.InfluxDB = []influxdb.Config{influxdb.NewConfig()} c.Logging = diagnostic.NewConfig() c.ConfigOverride = config.NewConfig() + c.TLS = tlsconfig.NewConfig() c.Collectd = collectd.NewConfig() c.OpenTSDB = opentsdb.NewConfig() @@ -222,6 +225,9 @@ func (c *Config) Validate() error { if err := c.Task.Validate(); err != nil { return errors.Wrap(err, "task") } + if err := c.TLS.Validate(); err != nil { + return errors.Wrap(err, "tls") + } if err := c.Load.Validate(); err != nil { return err } diff --git a/server/server.go b/server/server.go index f77982706..b5879d3e3 100644 --- a/server/server.go +++ b/server/server.go @@ -2,6 +2,7 @@ package server import ( + "crypto/tls" "fmt" "io/ioutil" "os" @@ -98,7 +99,8 @@ type Server struct { dataDir string hostname string - config *Config + config *Config + tlsConfig *tls.Config err chan error @@ -158,9 +160,18 @@ func New(c *Config, buildInfo BuildInfo, diagService *diagnostic.Service) (*Serv if err != nil { return nil, fmt.Errorf("invalid configuration: %s. To generate a valid configuration file run `kapacitord config > kapacitor.generated.conf`.", err) } + // Setup base TLS config used for the Kapacitor API + tlsConfig, err := c.TLS.Parse() + if err != nil { + return nil, errors.Wrap(err, "tls configuration") + } + if tlsConfig == nil { + tlsConfig = new(tls.Config) + } d := diagService.NewServerHandler() s := &Server{ config: c, + tlsConfig: tlsConfig, BuildInfo: buildInfo, dataDir: c.DataDir, hostname: c.Hostname, @@ -448,7 +459,7 @@ func (s *Server) appendInfluxDBService() error { func (s *Server) initHTTPDService() { d := s.DiagService.NewHTTPDHandler() - srv := httpd.NewService(s.config.HTTP, s.hostname, d) + srv := httpd.NewService(s.config.HTTP, s.hostname, s.tlsConfig, d) srv.LocalHandler.PointsWriter = s.TaskMaster srv.Handler.PointsWriter = s.TaskMaster diff --git a/services/httpd/service.go b/services/httpd/service.go index 1e502dbd8..ac2c20fed 100644 --- a/services/httpd/service.go +++ b/services/httpd/service.go @@ -58,12 +58,13 @@ type Diagnostic interface { } type Service struct { - ln net.Listener - addr string - https bool - cert string - key string - err chan error + ln net.Listener + addr string + https bool + cert string + tlsConfig *tls.Config + key string + err chan error externalURL string @@ -87,7 +88,7 @@ type Service struct { httpServerErrorLogger *log.Logger } -func NewService(c Config, hostname string, d Diagnostic) *Service { +func NewService(c Config, hostname string, t *tls.Config, d Diagnostic) *Service { statMap := &expvar.Map{} statMap.Init() @@ -108,6 +109,7 @@ func NewService(c Config, hostname string, d Diagnostic) *Service { key: c.HTTPSPrivateKey, externalURL: u.String(), err: make(chan error, 1), + tlsConfig: t, shutdownTimeout: time.Duration(c.ShutdownTimeout), Handler: NewHandler( c.AuthEnabled, @@ -153,9 +155,9 @@ func (s *Service) Open() error { return err } - listener, err := tls.Listen("tcp", s.addr, &tls.Config{ - Certificates: []tls.Certificate{cert}, - }) + tlsConfig := s.tlsConfig.Clone() + tlsConfig.Certificates = []tls.Certificate{cert} + listener, err := tls.Listen("tcp", s.addr, tlsConfig) if err != nil { return err } diff --git a/tlsconfig/config.go b/tlsconfig/config.go index 5b0812b1a..cda4f18ba 100644 --- a/tlsconfig/config.go +++ b/tlsconfig/config.go @@ -6,6 +6,8 @@ import ( "errors" "fmt" "io/ioutil" + "sort" + "strings" ) // Create creates a new tls.Config object from the given certs, key, and CA files. @@ -42,3 +44,123 @@ func Create( } return t, nil } + +type Config struct { + Ciphers []string `toml:"ciphers"` + MinVersion string `toml:"min-version"` + MaxVersion string `toml:"max-version"` +} + +func NewConfig() Config { + return Config{} +} + +func (c Config) Validate() error { + _, err := c.Parse() + return err +} + +func (c Config) Parse() (out *tls.Config, err error) { + if len(c.Ciphers) > 0 { + if out == nil { + out = new(tls.Config) + } + + for _, name := range c.Ciphers { + cipher, ok := ciphersMap[strings.ToUpper(name)] + if !ok { + return nil, unknownCipher(name) + } + out.CipherSuites = append(out.CipherSuites, cipher) + } + } + + if c.MinVersion != "" { + if out == nil { + out = new(tls.Config) + } + + version, ok := versionsMap[strings.ToUpper(c.MinVersion)] + if !ok { + return nil, unknownVersion(c.MinVersion) + } + out.MinVersion = version + } + + if c.MaxVersion != "" { + if out == nil { + out = new(tls.Config) + } + + version, ok := versionsMap[strings.ToUpper(c.MaxVersion)] + if !ok { + return nil, unknownVersion(c.MaxVersion) + } + out.MaxVersion = version + } + + return out, nil +} + +var ciphersMap = map[string]uint16{ + "TLS_RSA_WITH_RC4_128_SHA": tls.TLS_RSA_WITH_RC4_128_SHA, + "TLS_RSA_WITH_3DES_EDE_CBC_SHA": tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA, + "TLS_RSA_WITH_AES_128_CBC_SHA": tls.TLS_RSA_WITH_AES_128_CBC_SHA, + "TLS_RSA_WITH_AES_256_CBC_SHA": tls.TLS_RSA_WITH_AES_256_CBC_SHA, + "TLS_RSA_WITH_AES_128_CBC_SHA256": tls.TLS_RSA_WITH_AES_128_CBC_SHA256, + "TLS_RSA_WITH_AES_128_GCM_SHA256": tls.TLS_RSA_WITH_AES_128_GCM_SHA256, + "TLS_RSA_WITH_AES_256_GCM_SHA384": tls.TLS_RSA_WITH_AES_256_GCM_SHA384, + "TLS_ECDHE_ECDSA_WITH_RC4_128_SHA": tls.TLS_ECDHE_ECDSA_WITH_RC4_128_SHA, + "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA": tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, + "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA": tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, + "TLS_ECDHE_RSA_WITH_RC4_128_SHA": tls.TLS_ECDHE_RSA_WITH_RC4_128_SHA, + "TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA": tls.TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA, + "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA": tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, + "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA": tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, + "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256": tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256, + "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256": tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256, + "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256": tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256": tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384": tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384": tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, + "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305": tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305, + "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305": tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, +} + +func unknownCipher(name string) error { + available := make([]string, 0, len(ciphersMap)) + for name := range ciphersMap { + available = append(available, name) + } + sort.Strings(available) + + return fmt.Errorf("unknown cipher suite: %q. available ciphers: %s", + name, strings.Join(available, ", ")) +} + +var versionsMap = map[string]uint16{ + "SSL3.0": tls.VersionSSL30, + "TLS1.0": tls.VersionTLS10, + "1.0": tls.VersionTLS10, + "TLS1.1": tls.VersionTLS11, + "1.1": tls.VersionTLS11, + "TLS1.2": tls.VersionTLS12, + "1.2": tls.VersionTLS12, +} + +func unknownVersion(name string) error { + available := make([]string, 0, len(versionsMap)) + for name := range versionsMap { + // skip the ones that just begin with a number. they may be confusing + // due to the duplication, and just help if the user specifies without + // the TLS part. + if name[0] == '1' { + continue + } + available = append(available, name) + } + sort.Strings(available) + + return fmt.Errorf("unknown tls version: %q. available versions: %s", + name, strings.Join(available, ", ")) +}