Skip to content

Phase 1 & 2: OAUTHBEARER PoC — Results & Analysis

Summary

Objective: Prove that Azure Event Hubs can be used with KNative Eventing's Kafka Broker using OAUTHBEARER + Managed Identity — zero static credentials.

Result: ✅ End-to-end event flow working, token refresh validated over multiple expiry cycles.


Architecture

CloudEvent (HTTP)
  → kafka-broker-receiver (custom image + Conduktor jar)
    → Azure Event Hubs (SASL_SSL + OAUTHBEARER)
      → kafka-broker-dispatcher (custom image + Conduktor jar)
        → Trigger → event-display

Custom Images

The upstream KNative data-plane images don't ship with an OAUTHBEARER callback handler. We build derived images that add the Conduktor Azure OAUTHBEARER handler jar to the classpath:

  • Dockerfile.receiver — adds jar to kafka-broker-receiver
  • Dockerfile.dispatcher — adds jar to kafka-broker-dispatcher

The jar is built from source via build-conduktor-jar.sh (version 0.5.0 for Kafka 3.x compatibility).

Classpath Integration

The KNative data-plane images use a jib-classpath-file that lists jar paths. The Dockerfiles append the Conduktor jar path to this file so the JVM picks it up at startup.


The Workaround: ConfigMap-Based Auth

Why not the auth secret?

KafkaClientsAuth.java has an empty code path for OAUTHBEARER (see Phase 0 Recon):

// When mechanism == OAUTHBEARER:
// → no sasl.jaas.config set
// → no sasl.login.callback.handler.class set
// → client left with default OAuthBearerLoginModule (no token source)

The secret only supports: protocol, sasl.mechanism, user, password, ca.crt, user.crt, user.key. There's no key for sasl.jaas.config or sasl.login.callback.handler.class.

The ConfigMap path

The data-plane merges properties from ConfigMaps (config-kafka-broker-data-plane) with secret-derived properties. Since KafkaClientsAuth does nothing for OAUTHBEARER, the ConfigMap properties pass through unobstructed:

# config-kafka-broker-producer.properties / config-kafka-broker-consumer.properties
bootstrap.servers=evhns-knative-lab.servicebus.windows.net:9093
security.protocol=SASL_SSL
sasl.mechanism=OAUTHBEARER
sasl.jaas.config=org.apache.kafka.common.security.oauthbearer.OAuthBearerLoginModule required scope="https://evhns-knative-lab.servicebus.windows.net/.default";
sasl.login.callback.handler.class=io.conduktor.kafka.security.oauthbearer.azure.AzureManagedIdentityCallbackHandler

Why this is a workaround, not a solution

Concern Impact
Auth config in ConfigMap instead of Secret Credentials aren't secret here (no password), but it violates separation of concerns
Not following official docs An operator reading the KNative docs would put everything in the auth secret — and it won't work
Not portable upstream This setup only works because of the empty code path; a future fix could break it

The proper fix (Phase 3): upstream change to propagate sasl.jaas.config and sasl.login.callback.handler.class from the auth secret when sasl.mechanism=OAUTHBEARER.


Workload Identity Setup

Azure Workload Identity injects credentials into pods via a mutating webhook:

  1. Managed Identity — created by Terraform, assigned Azure Event Hubs Data Owner on the namespace
  2. Federated Credential — trusts the AKS OIDC issuer for ServiceAccount knative-kafka-broker-data-plane
  3. ServiceAccount annotations — azure.workload.identity/client-id + label azure.workload.identity/use=true
  4. Pod label — azure.workload.identity/use=true (the webhook injects env vars only on labeled pods)

Injected environment variables (used by the Conduktor handler): - AZURE_CLIENT_ID - AZURE_TENANT_ID - AZURE_FEDERATED_TOKEN_FILE (path to projected service account token) - AZURE_AUTHORITY_HOST


Phase 2: Token Refresh Soak Test

Setup

  • Continuous low-rate event flow (1 event/minute)
  • Test duration: >2.5 hours (covers at least 2 token expiry cycles — Azure tokens expire ~1h)
  • Monitoring: dispatcher and receiver logs for SaslAuthenticationException, reconnect events, consumer lag

Results

Metric Result
Auth failures 0
Events lost 0
Events duplicated 0
Token refreshes observed ✅ Transparent — no connection drops
Consumer lag spikes None

The Conduktor handler (v0.5.0) correctly implements token refresh: it returns a fresh token on each OAuthBearerTokenCallback rather than caching an expired one. The Kafka client's built-in re-authentication (available since Kafka 2.2) handles the rest transparently.

Gate 2: ✅ PASSED

Zero auth failures and zero event loss across multiple token expiry windows.


Deployment Script

Full deployment automated in oauthbearer-poc/setup-oauthbearer-poc.sh:

  1. Create auth secret (OAUTHBEARER, no user/password)
  2. Patch data-plane ConfigMap with JAAS config + callback handler
  3. Annotate ServiceAccount for Workload Identity
  4. Patch Deployment/StatefulSet with custom images + WI pod label
  5. Wait for rollout
  6. Verify AZURE_* env vars injected

Next Steps

→ Phase 3: Upstream PR to eventing-kafka-broker — make KafkaClientsAuth.java propagate sasl.jaas.config and sasl.login.callback.handler.class from the auth secret when mechanism is OAUTHBEARER. This removes the ConfigMap workaround and makes the setup follow the standard auth secret path.