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) } }