From d8c3b1490e0f0e065c09b8143e11005e5e48addb Mon Sep 17 00:00:00 2001 From: Tero Saarni Date: Thu, 11 May 2023 17:45:57 +0300 Subject: [PATCH] Deprecate ssl_version option This change defaults SSLContext to Python's ssl.create_default_context() and marks ssl_version option as deprecated. The option value will be ignored and warnign will be printed in stderr. The ssl_version option was depending on old method of setting TLS min/max version, which has not worked well anymore with modern Python versions. --- docs/gunicorn_ext.py | 4 +- docs/source/settings.rst | 97 ++++++++++++++++++++++++---------------- gunicorn/config.py | 71 ++++++++++++++--------------- gunicorn/sock.py | 4 +- tests/test_config.py | 35 --------------- tests/test_ssl.py | 8 ---- 6 files changed, 97 insertions(+), 122 deletions(-) diff --git a/docs/gunicorn_ext.py b/docs/gunicorn_ext.py index 5b478147..4310162e 100755 --- a/docs/gunicorn_ext.py +++ b/docs/gunicorn_ext.py @@ -48,7 +48,9 @@ def format_settings(app): def fmt_setting(s): - if callable(s.default): + if hasattr(s, "default_doc"): + val = s.default_doc + elif callable(s.default): val = inspect.getsource(s.default) val = "\n".join(" %s" % line for line in val.splitlines()) val = "\n\n.. code-block:: python\n\n" + val diff --git a/docs/source/settings.rst b/docs/source/settings.rst index 81558ac7..a23fb692 100644 --- a/docs/source/settings.rst +++ b/docs/source/settings.rst @@ -32,7 +32,7 @@ Config File **Default:** ``'./gunicorn.conf.py'`` -The Gunicorn config file. +:ref:`The Gunicorn config file`. A string of the form ``PATH``, ``file:PATH``, or ``python:MODULE_NAME``. @@ -305,16 +305,6 @@ The log config file to use. Gunicorn uses the standard Python logging module's Configuration file format. -.. _logconfig-json: - -logiconfig_json -~~~~~~~~~ - -* ``--log-config-json FILE`` -* ``None`` - -The log config file written in JSON. - .. _logconfig-dict: ``logconfig_dict`` @@ -324,9 +314,9 @@ The log config file written in JSON. The log config dictionary to use, using the standard Python logging module's dictionary configuration format. This option -takes precedence over the :ref:`logconfig` and :ref:`logConfigJson` options, which uses the -older file configuration format and JSON respectively. - +takes precedence over the :ref:`logconfig` and :ref:`logConfigJson` options, +which uses the older file configuration format and JSON +respectively. Format: https://docs.python.org/3/library/logging.config.html#logging.config.dictConfig @@ -334,6 +324,21 @@ For more context you can look at the default configuration dictionary for loggin .. versionadded:: 19.8 +.. _logconfig-json: + +``logconfig_json`` +~~~~~~~~~~~~~~~~~~ + +**Command line:** ``--log-config-json FILE`` + +**Default:** ``None`` + +The log config to read config from a JSON file + +Format: https://docs.python.org/3/library/logging.config.html#logging.config.jsonConfig + +.. versionadded:: 20.0 + .. _syslog-addr: ``syslog_addr`` @@ -519,7 +524,10 @@ SSL certificate file **Default:** ``<_SSLMethod.PROTOCOL_TLS: 2>`` -SSL version to use. +SSL version to use (see stdlib ssl module's). + +.. deprecated:: 20.2 + The option is deprecated and it is currently ignored. Use :ref:`ssl-context` instead. ============= ============ --ssl-version Description @@ -542,6 +550,9 @@ TLS_SERVER Auto-negotiate the highest protocol version like TLS, .. versionchanged:: 20.0 This setting now accepts string names based on ``ssl.PROTOCOL_`` constants. +.. versionchanged:: 20.0.1 + The default value has been changed from ``ssl.PROTOCOL_SSLv23`` to + ``ssl.PROTOCOL_TLS`` when Python >= 3.6 . .. _cert-reqs: @@ -554,15 +565,13 @@ TLS_SERVER Auto-negotiate the highest protocol version like TLS, Whether client certificate is required (see stdlib ssl module's) -**Options:** - -`--cert-reqs=0` --- no client veirifcation - -`--cert-reqs=1` --- ssl.CERT_OPTIONAL - -`--cert-reqs=2` --- ssl.CERT_REQUIRED - - +=========== =========================== +--cert-reqs Description +=========== =========================== +`0` no client veirifcation +`1` ssl.CERT_OPTIONAL +`2` ssl.CERT_REQUIRED +=========== =========================== .. _ca-certs: @@ -954,8 +963,7 @@ The callable needs to accept a single instance variable for the Arbiter. Called when SSLContext is needed. -Allows fully customized SSL context to be used in place of the default -context. +Allows customizing SSL context. The callable needs to accept an instance variable for the Config and a factory function that returns default SSLContext which is initialized @@ -963,6 +971,18 @@ 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. +Following example shows a configuration file that sets the minimum TLS version to 1.3: + +.. code-block:: python + + def ssl_context(conf, default_ssl_context_factory): + import ssl + context = default_ssl_context_factory() + context.minimum_version = ssl.TLSVersion.TLSv1_3 + return context + +.. versionadded:: 20.2 + Server Mechanics ---------------- @@ -1107,7 +1127,7 @@ If not set, the default temporary directory will be used. **Command line:** ``-u USER`` or ``--user USER`` -**Default:** ``501`` +**Default:** ``os.geteuid()`` Switch worker processes to run as this user. @@ -1122,7 +1142,7 @@ change the worker process user. **Command line:** ``-g GROUP`` or ``--group GROUP`` -**Default:** ``20`` +**Default:** ``os.getegid()`` Switch worker process to run as this group. @@ -1226,8 +1246,9 @@ 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``: + ``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:: @@ -1238,7 +1259,7 @@ variable. If it is not defined, the default is ``"127.0.0.1"``. } - .. list-table:: + .. list-table:: :header-rows: 1 :align: center :widths: auto @@ -1247,35 +1268,35 @@ variable. If it is not defined, the default is ``"127.0.0.1"``. - Secure Request Headers - Result - Explanation - * - .. code:: + * - .. code:: ["127.0.0.1"] - .. code:: X-Forwarded-Proto: https - - .. code:: + - .. code:: wsgi.url_scheme = "http" - IP address was not allowed - * - .. code:: + * - .. code:: "*" - - - .. code:: + - .. code:: wsgi.url_scheme = "http" - IP address allowed, but no secure headers provided - * - .. code:: + * - .. code:: "*" - .. code:: X-Forwarded-Proto: https - - .. code:: + - .. code:: wsgi.url_scheme = "https" - IP address allowed, one request header matched - * - .. code:: + * - .. code:: ["134.213.44.18"] - .. code:: diff --git a/gunicorn/config.py b/gunicorn/config.py index 51d1ac07..97c12588 100644 --- a/gunicorn/config.py +++ b/gunicorn/config.py @@ -364,25 +364,9 @@ def validate_pos_int(val): def validate_ssl_version(val): - ssl_versions = {} - for protocol in [p for p in dir(ssl) if p.startswith("PROTOCOL_")]: - ssl_versions[protocol[9:]] = getattr(ssl, protocol) - if val in ssl_versions: - # string matching PROTOCOL_... - return ssl_versions[val] - - try: - intval = validate_pos_int(val) - if intval in ssl_versions.values(): - # positive int matching a protocol int constant - return intval - except (ValueError, TypeError): - # negative integer or not an integer - # drop this in favour of the more descriptive ValueError below - pass - - raise ValueError("Invalid ssl_version: %s. Valid options: %s" - % (val, ', '.join(ssl_versions))) + if val != SSLVersion.default: + sys.stderr.write("Warning: option `ssl_version` is deprecated and it is ignored. Use ssl_context instead.\n") + return val def validate_string(val): @@ -736,7 +720,7 @@ class WorkerConnections(Setting): desc = """\ The maximum number of simultaneous clients. - This setting only affects the Eventlet and Gevent worker types. + This setting only affects the ``gthread``, ``eventlet`` and ``gevent`` worker types. """ @@ -1066,6 +1050,7 @@ class Chdir(Setting): cli = ["--chdir"] validator = validate_chdir default = util.getcwd() + default_doc = "``'.'``" desc = """\ Change directory to specified directory before loading apps. """ @@ -1157,6 +1142,7 @@ class User(Setting): meta = "USER" validator = validate_user default = os.geteuid() + default_doc = "``os.geteuid()``" desc = """\ Switch worker processes to run as this user. @@ -1173,6 +1159,7 @@ class Group(Setting): meta = "GROUP" validator = validate_group default = os.getegid() + default_doc = "``os.getegid()``" desc = """\ Switch worker process to run as this group. @@ -2019,14 +2006,25 @@ class NewSSLContext(Setting): desc = """\ Called when SSLContext is needed. - Allows fully customized SSL context to be used in place of the default - context. + Allows customizing SSL 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. + + Following example shows a configuration file that sets the minimum TLS version to 1.3: + + .. code-block:: python + + def ssl_context(conf, default_ssl_context_factory): + import ssl + context = default_ssl_context_factory() + context.minimum_version = ssl.TLSVersion.TLSv1_3 + return context + + .. versionadded:: 20.2 """ class ProxyProtocol(Setting): @@ -2105,17 +2103,12 @@ class SSLVersion(Setting): else: default = ssl.PROTOCOL_SSLv23 - desc = """\ - SSL version to use (see stdlib ssl module's) - - .. versionchanged:: 20.0.1 - The default value has been changed from ``ssl.PROTOCOL_SSLv23`` to - ``ssl.PROTOCOL_TLS`` when Python >= 3.6 . - - """ default = ssl.PROTOCOL_SSLv23 desc = """\ - SSL version to use. + SSL version to use (see stdlib ssl module's). + + .. deprecated:: 20.2 + The option is deprecated and it is currently ignored. Use :ref:`ssl-context` instead. ============= ============ --ssl-version Description @@ -2138,6 +2131,9 @@ class SSLVersion(Setting): .. versionchanged:: 20.0 This setting now accepts string names based on ``ssl.PROTOCOL_`` constants. + .. versionchanged:: 20.0.1 + The default value has been changed from ``ssl.PROTOCOL_SSLv23`` to + ``ssl.PROTOCOL_TLS`` when Python >= 3.6 . """ @@ -2149,13 +2145,14 @@ class CertReqs(Setting): default = ssl.CERT_NONE desc = """\ Whether client certificate is required (see stdlib ssl module's) - ============== =========================== - `--cert-reqs=0` --- no client veirifcation - `--cert-reqs=1` --- ssl.CERT_OPTIONAL - - `--cert-reqs=2` --- ssl.CERT_REQUIRED - ============== =========================== + =========== =========================== + --cert-reqs Description + =========== =========================== + `0` no client veirifcation + `1` ssl.CERT_OPTIONAL + `2` ssl.CERT_REQUIRED + =========== =========================== """ diff --git a/gunicorn/sock.py b/gunicorn/sock.py index 31b9919d..22e25b33 100644 --- a/gunicorn/sock.py +++ b/gunicorn/sock.py @@ -213,13 +213,11 @@ def close_sockets(listeners, unlink=True): def ssl_context(conf): def default_ssl_context_factory(): - context = ssl.SSLContext(conf.ssl_version) + context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH, cafile=conf.ca_certs) 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) diff --git a/tests/test_config.py b/tests/test_config.py index 211ee017..c094f6a2 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -453,41 +453,6 @@ def test_umask_config(options, expected): assert app.cfg.umask == expected -@pytest.mark.parametrize("options, expected", [ - (["--ssl-version", "SSLv23"], 2), - (["--ssl-version", "TLSv1"], 3), - (["--ssl-version", "2"], 2), - (["--ssl-version", "3"], 3), -]) -def test_ssl_version_named_constants_python3(options, expected): - _test_ssl_version(options, expected) - - -@pytest.mark.skipif(sys.version_info < (3, 6), - reason="requires python3.6+") -@pytest.mark.parametrize("options, expected", [ - (["--ssl-version", "TLS"], 2), - (["--ssl-version", "TLSv1_1"], 4), - (["--ssl-version", "TLSv1_2"], 5), - (["--ssl-version", "TLS_SERVER"], 17), -]) -def test_ssl_version_named_constants_python36(options, expected): - _test_ssl_version(options, expected) - - -@pytest.mark.parametrize("ssl_version", [ - "FOO", - "-99", - "99991234" -]) -def test_ssl_version_bad(ssl_version): - c = config.Config() - with pytest.raises(ValueError) as exc: - c.set("ssl_version", ssl_version) - assert 'Valid options' in str(exc.value) - assert "TLSv" in str(exc.value) - - def _test_ssl_version(options, expected): cmdline = ["prog_name"] cmdline.extend(options) diff --git a/tests/test_ssl.py b/tests/test_ssl.py index 97e05d86..7d15de17 100644 --- a/tests/test_ssl.py +++ b/tests/test_ssl.py @@ -32,14 +32,6 @@ def test_certfile(): assert CertFile.default is None -def test_ssl_version(): - assert issubclass(SSLVersion, Setting) - assert SSLVersion.name == 'ssl_version' - assert SSLVersion.section == 'SSL' - assert SSLVersion.cli == ['--ssl-version'] - assert SSLVersion.default == ssl.PROTOCOL_SSLv23 - - def test_cacerts(): assert issubclass(CACerts, Setting) assert CACerts.name == 'ca_certs'