Skip to content
Snippets Groups Projects
Commit 04874d6e authored by Stuart Gathman's avatar Stuart Gathman
Browse files

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.
parent 073f87dc
No related branches found
No related tags found
No related merge requests found
HOWTO 0 → 100644
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
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.
......
#!/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 ...@@ -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 = () ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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 ...@@ -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 ...@@ -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]
......
...@@ -24,7 +24,7 @@ ALT="Viewable With Any Browser" BORDER="0"></A> ...@@ -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 ...@@ -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
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please to comment