From b35a3886851543b964e9a5d8d48e01c140d824d0 Mon Sep 17 00:00:00 2001 From: Pascal Zimmermann Date: Thu, 20 Jul 2023 20:50:41 +0200 Subject: [PATCH] Add PostgreSQL & Redis TLS/mTLS support (#3435) --- doc/admin/config.rst | 31 ++++++++++++++++++++++++++ src/pretix/settings.py | 49 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 80 insertions(+) diff --git a/doc/admin/config.rst b/doc/admin/config.rst index d5978a327d..964b852f9e 100644 --- a/doc/admin/config.rst +++ b/doc/admin/config.rst @@ -152,6 +152,10 @@ Example:: password=abcd host=localhost port=3306 + sslmode=require + sslrootcert=/etc/pretix/postgresql-ca.crt + sslcert=/etc/pretix/postgresql-client-crt.crt + sslkey=/etc/pretix/postgresql-client-key.key ``backend`` One of ``sqlite3`` and ``postgresql``. @@ -163,6 +167,11 @@ Example:: ``user``, ``password``, ``host``, ``port`` Connection details for the database connection. Empty by default. +``sslmode``, ``sslrootcert`` + Connection TLS details for the PostgreSQL database connection. Possible values of ``sslmode`` are ``disable``, ``allow``, ``prefer``, ``require``, ``verify-ca``, and ``verify-full``. ``sslrootcert`` should be the accessible path of the ca certificate. Both values are empty by default. + +``sslcert``, ``sslkey`` + Connection mTLS details for the PostgreSQL database connection. It's also necessary to specify ``sslmode`` and ``sslrootcert`` parameters, please check the correct values from the TLS part. ``sslcert`` should be the accessible path of the client certificate. ``sslkey`` should be the accessible path of the client key. All values are empty by default. .. _`config-replica`: Database replica settings @@ -324,6 +333,10 @@ to speed up various operations:: ["sentinel_host_3", 26379] ] password=password + ssl_cert_reqs=required + ssl_ca_certs=/etc/pretix/redis-ca.pem + ssl_keyfile=/etc/pretix/redis-client-crt.pem + ssl_certfile=/etc/pretix/redis-client-key.key ``location`` The location of redis, as a URL of the form ``redis://[:password]@localhost:6379/0`` @@ -347,6 +360,22 @@ to speed up various operations:: If your redis setup doesn't require a password or you already specified it in the location you can omit this option. If this is set it will be passed to redis as the connection option PASSWORD. +``ssl_cert_reqs`` + If this is set it will be passed to redis as the connection option ``SSL_CERT_REQS``. + Possible values are ``none``, ``optional``, and ``required``. + +``ssl_ca_certs`` + If your redis setup doesn't require TLS you can omit this option. + If this is set it will be passed to redis as the connection option ``SSL_CA_CERTS``. Possible value is the ca path. + +``ssl_keyfile`` + If your redis setup doesn't require mTLS you can omit this option. + If this is set it will be passed to redis as the connection option ``SSL_KEYFILE``. Possible value is the keyfile path. + +``ssl_certfile`` + If your redis setup doesn't require mTLS you can omit this option. + If this is set it will be passed to redis as the connection option ``SSL_CERTFILE``. Possible value is the certfile path. + If redis is not configured, pretix will store sessions and locks in the database. If memcached is configured, memcached will be used for caching instead of redis. @@ -396,6 +425,8 @@ The two ``transport_options`` entries can be omitted in most cases. If they are present they need to be a valid JSON dictionary. For possible entries in that dictionary see the `Celery documentation`_. +It is possible the use Redis with TLS/mTLS for the broker or the backend. To do so, it is necessary to specify the TLS identifier ``rediss``, the ssl mode ``ssl_cert_reqs`` and optionally specify the CA (TLS) ``ssl_ca_certs``, cert ``ssl_certfile`` and key ``ssl_keyfile`` (mTLS) path as encoded string. the following uri describes the format and possible parameters ``rediss://0.0.0.0:6379/1?ssl_cert_reqs=required&ssl_ca_certs=%2Fetc%2Fpretix%2Fredis-ca.pem&ssl_certfile=%2Fetc%2Fpretix%2Fredis-client-crt.pem&ssl_keyfile=%2Fetc%2Fpretix%2Fredis-client-key.key`` + To use redis with sentinels set the broker or backend to ``sentinel://sentinel_host_1:26379;sentinel_host_2:26379/0`` and the respective transport_options to ``{"master_name":"mymaster"}``. If your redis instances behind the sentinel have a password use ``sentinel://:my_password@sentinel_host_1:26379;sentinel_host_2:26379/0``. diff --git a/src/pretix/settings.py b/src/pretix/settings.py index e57ad28354..5e2b965730 100644 --- a/src/pretix/settings.py +++ b/src/pretix/settings.py @@ -116,6 +116,29 @@ elif 'mysql' in db_backend: db_options = {} +postgresql_sslmode = config.get('database', 'sslmode', fallback='disable') +USE_DATABASE_TLS = postgresql_sslmode != 'disable' +USE_DATABASE_MTLS = USE_DATABASE_TLS and config.has_option('database', 'sslcert') + +if USE_DATABASE_TLS or USE_DATABASE_MTLS: + tls_config = {} + if not USE_DATABASE_MTLS: + if 'postgresql' in db_backend: + tls_config = { + 'sslmode': config.get('database', 'sslmode'), + 'sslrootcert': config.get('database', 'sslrootcert'), + } + else: + if 'postgresql' in db_backend: + tls_config = { + 'sslmode': config.get('database', 'sslmode'), + 'sslrootcert': config.get('database', 'sslrootcert'), + 'sslcert': config.get('database', 'sslcert'), + 'sslkey': config.get('database', 'sslkey'), + } + + db_options.update(tls_config) + DATABASES = { 'default': { 'ENGINE': 'django.db.backends.' + db_backend, @@ -228,6 +251,9 @@ if HAS_MEMCACHED: HAS_REDIS = config.has_option('redis', 'location') USE_REDIS_SENTINEL = config.has_option('redis', 'sentinels') +redis_ssl_cert_reqs = config.get('redis', 'ssl_cert_reqs', fallback='none') +USE_REDIS_TLS = redis_ssl_cert_reqs != 'none' +USE_REDIS_MTLS = USE_REDIS_TLS and config.has_option('redis', 'ssl_certfile') HAS_REDIS_PASSWORD = config.has_option('redis', 'password') if HAS_REDIS: OPTIONS = { @@ -243,6 +269,29 @@ if HAS_REDIS: OPTIONS["SENTINEL_KWARGS"] = {"socket_timeout": 1} OPTIONS["SENTINELS"] = [tuple(sentinel) for sentinel in loads(config.get('redis', 'sentinels'))] + if USE_REDIS_TLS or USE_REDIS_MTLS: + tls_config = {} + if not USE_REDIS_MTLS: + tls_config = { + 'ssl_cert_reqs': config.get('redis', 'ssl_cert_reqs'), + 'ssl_ca_certs': config.get('redis', 'ssl_ca_certs'), + } + else: + tls_config = { + 'ssl_cert_reqs': config.get('redis', 'ssl_cert_reqs'), + 'ssl_ca_certs': config.get('redis', 'ssl_ca_certs'), + 'ssl_keyfile': config.get('redis', 'ssl_keyfile'), + 'ssl_certfile': config.get('redis', 'ssl_certfile'), + } + + if USE_REDIS_SENTINEL is False: + # The CONNECTION_POOL_KWARGS option is necessary for self-signed certs. For further details, please check + # https://github.com/jazzband/django-redis/issues/554#issuecomment-949498321 + OPTIONS["CONNECTION_POOL_KWARGS"] = tls_config + OPTIONS["REDIS_CLIENT_KWARGS"].update(tls_config) + else: + OPTIONS["SENTINEL_KWARGS"].update(tls_config) + if HAS_REDIS_PASSWORD: OPTIONS["PASSWORD"] = config.get('redis', 'password')