Gunicorn: Default SSL ciphers setting disables the most secure ciphers

Created on 23 Jan 2019  ·  3Comments  ·  Source: benoitc/gunicorn

The default for the --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:

>>> 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 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.

Feedback Requested FeaturSSL

Most helpful comment

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 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.

All 3 comments

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 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.

Was this page helpful?
0 / 5 - 0 ratings