Merge pull request #2649 from Nordix/sslcontext

Update SSLContext handling
This commit is contained in:
Benoit Chesneau 2023-05-11 09:51:37 +02:00 committed by GitHub
commit f955a0c18c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 175 additions and 26 deletions

View File

@ -320,8 +320,6 @@ The log config file written in JSON.
``logconfig_dict``
~~~~~~~~~~~~~~~~~~
**Command line:** ``--log-config-dict``
**Default:** ``{}``
The log config dictionary to use, using the standard Python
@ -343,7 +341,7 @@ For more context you can look at the default configuration dictionary for loggin
**Command line:** ``--log-syslog-to SYSLOG_ADDR``
**Default:** ``'unix:///var/run/syslog'``
**Default:** ``'udp://localhost:514'``
Address to send syslog messages.
@ -932,6 +930,29 @@ Called just before exiting Gunicorn.
The callable needs to accept a single instance variable for the Arbiter.
.. _ssl-context:
``ssl_context``
~~~~~~~~~~~~~~~
**Default:**
.. code-block:: python
def ssl_context(config, default_ssl_context_factory):
return default_ssl_context_factory()
Called when SSLContext is needed.
Allows fully customized SSL context to be used in place of the default
context.
The callable needs to accept an instance variable for the Config and
a factory function that returns default SSLContext which is initialized
with certificates, private key, cert_reqs, and ciphers according to
config and can be further customized by the callable.
The callable needs to return SSLContext object.
Server Mechanics
----------------
@ -994,9 +1015,7 @@ Set the ``SO_REUSEPORT`` flag on the listening socket.
**Default:** ``'.'``
Change directory to specified directory before loading apps.
Default is the current directory.
Change directory to specified directory before loading apps.
.. _daemon:
@ -1157,10 +1176,16 @@ temporary directory.
**Default:** ``{'X-FORWARDED-PROTOCOL': 'ssl', 'X-FORWARDED-PROTO': 'https', 'X-FORWARDED-SSL': 'on'}``
A dictionary containing headers and values that the front-end proxy
uses to indicate HTTPS requests. These tell Gunicorn to set
uses to indicate HTTPS requests. If the source IP is permitted by
``forwarded-allow-ips`` (below), *and* at least one request header matches
a key-value pair listed in this dictionary, then Gunicorn will set
``wsgi.url_scheme`` to ``https``, so your application can tell that the
request is secure.
If the other headers listed in this dictionary are not present in the request, they will be ignored,
but if the other headers are present and do not match the provided values, then
the request will fail to parse. See the note below for more detailed examples of this behaviour.
The dictionary should map upper-case header names to exact string
values. The value comparisons are case-sensitive, unlike the header
names, so make sure they're exactly what your front-end proxy sends
@ -1188,6 +1213,68 @@ you still trust the environment).
By default, the value of the ``FORWARDED_ALLOW_IPS`` environment
variable. If it is not defined, the default is ``"127.0.0.1"``.
.. note::
The interplay between the request headers, the value of ``forwarded_allow_ips``, and the value of
``secure_scheme_headers`` is complex. Various scenarios are documented below to further elaborate. In each case, we
have a request from the remote address 134.213.44.18, and the default value of ``secure_scheme_headers``:
.. code::
secure_scheme_headers = {
'X-FORWARDED-PROTOCOL': 'ssl',
'X-FORWARDED-PROTO': 'https',
'X-FORWARDED-SSL': 'on'
}
.. list-table::
:header-rows: 1
:align: center
:widths: auto
* - ``forwarded-allow-ips``
- Secure Request Headers
- Result
- Explanation
* - .. code::
["127.0.0.1"]
- .. code::
X-Forwarded-Proto: https
- .. code::
wsgi.url_scheme = "http"
- IP address was not allowed
* - .. code::
"*"
- <none>
- .. code::
wsgi.url_scheme = "http"
- IP address allowed, but no secure headers provided
* - .. code::
"*"
- .. code::
X-Forwarded-Proto: https
- .. code::
wsgi.url_scheme = "https"
- IP address allowed, one request header matched
* - .. code::
["134.213.44.18"]
- .. code::
X-Forwarded-Ssl: on
X-Forwarded-Proto: http
- ``InvalidSchemeHeaders()`` raised
- IP address allowed, but the two secure headers disagreed on if HTTPS was used
.. _pythonpath:
``pythonpath``
@ -1360,8 +1447,9 @@ A positive integer generally in the ``2-4 x $(NUM_CORES)`` range.
You'll want to vary this a bit to find the best for your particular
application's work load.
By default, the value of the ``WEB_CONCURRENCY`` environment variable.
If it is not defined, the default is ``1``.
By default, the value of the ``WEB_CONCURRENCY`` environment variable,
which is set by some Platform-as-a-Service providers such as Heroku. If
it is not defined, the default is ``1``.
.. _worker-class:

View File

@ -214,3 +214,27 @@ def worker_int(worker):
def worker_abort(worker):
worker.log.info("worker received SIGABRT signal")
def ssl_context(conf, default_ssl_context_factory):
import ssl
# The default SSLContext returned by the factory function is initialized
# with the TLS parameters from config, including TLS certificates and other
# parameters.
context = default_ssl_context_factory()
# The SSLContext can be further customized, for example by enforcing
# minimum TLS version.
context.minimum_version = ssl.TLSVersion.TLSv1_3
# Server can also return different server certificate depending which
# hostname the client uses. Requires Python 3.7 or later.
def sni_callback(socket, server_hostname, context):
if server_hostname == "foo.127.0.0.1.nip.io":
new_context = default_ssl_context_factory()
new_context.load_cert_chain(certfile="foo.pem", keyfile="foo-key.pem")
socket.context = new_context
context.sni_callback = sni_callback
return context

View File

@ -1523,6 +1523,8 @@ class LogConfigDict(Setting):
Format: https://docs.python.org/3/library/logging.config.html#logging.config.dictConfig
For more context you can look at the default configuration dictionary for logging, which can be found at ``gunicorn.glogging.CONFIG_DEFAULTS``.
.. versionadded:: 19.8
"""
@ -2004,6 +2006,28 @@ class OnExit(Setting):
The callable needs to accept a single instance variable for the Arbiter.
"""
class NewSSLContext(Setting):
name = "ssl_context"
section = "Server Hooks"
validator = validate_callable(2)
type = callable
def ssl_context(config, default_ssl_context_factory):
return default_ssl_context_factory()
default = staticmethod(ssl_context)
desc = """\
Called when SSLContext is needed.
Allows fully customized SSL context to be used in place of the default
context.
The callable needs to accept an instance variable for the Config and
a factory function that returns default SSLContext which is initialized
with certificates, private key, cert_reqs, and ciphers according to
config and can be further customized by the callable.
The callable needs to return SSLContext object.
"""
class ProxyProtocol(Setting):
name = "proxy_protocol"

View File

@ -6,6 +6,7 @@
import errno
import os
import socket
import ssl
import stat
import sys
import time
@ -203,10 +204,27 @@ def create_sockets(conf, log, fds=None):
return listeners
def close_sockets(listeners, unlink=True):
for sock in listeners:
sock_name = sock.getsockname()
sock.close()
if unlink and _sock_type(sock_name) is UnixSocket:
os.unlink(sock_name)
def ssl_context(conf):
def default_ssl_context_factory():
context = ssl.SSLContext(conf.ssl_version)
context.load_cert_chain(certfile=conf.certfile, keyfile=conf.keyfile)
context.verify_mode = conf.cert_reqs
if conf.ciphers:
context.set_ciphers(conf.ciphers)
if conf.ca_certs:
context.load_verify_locations(cafile=conf.ca_certs)
return context
return conf.ssl_context(conf, default_ssl_context_factory)
def ssl_wrap_socket(sock, conf):
return ssl_context(conf).wrap_socket(sock, server_side=True,
suppress_ragged_eofs=conf.suppress_ragged_eofs,
do_handshake_on_connect=conf.do_handshake_on_connect)

View File

@ -21,6 +21,7 @@ import eventlet.wsgi
import greenlet
from gunicorn.workers.base_async import AsyncWorker
from gunicorn.sock import ssl_wrap_socket
# ALREADY_HANDLED is removed in 0.30.3+ now it's `WSGI_LOCAL.already_handled: bool`
# https://github.com/eventlet/eventlet/pull/544
@ -152,9 +153,7 @@ class EventletWorker(AsyncWorker):
def handle(self, listener, client, addr):
if self.cfg.is_ssl:
client = eventlet.wrap_ssl(client, server_side=True,
**self.cfg.ssl_options)
client = ssl_wrap_socket(client, self.cfg)
super().handle(listener, client, addr)
def run(self):

View File

@ -24,6 +24,7 @@ from gevent import hub, monkey, socket, pywsgi
import gunicorn
from gunicorn.http.wsgi import base_environ
from gunicorn.sock import ssl_context
from gunicorn.workers.base_async import AsyncWorker
VERSION = "gevent/%s gunicorn/%s" % (gevent.__version__, gunicorn.__version__)
@ -58,7 +59,7 @@ class GeventWorker(AsyncWorker):
ssl_args = {}
if self.cfg.is_ssl:
ssl_args = dict(server_side=True, **self.cfg.ssl_options)
ssl_args = dict(ssl_context=ssl_context(self.cfg))
for s in self.sockets:
s.setblocking(1)

View File

@ -27,6 +27,7 @@ from threading import RLock
from . import base
from .. import http
from .. import util
from .. import sock
from ..http import wsgi
@ -52,8 +53,7 @@ class TConn(object):
if self.parser is None:
# wrap the socket if needed
if self.cfg.is_ssl:
self.sock = ssl.wrap_socket(self.sock, server_side=True,
**self.cfg.ssl_options)
self.sock = sock.ssl_wrap_socket(self.sock, self.cfg)
# initialize the parser
self.parser = http.RequestParser(self.cfg, self.sock, self.client)

View File

@ -17,6 +17,7 @@ from tornado.ioloop import IOLoop, PeriodicCallback
from tornado.wsgi import WSGIContainer
from gunicorn.workers.base import Worker
from gunicorn import __version__ as gversion
from gunicorn.sock import ssl_context
# Tornado 5.0 updated its IOLoop, and the `io_loop` arguments to many
@ -140,16 +141,11 @@ class TornadoWorker(Worker):
server_class = _HTTPServer
if self.cfg.is_ssl:
_ssl_opt = copy.deepcopy(self.cfg.ssl_options)
# tornado refuses initialization if ssl_options contains following
# options
del _ssl_opt["do_handshake_on_connect"]
del _ssl_opt["suppress_ragged_eofs"]
if TORNADO5:
server = server_class(app, ssl_options=_ssl_opt)
server = server_class(app, ssl_options=ssl_context(self.cfg))
else:
server = server_class(app, io_loop=self.ioloop,
ssl_options=_ssl_opt)
ssl_options=ssl_context(self.cfg))
else:
if TORNADO5:
server = server_class(app)

View File

@ -14,6 +14,7 @@ import sys
import gunicorn.http as http
import gunicorn.http.wsgi as wsgi
import gunicorn.sock as sock
import gunicorn.util as util
import gunicorn.workers.base as base
@ -128,9 +129,7 @@ class SyncWorker(base.Worker):
req = None
try:
if self.cfg.is_ssl:
client = ssl.wrap_socket(client, server_side=True,
**self.cfg.ssl_options)
client = sock.ssl_wrap_socket(client, self.cfg)
parser = http.RequestParser(self.cfg, client, addr)
req = next(parser)
self.handle_request(listener, req, client, addr)