From a74233148472b0a14983a72f3340bd08d6821b87 Mon Sep 17 00:00:00 2001 From: Jens L Date: Thu, 18 Apr 2024 16:49:41 +0200 Subject: [PATCH] root: make redis settings more consistent (#9335) * make redis settings more consistent Signed-off-by: Jens Langhammer * add support to go Signed-off-by: Jens Langhammer * rewrite url Signed-off-by: Jens Langhammer * fix redis connect in wait_for_db Signed-off-by: Jens Langhammer * censor password when logging error Signed-off-by: Jens Langhammer * update docs Signed-off-by: Jens Langhammer * reword docs Signed-off-by: Jens Langhammer * add redis url generation helper Signed-off-by: Jens Langhammer --------- Signed-off-by: Jens Langhammer --- authentik/lib/config.py | 22 +++++++++- authentik/lib/default.yml | 1 + authentik/root/settings.py | 29 ++++---------- internal/config/struct.go | 15 +++---- .../outpost/proxyv2/application/session.go | 40 +++++++++++++++++-- lifecycle/wait_for_db.py | 20 ++++------ website/docs/installation/configuration.mdx | 5 ++- 7 files changed, 84 insertions(+), 48 deletions(-) diff --git a/authentik/lib/config.py b/authentik/lib/config.py index 3336be0eac..b35d0d6eef 100644 --- a/authentik/lib/config.py +++ b/authentik/lib/config.py @@ -14,7 +14,7 @@ from pathlib import Path 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 @@ class ConfigLoader: 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/lib/default.yml b/authentik/lib/default.yml index f4a05d8e15..149c0c0cc7 100644 --- a/authentik/lib/default.yml +++ b/authentik/lib/default.yml @@ -35,6 +35,7 @@ redis: password: "" tls: false tls_reqs: "none" + tls_ca_cert: null # broker: # url: "" diff --git a/authentik/root/settings.py b/authentik/root/settings.py index 6081acf9f9..7d3bf211ec 100644 --- a/authentik/root/settings.py +++ b/authentik/root/settings.py @@ -5,13 +5,12 @@ import os 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 @@ REST_FRAMEWORK = { }, } -_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 @@ CHANNEL_LAYERS = { "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 @@ CELERY = { "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/internal/config/struct.go b/internal/config/struct.go index 80f6fb69ea..17dd3ba246 100644 --- a/internal/config/struct.go +++ b/internal/config/struct.go @@ -25,13 +25,14 @@ type Config struct { } type RedisConfig struct { - Host string `yaml:"host" env:"HOST, overwrite"` - Port int `yaml:"port" env:"PORT, overwrite"` - DB int `yaml:"db" env:"DB, overwrite"` - Username string `yaml:"username" env:"USERNAME, overwrite"` - Password string `yaml:"password" env:"PASSWORD, overwrite"` - TLS bool `yaml:"tls" env:"TLS, overwrite"` - TLSReqs string `yaml:"tls_reqs" env:"TLS_REQS, overwrite"` + Host string `yaml:"host" env:"HOST, overwrite"` + Port int `yaml:"port" env:"PORT, overwrite"` + DB int `yaml:"db" env:"DB, overwrite"` + Username string `yaml:"username" env:"USERNAME, overwrite"` + Password string `yaml:"password" env:"PASSWORD, overwrite"` + TLS bool `yaml:"tls" env:"TLS, overwrite"` + TLSReqs string `yaml:"tls_reqs" env:"TLS_REQS, overwrite"` + TLSCaCert *string `yaml:"tls_ca_certs" env:"TLS_CA_CERT, overwrite"` } type ListenConfig struct { diff --git a/internal/outpost/proxyv2/application/session.go b/internal/outpost/proxyv2/application/session.go index a889eefc2f..a332a7d505 100644 --- a/internal/outpost/proxyv2/application/session.go +++ b/internal/outpost/proxyv2/application/session.go @@ -2,6 +2,8 @@ package application import ( "context" + "crypto/tls" + "crypto/x509" "fmt" "math" "net/http" @@ -19,6 +21,7 @@ import ( "goauthentik.io/internal/outpost/proxyv2/codecs" "goauthentik.io/internal/outpost/proxyv2/constants" "goauthentik.io/internal/outpost/proxyv2/redisstore" + "goauthentik.io/internal/utils" ) const RedisKeyPrefix = "authentik_proxy_session_" @@ -31,11 +34,40 @@ func (a *Application) getStore(p api.ProxyOutpostConfig, externalHost *url.URL) maxAge = int(*t) + 1 } if a.isEmbedded { + var tls *tls.Config + if config.Get().Redis.TLS { + tls = utils.GetTLSConfig() + switch strings.ToLower(config.Get().Redis.TLSReqs) { + case "none": + case "false": + tls.InsecureSkipVerify = true + case "required": + break + } + ca := config.Get().Redis.TLSCaCert + if ca != nil { + // Get the SystemCertPool, continue with an empty pool on error + rootCAs, _ := x509.SystemCertPool() + if rootCAs == nil { + rootCAs = x509.NewCertPool() + } + certs, err := os.ReadFile(*ca) + if err != nil { + a.log.WithError(err).Fatalf("Failed to append %s to RootCAs", *ca) + } + // Append our cert to the system pool + if ok := rootCAs.AppendCertsFromPEM(certs); !ok { + a.log.Println("No certs appended, using system certs only") + } + tls.RootCAs = rootCAs + } + } client := redis.NewClient(&redis.Options{ - Addr: fmt.Sprintf("%s:%d", config.Get().Redis.Host, config.Get().Redis.Port), - Username: config.Get().Redis.Username, - Password: config.Get().Redis.Password, - DB: config.Get().Redis.DB, + Addr: fmt.Sprintf("%s:%d", config.Get().Redis.Host, config.Get().Redis.Port), + Username: config.Get().Redis.Username, + Password: config.Get().Redis.Password, + DB: config.Get().Redis.DB, + TLSConfig: tls, }) // New default RedisStore diff --git a/lifecycle/wait_for_db.py b/lifecycle/wait_for_db.py index 450d8af076..82858ce968 100755 --- a/lifecycle/wait_for_db.py +++ b/lifecycle/wait_for_db.py @@ -9,7 +9,7 @@ from psycopg import OperationalError, connect 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") diff --git a/website/docs/installation/configuration.mdx b/website/docs/installation/configuration.mdx index 7d2260be4a..d11669aa56 100644 --- a/website/docs/installation/configuration.mdx +++ b/website/docs/installation/configuration.mdx @@ -72,7 +72,7 @@ To check if your config has been applied correctly, you can run the following co - `AUTHENTIK_POSTGRESQL__PASSWORD`: Database password, defaults to the environment variable `POSTGRES_PASSWORD` - `AUTHENTIK_POSTGRESQL__USE_PGBOUNCER`: Adjust configuration to support connection to PgBouncer - `AUTHENTIK_POSTGRESQL__USE_PGPOOL`: Adjust configuration to support connection to Pgpool -- `AUTHENTIK_POSTGRESQL__SSLMODE`: Strictness of ssl verification. Defaults to `verify-ca` +- `AUTHENTIK_POSTGRESQL__SSLMODE`: Strictness of ssl verification. Defaults to `"verify-ca"` - `AUTHENTIK_POSTGRESQL__SSLROOTCERT`: CA root for server ssl verification - `AUTHENTIK_POSTGRESQL__SSLCERT`: Path to x509 client certificate to authenticate to server - `AUTHENTIK_POSTGRESQL__SSLKEY`: Path to private key of `SSLCERT` certificate @@ -85,7 +85,8 @@ To check if your config has been applied correctly, you can run the following co - `AUTHENTIK_REDIS__USERNAME`: Redis server username when not using configuration URL - `AUTHENTIK_REDIS__PASSWORD`: Redis server password when not using configuration URL - `AUTHENTIK_REDIS__TLS`: Redis server connection using TLS when not using configuration URL -- `AUTHENTIK_REDIS__TLS_REQS`: Redis server TLS connection requirements when not using configuration URL +- `AUTHENTIK_REDIS__TLS_REQS`: Redis server TLS connection requirements when not using configuration URL. Defaults to `"none"`. Allowed values are `"none"` and `"required"`. +- `AUTHENTIK_REDIS__TLS_CA_CERT`: Path to the Redis server TLS CA root when not using configuration URL. Defaults to `null`. ## Result Backend Settings