SSO for Frigate with Keycloak

2026-06-06 tinkering security sso frigate keycloak oauth2

A growing fleet of services makes it more messy to have separate credentials for each of them. I’ve been tinkering with single sign on (SSO) with the goal to have one account per person for all the services in my home network. Not just HTTP, but other protocols as well, but I haven’t gotten to the latter part yet.

Fortunately, many popular pieces of software come with SSO support out of the box, typically OpenID Connect (OIDC) via OAuth2 rather than the more enterprise-y SAML. Grafana has built-in OIDC support, there are multiple OIDC addons for Home Assistant, and Miniflux, Open WebUI, and Paperless-NGX just work.

Synology DSM was a huge pain, because it provides no feedback as to why the current configuration isn’t working - just “talk to your administrator”. My brother in christ - I am the administrator.

Frigate proved to be challenging in a slightly different way.

As a bonus, I’m running my own private key infrastructure, so all the parts talking to each other need cooperate with certificates signed by my own certificate authority.

What’s so different about Frigate?🔗

Frigate doesn’t integrate with SSO protocols directly, but requires the use of a proxy to enforce authentication as all requests pass through it.

When first accessing the site, the user is prompted with the proxy’s authentication prompt that starts an OAuth2 login with the configured OIDC provider. After successfully authenticating at the OIDC provider’s site (or instantly if a session is already ongoing), the user is redirected back to the proxy, which now serves Frigate transparently.

Frigate knows the name of the current user and their role through one header each that is forwarded from the proxy to Frigate, which supports some variations of header names out of the box (e.g., X-Forwarded-User, X-Forwarded-Groups). The headers only have effect when an appropriate X-Proxy-Secret header is included as well, which contains a generated secret value shared by the proxy and Frigate.

This is the biggest security foot gun in the whole process: Frigate **trusts** these headers - a client that sends `X-Forwarded-Group: admin` and the correct `X-Proxy-Secret` can do everything an admin can. Make sure to use a fresh, well protected secret and don't bind Frigate to a public IP address. You're only a minor configuration error away from providing global public access otherwise.

The headers supported by Frigate out of the box are focused on Authelia and Authentik, which provide their own OAuth2 proxies. Keycloak doesn’t, so I’ll need another component.

Why use Keycloak anyway, if it lacks some functionality?🔗

The pragmatic reason is that I had already integrated several services with Keycloak by the time I started thinking about Frigate. The diligent reason is that I might be able to transfer experience to work.

It’s less flashy than other, but robust and rich in features (giving it a slightly enterprise-y reputation), but it works fine.

Keycloak configuration🔗

For Frigate, I set up a typical client with ID frigate in the realm of my choice via the UI with these settings:

  • Root URL, Home URL, Web origins, Admin URL: https://frigate.internal/
  • Redirect URL: https://frigate.internal/oauth2/callback
    • The path is provided by the proxy.
  • Client authentication: On
  • Standard flow: On
  • PKCE method: S256
  • In the roles tab, I created admin and viewer, matching Frigate’s supported roles.
  • In the client scopes tab, within the existing frigate-dedicated scope, I added a user client role mapper:
    • Client ID must match the client’s ID (frigate in my case).
    • Token claim name: frigate_role - this needs to match proxy configuration later.
    • Multi-valued: Off - Frigate only supports a single role.
    • Add ID to token, Add to access token, Add to userinfo, Add to token inrospection: On.

To grant a user access to Frigate, navigate to the user’s role mapping and assign a new client role. You’ll see the roles associated with client Frigate - pick the appropriate, assign, and be done.

Authentication proxy🔗

People’s go-to solution seems to be OAuth2 Proxy, which is rich in functionality and checks all the boxes.

It’s fairly easy to get up as a container:

services:
  # ...
  oauth2-proxy:
    image: quay.io/oauth2-proxy/oauth2-proxy:latest
    container_name: frigate-oauth2-proxy
    restart: unless-stopped
    command:
      - --redirect-url=https://frigate.internal/oauth2/callback
      # Generate the cookie secret with python -c 'import os,base64; print(base64.urlsafe_b64encode(os.urandom(32)).decode())'
      - --cookie-secret=[REDACTED]
      # Some stuff we need can't be configured via CLI - use new config format.
      - --alpha-config=/etc/oauth2-proxy/alpha-config.yaml
      - --cookie-secure=true
      - --session-cookie-minimal=true
      - --email-domain=*
    volumes:
      # Necessary for trusting the OIDC provider's certificate.
      - /etc/ssl/certs/ca-certificates.crt:/etc/ssl/certs/ca-certificates.crt:ro
      - ./alpha-config.yaml:/etc/oauth2-proxy/alpha-config.yaml:ro
    dns:
      # Necessary for resolving the OIDC provider's domain - my home net DNS server.
      - 10.42.10.10
    environment:
      OAUTH2_PROXY_SET_AUTHORIZATION_HEADER: "false"
    ports:
      # This is what my HTTPS-terminating reverse proxy will talk to for serving https://frigate.internal.
      - "4180:4180"

The proxy itself is configured (via alpha-config.yaml mounted in the container) with the OIDC provider, Frigate’s internal address, used as reverse proxy upstream, and information for wrangling data form the Oauth2 claim into headers containing role and user information, as well as the proxy secret:

server:
  # Nedds to match the forwarded Docker port.
  BindAddress: "0.0.0.0:4180"

providers:
  - id: keycloak
    provider: oidc
    name: ChriSSO
    # Match below with the settings in Keycloak:
    clientID: frigate
    clientSecret: "[REDACTED]"
    oidcConfig:
      issuerURL: https://id.internal/realms/master
    profileURL: https://id.internal/realms/master/protocol/openid-connect/userinfo
    caFiles:
      - /etc/ssl/certs/ca-certificates.crt
    scope: "openid profile email"
    code_challenge_method: S256
    # Pull the custom claim out of the ID token into the session
    additionalClaims:
      # This needs to match the client role mapper we set up in Keycloak.
      - frigate_role

injectRequestHeaders:
  - name: X-Forwarded-Groups
    values:
      - claimSource:
          # This needs to match the client role mapper we set up in Keycloak.
          claim: frigate_role
  - name: X-Forwarded-User
    values:
      - claimSource:
          claim: preferred_username
  - name: X-Proxy-Secret
    values:
      - secretSource:
          # Must be base64-encoded here; oauth2-proxy sends it decoded
          # Frigate must use the same value.
          value: "[REDACTED]"

upstreamConfig:
  upstreams:
    - id: frigate
      path: /
      uri: http://frigate:8971

Frigate is a container in the same Docker network as the proxy, so the proxy can talk to it by name, and the Frigate HTTP ports are not published to prevent access that doesn’t go through the proxy.

If Frigate was running on a different host, plain text traffic would have to go through the network, so I keep the proxy as a side car to the container it secures, keeping traffic local.

Frigate configuration🔗

This part is the easiest - just disable built-in authentication, set the proxy secret, and map the headers:

auth:
  enabled: false
proxy:
  auth_secret: "[REDACTED]" # This is the same as the X-Proxy-Secret defined in the proxy.
  header_map:
    user: x-frigate-user
    role: x-frigate-role

Pitfalls🔗

This looks all pretty straightforward in hindsight, but the proxy’s alpha configuration format isn’t perfectly documented yet (owed to its alpha status), so finding out what goes into the CLI and what goes into the YAML required trial and error.

Working with custom certificate authorities also isn’t exactly the beaten path. Some experimentation was required to find the right paths and settings.

I wasted the most time trying to get role mapping right: At first, all my sessions were with the viewer role, whereas I expected to be an admin. At the time, I was trying to map Keycloak roles to the client session, so if a user is a Keycloak admin, they are also a Frigate admin.

I’m sure that is somehow possible, but I failed to find that way. As a workaround, I described above how to define client roles and assign them to users in Frigate - these show up in the OAuth claim as expected.

Conclusion🔗

This was the last major HTTP service on my network that was missing SSO. I’m really happy with finally having gotten it to work, and gained a tool that will in the future allow me to protect other resources without deep SSO support as well. Even if they don’t support proxy authentication, it can still serve as a “you only get in with an account” type of firewall.

A significant tangible benefit is that this makes onboarding my wife to new services much easier.

For my next trick, I’d like to use SSO for SMB as well. I already had an LLDAP setup working fine with Keycloak and started integrating with Synology, but it turns out that LLDAP lacks support for something that Synology requires for secure authentication with SMB versions higher than 1. So, it was necessary to switch to the more feature-rich OpenLDAP, but features beget complexity, so that’s still work in progress.

That also shows again that it’s high time to ditch Synology in favor of something freedom-loving, but the hardware still works fine, and I have yet to find a way to install a custom OS.