diff --git a/alerts.go b/alerts.go index c8dc65f55..5d0b0ebb8 100644 --- a/alerts.go +++ b/alerts.go @@ -40,7 +40,7 @@ func getUpstreams() models.AlertmanagerAPISummary { for _, upstream := range upstreams { u := models.AlertmanagerAPIStatus{ Name: upstream.Name, - URI: upstream.URI, + URI: upstream.SanitizedURI(), Error: upstream.Error(), } summary.Instances = append(summary.Instances, u) diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index 55e39ab93..b48ff3ab0 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -67,6 +67,14 @@ alertmanager: testing with JSON files, see [mock](/internal/mock/) dir for examples, files in this directory are used for running tests and when running demo instance of unsee with `make run`. + If URI contains basic auth info + (`https://user:password@alertmanager.example.com`) and you don't want it to + be visible to users then ensure `proxy: true` is also set. + Without proxy mode full URI needs to be passed to unsee web UI code. + With proxy mode all requests will be routed via unsee HTTP server and since + unsee has full URI in the config it only needs Alertmanager name in that + request. + `proxy: true` in order to avoid leaking auth information to the browser. * `timeout` - timeout for requests send to this Alertmanager server, a string in [time.Duration](https://golang.org/pkg/time/#ParseDuration) format. * `proxy` - if enabled requests from user browsers to this Alertmanager will be diff --git a/internal/alertmanager/model_test.go b/internal/alertmanager/model_test.go new file mode 100644 index 000000000..12e11834f --- /dev/null +++ b/internal/alertmanager/model_test.go @@ -0,0 +1,62 @@ +package alertmanager + +import ( + "testing" +) + +type uriTest struct { + rawURI string + proxy bool + publicURI string +} + +var uriTests = []uriTest{ + { + rawURI: "http://alertmanager.example.com", + proxy: false, + publicURI: "http://alertmanager.example.com", + }, + { + rawURI: "http://alertmanager.example.com/foo", + proxy: false, + publicURI: "http://alertmanager.example.com/foo", + }, + { + rawURI: "http://alertmanager.example.com", + proxy: true, + publicURI: "/proxy/alertmanager/test", + }, + { + rawURI: "http://alertmanager.example.com/foo", + proxy: true, + publicURI: "/proxy/alertmanager/test", + }, + { + rawURI: "http://user:pass@alertmanager.example.com", + proxy: false, + publicURI: "http://user:pass@alertmanager.example.com", + }, + { + rawURI: "https://user:pass@alertmanager.example.com/foo", + proxy: false, + publicURI: "https://user:pass@alertmanager.example.com/foo", + }, + { + rawURI: "http://user:pass@alertmanager.example.com", + proxy: true, + publicURI: "/proxy/alertmanager/test", + }, +} + +func TestAlertmanagerURI(t *testing.T) { + for _, test := range uriTests { + am, err := NewAlertmanager("test", test.rawURI, WithProxy(test.proxy)) + if err != nil { + t.Error(err) + } + if am.publicURI() != test.publicURI { + t.Errorf("Public URI mismatch, expected '%s' => '%s', got '%s' (proxy: %v)", + test.rawURI, test.publicURI, am.publicURI(), test.proxy) + } + } +} diff --git a/internal/alertmanager/models.go b/internal/alertmanager/models.go index 5cec9914b..987cc5500 100644 --- a/internal/alertmanager/models.go +++ b/internal/alertmanager/models.go @@ -59,7 +59,7 @@ func (am *Alertmanager) detectVersion() string { url, err := uri.JoinURL(am.URI, "api/v1/status") if err != nil { - log.Errorf("Failed to join url '%s' and path 'api/v1/status': %s", am.URI, err) + log.Errorf("Failed to join url '%s' and path 'api/v1/status': %s", am.SanitizedURI(), err) return defaultVersion } @@ -68,7 +68,7 @@ func (am *Alertmanager) detectVersion() string { // read raw body from the source source, err := am.reader.Read(url) if err != nil { - log.Errorf("[%s] %s request failed: %s", am.Name, url, err) + log.Errorf("[%s] %s request failed: %s", am.Name, uri.SanitizeURI(url), err) return defaultVersion } defer source.Close() @@ -76,17 +76,17 @@ func (am *Alertmanager) detectVersion() string { // decode body as JSON err = json.NewDecoder(source).Decode(&ver) if err != nil { - log.Errorf("[%s] %s failed to decode as JSON: %s", am.Name, url, err) + log.Errorf("[%s] %s failed to decode as JSON: %s", am.Name, uri.SanitizeURI(url), err) return defaultVersion } if ver.Status != "success" { - log.Errorf("[%s] Request to %s returned status %s", am.Name, url, ver.Status) + log.Errorf("[%s] Request to %s returned status %s", am.Name, uri.SanitizeURI(url), ver.Status) return defaultVersion } if ver.Data.VersionInfo.Version == "" { - log.Errorf("[%s] No version information in Alertmanager API at %s", am.Name, url) + log.Errorf("[%s] No version information in Alertmanager API at %s", am.Name, uri.SanitizeURI(url)) return defaultVersion } @@ -125,7 +125,7 @@ func (am *Alertmanager) pullSilences(version string) error { // read raw body from the source source, err := am.reader.Read(url) if err != nil { - log.Errorf("[%s] %s request failed: %s", am.Name, url, err) + log.Errorf("[%s] %s request failed: %s", am.Name, uri.SanitizeURI(url), err) return err } defer source.Close() @@ -190,7 +190,7 @@ func (am *Alertmanager) pullAlerts(version string) error { // read raw body from the source source, err := am.reader.Read(url) if err != nil { - log.Errorf("[%s] %s request failed: %s", am.Name, url, err) + log.Errorf("[%s] %s request failed: %s", am.Name, uri.SanitizeURI(url), err) return err } defer source.Close() @@ -377,3 +377,9 @@ func (am *Alertmanager) Error() string { return am.lastError } + +// SanitizedURI returns a copy of Alertmanager.URI with password replaced by +// "xxx" +func (am *Alertmanager) SanitizedURI() string { + return uri.SanitizeURI(am.URI) +} diff --git a/internal/config/config.go b/internal/config/config.go index f6730f062..670ad8f16 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -8,6 +8,7 @@ import ( "strings" "time" + "github.com/cloudflare/unsee/internal/uri" "github.com/spf13/pflag" "github.com/spf13/viper" @@ -163,7 +164,7 @@ func (config *configSchema) LogValues() { for _, s := range cfg.Alertmanager.Servers { server := alertmanagerConfig{ Name: s.Name, - URI: hideURLPassword(s.URI), + URI: uri.SanitizeURI(s.URI), Timeout: s.Timeout, TLS: s.TLS, Proxy: s.Proxy, @@ -174,7 +175,7 @@ func (config *configSchema) LogValues() { // replace secret in Sentry DNS with 'xxx' if config.Sentry.Private != "" { - config.Sentry.Private = hideURLPassword(config.Sentry.Private) + config.Sentry.Private = uri.SanitizeURI(config.Sentry.Private) } out, err := yaml.Marshal(cfg) diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 238b25c56..f588f2e46 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -5,6 +5,7 @@ import ( "testing" "time" + "github.com/cloudflare/unsee/internal/uri" "github.com/pmezard/go-difflib/difflib" log "github.com/sirupsen/logrus" @@ -200,7 +201,7 @@ var urlSecretTests = []urlSecretTest{ func TestUrlSecretTest(t *testing.T) { for _, testCase := range urlSecretTests { - sanitized := hideURLPassword(testCase.raw) + sanitized := uri.SanitizeURI(testCase.raw) if sanitized != testCase.sanitized { t.Errorf("Invalid sanitized url, expected '%s', got '%s'", testCase.sanitized, sanitized) } diff --git a/internal/config/sanitize.go b/internal/config/sanitize.go deleted file mode 100644 index f21ec2d06..000000000 --- a/internal/config/sanitize.go +++ /dev/null @@ -1,17 +0,0 @@ -package config - -import "net/url" - -func hideURLPassword(s string) string { - u, err := url.Parse(s) - if err != nil { - return s - } - if u.User != nil { - if _, pwdSet := u.User.Password(); pwdSet { - u.User = url.UserPassword(u.User.Username(), "xxx") - } - return u.String() - } - return s -} diff --git a/internal/uri/http.go b/internal/uri/http.go index a4ced4fc0..6d9e8b2c4 100644 --- a/internal/uri/http.go +++ b/internal/uri/http.go @@ -15,7 +15,7 @@ type HTTPURIReader struct { } func (r *HTTPURIReader) Read(uri string) (io.ReadCloser, error) { - log.Infof("GET %s timeout=%s", uri, r.client.Timeout) + log.Infof("GET %s timeout=%s", SanitizeURI(uri), r.client.Timeout) request, err := http.NewRequest("GET", uri, nil) if err != nil { @@ -29,7 +29,7 @@ func (r *HTTPURIReader) Read(uri string) (io.ReadCloser, error) { } if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("Request to %s failed with %s", uri, resp.Status) + return nil, fmt.Errorf("Request to %s failed with %s", SanitizeURI(uri), resp.Status) } var reader io.ReadCloser diff --git a/internal/uri/urls.go b/internal/uri/urls.go index b9f0b416d..07c0e8035 100644 --- a/internal/uri/urls.go +++ b/internal/uri/urls.go @@ -15,3 +15,18 @@ func JoinURL(base string, sub string) (string, error) { u.Path = path.Join(u.Path, sub) return u.String(), nil } + +// SanitizeURI returns a copy of an URI string with password replaced by "xxx" +func SanitizeURI(s string) string { + u, err := url.Parse(s) + if err != nil { + return s + } + if u.User != nil { + if _, pwdSet := u.User.Password(); pwdSet { + u.User = url.UserPassword(u.User.Username(), "xxx") + } + return u.String() + } + return s +} diff --git a/internal/uri/urls_test.go b/internal/uri/urls_test.go index 9e7f97636..bc150f6ba 100644 --- a/internal/uri/urls_test.go +++ b/internal/uri/urls_test.go @@ -41,3 +41,53 @@ func TestJoinURL(t *testing.T) { } } } + +type sanitizeURITest struct { + raw string + sanitized string +} + +var sanitizeURITests = []sanitizeURITest{ + { + raw: "http://alertmanager.example.com", + sanitized: "http://alertmanager.example.com", + }, + { + raw: "http://alertmanager.example.com/foo", + sanitized: "http://alertmanager.example.com/foo", + }, + { + raw: "http://user:pass@alertmanager.example.com", + sanitized: "http://user:xxx@alertmanager.example.com", + }, + { + raw: "http://user:pass@alertmanager.example.com/foo", + sanitized: "http://user:xxx@alertmanager.example.com/foo", + }, + { + raw: "https://alertmanager.example.com", + sanitized: "https://alertmanager.example.com", + }, + { + raw: "https://alertmanager.example.com/foo", + sanitized: "https://alertmanager.example.com/foo", + }, + { + raw: "https://user:pass@alertmanager.example.com", + sanitized: "https://user:xxx@alertmanager.example.com", + }, + { + raw: "https://user:pass@alertmanager.example.com/foo", + sanitized: "https://user:xxx@alertmanager.example.com/foo", + }, +} + +func TestSanitizedURI(t *testing.T) { + for _, test := range sanitizeURITests { + s := uri.SanitizeURI(test.raw) + if s != test.sanitized { + t.Errorf("Sanitized URI mismatch, expected '%s' => '%s', got '%s'", + test.raw, test.sanitized, s) + } + } +}