diff --git a/letsencrypt/client/plugins/nginx/configurator.py b/letsencrypt/client/plugins/nginx/configurator.py index 65a7ebad5..ba4fc77e2 100644 --- a/letsencrypt/client/plugins/nginx/configurator.py +++ b/letsencrypt/client/plugins/nginx/configurator.py @@ -19,7 +19,6 @@ from letsencrypt.client import le_util from letsencrypt.client import reverter from letsencrypt.client.plugins.nginx import dvsni -from letsencrypt.client.plugins.nginx import obj from letsencrypt.client.plugins.nginx import parser @@ -165,7 +164,8 @@ class NginxConfigurator(object): ####################### def choose_vhost(self, target_name): """Chooses a virtual host based on the given domain name. NOTE: This - makes the vhost SSL-enabled if it isn't already. + makes the vhost SSL-enabled if it isn't already. Follows Nginx's server + block selection rules but prefers blocks that are already SSL. .. todo:: This should maybe return list if no obvious answer is presented. @@ -200,7 +200,7 @@ class NginxConfigurator(object): if vhost is not None: self.assoc[target_name] = vhost if not vhost.ssl: - vhost = self._make_vhost_ssl(vhost) + self._make_server_ssl(vhost.filep, vhost.names) return vhost @@ -276,83 +276,23 @@ class NginxConfigurator(object): return all_names - def _make_vhost_ssl(self, nonssl_vhost): # pylint: disable=too-many-locals - """Makes an ssl_vhost version of a nonssl_vhost. + def _make_server_ssl(self, filename, names): + """Makes a server SSL based on server_name and filename by adding + a 'listen 443 ssl' directive to the server block. - Duplicates vhost and adds default ssl options - New vhost will reside as (nonssl_vhost.path) + ``IConfig.le_vhost_ext`` + .. todo:: Maybe this should create a new block instead of modifying + the existing one? - .. note:: This function saves the configuration - - :param nonssl_vhost: Valid VH that doesn't have SSLEngine on - :type nonssl_vhost: - :class:`~letsencrypt.client.plugins.nginx.obj.VirtualHost` - - :returns: SSL vhost - :rtype: :class:`~letsencrypt.client.plugins.nginx.obj.VirtualHost` + :param str filename: The absolute filename of the config file. + :param set names: The server names of the block to add SSL in """ - avail_fp = nonssl_vhost.filep - # Get filepath of new ssl_vhost - if avail_fp.endswith(".conf"): - ssl_fp = avail_fp[:-(len(".conf"))] + self.config.le_vhost_ext - else: - ssl_fp = avail_fp + self.config.le_vhost_ext - - # First register the creation so that it is properly removed if - # configuration is rolled back - self.reverter.register_file_creation(False, ssl_fp) - - try: - with open(avail_fp, "r") as orig_file: - with open(ssl_fp, "w") as new_file: - new_file.write("\n") - for line in orig_file: - new_file.write(line) - new_file.write("\n") - except IOError: - logging.fatal("Error writing/reading to file in _make_vhost_ssl") - sys.exit(49) - - self.aug.load() - - ssl_addrs = set() - - # change address to address:443 - addr_match = "/files%s//* [label()=~regexp('%s')]/arg" - ssl_addr_p = self.aug.match( - addr_match % (ssl_fp, parser.case_i("VirtualHost"))) - - for addr in ssl_addr_p: - old_addr = obj.Addr.fromstring( - str(self.aug.get(addr))) - ssl_addr = old_addr.get_addr_obj("443") - self.aug.set(addr, str(ssl_addr)) - ssl_addrs.add(ssl_addr) - - # Add directives - vh_p = self.aug.match("/files%s//* [label()=~regexp('%s')]" % - (ssl_fp, parser.case_i("VirtualHost"))) - if len(vh_p) != 1: - logging.error("Error: should only be one vhost in %s", avail_fp) - sys.exit(1) - - self.parser.add_dir(vh_p[0], "SSLCertificateFile", - "/etc/ssl/certs/ssl-cert-snakeoil.pem") - self.parser.add_dir(vh_p[0], "SSLCertificateKeyFile", - "/etc/ssl/private/ssl-cert-snakeoil.key") - self.parser.add_dir(vh_p[0], "Include", self.parser.loc["ssl_options"]) - - # Log actions and create save notes - logging.info("Created an SSL vhost at %s", ssl_fp) - self.save_notes += "Created ssl vhost at %s\n" % ssl_fp - self.save() - - # We know the length is one because of the assertion above - ssl_vhost = self._create_vhost(vh_p[0]) - self.vhosts.append(ssl_vhost) - - return ssl_vhost + self.parser.add_server_directives( + filename, names, + [['listen', '443 ssl'], + ['ssl_certificate', '/etc/ssl/certs/ssl-cert-snakeoil.pem'], + ['ssl_key', '/etc/ssl/private/ssl-cert-snakeoil.key'], + ['include', self.parser.loc["ssl_options"]]]) def get_all_certs_keys(self): """Find all existing keys, certs from configuration. @@ -490,12 +430,12 @@ class NginxConfigurator(object): nginx_version = tuple([int(i) for i in version_matches[0].split(".")]) - # nginx <= 0.7.14 has an incompatible SSL configuration format + # nginx < 0.8.21 doesn't use default_server if (nginx_version[0] == 0 and - (nginx_version[1] < 7 or - (nginx_version[1] == 7 and nginx_version[2] < 15))): + (nginx_version[1] < 8 or + (nginx_version[1] == 8 and nginx_version[2] < 21))): raise errors.LetsEncryptConfiguratorError( - "Nginx version not supported") + "Nginx version must be 0.8.21+") return nginx_version diff --git a/letsencrypt/client/plugins/nginx/obj.py b/letsencrypt/client/plugins/nginx/obj.py index 835af91b0..6ac48fd7f 100644 --- a/letsencrypt/client/plugins/nginx/obj.py +++ b/letsencrypt/client/plugins/nginx/obj.py @@ -103,19 +103,15 @@ class VirtualHost(object): # pylint: disable=too-few-public-methods """ - def __init__(self, filep, addrs, ssl, enabled, names=None): + def __init__(self, filep, addrs, ssl, enabled, names): # pylint: disable=too-many-arguments """Initialize a VH.""" self.filep = filep self.addrs = addrs - self.names = set() if names is None else set(names) + self.names = names self.ssl = ssl self.enabled = enabled - def add_name(self, name): - """Add name to vhost.""" - self.names.add(name) - def __str__(self): addr_str = ", ".join(str(addr) for addr in self.addrs) return ("file: %s\n" diff --git a/letsencrypt/client/plugins/nginx/options-ssl.conf b/letsencrypt/client/plugins/nginx/options-ssl.conf index 8380542c0..f0081c1fc 100644 --- a/letsencrypt/client/plugins/nginx/options-ssl.conf +++ b/letsencrypt/client/plugins/nginx/options-ssl.conf @@ -1,27 +1,8 @@ -ssl_session_cache shared:SSL:1m; # 1MB is ~4000 sessions, if it fills old sessions are dropped -ssl_session_timeout 1440m; # Reuse sessions for 24hrs +ssl_session_cache shared:SSL:1m; +ssl_session_timeout 1440m; -# Redirect all traffic to SSL -server { - listen 80 default; - server_name www.example.com example.com; - access_log off; - error_log off; - return 301 https://example.com$request_uri; -} +ssl_protocols TLSv1 TLSv1.1 TLSv1.2; +ssl_prefer_server_ciphers on; -server { - listen 443 ssl default_server; - server_name example.com; - - ssl_certificate /path/to/bundle.crt; - ssl_certificate_key /path/to/private.key; - - ssl_protocols TLSv1 TLSv1.1 TLSv1.2; - ssl_prefer_server_ciphers on; - - # Using list of ciphers from "Bulletproof SSL and TLS" - ssl_ciphers "ECDHE-ECDSA-AES128-GCM-SHA256 ECDHE-ECDSA-AES256-GCM-SHA384 ECDHE-ECDSA-AES128-SHA ECDHE-ECDSA-AES256-SHA ECDHE-ECDSA-AES128-SHA256 ECDHE-ECDSA-AES256-SHA384 ECDHE-RSA-AES128-GCM-SHA256 ECDHE-RSA-AES256-GCM-SHA384 ECDHE-RSA-AES128-SHA ECDHE-RSA-AES128-SHA256 ECDHE-RSA-AES256-SHA384 DHE-RSA-AES128-GCM-SHA256 DHE-RSA-AES256-GCM-SHA384 DHE-RSA-AES128-SHA DHE-RSA-AES256-SHA DHE-RSA-AES128-SHA256 DHE-RSA-AES256-SHA256 EDH-RSA-DES-CBC3-SHA"; - - # Normal stuff below here -} +# Using list of ciphers from "Bulletproof SSL and TLS" +ssl_ciphers "ECDHE-ECDSA-AES128-GCM-SHA256 ECDHE-ECDSA-AES256-GCM-SHA384 ECDHE-ECDSA-AES128-SHA ECDHE-ECDSA-AES256-SHA ECDHE-ECDSA-AES128-SHA256 ECDHE-ECDSA-AES256-SHA384 ECDHE-RSA-AES128-GCM-SHA256 ECDHE-RSA-AES256-GCM-SHA384 ECDHE-RSA-AES128-SHA ECDHE-RSA-AES128-SHA256 ECDHE-RSA-AES256-SHA384 DHE-RSA-AES128-GCM-SHA256 DHE-RSA-AES256-GCM-SHA384 DHE-RSA-AES128-SHA DHE-RSA-AES256-SHA DHE-RSA-AES128-SHA256 DHE-RSA-AES256-SHA256 EDH-RSA-DES-CBC3-SHA"; diff --git a/letsencrypt/client/plugins/nginx/parser.py b/letsencrypt/client/plugins/nginx/parser.py index 2633b778c..4ff29962c 100644 --- a/letsencrypt/client/plugins/nginx/parser.py +++ b/letsencrypt/client/plugins/nginx/parser.py @@ -264,6 +264,30 @@ class NginxParser(object): except IOError: logging.error("Could not open file for writing: %s" % filename) + def add_server_directives(self, filename, names, directives): + """Adds directives to a server block whose server_name set is 'names'. + + :param str filename: The absolute filename of the config file + :param str names: The server_name to match + :param list directives: The directives to add + + """ + if len(names) == 0: + # Nothing to identify blocks with + return False + + def has_server_names(entry): + # Checks if a server block has the given names + # TODO: Make this work if some of the names are in included files + server_names = set() + for item in entry: + if item[0] == 'server_name': + server_names.update((' ').split(item[1])) + return server_names == names + + do_for_subarray(self.parsed[filename], lambda x: has_server_names(x), + lambda x: x.extend(directives)) + def do_for_subarray(entry, condition, func): """Executes a function for a subarray of a nested array if it matches @@ -291,7 +315,7 @@ def get_best_match(target_name, names): longest wildcard ending with * > regex). :param str target_name: The name to match - :param list names: The candidate server names + :param set names: The candidate server names :returns: Tuple of (type of match, the name that matched) :rtype: tuple