split out proxy from auth

This commit is contained in:
Blake Blackshear 2024-06-14 17:22:06 -05:00
parent b49cda274d
commit 2f89f5226e
5 changed files with 67 additions and 33 deletions

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

@ -176,6 +176,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

@ -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;