mirror of
https://github.com/prymitive/karma
synced 2026-08-19 11:36:24 +00:00
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:
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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,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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user