Repository: benoitc/gunicorn

Issue:
Default SSL ciphers setting disables the most secure ciphers
The default for the [--ciphers](http://docs.gunicorn.org/en/stable/settings.html#ciphers) option is `TLSv1`.

This value is a bad default, because it actively disables new, strong ciphers that are only available with TLSv1.2. In particular, there is no intersection between the set of ciphers configured by this default value and [the ciphers rated A+ by OWASP](https://www.owasp.org/index.php?title=TLS_Cipher_String_Cheat_Sheet&oldid=243849):

```python
>>> import ssl
>>> ctx = ssl.SSLContext()
>>> ctx.set_ciphers('TLSv1')
>>> tlsv1 = {c['name'] for c in ctx.get_ciphers()}
>>> ctx.set_ciphers('DHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256')  # From OWASP
>>> aplus = {c['name'] for c in ctx.get_ciphers()}
>>> aplus & tlsv1
set()
```

This can lead to OpenSSL `NO_SHARED_CIPHER` errors for clients configured with high security settings.

One solution would be to simply add TLSv1.2 ciphers to the set, by changing the default to the string `'TLSv1:TLSv1.2'`. This would add compatibility for clients configured with strict security settings, without breaking any existing users of gunicorn. However, this would leave weak ciphers in the set.

A more secure option would be to pick one of the [OWASP strings](https://www.owasp.org/index.php?title=TLS_Cipher_String_Cheat_Sheet&oldid=243849) as a default, based on the described compatibility level. For example OWASP Cipher String C or C- would preserve wide compatibility while ruling out known-weak ciphers.

In either case, the documentation could be updated to recommend replacing the default with a stronger default based on expected usage, including a link to the OWASP page as a reference.



Discussion:
Thank you for reporting this issue. We're absolutely well aware of this. Gunicorn has not had significant changes to its SSL support for a while. See #1933 and the issues linked in that discussion.

In the meantime, if there is a more sensible default now that Gunicorn only supports Python 3.4+, please submit a pull request and I'd be glad to merge it. We're also considering pushing that minimum version higher, so if there's a higher minimum Python version that would enable an even more secure default, please post that information here to help drive that decision before the next release.
The ciphers available (and the interpretation of the argument to `SSLContext.set_ciphers()`) are not affected by Python version, but by the version of OpenSSL/LibreSSL that Python builds against. On Linux that would typically be the version provided by the distro.

If your question is "can we set a more secure default that always works in Python 3.4+" then the strict answer is "no" because it depends how you compiled Python. It may be possible to compile a recent Python against a very old OpenSSL that lacks strong ciphers, so requiring strong ciphers in gunicorn could mean SSL doesn't work at all.

However, I think we're pretty safe. Python 3.4 was released in 2014, while OpenSSL 1.0.1a, which added TLSv1.2, was released in April 2012. So distro versions of Python will probably have strong ciphers. The other thing is that each Python release requires a certain OpenSSL version (eg. Python 3.7 requires OpenSSL 1.0.2 I understand), but I don't know exactly what Python versions require what.

Overall I would have to be guided by OWASP. They say C- is "Legacy, widest compatibility to real old browsers and legacy libraries" - with the added bonus that they can be cited as the source.
Also, Python has its own default cipher list: it never falls back to OpenSSL's default (which is apparently `DEFAULT:!aNULL:!eNULL`). It's accessible as `ssl._DEFAULT_CIPHERS` although that's internal/undocumented, so the way to access the defaults is to never call set_ciphers() on an SSLContext.

For Python 3.6 `_DEFAULT_CIPHERS` is 

```
'TLS13-AES-256-GCM-SHA384:TLS13-CHACHA20-POLY1305-SHA256:TLS13-AES-128-GCM-SHA256:ECDH+AESGCM:ECDH+CHACHA20:DH+AESGCM:DH+CHACHA20:ECDH+AES256:DH+AES256:ECDH+AES128:DH+AES:ECDH+HIGH:DH+HIGH:RSA+AESGCM:RSA+AES:RSA+HIGH:!aNULL:!eNULL:!MD5:!3DES'
```

In Python 3.7 it's

```
'DEFAULT:!aNULL:!eNULL:!MD5:!3DES:!DES:!RC4:!IDEA:!SEED:!aDSS:!SRP:!PSK'
```

with [a comment](https://github.com/python/cpython/blob/fd628cf5adaeee73eab579393cdff71c8f70cdf2/Modules/_ssl.c#L277) saying "DEFAULT: OpenSSL's default cipher list. Since 1.0.2 the list is in sensible order."

These settings seem to be pretty tight, and in the same spirit as the OWASP.

There's also a `_RESTRICTED_SERVER_CIPHERS` which is intended for use in servers (so perfect for gunicorn) and is tighter than `_DEFAULT_CIPHERS` in some Python versions, but that is both internal and not used anywhere - but has been maintained since at least 3.4.

So option 3 is default to Python's default cipher suite.

The only reason not to do that is if we think we could ship a list today that is more secure than that in older Python versions, or that we want to opt-in to one of the even tighter OWASP levels. Honestly that's pretty marginal.

Resolving patch:
diff --git a/gunicorn/config.py b/gunicorn/config.py
--- a/gunicorn/config.py
+++ b/gunicorn/config.py
@@ -1963,9 +1963,22 @@ class Ciphers(Setting):
     section = "SSL"
     cli = ["--ciphers"]
     validator = validate_string
-    default = 'TLSv1'
+    default = None
     desc = """\
-    Ciphers to use (see stdlib ssl module's)
+    SSL Cipher suite to use, in the format of an OpenSSL cipher list.
+
+    By default we use the default cipher list from Python's ``ssl`` module,
+    which contains ciphers considered strong at the time of each Python
+    release.
+
+    As a recommended alternative, the Open Web App Security Project (OWASP)
+    offers `a vetted set of strong cipher strings rated A+ to C-
+    <https://www.owasp.org/index.php/TLS_Cipher_String_Cheat_Sheet>`_.
+    OWASP provides details on user-agent compatibility at each security level.
+
+    See the `OpenSSL Cipher List Format Documentation
+    <https://www.openssl.org/docs/manmaster/man1/ciphers.html#CIPHER-LIST-FORMAT>`_
+    for details on the format of an OpenSSL cipher list.
     """
 
 


Test patch:
diff --git a/tests/test_ssl.py b/tests/test_ssl.py
--- a/tests/test_ssl.py
+++ b/tests/test_ssl.py
@@ -72,4 +72,4 @@ def test_ciphers():
     assert Ciphers.name == 'ciphers'
     assert Ciphers.section == 'SSL'
     assert Ciphers.cli == ['--ciphers']
-    assert Ciphers.default == 'TLSv1'
+    assert Ciphers.default is None