From 8f3b685f1733f690830f5e367a03901b166902fa Mon Sep 17 00:00:00 2001
From: Philip Laine 
Date: Wed, 14 Apr 2021 23:20:19 +0200
Subject: [PATCH 1/4] Add self signed cert to provider
Signed-off-by: Philip Laine 
---
 api/v1beta1/provider_types.go                 |  5 +++
 api/v1beta1/zz_generated.deepcopy.go          |  5 +++
 ...ification.toolkit.fluxcd.io_providers.yaml | 10 +++++
 controllers/provider_controller.go            | 24 +++++++++++-
 docs/api/notification.md                      | 30 +++++++++++++++
 docs/spec/v1beta1/provider.md                 | 13 +++++++
 internal/notifier/azure_devops.go             | 12 +++++-
 internal/notifier/azure_devops_test.go        |  6 +--
 internal/notifier/bitbucket.go                | 18 ++++++++-
 internal/notifier/bitbucket_test.go           |  6 +--
 internal/notifier/client.go                   |  7 +++-
 internal/notifier/client_test.go              |  2 +-
 internal/notifier/discord.go                  |  2 +-
 internal/notifier/factory.go                  | 21 ++++++-----
 internal/notifier/forwarder.go                |  9 +++--
 internal/notifier/forwarder_test.go           |  5 ++-
 internal/notifier/github.go                   | 19 +++++++++-
 internal/notifier/github_test.go              |  8 ++--
 internal/notifier/gitlab.go                   | 18 +++++++--
 internal/notifier/gitlab_test.go              |  8 ++--
 internal/notifier/google_chat.go              |  2 +-
 internal/notifier/rocket.go                   |  7 +++-
 internal/notifier/rocket_test.go              |  2 +-
 internal/notifier/sentry.go                   | 11 +++++-
 internal/notifier/sentry_test.go              |  7 ++--
 internal/notifier/slack.go                    |  2 +-
 internal/notifier/teams.go                    |  2 +-
 internal/notifier/webex.go                    |  6 ++-
 internal/notifier/webex_test.go               |  4 +-
 internal/server/event_handlers.go             | 37 ++++++++++++++++++-
 30 files changed, 252 insertions(+), 56 deletions(-)
diff --git a/api/v1beta1/provider_types.go b/api/v1beta1/provider_types.go
index 1928047..1a00397 100644
--- a/api/v1beta1/provider_types.go
+++ b/api/v1beta1/provider_types.go
@@ -56,6 +56,11 @@ type ProviderSpec struct {
 	// using "address" as data key
 	// +optional
 	SecretRef *meta.LocalObjectReference `json:"secretRef,omitempty"`
+
+	// CertSecretRef can be given the name of a secret containing
+	// a PEM-encoded CA certificate (`caFile`)
+	// +optional
+	CertSecretRef *meta.LocalObjectReference `json:"certSecretRef,omitempty"`
 }
 
 const (
diff --git a/api/v1beta1/zz_generated.deepcopy.go b/api/v1beta1/zz_generated.deepcopy.go
index e0d004c..569925c 100644
--- a/api/v1beta1/zz_generated.deepcopy.go
+++ b/api/v1beta1/zz_generated.deepcopy.go
@@ -215,6 +215,11 @@ func (in *ProviderSpec) DeepCopyInto(out *ProviderSpec) {
 		*out = new(meta.LocalObjectReference)
 		**out = **in
 	}
+	if in.CertSecretRef != nil {
+		in, out := &in.CertSecretRef, &out.CertSecretRef
+		*out = new(meta.LocalObjectReference)
+		**out = **in
+	}
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProviderSpec.
diff --git a/config/crd/bases/notification.toolkit.fluxcd.io_providers.yaml b/config/crd/bases/notification.toolkit.fluxcd.io_providers.yaml
index 3bdbee4..fdf5c15 100644
--- a/config/crd/bases/notification.toolkit.fluxcd.io_providers.yaml
+++ b/config/crd/bases/notification.toolkit.fluxcd.io_providers.yaml
@@ -50,6 +50,16 @@ spec:
                 description: HTTP/S webhook address of this provider
                 pattern: ^(http|https)://
                 type: string
+              certSecretRef:
+                description: CertSecretRef can be given the name of a secret containing
+                  a PEM-encoded CA certificate (`caFile`)
+                properties:
+                  name:
+                    description: Name of the referent
+                    type: string
+                required:
+                - name
+                type: object
               channel:
                 description: Alert channel for this provider
                 type: string
diff --git a/controllers/provider_controller.go b/controllers/provider_controller.go
index 0a311dc..7a55f6a 100644
--- a/controllers/provider_controller.go
+++ b/controllers/provider_controller.go
@@ -18,6 +18,7 @@ package controllers
 
 import (
 	"context"
+	"crypto/x509"
 	"fmt"
 	"time"
 
@@ -121,7 +122,28 @@ func (r *ProviderReconciler) validate(ctx context.Context, provider v1beta1.Prov
 		return fmt.Errorf("no address found in 'spec.address' nor in `spec.secretRef`")
 	}
 
-	factory := notifier.NewFactory(address, provider.Spec.Proxy, provider.Spec.Username, provider.Spec.Channel, token)
+	var certPool *x509.CertPool
+	if provider.Spec.CertSecretRef != nil {
+		var secret corev1.Secret
+		secretName := types.NamespacedName{Namespace: provider.Namespace, Name: provider.Spec.CertSecretRef.Name}
+
+		if err := r.Get(ctx, secretName, &secret); err != nil {
+			return fmt.Errorf("failed to read secret, error: %w", err)
+		}
+
+		caFile, ok := secret.Data["caFile"]
+		if !ok {
+			return fmt.Errorf("no caFile found in secret %q", provider.Spec.CertSecretRef.Name)
+		}
+
+		certPool = x509.NewCertPool()
+		ok = certPool.AppendCertsFromPEM(caFile)
+		if !ok {
+			return fmt.Errorf("could not append to cert pool")
+		}
+	}
+
+	factory := notifier.NewFactory(address, provider.Spec.Proxy, provider.Spec.Username, provider.Spec.Channel, token, certPool)
 	if _, err := factory.Notifier(provider.Spec.Type); err != nil {
 		return fmt.Errorf("failed to initialise provider, error: %w", err)
 	}
diff --git a/docs/api/notification.md b/docs/api/notification.md
index 9165836..d2489a2 100644
--- a/docs/api/notification.md
+++ b/docs/api/notification.md
@@ -298,6 +298,21 @@ github.com/fluxcd/pkg/apis/meta.LocalObjectReference
 using “address” as data key
 
 
+
+| + +certSecretRef+
+
+github.com/fluxcd/pkg/apis/meta.LocalObjectReference
+
+
+
 | +(Optional)
+ +CertSecretRef can be given the name of a secret containing
+a PEM-encoded CA certificate (+caFile) | 
 
 
 
@@ -761,6 +776,21 @@ github.com/fluxcd/pkg/apis/meta.LocalObjectReference
 using “address” as data key
 
 
+
+| + +certSecretRef+
+
+github.com/fluxcd/pkg/apis/meta.LocalObjectReference
+
+
+
 | +(Optional)
+ +CertSecretRef can be given the name of a secret containing
+a PEM-encoded CA certificate (+caFile) | 
 
 
 
diff --git a/docs/spec/v1beta1/provider.md b/docs/spec/v1beta1/provider.md
index dd126d4..5c493ca 100644
--- a/docs/spec/v1beta1/provider.md
+++ b/docs/spec/v1beta1/provider.md
@@ -209,3 +209,16 @@ The body of the request looks like this:
 ```
 
 The `involvedObject` key contains the object that triggered the event.
+
+### Self signed certificates
+
+The `certSecretRef` field names a secret with TLS certificate data. This is for the purpose
+of enabling a provider to communicate with a server using a self signed cert.
+
+To use the field create a secret, containing a CA file, in the same namespace and reference
+it from the provider.
+```shell
+SECRET_NAME=tls-certs
+kubectl create secret generic $SECRET_NAME \
+  --from-file=caFile=ca.crt
+```
diff --git a/internal/notifier/azure_devops.go b/internal/notifier/azure_devops.go
index 1c7e78c..aa4d72e 100644
--- a/internal/notifier/azure_devops.go
+++ b/internal/notifier/azure_devops.go
@@ -18,12 +18,15 @@ package notifier
 
 import (
 	"context"
+	"crypto/tls"
+	"crypto/x509"
 	"errors"
 	"fmt"
-	"github.com/fluxcd/pkg/runtime/events"
 	"strings"
 	"time"
 
+	"github.com/fluxcd/pkg/runtime/events"
+
 	"github.com/microsoft/azure-devops-go-api/azuredevops"
 	"github.com/microsoft/azure-devops-go-api/azuredevops/git"
 )
@@ -38,7 +41,7 @@ type AzureDevOps struct {
 }
 
 // NewAzureDevOps creates and returns a new AzureDevOps notifier.
-func NewAzureDevOps(addr string, token string) (*AzureDevOps, error) {
+func NewAzureDevOps(addr string, token string, certPool *x509.CertPool) (*AzureDevOps, error) {
 	if len(token) == 0 {
 		return nil, errors.New("azure devops token cannot be empty")
 	}
@@ -58,6 +61,11 @@ func NewAzureDevOps(addr string, token string) (*AzureDevOps, error) {
 
 	orgURL := fmt.Sprintf("%v/%v", host, org)
 	connection := azuredevops.NewPatConnection(orgURL, token)
+	if certPool != nil {
+		connection.TlsConfig = &tls.Config{
+			RootCAs: certPool,
+		}
+	}
 	client := connection.GetClientByUrl(orgURL)
 	gitClient := &git.ClientImpl{
 		Client: *client,
diff --git a/internal/notifier/azure_devops_test.go b/internal/notifier/azure_devops_test.go
index 5a72890..6e8e8de 100644
--- a/internal/notifier/azure_devops_test.go
+++ b/internal/notifier/azure_devops_test.go
@@ -24,19 +24,19 @@ import (
 )
 
 func TestNewAzureDevOpsBasic(t *testing.T) {
-	a, err := NewAzureDevOps("https://dev.azure.com/foo/bar/_git/baz", "foo")
+	a, err := NewAzureDevOps("https://dev.azure.com/foo/bar/_git/baz", "foo", nil)
 	assert.Nil(t, err)
 	assert.Equal(t, a.Project, "bar")
 	assert.Equal(t, a.Repo, "baz")
 }
 
 func TestNewAzureDevOpsInvalidUrl(t *testing.T) {
-	_, err := NewAzureDevOps("https://dev.azure.com/foo/bar/baz", "foo")
+	_, err := NewAzureDevOps("https://dev.azure.com/foo/bar/baz", "foo", nil)
 	assert.NotNil(t, err)
 }
 
 func TestNewAzureDevOpsMissingToken(t *testing.T) {
-	_, err := NewAzureDevOps("https://dev.azure.com/foo/bar/baz", "")
+	_, err := NewAzureDevOps("https://dev.azure.com/foo/bar/baz", "", nil)
 	assert.NotNil(t, err)
 }
 
diff --git a/internal/notifier/bitbucket.go b/internal/notifier/bitbucket.go
index af506de..11e739c 100644
--- a/internal/notifier/bitbucket.go
+++ b/internal/notifier/bitbucket.go
@@ -17,8 +17,11 @@ limitations under the License.
 package notifier
 
 import (
+	"crypto/tls"
+	"crypto/x509"
 	"errors"
 	"fmt"
+	"net/http"
 	"strings"
 
 	"github.com/fluxcd/pkg/runtime/events"
@@ -33,7 +36,7 @@ type Bitbucket struct {
 }
 
 // NewBitbucket creates and returns a new Bitbucket notifier.
-func NewBitbucket(addr string, token string) (*Bitbucket, error) {
+func NewBitbucket(addr string, token string, certPool *x509.CertPool) (*Bitbucket, error) {
 	if len(token) == 0 {
 		return nil, errors.New("bitbucket token cannot be empty")
 	}
@@ -57,10 +60,21 @@ func NewBitbucket(addr string, token string) (*Bitbucket, error) {
 	owner := comp[0]
 	repo := comp[1]
 
+	client := bitbucket.NewBasicAuth(username, password)
+	if certPool != nil {
+		tr := &http.Transport{
+			TLSClientConfig: &tls.Config{
+				RootCAs: certPool,
+			},
+		}
+		hc := &http.Client{Transport: tr}
+		client.HttpClient = hc
+	}
+
 	return &Bitbucket{
 		Owner:  owner,
 		Repo:   repo,
-		Client: bitbucket.NewBasicAuth(username, password),
+		Client: client,
 	}, nil
 }
 
diff --git a/internal/notifier/bitbucket_test.go b/internal/notifier/bitbucket_test.go
index 6108516..519506e 100644
--- a/internal/notifier/bitbucket_test.go
+++ b/internal/notifier/bitbucket_test.go
@@ -23,18 +23,18 @@ import (
 )
 
 func TestNewBitbucketBasic(t *testing.T) {
-	b, err := NewBitbucket("https://bitbucket.org/foo/bar", "foo:bar")
+	b, err := NewBitbucket("https://bitbucket.org/foo/bar", "foo:bar", nil)
 	assert.Nil(t, err)
 	assert.Equal(t, b.Owner, "foo")
 	assert.Equal(t, b.Repo, "bar")
 }
 
 func TestNewBitbucketInvalidUrl(t *testing.T) {
-	_, err := NewBitbucket("https://bitbucket.org/foo/bar/baz", "foo:bar")
+	_, err := NewBitbucket("https://bitbucket.org/foo/bar/baz", "foo:bar", nil)
 	assert.NotNil(t, err)
 }
 
 func TestNewBitbucketInvalidToken(t *testing.T) {
-	_, err := NewBitbucket("https://bitbucket.org/foo/bar", "bar")
+	_, err := NewBitbucket("https://bitbucket.org/foo/bar", "bar", nil)
 	assert.NotNil(t, err)
 }
diff --git a/internal/notifier/client.go b/internal/notifier/client.go
index 40aaff8..8ce985b 100644
--- a/internal/notifier/client.go
+++ b/internal/notifier/client.go
@@ -17,6 +17,8 @@ limitations under the License.
 package notifier
 
 import (
+	"crypto/tls"
+	"crypto/x509"
 	"encoding/json"
 	"fmt"
 	"net"
@@ -30,7 +32,7 @@ import (
 
 type requestOptFunc func(*retryablehttp.Request)
 
-func postMessage(address, proxy string, payload interface{}, reqOpts ...requestOptFunc) error {
+func postMessage(address, proxy string, certPool *x509.CertPool, payload interface{}, reqOpts ...requestOptFunc) error {
 	httpClient := retryablehttp.NewClient()
 
 	if proxy != "" {
@@ -40,6 +42,9 @@ func postMessage(address, proxy string, payload interface{}, reqOpts ...requestO
 		}
 		httpClient.HTTPClient.Transport = &http.Transport{
 			Proxy: http.ProxyURL(proxyURL),
+			TLSClientConfig: &tls.Config{
+				RootCAs: certPool,
+			},
 			DialContext: (&net.Dialer{
 				Timeout:   15 * time.Second,
 				KeepAlive: 30 * time.Second,
diff --git a/internal/notifier/client_test.go b/internal/notifier/client_test.go
index dcfa531..078f3c1 100644
--- a/internal/notifier/client_test.go
+++ b/internal/notifier/client_test.go
@@ -42,7 +42,7 @@ func Test_postMessage(t *testing.T) {
 		require.Equal(t, "success", payload["status"])
 	}))
 	defer ts.Close()
-	err := postMessage(ts.URL, "", map[string]string{"status": "success"})
+	err := postMessage(ts.URL, "", nil, map[string]string{"status": "success"})
 	require.NoError(t, err)
 }
 
diff --git a/internal/notifier/discord.go b/internal/notifier/discord.go
index ba4c311..c037842 100644
--- a/internal/notifier/discord.go
+++ b/internal/notifier/discord.go
@@ -99,7 +99,7 @@ func (s *Discord) Post(event events.Event) error {
 
 	payload.Attachments = []SlackAttachment{a}
 
-	err := postMessage(s.URL, s.ProxyURL, payload)
+	err := postMessage(s.URL, s.ProxyURL, nil, payload)
 	if err != nil {
 		return fmt.Errorf("postMessage failed: %w", err)
 	}
diff --git a/internal/notifier/factory.go b/internal/notifier/factory.go
index 8780d1e..a388b9b 100644
--- a/internal/notifier/factory.go
+++ b/internal/notifier/factory.go
@@ -17,6 +17,7 @@ limitations under the License.
 package notifier
 
 import (
+	"crypto/x509"
 	"fmt"
 
 	"github.com/fluxcd/notification-controller/api/v1beta1"
@@ -28,15 +29,17 @@ type Factory struct {
 	Username string
 	Channel  string
 	Token    string
+	CertPool *x509.CertPool
 }
 
-func NewFactory(url string, proxy string, username string, channel string, token string) *Factory {
+func NewFactory(url string, proxy string, username string, channel string, token string, certPool *x509.CertPool) *Factory {
 	return &Factory{
 		URL:      url,
 		ProxyURL: proxy,
 		Channel:  channel,
 		Username: username,
 		Token:    token,
+		CertPool: certPool,
 	}
 }
 
@@ -49,29 +52,29 @@ func (f Factory) Notifier(provider string) (Interface, error) {
 	var err error
 	switch provider {
 	case v1beta1.GenericProvider:
-		n, err = NewForwarder(f.URL, f.ProxyURL)
+		n, err = NewForwarder(f.URL, f.ProxyURL, f.CertPool)
 	case v1beta1.SlackProvider:
 		n, err = NewSlack(f.URL, f.ProxyURL, f.Username, f.Channel)
 	case v1beta1.DiscordProvider:
 		n, err = NewDiscord(f.URL, f.ProxyURL, f.Username, f.Channel)
 	case v1beta1.RocketProvider:
-		n, err = NewRocket(f.URL, f.ProxyURL, f.Username, f.Channel)
+		n, err = NewRocket(f.URL, f.ProxyURL, f.CertPool, f.Username, f.Channel)
 	case v1beta1.MSTeamsProvider:
 		n, err = NewMSTeams(f.URL, f.ProxyURL)
 	case v1beta1.GitHubProvider:
-		n, err = NewGitHub(f.URL, f.Token)
+		n, err = NewGitHub(f.URL, f.Token, f.CertPool)
 	case v1beta1.GitLabProvider:
-		n, err = NewGitLab(f.URL, f.Token)
+		n, err = NewGitLab(f.URL, f.Token, f.CertPool)
 	case v1beta1.BitbucketProvider:
-		n, err = NewBitbucket(f.URL, f.Token)
+		n, err = NewBitbucket(f.URL, f.Token, f.CertPool)
 	case v1beta1.AzureDevOpsProvider:
-		n, err = NewAzureDevOps(f.URL, f.Token)
+		n, err = NewAzureDevOps(f.URL, f.Token, f.CertPool)
 	case v1beta1.GoogleChatProvider:
 		n, err = NewGoogleChat(f.URL, f.ProxyURL)
 	case v1beta1.WebexProvider:
-		n, err = NewWebex(f.URL, f.ProxyURL)
+		n, err = NewWebex(f.URL, f.ProxyURL, f.CertPool)
 	case v1beta1.SentryProvider:
-		n, err = NewSentry(f.URL)
+		n, err = NewSentry(f.CertPool, f.URL)
 	default:
 		err = fmt.Errorf("provider %s not supported", provider)
 	}
diff --git a/internal/notifier/forwarder.go b/internal/notifier/forwarder.go
index 8d8425e..907badc 100644
--- a/internal/notifier/forwarder.go
+++ b/internal/notifier/forwarder.go
@@ -17,10 +17,12 @@ limitations under the License.
 package notifier
 
 import (
+	"crypto/x509"
 	"fmt"
-	"github.com/fluxcd/pkg/runtime/events"
 	"net/url"
 
+	"github.com/fluxcd/pkg/runtime/events"
+
 	"github.com/hashicorp/go-retryablehttp"
 )
 
@@ -33,9 +35,10 @@ const NotificationHeader = "gotk-component"
 type Forwarder struct {
 	URL      string
 	ProxyURL string
+	CertPool *x509.CertPool
 }
 
-func NewForwarder(hookURL string, proxyURL string) (*Forwarder, error) {
+func NewForwarder(hookURL string, proxyURL string, certPool *x509.CertPool) (*Forwarder, error) {
 	if _, err := url.ParseRequestURI(hookURL); err != nil {
 		return nil, fmt.Errorf("invalid hook URL %s: %w", hookURL, err)
 	}
@@ -47,7 +50,7 @@ func NewForwarder(hookURL string, proxyURL string) (*Forwarder, error) {
 }
 
 func (f *Forwarder) Post(event events.Event) error {
-	err := postMessage(f.URL, f.ProxyURL, event, func(req *retryablehttp.Request) {
+	err := postMessage(f.URL, f.ProxyURL, f.CertPool, event, func(req *retryablehttp.Request) {
 		req.Header.Set(NotificationHeader, event.ReportingController)
 	})
 
diff --git a/internal/notifier/forwarder_test.go b/internal/notifier/forwarder_test.go
index 228ba88..307ae90 100644
--- a/internal/notifier/forwarder_test.go
+++ b/internal/notifier/forwarder_test.go
@@ -18,12 +18,13 @@ package notifier
 
 import (
 	"encoding/json"
-	"github.com/fluxcd/pkg/runtime/events"
 	"io/ioutil"
 	"net/http"
 	"net/http/httptest"
 	"testing"
 
+	"github.com/fluxcd/pkg/runtime/events"
+
 	"github.com/stretchr/testify/require"
 )
 
@@ -41,7 +42,7 @@ func TestForwarder_Post(t *testing.T) {
 	}))
 	defer ts.Close()
 
-	forwarder, err := NewForwarder(ts.URL, "")
+	forwarder, err := NewForwarder(ts.URL, "", nil)
 	require.NoError(t, err)
 
 	err = forwarder.Post(testEvent())
diff --git a/internal/notifier/github.go b/internal/notifier/github.go
index 39f47d0..c2f4101 100644
--- a/internal/notifier/github.go
+++ b/internal/notifier/github.go
@@ -18,13 +18,17 @@ package notifier
 
 import (
 	"context"
+	"crypto/tls"
+	"crypto/x509"
 	"errors"
 	"fmt"
-	"github.com/fluxcd/pkg/runtime/events"
+	"net/http"
 	"net/url"
 	"strings"
 	"time"
 
+	"github.com/fluxcd/pkg/runtime/events"
+
 	"github.com/google/go-github/v32/github"
 	"golang.org/x/oauth2"
 )
@@ -35,7 +39,7 @@ type GitHub struct {
 	Client *github.Client
 }
 
-func NewGitHub(addr string, token string) (*GitHub, error) {
+func NewGitHub(addr string, token string, certPool *x509.CertPool) (*GitHub, error) {
 	if len(token) == 0 {
 		return nil, errors.New("github token cannot be empty")
 	}
@@ -59,6 +63,17 @@ func NewGitHub(addr string, token string) (*GitHub, error) {
 	tc := oauth2.NewClient(context.Background(), ts)
 	client := github.NewClient(tc)
 	if baseUrl.Host != "github.com" {
+		if certPool != nil {
+			tr := &http.Transport{
+				TLSClientConfig: &tls.Config{
+					RootCAs: certPool,
+				},
+			}
+			hc := &http.Client{Transport: tr}
+			ctx := context.WithValue(context.Background(), oauth2.HTTPClient, hc)
+			tc = oauth2.NewClient(ctx, ts)
+		}
+
 		client, err = github.NewEnterpriseClient(host, host, tc)
 		if err != nil {
 			return nil, fmt.Errorf("could not create enterprise GitHub client: %v", err)
diff --git a/internal/notifier/github_test.go b/internal/notifier/github_test.go
index 2757da5..a30bf50 100644
--- a/internal/notifier/github_test.go
+++ b/internal/notifier/github_test.go
@@ -24,7 +24,7 @@ import (
 )
 
 func TestNewGitHubBasic(t *testing.T) {
-	g, err := NewGitHub("https://github.com/foo/bar", "foobar")
+	g, err := NewGitHub("https://github.com/foo/bar", "foobar", nil)
 	assert.Nil(t, err)
 	assert.Equal(t, g.Owner, "foo")
 	assert.Equal(t, g.Repo, "bar")
@@ -32,7 +32,7 @@ func TestNewGitHubBasic(t *testing.T) {
 }
 
 func TestNewEmterpriseGitHubBasic(t *testing.T) {
-	g, err := NewGitHub("https://foobar.com/foo/bar", "foobar")
+	g, err := NewGitHub("https://foobar.com/foo/bar", "foobar", nil)
 	assert.Nil(t, err)
 	assert.Equal(t, g.Owner, "foo")
 	assert.Equal(t, g.Repo, "bar")
@@ -40,12 +40,12 @@ func TestNewEmterpriseGitHubBasic(t *testing.T) {
 }
 
 func TestNewGitHubInvalidUrl(t *testing.T) {
-	_, err := NewGitHub("https://github.com/foo/bar/baz", "foobar")
+	_, err := NewGitHub("https://github.com/foo/bar/baz", "foobar", nil)
 	assert.NotNil(t, err)
 }
 
 func TestNewGitHubEmptyToken(t *testing.T) {
-	_, err := NewGitHub("https://github.com/foo/bar", "")
+	_, err := NewGitHub("https://github.com/foo/bar", "", nil)
 	assert.NotNil(t, err)
 }
 
diff --git a/internal/notifier/gitlab.go b/internal/notifier/gitlab.go
index 112a59d..2834b30 100644
--- a/internal/notifier/gitlab.go
+++ b/internal/notifier/gitlab.go
@@ -17,7 +17,10 @@ limitations under the License.
 package notifier
 
 import (
+	"crypto/tls"
+	"crypto/x509"
 	"errors"
+	"net/http"
 
 	"github.com/fluxcd/pkg/runtime/events"
 	"github.com/xanzy/go-gitlab"
@@ -28,7 +31,7 @@ type GitLab struct {
 	Client *gitlab.Client
 }
 
-func NewGitLab(addr string, token string) (*GitLab, error) {
+func NewGitLab(addr string, token string, certPool *x509.CertPool) (*GitLab, error) {
 	if len(token) == 0 {
 		return nil, errors.New("gitlab token cannot be empty")
 	}
@@ -38,8 +41,17 @@ func NewGitLab(addr string, token string) (*GitLab, error) {
 		return nil, err
 	}
 
-	opt := gitlab.WithBaseURL(host)
-	client, err := gitlab.NewClient(token, opt)
+	opts := []gitlab.ClientOptionFunc{gitlab.WithBaseURL(host)}
+	if certPool != nil {
+		tr := &http.Transport{
+			TLSClientConfig: &tls.Config{
+				RootCAs: certPool,
+			},
+		}
+		hc := &http.Client{Transport: tr}
+		opts = append(opts, gitlab.WithHTTPClient(hc))
+	}
+	client, err := gitlab.NewClient(token, opts...)
 	if err != nil {
 		return nil, err
 	}
diff --git a/internal/notifier/gitlab_test.go b/internal/notifier/gitlab_test.go
index 24a58c7..32a50bd 100644
--- a/internal/notifier/gitlab_test.go
+++ b/internal/notifier/gitlab_test.go
@@ -23,25 +23,25 @@ import (
 )
 
 func TestNewGitLabBasic(t *testing.T) {
-	g, err := NewGitLab("https://gitlab.com/foo/bar", "foobar")
+	g, err := NewGitLab("https://gitlab.com/foo/bar", "foobar", nil)
 	assert.Nil(t, err)
 	assert.Equal(t, g.Id, "foo/bar")
 }
 
 func TestNewGitLabSubgroups(t *testing.T) {
-	g, err := NewGitLab("https://gitlab.com/foo/bar/baz", "foobar")
+	g, err := NewGitLab("https://gitlab.com/foo/bar/baz", "foobar", nil)
 	assert.Nil(t, err)
 	assert.Equal(t, g.Id, "foo/bar/baz")
 }
 
 func TestNewGitLabSelfHosted(t *testing.T) {
-	g, err := NewGitLab("https://example.com/foo/bar", "foo:bar")
+	g, err := NewGitLab("https://example.com/foo/bar", "foo:bar", nil)
 	assert.Nil(t, err)
 	assert.Equal(t, g.Id, "foo/bar")
 	assert.Equal(t, g.Client.BaseURL().Host, "example.com")
 }
 
 func TestNewGitLabEmptyToken(t *testing.T) {
-	_, err := NewGitLab("https://gitlab.com/foo/bar", "")
+	_, err := NewGitLab("https://gitlab.com/foo/bar", "", nil)
 	assert.NotNil(t, err)
 }
diff --git a/internal/notifier/google_chat.go b/internal/notifier/google_chat.go
index 2fec3db..5d152a4 100644
--- a/internal/notifier/google_chat.go
+++ b/internal/notifier/google_chat.go
@@ -141,7 +141,7 @@ func (s *GoogleChat) Post(event events.Event) error {
 		Cards: []GoogleChatCard{card},
 	}
 
-	err := postMessage(s.URL, s.ProxyURL, payload)
+	err := postMessage(s.URL, s.ProxyURL, nil, payload)
 	if err != nil {
 		return fmt.Errorf("postMessage failed: %w", err)
 	}
diff --git a/internal/notifier/rocket.go b/internal/notifier/rocket.go
index 80e063d..e6542a5 100644
--- a/internal/notifier/rocket.go
+++ b/internal/notifier/rocket.go
@@ -17,6 +17,7 @@ limitations under the License.
 package notifier
 
 import (
+	"crypto/x509"
 	"errors"
 	"fmt"
 	"net/url"
@@ -31,10 +32,11 @@ type Rocket struct {
 	ProxyURL string
 	Username string
 	Channel  string
+	CertPool *x509.CertPool
 }
 
 // NewRocket validates the Rocket URL and returns a Rocket object
-func NewRocket(hookURL string, proxyURL string, username string, channel string) (*Rocket, error) {
+func NewRocket(hookURL string, proxyURL string, certPool *x509.CertPool, username string, channel string) (*Rocket, error) {
 	_, err := url.ParseRequestURI(hookURL)
 	if err != nil {
 		return nil, fmt.Errorf("invalid Rocket hook URL %s", hookURL)
@@ -53,6 +55,7 @@ func NewRocket(hookURL string, proxyURL string, username string, channel string)
 		URL:      hookURL,
 		ProxyURL: proxyURL,
 		Username: username,
+		CertPool: certPool,
 	}, nil
 }
 
@@ -88,7 +91,7 @@ func (s *Rocket) Post(event events.Event) error {
 
 	payload.Attachments = []SlackAttachment{a}
 
-	err := postMessage(s.URL, s.ProxyURL, payload)
+	err := postMessage(s.URL, s.ProxyURL, s.CertPool, payload)
 	if err != nil {
 		return fmt.Errorf("postMessage failed: %w", err)
 	}
diff --git a/internal/notifier/rocket_test.go b/internal/notifier/rocket_test.go
index eaf867e..1ada7d2 100644
--- a/internal/notifier/rocket_test.go
+++ b/internal/notifier/rocket_test.go
@@ -39,7 +39,7 @@ func TestRocket_Post(t *testing.T) {
 	}))
 	defer ts.Close()
 
-	rocket, err := NewRocket(ts.URL, "", "test", "test")
+	rocket, err := NewRocket(ts.URL, "", nil, "test", "test")
 	require.NoError(t, err)
 
 	err = rocket.Post(testEvent())
diff --git a/internal/notifier/sentry.go b/internal/notifier/sentry.go
index a283127..af23b8c 100644
--- a/internal/notifier/sentry.go
+++ b/internal/notifier/sentry.go
@@ -17,7 +17,11 @@ limitations under the License.
 package notifier
 
 import (
+	"crypto/tls"
+	"crypto/x509"
 	"fmt"
+	"net/http"
+
 	"github.com/fluxcd/pkg/runtime/events"
 	"github.com/getsentry/sentry-go"
 )
@@ -28,9 +32,14 @@ type Sentry struct {
 }
 
 // NewSentry creates a Sentry client from the provided Data Source Name (DSN)
-func NewSentry(dsn string) (*Sentry, error) {
+func NewSentry(certPool *x509.CertPool, dsn string) (*Sentry, error) {
 	client, err := sentry.NewClient(sentry.ClientOptions{
 		Dsn: dsn,
+		HTTPTransport: &http.Transport{
+			TLSClientConfig: &tls.Config{
+				RootCAs: certPool,
+			},
+		},
 	})
 	if err != nil {
 		return nil, err
diff --git a/internal/notifier/sentry_test.go b/internal/notifier/sentry_test.go
index 6e7a464..4d89f28 100644
--- a/internal/notifier/sentry_test.go
+++ b/internal/notifier/sentry_test.go
@@ -17,19 +17,20 @@ limitations under the License.
 package notifier
 
 import (
+	"testing"
+	"time"
+
 	"github.com/fluxcd/pkg/runtime/events"
 	"github.com/getsentry/sentry-go"
 	"github.com/stretchr/testify/assert"
 	corev1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-	"testing"
-	"time"
 
 	"github.com/stretchr/testify/require"
 )
 
 func TestNewSentry(t *testing.T) {
-	s, err := NewSentry("https://test@localhost/1")
+	s, err := NewSentry(nil, "https://test@localhost/1")
 	require.NoError(t, err)
 	assert.Equal(t, s.Client.Options().Dsn, "https://test@localhost/1")
 }
diff --git a/internal/notifier/slack.go b/internal/notifier/slack.go
index c23926c..64c30ba 100644
--- a/internal/notifier/slack.go
+++ b/internal/notifier/slack.go
@@ -112,7 +112,7 @@ func (s *Slack) Post(event events.Event) error {
 
 	payload.Attachments = []SlackAttachment{a}
 
-	err := postMessage(s.URL, s.ProxyURL, payload)
+	err := postMessage(s.URL, s.ProxyURL, nil, payload)
 	if err != nil {
 		return fmt.Errorf("postMessage failed: %w", err)
 	}
diff --git a/internal/notifier/teams.go b/internal/notifier/teams.go
index 5298e99..bcf19b6 100644
--- a/internal/notifier/teams.go
+++ b/internal/notifier/teams.go
@@ -98,7 +98,7 @@ func (s *MSTeams) Post(event events.Event) error {
 		payload.ThemeColor = "FF0000"
 	}
 
-	err := postMessage(s.URL, s.ProxyURL, payload)
+	err := postMessage(s.URL, s.ProxyURL, nil, payload)
 	if err != nil {
 		return fmt.Errorf("postMessage failed: %w", err)
 	}
diff --git a/internal/notifier/webex.go b/internal/notifier/webex.go
index 969cf97..ddb5c77 100644
--- a/internal/notifier/webex.go
+++ b/internal/notifier/webex.go
@@ -17,6 +17,7 @@ limitations under the License.
 package notifier
 
 import (
+	"crypto/x509"
 	"fmt"
 	"net/url"
 	"strings"
@@ -28,6 +29,7 @@ import (
 type Webex struct {
 	URL      string
 	ProxyURL string
+	CertPool *x509.CertPool
 }
 
 // WebexPayload holds the message text
@@ -37,7 +39,7 @@ type WebexPayload struct {
 }
 
 // NewWebex validates the Webex URL and returns a Webex object
-func NewWebex(hookURL, proxyURL string) (*Webex, error) {
+func NewWebex(hookURL, proxyURL string, certPool *x509.CertPool) (*Webex, error) {
 	_, err := url.ParseRequestURI(hookURL)
 	if err != nil {
 		return nil, fmt.Errorf("invalid Webex hook URL %s", hookURL)
@@ -71,7 +73,7 @@ func (s *Webex) Post(event events.Event) error {
 		Markdown: markdown,
 	}
 
-	if err := postMessage(s.URL, s.ProxyURL, payload); err != nil {
+	if err := postMessage(s.URL, s.ProxyURL, s.CertPool, payload); err != nil {
 		return fmt.Errorf("postMessage failed: %w", err)
 	}
 	return nil
diff --git a/internal/notifier/webex_test.go b/internal/notifier/webex_test.go
index 14ba0cf..0a65b33 100644
--- a/internal/notifier/webex_test.go
+++ b/internal/notifier/webex_test.go
@@ -39,7 +39,7 @@ func TestWebex_Post(t *testing.T) {
 	}))
 	defer ts.Close()
 
-	webex, err := NewWebex(ts.URL, "")
+	webex, err := NewWebex(ts.URL, "", nil)
 	require.NoError(t, err)
 
 	err = webex.Post(testEvent())
@@ -47,7 +47,7 @@ func TestWebex_Post(t *testing.T) {
 }
 
 func TestWebex_PostUpdate(t *testing.T) {
-	webex, err := NewWebex("http://localhost", "")
+	webex, err := NewWebex("http://localhost", "", nil)
 	require.NoError(t, err)
 
 	event := testEvent()
diff --git a/internal/server/event_handlers.go b/internal/server/event_handlers.go
index 14e456b..c0d4ea6 100644
--- a/internal/server/event_handlers.go
+++ b/internal/server/event_handlers.go
@@ -18,6 +18,7 @@ package server
 
 import (
 	"context"
+	"crypto/x509"
 	"encoding/json"
 	"fmt"
 	"io/ioutil"
@@ -156,6 +157,40 @@ func (s *EventServer) handleEvent() func(w http.ResponseWriter, r *http.Request)
 				}
 			}
 
+			var certPool *x509.CertPool
+			if provider.Spec.CertSecretRef != nil {
+				var secret corev1.Secret
+				secretName := types.NamespacedName{Namespace: alert.Namespace, Name: provider.Spec.CertSecretRef.Name}
+
+				err = s.kubeClient.Get(ctx, secretName, &secret)
+				if err != nil {
+					s.logger.Error(err, "failed to read secret",
+						"reconciler kind", v1beta1.ProviderKind,
+						"name", providerName.Name,
+						"namespace", providerName.Namespace)
+					continue
+				}
+
+				caFile, ok := secret.Data["caFile"]
+				if !ok {
+					s.logger.Error(err, "failed to read secret key caFile",
+						"reconciler kind", v1beta1.ProviderKind,
+						"name", providerName.Name,
+						"namespace", providerName.Namespace)
+					continue
+				}
+
+				certPool = x509.NewCertPool()
+				ok = certPool.AppendCertsFromPEM(caFile)
+				if !ok {
+					s.logger.Error(err, "could not append to cert pool",
+						"reconciler kind", v1beta1.ProviderKind,
+						"name", providerName.Name,
+						"namespace", providerName.Namespace)
+					continue
+				}
+			}
+
 			if webhook == "" {
 				s.logger.Error(nil, "provider has no address",
 					"reconciler kind", v1beta1.ProviderKind,
@@ -164,7 +199,7 @@ func (s *EventServer) handleEvent() func(w http.ResponseWriter, r *http.Request)
 				continue
 			}
 
-			factory := notifier.NewFactory(webhook, provider.Spec.Proxy, provider.Spec.Username, provider.Spec.Channel, token)
+			factory := notifier.NewFactory(webhook, provider.Spec.Proxy, provider.Spec.Username, provider.Spec.Channel, token, certPool)
 			sender, err := factory.Notifier(provider.Spec.Type)
 			if err != nil {
 				s.logger.Error(err, "failed to initialise provider",
From a2377a84b9787fcd25dab44c77f9c23d1f243abd Mon Sep 17 00:00:00 2001
From: Philip Laine 
Date: Tue, 20 Apr 2021 00:10:37 +0200
Subject: [PATCH 2/4] Add tests for client self signed cert
Signed-off-by: Philip Laine 
---
 internal/notifier/client.go      | 19 +++++++++++++++----
 internal/notifier/client_test.go | 22 ++++++++++++++++++++++
 2 files changed, 37 insertions(+), 4 deletions(-)
diff --git a/internal/notifier/client.go b/internal/notifier/client.go
index 8ce985b..6d6deba 100644
--- a/internal/notifier/client.go
+++ b/internal/notifier/client.go
@@ -34,17 +34,28 @@ type requestOptFunc func(*retryablehttp.Request)
 
 func postMessage(address, proxy string, certPool *x509.CertPool, payload interface{}, reqOpts ...requestOptFunc) error {
 	httpClient := retryablehttp.NewClient()
+	if certPool != nil {
+		httpClient.HTTPClient.Transport = &http.Transport{
+			TLSClientConfig: &tls.Config{
+				RootCAs: certPool,
+			},
+		}
+	}
 
 	if proxy != "" {
 		proxyURL, err := url.Parse(proxy)
 		if err != nil {
 			return fmt.Errorf("unable to parse proxy URL '%s', error: %w", proxy, err)
 		}
-		httpClient.HTTPClient.Transport = &http.Transport{
-			Proxy: http.ProxyURL(proxyURL),
-			TLSClientConfig: &tls.Config{
+		var tlsConfig *tls.Config
+		if certPool != nil {
+			tlsConfig = &tls.Config{
 				RootCAs: certPool,
-			},
+			}
+		}
+		httpClient.HTTPClient.Transport = &http.Transport{
+			Proxy:           http.ProxyURL(proxyURL),
+			TLSClientConfig: tlsConfig,
 			DialContext: (&net.Dialer{
 				Timeout:   15 * time.Second,
 				KeepAlive: 30 * time.Second,
diff --git a/internal/notifier/client_test.go b/internal/notifier/client_test.go
index 078f3c1..0fa40e3 100644
--- a/internal/notifier/client_test.go
+++ b/internal/notifier/client_test.go
@@ -17,6 +17,7 @@ limitations under the License.
 package notifier
 
 import (
+	"crypto/x509"
 	"encoding/json"
 	"io/ioutil"
 	"net/http"
@@ -46,6 +47,27 @@ func Test_postMessage(t *testing.T) {
 	require.NoError(t, err)
 }
 
+func Test_postSelfSignedCert(t *testing.T) {
+	ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		b, err := ioutil.ReadAll(r.Body)
+		require.NoError(t, err)
+
+		var payload = make(map[string]string)
+		err = json.Unmarshal(b, &payload)
+		require.NoError(t, err)
+
+		require.Equal(t, "success", payload["status"])
+	}))
+	defer ts.Close()
+
+	cert, err := x509.ParseCertificate(ts.TLS.Certificates[0].Certificate[0])
+	require.NoError(t, err)
+	certpool := x509.NewCertPool()
+	certpool.AddCert(cert)
+	err = postMessage(ts.URL, "", certpool, map[string]string{"status": "success"})
+	require.NoError(t, err)
+}
+
 func testEvent() events.Event {
 	return events.Event{
 		InvolvedObject: corev1.ObjectReference{
From c27e013b3992a16ffd57c81185ff5e24df042769 Mon Sep 17 00:00:00 2001
From: Philip Laine 
Date: Tue, 20 Apr 2021 00:12:45 +0200
Subject: [PATCH 3/4] Add condition to cert in sentry
Signed-off-by: Philip Laine 
---
 internal/notifier/sentry.go | 12 ++++++++----
 1 file changed, 8 insertions(+), 4 deletions(-)
diff --git a/internal/notifier/sentry.go b/internal/notifier/sentry.go
index af23b8c..2ac73a2 100644
--- a/internal/notifier/sentry.go
+++ b/internal/notifier/sentry.go
@@ -33,13 +33,17 @@ type Sentry struct {
 
 // NewSentry creates a Sentry client from the provided Data Source Name (DSN)
 func NewSentry(certPool *x509.CertPool, dsn string) (*Sentry, error) {
-	client, err := sentry.NewClient(sentry.ClientOptions{
-		Dsn: dsn,
-		HTTPTransport: &http.Transport{
+	var tr *http.Transport
+	if certPool != nil {
+		tr = &http.Transport{
 			TLSClientConfig: &tls.Config{
 				RootCAs: certPool,
 			},
-		},
+		}
+	}
+	client, err := sentry.NewClient(sentry.ClientOptions{
+		Dsn:           dsn,
+		HTTPTransport: tr,
 	})
 	if err != nil {
 		return nil, err
From a98961f638c784f5b579a8d01def7c3a20dc52b5 Mon Sep 17 00:00:00 2001
From: Philip Laine 
Date: Wed, 21 Apr 2021 13:07:27 +0200
Subject: [PATCH 4/4] Fix error message formats
Signed-off-by: Philip Laine 
---
 controllers/provider_controller.go | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/controllers/provider_controller.go b/controllers/provider_controller.go
index 7a55f6a..a7e9105 100644
--- a/controllers/provider_controller.go
+++ b/controllers/provider_controller.go
@@ -133,13 +133,13 @@ func (r *ProviderReconciler) validate(ctx context.Context, provider v1beta1.Prov
 
 		caFile, ok := secret.Data["caFile"]
 		if !ok {
-			return fmt.Errorf("no caFile found in secret %q", provider.Spec.CertSecretRef.Name)
+			return fmt.Errorf("no caFile found in secret %s", provider.Spec.CertSecretRef.Name)
 		}
 
 		certPool = x509.NewCertPool()
 		ok = certPool.AppendCertsFromPEM(caFile)
 		if !ok {
-			return fmt.Errorf("could not append to cert pool")
+			return fmt.Errorf("could not append to cert pool: invalid CA found in %s", provider.Spec.CertSecretRef.Name)
 		}
 	}