Repository: goauthentik/authentik

Issue:
Add the ability to provide custom CA certs to the redis client
**Is your feature request related to a problem? Please describe.**
I would like to use self-signed certificates (for example, when using cert-manager in a Kubernetes cluster) for the redis connection. 

Currently, CA verification has to be disabled to prevent the following error:

```
{"event": "Redis Connection failed, retrying... (Error 1 connecting to redis-1f0f-master.authentik:6379. [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: self-signed certificate in certificate chain (_ssl.c:1000).)", "level": "info", "logger": "authentik.lib.config", "timestamp": 1712179998.699394, "redis_url": "rediss://:rC31F9HFEOUth9ob3fdBszKUlTlaGa9BtHmq1yJ1z0yGTgBlFNOIi8ZEkhSWJ5Rl@redis-1f0f-master.authentik:6379/0"}
Stream closed EOF for authentik/authentik-server-595cc7f6f9-psf4l (server)
```



**Describe the solution you'd like**

It would be ideal to expose an additional environment variable to pass a value into `ssl_ca_certs` for the redis client.

**Describe alternatives you've considered**

None

**Additional context**

None



Discussion:

Resolving patch:
diff --git a/authentik/lib/config.py b/authentik/lib/config.py
--- a/authentik/lib/config.py
+++ b/authentik/lib/config.py
@@ -14,7 +14,7 @@
 from sys import argv, stderr
 from time import time
 from typing import Any
-from urllib.parse import urlparse
+from urllib.parse import quote_plus, urlparse
 
 import yaml
 from django.conf import ImproperlyConfigured
@@ -331,6 +331,26 @@ def set(self, path: str, value: Any, sep="."):
 CONFIG = ConfigLoader()
 
 
+def redis_url(db: int) -> str:
+    """Helper to create a Redis URL for a specific database"""
+    _redis_protocol_prefix = "redis://"
+    _redis_tls_requirements = ""
+    if CONFIG.get_bool("redis.tls", False):
+        _redis_protocol_prefix = "rediss://"
+        _redis_tls_requirements = f"?ssl_cert_reqs={CONFIG.get('redis.tls_reqs')}"
+        if _redis_ca := CONFIG.get("redis.tls_ca_cert", None):
+            _redis_tls_requirements += f"&ssl_ca_certs={_redis_ca}"
+    _redis_url = (
+        f"{_redis_protocol_prefix}"
+        f"{quote_plus(CONFIG.get('redis.username'))}:"
+        f"{quote_plus(CONFIG.get('redis.password'))}@"
+        f"{quote_plus(CONFIG.get('redis.host'))}:"
+        f"{CONFIG.get_int('redis.port')}"
+        f"/{db}{_redis_tls_requirements}"
+    )
+    return _redis_url
+
+
 if __name__ == "__main__":
     if len(argv) < 2:  # noqa: PLR2004
         print(dumps(CONFIG.raw, indent=4, cls=AttrEncoder))
diff --git a/authentik/root/settings.py b/authentik/root/settings.py
--- a/authentik/root/settings.py
+++ b/authentik/root/settings.py
@@ -5,13 +5,12 @@
 from collections import OrderedDict
 from hashlib import sha512
 from pathlib import Path
-from urllib.parse import quote_plus
 
 from celery.schedules import crontab
 from sentry_sdk import set_tag
 
 from authentik import ENV_GIT_HASH_KEY, __version__
-from authentik.lib.config import CONFIG
+from authentik.lib.config import CONFIG, redis_url
 from authentik.lib.logging import get_logger_config, structlog_configure
 from authentik.lib.sentry import sentry_init
 from authentik.lib.utils.reflection import get_env
@@ -195,25 +194,15 @@
     },
 }
 
-_redis_protocol_prefix = "redis://"
-_redis_celery_tls_requirements = ""
-if CONFIG.get_bool("redis.tls", False):
-    _redis_protocol_prefix = "rediss://"
-    _redis_celery_tls_requirements = f"?ssl_cert_reqs={CONFIG.get('redis.tls_reqs')}"
-_redis_url = (
-    f"{_redis_protocol_prefix}"
-    f"{quote_plus(CONFIG.get('redis.username'))}:"
-    f"{quote_plus(CONFIG.get('redis.password'))}@"
-    f"{quote_plus(CONFIG.get('redis.host'))}:"
-    f"{CONFIG.get_int('redis.port')}"
-)
 
 CACHES = {
     "default": {
         "BACKEND": "django_redis.cache.RedisCache",
-        "LOCATION": CONFIG.get("cache.url") or f"{_redis_url}/{CONFIG.get('redis.db')}",
+        "LOCATION": CONFIG.get("cache.url") or redis_url(CONFIG.get("redis.db")),
         "TIMEOUT": CONFIG.get_int("cache.timeout", 300),
-        "OPTIONS": {"CLIENT_CLASS": "django_redis.client.DefaultClient"},
+        "OPTIONS": {
+            "CLIENT_CLASS": "django_redis.client.DefaultClient",
+        },
         "KEY_PREFIX": "authentik_cache",
         "KEY_FUNCTION": "django_tenants.cache.make_key",
         "REVERSE_KEY_FUNCTION": "django_tenants.cache.reverse_key",
@@ -276,7 +265,7 @@
     "default": {
         "BACKEND": "channels_redis.pubsub.RedisPubSubChannelLayer",
         "CONFIG": {
-            "hosts": [CONFIG.get("channel.url", f"{_redis_url}/{CONFIG.get('redis.db')}")],
+            "hosts": [CONFIG.get("channel.url") or redis_url(CONFIG.get("redis.db"))],
             "prefix": "authentik_channels_",
         },
     },
@@ -376,11 +365,9 @@
     "beat_scheduler": "authentik.tenants.scheduler:TenantAwarePersistentScheduler",
     "task_create_missing_queues": True,
     "task_default_queue": "authentik",
-    "broker_url": CONFIG.get("broker.url")
-    or f"{_redis_url}/{CONFIG.get('redis.db')}{_redis_celery_tls_requirements}",
+    "broker_url": CONFIG.get("broker.url") or redis_url(CONFIG.get("redis.db")),
+    "result_backend": CONFIG.get("result_backend.url") or redis_url(CONFIG.get("redis.db")),
     "broker_transport_options": CONFIG.get_dict_from_b64_json("broker.transport_options"),
-    "result_backend": CONFIG.get("result_backend.url")
-    or f"{_redis_url}/{CONFIG.get('redis.db')}{_redis_celery_tls_requirements}",
 }
 
 # Sentry integration
diff --git a/lifecycle/wait_for_db.py b/lifecycle/wait_for_db.py
--- a/lifecycle/wait_for_db.py
+++ b/lifecycle/wait_for_db.py
@@ -9,7 +9,7 @@
 from redis import Redis
 from redis.exceptions import RedisError
 
-from authentik.lib.config import CONFIG
+from authentik.lib.config import CONFIG, redis_url
 
 
 def check_postgres():
@@ -35,24 +35,18 @@ def check_postgres():
 
 
 def check_redis():
-    REDIS_PROTOCOL_PREFIX = "redis://"
-    if CONFIG.get_bool("redis.tls", False):
-        REDIS_PROTOCOL_PREFIX = "rediss://"
-    REDIS_URL = (
-        f"{REDIS_PROTOCOL_PREFIX}"
-        f"{quote_plus(CONFIG.get('redis.username'))}:"
-        f"{quote_plus(CONFIG.get('redis.password'))}@"
-        f"{quote_plus(CONFIG.get('redis.host'))}:"
-        f"{CONFIG.get_int('redis.port')}/{CONFIG.get('redis.db')}"
-    )
+    url = redis_url(CONFIG.get("redis.db"))
     while True:
         try:
-            redis = Redis.from_url(REDIS_URL)
+            redis = Redis.from_url(url)
             redis.ping()
             break
         except RedisError as exc:
             sleep(1)
-            CONFIG.log("info", f"Redis Connection failed, retrying... ({exc})", redis_url=REDIS_URL)
+            sanitized_url = url.replace(quote_plus(CONFIG.get("redis.password")), "******")
+            CONFIG.log(
+                "info", f"Redis Connection failed, retrying... ({exc})", redis_url=sanitized_url
+            )
     CONFIG.log("info", "Redis Connection successful")
 
 


Test patch: