benoitc / gunicorn

gunicorn 'Green Unicorn' is a WSGI HTTP Server for UNIX, fast clients and sleepy applications.
http://www.gunicorn.org
Other
9.7k stars 1.74k forks source link

How to specify TLSV1_3 with gunicorn #2579

Closed mariobriggs closed 2 years ago

mariobriggs commented 3 years ago

There are a set if tlsv1_3 ciphers available in my python env that i would like to use with gunicorn.

But the gunicorn --ssl-version= does not me to specify tlsv1_3

repro : Retrieve tlsv1_3 ciphers available to python ssl module

import ssl
ctx = ssl.SSLContext(ssl.PROTOCOL_SSLv23)
ciphers = ctx.get_ciphers()
v13 = []
for cip in ciphers:
  if cip['protocol'] == 'TLSv1.3':  
    v13.append(cip['name'])
':'.join(v13)

output

TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256:TLS_AES_128_CCM_SHA256

Try to use the 1_3 cipherlist with gunicorn

export CIP1_3=TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256:TLS_AES_128_CCM_SHA256
gunicorn -k uvicorn.workers.UvicornWorker --bind=0.0.0.0:9998 --workers=1 --certfile=<path_to_crt> --keyfile=<path_to_key> --ssl-version=TLSv1_2  --ciphers=$CIP1_3 xxx.yyy:app
[2021-05-04 14:11:05 +0000] [500] [INFO] Starting gunicorn 20.0.4
[2021-05-04 14:11:05 +0000] [500] [INFO] Listening at: https://0.0.0.0:9998 (500)
[2021-05-04 14:11:05 +0000] [500] [INFO] Using worker: uvicorn.workers.UvicornWorker
[2021-05-04 14:11:05 +0000] [502] [INFO] Booting worker with pid: 502
[2021-05-04 14:11:06 +0000] [502] [err] Exception in worker process
Traceback (most recent call last):
  File "/usr/local/lib/python3.8/site-packages/gunicorn/arbiter.py", line 583, in spawn_worker
    worker.init_process()
  File "/usr/local/lib/python3.8/site-packages/uvicorn/workers.py", line 63, in init_process
    super(UvicornWorker, self).init_process()
  File "/usr/local/lib/python3.8/site-packages/gunicorn/workers/base.py", line 140, in init_process
    self.run()
  File "/usr/local/lib/python3.8/site-packages/uvicorn/workers.py", line 76, in run
    loop.run_until_complete(server.serve(sockets=self.sockets))
  File "uvloop/loop.pyx", line 1494, in uvloop.loop.Loop.run_until_complete
  File "/usr/local/lib/python3.8/site-packages/uvicorn/server.py", line 55, in serve
    config.load()
  File "/usr/local/lib/python3.8/site-packages/uvicorn/config.py", line 273, in load
    self.ssl = create_ssl_context(
  File "/usr/local/lib/python3.8/site-packages/uvicorn/config.py", line 114, in create_ssl_context
    ctx.set_ciphers(ciphers)
ssl.SSLError: ('No cipher can be selected.',)

I believe this is because i have set --ssl-version=TLSv1_2 . I am not finding in the docs, how to set TLSV1_3

The above code works fine, when i extract the TLSv1_2 from the python ssl module

jamadden commented 3 years ago

Have you tried this with any native gunicorn workers? I ask because here it is uvicorn that is in the traceback and causing the error, not gunicorn.

mariobriggs commented 3 years ago

thanks @jamadden there are a couple of things i tried

1 - With uvicorn only, but select only TLSv1.2 ciphers available to python. This worked perfectly end-2-end. So i was curious how i specify TLSv1.3 to gunicorn and then how gunicorn passes that to uvicorn.

2 - Ran uvicorn directly now, and got the same error ssl.SSLError: ('No cipher can be selected.',) . So will ask the uvicorn git

export CIPHERS1_3=TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256:TLS_AES_128_CCM_SHA256
uvicorn xxx.yyy:app --ssl-ciphers=$CIPHERS1_3 --ssl-keyfile=/tmp/PROD_SELF_SIGNED_PRIVATEKEY --ssl-certfile=/tmp/PROD_SELF_SIGNED_CERTIFICATE --ssl-version=2
Traceback (most recent call last):
  File "/usr/local/bin/uvicorn", line 8, in <module>
    sys.exit(main())
  File "/usr/local/lib/python3.8/site-packages/click/core.py", line 829, in __call__
    return self.main(*args, **kwargs)
  File "/usr/local/lib/python3.8/site-packages/click/core.py", line 782, in main
    rv = self.invoke(ctx)
  File "/usr/local/lib/python3.8/site-packages/click/core.py", line 1066, in invoke
    return ctx.invoke(self.callback, **ctx.params)
  File "/usr/local/lib/python3.8/site-packages/click/core.py", line 610, in invoke
    return callback(*args, **kwargs)
  File "/usr/local/lib/python3.8/site-packages/uvicorn/main.py", line 362, in main
    run(**kwargs)
  File "/usr/local/lib/python3.8/site-packages/uvicorn/main.py", line 386, in run
    server.run()
  File "/usr/local/lib/python3.8/site-packages/uvicorn/server.py", line 48, in run
    loop.run_until_complete(self.serve(sockets=sockets))
  File "uvloop/loop.pyx", line 1494, in uvloop.loop.Loop.run_until_complete
  File "/usr/local/lib/python3.8/site-packages/uvicorn/server.py", line 55, in serve
    config.load()
  File "/usr/local/lib/python3.8/site-packages/uvicorn/config.py", line 273, in load
    self.ssl = create_ssl_context(
  File "/usr/local/lib/python3.8/site-packages/uvicorn/config.py", line 114, in create_ssl_context
    ctx.set_ciphers(ciphers)
ssl.SSLError: ('No cipher can be selected.',)
mariobriggs commented 3 years ago

@jamadden I reopened this. As apart from the uvicorn issue and you specifically asked Have you tried this with any native gunicorn workers?

i tried with sync , it starts up properly

gunicorn -k sync --bind=0.0.0.0:5004 --workers=1 --certfile=/tmp/PROD_SELF_SIGNED_CERTIFICATE --keyfile=/tmp/PROD_SELF_SIGNED_PRIVATEKEY --ssl-version=TLSv1_2  --ciphers=$CIPHERS1_3 monitor.api:app
[2021-05-09 07:36:39 +0000] [54] [INFO] Starting gunicorn 20.0.4
[2021-05-09 07:36:39 +0000] [54] [INFO] Listening at: https://0.0.0.0:5004 (54)
[2021-05-09 07:36:39 +0000] [54] [INFO] Using worker: sync
[2021-05-09 07:36:39 +0000] [56] [INFO] Booting worker with pid: 56

And then testing via curl gives a wrong record version #

curl -k -v https://localhost:5004/dp/monitor/v1/liveness
*   Trying ::1...
* TCP_NODELAY set
* connect to ::1 port 5004 failed: Connection refused
*   Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to localhost (127.0.0.1) port 5004 (#0)
* ALPN, offering h2
* ALPN, offering http/1.1
* successfully set certificate verify locations:
*   CAfile: /etc/pki/tls/certs/ca-bundle.crt
  CApath: none
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
* error:1408F10B:SSL routines:ssl3_get_record:wrong version number
curl: (35) error:1408F10B:SSL routines:ssl3_get_record:wrong version number

Which i believe is the server responding to the client's (TLSv1.3) hello that version ssl3 is not supported

mariobriggs commented 3 years ago

with gthread worker, server starts up fine, but runs into no cipher can be selected, when serving a request

curl -k -v https://localhost:8080/test
* TLSv1.2 (OUT), TLS handshake, Client hello (1):
* LibreSSL SSL_connect: SSL_ERROR_SYSCALL in connection to localhost:8080 
* Closing connection 0
curl: (35) LibreSSL SSL_connect: SSL_ERROR_SYSCALL in connection to localhost:8080 
class StandaloneApplication(gunicorn.app.base.BaseApplication):
  ...

if __name__ == '__main__':
    options = {
        'bind': '%s:%s' % ('127.0.0.1', '8080'),
        'certfile': './server.crt',
        'keyfile': '/.server.key',
        'workers': 2,
        'ssl-version': 'TLSv1_2',
        'ciphers': 'TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256:TLS_AES_128_CCM_SHA256',
        'worker_class': 'gthread',

    }
    StandaloneApplication(handler_app, options).run()

[2021-05-10 09:54:24 +0530] [26234] [INFO] Starting gunicorn 20.0.4
[2021-05-10 09:54:24 +0530] [26234] [INFO] Listening at: https://127.0.0.1:8080 (26234)
[2021-05-10 09:54:24 +0530] [26234] [INFO] Using worker: gthread
[2021-05-10 09:54:24 +0530] [26237] [INFO] Booting worker with pid: 26237
[2021-05-10 09:54:24 +0530] [26238] [INFO] Booting worker with pid: 26238
[2021-05-10 09:54:47 +0530] [26237] [ERROR] Exception in worker process
Traceback (most recent call last):
  File "/Users/mariobriggs/work/NLQ/git/venv-37/lib/python3.7/site-packages/gunicorn/arbiter.py", line 583, in spawn_worker
    worker.init_process()
  File "/Users/mariobriggs/work/NLQ/git/venv-37/lib/python3.7/site-packages/gunicorn/workers/gthread.py", line 92, in init_process
    super().init_process()
  File "/Users/mariobriggs/work/NLQ/git/venv-37/lib/python3.7/site-packages/gunicorn/workers/base.py", line 140, in init_process
    self.run()
  File "/Users/mariobriggs/work/NLQ/git/venv-37/lib/python3.7/site-packages/gunicorn/workers/gthread.py", line 203, in run
    callback(key.fileobj)
  File "/Users/mariobriggs/work/NLQ/git/venv-37/lib/python3.7/site-packages/gunicorn/workers/gthread.py", line 124, in accept
    self.enqueue_req(conn)
  File "/Users/mariobriggs/work/NLQ/git/venv-37/lib/python3.7/site-packages/gunicorn/workers/gthread.py", line 112, in enqueue_req
    conn.init()
  File "/Users/mariobriggs/work/NLQ/git/venv-37/lib/python3.7/site-packages/gunicorn/workers/gthread.py", line 53, in init
    **self.cfg.ssl_options)
  File "/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/ssl.py", line 1234, in wrap_socket
    context.set_ciphers(ciphers)
ssl.SSLError: ('No cipher can be selected.',)
[2021-05-10 09:54:47 +0530] [26237] [INFO] Worker exiting (pid: 26237)
javabrett commented 2 years ago

Firstly, there are some limitations on exactly what can be achieved currently in terms of allowed protocols and ciphersuites, and those limitations are a mixture of Python limitations and Gunicorn limitations.

What is possible with current Python 3.9 and Gunicorn 20.1.0 is:

So for example:

gunicorn --certfile cert.pem --key key.pem --ssl-version TLS_SERVER --ciphers AES256-GCM-SHA384:AES128-GCM-SHA256 -b 0.0.0.0:8000 --workers 1 flaskt:app

Would result in:

 SSLv2      not offered (OK)
 SSLv3      not offered (OK)
 TLS 1      not offered
 TLS 1.1    not offered
 TLS 1.2    offered (OK)
 TLS 1.3    offered (OK): final
 NPN/SPDY   not offered
 ALPN/HTTP2 not offered
TLSv1.2 (server order)
 x9d     AES256-GCM-SHA384                 RSA        AESGCM      256      TLS_RSA_WITH_AES_256_GCM_SHA384
 x9c     AES128-GCM-SHA256                 RSA        AESGCM      128      TLS_RSA_WITH_AES_128_GCM_SHA256
TLSv1.3 (server order)
 x1302   TLS_AES_256_GCM_SHA384            ECDH 253   AESGCM      256      TLS_AES_256_GCM_SHA384
 x1303   TLS_CHACHA20_POLY1305_SHA256      ECDH 253   ChaCha20    256      TLS_CHACHA20_POLY1305_SHA256
 x1301   TLS_AES_128_GCM_SHA256            ECDH 253   AESGCM      128      TLS_AES_128_GCM_SHA256

Notice that the TLS 1.2 cipher suites are filtered, but the TLS 1.3 ones are not.

I propose closing this now and picking-up additional functionality via or after #2649.

mariobriggs commented 2 years ago

no longer needed. Figured out issues was difference between standard names vs openssl names for the ciphers - https://www.mail-archive.com/openssl-users@openssl.org/msg89457.html

kamesh95 commented 1 year ago

@javabrett I have been messing my head around on how to add support for TLSv1.3 with gunicorn 20.1.0 and python 3.9 but seems like this is a blocker and I can't get this working. The example you mentioned above which promises to offer TLSv1.3 shows not offered for me. I have created a sample flask app and ran gunicorn with your config:

gunicorn --certfile cert.pem --key key.pem --ssl-version TLS_SERVER --ciphers AES256-GCM-SHA384:AES128-GCM-SHA256 -b 0.0.0.0:8000 --workers 1 flaskt:app

Result:

Screenshot 2023-03-06 at 16 21 32

Looking for some inputs here. Thanks.