1
0
mirror of https://blitiri.com.ar/repos/chasquid synced 2025-12-23 15:37:01 +00:00

courier: Add STS policy checking to the SMTP courier

This patch extends the SMTP courier to (optionally) do STS policy
checking when delivering mail.

As STS support is currently experimental, we gate this behind a flag and
is disabled by default.
This commit is contained in:
Alberto Bertogli
2017-02-26 02:32:59 +00:00
parent d66b06de51
commit 216cf47ffa
3 changed files with 98 additions and 14 deletions

View File

@@ -1,6 +1,7 @@
package main package main
import ( import (
"context"
"expvar" "expvar"
"flag" "flag"
"fmt" "fmt"
@@ -19,6 +20,7 @@ import (
"blitiri.com.ar/go/chasquid/internal/maillog" "blitiri.com.ar/go/chasquid/internal/maillog"
"blitiri.com.ar/go/chasquid/internal/normalize" "blitiri.com.ar/go/chasquid/internal/normalize"
"blitiri.com.ar/go/chasquid/internal/smtpsrv" "blitiri.com.ar/go/chasquid/internal/smtpsrv"
"blitiri.com.ar/go/chasquid/internal/sts"
"blitiri.com.ar/go/chasquid/internal/systemd" "blitiri.com.ar/go/chasquid/internal/systemd"
"blitiri.com.ar/go/chasquid/internal/userdb" "blitiri.com.ar/go/chasquid/internal/userdb"
@@ -135,12 +137,18 @@ func main() {
dinfo := s.InitDomainInfo(conf.DataDir + "/domaininfo") dinfo := s.InitDomainInfo(conf.DataDir + "/domaininfo")
stsCache, err := sts.NewCache(conf.DataDir + "/sts-cache")
if err != nil {
log.Fatalf("Failed to initialize STS cache: %v", err)
}
go stsCache.PeriodicallyRefresh(context.Background())
localC := &courier.Procmail{ localC := &courier.Procmail{
Binary: conf.MailDeliveryAgentBin, Binary: conf.MailDeliveryAgentBin,
Args: conf.MailDeliveryAgentArgs, Args: conf.MailDeliveryAgentArgs,
Timeout: 30 * time.Second, Timeout: 30 * time.Second,
} }
remoteC := &courier.SMTP{Dinfo: dinfo} remoteC := &courier.SMTP{Dinfo: dinfo, STSCache: stsCache}
s.InitQueue(conf.DataDir+"/queue", localC, remoteC) s.InitQueue(conf.DataDir+"/queue", localC, remoteC)
// Load the addresses and listeners. // Load the addresses and listeners.

View File

@@ -1,6 +1,7 @@
package courier package courier
import ( import (
"context"
"crypto/tls" "crypto/tls"
"expvar" "expvar"
"flag" "flag"
@@ -13,6 +14,7 @@ import (
"blitiri.com.ar/go/chasquid/internal/domaininfo" "blitiri.com.ar/go/chasquid/internal/domaininfo"
"blitiri.com.ar/go/chasquid/internal/envelope" "blitiri.com.ar/go/chasquid/internal/envelope"
"blitiri.com.ar/go/chasquid/internal/smtp" "blitiri.com.ar/go/chasquid/internal/smtp"
"blitiri.com.ar/go/chasquid/internal/sts"
"blitiri.com.ar/go/chasquid/internal/trace" "blitiri.com.ar/go/chasquid/internal/trace"
) )
@@ -26,6 +28,11 @@ var (
smtpPort = flag.String("testing__outgoing_smtp_port", "25", smtpPort = flag.String("testing__outgoing_smtp_port", "25",
"port to use for outgoing SMTP connections, ONLY FOR TESTING") "port to use for outgoing SMTP connections, ONLY FOR TESTING")
// Enable STS policy checking; this is an experimental flag and will be
// removed in the future, once this is made the default.
enableSTS = flag.Bool("experimental__enable_sts", false,
"enable STS policy checking; EXPERIMENTAL")
// Fake MX records, used for testing only. // Fake MX records, used for testing only.
fakeMX = map[string][]string{} fakeMX = map[string][]string{}
) )
@@ -34,20 +41,25 @@ var (
var ( var (
tlsCount = expvar.NewMap("chasquid/smtpOut/tlsCount") tlsCount = expvar.NewMap("chasquid/smtpOut/tlsCount")
slcResults = expvar.NewMap("chasquid/smtpOut/securityLevelChecks") slcResults = expvar.NewMap("chasquid/smtpOut/securityLevelChecks")
stsSecurityModes = expvar.NewMap("chasquid/smtpOut/sts/mode")
stsSecurityResults = expvar.NewMap("chasquid/smtpOut/sts/security")
) )
// SMTP delivers remote mail via outgoing SMTP. // SMTP delivers remote mail via outgoing SMTP.
type SMTP struct { type SMTP struct {
Dinfo *domaininfo.DB Dinfo *domaininfo.DB
STSCache *sts.PolicyCache
} }
func (s *SMTP) Deliver(from string, to string, data []byte) (error, bool) { func (s *SMTP) Deliver(from string, to string, data []byte) (error, bool) {
a := &attempt{ a := &attempt{
courier: s, courier: s,
from: from, from: from,
to: to, to: to,
data: data, data: data,
tr: trace.New("Courier.SMTP", to), toDomain: envelope.DomainOf(to),
tr: trace.New("Courier.SMTP", to),
} }
defer a.tr.Finish() defer a.tr.Finish()
a.tr.Debugf("%s -> %s", from, to) a.tr.Debugf("%s -> %s", from, to)
@@ -57,8 +69,9 @@ func (s *SMTP) Deliver(from string, to string, data []byte) (error, bool) {
a.from = "" a.from = ""
} }
toDomain := envelope.DomainOf(to) a.stsPolicy = s.fetchSTSPolicy(a.tr, a.toDomain)
mxs, err := lookupMXs(a.tr, toDomain)
mxs, err := lookupMXs(a.tr, a.toDomain, a.stsPolicy)
if err != nil { if err != nil {
// Note this is considered a permanent error. // Note this is considered a permanent error.
// This is in line with what other servers (Exim) do. However, the // This is in line with what other servers (Exim) do. However, the
@@ -104,6 +117,8 @@ type attempt struct {
toDomain string toDomain string
helloDomain string helloDomain string
stsPolicy *sts.Policy
tr *trace.Trace tr *trace.Trace
} }
@@ -171,6 +186,18 @@ retry:
} }
slcResults.Add("pass", 1) slcResults.Add("pass", 1)
if a.stsPolicy != nil && a.stsPolicy.Mode == sts.Enforce {
// The connection MUST be validated TLS.
// https://tools.ietf.org/html/draft-ietf-uta-mta-sts-03#section-4.2
if secLevel != domaininfo.SecLevel_TLS_SECURE {
stsSecurityResults.Add("fail", 1)
return a.tr.Errorf("invalid security level (%v) for STS policy",
secLevel), false
}
stsSecurityResults.Add("pass", 1)
a.tr.Debugf("STS policy: connection is using valid TLS")
}
if err = c.MailAndRcpt(a.from, a.to); err != nil { if err = c.MailAndRcpt(a.from, a.to); err != nil {
return a.tr.Errorf("MAIL+RCPT %v", err), smtp.IsPermanent(err) return a.tr.Errorf("MAIL+RCPT %v", err), smtp.IsPermanent(err)
} }
@@ -195,7 +222,29 @@ retry:
return nil, false return nil, false
} }
func lookupMXs(tr *trace.Trace, domain string) ([]string, error) { func (s *SMTP) fetchSTSPolicy(tr *trace.Trace, domain string) *sts.Policy {
if !*enableSTS {
return nil
}
if s.STSCache == nil {
return nil
}
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute)
defer cancel()
policy, err := s.STSCache.Fetch(ctx, domain)
if err != nil {
return nil
}
tr.Debugf("got STS policy")
stsSecurityModes.Add(string(policy.Mode), 1)
return policy
}
func lookupMXs(tr *trace.Trace, domain string, policy *sts.Policy) ([]string, error) {
if v, ok := fakeMX[domain]; ok { if v, ok := fakeMX[domain]; ok {
return v, nil return v, nil
} }
@@ -239,12 +288,39 @@ func lookupMXs(tr *trace.Trace, domain string) ([]string, error) {
// This case is explicitly covered by the SMTP RFC. // This case is explicitly covered by the SMTP RFC.
// https://tools.ietf.org/html/rfc5321#section-5.1 // https://tools.ietf.org/html/rfc5321#section-5.1
// Cap the list of MXs to 5 hosts, to keep delivery attempt times sane mxs = filterMXs(tr, policy, mxs)
// and prevent abuse. if len(mxs) == 0 {
if len(mxs) > 5 { tr.Errorf("domain %q has no valid MX/A record", domain)
} else if len(mxs) > 5 {
// Cap the list of MXs to 5 hosts, to keep delivery attempt times
// sane and prevent abuse.
mxs = mxs[:5] mxs = mxs[:5]
} }
tr.Debugf("MXs: %v", mxs) tr.Debugf("MXs: %v", mxs)
return mxs, nil return mxs, nil
} }
func filterMXs(tr *trace.Trace, p *sts.Policy, mxs []string) []string {
if p == nil {
return mxs
}
filtered := []string{}
for _, mx := range mxs {
if p.MXIsAllowed(mx) {
filtered = append(filtered, mx)
} else {
tr.Printf("MX %q not allowed by policy, skipping", mx)
}
}
// We don't want to return an empty set if the mode is not enforce.
// This prevents failures for policies in reporting mode.
// https://tools.ietf.org/html/draft-ietf-uta-mta-sts-03#section-5.2
if len(filtered) == 0 && p.Mode != sts.Enforce {
filtered = mxs
}
return filtered
}

View File

@@ -23,7 +23,7 @@ func newSMTP(t *testing.T) (*SMTP, string) {
t.Fatal(err) t.Fatal(err)
} }
return &SMTP{dinfo}, dir return &SMTP{dinfo, nil}, dir
} }
// Fake server, to test SMTP out. // Fake server, to test SMTP out.