diff --git a/pkg/services/serviceaccounts/manager/service.go b/pkg/services/serviceaccounts/manager/service.go index 80c1fb96cb6..daf3ca637c7 100644 --- a/pkg/services/serviceaccounts/manager/service.go +++ b/pkg/services/serviceaccounts/manager/service.go @@ -78,7 +78,13 @@ func ProvideServiceAccountsService( s.secretScanInterval = cfg.SectionWithEnvOverrides("secretscan"). Key("interval").MustDuration(defaultSecretScanInterval) if s.secretScanEnabled { - s.secretScanService = secretscan.NewService(s.store, cfg) + var errSecret error + s.secretScanService, errSecret = secretscan.NewService(s.store, cfg) + if errSecret != nil { + s.secretScanEnabled = false + s.log.Warn("failed to initialize secret scan service. secret scan is disabled", + "error", errSecret.Error()) + } } return s, nil diff --git a/pkg/services/serviceaccounts/secretscan/client.go b/pkg/services/serviceaccounts/secretscan/client.go index 9f52c95d546..96b6b9f50f3 100644 --- a/pkg/services/serviceaccounts/secretscan/client.go +++ b/pkg/services/serviceaccounts/secretscan/client.go @@ -3,9 +3,12 @@ package secretscan import ( "bytes" "context" + "crypto/tls" "encoding/json" "fmt" + "net" "net/http" + "strings" "time" "github.com/pkg/errors" @@ -34,19 +37,37 @@ type Token struct { ReportedAt string `json:"reported_at"` //nolint } -var ErrInvalidStatusCode = errors.New("invalid status code") +var ( + ErrInvalidStatusCode = errors.New("invalid status code") + errSecretScanURL = errors.New("secretscan url must be https") +) + +func newClient(url, version string, dev bool) (*client, error) { + if !strings.HasPrefix(url, "https://") && !dev { + return nil, errSecretScanURL + } -func newClient(url, version string) *client { return &client{ version: version, baseURL: url, httpClient: &http.Client{ - Timeout: timeout, - Transport: nil, - CheckRedirect: nil, - Jar: nil, + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{ + Renegotiation: tls.RenegotiateFreelyAsClient, + }, + Proxy: http.ProxyFromEnvironment, + DialContext: (&net.Dialer{ + Timeout: timeout, + KeepAlive: 15 * time.Second, + }).DialContext, + TLSHandshakeTimeout: 10 * time.Second, + ExpectContinueTimeout: 1 * time.Second, + MaxIdleConns: 100, + IdleConnTimeout: 30 * time.Second, + }, + Timeout: time.Second * 30, }, - } + }, nil } // checkTokens checks if any leaked tokens exist. diff --git a/pkg/services/serviceaccounts/secretscan/service.go b/pkg/services/serviceaccounts/secretscan/service.go index 3b77067d56c..d06344d85f2 100644 --- a/pkg/services/serviceaccounts/secretscan/service.go +++ b/pkg/services/serviceaccounts/secretscan/service.go @@ -11,7 +11,7 @@ import ( "github.com/grafana/grafana/pkg/setting" ) -const defaultURL = "https://secretscan.grafana.com" +const defaultURL = "https://secret-scanning.grafana.net" type Checker interface { CheckTokens(ctx context.Context) error @@ -40,20 +40,34 @@ type Service struct { revoke bool // whether to revoke leaked tokens } -func NewService(store SATokenRetriever, cfg *setting.Cfg) *Service { +func NewService(store SATokenRetriever, cfg *setting.Cfg) (*Service, error) { secretscanBaseURL := cfg.SectionWithEnvOverrides("secretscan").Key("base_url").MustString(defaultURL) // URL to send outgoing webhook when a token is leaked. oncallURL := cfg.SectionWithEnvOverrides("secretscan").Key("oncall_url").MustString("") revoke := cfg.SectionWithEnvOverrides("secretscan").Key("revoke").MustBool(true) + client, err := newClient(secretscanBaseURL, cfg.BuildVersion, cfg.Env == setting.Dev) + if err != nil { + return nil, fmt.Errorf("failed to create secretscan client: %w", err) + } + + var webHookClient WebHookClient + if oncallURL != "" { + var errWebhook error + webHookClient, errWebhook = newWebHookClient(oncallURL, cfg.BuildVersion, cfg.Env == setting.Dev) + if errWebhook != nil { + return nil, fmt.Errorf("failed to create secretscan webhook client: %w", errWebhook) + } + } + return &Service{ store: store, - client: newClient(secretscanBaseURL, cfg.BuildVersion), - webHookClient: newWebHookClient(oncallURL, cfg.BuildVersion), + client: client, + webHookClient: webHookClient, logger: log.New("secretscan"), webHookNotify: oncallURL != "", revoke: revoke, - } + }, nil } func (s *Service) RetrieveActiveTokens(ctx context.Context) ([]apikey.APIKey, error) { diff --git a/pkg/services/serviceaccounts/secretscan/webhook.go b/pkg/services/serviceaccounts/secretscan/webhook.go index e8a84841100..fb05bf281eb 100644 --- a/pkg/services/serviceaccounts/secretscan/webhook.go +++ b/pkg/services/serviceaccounts/secretscan/webhook.go @@ -3,14 +3,20 @@ package secretscan import ( "bytes" "context" + "crypto/tls" "encoding/json" "fmt" + "net" "net/http" + "strings" + "time" "github.com/google/uuid" "github.com/pkg/errors" ) +var errWebHookURL = errors.New("webhook url must be https") + // webHookClient is a client for sending leak notifications. type webHookClient struct { httpClient *http.Client @@ -20,17 +26,32 @@ type webHookClient struct { var ErrInvalidWebHookStatusCode = errors.New("invalid webhook status code") -func newWebHookClient(url, version string) *webHookClient { +func newWebHookClient(url, version string, dev bool) (*webHookClient, error) { + if !strings.HasPrefix(url, "https://") && !dev { + return nil, errWebHookURL + } + return &webHookClient{ version: version, url: url, httpClient: &http.Client{ - Transport: nil, - CheckRedirect: nil, - Jar: nil, - Timeout: timeout, + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{ + Renegotiation: tls.RenegotiateFreelyAsClient, + }, + Proxy: http.ProxyFromEnvironment, + DialContext: (&net.Dialer{ + Timeout: timeout, + KeepAlive: 15 * time.Second, + }).DialContext, + TLSHandshakeTimeout: 10 * time.Second, + ExpectContinueTimeout: 1 * time.Second, + MaxIdleConns: 100, + IdleConnTimeout: 30 * time.Second, + }, + Timeout: time.Second * 30, }, - } + }, nil } func (wClient *webHookClient) Notify(ctx context.Context, @@ -45,7 +66,6 @@ func (wClient *webHookClient) Notify(ctx context.Context, values := map[string]interface{}{ "alert_uid": uuid.NewString(), "title": "SecretScan Alert: Grafana Token leaked", - "image_url": "https://images.pexels.com/photos/5119737/pexels-photo-5119737.jpeg?auto=compress&cs=tinysrgb&w=300", //nolint "state": "alerting", "link_to_upstream_details": token.URL, "message": "Token of type " +