Skip to main content
Version: Next

Authentication, authorization & backend identity

QueryFlux separates four 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
Which tables/columns may they see?accessControlallow/deny, row filters, and column masks via OPA (overview); connection + per–cluster-group scope in Studio Access Control, policy in Rego

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. The fourth — data access control — rewrites or denies SQL at the gateway based on the verified client identity; see 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), Snowflake (ADBC, per-caller connection)
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 yet → serviceAccount. 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.
  • Snowflake passthrough — the caller's own Snowflake username/password (captured at HTTP wire v1 login) opens a dedicated per-caller connection; QueryFlux does not verify the password itself, the connection attempt is the verification. Not available over SQL API v2 (stateless, Bearer-only) — a passthrough cluster reached that way fails closed.

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

ADBC scoped connection pools​

ADBC isolates tokenExchange and Snowflake passthrough credentials in separate connection sub-pools. Snowflake session overrides (USE ROLE, USE WAREHOUSE, USE SCHEMA) also create separate scopes when they differ from the cluster config. These pools share a per-cluster cache with the following optional ADBC cluster config fields (admin API/UI, alongside driver, uri, and poolSize):

FieldDefaultMeaning
scopedPoolIdleTimeoutSecs900Seconds since the last pool lookup before a cached scope expires.
scopedPoolMaxCount500Maximum cached scopes per cluster per process; evicts the least recently used scope at capacity.

Both values must be positive integers. The limits cover identity and session-override scopes together; the base service-account pool is separate. Identity scopes have at most two connections each; scopes containing only session overrides use poolSize.

A background task sweeps expired scopes every min(scopedPoolIdleTimeoutSecs, 60) seconds, including when no queries arrive. Expired scopes are not reused between sweeps. Eviction drops the cache's pool reference, allowing r2d2 and the ADBC driver to release connections and backend sessions. In-flight queries retain their pools until completion, so eviction does not interrupt them; retained pools and builds in progress can temporarily exceed the cache count limit.

Prometheus /metrics exposes queryflux_adbc_scoped_pools (cached pool count) and queryflux_adbc_scoped_pool_evictions_total (counter). Both have cluster_group and cluster_name labels; the counter also has reason="idle" or reason="lru". No identity, password, or token is included in metric labels. The gauge includes both adapter generations while a config reload drains the old adapter. Expired scopes remain in the gauge until the next background sweep or pool insertion removes them, even if a lookup has already rejected them as expired. Allow for this cleanup delay when alerting on the gauge: the background task waits min(scopedPoolIdleTimeoutSecs, 60) seconds between sweeps, with additional delay possible from worker scheduling and cleanup. Connection cleanup runs on blocking workers because driver release callbacks can involve network I/O.


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.


Gateway auth/authz and backend identity do not by themselves rewrite SQL for row filters or column masks. For identity-driven table grants, filters, and masks via OPA, see Access control and OPA provider.