split out proxy from auth (#11963)

* split out proxy from auth

* update documentation

* fixup auth mode check
This commit is contained in:
Blake Blackshear 2024-06-14 18:02:13 -05:00 committed by GitHub
parent b49cda274d
commit 9ceffeb191
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 106 additions and 74 deletions

View File

@ -5,37 +5,26 @@ title: Authentication
# 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. 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. 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. 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. 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). 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 ```yaml
auth: auth:
mode: native
failed_login_rate_limit: "1/second;5/minute;20/hour" failed_login_rate_limit: "1/second;5/minute;20/hour"
trusted_proxies: trusted_proxies:
- 172.18.0.0/16 # <---- this is the subnet for the internal docker compose network - 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. 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. 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. Here is an example of how to disable Frigate's authentication and also ensure the requests come only from your known proxy.
:::
#### 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.
```yaml ```yaml
auth: auth:
enabled: False
proxy:
auth_secret: <some random long string>
```
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: header_map:
user: x-forwarded-user 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. 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. 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. 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.

View File

@ -66,13 +66,28 @@ database:
# Optional: TLS configuration # Optional: TLS configuration
tls: tls:
# Optional: Enable TLS for port 8080 (default: shown below) # 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 # Optional: Authentication configuration
auth: auth:
# Optional: Authentication mode (default: shown below) # Optional: Enable authentication
# Valid values are: native, proxy enabled: True
mode: native
# Optional: Reset the admin user password on startup (default: shown below) # Optional: Reset the admin user password on startup (default: shown below)
# New password is printed in the logs # New password is printed in the logs
reset_admin_password: False reset_admin_password: False
@ -87,23 +102,14 @@ auth:
# When the session is going to expire in less time than this setting, # When the session is going to expire in less time than this setting,
# it will be refreshed back to the session_length. # it will be refreshed back to the session_length.
refresh_time: 43200 # 12 hours 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 # Optional: Rate limiting for login failures to help prevent brute force
# login attacks (default: shown below) # login attacks (default: shown below)
# See the docs for more information on valid values # See the docs for more information on valid values
failed_login_rate_limit: None failed_login_rate_limit: None
# Optional: Trusted proxies for determining IP address to rate limit # Optional: Trusted proxies for determining IP address to rate limit
# NOTE: This is only used for rate limiting login attempts and does not bypass # 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: [] 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 # Optional: Number of hashing iterations for user passwords
# As of Feb 2023, OWASP recommends 600000 iterations for PBKDF2-SHA256 # As of Feb 2023, OWASP recommends 600000 iterations for PBKDF2-SHA256
# NOTE: changing this value will not automatically update password hashes, you # NOTE: changing this value will not automatically update password hashes, you

View File

@ -21,7 +21,7 @@ from frigate.api.export import ExportBp
from frigate.api.media import MediaBp from frigate.api.media import MediaBp
from frigate.api.preview import PreviewBp from frigate.api.preview import PreviewBp
from frigate.api.review import ReviewBp 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.const import CONFIG_DIR
from frigate.events.external import ExternalEventProcessor from frigate.events.external import ExternalEventProcessor
from frigate.models import Event, Timeline from frigate.models import Event, Timeline
@ -86,9 +86,7 @@ def create_app(
app.plus_api = plus_api app.plus_api = plus_api
app.camera_error_image = None app.camera_error_image = None
app.stats_emitter = stats_emitter app.stats_emitter = stats_emitter
app.jwt_token = ( app.jwt_token = get_jwt_secret() if frigate_config.auth.enabled else None
get_jwt_secret() if frigate_config.auth.mode == AuthModeEnum.native else None
)
# update the request_address with the x-forwarded-for header from nginx # update the request_address with the x-forwarded-for header from nginx
app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1) app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1)
# initialize the rate limiter for the login endpoint # initialize the rate limiter for the login endpoint
@ -176,6 +174,9 @@ def config():
# remove the mqtt password # remove the mqtt password
config["mqtt"].pop("password", None) 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(): for camera_name, camera in current_app.frigate_config.cameras.items():
camera_dict = config["cameras"][camera_name] camera_dict = config["cameras"][camera_name]

View File

@ -17,7 +17,7 @@ from flask_limiter import Limiter
from joserfc import jwt from joserfc import jwt
from peewee import DoesNotExist 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.const import CONFIG_DIR, JWT_SECRET_ENV_VAR, PASSWORD_HASH_ALGORITHM
from frigate.models import User 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 # Endpoint for use with nginx auth_request
@AuthBp.route("/auth") @AuthBp.route("/auth")
def auth(): def auth():
auth_config: AuthConfig = current_app.frigate_config.auth
proxy_config: ProxyConfig = current_app.frigate_config.proxy
success_response = make_response({}, 202) success_response = make_response({}, 202)
# dont require auth if the request is on the internal port # 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: if request.headers.get("x-server-port", 0, type=int) == 5000:
return success_response return success_response
# if proxy auth mode fail_response = make_response({}, 401)
if current_app.frigate_config.auth.mode == AuthModeEnum.proxy:
# 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 # pass the user header value from the upstream proxy if a mapping is specified
# or use anonymous if none are 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( upstream_user_header_value = request.headers.get(
current_app.frigate_config.auth.header_map.user, current_app.frigate_config.auth.header_map.user,
type=str, type=str,
@ -188,7 +202,7 @@ def auth():
success_response.headers["remote-user"] = "anonymous" success_response.headers["remote-user"] = "anonymous"
return success_response return success_response
fail_response = make_response({}, 401) # now apply authentication
fail_response.headers["location"] = "/login" fail_response.headers["location"] = "/login"
JWT_COOKIE_NAME = current_app.frigate_config.auth.cookie_name JWT_COOKIE_NAME = current_app.frigate_config.auth.cookie_name

View File

@ -27,7 +27,7 @@ from frigate.comms.dispatcher import Communicator, Dispatcher
from frigate.comms.inter_process import InterProcessCommunicator from frigate.comms.inter_process import InterProcessCommunicator
from frigate.comms.mqtt import MqttClient from frigate.comms.mqtt import MqttClient
from frigate.comms.ws import WebSocketClient from frigate.comms.ws import WebSocketClient
from frigate.config import AuthModeEnum, FrigateConfig from frigate.config import FrigateConfig
from frigate.const import ( from frigate.const import (
CACHE_DIR, CACHE_DIR,
CLIPS_DIR, CLIPS_DIR,
@ -593,7 +593,7 @@ class FrigateApp:
) )
def init_auth(self) -> None: def init_auth(self) -> None:
if self.config.auth.mode == AuthModeEnum.native: if self.config.auth.enabled:
if User.select().count() == 0: if User.select().count() == 0:
password = secrets.token_hex(16) password = secrets.token_hex(16)
password_hash = hash_password( password_hash = hash_password(

View File

@ -119,19 +119,28 @@ class TlsConfig(FrigateBaseModel):
enabled: bool = Field(default=True, title="Enable TLS for port 8080") enabled: bool = Field(default=True, title="Enable TLS for port 8080")
class AuthModeEnum(str, Enum):
native = "native"
proxy = "proxy"
class HeaderMappingConfig(FrigateBaseModel): class HeaderMappingConfig(FrigateBaseModel):
user: str = Field( user: str = Field(
default=None, title="Header name from upstream proxy to identify user." 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): 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( reset_admin_password: bool = Field(
default=False, title="Reset the admin password on startup" 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", title="Refresh the session if it is going to expire in this many seconds",
ge=30, ge=30,
) )
header_map: HeaderMappingConfig = Field(
default_factory=HeaderMappingConfig,
title="Header mapping definitions for proxy auth mode.",
)
failed_login_rate_limit: Optional[str] = Field( failed_login_rate_limit: Optional[str] = Field(
default=None, default=None,
title="Rate limits for failed login attempts.", title="Rate limits for failed login attempts.",
@ -159,9 +164,6 @@ class AuthConfig(FrigateBaseModel):
default=[], default=[],
title="Trusted proxies for determining IP address to rate limit", 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 # As of Feb 2023, OWASP recommends 600000 iterations for PBKDF2-SHA256
hash_iterations: int = Field(default=600000, title="Password hash iterations") hash_iterations: int = Field(default=600000, title="Password hash iterations")
@ -1308,6 +1310,9 @@ class FrigateConfig(FrigateBaseModel):
default_factory=DatabaseConfig, title="Database configuration." default_factory=DatabaseConfig, title="Database configuration."
) )
tls: TlsConfig = Field(default_factory=TlsConfig, title="TLS 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.") auth: AuthConfig = Field(default_factory=AuthConfig, title="Auth configuration.")
environment_vars: Dict[str, str] = Field( environment_vars: Dict[str, str] = Field(
default_factory=dict, title="Frigate environment variables." default_factory=dict, title="Frigate environment variables."
@ -1373,6 +1378,12 @@ class FrigateConfig(FrigateBaseModel):
"""Merge camera config with globals.""" """Merge camera config with globals."""
config = self.model_copy(deep=True) 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 # MQTT user/password substitutions
if config.mqtt.user or config.mqtt.password: if config.mqtt.user or config.mqtt.password:
config.mqtt.user = config.mqtt.user.format(**FRIGATE_ENV_VARS) config.mqtt.user = config.mqtt.user.format(**FRIGATE_ENV_VARS)

View File

@ -26,7 +26,7 @@ type AccountSettingsProps = {
export default function AccountSettings({ className }: AccountSettingsProps) { export default function AccountSettings({ className }: AccountSettingsProps) {
const { data: profile } = useSWR("profile"); const { data: profile } = useSWR("profile");
const { data: config } = useSWR("config"); 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 Container = isDesktop ? DropdownMenu : Drawer;
const Trigger = isDesktop ? DropdownMenuTrigger : DrawerTrigger; const Trigger = isDesktop ? DropdownMenuTrigger : DrawerTrigger;