Skip to content

Commit

Permalink
split out proxy from auth (#11963)
Browse files Browse the repository at this point in the history
* split out proxy from auth

* update documentation

* fixup auth mode check
  • Loading branch information
blakeblackshear committed Jun 14, 2024
1 parent b49cda2 commit 9ceffeb
Show file tree
Hide file tree
Showing 7 changed files with 104 additions and 72 deletions.
66 changes: 33 additions & 33 deletions docs/docs/configuration/authentication.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).

Expand All @@ -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.

Expand All @@ -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

Frigate can be configured to leverage features of common upstream authentication proxies such as Authelia, Authentik, oauth2_proxy, or traefik-forward-auth.

Proxy mode is designed to complement common upstream authentication proxies such as Authelia, Authentik, oauth2_proxy, or traefik-forward-auth.
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.

:::danger
Here is an example of how to disable Frigate's authentication and also ensure the requests come only from your known proxy.

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.
```yaml
auth:
enabled: False

:::
proxy:
auth_secret: <some random long string>
```

#### Header mapping
You can use the following code to generate a random secret.

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.
```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
auth:
proxy:
...
header_map:
user: x-forwarded-user
Expand Down Expand Up @@ -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.
34 changes: 20 additions & 14 deletions docs/docs/configuration/reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
9 changes: 5 additions & 4 deletions frigate/api/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
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
Expand Down Expand Up @@ -86,9 +86,7 @@ def _db_close(exc):
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
Expand Down Expand Up @@ -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]

Expand Down
24 changes: 19 additions & 5 deletions frigate/api/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
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

Expand Down Expand Up @@ -166,18 +166,32 @@ 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
# this header is set by Frigate's nginx proxy, so it cant be spoofed
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,
Expand All @@ -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
Expand Down
4 changes: 2 additions & 2 deletions frigate/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
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,
Expand Down Expand Up @@ -593,7 +593,7 @@ def check_shm(self) -> None:
)

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(
Expand Down
37 changes: 24 additions & 13 deletions frigate/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand All @@ -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.",
Expand All @@ -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")

Expand Down Expand Up @@ -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."
Expand Down Expand Up @@ -1373,6 +1378,12 @@ def runtime_config(self, plus_api: PlusApi = None) -> FrigateConfig:
"""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)
Expand Down
2 changes: 1 addition & 1 deletion web/src/components/menu/AccountSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down

0 comments on commit 9ceffeb

Please sign in to comment.