From 454aa94ccd04b3a4097a69e3e3d6966fe5c196f2 Mon Sep 17 00:00:00 2001 From: Alexander Akhmetov Date: Thu, 22 Aug 2024 19:53:45 +0200 Subject: [PATCH 1/7] Add TLS, QoS and retain options to the MQTT receiver --- receivers/mqtt/client.go | 22 +++++++++++++-- receivers/mqtt/client_test.go | 22 +++++++++++++-- receivers/mqtt/config.go | 35 +++++++++++++++++------- receivers/mqtt/mqtt.go | 50 +++++++++++++++++++++++++++++++---- receivers/mqtt/mqtt_test.go | 39 +++++++++++++++++++++------ receivers/mqtt/testing.go | 12 +++++++-- 6 files changed, 151 insertions(+), 29 deletions(-) diff --git a/receivers/mqtt/client.go b/receivers/mqtt/client.go index 8730eef8..500aca6d 100644 --- a/receivers/mqtt/client.go +++ b/receivers/mqtt/client.go @@ -4,6 +4,7 @@ import ( "context" "crypto/tls" "errors" + "fmt" "time" mqttLib "github.com/at-wat/mqtt-go" @@ -63,10 +64,27 @@ func (c *mqttClient) Publish(ctx context.Context, message message) error { return errors.New("failed to publish: client is not connected to the broker") } + var mqttQoS mqttLib.QoS + var err error + switch message.qos { + case 0: + mqttQoS = mqttLib.QoS0 + case 1: + mqttQoS = mqttLib.QoS1 + case 2: + mqttQoS = mqttLib.QoS2 + default: + err = fmt.Errorf("failed to publish: invalid QoS level %d", message.qos) + } + + if err != nil { + return err + } + return c.client.Publish(ctx, &mqttLib.Message{ Topic: message.topic, - QoS: mqttLib.QoS0, - Retain: false, + QoS: mqttQoS, + Retain: message.retain, Payload: message.payload, }) } diff --git a/receivers/mqtt/client_test.go b/receivers/mqtt/client_test.go index 8335cfb6..af4e1e07 100644 --- a/receivers/mqtt/client_test.go +++ b/receivers/mqtt/client_test.go @@ -58,11 +58,15 @@ func TestMqttClientPublish(t *testing.T) { name string topic string payload []byte + retain bool + qos int }{ { name: "Simple publish", topic: "test", payload: []byte("test"), + retain: true, + qos: 1, }, } @@ -73,17 +77,31 @@ func TestMqttClientPublish(t *testing.T) { client: mc, } + var expectedQoS mqttLib.QoS + switch tc.qos { + case 0: + expectedQoS = mqttLib.QoS0 + case 1: + expectedQoS = mqttLib.QoS1 + case 2: + expectedQoS = mqttLib.QoS2 + default: + require.Fail(t, "invalid QoS level") + } + ctx := context.Background() mc.On("Publish", ctx, &mqttLib.Message{ Topic: tc.topic, Payload: tc.payload, - QoS: mqttLib.QoS0, - Retain: false, + QoS: expectedQoS, + Retain: tc.retain, }).Return(nil) err := c.Publish(ctx, message{ topic: tc.topic, payload: tc.payload, + retain: tc.retain, + qos: tc.qos, }) require.NoError(t, err) diff --git a/receivers/mqtt/config.go b/receivers/mqtt/config.go index a16816e7..b88c804f 100644 --- a/receivers/mqtt/config.go +++ b/receivers/mqtt/config.go @@ -16,14 +16,19 @@ const ( ) type Config struct { - BrokerURL string `json:"brokerUrl,omitempty" yaml:"brokerUrl,omitempty"` - ClientID string `json:"clientId,omitempty" yaml:"clientId,omitempty"` - Topic string `json:"topic,omitempty" yaml:"topic,omitempty"` - Message string `json:"message,omitempty" yaml:"message,omitempty"` - MessageFormat string `json:"messageFormat,omitempty" yaml:"messageFormat,omitempty"` - Username string `json:"username,omitempty" yaml:"username,omitempty"` - Password string `json:"password,omitempty" yaml:"password,omitempty"` - InsecureSkipVerify bool `json:"insecureSkipVerify,omitempty" yaml:"insecureSkipVerify,omitempty"` + BrokerURL string `json:"brokerUrl,omitempty" yaml:"brokerUrl,omitempty"` + ClientID string `json:"clientId,omitempty" yaml:"clientId,omitempty"` + Topic string `json:"topic,omitempty" yaml:"topic,omitempty"` + Message string `json:"message,omitempty" yaml:"message,omitempty"` + MessageFormat string `json:"messageFormat,omitempty" yaml:"messageFormat,omitempty"` + Username string `json:"username,omitempty" yaml:"username,omitempty"` + Password string `json:"password,omitempty" yaml:"password,omitempty"` + QoS receivers.OptionalNumber `json:"qos,omitempty" yaml:"qos,omitempty"` + InsecureSkipVerify bool `json:"insecureSkipVerify,omitempty" yaml:"insecureSkipVerify,omitempty"` + Retain bool `json:"retain,omitempty" yaml:"retain,omitempty"` + TLSCACertificate string `json:"tlsCACertificate,omitempty" yaml:"tlsCACertificate,omitempty"` + TLSClientCertificate string `json:"tlsClientCertificate,omitempty" yaml:"tlsClientCertificate,omitempty"` + TLSClientKey string `json:"tlsClientKey,omitempty" yaml:"tlsClientKey,omitempty"` } func NewConfig(jsonData json.RawMessage, decryptFn receivers.DecryptFunc) (Config, error) { @@ -56,8 +61,18 @@ func NewConfig(jsonData json.RawMessage, decryptFn receivers.DecryptFunc) (Confi return Config{}, errors.New("Invalid message format, must be 'json' or 'text'") } - password := decryptFn("password", settings.Password) - settings.Password = password + qos, err := settings.QoS.Int64() + if err != nil { + return Config{}, fmt.Errorf("Failed to parse QoS: %w", err) + } + if qos < 0 || qos > 2 { + return Config{}, fmt.Errorf("Invalid QoS level: %d. Must be 0, 1 or 2", qos) + } + + settings.Password = decryptFn("password", settings.Password) + settings.TLSCACertificate = decryptFn("tlsCACertificate", settings.TLSCACertificate) + settings.TLSClientCertificate = decryptFn("tlsClientCertificate", settings.TLSClientCertificate) + settings.TLSClientKey = decryptFn("tlsClientKey", settings.TLSClientKey) return settings, nil } diff --git a/receivers/mqtt/mqtt.go b/receivers/mqtt/mqtt.go index 26144a80..143788f9 100644 --- a/receivers/mqtt/mqtt.go +++ b/receivers/mqtt/mqtt.go @@ -3,9 +3,11 @@ package mqtt import ( "context" "crypto/tls" + "crypto/x509" "encoding/json" "errors" "fmt" + "net/url" "github.com/prometheus/alertmanager/notify" "github.com/prometheus/alertmanager/types" @@ -24,6 +26,8 @@ type client interface { type message struct { topic string payload []byte + retain bool + qos int } type Notifier struct { @@ -67,11 +71,10 @@ func (n *Notifier) Notify(ctx context.Context, as ...*types.Alert) (bool, error) return false, err } - var tlsCfg *tls.Config - if n.settings.InsecureSkipVerify { - tlsCfg = &tls.Config{ - InsecureSkipVerify: true, - } + tlsCfg, err := n.buildTLSConfig() + if err != nil { + n.log.Error("Failed to build TLS config", "error", err.Error()) + return false, fmt.Errorf("failed to build TLS config: %s", err.Error()) } err = n.client.Connect(ctx, n.settings.BrokerURL, n.settings.ClientID, n.settings.Username, n.settings.Password, tlsCfg) @@ -86,11 +89,19 @@ func (n *Notifier) Notify(ctx context.Context, as ...*types.Alert) (bool, error) } }() + qos, err := n.settings.QoS.Int64() + if err != nil { + n.log.Error("Failed to parse QoS", "error", err.Error()) + return false, fmt.Errorf("Failed to parse QoS: %s", err.Error()) + } + err = n.client.Publish( ctx, message{ topic: n.settings.Topic, payload: []byte(msg), + retain: n.settings.Retain, + qos: int(qos), }, ) @@ -102,6 +113,35 @@ func (n *Notifier) Notify(ctx context.Context, as ...*types.Alert) (bool, error) return true, nil } +func (n *Notifier) buildTLSConfig() (*tls.Config, error) { + parsedURL, err := url.Parse(n.settings.BrokerURL) + if err != nil { + n.log.Error("Failed to parse broker URL", "error", err.Error()) + return nil, err + } + + tlsCfg := &tls.Config{ + InsecureSkipVerify: n.settings.InsecureSkipVerify, + ServerName: parsedURL.Hostname(), + } + + if n.settings.TLSCACertificate != "" { + tlsCfg.RootCAs = x509.NewCertPool() + tlsCfg.RootCAs.AppendCertsFromPEM([]byte(n.settings.TLSCACertificate)) + } + + if n.settings.TLSClientCertificate != "" || n.settings.TLSClientKey != "" { + cert, err := tls.X509KeyPair([]byte(n.settings.TLSClientCertificate), []byte(n.settings.TLSClientKey)) + if err != nil { + n.log.Error("Failed to load client certificate", "error", err.Error()) + return nil, err + } + tlsCfg.Certificates = append(tlsCfg.Certificates, cert) + } + + return tlsCfg, nil +} + func (n *Notifier) buildMessage(ctx context.Context, as ...*types.Alert) (string, error) { groupKey, err := notify.ExtractGroupKey(ctx) if err != nil { diff --git a/receivers/mqtt/mqtt_test.go b/receivers/mqtt/mqtt_test.go index dd4b4bac..6bb925ed 100644 --- a/receivers/mqtt/mqtt_test.go +++ b/receivers/mqtt/mqtt_test.go @@ -94,6 +94,34 @@ func TestNotify(t *testing.T) { expMessage: message{ topic: "alert1", payload: []byte("{\"receiver\":\"\",\"status\":\"firing\",\"alerts\":[{\"status\":\"firing\",\"labels\":{\"alertname\":\"alert1\",\"lbl1\":\"val1\"},\"annotations\":{\"ann1\":\"annv1\"},\"startsAt\":\"0001-01-01T00:00:00Z\",\"endsAt\":\"0001-01-01T00:00:00Z\",\"generatorURL\":\"a URL\",\"fingerprint\":\"fac0861a85de433a\",\"silenceURL\":\"http://localhost/base/alerting/silence/new?alertmanager=grafana\\u0026matcher=alertname%3Dalert1\\u0026matcher=lbl1%3Dval1\",\"dashboardURL\":\"http://localhost/base/d/abcd\",\"panelURL\":\"http://localhost/base/d/abcd?viewPanel=efgh\",\"values\":null,\"valueString\":\"\"}],\"groupLabels\":{\"alertname\":\"\"},\"commonLabels\":{\"alertname\":\"alert1\",\"lbl1\":\"val1\"},\"commonAnnotations\":{\"ann1\":\"annv1\"},\"externalURL\":\"http://localhost/base\",\"version\":\"1\",\"groupKey\":\"alertname\",\"message\":\"**Firing**\\n\\nValue: [no value]\\nLabels:\\n - alertname = alert1\\n - lbl1 = val1\\nAnnotations:\\n - ann1 = annv1\\nSource: a URL\\nSilence: http://localhost/base/alerting/silence/new?alertmanager=grafana\\u0026matcher=alertname%3Dalert1\\u0026matcher=lbl1%3Dval1\\nDashboard: http://localhost/base/d/abcd\\nPanel: http://localhost/base/d/abcd?viewPanel=efgh\\n\"}"), + retain: false, + qos: 0, + }, + expError: nil, + }, + { + name: "A single alert with the default template in JSON with retain and QoS", + settings: Config{ + Topic: "alert1", + Message: templates.DefaultMessageEmbed, + MessageFormat: MessageFormatJSON, + Retain: true, + QoS: "1", + }, + alerts: []*types.Alert{ + { + Alert: model.Alert{ + Labels: model.LabelSet{"alertname": "alert1", "lbl1": "val1"}, + Annotations: model.LabelSet{"ann1": "annv1", "__dashboardUid__": "abcd", "__panelId__": "efgh"}, + GeneratorURL: "a URL", + }, + }, + }, + expMessage: message{ + topic: "alert1", + payload: []byte("{\"receiver\":\"\",\"status\":\"firing\",\"alerts\":[{\"status\":\"firing\",\"labels\":{\"alertname\":\"alert1\",\"lbl1\":\"val1\"},\"annotations\":{\"ann1\":\"annv1\"},\"startsAt\":\"0001-01-01T00:00:00Z\",\"endsAt\":\"0001-01-01T00:00:00Z\",\"generatorURL\":\"a URL\",\"fingerprint\":\"fac0861a85de433a\",\"silenceURL\":\"http://localhost/base/alerting/silence/new?alertmanager=grafana\\u0026matcher=alertname%3Dalert1\\u0026matcher=lbl1%3Dval1\",\"dashboardURL\":\"http://localhost/base/d/abcd\",\"panelURL\":\"http://localhost/base/d/abcd?viewPanel=efgh\",\"values\":null,\"valueString\":\"\"}],\"groupLabels\":{\"alertname\":\"\"},\"commonLabels\":{\"alertname\":\"alert1\",\"lbl1\":\"val1\"},\"commonAnnotations\":{\"ann1\":\"annv1\"},\"externalURL\":\"http://localhost/base\",\"version\":\"1\",\"groupKey\":\"alertname\",\"message\":\"**Firing**\\n\\nValue: [no value]\\nLabels:\\n - alertname = alert1\\n - lbl1 = val1\\nAnnotations:\\n - ann1 = annv1\\nSource: a URL\\nSilence: http://localhost/base/alerting/silence/new?alertmanager=grafana\\u0026matcher=alertname%3Dalert1\\u0026matcher=lbl1%3Dval1\\nDashboard: http://localhost/base/d/abcd\\nPanel: http://localhost/base/d/abcd?viewPanel=efgh\\n\"}"), + retain: true, + qos: 1, }, expError: nil, }, @@ -247,7 +275,7 @@ func TestNotify(t *testing.T) { mock.Anything, ).Return(nil) mockMQTTClient.On("Disconnect", mock.Anything).Return(nil) - mockMQTTClient.On("Publish", mock.Anything, mock.Anything).Return(nil) + mockMQTTClient.On("Publish", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil) n := &Notifier{ Base: &receivers.Base{ @@ -275,17 +303,12 @@ func TestNotify(t *testing.T) { require.Equal(t, 1, len(mockMQTTClient.publishedMessages)) require.Equal(t, c.expMessage, mockMQTTClient.publishedMessages[0]) - require.Equal(t, c.expUsername, mockMQTTClient.username) require.Equal(t, c.expPassword, mockMQTTClient.password) require.Equal(t, c.settings.ClientID, mockMQTTClient.clientID) require.Equal(t, c.settings.BrokerURL, mockMQTTClient.brokerURL) - if c.settings.InsecureSkipVerify { - require.NotNil(t, mockMQTTClient.tlsCfg) - require.True(t, mockMQTTClient.tlsCfg.InsecureSkipVerify) - } else { - require.Nil(t, mockMQTTClient.tlsCfg) - } + require.NotNil(t, mockMQTTClient.tlsCfg) + require.Equal(t, mockMQTTClient.tlsCfg.InsecureSkipVerify, c.settings.InsecureSkipVerify) }) } } diff --git a/receivers/mqtt/testing.go b/receivers/mqtt/testing.go index 2b4c4926..716174ca 100644 --- a/receivers/mqtt/testing.go +++ b/receivers/mqtt/testing.go @@ -7,11 +7,19 @@ const FullValidConfigForTesting = `{ "messageFormat": "json", "clientId": "grafana-test-client-id", "username": "test-username", + "insecureSkipVerify": false, + "qos": 0, + "retain": false, "password": "test-password", - "insecureSkipVerify": false + "tlsCACertificate": "test-tls-ca-certificate", + "tlsClientCertificate": "test-tls-client-certificate", + "tlsClientKey": "test-tls-client-key" }` // FullValidSecretsForTesting is a string representation of JSON object that contains all fields that can be overridden from secrets const FullValidSecretsForTesting = `{ - "password": "test-password" + "password": "test-password", + "tlsCACertificate": "test-tls-ca-certificate", + "tlsClientCertificate": "test-tls-client-certificate", + "tlsClientKey": "test-tls-client-key" }` From 605e05ca54668b3b7f15275e97151c499c75da87 Mon Sep 17 00:00:00 2001 From: Alexander Akhmetov Date: Thu, 29 Aug 2024 18:50:43 +0200 Subject: [PATCH 2/7] Use nested tls configuration struct --- receivers/mqtt/config.go | 32 ++++++------ receivers/mqtt/config_test.go | 15 +++--- receivers/mqtt/mqtt.go | 16 +++--- receivers/mqtt/mqtt_test.go | 94 +++++++++++++++++++++++++++++++---- receivers/util.go | 7 +++ 5 files changed, 127 insertions(+), 37 deletions(-) diff --git a/receivers/mqtt/config.go b/receivers/mqtt/config.go index b88c804f..a9ea65a1 100644 --- a/receivers/mqtt/config.go +++ b/receivers/mqtt/config.go @@ -16,19 +16,16 @@ const ( ) type Config struct { - BrokerURL string `json:"brokerUrl,omitempty" yaml:"brokerUrl,omitempty"` - ClientID string `json:"clientId,omitempty" yaml:"clientId,omitempty"` - Topic string `json:"topic,omitempty" yaml:"topic,omitempty"` - Message string `json:"message,omitempty" yaml:"message,omitempty"` - MessageFormat string `json:"messageFormat,omitempty" yaml:"messageFormat,omitempty"` - Username string `json:"username,omitempty" yaml:"username,omitempty"` - Password string `json:"password,omitempty" yaml:"password,omitempty"` - QoS receivers.OptionalNumber `json:"qos,omitempty" yaml:"qos,omitempty"` - InsecureSkipVerify bool `json:"insecureSkipVerify,omitempty" yaml:"insecureSkipVerify,omitempty"` - Retain bool `json:"retain,omitempty" yaml:"retain,omitempty"` - TLSCACertificate string `json:"tlsCACertificate,omitempty" yaml:"tlsCACertificate,omitempty"` - TLSClientCertificate string `json:"tlsClientCertificate,omitempty" yaml:"tlsClientCertificate,omitempty"` - TLSClientKey string `json:"tlsClientKey,omitempty" yaml:"tlsClientKey,omitempty"` + BrokerURL string `json:"brokerUrl,omitempty" yaml:"brokerUrl,omitempty"` + ClientID string `json:"clientId,omitempty" yaml:"clientId,omitempty"` + Topic string `json:"topic,omitempty" yaml:"topic,omitempty"` + Message string `json:"message,omitempty" yaml:"message,omitempty"` + MessageFormat string `json:"messageFormat,omitempty" yaml:"messageFormat,omitempty"` + Username string `json:"username,omitempty" yaml:"username,omitempty"` + Password string `json:"password,omitempty" yaml:"password,omitempty"` + QoS receivers.OptionalNumber `json:"qos,omitempty" yaml:"qos,omitempty"` + Retain bool `json:"retain,omitempty" yaml:"retain,omitempty"` + TLS *receivers.TLSConfig `json:"tls,omitempty" yaml:"tls,omitempty"` } func NewConfig(jsonData json.RawMessage, decryptFn receivers.DecryptFunc) (Config, error) { @@ -70,9 +67,12 @@ func NewConfig(jsonData json.RawMessage, decryptFn receivers.DecryptFunc) (Confi } settings.Password = decryptFn("password", settings.Password) - settings.TLSCACertificate = decryptFn("tlsCACertificate", settings.TLSCACertificate) - settings.TLSClientCertificate = decryptFn("tlsClientCertificate", settings.TLSClientCertificate) - settings.TLSClientKey = decryptFn("tlsClientKey", settings.TLSClientKey) + + if settings.TLS != nil { + settings.TLS.CACertificate = decryptFn("tlsCACertificate", settings.TLS.CACertificate) + settings.TLS.ClientCertificate = decryptFn("tlsClientCertificate", settings.TLS.ClientCertificate) + settings.TLS.ClientKey = decryptFn("tlsClientKey", settings.TLS.ClientKey) + } return settings, nil } diff --git a/receivers/mqtt/config_test.go b/receivers/mqtt/config_test.go index 83e1381c..8e15db9d 100644 --- a/receivers/mqtt/config_test.go +++ b/receivers/mqtt/config_test.go @@ -6,6 +6,7 @@ import ( "github.com/stretchr/testify/require" + "github.com/grafana/alerting/receivers" receiversTesting "github.com/grafana/alerting/receivers/testing" "github.com/grafana/alerting/templates" ) @@ -50,13 +51,15 @@ func TestNewConfig(t *testing.T) { }, { name: "Configuration with insecureSkipVerify", - settings: `{ "brokerUrl" : "tcp://localhost:1883", "topic": "grafana/alerts", "insecureSkipVerify": true}`, + settings: `{ "brokerUrl" : "tcp://localhost:1883", "topic": "grafana/alerts", "tls": {"insecureSkipVerify": true}}`, expectedConfig: Config{ - Message: templates.DefaultMessageEmbed, - BrokerURL: "tcp://localhost:1883", - Topic: "grafana/alerts", - MessageFormat: MessageFormatJSON, - InsecureSkipVerify: true, + Message: templates.DefaultMessageEmbed, + BrokerURL: "tcp://localhost:1883", + Topic: "grafana/alerts", + MessageFormat: MessageFormatJSON, + TLS: &receivers.TLSConfig{ + InsecureSkipVerify: true, + }, }, }, { diff --git a/receivers/mqtt/mqtt.go b/receivers/mqtt/mqtt.go index 143788f9..e4f86eba 100644 --- a/receivers/mqtt/mqtt.go +++ b/receivers/mqtt/mqtt.go @@ -63,7 +63,7 @@ type mqttMessage struct { } func (n *Notifier) Notify(ctx context.Context, as ...*types.Alert) (bool, error) { - n.log.Debug("Sending an MQTT message") + n.log.Debug("Sending an MQTT message", "topic", n.settings.Topic, "qos", n.settings.QoS, "retain", n.settings.Retain) msg, err := n.buildMessage(ctx, as...) if err != nil { @@ -114,6 +114,10 @@ func (n *Notifier) Notify(ctx context.Context, as ...*types.Alert) (bool, error) } func (n *Notifier) buildTLSConfig() (*tls.Config, error) { + if n.settings.TLS == nil { + return nil, nil + } + parsedURL, err := url.Parse(n.settings.BrokerURL) if err != nil { n.log.Error("Failed to parse broker URL", "error", err.Error()) @@ -121,17 +125,17 @@ func (n *Notifier) buildTLSConfig() (*tls.Config, error) { } tlsCfg := &tls.Config{ - InsecureSkipVerify: n.settings.InsecureSkipVerify, + InsecureSkipVerify: n.settings.TLS.InsecureSkipVerify, ServerName: parsedURL.Hostname(), } - if n.settings.TLSCACertificate != "" { + if n.settings.TLS.CACertificate != "" { tlsCfg.RootCAs = x509.NewCertPool() - tlsCfg.RootCAs.AppendCertsFromPEM([]byte(n.settings.TLSCACertificate)) + tlsCfg.RootCAs.AppendCertsFromPEM([]byte(n.settings.TLS.CACertificate)) } - if n.settings.TLSClientCertificate != "" || n.settings.TLSClientKey != "" { - cert, err := tls.X509KeyPair([]byte(n.settings.TLSClientCertificate), []byte(n.settings.TLSClientKey)) + if n.settings.TLS.ClientCertificate != "" || n.settings.TLS.ClientKey != "" { + cert, err := tls.X509KeyPair([]byte(n.settings.TLS.ClientCertificate), []byte(n.settings.TLS.ClientKey)) if err != nil { n.log.Error("Failed to load client certificate", "error", err.Error()) return nil, err diff --git a/receivers/mqtt/mqtt_test.go b/receivers/mqtt/mqtt_test.go index 6bb925ed..9c033466 100644 --- a/receivers/mqtt/mqtt_test.go +++ b/receivers/mqtt/mqtt_test.go @@ -3,6 +3,7 @@ package mqtt import ( "context" "crypto/tls" + "crypto/x509" "net/url" "testing" @@ -18,6 +19,32 @@ import ( "github.com/grafana/alerting/templates" ) +// Test certificates from https://github.com/golang/go/blob/4f852b9734249c063928b34a02dd689e03a8ab2c/src/crypto/tls/tls_test.go#L34 +const ( + testRsaCertPem = `-----BEGIN CERTIFICATE----- +MIIB0zCCAX2gAwIBAgIJAI/M7BYjwB+uMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV +BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX +aWRnaXRzIFB0eSBMdGQwHhcNMTIwOTEyMjE1MjAyWhcNMTUwOTEyMjE1MjAyWjBF +MQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50 +ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBANLJ +hPHhITqQbPklG3ibCVxwGMRfp/v4XqhfdQHdcVfHap6NQ5Wok/4xIA+ui35/MmNa +rtNuC+BdZ1tMuVCPFZcCAwEAAaNQME4wHQYDVR0OBBYEFJvKs8RfJaXTH08W+SGv +zQyKn0H8MB8GA1UdIwQYMBaAFJvKs8RfJaXTH08W+SGvzQyKn0H8MAwGA1UdEwQF +MAMBAf8wDQYJKoZIhvcNAQEFBQADQQBJlffJHybjDGxRMqaRmDhX0+6v02TUKZsW +r5QuVbpQhH6u+0UgcW0jp9QwpxoPTLTWGXEWBBBurxFwiCBhkQ+V +-----END CERTIFICATE-----` + + testRsaKeyPem = `-----BEGIN RSA PRIVATE KEY----- +MIIBOwIBAAJBANLJhPHhITqQbPklG3ibCVxwGMRfp/v4XqhfdQHdcVfHap6NQ5Wo +k/4xIA+ui35/MmNartNuC+BdZ1tMuVCPFZcCAwEAAQJAEJ2N+zsR0Xn8/Q6twa4G +6OB1M1WO+k+ztnX/1SvNeWu8D6GImtupLTYgjZcHufykj09jiHmjHx8u8ZZB/o1N +MQIhAPW+eyZo7ay3lMz1V01WVjNKK9QSn1MJlb06h/LuYv9FAiEA25WPedKgVyCW +SmUwbPw8fnTcpqDWE3yTO3vKcebqMSsCIBF3UmVue8YU3jybC3NxuXq3wNm34R8T +xVLHwDXh/6NJAiEAl2oHGGLz64BuAfjKrqwz7qMYr9HCLIe/YsoWq/olzScCIQDi +D2lWusoe2/nEqfDVVWGWlyJ7yOmqaVm/iNUN9B2N2g== +-----END RSA PRIVATE KEY-----` +) + type mockMQTTClient struct { mock.Mock publishedMessages []message @@ -260,6 +287,34 @@ func TestNotify(t *testing.T) { expPassword: "pass", expError: nil, }, + { + name: "With TLS config", + settings: Config{ + Topic: "alert1", + Message: templates.DefaultMessageEmbed, + MessageFormat: MessageFormatJSON, + TLS: &receivers.TLSConfig{ + InsecureSkipVerify: true, + CACertificate: testRsaCertPem, + ClientCertificate: testRsaCertPem, + ClientKey: testRsaKeyPem, + }, + }, + alerts: []*types.Alert{ + { + Alert: model.Alert{ + Labels: model.LabelSet{"alertname": "alert1", "lbl1": "val1"}, + Annotations: model.LabelSet{"ann1": "annv1", "__dashboardUid__": "abcd", "__panelId__": "efgh"}, + GeneratorURL: "a URL", + }, + }, + }, + expMessage: message{ + topic: "alert1", + payload: []byte("{\"receiver\":\"\",\"status\":\"firing\",\"alerts\":[{\"status\":\"firing\",\"labels\":{\"alertname\":\"alert1\",\"lbl1\":\"val1\"},\"annotations\":{\"ann1\":\"annv1\"},\"startsAt\":\"0001-01-01T00:00:00Z\",\"endsAt\":\"0001-01-01T00:00:00Z\",\"generatorURL\":\"a URL\",\"fingerprint\":\"fac0861a85de433a\",\"silenceURL\":\"http://localhost/base/alerting/silence/new?alertmanager=grafana\\u0026matcher=alertname%3Dalert1\\u0026matcher=lbl1%3Dval1\",\"dashboardURL\":\"http://localhost/base/d/abcd\",\"panelURL\":\"http://localhost/base/d/abcd?viewPanel=efgh\",\"values\":null,\"valueString\":\"\"}],\"groupLabels\":{\"alertname\":\"\"},\"commonLabels\":{\"alertname\":\"alert1\",\"lbl1\":\"val1\"},\"commonAnnotations\":{\"ann1\":\"annv1\"},\"externalURL\":\"http://localhost/base\",\"version\":\"1\",\"groupKey\":\"alertname\",\"message\":\"**Firing**\\n\\nValue: [no value]\\nLabels:\\n - alertname = alert1\\n - lbl1 = val1\\nAnnotations:\\n - ann1 = annv1\\nSource: a URL\\nSilence: http://localhost/base/alerting/silence/new?alertmanager=grafana\\u0026matcher=alertname%3Dalert1\\u0026matcher=lbl1%3Dval1\\nDashboard: http://localhost/base/d/abcd\\nPanel: http://localhost/base/d/abcd?viewPanel=efgh\\n\"}"), + }, + expError: nil, + }, } for _, c := range cases { @@ -307,8 +362,27 @@ func TestNotify(t *testing.T) { require.Equal(t, c.expPassword, mockMQTTClient.password) require.Equal(t, c.settings.ClientID, mockMQTTClient.clientID) require.Equal(t, c.settings.BrokerURL, mockMQTTClient.brokerURL) - require.NotNil(t, mockMQTTClient.tlsCfg) - require.Equal(t, mockMQTTClient.tlsCfg.InsecureSkipVerify, c.settings.InsecureSkipVerify) + + if c.settings.TLS == nil { + require.Nil(t, mockMQTTClient.tlsCfg) + } else { + require.NotNil(t, mockMQTTClient.tlsCfg) + require.Equal(t, mockMQTTClient.tlsCfg.InsecureSkipVerify, c.settings.TLS.InsecureSkipVerify) + + // Check if the client certificate and key are set correctly. + if c.settings.TLS.ClientCertificate != "" && c.settings.TLS.ClientKey != "" { + clientCert, err := tls.X509KeyPair([]byte(c.settings.TLS.ClientCertificate), []byte(c.settings.TLS.ClientKey)) + require.NoError(t, err) + require.Equal(t, clientCert, mockMQTTClient.tlsCfg.Certificates[0]) + } + + // Check if the CA certificate is set correctly. + if c.settings.TLS.CACertificate != "" { + expectedRootCAs := x509.NewCertPool() + expectedRootCAs.AppendCertsFromPEM([]byte(c.settings.TLS.CACertificate)) + require.True(t, mockMQTTClient.tlsCfg.RootCAs.Equal(expectedRootCAs)) + } + } }) } } @@ -335,13 +409,15 @@ func TestNew(t *testing.T) { { name: "Configuration with insecureSkipVerify", cfg: Config{ - Topic: "alerts", - Message: templates.DefaultMessageEmbed, - Username: "user", - Password: "pass", - BrokerURL: "tcp://127.0.0.1:1883", - ClientID: "test-grafana", - InsecureSkipVerify: true, + Topic: "alerts", + Message: templates.DefaultMessageEmbed, + Username: "user", + Password: "pass", + BrokerURL: "tcp://127.0.0.1:1883", + ClientID: "test-grafana", + TLS: &receivers.TLSConfig{ + InsecureSkipVerify: true, + }, }, }, } diff --git a/receivers/util.go b/receivers/util.go index 2055949b..c369bc4d 100644 --- a/receivers/util.go +++ b/receivers/util.go @@ -41,6 +41,13 @@ type HTTPCfg struct { Password string } +type TLSConfig struct { + CACertificate string `json:"caCertificate,omitempty" yaml:"caCertificate,omitempty"` + ClientCertificate string `json:"clientCertificate,omitempty" yaml:"clientCertificate,omitempty"` + ClientKey string `json:"clientKey,omitempty" yaml:"clientKey,omitempty"` + InsecureSkipVerify bool `json:"insecureSkipVerify,omitempty" yaml:"insecureSkipVerify,omitempty"` +} + // SendHTTPRequest sends an HTTP request. // Stubbable by tests. // From 2a01a54af2f7911842d9d016c04bc75555b8af5d Mon Sep 17 00:00:00 2001 From: Alexander Akhmetov Date: Thu, 29 Aug 2024 19:16:59 +0200 Subject: [PATCH 3/7] Fix test data --- receivers/mqtt/testing.go | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/receivers/mqtt/testing.go b/receivers/mqtt/testing.go index 716174ca..95c624d9 100644 --- a/receivers/mqtt/testing.go +++ b/receivers/mqtt/testing.go @@ -7,19 +7,18 @@ const FullValidConfigForTesting = `{ "messageFormat": "json", "clientId": "grafana-test-client-id", "username": "test-username", - "insecureSkipVerify": false, - "qos": 0, + "qos": "0", "retain": false, "password": "test-password", - "tlsCACertificate": "test-tls-ca-certificate", - "tlsClientCertificate": "test-tls-client-certificate", - "tlsClientKey": "test-tls-client-key" + "tls": { + "insecureSkipVerify": false, + "caCertificate": "test-tls-ca-certificate", + "clientCertificate": "test-tls-client-certificate", + "clientKey": "test-tls-client-key" + } }` // FullValidSecretsForTesting is a string representation of JSON object that contains all fields that can be overridden from secrets const FullValidSecretsForTesting = `{ - "password": "test-password", - "tlsCACertificate": "test-tls-ca-certificate", - "tlsClientCertificate": "test-tls-client-certificate", - "tlsClientKey": "test-tls-client-key" + "password": "test-password" }` From aa466962ea18d147547fad80a2bd6944d7840e46 Mon Sep 17 00:00:00 2001 From: Alexander Akhmetov Date: Fri, 30 Aug 2024 19:23:37 +0200 Subject: [PATCH 4/7] Rename tls field to tlsConfig --- receivers/mqtt/config.go | 10 +++++----- receivers/mqtt/config_test.go | 4 ++-- receivers/mqtt/mqtt.go | 12 ++++++------ receivers/mqtt/mqtt_test.go | 16 ++++++++-------- receivers/mqtt/testing.go | 2 +- 5 files changed, 22 insertions(+), 22 deletions(-) diff --git a/receivers/mqtt/config.go b/receivers/mqtt/config.go index a9ea65a1..d5b89759 100644 --- a/receivers/mqtt/config.go +++ b/receivers/mqtt/config.go @@ -25,7 +25,7 @@ type Config struct { Password string `json:"password,omitempty" yaml:"password,omitempty"` QoS receivers.OptionalNumber `json:"qos,omitempty" yaml:"qos,omitempty"` Retain bool `json:"retain,omitempty" yaml:"retain,omitempty"` - TLS *receivers.TLSConfig `json:"tls,omitempty" yaml:"tls,omitempty"` + TLSConfig *receivers.TLSConfig `json:"tlsConfig,omitempty" yaml:"tlsConfig,omitempty"` } func NewConfig(jsonData json.RawMessage, decryptFn receivers.DecryptFunc) (Config, error) { @@ -68,10 +68,10 @@ func NewConfig(jsonData json.RawMessage, decryptFn receivers.DecryptFunc) (Confi settings.Password = decryptFn("password", settings.Password) - if settings.TLS != nil { - settings.TLS.CACertificate = decryptFn("tlsCACertificate", settings.TLS.CACertificate) - settings.TLS.ClientCertificate = decryptFn("tlsClientCertificate", settings.TLS.ClientCertificate) - settings.TLS.ClientKey = decryptFn("tlsClientKey", settings.TLS.ClientKey) + if settings.TLSConfig != nil { + settings.TLSConfig.CACertificate = decryptFn("tlsCACertificate", settings.TLSConfig.CACertificate) + settings.TLSConfig.ClientCertificate = decryptFn("tlsClientCertificate", settings.TLSConfig.ClientCertificate) + settings.TLSConfig.ClientKey = decryptFn("tlsClientKey", settings.TLSConfig.ClientKey) } return settings, nil diff --git a/receivers/mqtt/config_test.go b/receivers/mqtt/config_test.go index 8e15db9d..c0482715 100644 --- a/receivers/mqtt/config_test.go +++ b/receivers/mqtt/config_test.go @@ -51,13 +51,13 @@ func TestNewConfig(t *testing.T) { }, { name: "Configuration with insecureSkipVerify", - settings: `{ "brokerUrl" : "tcp://localhost:1883", "topic": "grafana/alerts", "tls": {"insecureSkipVerify": true}}`, + settings: `{ "brokerUrl" : "tcp://localhost:1883", "topic": "grafana/alerts", "tlsConfig": {"insecureSkipVerify": true}}`, expectedConfig: Config{ Message: templates.DefaultMessageEmbed, BrokerURL: "tcp://localhost:1883", Topic: "grafana/alerts", MessageFormat: MessageFormatJSON, - TLS: &receivers.TLSConfig{ + TLSConfig: &receivers.TLSConfig{ InsecureSkipVerify: true, }, }, diff --git a/receivers/mqtt/mqtt.go b/receivers/mqtt/mqtt.go index e4f86eba..6e351ab2 100644 --- a/receivers/mqtt/mqtt.go +++ b/receivers/mqtt/mqtt.go @@ -114,7 +114,7 @@ func (n *Notifier) Notify(ctx context.Context, as ...*types.Alert) (bool, error) } func (n *Notifier) buildTLSConfig() (*tls.Config, error) { - if n.settings.TLS == nil { + if n.settings.TLSConfig == nil { return nil, nil } @@ -125,17 +125,17 @@ func (n *Notifier) buildTLSConfig() (*tls.Config, error) { } tlsCfg := &tls.Config{ - InsecureSkipVerify: n.settings.TLS.InsecureSkipVerify, + InsecureSkipVerify: n.settings.TLSConfig.InsecureSkipVerify, ServerName: parsedURL.Hostname(), } - if n.settings.TLS.CACertificate != "" { + if n.settings.TLSConfig.CACertificate != "" { tlsCfg.RootCAs = x509.NewCertPool() - tlsCfg.RootCAs.AppendCertsFromPEM([]byte(n.settings.TLS.CACertificate)) + tlsCfg.RootCAs.AppendCertsFromPEM([]byte(n.settings.TLSConfig.CACertificate)) } - if n.settings.TLS.ClientCertificate != "" || n.settings.TLS.ClientKey != "" { - cert, err := tls.X509KeyPair([]byte(n.settings.TLS.ClientCertificate), []byte(n.settings.TLS.ClientKey)) + if n.settings.TLSConfig.ClientCertificate != "" || n.settings.TLSConfig.ClientKey != "" { + cert, err := tls.X509KeyPair([]byte(n.settings.TLSConfig.ClientCertificate), []byte(n.settings.TLSConfig.ClientKey)) if err != nil { n.log.Error("Failed to load client certificate", "error", err.Error()) return nil, err diff --git a/receivers/mqtt/mqtt_test.go b/receivers/mqtt/mqtt_test.go index 9c033466..921c62bf 100644 --- a/receivers/mqtt/mqtt_test.go +++ b/receivers/mqtt/mqtt_test.go @@ -293,7 +293,7 @@ func TestNotify(t *testing.T) { Topic: "alert1", Message: templates.DefaultMessageEmbed, MessageFormat: MessageFormatJSON, - TLS: &receivers.TLSConfig{ + TLSConfig: &receivers.TLSConfig{ InsecureSkipVerify: true, CACertificate: testRsaCertPem, ClientCertificate: testRsaCertPem, @@ -363,23 +363,23 @@ func TestNotify(t *testing.T) { require.Equal(t, c.settings.ClientID, mockMQTTClient.clientID) require.Equal(t, c.settings.BrokerURL, mockMQTTClient.brokerURL) - if c.settings.TLS == nil { + if c.settings.TLSConfig == nil { require.Nil(t, mockMQTTClient.tlsCfg) } else { require.NotNil(t, mockMQTTClient.tlsCfg) - require.Equal(t, mockMQTTClient.tlsCfg.InsecureSkipVerify, c.settings.TLS.InsecureSkipVerify) + require.Equal(t, mockMQTTClient.tlsCfg.InsecureSkipVerify, c.settings.TLSConfig.InsecureSkipVerify) // Check if the client certificate and key are set correctly. - if c.settings.TLS.ClientCertificate != "" && c.settings.TLS.ClientKey != "" { - clientCert, err := tls.X509KeyPair([]byte(c.settings.TLS.ClientCertificate), []byte(c.settings.TLS.ClientKey)) + if c.settings.TLSConfig.ClientCertificate != "" && c.settings.TLSConfig.ClientKey != "" { + clientCert, err := tls.X509KeyPair([]byte(c.settings.TLSConfig.ClientCertificate), []byte(c.settings.TLSConfig.ClientKey)) require.NoError(t, err) require.Equal(t, clientCert, mockMQTTClient.tlsCfg.Certificates[0]) } // Check if the CA certificate is set correctly. - if c.settings.TLS.CACertificate != "" { + if c.settings.TLSConfig.CACertificate != "" { expectedRootCAs := x509.NewCertPool() - expectedRootCAs.AppendCertsFromPEM([]byte(c.settings.TLS.CACertificate)) + expectedRootCAs.AppendCertsFromPEM([]byte(c.settings.TLSConfig.CACertificate)) require.True(t, mockMQTTClient.tlsCfg.RootCAs.Equal(expectedRootCAs)) } } @@ -415,7 +415,7 @@ func TestNew(t *testing.T) { Password: "pass", BrokerURL: "tcp://127.0.0.1:1883", ClientID: "test-grafana", - TLS: &receivers.TLSConfig{ + TLSConfig: &receivers.TLSConfig{ InsecureSkipVerify: true, }, }, diff --git a/receivers/mqtt/testing.go b/receivers/mqtt/testing.go index 95c624d9..a035723f 100644 --- a/receivers/mqtt/testing.go +++ b/receivers/mqtt/testing.go @@ -10,7 +10,7 @@ const FullValidConfigForTesting = `{ "qos": "0", "retain": false, "password": "test-password", - "tls": { + "tlsConfig": { "insecureSkipVerify": false, "caCertificate": "test-tls-ca-certificate", "clientCertificate": "test-tls-client-certificate", From b5db19e7f66bfb384d4bfae6324e7e18dd5ea3ba Mon Sep 17 00:00:00 2001 From: Alexander Akhmetov Date: Wed, 11 Sep 2024 18:23:01 +0200 Subject: [PATCH 5/7] Always parse tls secrets --- receivers/mqtt/config.go | 10 ++++++---- receivers/mqtt/config_test.go | 24 ++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/receivers/mqtt/config.go b/receivers/mqtt/config.go index d5b89759..4a2b237b 100644 --- a/receivers/mqtt/config.go +++ b/receivers/mqtt/config.go @@ -68,11 +68,13 @@ func NewConfig(jsonData json.RawMessage, decryptFn receivers.DecryptFunc) (Confi settings.Password = decryptFn("password", settings.Password) - if settings.TLSConfig != nil { - settings.TLSConfig.CACertificate = decryptFn("tlsCACertificate", settings.TLSConfig.CACertificate) - settings.TLSConfig.ClientCertificate = decryptFn("tlsClientCertificate", settings.TLSConfig.ClientCertificate) - settings.TLSConfig.ClientKey = decryptFn("tlsClientKey", settings.TLSConfig.ClientKey) + if settings.TLSConfig == nil { + settings.TLSConfig = &receivers.TLSConfig{} } + settings.TLSConfig.CACertificate = decryptFn("tlsConfig.caCertificate", settings.TLSConfig.CACertificate) + settings.TLSConfig.ClientCertificate = decryptFn("tlsConfig.clientCertificate", settings.TLSConfig.ClientCertificate) + settings.TLSConfig.ClientKey = decryptFn("tlsConfig.clientKey", settings.TLSConfig.ClientKey) + return settings, nil } diff --git a/receivers/mqtt/config_test.go b/receivers/mqtt/config_test.go index c0482715..9a986b9f 100644 --- a/receivers/mqtt/config_test.go +++ b/receivers/mqtt/config_test.go @@ -47,6 +47,7 @@ func TestNewConfig(t *testing.T) { BrokerURL: "tcp://localhost:1883", Topic: "grafana/alerts", MessageFormat: MessageFormatJSON, + TLSConfig: &receivers.TLSConfig{}, }, }, { @@ -71,6 +72,7 @@ func TestNewConfig(t *testing.T) { Topic: "grafana/alerts", MessageFormat: MessageFormatJSON, ClientID: "test-client-id", + TLSConfig: &receivers.TLSConfig{}, }, }, { @@ -86,6 +88,28 @@ func TestNewConfig(t *testing.T) { MessageFormat: MessageFormatJSON, Username: "grafana", Password: "testpasswd", + TLSConfig: &receivers.TLSConfig{}, + }, + }, + { + name: "Configuration with tlsConfig", + settings: `{ "brokerUrl" : "tcp://localhost:1883", "topic": "grafana/alerts"}`, + secureSettings: map[string][]byte{ + "tlsConfig.caCertificate": []byte("test-ca-cert"), + "tlsConfig.clientCertificate": []byte("test-client-cert"), + "tlsConfig.clientKey": []byte("test-client-key"), + }, + expectedConfig: Config{ + Message: templates.DefaultMessageEmbed, + BrokerURL: "tcp://localhost:1883", + Topic: "grafana/alerts", + MessageFormat: MessageFormatJSON, + TLSConfig: &receivers.TLSConfig{ + InsecureSkipVerify: false, + CACertificate: "test-ca-cert", + ClientKey: "test-client-key", + ClientCertificate: "test-client-cert", + }, }, }, } From c2f1eec6fc221f65d9270fffbf5be6e770bce91e Mon Sep 17 00:00:00 2001 From: Alexander Akhmetov Date: Thu, 12 Sep 2024 13:23:17 +0200 Subject: [PATCH 6/7] Move buildTLSConfig to receivers.TLSConfig.ToTLSConfig --- receivers/mqtt/config.go | 7 +++ receivers/mqtt/config_test.go | 14 ++++- receivers/mqtt/mqtt.go | 40 ++----------- receivers/util.go | 28 +++++++++ receivers/util_test.go | 108 ++++++++++++++++++++++++++++++++++ 5 files changed, 158 insertions(+), 39 deletions(-) create mode 100644 receivers/util_test.go diff --git a/receivers/mqtt/config.go b/receivers/mqtt/config.go index 4a2b237b..b2eff868 100644 --- a/receivers/mqtt/config.go +++ b/receivers/mqtt/config.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "math/rand" + "net/url" "github.com/grafana/alerting/receivers" "github.com/grafana/alerting/templates" @@ -76,5 +77,11 @@ func NewConfig(jsonData json.RawMessage, decryptFn receivers.DecryptFunc) (Confi settings.TLSConfig.ClientCertificate = decryptFn("tlsConfig.clientCertificate", settings.TLSConfig.ClientCertificate) settings.TLSConfig.ClientKey = decryptFn("tlsConfig.clientKey", settings.TLSConfig.ClientKey) + parsedURL, err := url.Parse(settings.BrokerURL) + if err != nil { + return Config{}, errors.New("Failed to parse broker URL") + } + settings.TLSConfig.ServerName = parsedURL.Hostname() + return settings, nil } diff --git a/receivers/mqtt/config_test.go b/receivers/mqtt/config_test.go index 9a986b9f..f3b48c5a 100644 --- a/receivers/mqtt/config_test.go +++ b/receivers/mqtt/config_test.go @@ -47,7 +47,9 @@ func TestNewConfig(t *testing.T) { BrokerURL: "tcp://localhost:1883", Topic: "grafana/alerts", MessageFormat: MessageFormatJSON, - TLSConfig: &receivers.TLSConfig{}, + TLSConfig: &receivers.TLSConfig{ + ServerName: "localhost", + }, }, }, { @@ -60,6 +62,7 @@ func TestNewConfig(t *testing.T) { MessageFormat: MessageFormatJSON, TLSConfig: &receivers.TLSConfig{ InsecureSkipVerify: true, + ServerName: "localhost", }, }, }, @@ -72,7 +75,9 @@ func TestNewConfig(t *testing.T) { Topic: "grafana/alerts", MessageFormat: MessageFormatJSON, ClientID: "test-client-id", - TLSConfig: &receivers.TLSConfig{}, + TLSConfig: &receivers.TLSConfig{ + ServerName: "localhost", + }, }, }, { @@ -88,7 +93,9 @@ func TestNewConfig(t *testing.T) { MessageFormat: MessageFormatJSON, Username: "grafana", Password: "testpasswd", - TLSConfig: &receivers.TLSConfig{}, + TLSConfig: &receivers.TLSConfig{ + ServerName: "localhost", + }, }, }, { @@ -106,6 +113,7 @@ func TestNewConfig(t *testing.T) { MessageFormat: MessageFormatJSON, TLSConfig: &receivers.TLSConfig{ InsecureSkipVerify: false, + ServerName: "localhost", CACertificate: "test-ca-cert", ClientKey: "test-client-key", ClientCertificate: "test-client-cert", diff --git a/receivers/mqtt/mqtt.go b/receivers/mqtt/mqtt.go index 6e351ab2..063eed82 100644 --- a/receivers/mqtt/mqtt.go +++ b/receivers/mqtt/mqtt.go @@ -3,11 +3,9 @@ package mqtt import ( "context" "crypto/tls" - "crypto/x509" "encoding/json" "errors" "fmt" - "net/url" "github.com/prometheus/alertmanager/notify" "github.com/prometheus/alertmanager/types" @@ -71,7 +69,10 @@ func (n *Notifier) Notify(ctx context.Context, as ...*types.Alert) (bool, error) return false, err } - tlsCfg, err := n.buildTLSConfig() + var tlsCfg *tls.Config + if n.settings.TLSConfig != nil { + tlsCfg, err = n.settings.TLSConfig.ToTLSConfig() + } if err != nil { n.log.Error("Failed to build TLS config", "error", err.Error()) return false, fmt.Errorf("failed to build TLS config: %s", err.Error()) @@ -113,39 +114,6 @@ func (n *Notifier) Notify(ctx context.Context, as ...*types.Alert) (bool, error) return true, nil } -func (n *Notifier) buildTLSConfig() (*tls.Config, error) { - if n.settings.TLSConfig == nil { - return nil, nil - } - - parsedURL, err := url.Parse(n.settings.BrokerURL) - if err != nil { - n.log.Error("Failed to parse broker URL", "error", err.Error()) - return nil, err - } - - tlsCfg := &tls.Config{ - InsecureSkipVerify: n.settings.TLSConfig.InsecureSkipVerify, - ServerName: parsedURL.Hostname(), - } - - if n.settings.TLSConfig.CACertificate != "" { - tlsCfg.RootCAs = x509.NewCertPool() - tlsCfg.RootCAs.AppendCertsFromPEM([]byte(n.settings.TLSConfig.CACertificate)) - } - - if n.settings.TLSConfig.ClientCertificate != "" || n.settings.TLSConfig.ClientKey != "" { - cert, err := tls.X509KeyPair([]byte(n.settings.TLSConfig.ClientCertificate), []byte(n.settings.TLSConfig.ClientKey)) - if err != nil { - n.log.Error("Failed to load client certificate", "error", err.Error()) - return nil, err - } - tlsCfg.Certificates = append(tlsCfg.Certificates, cert) - } - - return tlsCfg, nil -} - func (n *Notifier) buildMessage(ctx context.Context, as ...*types.Alert) (string, error) { groupKey, err := notify.ExtractGroupKey(ctx) if err != nil { diff --git a/receivers/util.go b/receivers/util.go index c369bc4d..61917bb6 100644 --- a/receivers/util.go +++ b/receivers/util.go @@ -4,6 +4,8 @@ import ( "bytes" "context" "crypto/tls" + "crypto/x509" + "errors" "fmt" "io" "net" @@ -46,6 +48,32 @@ type TLSConfig struct { ClientCertificate string `json:"clientCertificate,omitempty" yaml:"clientCertificate,omitempty"` ClientKey string `json:"clientKey,omitempty" yaml:"clientKey,omitempty"` InsecureSkipVerify bool `json:"insecureSkipVerify,omitempty" yaml:"insecureSkipVerify,omitempty"` + ServerName string +} + +func (cfg *TLSConfig) ToTLSConfig() (*tls.Config, error) { + tlsCfg := &tls.Config{ + InsecureSkipVerify: cfg.InsecureSkipVerify, + ServerName: cfg.ServerName, + } + + if cfg.CACertificate != "" { + tlsCfg.RootCAs = x509.NewCertPool() + ok := tlsCfg.RootCAs.AppendCertsFromPEM([]byte(cfg.CACertificate)) + if !ok { + return nil, errors.New("Unable to use the provided CA certificate") + } + } + + if cfg.ClientCertificate != "" || cfg.ClientKey != "" { + cert, err := tls.X509KeyPair([]byte(cfg.ClientCertificate), []byte(cfg.ClientKey)) + if err != nil { + return nil, fmt.Errorf("failed to load client certificate: %w", err) + } + tlsCfg.Certificates = append(tlsCfg.Certificates, cert) + } + + return tlsCfg, nil } // SendHTTPRequest sends an HTTP request. diff --git a/receivers/util_test.go b/receivers/util_test.go new file mode 100644 index 00000000..421b8a44 --- /dev/null +++ b/receivers/util_test.go @@ -0,0 +1,108 @@ +package receivers + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +// Test certificates from https://github.com/golang/go/blob/4f852b9734249c063928b34a02dd689e03a8ab2c/src/crypto/tls/tls_test.go#L34 +const ( + testRsaCertPem = `-----BEGIN CERTIFICATE----- +MIIB0zCCAX2gAwIBAgIJAI/M7BYjwB+uMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV +BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX +aWRnaXRzIFB0eSBMdGQwHhcNMTIwOTEyMjE1MjAyWhcNMTUwOTEyMjE1MjAyWjBF +MQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50 +ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBANLJ +hPHhITqQbPklG3ibCVxwGMRfp/v4XqhfdQHdcVfHap6NQ5Wok/4xIA+ui35/MmNa +rtNuC+BdZ1tMuVCPFZcCAwEAAaNQME4wHQYDVR0OBBYEFJvKs8RfJaXTH08W+SGv +zQyKn0H8MB8GA1UdIwQYMBaAFJvKs8RfJaXTH08W+SGvzQyKn0H8MAwGA1UdEwQF +MAMBAf8wDQYJKoZIhvcNAQEFBQADQQBJlffJHybjDGxRMqaRmDhX0+6v02TUKZsW +r5QuVbpQhH6u+0UgcW0jp9QwpxoPTLTWGXEWBBBurxFwiCBhkQ+V +-----END CERTIFICATE-----` + + testRsaKeyPem = `-----BEGIN RSA PRIVATE KEY----- +MIIBOwIBAAJBANLJhPHhITqQbPklG3ibCVxwGMRfp/v4XqhfdQHdcVfHap6NQ5Wo +k/4xIA+ui35/MmNartNuC+BdZ1tMuVCPFZcCAwEAAQJAEJ2N+zsR0Xn8/Q6twa4G +6OB1M1WO+k+ztnX/1SvNeWu8D6GImtupLTYgjZcHufykj09jiHmjHx8u8ZZB/o1N +MQIhAPW+eyZo7ay3lMz1V01WVjNKK9QSn1MJlb06h/LuYv9FAiEA25WPedKgVyCW +SmUwbPw8fnTcpqDWE3yTO3vKcebqMSsCIBF3UmVue8YU3jybC3NxuXq3wNm34R8T +xVLHwDXh/6NJAiEAl2oHGGLz64BuAfjKrqwz7qMYr9HCLIe/YsoWq/olzScCIQDi +D2lWusoe2/nEqfDVVWGWlyJ7yOmqaVm/iNUN9B2N2g== +-----END RSA PRIVATE KEY-----` +) + +func TestNewTLSConfig(t *testing.T) { + tests := []struct { + name string + cfg TLSConfig + expectError bool + }{ + { + name: "empty TLSConfig", + cfg: TLSConfig{}, + expectError: false, + }, + { + name: "valid CA certificate", + cfg: TLSConfig{ + CACertificate: string(testRsaCertPem), + }, + expectError: false, + }, + { + name: "invalid CA certificate", + cfg: TLSConfig{ + CACertificate: "invalid-cert", + }, + expectError: true, + }, + { + name: "valid client certificate and key", + cfg: TLSConfig{ + ClientCertificate: string(testRsaCertPem), + ClientKey: string(testRsaKeyPem), + }, + expectError: false, + }, + { + name: "invalid client certificate", + cfg: TLSConfig{ + ClientCertificate: string(testRsaCertPem), + }, + expectError: true, + }, + { + name: "set InsecureSkipVerify and ServerName", + cfg: TLSConfig{ + InsecureSkipVerify: true, + ServerName: "example.com", + }, + expectError: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tlsCfg, err := tt.cfg.ToTLSConfig() + + if tt.expectError { + require.Error(t, err) + require.Nil(t, tlsCfg) + } else { + require.NoError(t, err) + + require.Equal(t, tt.cfg.InsecureSkipVerify, tlsCfg.InsecureSkipVerify, "InsecureSkipVerify mismatch") + require.Equal(t, tt.cfg.ServerName, tlsCfg.ServerName, "ServerName mismatch") + + if tt.cfg.CACertificate != "" { + require.NotNil(t, tlsCfg.RootCAs, "expected RootCAs to be initialized, but it was nil") + } + + if tt.cfg.ClientCertificate != "" && tt.cfg.ClientKey != "" { + require.NotEmpty(t, tlsCfg.Certificates, "expected Certificates to be set, but it was empty") + } + } + }) + } +} From 6829670894200fc1e2f26aa6979dd3b305dddf45 Mon Sep 17 00:00:00 2001 From: Alexander Akhmetov Date: Thu, 12 Sep 2024 14:33:30 +0200 Subject: [PATCH 7/7] Rename ToTLSConfig to ToCryptoTLSConfig --- receivers/mqtt/mqtt.go | 2 +- receivers/util.go | 2 +- receivers/util_test.go | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/receivers/mqtt/mqtt.go b/receivers/mqtt/mqtt.go index 063eed82..29a21823 100644 --- a/receivers/mqtt/mqtt.go +++ b/receivers/mqtt/mqtt.go @@ -71,7 +71,7 @@ func (n *Notifier) Notify(ctx context.Context, as ...*types.Alert) (bool, error) var tlsCfg *tls.Config if n.settings.TLSConfig != nil { - tlsCfg, err = n.settings.TLSConfig.ToTLSConfig() + tlsCfg, err = n.settings.TLSConfig.ToCryptoTLSConfig() } if err != nil { n.log.Error("Failed to build TLS config", "error", err.Error()) diff --git a/receivers/util.go b/receivers/util.go index 61917bb6..cc04aa1f 100644 --- a/receivers/util.go +++ b/receivers/util.go @@ -51,7 +51,7 @@ type TLSConfig struct { ServerName string } -func (cfg *TLSConfig) ToTLSConfig() (*tls.Config, error) { +func (cfg *TLSConfig) ToCryptoTLSConfig() (*tls.Config, error) { tlsCfg := &tls.Config{ InsecureSkipVerify: cfg.InsecureSkipVerify, ServerName: cfg.ServerName, diff --git a/receivers/util_test.go b/receivers/util_test.go index 421b8a44..c0bf77e4 100644 --- a/receivers/util_test.go +++ b/receivers/util_test.go @@ -84,7 +84,7 @@ func TestNewTLSConfig(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - tlsCfg, err := tt.cfg.ToTLSConfig() + tlsCfg, err := tt.cfg.ToCryptoTLSConfig() if tt.expectError { require.Error(t, err)