fix(notifier): use sane default connection timeout (#2273)

pull/2276/head
James Elliott 2021-08-10 10:52:41 +10:00 committed by GitHub
parent e2ebdb7e41
commit c0ebe3eb8c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 144 additions and 116 deletions

View File

@ -541,20 +541,39 @@ notifier:
## - validate the SMTP server x509 certificate during the TLS handshake against the hosts trusted certificates ## - validate the SMTP server x509 certificate during the TLS handshake against the hosts trusted certificates
## (configure in tls section) ## (configure in tls section)
smtp: smtp:
username: test ## The SMTP host to connect to.
## Password can also be set using a secret: https://www.authelia.com/docs/configuration/secrets.html
password: password
host: 127.0.0.1 host: 127.0.0.1
## The port to connect to the SMTP host on.
port: 1025 port: 1025
## The connection timeout.
timeout: 5s
## The username used for SMTP authentication.
username: test
## The password used for SMTP authentication.
## Can also be set using a secret: https://www.authelia.com/docs/configuration/secrets.html
password: password
## The address to send the email FROM.
sender: admin@example.com sender: admin@example.com
## HELO/EHLO Identifier. Some SMTP Servers may reject the default of localhost. ## HELO/EHLO Identifier. Some SMTP Servers may reject the default of localhost.
identifier: localhost identifier: localhost
## Subject configuration of the emails sent. {title} is replaced by the text from the notifier. ## Subject configuration of the emails sent. {title} is replaced by the text from the notifier.
subject: "[Authelia] {title}" subject: "[Authelia] {title}"
## This address is used during the startup check to verify the email configuration is correct. ## This address is used during the startup check to verify the email configuration is correct.
## It's not important what it is except if your email server only allows local delivery. ## It's not important what it is except if your email server only allows local delivery.
startup_check_address: test@authelia.com startup_check_address: test@authelia.com
## By default we require some form of TLS. This disables this check though is not advised.
disable_require_tls: false disable_require_tls: false
## Disables sending HTML formatted emails.
disable_html_emails: false disable_html_emails: false
tls: tls:
@ -569,16 +588,6 @@ notifier:
## Minimum TLS version for either StartTLS or SMTPS. ## Minimum TLS version for either StartTLS or SMTPS.
minimum_version: TLS1.2 minimum_version: TLS1.2
## Sending an email using a Gmail account is as simple as the next section.
## You need to create an app password by following: https://support.google.com/accounts/answer/185833?hl=en
# smtp:
# username: myaccount@gmail.com
# ## Password can also be set using a secret: https://www.authelia.com/docs/configuration/secrets.html
# password: yourapppassword
# sender: admin@example.com
# host: smtp.gmail.com
# port: 587
## ##
## Identity Providers ## Identity Providers
## ##

View File

@ -16,10 +16,11 @@ It can be configured as described below.
notifier: notifier:
disable_startup_check: false disable_startup_check: false
smtp: smtp:
username: test
password: password
host: 127.0.0.1 host: 127.0.0.1
port: 1025 port: 1025
timeout: 5s
username: test
password: password
sender: admin@example.com sender: admin@example.com
identifier: localhost identifier: localhost
subject: "[Authelia] {title}" subject: "[Authelia] {title}"
@ -34,27 +35,6 @@ notifier:
## Options ## Options
### username
<div markdown="1">
type: string
{: .label .label-config .label-purple }
required: no
{: .label .label-config .label-green }
</div>
The username sent for authentication with the SMTP server. Paired with the password.
### password
<div markdown="1">
type: string
{: .label .label-config .label-purple }
required: no
{: .label .label-config .label-green }
</div>
The password sent for authentication with the SMTP server. Paired with the username. Can also be defined using a
[secret](../secrets.md) which is the recommended for containerized deployments.
### host ### host
<div markdown="1"> <div markdown="1">
type: integer type: integer
@ -82,6 +62,39 @@ required: yes
The port the SMTP service is listening on. The port the SMTP service is listening on.
### timeout
<div markdown="1">
type: duration
{: .label .label-config .label-purple }
default: 5s
{: .label .label-config .label-blue }
required: no
{: .label .label-config .label-green }
</div>
The SMTP connection timeout.
### username
<div markdown="1">
type: string
{: .label .label-config .label-purple }
required: no
{: .label .label-config .label-green }
</div>
The username sent for authentication with the SMTP server. Paired with the password.
### password
<div markdown="1">
type: string
{: .label .label-config .label-purple }
required: no
{: .label .label-config .label-green }
</div>
The password sent for authentication with the SMTP server. Paired with the username. Can also be defined using a
[secret](../secrets.md) which is the recommended for containerized deployments.
### sender ### sender
<div markdown="1"> <div markdown="1">
type: string type: string
@ -93,7 +106,7 @@ required: no
The address sent in the FROM header for the email. Basically who the email appears to come from. It should be noted The address sent in the FROM header for the email. Basically who the email appears to come from. It should be noted
that some SMTP servers require the username provided to have access to send from the specific address listed here. that some SMTP servers require the username provided to have access to send from the specific address listed here.
### identifer ### identifier
<div markdown="1"> <div markdown="1">
type: string type: string
{: .label .label-config .label-purple } {: .label .label-config .label-purple }

View File

@ -120,7 +120,7 @@ func getProviders(config *schema.Configuration) (providers middlewares.Providers
switch { switch {
case config.Notifier.SMTP != nil: case config.Notifier.SMTP != nil:
notifier = notification.NewSMTPNotifier(*config.Notifier.SMTP, autheliaCertPool) notifier = notification.NewSMTPNotifier(config.Notifier.SMTP, autheliaCertPool)
case config.Notifier.FileSystem != nil: case config.Notifier.FileSystem != nil:
notifier = notification.NewFileNotifier(*config.Notifier.FileSystem) notifier = notification.NewFileNotifier(*config.Notifier.FileSystem)
default: default:

View File

@ -541,20 +541,39 @@ notifier:
## - validate the SMTP server x509 certificate during the TLS handshake against the hosts trusted certificates ## - validate the SMTP server x509 certificate during the TLS handshake against the hosts trusted certificates
## (configure in tls section) ## (configure in tls section)
smtp: smtp:
username: test ## The SMTP host to connect to.
## Password can also be set using a secret: https://www.authelia.com/docs/configuration/secrets.html
password: password
host: 127.0.0.1 host: 127.0.0.1
## The port to connect to the SMTP host on.
port: 1025 port: 1025
## The connection timeout.
timeout: 5s
## The username used for SMTP authentication.
username: test
## The password used for SMTP authentication.
## Can also be set using a secret: https://www.authelia.com/docs/configuration/secrets.html
password: password
## The address to send the email FROM.
sender: admin@example.com sender: admin@example.com
## HELO/EHLO Identifier. Some SMTP Servers may reject the default of localhost. ## HELO/EHLO Identifier. Some SMTP Servers may reject the default of localhost.
identifier: localhost identifier: localhost
## Subject configuration of the emails sent. {title} is replaced by the text from the notifier. ## Subject configuration of the emails sent. {title} is replaced by the text from the notifier.
subject: "[Authelia] {title}" subject: "[Authelia] {title}"
## This address is used during the startup check to verify the email configuration is correct. ## This address is used during the startup check to verify the email configuration is correct.
## It's not important what it is except if your email server only allows local delivery. ## It's not important what it is except if your email server only allows local delivery.
startup_check_address: test@authelia.com startup_check_address: test@authelia.com
## By default we require some form of TLS. This disables this check though is not advised.
disable_require_tls: false disable_require_tls: false
## Disables sending HTML formatted emails.
disable_html_emails: false disable_html_emails: false
tls: tls:
@ -569,16 +588,6 @@ notifier:
## Minimum TLS version for either StartTLS or SMTPS. ## Minimum TLS version for either StartTLS or SMTPS.
minimum_version: TLS1.2 minimum_version: TLS1.2
## Sending an email using a Gmail account is as simple as the next section.
## You need to create an app password by following: https://support.google.com/accounts/answer/185833?hl=en
# smtp:
# username: myaccount@gmail.com
# ## Password can also be set using a secret: https://www.authelia.com/docs/configuration/secrets.html
# password: yourapppassword
# sender: admin@example.com
# host: smtp.gmail.com
# port: 587
## ##
## Identity Providers ## Identity Providers
## ##

View File

@ -1,5 +1,7 @@
package schema package schema
import "time"
// FileSystemNotifierConfiguration represents the configuration of the notifier writing emails in a file. // FileSystemNotifierConfiguration represents the configuration of the notifier writing emails in a file.
type FileSystemNotifierConfiguration struct { type FileSystemNotifierConfiguration struct {
Filename string `koanf:"filename"` Filename string `koanf:"filename"`
@ -7,17 +9,18 @@ type FileSystemNotifierConfiguration struct {
// SMTPNotifierConfiguration represents the configuration of the SMTP server to send emails with. // SMTPNotifierConfiguration represents the configuration of the SMTP server to send emails with.
type SMTPNotifierConfiguration struct { type SMTPNotifierConfiguration struct {
Host string `koanf:"host"` Host string `koanf:"host"`
Port int `koanf:"port"` Port int `koanf:"port"`
Username string `koanf:"username"` Timeout time.Duration `koanf:"timeout"`
Password string `koanf:"password"` Username string `koanf:"username"`
Identifier string `koanf:"identifier"` Password string `koanf:"password"`
Sender string `koanf:"sender"` Identifier string `koanf:"identifier"`
Subject string `koanf:"subject"` Sender string `koanf:"sender"`
StartupCheckAddress string `koanf:"startup_check_address"` Subject string `koanf:"subject"`
DisableRequireTLS bool `koanf:"disable_require_tls"` StartupCheckAddress string `koanf:"startup_check_address"`
DisableHTMLEmails bool `koanf:"disable_html_emails"` DisableRequireTLS bool `koanf:"disable_require_tls"`
TLS *TLSConfig `koanf:"tls"` DisableHTMLEmails bool `koanf:"disable_html_emails"`
TLS *TLSConfig `koanf:"tls"`
} }
// NotifierConfiguration represents the configuration of the notifier to use when sending notifications to users. // NotifierConfiguration represents the configuration of the notifier to use when sending notifications to users.
@ -29,6 +32,7 @@ type NotifierConfiguration struct {
// DefaultSMTPNotifierConfiguration represents default configuration parameters for the SMTP notifier. // DefaultSMTPNotifierConfiguration represents default configuration parameters for the SMTP notifier.
var DefaultSMTPNotifierConfiguration = SMTPNotifierConfiguration{ var DefaultSMTPNotifierConfiguration = SMTPNotifierConfiguration{
Timeout: time.Second * 5,
Subject: "[Authelia] {title}", Subject: "[Authelia] {title}",
Identifier: "localhost", Identifier: "localhost",
TLS: &TLSConfig{ TLS: &TLSConfig{

View File

@ -232,6 +232,7 @@ var ValidKeys = []string{
// SMTP Notifier Keys. // SMTP Notifier Keys.
"notifier.smtp.host", "notifier.smtp.host",
"notifier.smtp.port", "notifier.smtp.port",
"notifier.smtp.timeout",
"notifier.smtp.username", "notifier.smtp.username",
"notifier.smtp.password", "notifier.smtp.password",
"notifier.smtp.identifier", "notifier.smtp.identifier",

View File

@ -42,6 +42,10 @@ func validateSMTPNotifier(configuration *schema.SMTPNotifierConfiguration, valid
validator.Push(fmt.Errorf(errFmtNotifierSMTPNotConfigured, "port")) validator.Push(fmt.Errorf(errFmtNotifierSMTPNotConfigured, "port"))
} }
if configuration.Timeout == 0 {
configuration.Timeout = schema.DefaultSMTPNotifierConfiguration.Timeout
}
if configuration.Sender == "" { if configuration.Sender == "" {
validator.Push(fmt.Errorf(errFmtNotifierSMTPNotConfigured, "sender")) validator.Push(fmt.Errorf(errFmtNotifierSMTPNotConfigured, "sender"))
} }

View File

@ -5,6 +5,7 @@ import (
"crypto/x509" "crypto/x509"
"errors" "errors"
"fmt" "fmt"
"net"
"net/smtp" "net/smtp"
"strings" "strings"
"time" "time"
@ -16,34 +17,16 @@ import (
// SMTPNotifier a notifier to send emails to SMTP servers. // SMTPNotifier a notifier to send emails to SMTP servers.
type SMTPNotifier struct { type SMTPNotifier struct {
username string configuration *schema.SMTPNotifierConfiguration
password string client *smtp.Client
sender string tlsConfig *tls.Config
identifier string
host string
port int
disableRequireTLS bool
address string
subject string
startupCheckAddress string
client *smtp.Client
tlsConfig *tls.Config
} }
// NewSMTPNotifier creates a SMTPNotifier using the notifier configuration. // NewSMTPNotifier creates a SMTPNotifier using the notifier configuration.
func NewSMTPNotifier(configuration schema.SMTPNotifierConfiguration, certPool *x509.CertPool) *SMTPNotifier { func NewSMTPNotifier(configuration *schema.SMTPNotifierConfiguration, certPool *x509.CertPool) *SMTPNotifier {
notifier := &SMTPNotifier{ notifier := &SMTPNotifier{
username: configuration.Username, configuration: configuration,
password: configuration.Password, tlsConfig: utils.NewTLSConfig(configuration.TLS, tls.VersionTLS12, certPool),
sender: configuration.Sender,
identifier: configuration.Identifier,
host: configuration.Host,
port: configuration.Port,
disableRequireTLS: configuration.DisableRequireTLS,
address: fmt.Sprintf("%s:%d", configuration.Host, configuration.Port),
subject: configuration.Subject,
startupCheckAddress: configuration.StartupCheckAddress,
tlsConfig: utils.NewTLSConfig(configuration.TLS, tls.VersionTLS12, certPool),
} }
return notifier return notifier
@ -68,7 +51,7 @@ func (n *SMTPNotifier) startTLS() error {
logger.Debug("Notifier SMTP STARTTLS completed without error") logger.Debug("Notifier SMTP STARTTLS completed without error")
default: default:
switch n.disableRequireTLS { switch n.configuration.DisableRequireTLS {
case true: case true:
logger.Warn("Notifier SMTP server does not support STARTTLS and SMTP configuration is set to disable the TLS requirement (only useful for unauthenticated emails over plain text)") logger.Warn("Notifier SMTP server does not support STARTTLS and SMTP configuration is set to disable the TLS requirement (only useful for unauthenticated emails over plain text)")
default: default:
@ -83,7 +66,7 @@ func (n *SMTPNotifier) startTLS() error {
func (n *SMTPNotifier) auth() error { func (n *SMTPNotifier) auth() error {
logger := logging.Logger() logger := logging.Logger()
// Attempt AUTH if password is specified only. // Attempt AUTH if password is specified only.
if n.password != "" { if n.configuration.Password != "" {
_, ok := n.client.TLSConnectionState() _, ok := n.client.TLSConnectionState()
if !ok { if !ok {
return errors.New("Notifier SMTP client does not support authentication over plain text and the connection is currently plain text") return errors.New("Notifier SMTP client does not support authentication over plain text and the connection is currently plain text")
@ -99,11 +82,11 @@ func (n *SMTPNotifier) auth() error {
// Adaptively select the AUTH mechanism to use based on what the server advertised. // Adaptively select the AUTH mechanism to use based on what the server advertised.
if utils.IsStringInSlice("PLAIN", mechanisms) { if utils.IsStringInSlice("PLAIN", mechanisms) {
auth = smtp.PlainAuth("", n.username, n.password, n.host) auth = smtp.PlainAuth("", n.configuration.Username, n.configuration.Password, n.configuration.Host)
logger.Debug("Notifier SMTP client attempting AUTH PLAIN with server") logger.Debug("Notifier SMTP client attempting AUTH PLAIN with server")
} else if utils.IsStringInSlice("LOGIN", mechanisms) { } else if utils.IsStringInSlice("LOGIN", mechanisms) {
auth = newLoginAuth(n.username, n.password, n.host) auth = newLoginAuth(n.configuration.Username, n.configuration.Password, n.configuration.Host)
logger.Debug("Notifier SMTP client attempting AUTH LOGIN with server") logger.Debug("Notifier SMTP client attempting AUTH LOGIN with server")
} }
@ -135,7 +118,7 @@ func (n *SMTPNotifier) compose(recipient, subject, body, htmlBody string) error
logger := logging.Logger() logger := logging.Logger()
logger.Debugf("Notifier SMTP client attempting to send email body to %s", recipient) logger.Debugf("Notifier SMTP client attempting to send email body to %s", recipient)
if !n.disableRequireTLS { if !n.configuration.DisableRequireTLS {
_, ok := n.client.TLSConnectionState() _, ok := n.client.TLSConnectionState()
if !ok { if !ok {
return errors.New("Notifier SMTP client can't send an email over plain text connection") return errors.New("Notifier SMTP client can't send an email over plain text connection")
@ -153,7 +136,7 @@ func (n *SMTPNotifier) compose(recipient, subject, body, htmlBody string) error
now := time.Now() now := time.Now()
msg := "Date:" + now.Format(rfc5322DateTimeLayout) + "\n" + msg := "Date:" + now.Format(rfc5322DateTimeLayout) + "\n" +
"From: " + n.sender + "\n" + "From: " + n.configuration.Sender + "\n" +
"To: " + recipient + "\n" + "To: " + recipient + "\n" +
"Subject: " + subject + "\n" + "Subject: " + subject + "\n" +
"MIME-version: 1.0\n" + "MIME-version: 1.0\n" +
@ -188,33 +171,40 @@ func (n *SMTPNotifier) compose(recipient, subject, body, htmlBody string) error
} }
// Dial the SMTP server with the SMTPNotifier config. // Dial the SMTP server with the SMTPNotifier config.
func (n *SMTPNotifier) dial() error { func (n *SMTPNotifier) dial() (err error) {
logger := logging.Logger() logger := logging.Logger()
logger.Debugf("Notifier SMTP client attempting connection to %s", n.address) logger.Debugf("Notifier SMTP client attempting connection to %s:%d", n.configuration.Host, n.configuration.Port)
if n.port == 465 { var (
client *smtp.Client
conn net.Conn
)
dialer := &net.Dialer{
Timeout: n.configuration.Timeout,
}
if n.configuration.Port == 465 {
logger.Infof("Notifier SMTP client using submissions port 465. Make sure the mail server you are connecting to is configured for submissions and not SMTPS.") logger.Infof("Notifier SMTP client using submissions port 465. Make sure the mail server you are connecting to is configured for submissions and not SMTPS.")
conn, err := tls.Dial("tcp", n.address, n.tlsConfig) conn, err = tls.DialWithDialer(dialer, "tcp", fmt.Sprintf("%s:%d", n.configuration.Host, n.configuration.Port), n.tlsConfig)
if err != nil { if err != nil {
return err return err
} }
client, err := smtp.NewClient(conn, n.host)
if err != nil {
return err
}
n.client = client
} else { } else {
client, err := smtp.Dial(n.address) conn, err = dialer.Dial("tcp", fmt.Sprintf("%s:%d", n.configuration.Host, n.configuration.Port))
if err != nil { if err != nil {
return err return err
} }
n.client = client
} }
client, err = smtp.NewClient(conn, n.configuration.Host)
if err != nil {
return err
}
n.client = client
logger.Debug("Notifier SMTP client connected successfully") logger.Debug("Notifier SMTP client connected successfully")
return nil return nil
@ -238,7 +228,7 @@ func (n *SMTPNotifier) StartupCheck() (bool, error) {
defer n.cleanup() defer n.cleanup()
if err := n.client.Hello(n.identifier); err != nil { if err := n.client.Hello(n.configuration.Identifier); err != nil {
return false, err return false, err
} }
@ -250,11 +240,11 @@ func (n *SMTPNotifier) StartupCheck() (bool, error) {
return false, err return false, err
} }
if err := n.client.Mail(n.sender); err != nil { if err := n.client.Mail(n.configuration.Sender); err != nil {
return false, err return false, err
} }
if err := n.client.Rcpt(n.startupCheckAddress); err != nil { if err := n.client.Rcpt(n.configuration.StartupCheckAddress); err != nil {
return false, err return false, err
} }
@ -268,7 +258,7 @@ func (n *SMTPNotifier) StartupCheck() (bool, error) {
// Send is used to send an email to a recipient. // Send is used to send an email to a recipient.
func (n *SMTPNotifier) Send(recipient, title, body, htmlBody string) error { func (n *SMTPNotifier) Send(recipient, title, body, htmlBody string) error {
logger := logging.Logger() logger := logging.Logger()
subject := strings.ReplaceAll(n.subject, "{title}", title) subject := strings.ReplaceAll(n.configuration.Subject, "{title}", title)
if err := n.dial(); err != nil { if err := n.dial(); err != nil {
return err return err
@ -277,7 +267,7 @@ func (n *SMTPNotifier) Send(recipient, title, body, htmlBody string) error {
// Always execute QUIT at the end once we're connected. // Always execute QUIT at the end once we're connected.
defer n.cleanup() defer n.cleanup()
if err := n.client.Hello(n.identifier); err != nil { if err := n.client.Hello(n.configuration.Identifier); err != nil {
return err return err
} }
@ -291,7 +281,7 @@ func (n *SMTPNotifier) Send(recipient, title, body, htmlBody string) error {
} }
// Set the sender and recipient first. // Set the sender and recipient first.
if err := n.client.Mail(n.sender); err != nil { if err := n.client.Mail(n.configuration.Sender); err != nil {
logger.Debugf("Notifier SMTP failed while sending MAIL FROM (using sender) with error: %s", err) logger.Debugf("Notifier SMTP failed while sending MAIL FROM (using sender) with error: %s", err)
return err return err
} }

View File

@ -25,12 +25,11 @@ func TestShouldConfigureSMTPNotifierWithTLS11AndDefaultHostname(t *testing.T) {
sv := schema.NewStructValidator() sv := schema.NewStructValidator()
validator.ValidateNotifier(config, sv) validator.ValidateNotifier(config, sv)
notifier := NewSMTPNotifier(*config.SMTP, nil) notifier := NewSMTPNotifier(config.SMTP, nil)
assert.Equal(t, "smtp.example.com", notifier.tlsConfig.ServerName) assert.Equal(t, "smtp.example.com", notifier.tlsConfig.ServerName)
assert.Equal(t, uint16(tls.VersionTLS11), notifier.tlsConfig.MinVersion) assert.Equal(t, uint16(tls.VersionTLS11), notifier.tlsConfig.MinVersion)
assert.False(t, notifier.tlsConfig.InsecureSkipVerify) assert.False(t, notifier.tlsConfig.InsecureSkipVerify)
assert.Equal(t, "smtp.example.com:25", notifier.address)
} }
func TestShouldConfigureSMTPNotifierWithServerNameOverrideAndDefaultTLS12(t *testing.T) { func TestShouldConfigureSMTPNotifierWithServerNameOverrideAndDefaultTLS12(t *testing.T) {
@ -48,10 +47,9 @@ func TestShouldConfigureSMTPNotifierWithServerNameOverrideAndDefaultTLS12(t *tes
sv := schema.NewStructValidator() sv := schema.NewStructValidator()
validator.ValidateNotifier(config, sv) validator.ValidateNotifier(config, sv)
notifier := NewSMTPNotifier(*config.SMTP, nil) notifier := NewSMTPNotifier(config.SMTP, nil)
assert.Equal(t, "smtp.golang.org", notifier.tlsConfig.ServerName) assert.Equal(t, "smtp.golang.org", notifier.tlsConfig.ServerName)
assert.Equal(t, uint16(tls.VersionTLS12), notifier.tlsConfig.MinVersion) assert.Equal(t, uint16(tls.VersionTLS12), notifier.tlsConfig.MinVersion)
assert.False(t, notifier.tlsConfig.InsecureSkipVerify) assert.False(t, notifier.tlsConfig.InsecureSkipVerify)
assert.Equal(t, "smtp.example.com:25", notifier.address)
} }