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
pull/546/head
James Elliott 2019-12-30 13:03:51 +11:00 committed by Clément Michaud
parent 1ef3485418
commit 242386e279
23 changed files with 356 additions and 86 deletions

View File

@ -277,14 +277,26 @@ notifier:
## filesystem:
## filename: /tmp/authelia/notification.txt
# Use a SMTP server for sending notifications. Authelia uses PLAIN method to authenticate.
# [Security] Make sure the connection is made over TLS otherwise your password will transit in plain text.
# Use a SMTP server for sending notifications. Authelia uses PLAIN or LOGIN method to authenticate.
# [Security] By default Authelia will:
# - force all SMTP connections over TLS including unauthenticated connections
# - use the disable_require_tls boolean value to disable this requirement (only works for unauthenticated connections)
# - validate the SMTP server x509 certificate during the TLS handshake against the hosts trusted certificates
# - trusted_cert option:
# - this is a string value, that may specify the path of a PEM format cert, it is completely optional
# - if it is not set, a blank string, or an invalid path; will still trust the host machine/containers cert store
# - defaults to the host machine (or docker container's) trusted certificate chain for validation
# - use the trusted_cert string value to specify the path of a PEM format public cert to trust in addition to the hosts trusted certificates
# - use the disable_verify_cert boolean value to disable the validation (prefer the trusted_cert option as it's more secure)
smtp:
username: test
password: password
host: 127.0.0.1
port: 1025
sender: admin@example.com
## disable_require_tls: false
## disable_verify_cert: false
## trusted_cert: ""
# 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:

View File

@ -21,6 +21,49 @@ that the attacker must also require the certificate to retrieve the cookies.
Note that using [HSTS] has consequences. That's why you should read the blog
post nginx has written on [HSTS].
## Notifier security measures (SMTP)
By default the SMTP Notifier implementation does not allow connections that are not secure.
As such all connections require the following:
1. STARTTLS before authentication or sending emails (unauthenticated connections
require it as well)
2. Valid X509 Certificate presented to the client during the STARTTLS handshake
There is an option to disable both of these security measures however they are
not recommended. You should only do this in a situation where you control all
networks between Authelia and the SMTP server. The following configuration options
exist to configure the security level:
### Configuration Option: disable_verify_cert
This is a YAML boolean type (true/false, y/n, 1/0, etc). This disables the X509 PKI
verification mechanism. We recommend using the trusted_cert option over this, as
disabling this security feature makes you vulnerable to MITM attacks.
### Configuration Option: disable_require_tls
This is a YAML boolean type (true/false, y/n, 1/0, etc). This disables the
requirement that all connections must be over TLS. This is only usable currently
with authentication disabled (comment the password) and as such is only an
option for SMTP servers that allow unauthenticated relay (bad practice).
### Configuration Option: trusted_cert
This is a YAML string type. This specifies the file location of a pub certificate
that can be used to validate the authenticity of a server with a self signed
certificate. This can either be the public cert of the certificate authority
used to sign the certificate or the public key itself. They must be in the PEM
format. The certificate is added in addition to the certificates trusted by the
;host machine. If the certificate is invalid, inaccessible, or is otherwise not
configured; Authelia just uses the hosts certificates.
### Explanation
There are a few reasons for the security measures implemented:
1. Transmitting usernames and passwords over plain-text is an obvious vulnerability
2. The emails generated by Authelia, if transmitted in plain-text could allow
an attacker to intercept a link used to setup 2FA; which reduces security
3. Not validating the identity of the server allows man-in-the-middle attacks
## More protections measures with Nginx
You can also apply the following headers to your nginx configuration for

View File

@ -100,3 +100,4 @@ notifier:
host: "mailcatcher-service"
port: 1025
sender: admin@example.com
disable_require_tls: true

View File

@ -15,12 +15,14 @@ type EmailNotifierConfiguration struct {
// SMTPNotifierConfiguration represents the configuration of the SMTP server to send emails with.
type SMTPNotifierConfiguration struct {
Username string `yaml:"username"`
Password string `yaml:"password"`
Sender string `yaml:"sender"`
Host string `yaml:"host"`
Port int `yaml:"port"`
Secure bool `yaml:"secure"`
Username string `yaml:"username"`
Password string `yaml:"password"`
Sender string `yaml:"sender"`
Host string `yaml:"host"`
Port int `yaml:"port"`
TrustedCert string `yaml:"trusted_cert"`
DisableVerifyCert bool `yaml:"disable_verify_cert"`
DisableRequireTLS bool `yaml:"disable_require_tls"`
}
// NotifierConfiguration represents the configuration of the notifier to use when sending notifications to users.

View File

@ -115,7 +115,7 @@ notifier:
smtp:
username: test
password: password
secure: false
host: 127.0.0.1
port: 1025
sender: admin@example.com
disable_require_tls: true

View File

@ -21,7 +21,7 @@ func NewFileNotifier(configuration schema.FileSystemNotifierConfiguration) *File
}
// Send send a identity verification link to a user.
func (n *FileNotifier) Send(recipient string, subject string, body string) error {
func (n *FileNotifier) Send(recipient, subject, body string) error {
content := fmt.Sprintf("Date: %s\nRecipient: %s\nSubject: %s\nBody: %s", time.Now(), recipient, subject, body)
err := ioutil.WriteFile(n.path, []byte(content), 0755)

View File

@ -2,5 +2,5 @@ package notification
// Notifier interface for sending the identity verification link.
type Notifier interface {
Send(to string, subject string, link string) error
Send(recipient, subject, body string) error
}

View File

@ -2,6 +2,7 @@ package notification
import (
"bytes"
"errors"
"fmt"
"net/smtp"
)
@ -9,13 +10,20 @@ import (
type loginAuth struct {
username string
password string
host string
}
func LoginAuth(username, password string) smtp.Auth {
return &loginAuth{username, password}
func newLoginAuth(username, password, host string) smtp.Auth {
return &loginAuth{username, password, host}
}
func (a *loginAuth) Start(server *smtp.ServerInfo) (string, []byte, error) {
if !server.TLS && !(server.Name == "localhost" || server.Name == "127.0.0.1" || server.Name == "::1") {
return "", nil, errors.New("connection over plain-text")
}
if server.Name != a.host {
return "", nil, errors.New("unexpected hostname from server")
}
return "LOGIN", []byte{}, nil
}
@ -29,6 +37,6 @@ func (a *loginAuth) Next(fromServer []byte, more bool) ([]byte, error) {
case bytes.Equal(fromServer, []byte("Password:")):
return []byte(a.password), nil
default:
return nil, fmt.Errorf("Unexpected challenge/data from server: %s.", fromServer)
return nil, fmt.Errorf("unexpected server challenge: %s", fromServer)
}
}

View File

@ -0,0 +1,76 @@
package notification
import (
"fmt"
"net/smtp"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestFullLoginAuth(t *testing.T) {
username := "john"
password := "strongpw123"
serverInfo := &smtp.ServerInfo{
Name: "mail.authelia.com",
TLS: true,
Auth: nil,
}
auth := newLoginAuth(username, password, "mail.authelia.com")
proto, _, err := auth.Start(serverInfo)
assert.Equal(t,"LOGIN", proto)
require.NoError(t, err)
toServer, err := auth.Next([]byte("Username:"), true)
assert.Equal(t,[]byte(username), toServer)
require.NoError(t, err)
toServer, err = auth.Next([]byte("Password:"), true)
assert.Equal(t, []byte(password), toServer)
require.NoError(t, err)
toServer, err = auth.Next([]byte(nil), false)
assert.Equal(t,[]byte(nil), toServer)
require.NoError(t, err)
toServer, err = auth.Next([]byte("test"), true)
assert.Equal(t, []byte(nil), toServer)
assert.EqualError(t, err, fmt.Sprintf("unexpected server challenge: %s", []byte("test")))
}
func TestShouldHaveUnexpectedHostname(t *testing.T) {
serverInfo := &smtp.ServerInfo{
Name: "localhost",
TLS: true,
Auth: nil,
}
auth := newLoginAuth("john", "strongpw123", "mail.authelia.com")
_, _, err := auth.Start(serverInfo)
assert.EqualError(t, err, "unexpected hostname from server")
}
func TestTLSNotNeededForLocalhost(t *testing.T) {
serverInfo := &smtp.ServerInfo{
Name: "localhost",
TLS: false,
Auth: nil,
}
auth := newLoginAuth("john", "strongpw123", "localhost")
proto, _, err := auth.Start(serverInfo)
assert.Equal(t,"LOGIN", proto)
require.NoError(t, err)
}
func TestTLSNeededForNonLocalhost(t *testing.T) {
serverInfo := &smtp.ServerInfo{
Name: "mail.authelia.com",
TLS: false,
Auth: nil,
}
auth := newLoginAuth("john", "strongpw123", "mail.authelia.com")
_, _, err := auth.Start(serverInfo)
assert.EqualError(t, err, "connection over plain-text")
}

View File

@ -2,8 +2,10 @@ package notification
import (
"crypto/tls"
"crypto/x509"
"errors"
"fmt"
"io/ioutil"
"net/smtp"
"strings"
@ -12,130 +14,244 @@ import (
log "github.com/sirupsen/logrus"
)
// SMTPNotifier a notifier to send emails to SMTP servers.
// SMTPNotifier a notifier to send emails to SMTP servers
type SMTPNotifier struct {
username string
password string
sender string
host string
port int
secure bool
address string
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.
// NewSMTPNotifier create an SMTPNotifier targeting a given address
func NewSMTPNotifier(configuration schema.SMTPNotifierConfiguration) *SMTPNotifier {
return &SMTPNotifier{
username: configuration.Username,
password: configuration.Password,
sender: configuration.Sender,
host: configuration.Host,
port: configuration.Port,
secure: configuration.Secure,
address: fmt.Sprintf("%s:%d", configuration.Host, configuration.Port),
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,
}
}
// Send send a identity verification link to a user.
func (n *SMTPNotifier) Send(recipient string, subject string, body string) error {
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
c, err := smtp.Dial(n.address)
if err != nil {
return err
// 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
}
// Do StartTLS if available (some servers only provide the auth extnesion after, and encryption is preferred)
starttls, _ := c.Extension("STARTTLS")
if starttls {
tlsconfig := &tls.Config{
InsecureSkipVerify: !n.secure,
ServerName: n.host,
}
log.Debugf("SMTP server supports STARTTLS (InsecureSkipVerify: %t, ServerName: %s), attempting", tlsconfig.InsecureSkipVerify, tlsconfig.ServerName)
err := c.StartTLS(tlsconfig)
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 err
return ok, err
} else {
log.Debug("SMTP STARTTLS completed without error")
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 {
log.Debug("SMTP server does not support STARTTLS, skipping")
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 != "" {
if !starttls {
log.Warn("Authentication is being attempted over an insecure connection. Using a SMTP server that supports STARTTLS is recommended, especially if the server is not on your local network (username and pasword are being transmitted in plain-text).")
_, 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
authExtension, m := c.Extension("AUTH")
if authExtension {
log.Debugf("Config has SMTP password and server supports AUTH with the following mechanisms: %s.", m)
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("SMTP server supports AUTH PLAIN, attempting...")
log.Debug("Notifier SMTP client attempting AUTH PLAIN with server")
} else if utils.IsStringInSlice("LOGIN", mechanisms) {
auth = LoginAuth(n.username, n.password)
log.Debug("SMTP server supports AUTH LOGIN, attempting...")
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 fmt.Errorf("SMTP server does not advertise a AUTH mechanism that Authelia supports (PLAIN or LOGIN). Advertised mechanisms: %s.", m)
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 := c.Auth(auth)
err := n.client.Auth(auth)
if err != nil {
return err
return false, err
} else {
log.Debug("SMTP AUTH completed successfully.")
log.Debug("Notifier SMTP client authenticated successfully with the server")
return true, nil
}
} else {
return errors.New("SMTP server does not advertise the AUTH extension but a password was specified. Either disable auth (don't specify a password/comment the password), or specify an SMTP host and port that supports AUTH PLAIN or AUTH LOGIN.")
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("SMTP config has no password specified for use with AUTH, skipping.")
log.Debug("Notifier SMTP config has no password specified so authentication is being skipped")
return false, nil
}
}
// Set the sender and recipient first
if err := c.Mail(n.sender); err != nil {
return err
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")
}
}
if err := c.Rcpt(recipient); err != nil {
return err
}
// Send the email body.
wc, err := c.Data()
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 {
return err
}
err = wc.Close()
if err != nil {
log.Debugf("Notifier SMTP client error while sending email body over WriteCloser: %s", err)
return err
}
// Send the QUIT command and close the connection.
err = c.Quit()
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
}

View File

@ -41,3 +41,4 @@ notifier:
host: smtp
port: 1025
sender: admin@example.com
disable_require_tls: true

View File

@ -79,3 +79,4 @@ notifier:
host: smtp
port: 1025
sender: admin@example.com
disable_require_tls: true

View File

@ -97,3 +97,4 @@ notifier:
host: smtp
port: 1025
sender: admin@example.com
disable_require_tls: true

View File

@ -39,3 +39,4 @@ notifier:
host: smtp
port: 1025
sender: admin@example.com
disable_require_tls: true

View File

@ -241,3 +241,4 @@ notifier:
host: smtp
port: 1025
sender: admin@example.com
disable_require_tls: true

View File

@ -95,3 +95,4 @@ notifier:
host: smtp
port: 1025
sender: admin@example.com
disable_require_tls: true

View File

@ -65,3 +65,4 @@ notifier:
host: smtp
port: 1025
sender: admin@example.com
disable_require_tls: true

View File

@ -59,3 +59,4 @@ notifier:
host: smtp
port: 1025
sender: admin@example.com
disable_require_tls: true

View File

@ -65,3 +65,4 @@ notifier:
host: smtp
port: 1025
sender: admin@example.com
disable_require_tls: true

View File

@ -70,3 +70,4 @@ notifier:
host: smtp
port: 1025
sender: admin@example.com
disable_require_tls: true

View File

@ -79,3 +79,4 @@ notifier:
host: smtp
port: 1025
sender: admin@example.com
disable_require_tls: true

View File

@ -39,3 +39,4 @@ notifier:
host: smtp
port: 1025
sender: admin@example.com
disable_require_tls: true

View File

@ -26,7 +26,7 @@ const emailContent = `
<title>Authelia</title>
<style type="text/css">
/* Client-specific Styles */
/* client-specific Styles */
#outlook a {
padding: 0;
}