CVE-2026-76187: Apache Airflow Keycloak provider cross-client authentication bypass

Credentials for one application become an Airflow identity

The Apache Airflow Keycloak provider lets Airflow 3 authenticate through Keycloak, an identity service. Its vulnerable token handler accepts credentials for an unrelated application in the same Keycloak realm, a domain that groups clients and identities. Airflow then issues its own session token for that application’s service account, crossing the intended boundary between applications.

The affected path uses OAuth’s client_credentials grant: an application authenticates with a client ID and secret instead of an interactive user login. A confidential client is an application registered to authenticate using credentials it can keep secret. With service accounts enabled, that client can authenticate as a nonhuman identity. The flaw is that valid credentials for such an identity are treated as sufficient authority to create an Airflow-authenticated identity, without checking whether the client is the one configured for Airflow.

Exposure and affected versions

Exploitation requires Airflow 3 to use KeycloakAuthManager with a vulnerable provider version that implements the client-credentials flow. The configured realm must also contain another confidential client with service accounts enabled, and the attacker must possess that other client’s valid ID and secret. No Airflow credentials or user interaction are required.

The attacker must be able to reach POST /auth/token, and Airflow must be able to contact its configured Keycloak service. Direct attacker access to Keycloak is unnecessary because Airflow performs the credential exchange.

The declared affected range for apache-airflow-providers-keycloak is all versions below 0.10.0. Version 0.9.0 is identified as the last affected release. An inferred exploitable window of 0.5.0–0.9.0 follows from the client-credentials implementation history and release dates, but its lower bound was not established by inspecting every version’s source. It should not replace the declared range when assessing upgrades.

The behavioral evidence covers provider 0.9.0 with Airflow 3.3.1 and Keycloak 26.3.5. It does not establish identical runtime behavior across the entire declared affected range.

Where the client check is missing

The authentication-manager application is mounted at /auth, making its unauthenticated /token route reachable as POST /auth/token. The handler accepts a request body containing a grant type and credentials. For grant_type=client_credentials, it dispatches to TokenClientCredentialsBody.create_token, which takes client_id and client_secret from that body.

The service function create_client_credentials_token passes these caller-controlled values to KeycloakAuthManager.get_keycloak_client and requests a client-credentials token from the configured realm. It neither compares the submitted ID with [keycloak_auth_manager] client_id nor applies a client allowlist. Keycloak therefore receives credentials for whichever client the caller selected.

Keycloak can correctly authenticate that client while Airflow still makes the wrong trust decision. Successful authentication proves that the caller holds the selected client’s credentials; it does not establish that the client is permitted to authenticate to Airflow. The vulnerable handler fails to enforce that distinction.

After a successful exchange, Airflow creates a KeycloakAuthManagerUser using the returned sub field—the identity’s subject identifier. It derives the name from preferred_username, clientId, or a service-account fallback, then calls generate_jwt to issue an Airflow session JSON Web Token (JWT). JWTs carry identity claims protected by a signature. Here, Airflow signs the token itself: the attacker does not need to forge a signature.

Successful issuance returns HTTP 201 with the JWT in access_token; rejected Keycloak credentials return HTTP 403. In the observed deployment, the session token had audience apache-airflow, identifying its intended recipient, and a 24-hour lifetime. Those details are observations from the tested versions, not established properties of every affected release.

Session access and the limits of the bypass

The issued JWT is consumed through Airflow’s session-cookie authentication path. On the described Airflow 3.3+ path, KeycloakJWTMiddleware processes the _token, _access_token, and _refresh_token cookies. Its get_user_from_token method returns no user when the access-token cookie is absent, and the service-account flow provides no refresh token. Successful session use therefore depends on the middleware’s cookie requirements; possessing the issued JWT alone is insufficient to establish that the session will work.

Authenticated access was demonstrated through GET /ui/auth/me: the unrelated service-account identity received HTTP 200, while a request without credentials received HTTP 401. This establishes access to an endpoint requiring authentication.

The bypass does not itself grant administrator privileges. Access extends to authentication-only endpoints and any resource permissions the service account already holds. Protected /api/v2/* endpoints still apply subject-specific authorization, including Keycloak User-Managed Access (UMA), which evaluates permissions for protected resources.

No successful resource-level operation was established. Protected API requests encountered HTTP 500 because the described realm lacked UMA authorization resources, a configuration-dependent result that does not establish either broad access or reliable protection. HTTP 200 responses from the unauthenticated /api/v2/monitor/health and /api/v2/version endpoints do not demonstrate the bypass.

The token endpoint also acts as a credential-validation oracle: a caller can distinguish accepted from rejected credentials for other clients in the realm through the HTTP 201 and HTTP 403 responses. This follows from forwarding the supplied credentials to Keycloak before restricting the client.

Upgrade and examine the authenticated identity

Upgrade apache-airflow-providers-keycloak to the fixed release, 0.10.0. The fix requires the submitted client_id to equal [keycloak_auth_manager] client_id before Airflow makes any request to Keycloak. A mismatch receives HTTP 403 with the same “Client credentials authentication failed” response used for a failed credential exchange.

The ordering matters: rejecting the wrong client before contacting Keycloak prevents Airflow from validating secrets for unrelated clients. Reusing the failure response also limits client-ID enumeration. The 0.10.0 fix is supported by patch source and the upstream announcement; runtime validation of that release is not part of the available evidence.

For investigation, correlate successful POST /auth/token requests with subsequent successful GET /ui/auth/me requests and examine the associated identity. An issued JWT name and authenticated identity response identifying a service account for a client other than the configured Airflow client are evidence of the crossed boundary. HTTP 201 alone is insufficient: legitimate issuance for the configured client also returns 201. Likewise, HTTP 403 can reflect an invalid secret and does not by itself establish an exploitation attempt.

References