diff --git a/config/config.go b/config/config.go index efc1a9f3c6..394ac6afb7 100644 --- a/config/config.go +++ b/config/config.go @@ -335,6 +335,10 @@ func (c *Config) UnmarshalYAML(unmarshal func(interface{}) error) error { return fmt.Errorf("at most one of opsgenie_api_key & opsgenie_api_key_file must be configured") } + if len(c.Global.SMTPAuthPassword) > 0 && len(c.Global.SMTPAuthPasswordFile) > 0 { + return fmt.Errorf("at most one of smtp_auth_password & smtp_auth_password_file must be configured") + } + names := map[string]struct{}{} for _, rcv := range c.Receivers { @@ -365,8 +369,13 @@ func (c *Config) UnmarshalYAML(unmarshal func(interface{}) error) error { if ec.AuthUsername == "" { ec.AuthUsername = c.Global.SMTPAuthUsername } - if ec.AuthPassword == "" { + // require a password only if a username is provided + if len(ec.AuthUsername) > 0 && ec.AuthPassword == "" && ec.AuthPasswordFile == "" { + if c.Global.SMTPAuthPassword == "" && c.Global.SMTPAuthPasswordFile == "" { + return fmt.Errorf("SMTP username provided, but no global SMTP password set either inline or in a file") + } ec.AuthPassword = c.Global.SMTPAuthPassword + ec.AuthPasswordFile = c.Global.SMTPAuthPasswordFile } if ec.AuthSecret == "" { ec.AuthSecret = c.Global.SMTPAuthSecret @@ -693,26 +702,27 @@ type GlobalConfig struct { HTTPConfig *commoncfg.HTTPClientConfig `yaml:"http_config,omitempty" json:"http_config,omitempty"` - SMTPFrom string `yaml:"smtp_from,omitempty" json:"smtp_from,omitempty"` - SMTPHello string `yaml:"smtp_hello,omitempty" json:"smtp_hello,omitempty"` - SMTPSmarthost HostPort `yaml:"smtp_smarthost,omitempty" json:"smtp_smarthost,omitempty"` - SMTPAuthUsername string `yaml:"smtp_auth_username,omitempty" json:"smtp_auth_username,omitempty"` - SMTPAuthPassword Secret `yaml:"smtp_auth_password,omitempty" json:"smtp_auth_password,omitempty"` - SMTPAuthSecret Secret `yaml:"smtp_auth_secret,omitempty" json:"smtp_auth_secret,omitempty"` - SMTPAuthIdentity string `yaml:"smtp_auth_identity,omitempty" json:"smtp_auth_identity,omitempty"` - SMTPRequireTLS bool `yaml:"smtp_require_tls" json:"smtp_require_tls,omitempty"` - SlackAPIURL *SecretURL `yaml:"slack_api_url,omitempty" json:"slack_api_url,omitempty"` - SlackAPIURLFile string `yaml:"slack_api_url_file,omitempty" json:"slack_api_url_file,omitempty"` - PagerdutyURL *URL `yaml:"pagerduty_url,omitempty" json:"pagerduty_url,omitempty"` - OpsGenieAPIURL *URL `yaml:"opsgenie_api_url,omitempty" json:"opsgenie_api_url,omitempty"` - OpsGenieAPIKey Secret `yaml:"opsgenie_api_key,omitempty" json:"opsgenie_api_key,omitempty"` - OpsGenieAPIKeyFile string `yaml:"opsgenie_api_key_file,omitempty" json:"opsgenie_api_key_file,omitempty"` - WeChatAPIURL *URL `yaml:"wechat_api_url,omitempty" json:"wechat_api_url,omitempty"` - WeChatAPISecret Secret `yaml:"wechat_api_secret,omitempty" json:"wechat_api_secret,omitempty"` - WeChatAPICorpID string `yaml:"wechat_api_corp_id,omitempty" json:"wechat_api_corp_id,omitempty"` - VictorOpsAPIURL *URL `yaml:"victorops_api_url,omitempty" json:"victorops_api_url,omitempty"` - VictorOpsAPIKey Secret `yaml:"victorops_api_key,omitempty" json:"victorops_api_key,omitempty"` - TelegramAPIUrl *URL `yaml:"telegram_api_url,omitempty" json:"telegram_api_url,omitempty"` + SMTPFrom string `yaml:"smtp_from,omitempty" json:"smtp_from,omitempty"` + SMTPHello string `yaml:"smtp_hello,omitempty" json:"smtp_hello,omitempty"` + SMTPSmarthost HostPort `yaml:"smtp_smarthost,omitempty" json:"smtp_smarthost,omitempty"` + SMTPAuthUsername string `yaml:"smtp_auth_username,omitempty" json:"smtp_auth_username,omitempty"` + SMTPAuthPassword Secret `yaml:"smtp_auth_password,omitempty" json:"smtp_auth_password,omitempty"` + SMTPAuthPasswordFile Secret `yaml:"smtp_auth_password_file,omitempty" json:"smtp_auth_password_file,omitempty"` + SMTPAuthSecret Secret `yaml:"smtp_auth_secret,omitempty" json:"smtp_auth_secret,omitempty"` + SMTPAuthIdentity string `yaml:"smtp_auth_identity,omitempty" json:"smtp_auth_identity,omitempty"` + SMTPRequireTLS bool `yaml:"smtp_require_tls" json:"smtp_require_tls,omitempty"` + SlackAPIURL *SecretURL `yaml:"slack_api_url,omitempty" json:"slack_api_url,omitempty"` + SlackAPIURLFile string `yaml:"slack_api_url_file,omitempty" json:"slack_api_url_file,omitempty"` + PagerdutyURL *URL `yaml:"pagerduty_url,omitempty" json:"pagerduty_url,omitempty"` + OpsGenieAPIURL *URL `yaml:"opsgenie_api_url,omitempty" json:"opsgenie_api_url,omitempty"` + OpsGenieAPIKey Secret `yaml:"opsgenie_api_key,omitempty" json:"opsgenie_api_key,omitempty"` + OpsGenieAPIKeyFile string `yaml:"opsgenie_api_key_file,omitempty" json:"opsgenie_api_key_file,omitempty"` + WeChatAPIURL *URL `yaml:"wechat_api_url,omitempty" json:"wechat_api_url,omitempty"` + WeChatAPISecret Secret `yaml:"wechat_api_secret,omitempty" json:"wechat_api_secret,omitempty"` + WeChatAPICorpID string `yaml:"wechat_api_corp_id,omitempty" json:"wechat_api_corp_id,omitempty"` + VictorOpsAPIURL *URL `yaml:"victorops_api_url,omitempty" json:"victorops_api_url,omitempty"` + VictorOpsAPIKey Secret `yaml:"victorops_api_key,omitempty" json:"victorops_api_key,omitempty"` + TelegramAPIUrl *URL `yaml:"telegram_api_url,omitempty" json:"telegram_api_url,omitempty"` } // UnmarshalYAML implements the yaml.Unmarshaler interface for GlobalConfig. diff --git a/config/config_test.go b/config/config_test.go index 7e7a1fee6e..9ccd03b2ee 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -965,6 +965,52 @@ func TestSMTPHello(t *testing.T) { } } +func TestSMTPBothPasswordAndFile(t *testing.T) { + _, err := LoadFile("testdata/conf.smtp-both-password-and-file.yml") + if err == nil { + t.Fatalf("Expected an error parsing %s: %s", "testdata/conf.smtp-both-password-and-file.yml", err) + } + if err.Error() != "at most one of smtp_auth_password & smtp_auth_password_file must be configured" { + t.Errorf("Expected: %s\nGot: %s", "at most one of auth_password & auth_password_file must be configured", err.Error()) + } +} + +func TestSMTPNoUsernameOrPassword(t *testing.T) { + _, err := LoadFile("testdata/conf.smtp-no-username-or-password.yml") + if err != nil { + t.Fatalf("Error parsing %s: %s", "testdata/conf.smtp-no-username-or-password.yml", err) + } +} + +func TestSMTPNoPassword(t *testing.T) { + _, err := LoadFile("testdata/conf.smtp-no-password.yml") + if err == nil { + t.Fatalf("Expected an error parsing %s: %s", "testdata/conf.smtp-no-password.yml", err) + } + if err.Error() != "SMTP username provided, but no global SMTP password set either inline or in a file" { + t.Errorf("Expected: %s\nGot: %s", "no global SMTP password set either inline or in a file", err.Error()) + } +} + +func TestGlobalAndLocalSMTPPassword(t *testing.T) { + config, err := LoadFile("testdata/conf.smtp-password-global-and-local.yml") + if err != nil { + t.Fatalf("Error parsing %s: %s", "testdata/conf.smtp-password-global-and-local.yml", err) + } + + if config.Receivers[0].EmailConfigs[0].AuthPasswordFile != "/tmp/globaluserpassword" { + t.Fatalf("first email should use password file /tmp/globaluserpassword") + } + + if config.Receivers[0].EmailConfigs[1].AuthPasswordFile != "/tmp/localuser1password" { + t.Fatalf("second email should use password file /tmp/localuser1password") + } + + if config.Receivers[0].EmailConfigs[2].AuthPassword != "mysecret" { + t.Fatalf("third email should use password mysecret") + } +} + func TestGroupByAll(t *testing.T) { c, err := LoadFile("testdata/conf.group-by-all.yml") if err != nil { diff --git a/config/notifiers.go b/config/notifiers.go index 95df28db43..6f72f46791 100644 --- a/config/notifiers.go +++ b/config/notifiers.go @@ -165,19 +165,20 @@ type EmailConfig struct { NotifierConfig `yaml:",inline" json:",inline"` // Email address to notify. - To string `yaml:"to,omitempty" json:"to,omitempty"` - From string `yaml:"from,omitempty" json:"from,omitempty"` - Hello string `yaml:"hello,omitempty" json:"hello,omitempty"` - Smarthost HostPort `yaml:"smarthost,omitempty" json:"smarthost,omitempty"` - AuthUsername string `yaml:"auth_username,omitempty" json:"auth_username,omitempty"` - AuthPassword Secret `yaml:"auth_password,omitempty" json:"auth_password,omitempty"` - AuthSecret Secret `yaml:"auth_secret,omitempty" json:"auth_secret,omitempty"` - AuthIdentity string `yaml:"auth_identity,omitempty" json:"auth_identity,omitempty"` - Headers map[string]string `yaml:"headers,omitempty" json:"headers,omitempty"` - HTML string `yaml:"html,omitempty" json:"html,omitempty"` - Text string `yaml:"text,omitempty" json:"text,omitempty"` - RequireTLS *bool `yaml:"require_tls,omitempty" json:"require_tls,omitempty"` - TLSConfig commoncfg.TLSConfig `yaml:"tls_config,omitempty" json:"tls_config,omitempty"` + To string `yaml:"to,omitempty" json:"to,omitempty"` + From string `yaml:"from,omitempty" json:"from,omitempty"` + Hello string `yaml:"hello,omitempty" json:"hello,omitempty"` + Smarthost HostPort `yaml:"smarthost,omitempty" json:"smarthost,omitempty"` + AuthUsername string `yaml:"auth_username,omitempty" json:"auth_username,omitempty"` + AuthPassword Secret `yaml:"auth_password,omitempty" json:"auth_password,omitempty"` + AuthPasswordFile Secret `yaml:"auth_password_file,omitempty" json:"auth_password_file,omitempty"` + AuthSecret Secret `yaml:"auth_secret,omitempty" json:"auth_secret,omitempty"` + AuthIdentity string `yaml:"auth_identity,omitempty" json:"auth_identity,omitempty"` + Headers map[string]string `yaml:"headers,omitempty" json:"headers,omitempty"` + HTML string `yaml:"html,omitempty" json:"html,omitempty"` + Text string `yaml:"text,omitempty" json:"text,omitempty"` + RequireTLS *bool `yaml:"require_tls,omitempty" json:"require_tls,omitempty"` + TLSConfig commoncfg.TLSConfig `yaml:"tls_config,omitempty" json:"tls_config,omitempty"` } // UnmarshalYAML implements the yaml.Unmarshaler interface. diff --git a/config/testdata/conf.smtp-both-password-and-file.yml b/config/testdata/conf.smtp-both-password-and-file.yml new file mode 100644 index 0000000000..9cb0cd108f --- /dev/null +++ b/config/testdata/conf.smtp-both-password-and-file.yml @@ -0,0 +1,53 @@ +global: + smtp_smarthost: 'localhost:25' + smtp_from: 'alertmanager@example.org' + smtp_auth_username: 'alertmanager' + smtp_auth_password: "multiline\nmysecret" + smtp_auth_password_file: "/tmp/global" + smtp_hello: "host.example.org" +route: + group_by: ['alertname', 'cluster', 'service'] + group_wait: 30s + group_interval: 5m + repeat_interval: 3h + receiver: team-X-mails + routes: + - match_re: + service: ^(foo1|foo2|baz)$ + receiver: team-X-mails + routes: + - match: + severity: critical + receiver: team-X-pager + - match: + service: files + receiver: team-Y-mails + routes: + - match: + severity: critical + receiver: team-Y-pager + - match: + service: database + receiver: team-DB-pager + group_by: [alertname, cluster, database] + routes: + - match: + owner2: team-X + receiver: team-X-pager + continue: true + - match: + owner: team-Y + receiver: team-Y-pager + # continue: true +receivers: + - name: 'team-X-mails' + email_configs: + - to: 'team-X+alerts@example.org' + - name: 'team-X-pager' + email_configs: + - to: 'team-X+alerts-critical@example.org' + pagerduty_configs: + - routing_key: "mysecret" + - name: 'team-Y-mails' + email_configs: + - to: 'team-Y+alerts@example.org' diff --git a/config/testdata/conf.smtp-no-password.yml b/config/testdata/conf.smtp-no-password.yml new file mode 100644 index 0000000000..a82cafef87 --- /dev/null +++ b/config/testdata/conf.smtp-no-password.yml @@ -0,0 +1,50 @@ +global: + smtp_smarthost: 'localhost:25' + smtp_from: 'alertmanager@example.org' + smtp_auth_username: 'alertmanager' + smtp_hello: "host.example.org" +route: + group_by: ['alertname', 'cluster', 'service'] + group_wait: 30s + group_interval: 5m + repeat_interval: 3h + receiver: team-X-mails + routes: + - match_re: + service: ^(foo1|foo2|baz)$ + receiver: team-X-mails + routes: + - match: + severity: critical + receiver: team-X-pager + - match: + service: files + receiver: team-Y-mails + routes: + - match: + severity: critical + receiver: team-Y-pager + - match: + service: database + receiver: team-DB-pager + group_by: [alertname, cluster, database] + routes: + - match: + owner2: team-X + receiver: team-X-pager + continue: true + - match: + owner: team-Y + receiver: team-Y-pager +receivers: + - name: 'team-X-mails' + email_configs: + - to: 'team-X+alerts@example.org' + - name: 'team-X-pager' + email_configs: + - to: 'team-X+alerts-critical@example.org' + pagerduty_configs: + - routing_key: "mysecret" + - name: 'team-Y-mails' + email_configs: + - to: 'team-Y+alerts@example.org' diff --git a/config/testdata/conf.smtp-no-username-or-password.yml b/config/testdata/conf.smtp-no-username-or-password.yml new file mode 100644 index 0000000000..52ec672a59 --- /dev/null +++ b/config/testdata/conf.smtp-no-username-or-password.yml @@ -0,0 +1,10 @@ +global: + smtp_smarthost: 'localhost:25' + smtp_from: 'alertmanager@example.org' + smtp_hello: "host.example.org" +route: + receiver: 'email-notifications' +receivers: + - name: 'email-notifications' + email_configs: + - to: 'one@example.org' diff --git a/config/testdata/conf.smtp-password-global-and-local.yml b/config/testdata/conf.smtp-password-global-and-local.yml new file mode 100644 index 0000000000..ac077b4a2a --- /dev/null +++ b/config/testdata/conf.smtp-password-global-and-local.yml @@ -0,0 +1,21 @@ +global: + smtp_smarthost: 'localhost:25' + smtp_from: 'alertmanager@example.org' + smtp_auth_username: 'globaluser' + smtp_auth_password_file: '/tmp/globaluserpassword' + smtp_hello: "host.example.org" +route: + receiver: 'email-notifications' +receivers: + - name: 'email-notifications' + email_configs: + # Use global + - to: 'one@example.org' + # Override global with other file + - to: 'two@example.org' + auth_username: 'localuser1' + auth_password_file: '/tmp/localuser1password' + # Override global with inline password + - to: 'three@example.org' + auth_username: 'localuser2' + auth_password: 'mysecret' diff --git a/docs/configuration.md b/docs/configuration.md index 2f468d7a1d..94202413b9 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -71,6 +71,8 @@ global: [ smtp_auth_username: ] # SMTP Auth using LOGIN and PLAIN. [ smtp_auth_password: ] + # SMTP Auth using LOGIN and PLAIN. + [ smtp_auth_password_file: ] # SMTP Auth using PLAIN. [ smtp_auth_identity: ] # SMTP Auth using CRAM-MD5. @@ -515,7 +517,10 @@ to: # SMTP authentication information. [ auth_username: | default = global.smtp_auth_username ] +# The SMTP password. Either auth_password or auth_password_file should be set. +# Defaults to global settings if none are set here. [ auth_password: | default = global.smtp_auth_password ] +[ auth_password_file: | default = global.smtp_auth_password_file ] [ auth_secret: | default = global.smtp_auth_secret ] [ auth_identity: | default = global.smtp_auth_identity ] diff --git a/notify/email/email.go b/notify/email/email.go index eebf20c7c6..25e2b9915a 100644 --- a/notify/email/email.go +++ b/notify/email/email.go @@ -91,7 +91,7 @@ func (n *Email) auth(mechs string) (smtp.Auth, error) { return smtp.CRAMMD5Auth(username, secret), nil case "PLAIN": - password := string(n.conf.AuthPassword) + password := n.getPassword() if password == "" { err.Add(errors.New("missing password for PLAIN auth mechanism")) continue @@ -100,7 +100,7 @@ func (n *Email) auth(mechs string) (smtp.Auth, error) { return smtp.PlainAuth(identity, username, password, n.conf.Smarthost.Host), nil case "LOGIN": - password := string(n.conf.AuthPassword) + password := n.getPassword() if password == "" { err.Add(errors.New("missing password for LOGIN auth mechanism")) continue @@ -353,3 +353,14 @@ func (a *loginAuth) Next(fromServer []byte, more bool) ([]byte, error) { } return nil, nil } + +func (n *Email) getPassword() string { + if len(n.conf.AuthPassword) > 0 { + return string(n.conf.AuthPassword) + } + content, err := os.ReadFile(string(n.conf.AuthPasswordFile)) + if err != nil { + return "" + } + return string(content) +}