From 2f89f5226e896834bf1de1e86fe4bac6f3056577 Mon Sep 17 00:00:00 2001 From: Blake Blackshear Date: Fri, 14 Jun 2024 17:22:06 -0500 Subject: [PATCH] split out proxy from auth --- docs/docs/configuration/reference.md | 34 +++++++++++-------- frigate/api/app.py | 3 ++ frigate/api/auth.py | 24 ++++++++++--- frigate/config.py | 37 +++++++++++++-------- web/src/components/menu/AccountSettings.tsx | 2 +- 5 files changed, 67 insertions(+), 33 deletions(-) diff --git a/docs/docs/configuration/reference.md b/docs/docs/configuration/reference.md index 9920e4642..90bdce8a9 100644 --- a/docs/docs/configuration/reference.md +++ b/docs/docs/configuration/reference.md @@ -66,13 +66,28 @@ database: # Optional: TLS configuration tls: # Optional: Enable TLS for port 8080 (default: shown below) - enabled: true + enabled: True + +# Optional: Proxy configuration +proxy: + # Optional: Mapping for headers from upstream proxies. Only used if Frigate's auth + # is disabled. + # NOTE: Many authentication proxies pass a header downstream with the authenticated + # user name. Not all values are supported. It must be a whitelisted header. + # See the docs for more info. + header_map: + user: x-forwarded-user + # Optional: Url for logging out a user. This sets the location of the logout url in + # the UI. + logout_url: /api/logout + # Optional: Auth secret that is checked against the X-Proxy-Secret header sent from + # the proxy. If not set, all requests are trusted regardless of origin. + auth_secret: None # Optional: Authentication configuration auth: - # Optional: Authentication mode (default: shown below) - # Valid values are: native, proxy - mode: native + # Optional: Enable authentication + enabled: True # Optional: Reset the admin user password on startup (default: shown below) # New password is printed in the logs reset_admin_password: False @@ -87,23 +102,14 @@ auth: # When the session is going to expire in less time than this setting, # it will be refreshed back to the session_length. refresh_time: 43200 # 12 hours - # Optional: Mapping for headers from upstream proxies. Only used in proxy auth mode. - # NOTE: Many authentication proxies pass a header downstream with the authenticated - # user name. Not all values are supported. It must be a whitelisted header. - # See the docs for more info. - header_map: - user: x-forwarded-user # Optional: Rate limiting for login failures to help prevent brute force # login attacks (default: shown below) # See the docs for more information on valid values failed_login_rate_limit: None # Optional: Trusted proxies for determining IP address to rate limit # NOTE: This is only used for rate limiting login attempts and does not bypass - # authentication in any way + # authentication. See the authentication docs for more details. trusted_proxies: [] - # Optional: Url for logging out a user. This only needs to be set if you are using - # proxy mode. - logout_url: /api/logout # Optional: Number of hashing iterations for user passwords # As of Feb 2023, OWASP recommends 600000 iterations for PBKDF2-SHA256 # NOTE: changing this value will not automatically update password hashes, you diff --git a/frigate/api/app.py b/frigate/api/app.py index a11705fcf..8002ecb7e 100644 --- a/frigate/api/app.py +++ b/frigate/api/app.py @@ -176,6 +176,9 @@ def config(): # remove the mqtt password config["mqtt"].pop("password", None) + # remove the proxy secret + config["proxy"].pop("auth_secret", None) + for camera_name, camera in current_app.frigate_config.cameras.items(): camera_dict = config["cameras"][camera_name] diff --git a/frigate/api/auth.py b/frigate/api/auth.py index 96bd9e2b2..fb2ad3a34 100644 --- a/frigate/api/auth.py +++ b/frigate/api/auth.py @@ -17,7 +17,7 @@ from flask_limiter import Limiter from joserfc import jwt from peewee import DoesNotExist -from frigate.config import AuthConfig, AuthModeEnum +from frigate.config import AuthConfig, ProxyConfig from frigate.const import CONFIG_DIR, JWT_SECRET_ENV_VAR, PASSWORD_HASH_ALGORITHM from frigate.models import User @@ -166,6 +166,9 @@ def set_jwt_cookie(response, cookie_name, encoded_jwt, expiration, secure): # Endpoint for use with nginx auth_request @AuthBp.route("/auth") def auth(): + auth_config: AuthConfig = current_app.frigate_config.auth + proxy_config: ProxyConfig = current_app.frigate_config.proxy + success_response = make_response({}, 202) # dont require auth if the request is on the internal port @@ -173,11 +176,22 @@ def auth(): if request.headers.get("x-server-port", 0, type=int) == 5000: return success_response - # if proxy auth mode - if current_app.frigate_config.auth.mode == AuthModeEnum.proxy: + fail_response = make_response({}, 401) + + # ensure the proxy secret matches if configured + if ( + proxy_config.auth_secret is not None + and request.headers.get("x-proxy-secret", "", type=str) + != proxy_config.auth_secret + ): + logger.debug("X-Proxy-Secret header does not match configured secret value") + return fail_response + + # if auth is disabled, just apply the proxy header map and return success + if not auth_config.enabled: # pass the user header value from the upstream proxy if a mapping is specified # or use anonymous if none are specified - if current_app.frigate_config.auth.header_map.user is not None: + if proxy_config.header_map.user is not None: upstream_user_header_value = request.headers.get( current_app.frigate_config.auth.header_map.user, type=str, @@ -188,7 +202,7 @@ def auth(): success_response.headers["remote-user"] = "anonymous" return success_response - fail_response = make_response({}, 401) + # now apply authentication fail_response.headers["location"] = "/login" JWT_COOKIE_NAME = current_app.frigate_config.auth.cookie_name diff --git a/frigate/config.py b/frigate/config.py index 2f9f73a14..59ce58ea3 100644 --- a/frigate/config.py +++ b/frigate/config.py @@ -119,19 +119,28 @@ class TlsConfig(FrigateBaseModel): enabled: bool = Field(default=True, title="Enable TLS for port 8080") -class AuthModeEnum(str, Enum): - native = "native" - proxy = "proxy" - - class HeaderMappingConfig(FrigateBaseModel): user: str = Field( default=None, title="Header name from upstream proxy to identify user." ) +class ProxyConfig(FrigateBaseModel): + header_map: HeaderMappingConfig = Field( + default_factory=HeaderMappingConfig, + title="Header mapping definitions for proxy user passing.", + ) + logout_url: Optional[str] = Field( + default=None, title="Redirect url for logging out with proxy." + ) + auth_secret: Optional[str] = Field( + default=None, + title="Secret value for proxy authentication.", + ) + + class AuthConfig(FrigateBaseModel): - mode: AuthModeEnum = Field(default=AuthModeEnum.native, title="Authentication mode") + enabled: bool = Field(default=True, title="Enable authentication") reset_admin_password: bool = Field( default=False, title="Reset the admin password on startup" ) @@ -147,10 +156,6 @@ class AuthConfig(FrigateBaseModel): title="Refresh the session if it is going to expire in this many seconds", ge=30, ) - header_map: HeaderMappingConfig = Field( - default_factory=HeaderMappingConfig, - title="Header mapping definitions for proxy auth mode.", - ) failed_login_rate_limit: Optional[str] = Field( default=None, title="Rate limits for failed login attempts.", @@ -159,9 +164,6 @@ class AuthConfig(FrigateBaseModel): default=[], title="Trusted proxies for determining IP address to rate limit", ) - logout_url: Optional[str] = Field( - default=None, title="Redirect url for logging out in proxy mode." - ) # As of Feb 2023, OWASP recommends 600000 iterations for PBKDF2-SHA256 hash_iterations: int = Field(default=600000, title="Password hash iterations") @@ -1308,6 +1310,9 @@ class FrigateConfig(FrigateBaseModel): default_factory=DatabaseConfig, title="Database configuration." ) tls: TlsConfig = Field(default_factory=TlsConfig, title="TLS configuration.") + proxy: ProxyConfig = Field( + default_factory=ProxyConfig, title="Proxy configuration." + ) auth: AuthConfig = Field(default_factory=AuthConfig, title="Auth configuration.") environment_vars: Dict[str, str] = Field( default_factory=dict, title="Frigate environment variables." @@ -1373,6 +1378,12 @@ class FrigateConfig(FrigateBaseModel): """Merge camera config with globals.""" config = self.model_copy(deep=True) + # Proxy secret substitution + if config.proxy.auth_secret: + config.proxy.auth_secret = config.proxy.auth_secret.format( + **FRIGATE_ENV_VARS + ) + # MQTT user/password substitutions if config.mqtt.user or config.mqtt.password: config.mqtt.user = config.mqtt.user.format(**FRIGATE_ENV_VARS) diff --git a/web/src/components/menu/AccountSettings.tsx b/web/src/components/menu/AccountSettings.tsx index 91cca5f43..41bbfa06e 100644 --- a/web/src/components/menu/AccountSettings.tsx +++ b/web/src/components/menu/AccountSettings.tsx @@ -26,7 +26,7 @@ type AccountSettingsProps = { export default function AccountSettings({ className }: AccountSettingsProps) { const { data: profile } = useSWR("profile"); const { data: config } = useSWR("config"); - const logoutUrl = config?.auth.logout_url || "/api/logout"; + const logoutUrl = config?.proxy.logout_url || "/api/logout"; const Container = isDesktop ? DropdownMenu : Drawer; const Trigger = isDesktop ? DropdownMenuTrigger : DrawerTrigger;