sanic-org / sanic

Accelerate your web app development | Build fast. Run fast.

Home Page:https://sanic.dev

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Can't run normally when using SSLContext

sdir opened this issue · comments

commented

Is there an existing issue for this?

  • I have searched the existing issues

Describe the bug

   app.run(host='0.0.0.0', port=443, ssl=context, dev=True)
  File "/usr/lib/python3.10/site-packages/sanic/mixins/startup.py", line 209, in run
    serve(primary=self)  # type: ignore
  File "/usr/lib/python3.10/site-packages/sanic/mixins/startup.py", line 862, in serve
    manager.run()
  File "/usr/lib/python3.10/site-packages/sanic/worker/manager.py", line 94, in run
    self.start()
  File "/usr/lib/python3.10/site-packages/sanic/worker/manager.py", line 101, in start
    process.start()
  File "/usr/lib/python3.10/site-packages/sanic/worker/process.py", line 53, in start
    self._current_process.start()
  File "/usr/lib/python3.10/multiprocessing/process.py", line 121, in start
    self._popen = self._Popen(self)
  File "/usr/lib/python3.10/multiprocessing/context.py", line 284, in _Popen
    return Popen(process_obj)
  File "/usr/lib/python3.10/multiprocessing/popen_spawn_posix.py", line 32, in __init__
    super().__init__(process_obj)
  File "/usr/lib/python3.10/multiprocessing/popen_fork.py", line 19, in __init__
    self._launch(process_obj)
  File "/usr/lib/python3.10/multiprocessing/popen_spawn_posix.py", line 47, in _launch
    reduction.dump(process_obj, fp)
  File "/usr/lib/python3.10/multiprocessing/reduction.py", line 60, in dump
    ForkingPickler(file, protocol).dump(obj)
TypeError: cannot pickle 'SSLContext' object

Code snippet

from sanic import Sanic
import ssl

context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
context.load_cert_chain("certs/fullchain.pem", "certs/privkey.pem")

app = Sanic('test')

if __name__ == "__main__":
    app.run(host='0.0.0.0', port=443, ssl=context, dev=True)

Expected Behavior

No response

How do you run Sanic?

As a script (app.run or Sanic.serve)

Operating System

linux

Sanic Version

22.12.0

Additional context

pickle does not support dump ssl.SSLContext causing this problem.
multiprocessing.context use pickle

import ssl

context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
context.load_cert_chain("certs/fullchain.pem", "certs/privkey.pem")
pickle.dumps(context)

This is known. You can pass a dict of the paths, or use context Wil a single process or legacy mode. Working in an alternative.

from sanic import Sanic

app = Sanic('test')

if __name__ == "__main__":
    app.run(host='0.0.0.0', port=443, ssl={
        "cert": "certs/fullchain.pem",
        "key": "certs/privkey.pem",
    }, dev=True)

@teixeirazeus That is equivalent to simply passing the cert dir as a string, as long as the files are named fullchain.pem and privkey.pem:

app.run(host='0.0.0.0', port=443, ssl="certs", dev=True)

Or you may leave out app.run entirely (actually this is now preferred) and use the CLI instead. Assuming your file is main.py:

sanic --host 0.0.0.0 --port 443 --tls certs --dev main:app
commented

I have read the code and documentation in detail. I know all these ways, @Tronic @teixeirazeus thank you for your answer.

I need full control over details such as which crypto algorithms are permitted.

@sdir The problem is that SSLContext cannot be transferred to worker processes by pickling. This can be avoided by using

from sanic import Sanic

Sanic.start_method = "fork"

On Linux you should be fine with that as a workaround. Sanic normally uses spawn (and thus pickling) because the fork mode doesn't work properly on Mac or Windows.

I believe @ahopkins is working on a solution that would allow you to construct your custom SSLContext in each worker process, avoiding the pickling even in spawn mode, and the issue is open waiting for that as the ultimate solution. I hope that either the workaround or whatever Adam brews up helps you.

I need full control over details such as which crypto algorithms are permitted.

@sdir On this detail I would like to know more. Since TLS 1.2 and 1.3 there isn't much to control, but we are open to any input to make Sanic's selections on those more secure. If the tweaks that you do are reasonably compatible with browsers and not for weakening security, we might wish to implement the same in Sanic as well.

EDIT: Sanic currently gets grade A on https://www.ssllabs.com/ssltest (likely A+ if you add HSTS headers which my test app didn't have).

commented

I need full control over details such as which crypto algorithms are permitted.

@Tronic I didn't express myself clearly, This is just an example, not actually a crypto algorithms issue.
I just took what was mentioned in the document. If Context is not supported, should modify the document?

eg: load_verify_locations

import ssl
context = ssl.SSLcontext()
context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
context.load_cert_chain("certs/fullchain.pem", "certs/privkey.pem")
context.load_verify_locations("certs/ca.crt")

And this is very useful for me.

from sanic import Sanic
Sanic.start_method = "fork"

Normally load_verify_locations is only used on client side, to obtain CAs that are then used for verifying server certs. I am not sure if it is even possible (especially with Python) but in principle a server could also verify browsers' certificates. I have never seen this done anywhere. Unless you are doing that, you don't need the call.

For other checks and manipulations on certs, you can do it using temporary SSLContext (or other SSL tools because Python is fairly limited) and then not pass it to Sanic but let Sanic load the same cert files on its own.

As for cipher suites, Sanic only allows TLS 1.2 and 1.3, with the following cipher suites:

# TLS 1.3 (suites in server-preferred order)
TLS_AES_256_GCM_SHA384 (0x1302)   ECDH x25519 (eq. 3072 bits RSA)   FS	256
TLS_CHACHA20_POLY1305_SHA256 (0x1303)   ECDH x25519 (eq. 3072 bits RSA)   FS	256
TLS_AES_128_GCM_SHA256 (0x1301)   ECDH x25519 (eq. 3072 bits RSA)   FS	128
# TLS 1.2 (suites in server-preferred order)
TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 (0xcca9)   ECDH x25519 (eq. 3072 bits RSA)   FS	256
TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 (0xc02c)   ECDH x25519 (eq. 3072 bits RSA)   FS	256
TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 (0xc02b)   ECDH x25519 (eq. 3072 bits RSA)   FS	128

The weaker AES-128 MUST by implemented by the spec. The only thing I might change here is prefer CHACHA20 first on TLS 1.3 as well, but I don't think this can be configured with Python's SSLContext. But all three suites are actually very secure and supporting forward secrecy.

Another thing you could do with a customized context is to do something with the SNI sent in Client Hello (via a callback in SSLContext). Sanic uses this to select the correct vhost certificate if multiple certificates for different domains are available, or to reject the connection if no valid name was sent.

I'll leave it up to @sdir and @ahopkins to decide whether there is need to have a mechanism for constructing truly custom SSLContext (especially for platforms where fork cannot be used), or whether these issues are in fact not worth the effort.

+1 for this. My organization uses certificates for authentication internally, so load_verify_locations is mandatory because the server needs to have a copy of the companies CA that issues a certificate to each user. The server then verifies that the user is presenting a valid certificate signed by the company CA.

It works fine on the older Sanic, so it's a clear regression.

Then two paths to that, either specifically add support for load_verify_locations (assuming this is the missing piece that everyone needs), or add a signal for creating/manipulating SSLContext right as the workers are starting. Probably the latter as other needs may arise, and it is fairly simple to implement both in Sanic and for app developers (who will in any case need to make changes, unfortunately).

commented

@goatpop try custom class

class SSLContextSimple(ssl.SSLContext):

    def config(self, conf:dict):
        self.conf = conf
        self.load_config(conf)

    def load_config(self, conf):
        self.load_cert_chain(conf['cert'], conf['key'])
        self.load_verify_locations(conf['ca'])

    def __getnewargs__(self):
        return (self.protocol,)

    def __getstate__(self):
        return self.conf

    def __setstate__(self, state):
        self.load_config(state)

context = SSLContextSimple(ssl.PROTOCOL_TLS_SERVER)
context.config({
    'cert': 'certs/fullchain.pem',
    'key' : 'certs/privkey.pem',
    'ca': 'certs/ca.crt'
})


if __name__ == "__main__":
    app.run(host='0.0.0.0', port=443, ssl=context, dev=True)

add a signal for creating/manipulating SSLContext right as the workers are starting

Yes, this is the approach I was after.