authelia/internal/notification/smtp_notifier.go
James Elliott 242386e279 Force TLS and valid x509 certs in SMTP Notifier by default
- Adjust AUTH LOGIN functionality to be closer to AUTH PLAIN
- Removed: secure (notifier smtp conf) boolean string
- Added: disable_verify_cert (notifier smtp conf) boolean
    - disables X509 validation of certificates
- Added: disable_require_tls (notifier smtp conf) boolean
    - allows emails to be sent over plain text (for non-authenticated only)
- Added: trusted_cert (notifier smtp conf) string (path)
    - allows specifying the path of a PEM format cert to add to trusted cert pool
- Make SMTP notifier return errors on connection over plain text
- Make SMTP notifier return errors on TLS connection with invalid certs
- Implemented various debug logging for the SMTP notifier
- Implemented explicit SMTP closes on errors (previously left con open)
- Split SMTPNotifier Send func to seperate funcs for:
    - writing future test suites and startup checks more easily
    - organization and readability
- Add details of changes to docs/security.yml
- Adjust config.yml's (template and test) for the changes
2020-01-10 17:37:16 +01:00

258 lines
8.2 KiB
Go

package notification
import (
"crypto/tls"
"crypto/x509"
"errors"
"fmt"
"io/ioutil"
"net/smtp"
"strings"
"github.com/authelia/authelia/internal/configuration/schema"
"github.com/authelia/authelia/internal/utils"
log "github.com/sirupsen/logrus"
)
// SMTPNotifier a notifier to send emails to SMTP servers
type SMTPNotifier struct {
username string
password string
sender string
host string
port int
trustedCert string
disableVerifyCert bool
disableRequireTLS bool
address string
client *smtp.Client
tlsConfig *tls.Config
}
// NewSMTPNotifier create an SMTPNotifier targeting a given address
func NewSMTPNotifier(configuration schema.SMTPNotifierConfiguration) *SMTPNotifier {
notifier := &SMTPNotifier{
username: configuration.Username,
password: configuration.Password,
sender: configuration.Sender,
host: configuration.Host,
port: configuration.Port,
trustedCert: configuration.TrustedCert,
disableVerifyCert: configuration.DisableVerifyCert,
disableRequireTLS: configuration.DisableRequireTLS,
address: fmt.Sprintf("%s:%d", configuration.Host, configuration.Port),
}
notifier.initializeTLSConfig()
return notifier
}
func (n *SMTPNotifier) initializeTLSConfig() {
// Do not allow users to disable verification of certs if they have also set a trusted cert that was loaded
// The second part of this check happens in the Configure Cert Pool code block
log.Debug("Notifier SMTP client initializing TLS configuration")
insecureSkipVerify := false
if n.disableVerifyCert {
insecureSkipVerify = true
}
//Configure Cert Pool
certPool, err := x509.SystemCertPool()
if err != nil || certPool == nil {
certPool = x509.NewCertPool()
}
if n.trustedCert != "" {
log.Debugf("Notifier SMTP client attempting to load certificate from %s", n.trustedCert)
if exists, err := utils.FileExists(n.trustedCert); exists {
pem, err := ioutil.ReadFile(n.trustedCert)
if err != nil {
log.Warnf("Notifier SMTP failed to load cert from file with error: %s", err)
} else {
if ok := certPool.AppendCertsFromPEM(pem); !ok {
log.Warn("Notifier SMTP failed to import cert loaded from file")
} else {
log.Debug("Notifier SMTP successfully loaded certificate")
if n.disableVerifyCert {
log.Warn("Notifier SMTP when trusted_cert is specified we force disable_verify_cert to false, if you want to disable certificate validation please comment/delete trusted_cert from your config")
insecureSkipVerify = false
}
}
}
} else {
log.Warnf("Notifier SMTP failed to load cert from file (file does not exist) with error: %s", err)
}
}
n.tlsConfig = &tls.Config{
InsecureSkipVerify: insecureSkipVerify,
ServerName: n.host,
RootCAs: certPool,
}
}
// Do startTLS if available (some servers only provide the auth extension after, and encryption is preferred)
func (n *SMTPNotifier) startTLS() (bool, error) {
// Only start if not already encrypted
if _, ok := n.client.TLSConnectionState(); ok {
log.Debugf("Notifier SMTP connection is already encrypted, skipping STARTTLS")
return ok, nil
}
ok, _ := n.client.Extension("STARTTLS")
if ok {
log.Debugf("Notifier SMTP server supports STARTTLS (disableVerifyCert: %t, ServerName: %s), attempting", n.tlsConfig.InsecureSkipVerify, n.tlsConfig.ServerName)
err := n.client.StartTLS(n.tlsConfig)
if err != nil {
return ok, err
} else {
log.Debug("Notifier SMTP STARTTLS completed without error")
}
} else if n.disableRequireTLS {
log.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)")
} else {
return ok, errors.New("Notifier SMTP server does not support TLS and it is required by default (see documentation if you want to disable this highly recommended requirement)")
}
return ok, nil
}
// Attempt Authentication
func (n *SMTPNotifier) auth() (bool, error) {
// Attempt AUTH if password is specified only
if n.password != "" {
_, ok := n.client.TLSConnectionState()
if !ok {
return false, errors.New("Notifier SMTP client does not support authentication over plain text and the connection is currently plain text")
}
// Check the server supports AUTH, and get the mechanisms
ok, m := n.client.Extension("AUTH")
if ok {
log.Debugf("Notifier SMTP server supports authentication with the following mechanisms: %s", m)
mechanisms := strings.Split(m, " ")
var auth smtp.Auth
// Adaptively select the AUTH mechanism to use based on what the server advertised
if utils.IsStringInSlice("PLAIN", mechanisms) {
auth = smtp.PlainAuth("", n.username, n.password, n.host)
log.Debug("Notifier SMTP client attempting AUTH PLAIN with server")
} else if utils.IsStringInSlice("LOGIN", mechanisms) {
auth = newLoginAuth(n.username, n.password, n.host)
log.Debug("Notifier SMTP client attempting AUTH LOGIN with server")
}
// Throw error since AUTH extension is not supported
if auth == nil {
return false, fmt.Errorf("notifier SMTP server does not advertise a AUTH mechanism that are supported by Authelia (PLAIN or LOGIN are supported, but server advertised %s mechanisms)", m)
}
// Authenticate
err := n.client.Auth(auth)
if err != nil {
return false, err
} else {
log.Debug("Notifier SMTP client authenticated successfully with the server")
return true, nil
}
} else {
return false, errors.New("Notifier SMTP server does not advertise the AUTH extension but config requires AUTH (password specified), either disable AUTH, or use an SMTP host that supports AUTH PLAIN or AUTH LOGIN")
}
} else {
log.Debug("Notifier SMTP config has no password specified so authentication is being skipped")
return false, nil
}
}
func (n *SMTPNotifier) compose(recipient, subject, body string) error {
log.Debugf("Notifier SMTP client attempting to send email body to %s", recipient)
if !n.disableRequireTLS {
_, ok := n.client.TLSConnectionState()
if !ok {
return errors.New("Notifier SMTP client can't send an email over plain text connection")
}
}
wc, err := n.client.Data()
if err != nil {
log.Debugf("Notifier SMTP client error while obtaining WriteCloser: %s", err)
return err
}
msg := "From: " + n.sender + "\n" +
"To: " + recipient + "\n" +
"Subject: " + subject + "\n" +
"MIME-version: 1.0;\nContent-Type: text/html; charset=\"UTF-8\";\n\n" +
body
_, err = fmt.Fprintf(wc, msg)
if err != nil {
log.Debugf("Notifier SMTP client error while sending email body over WriteCloser: %s", err)
return err
}
err = wc.Close()
if err != nil {
log.Debugf("Notifier SMTP client error while closing the WriteCloser: %s", err)
return err
}
return nil
}
// Dial the SMTP server with the SMTPNotifier config
func (n *SMTPNotifier) dial() error {
log.Debugf("Notifier SMTP client attempting connection to %s", n.address)
client, err := smtp.Dial(n.address)
if err != nil {
return err
}
log.Debug("Notifier SMTP client connected successfully")
n.client = client
return nil
}
// Send an email
func (n *SMTPNotifier) Send(recipient, subject, body string) error {
err := n.dial()
if err != nil {
_ = n.client.Close()
return err
}
_, err = n.startTLS()
if err != nil {
_ = n.client.Close()
return err
}
_, err = n.auth()
if err != nil {
_ = n.client.Close()
return err
}
// Set the sender and recipient first
if err := n.client.Mail(n.sender); err != nil {
log.Debugf("Notifier SMTP failed while sending MAIL FROM (using sender) with error: %s", err)
_ = n.client.Close()
return err
}
if err := n.client.Rcpt(recipient); err != nil {
log.Debugf("Notifier SMTP failed while sending RCPT TO (using recipient) with error: %s", err)
_ = n.client.Close()
return err
}
// compose and send the email body
if err := n.compose(recipient, subject, body); err != nil {
_ = n.client.Close()
return err
}
// Send the QUIT command and close the connection
err = n.client.Quit()
if err != nil {
_ = n.client.Close()
return err
}
log.Debug("Notifier SMTP client successfully sent email")
return nil
}