From be34a10b81c5ddbc7b9ef86ac97fd59bf9dac1d0 Mon Sep 17 00:00:00 2001 From: THIERRY SALLE Date: Wed, 12 Apr 2017 11:25:55 +0200 Subject: [PATCH] Added TLS configuration for Slack service to allow sending alerting to self hosted Mattermost --- CHANGELOG.md | 1 + integrations/streamer_test.go | 5 ++- server/server.go | 12 ++++-- server/server_test.go | 72 +++++++++++++++++++++-------------- services/slack/config.go | 9 +++++ services/slack/service.go | 71 ++++++++++++++++++++++++++++++++-- 6 files changed, 135 insertions(+), 35 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 29fe84946..9736569f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,6 +42,7 @@ kapacitor define-handler system aggregate_by_1m.yaml ### Features +- [#1322](https://github.com/influxdata/kapacitor/pull/1322): TLS configuration in Slack service for Mattermost compatibility - [#1159](https://github.com/influxdata/kapacitor/pulls/1159): Go version 1.7.4 -> 1.7.5 - [#1175](https://github.com/influxdata/kapacitor/pull/1175): BREAKING: Add generic error counters to every node type. Renamed `query_errors` to `errors` in batch node. diff --git a/integrations/streamer_test.go b/integrations/streamer_test.go index acdcd9d60..b361dbad0 100644 --- a/integrations/streamer_test.go +++ b/integrations/streamer_test.go @@ -6737,7 +6737,10 @@ stream c.Enabled = true c.URL = ts.URL + "/test/slack/url" c.Channel = "#channel" - sl := slack.NewService(c, logService.NewLogger("[test_slack] ", log.LstdFlags)) + sl, err := slack.NewService(c, logService.NewLogger("[test_slack] ", log.LstdFlags)) + if err != nil { + t.Error(err) + } tm.SlackService = sl } testStreamerNoOutput(t, "TestStream_Alert", script, 13*time.Second, tmInit) diff --git a/server/server.go b/server/server.go index cbec2dbe5..2ca38f6d3 100644 --- a/server/server.go +++ b/server/server.go @@ -190,7 +190,9 @@ func New(c *Config, buildInfo BuildInfo, logService logging.Interface) (*Server, s.appendPushoverService() s.appendSMTPService() s.appendTelegramService() - s.appendSlackService() + if err := s.appendSlackService(); err != nil { + return nil, errors.Wrap(err, "slack service") + } s.appendSNMPTrapService() s.appendSensuService() s.appendTalkService() @@ -472,16 +474,20 @@ func (s *Server) appendSensuService() { s.AppendService("sensu", srv) } -func (s *Server) appendSlackService() { +func (s *Server) appendSlackService() error { c := s.config.Slack l := s.LogService.NewLogger("[slack] ", log.LstdFlags) - srv := slack.NewService(c, l) + srv, err := slack.NewService(c, l) + if err != nil { + return err + } s.TaskMaster.SlackService = srv s.AlertService.SlackService = srv s.SetDynamicService("slack", srv) s.AppendService("slack", srv) + return nil } func (s *Server) appendSNMPTrapService() { diff --git a/server/server_test.go b/server/server_test.go index 997577823..153c262de 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -6037,13 +6037,17 @@ func TestServer_UpdateConfig(t *testing.T) { Elements: []client.ConfigElement{{ Link: client.Link{Relation: client.Self, Href: "/kapacitor/v1/config/slack/"}, Options: map[string]interface{}{ - "channel": "", - "enabled": false, - "global": true, - "icon-emoji": "", - "state-changes-only": false, - "url": false, - "username": "kapacitor", + "channel": "", + "enabled": false, + "global": true, + "icon-emoji": "", + "state-changes-only": false, + "url": false, + "username": "kapacitor", + "ssl-ca": "", + "ssl-cert": "", + "ssl-key": "", + "insecure-skip-verify": false, }, Redacted: []string{ "url", @@ -6053,13 +6057,17 @@ func TestServer_UpdateConfig(t *testing.T) { expDefaultElement: client.ConfigElement{ Link: client.Link{Relation: client.Self, Href: "/kapacitor/v1/config/slack/"}, Options: map[string]interface{}{ - "channel": "", - "enabled": false, - "global": true, - "icon-emoji": "", - "state-changes-only": false, - "url": false, - "username": "kapacitor", + "channel": "", + "enabled": false, + "global": true, + "icon-emoji": "", + "state-changes-only": false, + "url": false, + "username": "kapacitor", + "ssl-ca": "", + "ssl-cert": "", + "ssl-key": "", + "insecure-skip-verify": false, }, Redacted: []string{ "url", @@ -6080,13 +6088,17 @@ func TestServer_UpdateConfig(t *testing.T) { Elements: []client.ConfigElement{{ Link: client.Link{Relation: client.Self, Href: "/kapacitor/v1/config/slack/"}, Options: map[string]interface{}{ - "channel": "#general", - "enabled": true, - "global": false, - "icon-emoji": "", - "state-changes-only": false, - "url": true, - "username": "kapacitor", + "channel": "#general", + "enabled": true, + "global": false, + "icon-emoji": "", + "state-changes-only": false, + "url": true, + "username": "kapacitor", + "ssl-ca": "", + "ssl-cert": "", + "ssl-key": "", + "insecure-skip-verify": false, }, Redacted: []string{ "url", @@ -6096,13 +6108,17 @@ func TestServer_UpdateConfig(t *testing.T) { expElement: client.ConfigElement{ Link: client.Link{Relation: client.Self, Href: "/kapacitor/v1/config/slack/"}, Options: map[string]interface{}{ - "channel": "#general", - "enabled": true, - "global": false, - "icon-emoji": "", - "state-changes-only": false, - "url": true, - "username": "kapacitor", + "channel": "#general", + "enabled": true, + "global": false, + "icon-emoji": "", + "state-changes-only": false, + "url": true, + "username": "kapacitor", + "ssl-ca": "", + "ssl-cert": "", + "ssl-key": "", + "insecure-skip-verify": false, }, Redacted: []string{ "url", diff --git a/services/slack/config.go b/services/slack/config.go index e258f3420..82225e79f 100644 --- a/services/slack/config.go +++ b/services/slack/config.go @@ -26,6 +26,15 @@ type Config struct { // Whether all alerts should automatically use stateChangesOnly mode. // Only applies if global is also set. StateChangesOnly bool `toml:"state-changes-only" override:"state-changes-only"` + + // Path to CA file + SSLCA string `toml:"ssl-ca" override:"ssl-ca"` + // Path to host cert file + SSLCert string `toml:"ssl-cert" override:"ssl-cert"` + // Path to cert key file + SSLKey string `toml:"ssl-key" override:"ssl-key"` + // Use SSL but skip chain & host verification + InsecureSkipVerify bool `toml:"insecure-skip-verify" override:"insecure-skip-verify"` } func NewConfig() Config { diff --git a/services/slack/service.go b/services/slack/service.go index 672cc5f7d..6d2e5be44 100644 --- a/services/slack/service.go +++ b/services/slack/service.go @@ -2,6 +2,8 @@ package slack import ( "bytes" + "crypto/tls" + "crypto/x509" "encoding/json" "fmt" "io" @@ -16,15 +18,29 @@ import ( type Service struct { configValue atomic.Value + clientValue atomic.Value logger *log.Logger + client *http.Client } -func NewService(c Config, l *log.Logger) *Service { +func NewService(c Config, l *log.Logger) (*Service, error) { + tlsConfig, err := getTLSConfig(c.SSLCA, c.SSLCert, c.SSLKey, c.InsecureSkipVerify) + if err != nil { + return nil, err + } + if tlsConfig.InsecureSkipVerify { + l.Println("W! Slack service is configured to skip ssl verification") + } s := &Service{ logger: l, } s.configValue.Store(c) - return s + s.clientValue.Store(&http.Client{ + Transport: &http.Transport{ + TLSClientConfig: tlsConfig, + }, + }) + return s, nil } func (s *Service) Open() error { @@ -46,7 +62,19 @@ func (s *Service) Update(newConfig []interface{}) error { if c, ok := newConfig[0].(Config); !ok { return fmt.Errorf("expected config object to be of type %T, got %T", c, newConfig[0]) } else { + tlsConfig, err := getTLSConfig(c.SSLCA, c.SSLCert, c.SSLKey, c.InsecureSkipVerify) + if err != nil { + return err + } + if tlsConfig.InsecureSkipVerify { + s.logger.Println("W! Slack service is configured to skip ssl verification") + } s.configValue.Store(c) + s.clientValue.Store(&http.Client{ + Transport: &http.Transport{ + TLSClientConfig: tlsConfig, + }, + }) } return nil } @@ -99,7 +127,8 @@ func (s *Service) Alert(channel, message, username, iconEmoji string, level aler if err != nil { return err } - resp, err := http.Post(url, "application/json", post) + client := s.clientValue.Load().(*http.Client) + resp, err := client.Post(url, "application/json", post) if err != nil { return err } @@ -210,3 +239,39 @@ func (h *handler) Handle(event alert.Event) { h.logger.Println("E! failed to send event to Slack", err) } } + +// getTLSConfig creates a tls.Config object from the given certs, key, and CA files. +// you must give the full path to the files. +func getTLSConfig( + SSLCA, SSLCert, SSLKey string, + InsecureSkipVerify bool, +) (*tls.Config, error) { + t := &tls.Config{ + InsecureSkipVerify: InsecureSkipVerify, + } + if SSLCert != "" && SSLKey != "" { + cert, err := tls.LoadX509KeyPair(SSLCert, SSLKey) + if err != nil { + return nil, fmt.Errorf( + "Could not load TLS client key/certificate: %s", + err) + } + t.Certificates = []tls.Certificate{cert} + } else if SSLCert != "" { + return nil, errors.New("Must provide both key and cert files: only cert file provided.") + } else if SSLKey != "" { + return nil, errors.New("Must provide both key and cert files: only key file provided.") + } + + if SSLCA != "" { + caCert, err := ioutil.ReadFile(SSLCA) + if err != nil { + return nil, fmt.Errorf("Could not load TLS CA: %s", + err) + } + caCertPool := x509.NewCertPool() + caCertPool.AppendCertsFromPEM(caCert) + t.RootCAs = caCertPool + } + return t, nil +}