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 tokafka-broker-receiverDockerfile.dispatcher— adds jar tokafka-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:
- Managed Identity — created by Terraform, assigned
Azure Event Hubs Data Owneron the namespace - Federated Credential — trusts the AKS OIDC issuer for ServiceAccount
knative-kafka-broker-data-plane - ServiceAccount annotations —
azure.workload.identity/client-id+ labelazure.workload.identity/use=true - 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:
- Create auth secret (OAUTHBEARER, no user/password)
- Patch data-plane ConfigMap with JAAS config + callback handler
- Annotate ServiceAccount for Workload Identity
- Patch Deployment/StatefulSet with custom images + WI pod label
- Wait for rollout
- 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.