mirror of
https://github.com/certbot/certbot.git
synced 2026-06-09 08:42:57 -04:00
Merge branch 'master' of github.com:dmwilcox/starttls-everywhere into hackathon
This commit is contained in:
commit
5928fae89e
3 changed files with 163 additions and 31 deletions
42
README.md
42
README.md
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Reference in a new issue