From cb47973861827cf816b71264ced318544eb01cae Mon Sep 17 00:00:00 2001
From: Mar Alegre <mar@mar.alemor.org>
Date: Mon, 12 Aug 2024 16:24:42 -0400
Subject: [PATCH] add SPF checking for recieved mail

---
 mailsrv/Containerfile                   |   6 +-
 mailsrv/assets/postfix/main.cf.part     |   9 +-
 mailsrv/assets/postfix/master.cf        |   2 +
 mailsrv/assets/postfix/policyd-spf.conf | 138 ++++++++++++++++++++++++
 4 files changed, 151 insertions(+), 4 deletions(-)
 create mode 100644 mailsrv/assets/postfix/policyd-spf.conf

diff --git a/mailsrv/Containerfile b/mailsrv/Containerfile
index 49ba0d1..1c68c45 100644
--- a/mailsrv/Containerfile
+++ b/mailsrv/Containerfile
@@ -29,7 +29,7 @@ ARG FILESUID=5000
 ARG DEBIAN_FRONTEND=noninteractive
 
 # install packages we want
-RUN apt update -y && apt install -y rsyslog postfix dovecot-imapd dovecot-lmtpd dovecot-sieve cron
+RUN apt update -y && apt install -y rsyslog postfix dovecot-imapd dovecot-lmtpd dovecot-sieve cron postfix-policyd-spf-python
 
 # add virtual mail user
 RUN addgroup --gid ${FILESUID:?} vmail && \
@@ -64,6 +64,10 @@ RUN systemctl enable mkvirtual.service
 # copy postfix config
 COPY assets/postfix /etc/postfix
 
+# replace SPF server config file with symlink to the config file in the postfix dir
+RUN rm /etc/postfix-policyd-spf-python/policyd-spf.conf && \
+    ln -s /etc/postfix/policyd-spf.conf /etc/postfix-policyd-spf-python/policyd-spf.conf
+
 ###
 ### Dovecot
 ###
diff --git a/mailsrv/assets/postfix/main.cf.part b/mailsrv/assets/postfix/main.cf.part
index 4648cc4..5562eb8 100644
--- a/mailsrv/assets/postfix/main.cf.part
+++ b/mailsrv/assets/postfix/main.cf.part
@@ -74,10 +74,11 @@ smtpd_recipient_restrictions = check_recipient_access hash:/etc/postfix/deny, pe
 smtpd_client_restrictions = 
         permit_mynetworks, 
         permit_sasl_authenticated, 
-        reject_unknown_client_hostname, 
 # weaker version of reject_unknown_client_hostname
-# the strong version may cause problems with some legitimate senders (eg, Verizon)
-#        reject_unknown_reverse_client_hostname, 
+# the strong version may cause problems with some legitimate senders
+        reject_unknown_reverse_client_hostname,
+# check if sender meets SPF policy for its domain
+        check_policy_service unix:private/policy-spf,
 # reject clients from lists of known spammers
 #        reject_rbl_client zen.spamhaus.org,
 #        reject_rhsbl_reverse_client dbl.spamhaus.org,
@@ -94,3 +95,5 @@ biff = no
 compatibility_level = 2
 # maximum size allowed for sent messages, in bytes
 message_size_limit = 102400000
+# longer timeout for SPF policy server
+policy-spf_time_limit = 3600s
diff --git a/mailsrv/assets/postfix/master.cf b/mailsrv/assets/postfix/master.cf
index a95bb63..851b9c9 100644
--- a/mailsrv/assets/postfix/master.cf
+++ b/mailsrv/assets/postfix/master.cf
@@ -57,3 +57,5 @@ postlog   unix-dgram n  -       n       -       1       postlogd
 #
 dovecot   unix  -       n       n       -       -       pipe
   flags=DROhu user=vmail:vmail argv=/usr/lib/dovecot/dovecot-lda -f ${sender} -d ${user}@${nexthop}
+policy-spf  unix  -       n       n       -       -       spawn
+     user=nobody argv=/usr/bin/policyd-spf
diff --git a/mailsrv/assets/postfix/policyd-spf.conf b/mailsrv/assets/postfix/policyd-spf.conf
new file mode 100644
index 0000000..c908fe3
--- /dev/null
+++ b/mailsrv/assets/postfix/policyd-spf.conf
@@ -0,0 +1,138 @@
+#  Amount of debugging information logged.  0 logs no debugging messages
+#  5 includes all debug messages.
+debugLevel = 1 
+
+#  If set to 0, no messages are rejected by SPF.  This allows you to see the 
+#  potential impact of SPF checking in your mail logs without rejecting mail.
+TestOnly = 1
+
+#  Reject and deferred reason
+#Reason_Message = Message {rejectdefer} due to: {spf}. Please see {url}
+
+#  HELO check rejection policy. Options are:
+#  HELO_reject = SPF_Not_Pass (default) - Reject if result not Pass/None/Tempfail.
+#  HELO_reject = Softfail - Reject if result Softfail and Fail
+#  HELO_reject = Fail - Reject on HELO Fail
+#  HELO_reject = Null - Only reject HELO Fail for Null sender (SPF Classic)
+#  HELO_reject = False - Never reject/defer on HELO, append header only. 
+#  HELO_reject = No_Check - Never check HELO.
+HELO_reject = Fail
+
+#  HELO pass restriction policy.
+#  HELO_pass_restriction = helo_passed_spf - Apply the given restriction when
+#    the HELO checking result is Pass.  The given restriction must be an
+#    action as defined for a Postfix SMTP server access table access(5).
+#HELO_pass_restriction
+
+#  Mail From rejection policy.  Options are:
+#  Mail_From_reject = SPF_Not_Pass - Reject if result not Pass/None/Tempfail.
+#  Mail_From_reject = Softfail - Reject if result Softfail and Fail
+#  Mail_From_reject = Fail - Reject on Mail From Fail (default)
+#  Mail_From_reject = False - Never reject/defer on Mail From, append header only
+#  Mail_From_reject = No_Check - Never check Mail From/Return Path.
+Mail_From_reject = Fail
+
+#  Reject only from domains that send no mail. Options are:
+#  No_Mail = False - Normal SPF record processing (default)
+#  No_Mail = True - Only reject for "v=spf1 -all" records
+
+#  Mail From pass restriction policy.
+#  Mail_From_pass_restriction = mfrom_passed_spf - Apply the given
+#    restriction when the Mail From checking result is Pass.  The given 
+#    restriction must be an action as defined for a Postfix SMTP server
+#    access table access(5).
+#Mail_From_pass_restriction
+
+#  Reject mail for Netural/Softfail results for these domains.
+#  Recevier policy option to reject mail from certain domains when SPF is not
+#  Pass/None even if their SPF record does not produce a Fail result.  This
+#  Option does not change the effect of PermError_reject or TempError_Defer
+#Reject_Not_Pass_Domains = aol.com,hotmail.com
+
+#  Policy for rejecting due to SPF PermError.  Options are:
+#  PermError_reject = True
+#  PermError_reject = False
+PermError_reject = False
+
+#  Policy for deferring messages due to SPF TempError.  Options are:
+#  TempError_Defer = True
+#  TempError_Defer = False
+TempError_Defer = False
+
+#  Prospective SPF checking - Check to see if mail sent from the defined IP
+#  address would pass.
+#  Prospective = 192.168.0.4
+
+#  Do not check SPF for localhost addresses - add to skip addresses to 
+#  skip SPF for internal networks if desired. Defaults are standard IPv4 and
+#  IPv6 localhost addresses.
+skip_addresses = 127.0.0.0/8,::ffff:127.0.0.0/104,::1
+
+#  Whitelist: CIDR Notation list of IP addresses not to check SPF for.
+#  Example (default is no whitelist):
+#  Whitelist = 192.168.0.0/31,192.168.1.12
+
+# SPF HELO WHITELIST: HELO/EHLO host names to skip SPF checks for.
+# Example (default is no HELO_Whitelist):
+# HELO_Whitelist = relay.example.com,sender.example.org
+
+#  Domain_Whitelist: List of domains whose sending IPs (defined by passing
+#  their SPF check should be whitelisted from SPF.
+#  Example (default is no domain whitelist):
+#  Domain_Whitelist = pobox.com,trustedforwarder.org
+
+# Domain_Whitelist_PTR: List of domains to whitelist against SPF checks base
+# on PTR match.
+# Example (default is no PTR whitelist)
+# Domain_Whitelist_PTR = yahoo.com
+
+# SPF ENHANCED STATUS CODES: Override Postfix enhanced status codes to use the
+# RFC 7372 codes.  Disable by setting this option to "No".
+# SPF_Enhanced_Status_Codes = No
+
+# Type of header to insert to document SPF result. Can be Received-SPF (SPF)
+# or Authentication Results (AR). It cannot be both.
+# Examples: (default is Received-SPF):
+# Header_Type = AR
+# Header_Type = SPF
+
+# In order to avoid disclosing BCC recipients in SPF header fields,
+# Hide_Receiver is set to Yes by default in the interest of maximizing
+# privacy.  This setting will replace the actual recipient with <UNKNOWN> both
+# in header fields and SMTP responses.  The latter may make it more difficult
+# for senders to troubleshoot issues with their SPF deployments.
+#Hide_Receiver = No
+Hide_Receiver = Yes
+
+# Every Authentication-Results header field has an authentication identifier
+# field ('Authserv_Id'). This is similar in syntax to a fully-qualified domain
+# name. See policyd-spf.conf.5 and RFC 7001 paragraph 2.4 for details.
+# Default is HOSTNAME (as provided by socket.gethostname).  Authserv-Id must
+# be provided if Header_Type 'AR' is used.
+# Authserv_Id = mx.example.com
+Authserv_Id = HEADER
+
+# RFC 7208 recommends an elapsed time limit for SPF checks of at least 20
+# seconds.  Lookup_Time allows the maximum time (seconds) to be adjusted.  20
+# seconds is the default.
+# Lookup_Time = 20
+
+# Some of the available whitelisting mechanisms, i.e. Domain_Whitelist, 
+# Domain_Whitelist_PTR, and HELO_Whitelist, require specific non-SPF DNS
+# lookups to determine if a connection should be white listed from SPF checks.
+#  The maximum amount of time (in seconds) allocated for each of these checks,
+# when used (none are enabled by default), is controlled by the
+# Whitelist_Lookup_Time parameter.  It defaults to 10 seconds and is applied
+# independently to each whitelisting method in use. 
+# Whitelist_Lookup_Time = 10
+
+# RFC 7208 adds a new processing limit called "void lookup limit" (See section
+# 4.6.4).  Default is 2, but it can be adjusted.
+# Void_Limit = 2
+
+# In some versions of postfix, for bizarre Sendmail compatibility reasons, the
+# first header field added by a policy server is not visible to milters.  To
+# make this easy to work around, set the Mock value to true and a fixed header
+# field will be inserted so the actual SPF check will be the second field and
+# visible to milters such as DMARC milter.
+# Mock = False