Can't run normally when using SSLContext
sdir opened this issue · comments
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
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).
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. IfContext
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).
@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.