Sanitize Alertmanager URI before putting it in /alerts.json reponse

Alertmanager URI might contain basic auth username & password, we should replace password with 'xxx' in logs and and error messages.
Go will still print it in HTTP request errors, but that will be fixed in the next Go release - https://go-review.googlesource.com/c/go/+/102855
Fixes #259
This commit is contained in:
Łukasz Mierzwa
2018-04-27 09:36:12 -07:00
parent eab4c396df
commit fd0eb46adf
10 changed files with 156 additions and 30 deletions
+1 -1
View File
@@ -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)
+8
View File
@@ -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
+62
View File
@@ -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)
}
}
}
+13 -7
View File
@@ -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)
}
+3 -2
View File
@@ -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)
+2 -1
View File
@@ -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)
}
-17
View File
@@ -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
}
+2 -2
View File
@@ -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
+15
View File
@@ -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
}
+50
View File
@@ -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)
}
}
}