From 726afb8b95f7759724c487f1163d582119333332 Mon Sep 17 00:00:00 2001 From: Jacob Hoffman-Andrews Date: Fri, 17 Oct 2014 15:28:32 -0400 Subject: [PATCH 1/5] Install CAfile on config generation. --- MTAConfigGenerator.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/MTAConfigGenerator.py b/MTAConfigGenerator.py index a733ab27a..051d9676d 100755 --- a/MTAConfigGenerator.py +++ b/MTAConfigGenerator.py @@ -28,11 +28,16 @@ class PostfixConfigGenerator(MTAConfigGenerator): def __init__(self, policy_config, postfix_dir, fixup=False): self.fixup = fixup self.postfix_dir = postfix_dir - self.policy_file = os.path.join(postfix_dir, "starttls_everywhere_policy") - MTAConfigGenerator.__init__(self, policy_config) self.postfix_cf_file = self.find_postfix_cf() + if not os.access(self.postfix_cf_file, os.W_OK): + raise Exception("Can't write to %s, please re-run as root." + % self.postfix_cf_file) + self.policy_file = os.path.join(postfix_dir, "starttls_everywhere_policy") + self.ca_file = os.path.join(postfix_dir, "starttls_everywhere_CAfile") + MTAConfigGenerator.__init__(self, policy_config) self.wrangle_existing_config() self.set_domainwise_tls_policies() + self.update_CAfile() os.system("sudo service postfix reload") def ensure_cf_var(self, var, ideal, also_acceptable): @@ -49,7 +54,6 @@ class PostfixConfigGenerator(MTAConfigGenerator): values = map(parse_line, l) if len(set(values)) > 1: if self.fixup: - #print "Scheduling deletions:" + `values` conflicting_lines = [num for num,_var,val in values] self.deletions.extend(conflicting_lines) self.additions.append(var + " = " + ideal) @@ -57,7 +61,6 @@ class PostfixConfigGenerator(MTAConfigGenerator): raise ExistingConfigError, "Conflicting existing config values " + `l` val = values[0][2] if val not in acceptable: - #print "Scheduling deletions:" + `values` if self.fixup: self.deletions.append(values[0][0]) self.additions.append(var + " = " + ideal) @@ -86,6 +89,7 @@ class PostfixConfigGenerator(MTAConfigGenerator): policy_cf_entry = "texthash:" + self.policy_file self.ensure_cf_var("smtp_tls_policy_maps", policy_cf_entry, []) + self.ensure_cf_var("smtp_tls_CAfile", self.ca_file, []) self.maybe_add_config_lines() @@ -109,7 +113,6 @@ class PostfixConfigGenerator(MTAConfigGenerator): self.new_cf += line self.new_cf += sep + new_cf_lines - #print self.new_cf f = open(self.fn, "w") f.write(self.new_cf) f.close() @@ -142,6 +145,10 @@ class PostfixConfigGenerator(MTAConfigGenerator): f.write("\n".join(self.policy_lines) + "\n") f.close() + def update_CAfile(self): + os.system("cat /usr/share/ca-certificates/mozilla/*.crt > " + + self.ca_file) + if __name__ == "__main__": import ConfigParser if len(sys.argv) != 3: @@ -150,3 +157,4 @@ if __name__ == "__main__": c = ConfigParser.Config(sys.argv[1]) postfix_dir = sys.argv[2] pcgen = PostfixConfigGenerator(c, postfix_dir, fixup=True) + print "Done." From 1d47acddfdffe93f75c0d07ae0f127dd1693d984 Mon Sep 17 00:00:00 2001 From: Jacob Hoffman-Andrews Date: Fri, 17 Oct 2014 15:28:44 -0400 Subject: [PATCH 2/5] Remove extraneous print of config. --- ConfigParser.py | 1 - 1 file changed, 1 deletion(-) diff --git a/ConfigParser.py b/ConfigParser.py index 2d7c88ada..d1c413f74 100755 --- a/ConfigParser.py +++ b/ConfigParser.py @@ -90,7 +90,6 @@ class Config: # XXX is it ever permissible to have a domain with an acceptable-mx # that does not point to a TLS security policy? If not, check/warn/fail # here - print self.tls_policies def get_address_domains(self, mx_hostname): labels = mx_hostname.split(".") From 828c00b758063172990073bd4b667b61c1a30699 Mon Sep 17 00:00:00 2001 From: Jacob Hoffman-Andrews Date: Fri, 17 Oct 2014 15:28:49 -0400 Subject: [PATCH 3/5] Find deferred lines in log summary. Also add a cron mode that only emits output if there's something wrong. --- PostfixLogSummary.py | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/PostfixLogSummary.py b/PostfixLogSummary.py index 0348432b0..9b2f9c3b1 100755 --- a/PostfixLogSummary.py +++ b/PostfixLogSummary.py @@ -2,6 +2,7 @@ import re import sys import collections +import argparse import ConfigParser @@ -39,8 +40,8 @@ def get_counts(input, config): deferred = deferred_re.search(line) connected = connected_re.search(line) if connected: - validation = result.group(1) - mx_hostname = result.group(2).lower() + validation = connected.group(1) + mx_hostname = connected.group(2).lower() if validation == "Trusted" or validation == "Verified": seen_trusted = True address_domains = config.get_address_domains(mx_hostname) @@ -49,13 +50,13 @@ def get_counts(input, config): counts[d][validation] += 1 counts[d]["all"] += 1 elif deferred: - mx_hostname = result.group(1).lower() + mx_hostname = deferred.group(1).lower() tls_deferred[mx_hostname] += 1 if not seen_trusted: # Postfix will only emit 'Trusted' if the certificate validates according to # the set of trust roots (CA certs) configured in smtp_tls_CAfile. print "Didn't see any trusted connections. Need to install some trust roots?" - return (counts, tls_deferred) + return (counts, tls_deferred, seen_trusted) def print_summary(counts): for mx_hostname, validations in counts.items(): @@ -65,7 +66,19 @@ def print_summary(counts): print mx_hostname, validation, validation_count / validations["all"], "of", validations["all"] if __name__ == "__main__": + arg_parser = argparse.ArgumentParser(description='This is a PyMOTW sample program') + arg_parser.add_argument('-c', action="store_true", dest="cron", default=False) + args = arg_parser.parse_args() + config = ConfigParser.Config("starttls-everywhere.json") - (counts, tls_deferred) = get_counts(sys.stdin, config) - print_summary(counts) - print tls_deferred + (counts, tls_deferred, seen_trusted) = get_counts(sys.stdin, config) + + # If not running in cron, print an overall summary of log lines seen from known hosts. + if not args.cron: + print_summary(counts) + if not seen_trusted: + print 'No Trusted connections seen! Probably need to install a CAfile.' + + if len(tls_deferred) > 0: + print "Some mail was deferred due to TLS problems:" + print tls_deferred From f04e8259a9aef0e6547fe22884268581461512e2 Mon Sep 17 00:00:00 2001 From: Jacob Hoffman-Andrews Date: Mon, 20 Oct 2014 09:29:22 -0400 Subject: [PATCH 4/5] Protocols separated by colons, not commas. --- MTAConfigGenerator.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/MTAConfigGenerator.py b/MTAConfigGenerator.py index 051d9676d..5ca8f5aee 100755 --- a/MTAConfigGenerator.py +++ b/MTAConfigGenerator.py @@ -132,11 +132,11 @@ class PostfixConfigGenerator(MTAConfigGenerator): entry = address_domain + " encrypt" if "min-tls-version" in mx_policy: if mx_policy["min-tls-version"].lower() == "tlsv1": - entry += " protocols=!SSLv2,!SSLv3" + entry += " protocols=!SSLv2:!SSLv3" elif mx_policy["min-tls-version"].lower() == "tlsv1.1": - entry += " protocols=!SSLv2,!SSLv3,!TLSv1" + entry += " protocols=!SSLv2:!SSLv3:!TLSv1" elif mx_policy["min-tls-version"].lower() == "tlsv1.2": - entry += " protocols=!SSLv2,!SSLv3,!TLSv1,!TLSv1.1" + entry += " protocols=!SSLv2:!SSLv3:!TLSv1:!TLSv1.1" else: print mx_policy["min-tls-version"] self.policy_lines.append(entry) From 613a8f5e8877878f3d0b169a045b0f6b2b31adec Mon Sep 17 00:00:00 2001 From: Jacob Hoffman-Andrews Date: Mon, 20 Oct 2014 16:15:13 -0400 Subject: [PATCH 5/5] Improve cronjob operation to only process diffs. --- PostfixLogSummary.py | 34 +++++++++++++++++++++++----------- 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/PostfixLogSummary.py b/PostfixLogSummary.py index 9b2f9c3b1..be130d958 100755 --- a/PostfixLogSummary.py +++ b/PostfixLogSummary.py @@ -1,11 +1,15 @@ #!/usr/bin/python2.7 +import argparse +import collections +import os import re import sys -import collections -import argparse +import time import ConfigParser +TIME_FORMAT = "%b %d %H:%M:%S" + # TODO: There's more to be learned from postfix logs! Here's one sample # observed during failures from the sender vagrant vm: @@ -22,7 +26,7 @@ import ConfigParser # # Also: # Oct 10 19:12:13 sender postfix/smtp[1711]: 62D3F481249: to=, relay=valid-example-recipient.com[192.168.33.7]:25, delay=0.07, delays=0.03/0.01/0.03/0, dsn=4.7.4, status=deferred (TLS is required, but was not offered by host valid-example-recipient.com[192.168.33.7]) -def get_counts(input, config): +def get_counts(input, config, earliest_timestamp): seen_trusted = False counts = collections.defaultdict(lambda: collections.defaultdict(int)) @@ -32,11 +36,15 @@ def get_counts(input, config): # indicate a problem that should be alerted on. # ([^[]*) <--- any group of characters that is not "[" # Log lines for when a message is deferred for a TLS-related reason. These - deferred_re = re.compile("relay=([^[]*).* status=deferred.*TLS") + deferred_re = re.compile("relay=([^[ ]*).* status=deferred.*TLS") # Log lines for when a TLS connection was successfully established. These can # indicate the difference between Untrusted, Trusted, and Verified certs. connected_re = re.compile("([A-Za-z]+) TLS connection established to ([^[]*)") + timestamp = 0 for line in sys.stdin: + timestamp = time.strptime(line[0:15], TIME_FORMAT) + if timestamp < earliest_timestamp: + continue deferred = deferred_re.search(line) connected = connected_re.search(line) if connected: @@ -52,11 +60,7 @@ def get_counts(input, config): elif deferred: mx_hostname = deferred.group(1).lower() tls_deferred[mx_hostname] += 1 - if not seen_trusted: - # Postfix will only emit 'Trusted' if the certificate validates according to - # the set of trust roots (CA certs) configured in smtp_tls_CAfile. - print "Didn't see any trusted connections. Need to install some trust roots?" - return (counts, tls_deferred, seen_trusted) + return (counts, tls_deferred, seen_trusted, timestamp) def print_summary(counts): for mx_hostname, validations in counts.items(): @@ -71,7 +75,14 @@ if __name__ == "__main__": args = arg_parser.parse_args() config = ConfigParser.Config("starttls-everywhere.json") - (counts, tls_deferred, seen_trusted) = get_counts(sys.stdin, config) + + last_timestamp_processed = 0 + timestamp_file = '/tmp/starttls-everywhere-last-timestamp-processed.txt' + if os.path.isfile(timestamp_file): + last_timestamp_processed = time.strptime(open(timestamp_file).read(), TIME_FORMAT) + (counts, tls_deferred, seen_trusted, latest_timestamp) = get_counts(sys.stdin, config, last_timestamp_processed) + with open(timestamp_file, "w") as f: + f.write(time.strftime(TIME_FORMAT, latest_timestamp)) # If not running in cron, print an overall summary of log lines seen from known hosts. if not args.cron: @@ -81,4 +92,5 @@ if __name__ == "__main__": if len(tls_deferred) > 0: print "Some mail was deferred due to TLS problems:" - print tls_deferred + for (k, v) in tls_deferred.iteritems(): + print "%s: %s" % (k, v)