Skip to main content
Version: Next

Authentication, authorization & backend identity

QueryFlux separates three questions, each configured independently:

QuestionConfig keyAnswers
Who is this client?authnone, static users, OIDC, LDAP
What are they allowed to do?authorizationallow-all, per-group allow-lists, OpenFGA
Which identity reaches the backend engine?clusters[].queryAuththe service account, the client's own credential, an impersonated user, or an exchanged token

The first two gate access to QueryFlux itself. The third — backend identity — decides what Trino, ClickHouse, StarRocks, or Snowflake see as the query's principal, which matters for the backend's own audit log, row-level security, and access control.


Frontend authentication (auth)

auth:
provider: oidc # none | static | oidc | ldap
required: true
oidc:
issuer: https://keycloak.internal/realms/my-realm
jwksUri: https://keycloak.internal/realms/my-realm/protocol/openid-connect/certs
audience: queryflux
groupsClaim: groups
rolesClaim: roles
ProviderUse when
noneLocal development, or the network path to QueryFlux is already trusted (e.g. a private VPC with its own perimeter)
staticA small, fixed set of service users — bcrypt-hashed passwords in config
oidcYou already run an IdP (Keycloak, Okta, Auth0, Entra ID) and want SSO
ldapActive Directory / OpenLDAP-backed organizations

auth.required: true rejects unauthenticated requests on every enabled frontend. AuthContext (the verified identity — user, groups, roles, and the raw OIDC token when applicable) is what the rest of this page builds on.


Authorization (authorization)

authorization:
provider: none # none | openfga

clusterGroups:
analytics:
authorization:
allowGroups: [data-team, analysts]
allowUsers: [oncall-bot]

provider: none with no allowGroups/allowUsers on any group is allow-all — anyone who authenticates to QueryFlux can route to anything. Add per-group allow-lists to restrict that without standing up a full policy engine, or set provider: openfga for Zanzibar-style fine-grained authorization.

warning

authorization gates routing — which cluster group a client can reach. It does not decide which identity a query runs under on the backend; that's queryAuth, below. A client authorized to route to a passthrough cluster still only sees what its own backend credential is allowed to see.


Backend identity (queryAuth)

This is the piece that decides what the engine itself sees as the query's user — independent of how the client authenticated to QueryFlux.

clusters:
trino-1:
engine: trino
endpoint: https://trino.internal:8443
auth: # Type 1 — QueryFlux's own service credential
type: basic
username: qf_svc
password: "..."
queryAuth: # Type 2 — which identity the query runs as
type: impersonate
ModeThe backend seesEngines
serviceAccount (default)QueryFlux's own service credential only — the user is known to QueryFlux for audit/routing, not proven to the backendAll
passthroughThe client's own credential, forwarded unchangedTrino, StarRocks (MySQL wire, LDAP-backed)
impersonateThe service account authenticates; the real user is injected via an engine-specific mechanism (X-Trino-User, ClickHouse EXECUTE AS)Trino, ClickHouse
tokenExchangeA backend-scoped OAuth token, exchanged (RFC 8693) from the client's own tokenTrino, Snowflake (ADBC)

Which mode should I use?

  • Same IdP on both sides, engine trusts bearer tokens directly (Trino with JWT auth against the same Keycloak realm) → passthrough. Simplest — the engine validates the token itself, QueryFlux just forwards it.
  • Engine needs a fixed service principal but you still want per-user attribution in its own audit log (Trino/ClickHouse ACLs, system.query_log) → impersonate.
  • Engine speaks OAuth but not your IdP's token format directly (Snowflake external OAuth, a Trino cluster in a different trust domain) → tokenExchange.
  • Engine has no per-user wire mechanism, or you haven't set any of this up yetserviceAccount. This is the default and always works; it's the starting point, not a fallback to be embarrassed about.

Every mode other than serviceAccount fails closed: if the client has no forwardable credential, the exchange fails, or the caller is unauthenticated, the query is rejected — QueryFlux never silently downgrades to the service account and submits under the wrong principal.

Engine-specific requirements

Backend identity needs a small amount of setup on the engine side — QueryFlux can't configure your engine's own access control for you:

  • Trino impersonate — requires file-based access control granting the service account impersonation rights (http-server.access-control.config-files). Trino denies impersonation by default.
  • ClickHouse impersonate — requires ClickHouse 25.11+, self-hosted (not supported on ClickHouse Cloud), access_control_improvements.allow_impersonate_user = 1, and GRANT IMPERSONATE ON {user} TO {service_account}. Cancelling an impersonated query additionally needs GRANT KILL QUERY ON *.* on the service account.
  • StarRocks passthrough — authenticates each query on a dedicated connection as the target user via authentication_ldap_simple. Requires the cluster endpoint to use TLS (?require_ssl=true) and the passthrough user to hold OPERATE for cancel_query to work.
  • Snowflake tokenExchange — the exchanged token is sent as adbc.snowflake.sql.client_option.auth_token with auth_type=auth_oauth; register QueryFlux as an OAuth client in the same IdP as the target Snowflake OAuth integration.

Full per-engine wiring, the resolver's fail-closed contract, and the queryAuth × engine compatibility matrix: Auth & authorization design.


Try it

examples/with-keycloak-oidc is a runnable Docker Compose stack: Keycloak as the IdP, Trino as the backend, and three ready-to-swap configs — config.yaml (passthrough), config-impersonate.yaml, config-token-exchange.yaml.

cd examples/with-keycloak-oidc
docker compose up -d --wait

TOKEN=$(curl -s http://localhost:8180/realms/queryflux/protocol/openid-connect/token \
-d grant_type=password -d client_id=queryflux \
-d username=alice -d password=alice | jq -r .access_token)

curl -X POST http://localhost:8080/v1/statement \
-H "Authorization: Bearer $TOKEN" -d "SELECT current_user"

See the example's own README for the full walkthrough, including how to swap between the three queryAuth modes.