From 9ceffeb1912dfedd408a082220921f6809f0a2e7 Mon Sep 17 00:00:00 2001 From: Blake Blackshear Date: Fri, 14 Jun 2024 18:02:13 -0500 Subject: [PATCH] split out proxy from auth (#11963) * split out proxy from auth * update documentation * fixup auth mode check --- docs/docs/configuration/authentication.md | 70 ++++++++++----------- docs/docs/configuration/reference.md | 34 +++++----- frigate/api/app.py | 9 +-- frigate/api/auth.py | 24 +++++-- frigate/app.py | 4 +- frigate/config.py | 37 +++++++---- web/src/components/menu/AccountSettings.tsx | 2 +- 7 files changed, 106 insertions(+), 74 deletions(-) diff --git a/docs/docs/configuration/authentication.md b/docs/docs/configuration/authentication.md index 488522852..2728d1421 100644 --- a/docs/docs/configuration/authentication.md +++ b/docs/docs/configuration/authentication.md @@ -5,37 +5,26 @@ title: Authentication # Authentication -## Modes - -Frigate supports two modes for authentication - -| Mode | Description | -| -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `native` | (default) Use this mode if you don't implement authentication with a proxy in front of Frigate. | -| `proxy` | Turns off Frigate's authentication. Use this mode if you have an existing proxy for authentication. Supports passing authenticated user downstream via common headers to Frigate for role-based authorization (future implementation). | - -The following ports are used to access the Frigate webUI - -| Port | Description | -| ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `8080` | Authenticated UI and API. Reverse proxies should use this port. | -| `5000` | Internal unauthenticated UI and API access. Access to this port should be limited. Intended to be used within the docker network for services that integrate with Frigate. | - -### Native mode - Frigate stores user information in its database. Password hashes are generated using industry standard PBKDF2-SHA256 with 600,000 iterations. Upon successful login, a JWT token is issued with an expiration date and set as a cookie. The cookie is refreshed as needed automatically. This JWT token can also be passed in the Authorization header as a bearer token. Users are managed in the UI under Settings > Users. -#### Onboarding +The following ports are available to access the Frigate web UI. + +| Port | Description | +| ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `8080` | Authenticated UI and API. Reverse proxies should use this port. | +| `5000` | Internal unauthenticated UI and API access. Access to this port should be limited. Intended to be used within the docker network for services that integrate with Frigate and do not support authentication. | + +## Onboarding On startup, an admin user and password are generated and printed in the logs. It is recommended to set a new password for the admin account after logging in for the first time under Settings > Users. -#### Resetting admin password +## Resetting admin password In the event that you are locked out of your instance, you can tell Frigate to reset the admin password and print it in the logs on next startup using the `reset_admin_password` setting in your config file. -#### Login failure rate limiting +## Login failure rate limiting In order to limit the risk of brute force attacks, rate limiting is available for login failures. This is implemented with Flask-Limiter, and the string notation for valid values is available in [the documentation](https://flask-limiter.readthedocs.io/en/stable/configuration.html#rate-limit-string-notation). @@ -53,13 +42,12 @@ If you are running a reverse proxy in the same docker compose file as Frigate, h ```yaml auth: - mode: native failed_login_rate_limit: "1/second;5/minute;20/hour" trusted_proxies: - 172.18.0.0/16 # <---- this is the subnet for the internal docker compose network ``` -#### JWT Token Secret +## JWT Token Secret The JWT token secret needs to be kept secure. Anyone with this secret can generate valid JWT tokens to authenticate with Frigate. This should be a cryptographically random string of at least 64 characters. @@ -80,22 +68,34 @@ If no secret is found on startup, Frigate generates one and stores it in a `.jwt Changing the secret will invalidate current tokens. -### Proxy mode +## Proxy configuration -Proxy mode is designed to complement common upstream authentication proxies such as Authelia, Authentik, oauth2_proxy, or traefik-forward-auth. +Frigate can be configured to leverage features of common upstream authentication proxies such as Authelia, Authentik, oauth2_proxy, or traefik-forward-auth. -:::danger +If you are leveraging the authentication of an upstream proxy, you likely want to disable Frigate's authentication. Optionally, if communication between the reverse proxy and Frigate is over an untrusted network, you should set an `auth_secret` in the `proxy` config and configure the proxy to send the secret value as a header named `X-Proxy-Secret`. Assuming this is an untrusted network, you will also want to [configure a real TLS certificate](tls.md) to ensure the traffic can't simply be sniffed to steal the secret. -Note that using proxy mode disables authentication checks in Frigate. This mode will pass headers so Frigate can be aware of the logged in user from the upstream proxy, but it does not validate that the request came from your proxy. If the proxy resides on a different device, you should consider using firewall rules or a VPN between Frigate and the proxy if the network is insecure. - -::: - -#### Header mapping - -If your proxy supports passing a header with the authenticated username, you can use the `header_map` config to specify the header name so it is passed to Frigate. For example, the following will map the `X-Forwarded-User` value. Header names are not case sensitive. +Here is an example of how to disable Frigate's authentication and also ensure the requests come only from your known proxy. ```yaml auth: + enabled: False + +proxy: + auth_secret: +``` + +You can use the following code to generate a random secret. + +```shell +python3 -c 'import secrets; print(secrets.token_hex(64))' +``` + +### Header mapping + +If you have disabled Frigate's authentication and your proxy supports passing a header with the authenticated username, you can use the `header_map` config to specify the header name so it is passed to Frigate. For example, the following will map the `X-Forwarded-User` value. Header names are not case sensitive. + +```yaml +proxy: ... header_map: user: x-forwarded-user @@ -123,10 +123,10 @@ If you would like to add more options, you can overwrite the default file with a Future versions of Frigate may leverage group and role headers for authorization in Frigate as well. -#### Login page redirection +### Login page redirection Frigate gracefully performs login page redirection that should work with most authentication proxies. If your reverse proxy returns a `Location` header on `401`, `302`, or `307` unauthorized responses, Frigate's frontend will automatically detect it and redirect to that URL. -#### Custom logout url +### Custom logout url If your reverse proxy has a dedicated logout url, you can specify using the `logout_url` config option. This will update the link for the `Logout` link in the UI. 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..e97509998 100644 --- a/frigate/api/app.py +++ b/frigate/api/app.py @@ -21,7 +21,7 @@ from frigate.api.export import ExportBp from frigate.api.media import MediaBp from frigate.api.preview import PreviewBp from frigate.api.review import ReviewBp -from frigate.config import AuthModeEnum, FrigateConfig +from frigate.config import FrigateConfig from frigate.const import CONFIG_DIR from frigate.events.external import ExternalEventProcessor from frigate.models import Event, Timeline @@ -86,9 +86,7 @@ def create_app( app.plus_api = plus_api app.camera_error_image = None app.stats_emitter = stats_emitter - app.jwt_token = ( - get_jwt_secret() if frigate_config.auth.mode == AuthModeEnum.native else None - ) + app.jwt_token = get_jwt_secret() if frigate_config.auth.enabled else None # update the request_address with the x-forwarded-for header from nginx app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1) # initialize the rate limiter for the login endpoint @@ -176,6 +174,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/app.py b/frigate/app.py index e2716a8a2..7e845f44a 100644 --- a/frigate/app.py +++ b/frigate/app.py @@ -27,7 +27,7 @@ from frigate.comms.dispatcher import Communicator, Dispatcher from frigate.comms.inter_process import InterProcessCommunicator from frigate.comms.mqtt import MqttClient from frigate.comms.ws import WebSocketClient -from frigate.config import AuthModeEnum, FrigateConfig +from frigate.config import FrigateConfig from frigate.const import ( CACHE_DIR, CLIPS_DIR, @@ -593,7 +593,7 @@ class FrigateApp: ) def init_auth(self) -> None: - if self.config.auth.mode == AuthModeEnum.native: + if self.config.auth.enabled: if User.select().count() == 0: password = secrets.token_hex(16) password_hash = hash_password( 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;