Merge branch 'master' of github.com:dmwilcox/starttls-everywhere into hackathon

This commit is contained in:
Daniel Wilcox 2016-03-29 15:04:23 -07:00
commit 5928fae89e
3 changed files with 163 additions and 31 deletions

View file

@ -1,11 +1,47 @@
# STARTTLS Everywhere
NOTE: this is a pre-alpha codebase. Do not run it on non-experimental systems
yet!
## Example usage
**WARNING: this is a pre-alpha codebase. Do not run it on production
mailservers!!!**
If you have a Postfix server you're willing to endanger deliverability on, you
can try obtain a certificate with the [Let's Encrypt Python Client](https://github.com/letsencrypt/letsencrypt), note the directory it lives in below `/etc/letsencrypt/live` and then do:
```
git clone https://github.com/EFForg/starttls-everywhere
cd starttls-everywhere
# Promise you don't care if deliverability breaks on this mail server
letsencrypt-postfix/PostfixConfigGenerator.py examples/starttls-everywhere.json /etc/postfix /etc/letsencrypt/live/YOUR.DOMAIN.EXAMPLE.COM
```
This will:
* Ensure your mail server initiates STARTTLS encryption
* Install the Let's Encrypt cert in Postfix
* Enforce mandatory TLS to some major email domains
* Enforce minimum TLS versions to some major email domains
## Project status
STARTTLS Everywhere development is re-starting after a hiatus. Initial
objectives:
* Postfix configuration generation: working pre-alpha, not yet safe
* Email security database: working pre-alpha, definitely not yet safe
* Fully integrated Let's Encrypt client postfix plugin: in progress, not yet ready
* DANE support: none yet
* SMTP-STS integration: none yet
* Direct mechanisms for mail domains to request inclusion: none yet
* Failure reporting mechanisms: early progress, not yet ready
* Mechanisms for secure multi-organization signature on the policy database:
none yet
* Support for mail servers other than Postfix: none yet
## Authors
Jacob Hoffman-Andrews <jsha@eff.org>, Peter Eckersley <pde@eff.org>, Daniel Wilcox <dmwilcox@gmail.com>
Jacob Hoffman-Andrews <jsha@eff.org>, Peter Eckersley <pde@eff.org>, Daniel Wilcox <dmwilcox@gmail.com>, Aaron Zauner <azet@azet.org>
## Mailing List

View file

@ -1,6 +1,7 @@
#!/usr/bin/env python
import sys
import string
import subprocess
import os, os.path
@ -28,6 +29,7 @@ class PostfixConfigGenerator:
self.postfix_dir = postfix_dir
self.policy_config = policy_config
self.policy_file = os.path.join(postfix_dir, "starttls_everywhere_policy")
self.ca_file = os.path.join(postfix_dir, "starttls_everywhere_CAfile")
self.additions = []
self.deletions = []
@ -56,7 +58,6 @@ class PostfixConfigGenerator:
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)
@ -64,7 +65,6 @@ class PostfixConfigGenerator:
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,7 +86,15 @@ class PostfixConfigGenerator:
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, [])
# Disable SSLv2 and SSLv3. Syntax for `smtp_tls_protocols` changed
# between Postfix version 2.5 and 2.6, since we only support => 2.11
# we don't use nor support legacy Postfix syntax.
# - Server:
self.ensure_cf_var("smtp_tls_protocols", "!SSLv2, !SSLv3", [])
# - Client:
self.ensure_cf_var("smtp_tls_mandatory_protocols", "!SSLv2, !SSLv3", [])
def maybe_add_config_lines(self):
if not self.additions:
@ -107,10 +115,11 @@ class PostfixConfigGenerator:
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()
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)
with open(self.fn, "w") as f:
f.write(self.new_cf)
def set_domainwise_tls_policies(self):
all_acceptable_mxs = self.policy_config.acceptable_mxs
@ -124,11 +133,11 @@ class PostfixConfigGenerator:
mx_policy = self.policy_config.get_tls_policy(mx_domain)
entry = address_domain + " encrypt"
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)
@ -138,6 +147,7 @@ class PostfixConfigGenerator:
f.close()
### Let's Encrypt client IPlugin ###
# https://github.com/letsencrypt/letsencrypt/blob/master/letsencrypt/plugins/common.py#L35
def prepare(self):
"""Prepare the plugin.
@ -150,12 +160,65 @@ class PostfixConfigGenerator:
:raises .NoInstallationError:
when the necessary programs/files cannot be located. Plugin
will NOT be displayed on a list of available plugins.
:raises .NotSupportedError:
when the installation is recognized, but the version is not
currently supported.
"""
:raises .NotSupportedError:
when the installation is recognized, but the version is not
currently supported.
:rtype tuple:
"""
# XXX ensure we raise the right kinds of exceptions
# Parse Postfix version number (feature support, syntax changes etc.)
mail_version = subprocess.Popen(['/usr/sbin/postconf', '-d', 'mail_version'], \
stdout=subprocess.PIPE) \
.communicate()[0].split()[2]
maj, min, rev = mail_version.split('.')
self.postfix_version = mail_version
# Postfix has changed support for TLS features, supported protocol versions
# KEX methods, ciphers et cetera over the years. We sort out version dependend
# differences here and pass them onto other configuration functions.
# see:
# http://www.postfix.org/TLS_README.html
# http://www.postfix.org/FORWARD_SECRECY_README.html
# Postfix == 2.2:
# - TLS support introduced via 3rd party patch, see:
# http://www.postfix.org/TLS_LEGACY_README.html
# Postfix => 2.2:
# - built-in TLS support added
# - Support for PFS introduced
# - Support for (E)DHE params >= 1024bit (need to be generated), default 1k
# Postfix => 2.5:
# - Syntax to specify mandatory protocol version changes:
# * < 2.5: `smtpd_tls_mandatory_protocols = TLSv1`
# * => 2.5: `smtpd_tls_mandatory_protocols = !SSLv2, !SSLv3`
# - Certificate fingerprint verification added
# Postfix => 2.6:
# - Support for ECDHE NIST P-256 curve (enable `smtpd_tls_eecdh_grade = strong`)
# - Support for configurable cipher-suites and protocol versions added, pre-2.6
# releases always set EXPORT, options: `smtp_tls_ciphers` and `smtp_tls_protocols`
# - `smtp_tls_eccert_file` and `smtp_tls_eckey_file` config. options added
# Postfix => 2.8:
# - Override Client suite preference w. `tls_preempt_cipherlist = yes`
# - Elliptic curve crypto. support enabled by default
# Postfix => 2.9:
# - Public key fingerprint support added
# - `permit_tls_clientcerts`, `permit_tls_all_clientcerts` and
# `check_ccert_access` config. options added
# Postfix <= 2.9.5:
# - BUG: Public key fingerprint is computed incorrectly
# Postfix => 3.1:
# - Built-in support for TLS management and DANE added, see:
# http://www.postfix.org/postfix-tls.1.html
return maj, min, rev
def more_info(self):
"""Human-readable string to help the user.
@ -166,6 +229,7 @@ class PostfixConfigGenerator:
### Let's Encrypt client IInstaller ###
# https://github.com/letsencrypt/letsencrypt/blob/master/letsencrypt/interfaces.py#L232
def get_all_names(self):
"""Returns all names that may be authenticated.
@ -195,6 +259,7 @@ class PostfixConfigGenerator:
self.ensure_cf_var("smtpd_tls_cert_file", fullchain_path, [])
self.ensure_cf_var("smtpd_tls_key_file", key_path, [])
self.set_domainwise_tls_policies()
self.update_CAfile()
def enhance(self, domain, enhancement, options=None):
"""Perform a configuration enhancement.
@ -268,17 +333,22 @@ class PostfixConfigGenerator:
"""Restart or refresh the server content.
:raises .PluginError: when server cannot be restarted
"""
print "Reloading postfix config..."
if os.geteuid() != 0:
os.system("sudo service postfix reload")
else:
os.system("service postfix reload")
def update_CAfile(self):
os.system("cat /usr/share/ca-certificates/mozilla/*.crt > " + self.ca_file)
def usage():
print ("Usage: %s starttls-everywhere.json /etc/postfix /etc/letsencrypt/live/example.com/" %
sys.argv[0])
sys.exit(1)
if __name__ == "__main__":
import Config as config
if len(sys.argv) != 4:

View file

@ -1,10 +1,15 @@
#!/usr/bin/env python
import argparse
import collections
import os
import re
import sys
import collections
import time
import Config
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:
@ -21,7 +26,7 @@ import Config
#
# Also:
# Oct 10 19:12:13 sender postfix/smtp[1711]: 62D3F481249: to=<vagrant@valid-example-recipient.com>, 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))
@ -31,13 +36,17 @@ 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 ([^[]*)")
mx_to_domain_mapping = config.get_mx_to_domain_policy_map()
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:
@ -54,11 +63,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)
return (counts, tls_deferred, seen_trusted, timestamp)
def print_summary(counts):
for mx_hostname, validations in counts.items():
@ -68,11 +73,32 @@ def print_summary(counts):
print mx_hostname, validation, validation_count / validations["all"], "of", validations["all"]
if __name__ == "__main__":
if len(sys.argv) != 2:
print "Usage: %s starttls-everywhere.json" % sys.argv[0]
sys.exit(1)
arg_parser = argparse.ArgumentParser(description='Detect delivery problems'
' in Postfix log files that may be caused by security policies')
arg_parser.add_argument('-c', action="store_true", dest="cron", default=False)
arg_parser.add_argument("policy_file", nargs='?',
default=os.path.join("examples", "starttls-everywhere.json"),
help="STARTTLS Everywhere policy file")
args = arg_parser.parse_args()
config = Config.Config()
config.load_from_json_file(sys.argv[1])
(counts, tls_deferred) = get_counts(sys.stdin, config)
print_summary(counts)
print tls_deferred
config.load_from_json_file(args.policy_file)
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:
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:"
for (k, v) in tls_deferred.iteritems():
print "%s: %s" % (k, v)