Banned users option. Experimental feature to supply Sender when
missing and MFROM domain doesn't match From. Log cipher bits for SMTP AUTH. Sketch access file feature.
This commit is contained in:
@@ -0,0 +1,122 @@
|
|||||||
|
Step one. Which DSPAM is right for you?
|
||||||
|
|
||||||
|
The DSPAM project makes dspam part of the LDA (Local Delivery Agent).
|
||||||
|
Pydspam puts dspam into the MTA (Mail Transfer Agent - sendmail with pymilter).
|
||||||
|
|
||||||
|
The advantage of doing dspam in the LDA is that any aliasing has already been
|
||||||
|
resolved. You need only configure mailboxes.
|
||||||
|
|
||||||
|
The advantage of doing dspam in the MTA is it can screen an entire
|
||||||
|
company as a gateway with multiple domains. Unfortunately, this
|
||||||
|
means you have to tell it about all the aliases that comprise each
|
||||||
|
account. (Also, pydspam is still uses dspam-2.6.5.2 - the Dspam API
|
||||||
|
has changed for newer versions.)
|
||||||
|
|
||||||
|
If the LDA is right for you, you'll want to use the official Dspam
|
||||||
|
package. http://www.nuclearelephant.com/projects/dspam/
|
||||||
|
|
||||||
|
If the MTA approach is what you want, then pydspam is what you want.
|
||||||
|
|
||||||
|
In either case, you will still want pymilter to block forgeries, Windows
|
||||||
|
executables, etc.
|
||||||
|
|
||||||
|
So, lets assume you want to install pymilter, and may or may not
|
||||||
|
wish to install pydspam.
|
||||||
|
|
||||||
|
Step two. Obtaining RPMS.
|
||||||
|
|
||||||
|
For basic pymilter you'll need:
|
||||||
|
|
||||||
|
python-2.4
|
||||||
|
milter-0.8.2 (the RH9 rpm should work on Fedora Core - let me know)
|
||||||
|
sendmail-8.13.x (with milter support enabled)
|
||||||
|
|
||||||
|
and for SPF you'll need:
|
||||||
|
|
||||||
|
pydns-2.3.0-2.4
|
||||||
|
|
||||||
|
and for SRS you'll need:
|
||||||
|
|
||||||
|
pysrs-0.30.9-1.py24
|
||||||
|
|
||||||
|
I'm pretty sure you will want to have SPF and SRS available.
|
||||||
|
|
||||||
|
Step three. Activate basic milter.
|
||||||
|
|
||||||
|
Activate the basic milter by editing /etc/mail/sendmail.mc and adding:
|
||||||
|
|
||||||
|
INPUT_MAIL_FILTER(`pythonfilter', `S=local:/var/run/milter/pythonsock, F=T, T=C:5m;S:20s;R:5m;E:5m')
|
||||||
|
|
||||||
|
You can then "make sendmail.cf" and restart sendmail.
|
||||||
|
|
||||||
|
Tail /var/log/milter/milter.log while SMTP clients connect to your
|
||||||
|
sendmail instance. This should show you what the milter is doing.
|
||||||
|
|
||||||
|
By default, milter-0.8.2 rejects on SPF fail, except for listed domains
|
||||||
|
(that are known to be broken). Some admins don't like that, and 0.8.3 will use
|
||||||
|
the /etc/mail/access database to configure SPF responses. For now,
|
||||||
|
if you don't like SPF, you can disable spf by replacing "import spf"
|
||||||
|
with "spf = None" around line 285 in /var/log/milter/bms.py.
|
||||||
|
|
||||||
|
Step four. Tweaking the basic config.
|
||||||
|
|
||||||
|
Most pymilter configuration is in /etc/mail/pymilter.cfg.
|
||||||
|
|
||||||
|
By default, milter scans attachments for executable extensions. You can
|
||||||
|
turn this off by setting banned_exts to the empty list. There are options
|
||||||
|
to scan ZIP attachments and rfc822 attachments. When it finds a banned
|
||||||
|
file type, milter saves the original message in /var/log/milter/save,
|
||||||
|
and replaces the attachment with a plain text warning message.
|
||||||
|
|
||||||
|
Configure hello_blacklist with your own helo name and domains - which
|
||||||
|
you know cannot legitimately be used by external MTAs.
|
||||||
|
|
||||||
|
Configure trusted_relay with your secondary MX servers, if any. These
|
||||||
|
should also run pymilter with similar policies. (But this isn't
|
||||||
|
needed for initial testing.)
|
||||||
|
|
||||||
|
Configure internal_connect with subnets of your internal SMTP clients.
|
||||||
|
Internal connections skip SPF testing and other policies.
|
||||||
|
|
||||||
|
Configure internal_domains with domains used by your internal SMTP clients.
|
||||||
|
If they attempt to use any other domain, the attempt is blocked and the
|
||||||
|
client is logged as a "zombie". Conversely, any attempt by an external
|
||||||
|
MTA to use one of your internal domains is treated as a forgery and
|
||||||
|
blocked (a simplified form of local SPF).
|
||||||
|
|
||||||
|
Adjust porn_words and spam_words - these block emails with a Subject
|
||||||
|
containing the listed strings. They can be empty to disable Subject
|
||||||
|
string blocking.
|
||||||
|
|
||||||
|
Advanced SPF configuration.
|
||||||
|
|
||||||
|
The sendmail access file, or another readonly database with that
|
||||||
|
format, can be used for detail spf policy. SPF access policy
|
||||||
|
record are tagged with "SPF-{Result}:". Results are
|
||||||
|
Pass, Neutral, Softfail, Fail, TempError, PermError. Currently supported
|
||||||
|
policy keywords are OK, CBV, REJECT, TEMPFAIL, ERROR:"550 description".
|
||||||
|
|
||||||
|
The default policies are as follows:
|
||||||
|
|
||||||
|
SPF-Fail: REJECT
|
||||||
|
SPF-Softfail: CBV
|
||||||
|
SPF-Neutral: OK
|
||||||
|
SPF-PermError: REJECT
|
||||||
|
SPF-TempError: TEMPFAIL
|
||||||
|
SPF-Pass: OK
|
||||||
|
|
||||||
|
The tag may be followed by a specific domain. For instance, to
|
||||||
|
require a Pass from aol.com:
|
||||||
|
|
||||||
|
SPF-Neutral:aol.com ERROR:"550 AOL mail must get SPF PASS"
|
||||||
|
SPF-Softfail:aol.com ERROR:"550 AOL mail must get SPF PASS"
|
||||||
|
|
||||||
|
To be continued.
|
||||||
|
|
||||||
|
Forthcoming topics:
|
||||||
|
|
||||||
|
SRS config
|
||||||
|
|
||||||
|
|
||||||
|
pydspam config
|
||||||
|
wiretap config
|
||||||
@@ -1,7 +1,17 @@
|
|||||||
|
Find rfc2822 policy for MFROM quoting.
|
||||||
|
|
||||||
|
Use /etc/mail/access for domain specific SPF policies.
|
||||||
|
|
||||||
|
SPF-Fail: REJECT
|
||||||
|
SPF-Softfail: OK
|
||||||
|
SPF-Neutral: OK
|
||||||
|
SPF-Neutral:aol.com ERROR:"550 AOL mail must get SPF PASS"
|
||||||
|
|
||||||
Defer TEMPERROR in SPF evaluation - give precedence to security
|
Defer TEMPERROR in SPF evaluation - give precedence to security
|
||||||
(only defer for PASS mechanisms).
|
(only defer for PASS mechanisms).
|
||||||
|
|
||||||
Option to add Received-SPF header, but never reject on SPF.
|
Option to add Received-SPF header, but never reject on SPF.
|
||||||
|
I think the above will handle this.
|
||||||
|
|
||||||
Create null config that does nothing - except maybe add Received-SPF
|
Create null config that does nothing - except maybe add Received-SPF
|
||||||
headers. Many admins would like to turn features on one at a time.
|
headers. Many admins would like to turn features on one at a time.
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
# A simple milter that has grown quite a bit.
|
# A simple milter that has grown quite a bit.
|
||||||
# $Log$
|
# $Log$
|
||||||
|
# Revision 1.25 2005/09/08 03:55:08 customdesigned
|
||||||
|
# Handle perverse MFROM quoting.
|
||||||
|
#
|
||||||
# Revision 1.24 2005/08/18 03:36:54 customdesigned
|
# Revision 1.24 2005/08/18 03:36:54 customdesigned
|
||||||
# Don't innoculate with SCREENED mail.
|
# Don't innoculate with SCREENED mail.
|
||||||
#
|
#
|
||||||
@@ -312,6 +315,7 @@ scan_rfc822 = True
|
|||||||
internal_connect = ()
|
internal_connect = ()
|
||||||
trusted_relay = ()
|
trusted_relay = ()
|
||||||
internal_domains = ()
|
internal_domains = ()
|
||||||
|
banned_users = ()
|
||||||
hello_blacklist = ()
|
hello_blacklist = ()
|
||||||
smart_alias = {}
|
smart_alias = {}
|
||||||
dspam_dict = None
|
dspam_dict = None
|
||||||
@@ -332,7 +336,6 @@ spf_accept_softfail = ()
|
|||||||
spf_accept_fail = ()
|
spf_accept_fail = ()
|
||||||
spf_best_guess = False
|
spf_best_guess = False
|
||||||
spf_reject_noptr = False
|
spf_reject_noptr = False
|
||||||
multiple_bounce_recipients = True
|
|
||||||
time_format = '%Y%b%d %H:%M:%S %Z'
|
time_format = '%Y%b%d %H:%M:%S %Z'
|
||||||
timeout = 600
|
timeout = 600
|
||||||
cbv_cache = {}
|
cbv_cache = {}
|
||||||
@@ -496,7 +499,7 @@ def read_config(list):
|
|||||||
if srs_config: cp.read([srs_config])
|
if srs_config: cp.read([srs_config])
|
||||||
srs_secret = cp.getdefault('srs','secret')
|
srs_secret = cp.getdefault('srs','secret')
|
||||||
if SRS and srs_secret:
|
if SRS and srs_secret:
|
||||||
global ses,srs,srs_reject_spoofed,srs_domain
|
global ses,srs,srs_reject_spoofed,srs_domain,banned_users
|
||||||
database = cp.getdefault('srs','database')
|
database = cp.getdefault('srs','database')
|
||||||
srs_reject_spoofed = cp.getboolean('srs','reject_spoofed')
|
srs_reject_spoofed = cp.getboolean('srs','reject_spoofed')
|
||||||
maxage = cp.getint('srs','maxage')
|
maxage = cp.getint('srs','maxage')
|
||||||
@@ -515,6 +518,7 @@ def read_config(list):
|
|||||||
else:
|
else:
|
||||||
srs_domain = []
|
srs_domain = []
|
||||||
srs_domain.append(cp.getdefault('srs','fwdomain'))
|
srs_domain.append(cp.getdefault('srs','fwdomain'))
|
||||||
|
banned_users = cp.getlist('srs','banned_users')
|
||||||
#print srs_domain
|
#print srs_domain
|
||||||
|
|
||||||
def parse_addr(t):
|
def parse_addr(t):
|
||||||
@@ -537,6 +541,8 @@ def parse_addr(t):
|
|||||||
return t.split('@')
|
return t.split('@')
|
||||||
|
|
||||||
def parse_header(val):
|
def parse_header(val):
|
||||||
|
"""Decode headers gratuitously encoded to hide the content.
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
h = decode_header(val)
|
h = decode_header(val)
|
||||||
if not len(h) or (not h[0][1] and len(h) == 1): return val
|
if not len(h) or (not h[0][1] and len(h) == 1): return val
|
||||||
@@ -689,18 +695,37 @@ class bmsMilter(Milter.Milter):
|
|||||||
t = parse_addr(f)
|
t = parse_addr(f)
|
||||||
if len(t) == 2: t[1] = t[1].lower()
|
if len(t) == 2: t[1] = t[1].lower()
|
||||||
self.canon_from = '@'.join(t)
|
self.canon_from = '@'.join(t)
|
||||||
|
# Some braindead MTAs can't be relied upon to properly flag DSNs.
|
||||||
|
# This heuristic tries to recognize such.
|
||||||
|
self.is_bounce = (f == '<>' or t[0].lower() in banned_users
|
||||||
|
#and t[1] == self.hello_name
|
||||||
|
)
|
||||||
|
|
||||||
# Check SMTP AUTH, also available:
|
# Check SMTP AUTH, also available:
|
||||||
|
# auth_authen authenticated user
|
||||||
# auth_author (ESMTP AUTH= param)
|
# auth_author (ESMTP AUTH= param)
|
||||||
# auth_ssf (connection security, 0 = unencrypted)
|
# auth_ssf (connection security, 0 = unencrypted)
|
||||||
# auth_type (authentication method, CRAM-MD5, DIGEST-MD5, PLAIN, etc)
|
# auth_type (authentication method, CRAM-MD5, DIGEST-MD5, PLAIN, etc)
|
||||||
|
# cipher_bits SSL encryption strength
|
||||||
|
# cert_subject SSL cert subject
|
||||||
|
# verify SSL cert verified
|
||||||
|
|
||||||
self.user = self.getsymval('{auth_authen}')
|
self.user = self.getsymval('{auth_authen}')
|
||||||
if self.user:
|
if self.user:
|
||||||
|
# Very simple SMTP AUTH policy by defaul:
|
||||||
# any successful authentication is considered INTERNAL
|
# any successful authentication is considered INTERNAL
|
||||||
|
# FIXME: configure allowed MAIL FROM by user
|
||||||
self.internal_connection = True
|
self.internal_connection = True
|
||||||
self.log("SMTP AUTH:",self.user,
|
self.log(
|
||||||
self.getsymval('{auth_type}'),'ssf =',self.getsymval('{auth_ssf}'),
|
"SMTP AUTH:",self.user, self.getsymval('{auth_type}'),
|
||||||
"INTERNAL")
|
"sslbits =",self.getsymval('{cipher_bits}'),
|
||||||
|
"ssf =",self.getsymval('{auth_ssf}'), "INTERNAL"
|
||||||
|
)
|
||||||
|
if self.getsymval('{verify}'):
|
||||||
|
self.log("SSL AUTH:",
|
||||||
|
self.getsymval('{cert_subject}'),
|
||||||
|
"verify =",self.getsymval('{verify}')
|
||||||
|
)
|
||||||
|
|
||||||
self.fp.write('From %s %s\n' % (self.canon_from,time.ctime()))
|
self.fp.write('From %s %s\n' % (self.canon_from,time.ctime()))
|
||||||
if len(t) == 2:
|
if len(t) == 2:
|
||||||
@@ -859,11 +884,7 @@ class bmsMilter(Milter.Milter):
|
|||||||
t = parse_addr(to.lower())
|
t = parse_addr(to.lower())
|
||||||
if len(t) == 2:
|
if len(t) == 2:
|
||||||
user,domain = t
|
user,domain = t
|
||||||
if self.mailfrom == '<>' or self.canon_from.startswith('postmaster@') \
|
if self.is_bounce and srs and domain in srs_domain:
|
||||||
or self.canon_from.startswith('mailer-daemon@'):
|
|
||||||
if self.recipients and not multiple_bounce_recipients:
|
|
||||||
self.data_allowed = False
|
|
||||||
if srs and domain in srs_domain:
|
|
||||||
oldaddr = '@'.join(parse_addr(to))
|
oldaddr = '@'.join(parse_addr(to))
|
||||||
try:
|
try:
|
||||||
if ses:
|
if ses:
|
||||||
@@ -887,6 +908,7 @@ class bmsMilter(Milter.Milter):
|
|||||||
self.setreply('550','5.7.1','Invalid SES signature')
|
self.setreply('550','5.7.1','Invalid SES signature')
|
||||||
return Milter.REJECT
|
return Milter.REJECT
|
||||||
self.data_allowed = not srs_reject_spoofed
|
self.data_allowed = not srs_reject_spoofed
|
||||||
|
|
||||||
# non DSN mail to SRS address will bounce due to invalid local part
|
# non DSN mail to SRS address will bounce due to invalid local part
|
||||||
self.recipients.append('@'.join(t))
|
self.recipients.append('@'.join(t))
|
||||||
users = check_user.get(domain)
|
users = check_user.get(domain)
|
||||||
@@ -964,14 +986,19 @@ class bmsMilter(Milter.Milter):
|
|||||||
return Milter.CONTINUE
|
return Milter.CONTINUE
|
||||||
|
|
||||||
def forged_bounce(self):
|
def forged_bounce(self):
|
||||||
if len(self.recipients) > 1:
|
if self.mailfrom != '<>':
|
||||||
self.log('REJECT: Multiple bounce recipients')
|
self.log("REJECT: bogus DSN")
|
||||||
self.setreply('550','5.7.1','Multiple bounce recipients')
|
self.setreply('550','5.7.1',
|
||||||
|
"I do not accept mail from postmaster, mailer-daemon, or clamav.",
|
||||||
|
"All such mail has turned out to be Delivery Status Notifications",
|
||||||
|
"which failed to be marked as such. Please send a real DSN if",
|
||||||
|
"you need to. Use another MAIL FROM if you need to send me mail."
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
self.log('REJECT: bounce with no SRS encoding')
|
self.log('REJECT: bounce with no SRS encoding')
|
||||||
self.setreply('550','5.7.1',
|
self.setreply('550','5.7.1',
|
||||||
"I did not send you that message. Please consider implementing SPF",
|
"I did not send you that message. Please consider implementing SPF",
|
||||||
"(http://spf.pobox.com) to avoid bouncing mail to spoofed senders.",
|
"(http://openspf.com) to avoid bouncing mail to spoofed senders.",
|
||||||
"Thank you."
|
"Thank you."
|
||||||
)
|
)
|
||||||
return Milter.REJECT
|
return Milter.REJECT
|
||||||
@@ -1018,6 +1045,7 @@ class bmsMilter(Milter.Milter):
|
|||||||
self.fp.write("\n") # terminate headers
|
self.fp.write("\n") # terminate headers
|
||||||
self.fp.seek(0)
|
self.fp.seek(0)
|
||||||
# log when neither sender nor from domains matches mail from domain
|
# log when neither sender nor from domains matches mail from domain
|
||||||
|
if self.mailfrom != '<>':
|
||||||
mf_domain = self.canon_from.split('@')[-1]
|
mf_domain = self.canon_from.split('@')[-1]
|
||||||
msg = rfc822.Message(self.fp)
|
msg = rfc822.Message(self.fp)
|
||||||
for rn,hf in msg.getaddrlist('from')+msg.getaddrlist('sender'):
|
for rn,hf in msg.getaddrlist('from')+msg.getaddrlist('sender'):
|
||||||
@@ -1025,10 +1053,15 @@ class bmsMilter(Milter.Milter):
|
|||||||
if len(t) == 2 and t[1].lower() == mf_domain:
|
if len(t) == 2 and t[1].lower() == mf_domain:
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
self.log("NOTE: MFROM domain doesn't match From or Sender");
|
for f in msg.getallmatchingheaders('from'):
|
||||||
for f in msg.getallmatchingheaders('from') \
|
|
||||||
+ msg.getallmatchingheaders('sender'):
|
|
||||||
self.log(f)
|
self.log(f)
|
||||||
|
sender = msg.getallmatchingheaders('sender')
|
||||||
|
if sender:
|
||||||
|
for f in sender:
|
||||||
|
self.log(f)
|
||||||
|
else:
|
||||||
|
self.log("NOTE: Supplying MFROM as Sender");
|
||||||
|
self.add_header('Sender',self.mailfrom)
|
||||||
del msg
|
del msg
|
||||||
# copy headers to a temp file for scanning the body
|
# copy headers to a temp file for scanning the body
|
||||||
self.fp.seek(0)
|
self.fp.seek(0)
|
||||||
|
|||||||
@@ -70,6 +70,10 @@ config=/etc/mail/pysrs.cfg
|
|||||||
;fwdomain = mydomain.com
|
;fwdomain = mydomain.com
|
||||||
# turn this on after a grace period to reject spoofed DSNs
|
# turn this on after a grace period to reject spoofed DSNs
|
||||||
reject_spoofed = 0
|
reject_spoofed = 0
|
||||||
|
# Many braindead MTAs send DSNs with a non-DSN MFROM (e.g. to report that
|
||||||
|
# some virus claiming to be sent by you). This heuristic
|
||||||
|
# refuses mail from user names commonly abused in that way.
|
||||||
|
;banned_users = postmaster, mailer-daemon, clamav
|
||||||
|
|
||||||
# See http://spf.pobox.com for more info on SPF.
|
# See http://spf.pobox.com for more info on SPF.
|
||||||
[spf]
|
[spf]
|
||||||
@@ -88,6 +92,8 @@ reject_spoofed = 0
|
|||||||
# treat fail from these domains like softfail: because their SPF record
|
# treat fail from these domains like softfail: because their SPF record
|
||||||
# or an important sender is screwed up. Must have valid HELO, however.
|
# or an important sender is screwed up. Must have valid HELO, however.
|
||||||
;accept_fail = custhelp.com
|
;accept_fail = custhelp.com
|
||||||
|
# use sendmail access file or similar format for detailed spf policy
|
||||||
|
;access_file = /etc/mail/access.db
|
||||||
|
|
||||||
# features intended to clean up outgoing mail
|
# features intended to clean up outgoing mail
|
||||||
[scrub]
|
[scrub]
|
||||||
|
|||||||
+8
-7
@@ -24,7 +24,7 @@ ALT="Viewable With Any Browser" BORDER="0"></A>
|
|||||||
Stuart D. Gathman</a><br>
|
Stuart D. Gathman</a><br>
|
||||||
This web page is written by Stuart D. Gathman<br>and<br>sponsored by
|
This web page is written by Stuart D. Gathman<br>and<br>sponsored by
|
||||||
<a href="http://www.bmsi.com">Business Management Systems, Inc.</a> <br>
|
<a href="http://www.bmsi.com">Business Management Systems, Inc.</a> <br>
|
||||||
Last updated Jul 20, 2005</h4>
|
Last updated Aug 28, 2005</h4>
|
||||||
|
|
||||||
See the <a href="faq.html">FAQ</a> | <a href="http://sourceforge.net/project/showfiles.php?group_id=139894">Download now</a> |
|
See the <a href="faq.html">FAQ</a> | <a href="http://sourceforge.net/project/showfiles.php?group_id=139894">Download now</a> |
|
||||||
<a href="/mailman/listinfo/pymilter">Subscribe to mailing list</a> |
|
<a href="/mailman/listinfo/pymilter">Subscribe to mailing list</a> |
|
||||||
@@ -51,12 +51,13 @@ Python milter is being moved to
|
|||||||
<a href="http://sourceforge.net/projects/pymilter/">pymilter Sourceforge
|
<a href="http://sourceforge.net/projects/pymilter/">pymilter Sourceforge
|
||||||
project</a> for development and release downloads.
|
project</a> for development and release downloads.
|
||||||
<p>
|
<p>
|
||||||
Release 0.8.2 has changes to SPF to bring it in line with the newly
|
Release 0.8.2 has changes to <a href="http://openspf.net">SPF</a> to bring it
|
||||||
official RFC. It adds SES support (the original SES without body hash)
|
in line with the newly official RFC. It adds
|
||||||
for pysrs-0.30.10, and honeypot support for pydspam-1.1.9. There is
|
<a href="http://ses.codeshare.ca/">SES</a>
|
||||||
a new method in the base milter module. milter.set_exception_policy(i)
|
support (the original SES without body hash) for pysrs-0.30.10, and honeypot
|
||||||
lets you choose a policy of CONTINUE, REJECT, or TEMPFAIL (default) for
|
support for pydspam-1.1.9. There is a new method in the base milter module.
|
||||||
untrapped exceptions encountered in a milter callback.
|
milter.set_exception_policy(i) lets you choose a policy of CONTINUE, REJECT, or
|
||||||
|
TEMPFAIL (default) for untrapped exceptions encountered in a milter callback.
|
||||||
<p>
|
<p>
|
||||||
Release 0.8.0 is the first <a href="http://sourceforge.net/">Sourceforge</a>
|
Release 0.8.0 is the first <a href="http://sourceforge.net/">Sourceforge</a>
|
||||||
release. It supports Python-2.4, and provides an option to accept mail
|
release. It supports Python-2.4, and provides an option to accept mail
|
||||||
|
|||||||
Reference in New Issue
Block a user