Skip to content

Commit

Permalink
SMTP config: add global and local password file fields
Browse files Browse the repository at this point in the history
Add config fields (for both global email config and route-specific email
config) that specify path to file containing SMTP password.  We don't
want the password in the config file itself, and reading the password
from a k8s-secret-backed file keeps the password itself "encrypted at
rest" in etcd, and cleanly separated from the rest of the AM config.

I used the same approach as pull request #2534 "Add support to set the
Slack URL in the file"
<https://github.com/prometheus/alertmanager/pull/2534/files> in the
upstream repo.
  • Loading branch information
ericrrath committed Aug 12, 2022
1 parent c732372 commit a93e4eb
Show file tree
Hide file tree
Showing 9 changed files with 243 additions and 36 deletions.
52 changes: 31 additions & 21 deletions config/config.go
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
46 changes: 46 additions & 0 deletions config/config_test.go
Expand Up @@ -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 {
Expand Down
27 changes: 14 additions & 13 deletions config/notifiers.go
Expand Up @@ -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.
Expand Down
53 changes: 53 additions & 0 deletions 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'
50 changes: 50 additions & 0 deletions 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'
10 changes: 10 additions & 0 deletions 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'
21 changes: 21 additions & 0 deletions 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'
5 changes: 5 additions & 0 deletions docs/configuration.md
Expand Up @@ -71,6 +71,8 @@ global:
[ smtp_auth_username: <string> ]
# SMTP Auth using LOGIN and PLAIN.
[ smtp_auth_password: <secret> ]
# SMTP Auth using LOGIN and PLAIN.
[ smtp_auth_password_file: <secret> ]
# SMTP Auth using PLAIN.
[ smtp_auth_identity: <string> ]
# SMTP Auth using CRAM-MD5.
Expand Down Expand Up @@ -515,7 +517,10 @@ to: <tmpl_string>

# SMTP authentication information.
[ auth_username: <string> | 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: <secret> | default = global.smtp_auth_password ]
[ auth_password_file: <secret> | default = global.smtp_auth_password_file ]
[ auth_secret: <secret> | default = global.smtp_auth_secret ]
[ auth_identity: <string> | default = global.smtp_auth_identity ]

Expand Down
15 changes: 13 additions & 2 deletions notify/email/email.go
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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)
}

0 comments on commit a93e4eb

Please sign in to comment.