Repository: certbot/certbot

Issue:
Disable TLS session tickets in Nginx?
Inspired by https://jhalderm.com/pub/papers/forward-secrecy-imc16.pdf, we might want to look into changing our nginx plugin's behavior around TLS session tickets. The [Mozilla site](https://mozilla.github.io/server-side-tls/ssl-config-generator/) where we get our TLS config from has TLS session tickets disabled, but this setting is not present in our config file at https://github.com/certbot/certbot/blob/master/certbot-nginx/certbot_nginx/options-ssl-nginx.conf.

This is likely due to the minimum version Nginx that supports these settings, however, I don't believe this has been investigated for a while. We should see what version these settings were added to Nginx and what versions are available in major distros.

Even if support is not available everywhere yet, it is probably worth adding these values conditionally as I suspect it will work on the vast majority of systems.


Discussion:
`ssl_session_tickets` is available in nginx 1.5.9.
Nginx versions in oldest distros we support: Debian 8 (jessie):1.6.2; Centos 6 EPEL: 1.10.2; Ubuntu 16.04: 1.10.3
Ubuntu 14 (trusty) is listed on our site, but it's supposed to be EOL'ed by now?

Either way, this will work on most major distros, but we still do gate [OCSP stapling](https://github.com/certbot/certbot/blob/master/certbot-nginx/certbot_nginx/configurator.py#L832) for versions of nginx > 1.3.7 so we should probably do the same here.

Given the way `options-ssl-nginx.conf` is carefully version-controlled and managed, and that nginx will error if it encounters a directive it doesn't recognize, one way to do this might be to create more versions of the file that are included conditionally. If we do this for more config defaults in the future it might lead to file bloat, but I'd feel better doing it this way rather than arbitrarily changing a file in code that's so carefully version-controlled.
That plan sounds good to me! That's what we currently do in the Apache plugin. Let me or Joona know if you'd like a bit of a primer on how that works.

I think we can separately work on deprecating support for old versions of Nginx if that's what we want to do, but I think we should probably do ~~than~~ that separately and more gracefully than suddenly causing your nginx config to break when your TLS settings are updated.

Resolving patch:
diff --git a/certbot-nginx/certbot_nginx/configurator.py b/certbot-nginx/certbot_nginx/configurator.py
--- a/certbot-nginx/certbot_nginx/configurator.py
+++ b/certbot-nginx/certbot_nginx/configurator.py
@@ -6,6 +6,8 @@
 import tempfile
 import time
 
+import pkg_resources
+
 import OpenSSL
 import zope.interface
 
@@ -120,6 +122,14 @@ def __init__(self, *args, **kwargs):
 
         self.reverter.recovery_routine()
 
+    @property
+    def mod_ssl_conf_src(self):
+        """Full absolute path to SSL configuration file source."""
+        config_filename = "options-ssl-nginx.conf"
+        if self.version < (1, 5, 9):
+            config_filename = "options-ssl-nginx-old.conf"
+        return pkg_resources.resource_filename("certbot_nginx", config_filename)
+
     @property
     def mod_ssl_conf(self):
         """Full absolute path to SSL configuration file."""
@@ -130,6 +140,11 @@ def updated_mod_ssl_conf_digest(self):
         """Full absolute path to digest of updated SSL configuration file."""
         return os.path.join(self.config.config_dir, constants.UPDATED_MOD_SSL_CONF_DIGEST)
 
+    def install_ssl_options_conf(self, options_ssl, options_ssl_digest):
+        """Copy Certbot's SSL options file into the system's config dir if required."""
+        return common.install_version_controlled_file(options_ssl, options_ssl_digest,
+            self.mod_ssl_conf_src, constants.ALL_SSL_OPTIONS_HASHES)
+
     # This is called in determine_authenticator and determine_installer
     def prepare(self):
         """Prepare the authenticator/installer.
@@ -148,14 +163,14 @@ def prepare(self):
 
         self.parser = parser.NginxParser(self.conf('server-root'))
 
-        install_ssl_options_conf(self.mod_ssl_conf, self.updated_mod_ssl_conf_digest)
-
-        self.install_ssl_dhparams()
-
         # Set Version
         if self.version is None:
             self.version = self.get_version()
 
+        self.install_ssl_options_conf(self.mod_ssl_conf, self.updated_mod_ssl_conf_digest)
+
+        self.install_ssl_dhparams()
+
         # Prevent two Nginx plugins from modifying a config at once
         try:
             util.lock_dir_until_exit(self.conf('server-root'))
@@ -1131,12 +1146,6 @@ def nginx_restart(nginx_ctl, nginx_conf):
     time.sleep(1)
 
 
-def install_ssl_options_conf(options_ssl, options_ssl_digest):
-    """Copy Certbot's SSL options file into the system's config dir if required."""
-    return common.install_version_controlled_file(options_ssl, options_ssl_digest,
-        constants.MOD_SSL_CONF_SRC, constants.ALL_SSL_OPTIONS_HASHES)
-
-
 def _determine_default_server_root():
     if os.environ.get("CERTBOT_DOCS") == "1":
         default_server_root = "%s or %s" % (constants.LINUX_SERVER_ROOT,
diff --git a/certbot-nginx/certbot_nginx/constants.py b/certbot-nginx/certbot_nginx/constants.py
--- a/certbot-nginx/certbot_nginx/constants.py
+++ b/certbot-nginx/certbot_nginx/constants.py
@@ -1,8 +1,6 @@
 """nginx plugin constants."""
 import platform
 
-import pkg_resources
-
 FREEBSD_DARWIN_SERVER_ROOT = "/usr/local/etc/nginx"
 LINUX_SERVER_ROOT = "/etc/nginx"
 
@@ -21,14 +19,13 @@
 MOD_SSL_CONF_DEST = "options-ssl-nginx.conf"
 """Name of the mod_ssl config file as saved in `IConfig.config_dir`."""
 
-MOD_SSL_CONF_SRC = pkg_resources.resource_filename(
-    "certbot_nginx", "options-ssl-nginx.conf")
-"""Path to the nginx mod_ssl config file found in the Certbot
-distribution."""
-
 UPDATED_MOD_SSL_CONF_DIGEST = ".updated-options-ssl-nginx-conf-digest.txt"
 """Name of the hash of the updated or informed mod_ssl_conf as saved in `IConfig.config_dir`."""
 
+SSL_OPTIONS_HASHES_NEW = [
+    '63e2bddebb174a05c9d8a7cf2adf72f7af04349ba59a1a925fe447f73b2f1abf',
+]
+"""SHA256 hashes of the contents of versions of MOD_SSL_CONF_SRC for nginx >= 1.5.9"""
 
 ALL_SSL_OPTIONS_HASHES = [
     '0f81093a1465e3d4eaa8b0c14e77b2a2e93568b0fc1351c2b87893a95f0de87c',
@@ -37,7 +34,7 @@
     '7f95624dd95cf5afc708b9f967ee83a24b8025dc7c8d9df2b556bbc64256b3ff',
     '394732f2bbe3e5e637c3fb5c6e980a1f1b90b01e2e8d6b7cff41dde16e2a756d',
     '4b16fec2bcbcd8a2f3296d886f17f9953ffdcc0af54582452ca1e52f5f776f16',
-]
+] + SSL_OPTIONS_HASHES_NEW
 """SHA256 hashes of the contents of all versions of MOD_SSL_CONF_SRC"""
 
 def os_constant(key):


Test patch:
diff --git a/certbot-nginx/certbot_nginx/tests/configurator_test.py b/certbot-nginx/certbot_nginx/tests/configurator_test.py
--- a/certbot-nginx/certbot_nginx/tests/configurator_test.py
+++ b/certbot-nginx/certbot_nginx/tests/configurator_test.py
@@ -13,7 +13,6 @@
 from certbot.compat import os
 from certbot.tests import util as certbot_test_util
 
-from certbot_nginx import constants
 from certbot_nginx import obj
 from certbot_nginx import parser
 from certbot_nginx.configurator import _redirect_block_for_domain
@@ -883,12 +882,11 @@ def setUp(self):
             self.config_path, self.config_dir, self.work_dir, self.logs_dir)
 
     def _call(self):
-        from certbot_nginx.configurator import install_ssl_options_conf
-        install_ssl_options_conf(self.config.mod_ssl_conf, self.config.updated_mod_ssl_conf_digest)
+        self.config.install_ssl_options_conf(self.config.mod_ssl_conf,
+            self.config.updated_mod_ssl_conf_digest)
 
     def _current_ssl_options_hash(self):
-        from certbot_nginx.constants import MOD_SSL_CONF_SRC
-        return crypto_util.sha256sum(MOD_SSL_CONF_SRC)
+        return crypto_util.sha256sum(self.config.mod_ssl_conf_src)
 
     def _assert_current_file(self):
         self.assertTrue(os.path.isfile(self.config.mod_ssl_conf))
@@ -908,12 +906,32 @@ def test_current_file(self):
         self._call()
         self._assert_current_file()
 
+    def _mock_hash_except_ssl_conf_src(self, fake_hash):
+        # Write a bad file in place so that update tests fail if no update occurs.
+        # We're going to pretend this file (the currently installed conf file)
+        # actually hashes to `fake_hash` for the update tests.
+        with open(self.config.mod_ssl_conf, "w") as f:
+            f.write("bogus")
+        sha256 = crypto_util.sha256sum
+        def _hash(filename):
+            return sha256(filename) if filename == self.config.mod_ssl_conf_src else fake_hash
+        return _hash
+
     def test_prev_file_updates_to_current(self):
         from certbot_nginx.constants import ALL_SSL_OPTIONS_HASHES
-        with mock.patch('certbot.crypto_util.sha256sum') as mock_sha256:
-            mock_sha256.return_value = ALL_SSL_OPTIONS_HASHES[0]
+        with mock.patch('certbot.crypto_util.sha256sum',
+                new=self._mock_hash_except_ssl_conf_src(ALL_SSL_OPTIONS_HASHES[0])):
+            self._call()
+        self._assert_current_file()
+
+    def test_prev_file_updates_to_current_old_nginx(self):
+        from certbot_nginx.constants import ALL_SSL_OPTIONS_HASHES, SSL_OPTIONS_HASHES_NEW
+        self.config.version = (1, 5, 8)
+        with mock.patch('certbot.crypto_util.sha256sum',
+                new=self._mock_hash_except_ssl_conf_src(ALL_SSL_OPTIONS_HASHES[0])):
             self._call()
         self._assert_current_file()
+        self.assertTrue(self._current_ssl_options_hash() not in SSL_OPTIONS_HASHES_NEW)
 
     def test_manually_modified_current_file_does_not_update(self):
         with open(self.config.mod_ssl_conf, "a") as mod_ssl_conf:
@@ -922,7 +940,7 @@ def test_manually_modified_current_file_does_not_update(self):
             self._call()
             self.assertFalse(mock_logger.warning.called)
         self.assertTrue(os.path.isfile(self.config.mod_ssl_conf))
-        self.assertEqual(crypto_util.sha256sum(constants.MOD_SSL_CONF_SRC),
+        self.assertEqual(crypto_util.sha256sum(self.config.mod_ssl_conf_src),
             self._current_ssl_options_hash())
         self.assertNotEqual(crypto_util.sha256sum(self.config.mod_ssl_conf),
             self._current_ssl_options_hash())
@@ -937,7 +955,7 @@ def test_manually_modified_past_file_warns(self):
             self.assertEqual(mock_logger.warning.call_args[0][0],
                 "%s has been manually modified; updated file "
                 "saved to %s. We recommend updating %s for security purposes.")
-        self.assertEqual(crypto_util.sha256sum(constants.MOD_SSL_CONF_SRC),
+        self.assertEqual(crypto_util.sha256sum(self.config.mod_ssl_conf_src),
             self._current_ssl_options_hash())
         # only print warning once
         with mock.patch("certbot.plugins.common.logger") as mock_logger:
@@ -950,6 +968,16 @@ def test_current_file_hash_in_all_hashes(self):
             "Constants.ALL_SSL_OPTIONS_HASHES must be appended"
             " with the sha256 hash of self.config.mod_ssl_conf when it is updated.")
 
+    def test_old_nginx_version_uses_old_config(self):
+        self.config.version = (1, 5, 8)
+        self.assertEqual(os.path.basename(self.config.mod_ssl_conf_src),
+                         "options-ssl-nginx-old.conf")
+        self._call()
+        self._assert_current_file()
+        self.config.version = (1, 5, 9)
+        self.assertEqual(os.path.basename(self.config.mod_ssl_conf_src),
+                         "options-ssl-nginx.conf")
+
 
 class DetermineDefaultServerRootTest(certbot_test_util.ConfigTestCase):
     """Tests for certbot_nginx.configurator._determine_default_server_root."""