Repository: prometheus/alertmanager
Pull request #4158 (2024-12-06 18:20:16)

Update config to allow showing secret values when marshaled
I have a deployment where a sidecar between the config-reloader and alertmanager loads and unmarshals a base configuration, extends it with more routes and receivers, and then marshals it back for the alertmanager. 

![image](https://github.com/user-attachments/assets/252a52c9-8d19-462f-b1d9-a74d5211ffbe)

However, the default Marshal implementations in the receivers hide all Secret and SecretURLs when marshaling the config back to a YAML. We have to duplicate all the receiver structs in our code to be able to switch them to plain strings and URLs.

I'd be helpful to be able to re-use the structs already here in the alertmanager. 

This is already supported by some of the Prometheus common configs by using a global `MarshalSecretValue` [here](https://github.com/prometheus/common/blob/aeb616a795915a559fe308885ab6de6101180ed6/config/config.go#L33).

For now, I'm adding support for a `MarshalSecretValue` here as well. I think the Secret in this repo could also be replaced for the Secret in Prometheus common configs, but I'd like to try this first, as switching to the common Secret would be a longer PR.


Patch:
diff --git a/config/config.go b/config/config.go
index 890592ca..f86c6563 100644
--- a/config/config.go
+++ b/config/config.go
@@ -39,6 +39,11 @@ const secretToken = "<secret>"
 
 var secretTokenJSON string
 
+// MarshalSecretValue if set to true will expose Secret type
+// through the marshal interfaces. Useful for outside projects
+// that load and marshal the Alertmanager config.
+var MarshalSecretValue bool = commoncfg.MarshalSecretValue
+
 func init() {
 	b, err := json.Marshal(secretToken)
 	if err != nil {
@@ -52,6 +57,9 @@ type Secret string
 
 // MarshalYAML implements the yaml.Marshaler interface for Secret.
 func (s Secret) MarshalYAML() (interface{}, error) {
+	if MarshalSecretValue {
+		return string(s), nil
+	}
 	if s != "" {
 		return secretToken, nil
 	}
@@ -66,6 +74,12 @@ func (s *Secret) UnmarshalYAML(unmarshal func(interface{}) error) error {
 
 // MarshalJSON implements the json.Marshaler interface for Secret.
 func (s Secret) MarshalJSON() ([]byte, error) {
+	if MarshalSecretValue {
+		return json.Marshal(string(s))
+	}
+	if len(s) == 0 {
+		return json.Marshal("")
+	}
 	return json.Marshal(secretToken)
 }
 
@@ -130,6 +144,9 @@ type SecretURL URL
 // MarshalYAML implements the yaml.Marshaler interface for SecretURL.
 func (s SecretURL) MarshalYAML() (interface{}, error) {
 	if s.URL != nil {
+		if MarshalSecretValue {
+			return s.URL.String(), nil
+		}
 		return secretToken, nil
 	}
 	return nil, nil
@@ -153,6 +170,12 @@ func (s *SecretURL) UnmarshalYAML(unmarshal func(interface{}) error) error {
 
 // MarshalJSON implements the json.Marshaler interface for SecretURL.
 func (s SecretURL) MarshalJSON() ([]byte, error) {
+	if s.URL == nil {
+		return json.Marshal("")
+	}
+	if MarshalSecretValue {
+		return json.Marshal(s.URL.String())
+	}
 	return json.Marshal(secretToken)
 }
 
@@ -167,6 +190,9 @@ func (s *SecretURL) UnmarshalJSON(data []byte) error {
 	}
 	// Redact the secret URL in case of errors
 	if err := json.Unmarshal(data, (*URL)(s)); err != nil {
+		if MarshalSecretValue {
+			return err
+		}
 		return errors.New(strings.ReplaceAll(err.Error(), string(data), "[REDACTED]"))
 	}
 


Test patch:
diff --git a/config/config_test.go b/config/config_test.go
index 15edfeed..93a525d8 100644
--- a/config/config_test.go
+++ b/config/config_test.go
@@ -521,6 +521,22 @@ func TestHideConfigSecrets(t *testing.T) {
 	}
 }
 
+func TestShowMarshalSecretValues(t *testing.T) {
+	MarshalSecretValue = true
+	defer func() { MarshalSecretValue = false }()
+
+	c, err := LoadFile("testdata/conf.good.yml")
+	if err != nil {
+		t.Fatalf("Error parsing %s: %s", "testdata/conf.good.yml", err)
+	}
+
+	// String method must reveal authentication credentials.
+	s := c.String()
+	if strings.Count(s, "<secret>") > 0 || !strings.Contains(s, "mysecret") {
+		t.Fatal("config's String method must reveal authentication credentials when MarshalSecretValue = true.")
+	}
+}
+
 func TestJSONMarshal(t *testing.T) {
 	c, err := LoadFile("testdata/conf.good.yml")
 	if err != nil {
@@ -533,7 +549,7 @@ func TestJSONMarshal(t *testing.T) {
 	}
 }
 
-func TestJSONMarshalSecret(t *testing.T) {
+func TestJSONMarshalHideSecret(t *testing.T) {
 	test := struct {
 		S Secret
 	}{
@@ -550,7 +566,24 @@ func TestJSONMarshalSecret(t *testing.T) {
 	require.Equal(t, "{\"S\":\"\\u003csecret\\u003e\"}", string(c), "Secret not properly elided.")
 }
 
-func TestMarshalSecretURL(t *testing.T) {
+func TestJSONMarshalShowSecret(t *testing.T) {
+	MarshalSecretValue = true
+	defer func() { MarshalSecretValue = false }()
+
+	test := struct {
+		S Secret
+	}{
+		S: Secret("test"),
+	}
+
+	c, err := json.Marshal(test)
+	if err != nil {
+		t.Fatal(err)
+	}
+	require.Equal(t, "{\"S\":\"test\"}", string(c), "config's String method must reveal authentication credentials when MarshalSecretValue = true.")
+}
+
+func TestJSONMarshalHideSecretURL(t *testing.T) {
 	urlp, err := url.Parse("http://example.com/")
 	if err != nil {
 		t.Fatal(err)
@@ -584,6 +617,23 @@ func TestMarshalSecretURL(t *testing.T) {
 	}
 }
 
+func TestJSONMarshalShowSecretURL(t *testing.T) {
+	MarshalSecretValue = true
+	defer func() { MarshalSecretValue = false }()
+
+	urlp, err := url.Parse("http://example.com/")
+	if err != nil {
+		t.Fatal(err)
+	}
+	u := &SecretURL{urlp}
+
+	c, err := json.Marshal(u)
+	if err != nil {
+		t.Fatal(err)
+	}
+	require.Equal(t, "\"http://example.com/\"", string(c), "config's String method must reveal authentication credentials when MarshalSecretValue = true.")
+}
+
 func TestUnmarshalSecretURL(t *testing.T) {
 	b := []byte(`"http://example.com/se cret"`)
 	var u SecretURL
@@ -611,6 +661,18 @@ func TestHideSecretURL(t *testing.T) {
 	require.NotContains(t, err.Error(), "wrongurl")
 }
 
+func TestShowMarshalSecretURL(t *testing.T) {
+	MarshalSecretValue = true
+	defer func() { MarshalSecretValue = false }()
+
+	b := []byte(`"://wrongurl/"`)
+	var u SecretURL
+
+	err := json.Unmarshal(b, &u)
+	require.Error(t, err)
+	require.Contains(t, err.Error(), "wrongurl")
+}
+
 func TestMarshalURL(t *testing.T) {
 	for name, tc := range map[string]struct {
 		input        *URL