This is an automated email from the ASF dual-hosted git repository.

kumaab pushed a commit to branch ranger-2.10
in repository https://gitbox.apache.org/repos/asf/ranger.git


The following commit(s) were added to refs/heads/ranger-2.10 by this push:
     new 17aa1cefe RANGER-5827: Add shared Kafka client authentication module 
with support for both SASL/OAUTHBEARER and SASL/GSSAPI (#1297)
17aa1cefe is described below

commit 17aa1cefe556e29bb9e8aa6bc8a91edf865714a5
Author: Abhishek Kumar <[email protected]>
AuthorDate: Fri Oct 16 14:24:41 2026 -0700

    RANGER-5827: Add shared Kafka client authentication module with support for 
both SASL/OAUTHBEARER and SASL/GSSAPI (#1297)
    
    (cherry picked from commit a1f647b0ad1794c0aca5f6227696c8ad35ab19fb)
---
 audit-server/audit-common/pom.xml                  |   5 +
 .../ranger/audit/server/AuditServerConstants.java  |  11 -
 .../ranger/audit/utils/AuditMessageQueueUtils.java |  84 +-----
 .../dispatcher/kafka/AuditDispatcherBase.java      |  13 +-
 .../conf/ranger-audit-dispatcher-hdfs-site.xml     |  25 +-
 .../ranger-audit-dispatcher-opensearch-site.xml    |  25 +-
 .../conf/ranger-audit-dispatcher-solr-site.xml     |  25 +-
 .../ranger/audit/producer/kafka/AuditProducer.java |  13 +-
 .../ranger/audit/server/AuditServerConfig.java     |  11 +
 .../resources/conf/ranger-audit-ingestor-site.xml  |  25 +-
 .../audit/producer/kafka/AuditProducerTest.java    |  65 ++++-
 .../ranger-docker/Dockerfile.ranger-audit-ingestor |   7 +-
 dev-support/ranger-docker/Dockerfile.ranger-kafka  |   2 +-
 .../ranger-docker/README-KAFKA-CLIENT-AUTH.md      |  52 ++++
 dev-support/ranger-docker/README.md                |   4 +
 .../docker-compose.ranger-audit-service.yml        |   7 +
 .../ranger-docker/docker-compose.ranger-kafka.yml  |   5 +
 .../docker-compose.ranger-tagsync.yml              |   4 +
 .../ranger-audit-dispatcher-hdfs-site.xml          |  25 +-
 .../ranger-audit-dispatcher-opensearch-site.xml    |  25 +-
 .../ranger-audit-dispatcher-solr-site.xml          |  25 +-
 .../audit-ingestor/audit-ingestor-config.py        | 105 +++++++
 .../configs/ranger-audit-ingestor-site.yaml        | 130 +++++++++
 .../audit-ingestor/ranger-audit-ingestor.sh        |   7 +
 .../scripts/kafka/kafka-oauth-token-minter.sh      |  65 +++++
 .../scripts/kafka/ranger-kafka-setup.sh            |   8 +-
 .../ranger-docker/scripts/kafka/ranger-kafka.sh    |   2 +
 .../tagsync/ranger-tagsync-install.properties      |   3 +
 .../scripts/tagsync/ranger-tagsync.sh              |  12 +
 distro/src/main/assembly/tagsync.xml               |   1 +
 pom.xml                                            |   4 +
 .../pom.xml                                        |  40 +--
 .../auth/RangerKafkaClientSecurityConfig.java      | 320 +++++++++++++++++++++
 ...RangerKafkaOAuthBearerLoginCallbackHandler.java | 225 +++++++++++++++
 .../ranger/kafka/auth/RangerOAuthBearerToken.java  |  69 +++++
 .../auth/RangerKafkaClientSecurityConfigTest.java  | 301 +++++++++++++++++++
 ...erKafkaOAuthBearerLoginCallbackHandlerTest.java | 231 +++++++++++++++
 tagsync/conf/templates/installprop2xml.properties  |   2 +
 tagsync/pom.xml                                    |   5 +
 tagsync/scripts/install.properties                 |   4 +
 tagsync/scripts/setup.py                           |  44 ++-
 41 files changed, 1887 insertions(+), 149 deletions(-)

diff --git a/audit-server/audit-common/pom.xml 
b/audit-server/audit-common/pom.xml
index d9a5cd0b4..ed0b2f587 100644
--- a/audit-server/audit-common/pom.xml
+++ b/audit-server/audit-common/pom.xml
@@ -36,6 +36,11 @@
             <artifactId>kafka-clients</artifactId>
             <version>${kafka.version}</version>
         </dependency>
+        <dependency>
+            <groupId>org.apache.ranger</groupId>
+            <artifactId>ranger-kafka-client-auth</artifactId>
+            <version>${project.version}</version>
+        </dependency>
         <dependency>
             <groupId>org.apache.ranger</groupId>
             <artifactId>ranger-plugins-common</artifactId>
diff --git 
a/audit-server/audit-common/src/main/java/org/apache/ranger/audit/server/AuditServerConstants.java
 
b/audit-server/audit-common/src/main/java/org/apache/ranger/audit/server/AuditServerConstants.java
index 58b136b85..bcd3c955f 100644
--- 
a/audit-server/audit-common/src/main/java/org/apache/ranger/audit/server/AuditServerConstants.java
+++ 
b/audit-server/audit-common/src/main/java/org/apache/ranger/audit/server/AuditServerConstants.java
@@ -34,13 +34,6 @@ private AuditServerConstants() {}
      AUDIT-SERVER INGESTOR Configuration
      **************************************/
     // kafka configuration for audit ingestor
-    public static final String JAAS_KRB5_MODULE                              = 
"com.sun.security.auth.module.Krb5LoginModule required";
-    public static final String JAAS_USE_KEYTAB                               = 
"useKeyTab=true";
-    public static final String JAAS_KEYTAB                                   = 
"keyTab=\"";
-    public static final String JAAS_STOKE_KEY                                = 
"storeKey=true";
-    public static final String JAAS_SERVICE_NAME                             = 
"serviceName=kafka";
-    public static final String JAAS_USER_TICKET_CACHE                        = 
"useTicketCache=false";
-    public static final String JAAS_PRINCIPAL                                = 
"principal=\"";
     public static final String PROP_KAFKA_PROP_PREFIX                        = 
"xasecure.audit.destination.kafka";
     public static final String PROP_BOOTSTRAP_SERVERS                        = 
"kafka.bootstrap.servers";
     public static final String PROP_TOPIC_NAME                               = 
"kafka.topic.name";
@@ -51,7 +44,6 @@ private AuditServerConstants() {}
     public static final String PROP_REQ_TIMEOUT_MS                           = 
"kafka.request.timeout.ms";
     public static final String PROP_CONN_MAX_IDEAL_MS                        = 
"kafka.connections.max.idle.ms";
     public static final String PROP_SASL_JAAS_CONFIG                         = 
"sasl.jaas.config";
-    public static final String PROP_SASL_KERBEROS_SERVICE_NAME               = 
"sasl.kerberos.service.name";
 
     // kafka topic - ranger_audits configuration
     public static final String PROP_TOPIC_PARTITIONS                         = 
"kafka.topic.partitions";
@@ -96,10 +88,7 @@ private AuditServerConstants() {}
 
     // Kafka Topic defaults
     public static final String DEFAULT_TOPIC                                 = 
"ranger_audits";
-    public static final String DEFAULT_SASL_MECHANISM                        = 
"PLAIN";
-    public static final String DEFAULT_SECURITY_PROTOCOL                     = 
"PLAINTEXT";
     public static final String DEFAULT_SERVICE_NAME                          = 
"kafka";
-    public static final String PROP_SECURITY_PROTOCOL_VALUE                  = 
"SASL";
 
     // kafka Offset commit strategies
     public static final String PROP_OFFSET_COMMIT_STRATEGY_MANUAL            = 
"manual";
diff --git 
a/audit-server/audit-common/src/main/java/org/apache/ranger/audit/utils/AuditMessageQueueUtils.java
 
b/audit-server/audit-common/src/main/java/org/apache/ranger/audit/utils/AuditMessageQueueUtils.java
index bfba27260..80512619a 100644
--- 
a/audit-server/audit-common/src/main/java/org/apache/ranger/audit/utils/AuditMessageQueueUtils.java
+++ 
b/audit-server/audit-common/src/main/java/org/apache/ranger/audit/utils/AuditMessageQueueUtils.java
@@ -19,7 +19,6 @@
 
 package org.apache.ranger.audit.utils;
 
-import org.apache.hadoop.security.SecureClientLogin;
 import org.apache.kafka.clients.admin.AdminClient;
 import org.apache.kafka.clients.admin.AdminClientConfig;
 import org.apache.kafka.clients.admin.CreatePartitionsResult;
@@ -29,10 +28,10 @@
 import org.apache.kafka.clients.admin.TopicDescription;
 import org.apache.ranger.audit.provider.MiscUtil;
 import org.apache.ranger.audit.server.AuditServerConstants;
+import org.apache.ranger.kafka.auth.RangerKafkaClientSecurityConfig;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import java.io.File;
 import java.time.Duration;
 import java.util.Collections;
 import java.util.HashMap;
@@ -52,8 +51,6 @@ public static String createAuditsTopicIfNotExists(Properties 
props, String propP
         String ret                  = null;
         String topicName            = MiscUtil.getStringProperty(props, 
propPrefix + "." + AuditServerConstants.PROP_TOPIC_NAME, 
AuditServerConstants.DEFAULT_TOPIC);
         String bootstrapServers     = MiscUtil.getStringProperty(props, 
propPrefix + "." + AuditServerConstants.PROP_BOOTSTRAP_SERVERS);
-        String securityProtocol     = MiscUtil.getStringProperty(props, 
propPrefix + "." + AuditServerConstants.PROP_SECURITY_PROTOCOL, 
AuditServerConstants.DEFAULT_SECURITY_PROTOCOL);
-        String saslMechanism        = MiscUtil.getStringProperty(props, 
propPrefix + "." + AuditServerConstants.PROP_SASL_MECHANISM, 
AuditServerConstants.DEFAULT_SASL_MECHANISM);
         int    connMaxIdleTimeoutMS = MiscUtil.getIntProperty(props, 
propPrefix + "." + AuditServerConstants.PROP_CONN_MAX_IDEAL_MS, 10000);
         int    partitions           = getPartitions(props, propPrefix);
         short  replicationFactor    = (short) MiscUtil.getIntProperty(props, 
propPrefix + "." + AuditServerConstants.PROP_REPLICATION_FACTOR, 
AuditServerConstants.DEFAULT_REPLICATION_FACTOR);
@@ -64,13 +61,8 @@ public static String createAuditsTopicIfNotExists(Properties 
props, String propP
         Map<String, Object> kafkaProp = new HashMap<>();
 
         kafkaProp.put(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, 
bootstrapServers);
-        kafkaProp.put("sasl.mechanism", saslMechanism);
-        kafkaProp.put(AdminClientConfig.SECURITY_PROTOCOL_CONFIG, 
securityProtocol);
 
-        if (securityProtocol != null && 
securityProtocol.toUpperCase().contains("SASL")) {
-            kafkaProp.put(AuditServerConstants.PROP_SASL_JAAS_CONFIG, 
getJAASConfig(props, propPrefix));
-            
kafkaProp.put(AuditServerConstants.PROP_SASL_KERBEROS_SERVICE_NAME, 
AuditServerConstants.DEFAULT_SERVICE_NAME);
-        }
+        RangerKafkaClientSecurityConfig.apply(props, propPrefix, kafkaProp);
 
         kafkaProp.put(AdminClientConfig.REQUEST_TIMEOUT_MS_CONFIG, 
reqTimeoutMS);
         kafkaProp.put(AdminClientConfig.CONNECTIONS_MAX_IDLE_MS_CONFIG, 
connMaxIdleTimeoutMS);
@@ -143,78 +135,6 @@ public static String 
createAuditsTopicIfNotExists(Properties props, String propP
         return ret;
     }
 
-    public static String getJAASConfig(Properties props, String propPrefix) {
-        // Use ranger service principal and keytab for Kafka authentication
-        // This ensures consistent identity across all Ranger services and 
destination writes
-        String hostName  = props.getProperty(propPrefix + "." + "host");
-        String principal = props.getProperty(propPrefix + "." + 
AuditServerConstants.PROP_AUDIT_SERVICE_PRINCIPAL);
-        String keytab    = props.getProperty(propPrefix + "." + 
AuditServerConstants.PROP_AUDIT_SERVICE_KEYTAB);
-
-        AuditServerLogFormatter.builder("Kerberos Configuration")
-                .add("Principal (raw)", principal)
-                .add("Hostname", hostName)
-                .add("Keytab path", keytab)
-                .logInfo(LOG);
-
-        // Validate keytab file exists and is readable
-        if (keytab != null) {
-            File keytabFile = new File(keytab);
-
-            if (!keytabFile.exists()) {
-                LOG.error("ERROR: Keytab file does not exist: {}", keytab);
-
-                throw new IllegalStateException("Keytab file not found: " + 
keytab);
-            } else if (!keytabFile.canRead()) {
-                LOG.error("ERROR: Keytab file is not readable: {}", keytab);
-
-                throw new IllegalStateException("Keytab file not readable: " + 
keytab);
-            }
-
-            AuditServerLogFormatter.builder("Keytab File Validation")
-                    .add("Exists", keytabFile.exists())
-                    .add("Readable", keytabFile.canRead())
-                    .add("Size (bytes)", keytabFile.length())
-                    .logInfo(LOG);
-        }
-
-        try {
-            principal = SecureClientLogin.getPrincipal(principal, hostName);
-
-            LOG.info("Principal (resolved): {}", principal);
-        } catch (Exception e) {
-            principal = null;
-
-            LOG.error("ERROR: Failed to resolve principal from _HOST 
pattern!", e);
-        }
-
-        if (keytab == null || principal == null) {
-            AuditServerLogFormatter.builder("Please configure the following 
properties in ranger-audit-ingestor-site.xml:")
-                    .add(propPrefix + "." + 
AuditServerConstants.PROP_AUDIT_SERVICE_PRINCIPAL, "ranger/_HOST@YOUR-REALM")
-                    .add(propPrefix + "." + 
AuditServerConstants.PROP_AUDIT_SERVICE_KEYTAB, "/path/to/ranger.keytab")
-                    .logError(LOG);
-
-            throw new IllegalStateException("Ranger service principal and 
keytab must be configured for Kafka authentication. ");
-        }
-
-        String jaasConfig = new StringBuilder()
-                .append(AuditServerConstants.JAAS_KRB5_MODULE).append(" ")
-                .append(AuditServerConstants.JAAS_USE_KEYTAB).append(" ")
-                
.append(AuditServerConstants.JAAS_KEYTAB).append(keytab).append("\"").append(" 
")
-                .append(AuditServerConstants.JAAS_STOKE_KEY).append(" ")
-                .append(AuditServerConstants.JAAS_USER_TICKET_CACHE).append(" 
")
-                .append(AuditServerConstants.JAAS_SERVICE_NAME).append(" ")
-                
.append(AuditServerConstants.JAAS_PRINCIPAL).append(principal).append("\";")
-                .toString();
-
-        AuditServerLogFormatter.builder("JAAS Configuration Generated")
-                .add("Principal", principal)
-                .add("Keytab", keytab)
-                .add("Full JAAS Config", jaasConfig)
-                .logInfo(LOG);
-
-        return jaasConfig;
-    }
-
     private static String updateExistingTopicPartitions(AdminClient admin, 
String topicName, int partitions) {
         LOG.info("==> AuditMessageQueueUtils:updateExistingTopicPartitions() 
topic: {}, desired partitions: {}", topicName, partitions);
 
diff --git 
a/audit-server/audit-dispatcher/dispatcher-common/src/main/java/org/apache/ranger/audit/dispatcher/kafka/AuditDispatcherBase.java
 
b/audit-server/audit-dispatcher/dispatcher-common/src/main/java/org/apache/ranger/audit/dispatcher/kafka/AuditDispatcherBase.java
index ae9327129..29112b612 100644
--- 
a/audit-server/audit-dispatcher/dispatcher-common/src/main/java/org/apache/ranger/audit/dispatcher/kafka/AuditDispatcherBase.java
+++ 
b/audit-server/audit-dispatcher/dispatcher-common/src/main/java/org/apache/ranger/audit/dispatcher/kafka/AuditDispatcherBase.java
@@ -18,7 +18,6 @@
  */
 package org.apache.ranger.audit.dispatcher.kafka;
 
-import org.apache.kafka.clients.admin.AdminClientConfig;
 import org.apache.kafka.clients.consumer.ConsumerConfig;
 import org.apache.kafka.clients.consumer.ConsumerRecords;
 import org.apache.kafka.clients.consumer.KafkaConsumer;
@@ -26,7 +25,7 @@
 import org.apache.kafka.common.TopicPartition;
 import org.apache.ranger.audit.provider.MiscUtil;
 import org.apache.ranger.audit.server.AuditServerConstants;
-import org.apache.ranger.audit.utils.AuditMessageQueueUtils;
+import org.apache.ranger.kafka.auth.RangerKafkaClientSecurityConfig;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -67,15 +66,7 @@ public AuditDispatcherBase(Properties props, String 
propPrefix, String dispatche
         dispatcherProps.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, 
Class.forName("org.apache.kafka.common.serialization.StringDeserializer"));
         dispatcherProps.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, 
Class.forName("org.apache.kafka.common.serialization.StringDeserializer"));
 
-        String securityProtocol = MiscUtil.getStringProperty(props, propPrefix 
+ "." + AuditServerConstants.PROP_SECURITY_PROTOCOL, 
AuditServerConstants.DEFAULT_SECURITY_PROTOCOL);
-        dispatcherProps.put(AdminClientConfig.SECURITY_PROTOCOL_CONFIG, 
securityProtocol);
-
-        dispatcherProps.put("sasl.mechanism", 
MiscUtil.getStringProperty(props, propPrefix + "." + 
AuditServerConstants.PROP_SASL_MECHANISM, 
AuditServerConstants.DEFAULT_SASL_MECHANISM));
-        
dispatcherProps.put(AuditServerConstants.PROP_SASL_KERBEROS_SERVICE_NAME, 
AuditServerConstants.DEFAULT_SERVICE_NAME);
-
-        if 
(securityProtocol.toUpperCase().contains(AuditServerConstants.PROP_SECURITY_PROTOCOL_VALUE))
 {
-            dispatcherProps.put(AuditServerConstants.PROP_SASL_JAAS_CONFIG, 
AuditMessageQueueUtils.getJAASConfig(props, propPrefix));
-        }
+        RangerKafkaClientSecurityConfig.apply(props, propPrefix, 
dispatcherProps);
 
         dispatcherProps.put(ConsumerConfig.MAX_POLL_RECORDS_CONFIG, 
MiscUtil.getIntProperty(props, propPrefix + "." + 
AuditServerConstants.PROP_DISPATCHER_MAX_POLL_RECORDS, 
AuditServerConstants.DEFAULT_MAX_POLL_RECORDS));
         dispatcherProps.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, 
"earliest");
diff --git 
a/audit-server/audit-dispatcher/dispatcher-hdfs/src/main/resources/conf/ranger-audit-dispatcher-hdfs-site.xml
 
b/audit-server/audit-dispatcher/dispatcher-hdfs/src/main/resources/conf/ranger-audit-dispatcher-hdfs-site.xml
index 7f1ffc335..45a3cbcca 100644
--- 
a/audit-server/audit-dispatcher/dispatcher-hdfs/src/main/resources/conf/ranger-audit-dispatcher-hdfs-site.xml
+++ 
b/audit-server/audit-dispatcher/dispatcher-hdfs/src/main/resources/conf/ranger-audit-dispatcher-hdfs-site.xml
@@ -61,7 +61,30 @@
 
     <property>
         <name>ranger.audit.dispatcher.kafka.sasl.mechanism</name>
-        <value>GSSAPI</value>
+        <value></value>
+        <description>SASL mechanism for SASL_PLAINTEXT/SASL_SSL: empty = 
GSSAPI (default; service.kerberos.principal/keytab with the Krb5LoginModule 
options below) or OAUTHBEARER</description>
+    </property>
+
+    <property>
+        <name>ranger.audit.dispatcher.kafka.sasl.jaas.option.useKeyTab</name>
+        <value>true</value>
+        <description>GSSAPI Krb5LoginModule option; any option can be set as 
ranger.audit.dispatcher.kafka.sasl.jaas.option.&lt;name&gt;</description>
+    </property>
+
+    <property>
+        <name>ranger.audit.dispatcher.kafka.sasl.jaas.option.storeKey</name>
+        <value>true</value>
+    </property>
+
+    <property>
+        
<name>ranger.audit.dispatcher.kafka.sasl.jaas.option.useTicketCache</name>
+        <value>false</value>
+    </property>
+
+    <property>
+        
<name>ranger.audit.dispatcher.kafka.sasl.oauthbearer.token.endpoint.url</name>
+        <value>file:///var/run/secrets/kafka/token</value>
+        <description>OAUTHBEARER only: a file:// JWT (e.g. Kubernetes 
projected service-account token) re-read on every login, or an http(s):// token 
endpoint for Kafka's client-credentials handler</description>
     </property>
 
     <!-- HDFS Dispatcher Threading Configuration -->
diff --git 
a/audit-server/audit-dispatcher/dispatcher-opensearch/src/main/resources/conf/ranger-audit-dispatcher-opensearch-site.xml
 
b/audit-server/audit-dispatcher/dispatcher-opensearch/src/main/resources/conf/ranger-audit-dispatcher-opensearch-site.xml
index b5f01f3d9..41b814348 100644
--- 
a/audit-server/audit-dispatcher/dispatcher-opensearch/src/main/resources/conf/ranger-audit-dispatcher-opensearch-site.xml
+++ 
b/audit-server/audit-dispatcher/dispatcher-opensearch/src/main/resources/conf/ranger-audit-dispatcher-opensearch-site.xml
@@ -62,7 +62,30 @@
 
     <property>
         <name>ranger.audit.dispatcher.kafka.sasl.mechanism</name>
-        <value>GSSAPI</value>
+        <value></value>
+        <description>SASL mechanism for SASL_PLAINTEXT/SASL_SSL: empty = 
GSSAPI (default; service.kerberos.principal/keytab with the Krb5LoginModule 
options below) or OAUTHBEARER</description>
+    </property>
+
+    <property>
+        <name>ranger.audit.dispatcher.kafka.sasl.jaas.option.useKeyTab</name>
+        <value>true</value>
+        <description>GSSAPI Krb5LoginModule option; any option can be set as 
ranger.audit.dispatcher.kafka.sasl.jaas.option.&lt;name&gt;</description>
+    </property>
+
+    <property>
+        <name>ranger.audit.dispatcher.kafka.sasl.jaas.option.storeKey</name>
+        <value>true</value>
+    </property>
+
+    <property>
+        
<name>ranger.audit.dispatcher.kafka.sasl.jaas.option.useTicketCache</name>
+        <value>false</value>
+    </property>
+
+    <property>
+        
<name>ranger.audit.dispatcher.kafka.sasl.oauthbearer.token.endpoint.url</name>
+        <value>file:///var/run/secrets/kafka/token</value>
+        <description>OAUTHBEARER token: a file:// JWT (e.g. Kubernetes 
projected service-account token) re-read on every login, or an http(s):// token 
endpoint for Kafka's client-credentials handler</description>
     </property>
 
     <!-- OpenSearch Dispatcher Threading Configuration -->
diff --git 
a/audit-server/audit-dispatcher/dispatcher-solr/src/main/resources/conf/ranger-audit-dispatcher-solr-site.xml
 
b/audit-server/audit-dispatcher/dispatcher-solr/src/main/resources/conf/ranger-audit-dispatcher-solr-site.xml
index b82128bee..2e96341ab 100644
--- 
a/audit-server/audit-dispatcher/dispatcher-solr/src/main/resources/conf/ranger-audit-dispatcher-solr-site.xml
+++ 
b/audit-server/audit-dispatcher/dispatcher-solr/src/main/resources/conf/ranger-audit-dispatcher-solr-site.xml
@@ -62,7 +62,30 @@
 
     <property>
         <name>ranger.audit.dispatcher.kafka.sasl.mechanism</name>
-        <value>GSSAPI</value>
+        <value></value>
+        <description>SASL mechanism for SASL_PLAINTEXT/SASL_SSL: empty = 
GSSAPI (default; service.kerberos.principal/keytab with the Krb5LoginModule 
options below) or OAUTHBEARER</description>
+    </property>
+
+    <property>
+        <name>ranger.audit.dispatcher.kafka.sasl.jaas.option.useKeyTab</name>
+        <value>true</value>
+        <description>GSSAPI Krb5LoginModule option; any option can be set as 
ranger.audit.dispatcher.kafka.sasl.jaas.option.&lt;name&gt;</description>
+    </property>
+
+    <property>
+        <name>ranger.audit.dispatcher.kafka.sasl.jaas.option.storeKey</name>
+        <value>true</value>
+    </property>
+
+    <property>
+        
<name>ranger.audit.dispatcher.kafka.sasl.jaas.option.useTicketCache</name>
+        <value>false</value>
+    </property>
+
+    <property>
+        
<name>ranger.audit.dispatcher.kafka.sasl.oauthbearer.token.endpoint.url</name>
+        <value>file:///var/run/secrets/kafka/token</value>
+        <description>OAUTHBEARER token: a file:// JWT (e.g. Kubernetes 
projected service-account token) re-read on every login, or an http(s):// token 
endpoint for Kafka's client-credentials handler</description>
     </property>
 
     <!-- Solr Dispatcher Threading Configuration -->
diff --git 
a/audit-server/audit-ingestor/src/main/java/org/apache/ranger/audit/producer/kafka/AuditProducer.java
 
b/audit-server/audit-ingestor/src/main/java/org/apache/ranger/audit/producer/kafka/AuditProducer.java
index 497848012..856a26c93 100644
--- 
a/audit-server/audit-ingestor/src/main/java/org/apache/ranger/audit/producer/kafka/AuditProducer.java
+++ 
b/audit-server/audit-ingestor/src/main/java/org/apache/ranger/audit/producer/kafka/AuditProducer.java
@@ -19,7 +19,6 @@
 
 package org.apache.ranger.audit.producer.kafka;
 
-import org.apache.kafka.clients.admin.AdminClientConfig;
 import org.apache.kafka.clients.producer.Callback;
 import org.apache.kafka.clients.producer.KafkaProducer;
 import org.apache.kafka.clients.producer.ProducerConfig;
@@ -27,8 +26,8 @@
 import org.apache.kafka.clients.producer.RecordMetadata;
 import org.apache.ranger.audit.provider.MiscUtil;
 import org.apache.ranger.audit.server.AuditServerConstants;
-import org.apache.ranger.audit.utils.AuditMessageQueueUtils;
 import org.apache.ranger.audit.utils.AuditServerLogFormatter;
+import org.apache.ranger.kafka.auth.RangerKafkaClientSecurityConfig;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -167,15 +166,7 @@ static Properties createProducerConfig(Properties props, 
String propPrefix) thro
                 producerPrefix + 
AuditServerConstants.PROP_PRODUCER_BATCH_SEND_TIMEOUT_MS,
                 AuditServerConstants.DEFAULT_PRODUCER_BATCH_SEND_TIMEOUT_MS);
 
-        String securityProtocol = MiscUtil.getStringProperty(props, propPrefix 
+ "." + AuditServerConstants.PROP_SECURITY_PROTOCOL, 
AuditServerConstants.DEFAULT_SECURITY_PROTOCOL);
-        producerProps.put(AdminClientConfig.SECURITY_PROTOCOL_CONFIG, 
securityProtocol);
-
-        producerProps.put(AuditServerConstants.PROP_SASL_MECHANISM, 
MiscUtil.getStringProperty(props, propPrefix + "." + 
AuditServerConstants.PROP_SASL_MECHANISM, 
AuditServerConstants.DEFAULT_SASL_MECHANISM));
-        
producerProps.put(AuditServerConstants.PROP_SASL_KERBEROS_SERVICE_NAME, 
AuditServerConstants.DEFAULT_SERVICE_NAME);
-
-        if 
(securityProtocol.toUpperCase().contains(AuditServerConstants.PROP_SECURITY_PROTOCOL_VALUE))
 {
-            producerProps.put(AuditServerConstants.PROP_SASL_JAAS_CONFIG, 
AuditMessageQueueUtils.getJAASConfig(props, propPrefix));
-        }
+        RangerKafkaClientSecurityConfig.apply(props, propPrefix, 
producerProps);
 
         String configuredPlugins = MiscUtil.getStringProperty(props, 
propPrefix + "." + AuditServerConstants.PROP_CONFIGURED_PLUGINS, "");
         if (configuredPlugins != null && !configuredPlugins.trim().isEmpty()) {
diff --git 
a/audit-server/audit-ingestor/src/main/java/org/apache/ranger/audit/server/AuditServerConfig.java
 
b/audit-server/audit-ingestor/src/main/java/org/apache/ranger/audit/server/AuditServerConfig.java
index 185ca1000..12d38843e 100644
--- 
a/audit-server/audit-ingestor/src/main/java/org/apache/ranger/audit/server/AuditServerConfig.java
+++ 
b/audit-server/audit-ingestor/src/main/java/org/apache/ranger/audit/server/AuditServerConfig.java
@@ -31,6 +31,10 @@ public class AuditServerConfig extends AuditConfig {
 
     private static final String CONFIG_FILE_PATH = 
"conf/ranger-audit-ingestor-site.xml";
 
+    /* optional explicit site XML (e.g. 
-Daudit.config=/opt/ranger/audit-ingestor/conf/ranger-audit-ingestor-site.xml);
+     * loaded after the packaged default so its values win */
+    public static final String PROP_AUDIT_CONFIG_FILE = "audit.config";
+
     private static volatile AuditServerConfig sInstance;
 
     private AuditServerConfig() {
@@ -66,6 +70,13 @@ private boolean addAuditServerResources() {
             ret = false;
         }
 
+        String configFile = System.getProperty(PROP_AUDIT_CONFIG_FILE);
+
+        if (configFile != null && !configFile.trim().isEmpty() && 
!addAuditResource(configFile.trim(), true)) {
+            LOG.error("Could not load configuration from -D{}={}", 
PROP_AUDIT_CONFIG_FILE, configFile);
+            ret = false;
+        }
+
         LOG.debug("<== AuditServerConfig.addAuditServerResources(), 
result={}", ret);
 
         return ret;
diff --git 
a/audit-server/audit-ingestor/src/main/resources/conf/ranger-audit-ingestor-site.xml
 
b/audit-server/audit-ingestor/src/main/resources/conf/ranger-audit-ingestor-site.xml
index a0acadfe0..1faae14ac 100644
--- 
a/audit-server/audit-ingestor/src/main/resources/conf/ranger-audit-ingestor-site.xml
+++ 
b/audit-server/audit-ingestor/src/main/resources/conf/ranger-audit-ingestor-site.xml
@@ -440,7 +440,30 @@
 
     <property>
         <name>ranger.audit.ingestor.kafka.sasl.mechanism</name>
-        <value>GSSAPI</value>
+        <value></value>
+        <description>SASL mechanism for SASL_PLAINTEXT/SASL_SSL: empty = 
GSSAPI (default; service.kerberos.principal/keytab with the Krb5LoginModule 
options below) or OAUTHBEARER</description>
+    </property>
+
+    <property>
+        <name>ranger.audit.ingestor.kafka.sasl.jaas.option.useKeyTab</name>
+        <value>true</value>
+        <description>GSSAPI Krb5LoginModule option; any option can be set as 
ranger.audit.ingestor.kafka.sasl.jaas.option.&lt;name&gt;</description>
+    </property>
+
+    <property>
+        <name>ranger.audit.ingestor.kafka.sasl.jaas.option.storeKey</name>
+        <value>true</value>
+    </property>
+
+    <property>
+        
<name>ranger.audit.ingestor.kafka.sasl.jaas.option.useTicketCache</name>
+        <value>false</value>
+    </property>
+
+    <property>
+        
<name>ranger.audit.ingestor.kafka.sasl.oauthbearer.token.endpoint.url</name>
+        <value>file:///var/run/secrets/kafka/token</value>
+        <description>OAUTHBEARER token: a file:// JWT (e.g. Kubernetes 
projected service-account token) re-read on every login, or an http(s):// token 
endpoint for Kafka's client-credentials handler</description>
     </property>
 
     <property>
diff --git 
a/audit-server/audit-ingestor/src/test/java/org/apache/ranger/audit/producer/kafka/AuditProducerTest.java
 
b/audit-server/audit-ingestor/src/test/java/org/apache/ranger/audit/producer/kafka/AuditProducerTest.java
index 356cd6111..1a31f6b0a 100644
--- 
a/audit-server/audit-ingestor/src/test/java/org/apache/ranger/audit/producer/kafka/AuditProducerTest.java
+++ 
b/audit-server/audit-ingestor/src/test/java/org/apache/ranger/audit/producer/kafka/AuditProducerTest.java
@@ -17,19 +17,29 @@
 
 package org.apache.ranger.audit.producer.kafka;
 
-import org.apache.kafka.clients.admin.AdminClientConfig;
+import org.apache.kafka.clients.CommonClientConfigs;
 import org.apache.kafka.clients.producer.ProducerConfig;
+import org.apache.kafka.common.config.SaslConfigs;
 import org.apache.ranger.audit.server.AuditServerConstants;
+import org.apache.ranger.kafka.auth.RangerKafkaClientSecurityConfig;
+import org.apache.ranger.kafka.auth.RangerKafkaOAuthBearerLoginCallbackHandler;
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
 
+import java.nio.file.Files;
+import java.nio.file.Path;
 import java.util.Properties;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
 
 public class AuditProducerTest {
     private static final String PROP_PREFIX = 
AuditServerConstants.PROP_PREFIX_AUDIT_SERVER + "kafka";
 
+    @TempDir
+    Path tempDir;
+
     private static void assertProducerProp(Properties props, String key, 
Object expected) {
         assertEquals(String.valueOf(expected), String.valueOf(props.get(key)));
     }
@@ -54,10 +64,61 @@ public void testCreateProducerConfigDefaults() throws 
Exception {
         assertProducerProp(producerProps, 
ProducerConfig.MAX_REQUEST_SIZE_CONFIG, 
AuditServerConstants.DEFAULT_PRODUCER_MAX_REQUEST_SIZE);
         assertProducerProp(producerProps, ProducerConfig.MAX_BLOCK_MS_CONFIG, 
AuditServerConstants.DEFAULT_PRODUCER_MAX_BLOCK_MS);
         assertProducerProp(producerProps, 
ProducerConfig.REQUEST_TIMEOUT_MS_CONFIG, 
AuditServerConstants.DEFAULT_PRODUCER_REQUEST_TIMEOUT_MS);
-        assertEquals("PLAINTEXT", 
producerProps.get(AdminClientConfig.SECURITY_PROTOCOL_CONFIG));
+        assertEquals("PLAINTEXT", 
producerProps.get(CommonClientConfigs.SECURITY_PROTOCOL_CONFIG));
+        assertFalse(producerProps.containsKey(SaslConfigs.SASL_MECHANISM));
+        assertFalse(producerProps.containsKey(SaslConfigs.SASL_JAAS_CONFIG));
         
assertFalse(producerProps.containsKey(ProducerConfig.PARTITIONER_CLASS_CONFIG));
     }
 
+    @Test
+    public void 
testCreateProducerConfigSaslDefaultGssapiKeytabLoginRequiresKeytab() {
+        Properties props = new Properties();
+        props.setProperty(PROP_PREFIX + "." + 
AuditServerConstants.PROP_BOOTSTRAP_SERVERS, "kafka:9092");
+        props.setProperty(PROP_PREFIX + "." + 
AuditServerConstants.PROP_SECURITY_PROTOCOL, "SASL_PLAINTEXT");
+        props.setProperty(PROP_PREFIX + "." + 
RangerKafkaClientSecurityConfig.PROP_JAAS_OPTION_PREFIX + "useKeyTab", "true");
+
+        assertThrows(IllegalStateException.class, () -> 
AuditProducer.createProducerConfig(props, PROP_PREFIX));
+    }
+
+    @Test
+    public void testCreateProducerConfigSaslDefaultGssapiKeytab() throws 
Exception {
+        Path       keytab = Files.createFile(tempDir.resolve("ranger.keytab"));
+        Properties props  = new Properties();
+        props.setProperty(PROP_PREFIX + "." + 
AuditServerConstants.PROP_BOOTSTRAP_SERVERS, "kafka:9092");
+        props.setProperty(PROP_PREFIX + "." + 
AuditServerConstants.PROP_SECURITY_PROTOCOL, "SASL_PLAINTEXT");
+        props.setProperty(PROP_PREFIX + "." + 
AuditServerConstants.PROP_AUDIT_SERVICE_PRINCIPAL, 
"[email protected]");
+        props.setProperty(PROP_PREFIX + "." + 
AuditServerConstants.PROP_AUDIT_SERVICE_KEYTAB, keytab.toString());
+        props.setProperty(PROP_PREFIX + "." + 
RangerKafkaClientSecurityConfig.PROP_JAAS_OPTION_PREFIX + "useKeyTab", "true");
+        props.setProperty(PROP_PREFIX + "." + 
RangerKafkaClientSecurityConfig.PROP_JAAS_OPTION_PREFIX + "storeKey", "true");
+        props.setProperty(PROP_PREFIX + "." + 
RangerKafkaClientSecurityConfig.PROP_JAAS_OPTION_PREFIX + "useTicketCache", 
"false");
+
+        Properties producerProps = AuditProducer.createProducerConfig(props, 
PROP_PREFIX);
+
+        assertEquals("GSSAPI", producerProps.get(SaslConfigs.SASL_MECHANISM));
+        assertEquals("kafka", 
producerProps.get(SaslConfigs.SASL_KERBEROS_SERVICE_NAME));
+        assertEquals(RangerKafkaClientSecurityConfig.KRB5_LOGIN_MODULE + " 
required keyTab=\"" + keytab + "\" serviceName=\"kafka\" 
principal=\"[email protected]\" storeKey=\"true\" 
useKeyTab=\"true\" useTicketCache=\"false\";",
+                producerProps.get(SaslConfigs.SASL_JAAS_CONFIG));
+    }
+
+    @Test
+    public void testCreateProducerConfigSaslOAuthBearerFileToken() throws 
Exception {
+        Path       token = Files.createFile(tempDir.resolve("kafka-token"));
+        Properties props = new Properties();
+        props.setProperty(PROP_PREFIX + "." + 
AuditServerConstants.PROP_BOOTSTRAP_SERVERS, "kafka:9093");
+        props.setProperty(PROP_PREFIX + "." + 
AuditServerConstants.PROP_SECURITY_PROTOCOL, "SASL_SSL");
+        props.setProperty(PROP_PREFIX + "." + 
AuditServerConstants.PROP_SASL_MECHANISM, "OAUTHBEARER");
+        props.setProperty(PROP_PREFIX + "." + 
RangerKafkaClientSecurityConfig.PROP_OAUTH_TOKEN_ENDPOINT_URL, 
token.toUri().toString());
+
+        Properties producerProps = AuditProducer.createProducerConfig(props, 
PROP_PREFIX);
+
+        assertEquals("SASL_SSL", 
producerProps.get(CommonClientConfigs.SECURITY_PROTOCOL_CONFIG));
+        assertEquals("OAUTHBEARER", 
producerProps.get(SaslConfigs.SASL_MECHANISM));
+        assertEquals(RangerKafkaClientSecurityConfig.OAUTHBEARER_LOGIN_MODULE 
+ " required;", producerProps.get(SaslConfigs.SASL_JAAS_CONFIG));
+        assertEquals(token.toUri().toString(), 
producerProps.get(SaslConfigs.SASL_OAUTHBEARER_TOKEN_ENDPOINT_URL));
+        
assertEquals(RangerKafkaOAuthBearerLoginCallbackHandler.class.getName(), 
producerProps.get(SaslConfigs.SASL_LOGIN_CALLBACK_HANDLER_CLASS));
+        
assertFalse(producerProps.containsKey(SaslConfigs.SASL_KERBEROS_SERVICE_NAME));
+    }
+
     @Test
     public void testCreateProducerConfigCustomTuning() throws Exception {
         Properties props = new Properties();
diff --git a/dev-support/ranger-docker/Dockerfile.ranger-audit-ingestor 
b/dev-support/ranger-docker/Dockerfile.ranger-audit-ingestor
index a9a9d1b31..f5223946d 100644
--- a/dev-support/ranger-docker/Dockerfile.ranger-audit-ingestor
+++ b/dev-support/ranger-docker/Dockerfile.ranger-audit-ingestor
@@ -27,6 +27,9 @@ RUN apt-get update && apt-get install -y \
     net-tools \
     && rm -rf /var/lib/apt/lists/*
 
+# audit-ingestor-config.py renders conf/ranger-audit-ingestor-site.xml from 
configs/*.yaml
+RUN pip install --no-cache-dir pyyaml
+
 # Volume for keytabs
 VOLUME /etc/keytabs
 
@@ -39,6 +42,7 @@ COPY ./dist/ranger-${RANGER_VERSION}-audit-ingestor.tar.gz 
/tmp/
 # Copy scripts and Kerberos configuration
 COPY ./scripts/audit-ingestor/service-check-functions.sh    ${RANGER_SCRIPTS}/
 COPY ./scripts/audit-ingestor/ranger-audit-ingestor.sh        
${RANGER_SCRIPTS}/
+COPY ./scripts/audit-ingestor/audit-ingestor-config.py        
${RANGER_SCRIPTS}/
 COPY ./scripts/kdc/krb5.conf /etc/krb5.conf
 
 # Extract and setup
@@ -51,7 +55,7 @@ RUN tar xzf 
/tmp/ranger-${RANGER_VERSION}-audit-ingestor.tar.gz -C /opt/ranger/a
     ln -sf /var/log/ranger/audit-ingestor /opt/ranger/audit-ingestor/logs && \
     chown -R ranger:ranger /opt/ranger/audit-ingestor/ /var/log/ranger/ 
${RANGER_SCRIPTS} && \
     chmod -R 755 /opt/ranger/audit-ingestor && \
-    chmod 755 ${RANGER_SCRIPTS}/ranger-audit-ingestor.sh
+    chmod 755 ${RANGER_SCRIPTS}/ranger-audit-ingestor.sh 
${RANGER_SCRIPTS}/audit-ingestor-config.py
 
 # Set Java home
 ENV JAVA_HOME=/opt/java/openjdk
@@ -60,6 +64,7 @@ ENV PATH=$JAVA_HOME/bin:$PATH
 # Environment variables for audit ingestor
 ENV AUDIT_INGESTOR_HOME_DIR=/opt/ranger/audit-ingestor
 ENV AUDIT_INGESTOR_CONF_DIR=/opt/ranger/audit-ingestor/conf
+ENV AUDIT_INGESTOR_CONFIGS=/opt/ranger/audit-ingestor/configs
 ENV AUDIT_INGESTOR_LOG_DIR=/var/log/ranger/audit-ingestor
 ENV RANGER_USER=ranger
 
diff --git a/dev-support/ranger-docker/Dockerfile.ranger-kafka 
b/dev-support/ranger-docker/Dockerfile.ranger-kafka
index 03089e097..c254439e8 100644
--- a/dev-support/ranger-docker/Dockerfile.ranger-kafka
+++ b/dev-support/ranger-docker/Dockerfile.ranger-kafka
@@ -32,7 +32,7 @@ RUN tar xvfz 
/home/ranger/dist/kafka_2.12-${KAFKA_VERSION}.tgz --directory=/opt/
     ln -s /opt/ranger/ranger-${KAFKA_PLUGIN_VERSION}-kafka-plugin 
/opt/ranger/ranger-kafka-plugin && \
     rm -f /home/ranger/dist/ranger-${KAFKA_PLUGIN_VERSION}-kafka-plugin.tar.gz 
&& \
     rm -f /opt/ranger/ranger-kafka-plugin/install.properties && \
-    chmod 744 ${RANGER_SCRIPTS}/ranger-kafka-setup.sh 
${RANGER_SCRIPTS}/ranger-kafka.sh
+    chmod 744 ${RANGER_SCRIPTS}/ranger-kafka-setup.sh 
${RANGER_SCRIPTS}/ranger-kafka.sh ${RANGER_SCRIPTS}/kafka-oauth-token-minter.sh
 
 ENV KAFKA_HOME=/opt/kafka
 ENV 
PATH=/usr/java/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/opt/kafka/bin
diff --git a/dev-support/ranger-docker/README-KAFKA-CLIENT-AUTH.md 
b/dev-support/ranger-docker/README-KAFKA-CLIENT-AUTH.md
new file mode 100644
index 000000000..1214f96da
--- /dev/null
+++ b/dev-support/ranger-docker/README-KAFKA-CLIENT-AUTH.md
@@ -0,0 +1,52 @@
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+# Kafka client authentication
+
+The audit ingestor, the audit dispatchers and tagsync authenticate to Kafka 
through the shared module `ranger-kafka-client-auth`
+
+## Mechanisms
+
+| `kafka.security.protocol` | `kafka.sasl.mechanism` | Credential |
+|---|---|---|
+| `PLAINTEXT` (default), `SSL` | none | none |
+| `SASL_PLAINTEXT`, `SASL_SSL` | `GSSAPI` (default when empty) | 
`service.kerberos.principal` and `service.kerberos.keytab` |
+| `SASL_PLAINTEXT`, `SASL_SSL` | `OAUTHBEARER` | JWT from 
`kafka.sasl.oauthbearer.token.endpoint.url` |
+| `SASL_PLAINTEXT`, `SASL_SSL` | other (`PLAIN`, `SCRAM-*`) | 
`kafka.sasl.jaas.loginModuleName` and `kafka.sasl.jaas.option.*`, or 
`kafka.sasl.jaas.config` |
+
+- OAUTHBEARER with a `file://` URL uses 
`RangerKafkaOAuthBearerLoginCallbackHandler`, which re-reads the
+  token on every login and refresh, so a token rotated in place (for example a 
Kubernetes projected
+  service-account token) is picked up without a restart. An `http(s)://` URL 
uses Kafka's own
+  client-credentials handler. The broker validates the token; its `sub` is the 
Kafka principal.
+- JAAS is built from `kafka.sasl.jaas.loginModuleName`, 
`kafka.sasl.jaas.loginModuleControlFlag` and any
+  `kafka.sasl.jaas.option.<name>` under the service prefix. For GSSAPI the 
service principal and keytab are passed
+  as `principal` and `keyTab`; every other Krb5LoginModule option comes from 
configuration, and the shipped site
+  XML sets `useKeyTab=true`, `storeKey=true`, `useTicketCache=false`.
+- Any `kafka.sasl.*` or `kafka.ssl.*` property under the service prefix is 
passed to the Kafka client as is.
+
+## Docker Setup
+
+With `KERBEROS_ENABLED=true` the broker accepts both GSSAPI and OAUTHBEARER, 
and the clients use OAUTHBEARER:
+`ranger-kafka` runs `scripts/kafka/kafka-oauth-token-minter.sh`, which writes 
unsigned dev JWTs for
+`rangerauditserver` and `rangertagsync` to the `ranger-kafka-oauth-tokens` 
volume (`/etc/kafka-oauth`) and
+rewrites them every two minutes with a ten minute lifetime; the broker accepts 
them with Kafka's unsecured
+validator. Dev only, never use this outside docker.
+
+To switch a client to GSSAPI set its mechanism to `GSSAPI` in 
`scripts/audit-ingestor/configs/ranger-audit-ingestor-site.yaml`,
+`scripts/audit-dispatcher/ranger-audit-dispatcher-<type>-site.xml` or 
`scripts/tagsync/ranger-tagsync-install.properties`;
+the service keytabs are already configured.
diff --git a/dev-support/ranger-docker/README.md 
b/dev-support/ranger-docker/README.md
index a1ff6ce4f..c500623b9 100644
--- a/dev-support/ranger-docker/README.md
+++ b/dev-support/ranger-docker/README.md
@@ -90,6 +90,10 @@ docker compose --profile ${AUDIT_DESTINATIONS} -f 
docker-compose.ranger.yml -f d
 
 # Ranger Admin can be accessed at http://localhost:6080 (admin/rangerR0cks!)
 ~~~
+
+#### Kafka client authentication for the audit pipeline and tagsync
+See [README-KAFKA-CLIENT-AUTH.md](README-KAFKA-CLIENT-AUTH.md) for the 
supported mechanisms (GSSAPI by default, OAUTHBEARER used here) and how this 
setup exercises them.
+
 #### Bring up hive container
 ~~~
 docker compose --profile ${AUDIT_DESTINATIONS} -f docker-compose.ranger.yml -f 
docker-compose.ranger-audit-service.yml -f docker-compose.ranger-hadoop.yml -f 
docker-compose.ranger-hive.yml up -d
diff --git a/dev-support/ranger-docker/docker-compose.ranger-audit-service.yml 
b/dev-support/ranger-docker/docker-compose.ranger-audit-service.yml
index c2da71b59..8311e14e6 100644
--- a/dev-support/ranger-docker/docker-compose.ranger-audit-service.yml
+++ b/dev-support/ranger-docker/docker-compose.ranger-audit-service.yml
@@ -194,6 +194,9 @@ services:
       KAFKA_BOOTSTRAP_SERVERS: ranger-kafka.rangernw:9092
     volumes:
       - ./dist/keytabs/ranger-audit-ingestor:/etc/keytabs
+      - ranger-kafka-oauth-tokens:/etc/kafka-oauth:ro
+      # audit-ingestor-config.py renders conf/ranger-audit-ingestor-site.xml 
from configs/*.yaml
+      - ./scripts/audit-ingestor/configs:/opt/ranger/audit-ingestor/configs:ro
       - ranger-audit-ingestor-logs:/var/log/ranger/audit-ingestor
       - ranger-audit-ingestor-spool:/var/log/ranger/audit-ingestor/audit
     healthcheck:
@@ -235,6 +238,7 @@ services:
       SOLR_URL: http://ranger-solr.rangernw:8983/solr
     volumes:
       - ./dist/keytabs/ranger-audit-dispatcher-solr:/etc/keytabs
+      - ranger-kafka-oauth-tokens:/etc/kafka-oauth:ro
       - ranger-audit-dispatcher-solr-logs:/var/log/ranger/audit-dispatcher/solr
       - 
./scripts/audit-dispatcher/ranger-audit-dispatcher-solr-site.xml:/opt/ranger/audit-dispatcher/conf/ranger-audit-dispatcher-solr-site.xml
 
@@ -270,6 +274,7 @@ services:
       OPENSEARCH_URL: http://ranger-opensearch.rangernw:9200
     volumes:
       - ./dist/keytabs/ranger-audit-dispatcher-opensearch:/etc/keytabs
+      - ranger-kafka-oauth-tokens:/etc/kafka-oauth:ro
       - 
ranger-audit-dispatcher-opensearch-logs:/var/log/ranger/audit-dispatcher/opensearch
       - 
./scripts/audit-dispatcher/ranger-audit-dispatcher-opensearch-site.xml:/opt/ranger/audit-dispatcher/conf/ranger-audit-dispatcher-opensearch-site.xml
 
@@ -303,10 +308,12 @@ services:
       HDFS_NAMENODE: ranger-hadoop.rangernw:9000
     volumes:
       - ./dist/keytabs/ranger-audit-dispatcher-hdfs:/etc/keytabs
+      - ranger-kafka-oauth-tokens:/etc/kafka-oauth:ro
       - ranger-audit-dispatcher-hdfs-logs:/var/log/ranger/audit-dispatcher/hdfs
       - 
./scripts/audit-dispatcher/ranger-audit-dispatcher-hdfs-site.xml:/opt/ranger/audit-dispatcher/conf/ranger-audit-dispatcher-hdfs-site.xml
 
 volumes:
+  ranger-kafka-oauth-tokens:
   ranger-audit-ingestor-logs:
   ranger-audit-ingestor-spool:
   ranger-audit-dispatcher-solr-logs:
diff --git a/dev-support/ranger-docker/docker-compose.ranger-kafka.yml 
b/dev-support/ranger-docker/docker-compose.ranger-kafka.yml
index 6628435ae..fac6f2385 100644
--- a/dev-support/ranger-docker/docker-compose.ranger-kafka.yml
+++ b/dev-support/ranger-docker/docker-compose.ranger-kafka.yml
@@ -21,6 +21,8 @@ services:
       - 
./scripts/kafka/kafka-client.properties:/opt/kafka/config/kafka-client.properties:ro
       - ./dist/version:/home/ranger/dist/version:ro
       - 
./scripts/kafka/ranger-kafka-plugin-install.properties:/opt/ranger/ranger-kafka-plugin/install.properties
+      # dev SASL/OAUTHBEARER tokens minted by kafka-oauth-token-minter.sh, 
read by the Ranger Kafka clients
+      - ranger-kafka-oauth-tokens:/etc/kafka-oauth
     stdin_open: true
     tty: true
     networks:
@@ -60,6 +62,9 @@ services:
       KAFKA_PLUGIN_VERSION: ${KAFKA_PLUGIN_VERSION}
       KERBEROS_ENABLED: ${KERBEROS_ENABLED}
 
+volumes:
+  ranger-kafka-oauth-tokens:
+
 networks:
   ranger:
     name: rangernw
diff --git a/dev-support/ranger-docker/docker-compose.ranger-tagsync.yml 
b/dev-support/ranger-docker/docker-compose.ranger-tagsync.yml
index 1f5594190..e09a530ac 100644
--- a/dev-support/ranger-docker/docker-compose.ranger-tagsync.yml
+++ b/dev-support/ranger-docker/docker-compose.ranger-tagsync.yml
@@ -18,6 +18,7 @@ services:
       - ./dist/version:/home/ranger/dist/version:ro
       - 
./scripts/tagsync/ranger-tagsync-tags.json:/opt/ranger/tagsync/data/tags.json:ro
       - 
./scripts/tagsync/ranger-tagsync-install.properties:/opt/ranger/tagsync/install.properties
+      - ranger-kafka-oauth-tokens:/etc/kafka-oauth:ro
     stdin_open: true
     tty: true
     networks:
@@ -30,6 +31,9 @@ services:
       - DEBUG_TAGSYNC=${DEBUG_TAGSYNC:-false}
       - KERBEROS_ENABLED
 
+volumes:
+  ranger-kafka-oauth-tokens:
+
 networks:
   ranger:
     name: rangernw
diff --git 
a/dev-support/ranger-docker/scripts/audit-dispatcher/ranger-audit-dispatcher-hdfs-site.xml
 
b/dev-support/ranger-docker/scripts/audit-dispatcher/ranger-audit-dispatcher-hdfs-site.xml
index 03cea2fba..42071f4af 100644
--- 
a/dev-support/ranger-docker/scripts/audit-dispatcher/ranger-audit-dispatcher-hdfs-site.xml
+++ 
b/dev-support/ranger-docker/scripts/audit-dispatcher/ranger-audit-dispatcher-hdfs-site.xml
@@ -68,7 +68,30 @@
 
     <property>
         <name>ranger.audit.dispatcher.kafka.sasl.mechanism</name>
-        <value>GSSAPI</value>
+        <value>OAUTHBEARER</value>
+        <description>OAUTHBEARER; GSSAPI is also supported and uses 
service.kerberos.principal/keytab</description>
+    </property>
+
+    <property>
+        <name>ranger.audit.dispatcher.kafka.sasl.jaas.option.useKeyTab</name>
+        <value>true</value>
+        <description>GSSAPI Krb5LoginModule option; any option can be set as 
ranger.audit.dispatcher.kafka.sasl.jaas.option.&lt;name&gt;</description>
+    </property>
+
+    <property>
+        <name>ranger.audit.dispatcher.kafka.sasl.jaas.option.storeKey</name>
+        <value>true</value>
+    </property>
+
+    <property>
+        
<name>ranger.audit.dispatcher.kafka.sasl.jaas.option.useTicketCache</name>
+        <value>false</value>
+    </property>
+
+    <property>
+        
<name>ranger.audit.dispatcher.kafka.sasl.oauthbearer.token.endpoint.url</name>
+        <value>file:///etc/kafka-oauth/rangerauditserver.token</value>
+        <description>Dev token minted by the ranger-kafka container; re-read 
on every login so rotation is picked up</description>
     </property>
 
     <!-- HDFS Dispatcher Threading Configuration -->
diff --git 
a/dev-support/ranger-docker/scripts/audit-dispatcher/ranger-audit-dispatcher-opensearch-site.xml
 
b/dev-support/ranger-docker/scripts/audit-dispatcher/ranger-audit-dispatcher-opensearch-site.xml
index 2ef249569..cb985ad34 100644
--- 
a/dev-support/ranger-docker/scripts/audit-dispatcher/ranger-audit-dispatcher-opensearch-site.xml
+++ 
b/dev-support/ranger-docker/scripts/audit-dispatcher/ranger-audit-dispatcher-opensearch-site.xml
@@ -56,7 +56,30 @@
 
     <property>
         <name>ranger.audit.dispatcher.kafka.sasl.mechanism</name>
-        <value>GSSAPI</value>
+        <value>OAUTHBEARER</value>
+        <description>OAUTHBEARER; GSSAPI is also supported and uses 
service.kerberos.principal/keytab</description>
+    </property>
+
+    <property>
+        <name>ranger.audit.dispatcher.kafka.sasl.jaas.option.useKeyTab</name>
+        <value>true</value>
+        <description>GSSAPI Krb5LoginModule option; any option can be set as 
ranger.audit.dispatcher.kafka.sasl.jaas.option.&lt;name&gt;</description>
+    </property>
+
+    <property>
+        <name>ranger.audit.dispatcher.kafka.sasl.jaas.option.storeKey</name>
+        <value>true</value>
+    </property>
+
+    <property>
+        
<name>ranger.audit.dispatcher.kafka.sasl.jaas.option.useTicketCache</name>
+        <value>false</value>
+    </property>
+
+    <property>
+        
<name>ranger.audit.dispatcher.kafka.sasl.oauthbearer.token.endpoint.url</name>
+        <value>file:///etc/kafka-oauth/rangerauditserver.token</value>
+        <description>Dev token minted by the ranger-kafka container; re-read 
on every login so rotation is picked up</description>
     </property>
 
     <property>
diff --git 
a/dev-support/ranger-docker/scripts/audit-dispatcher/ranger-audit-dispatcher-solr-site.xml
 
b/dev-support/ranger-docker/scripts/audit-dispatcher/ranger-audit-dispatcher-solr-site.xml
index 3f19c69a9..078a24419 100644
--- 
a/dev-support/ranger-docker/scripts/audit-dispatcher/ranger-audit-dispatcher-solr-site.xml
+++ 
b/dev-support/ranger-docker/scripts/audit-dispatcher/ranger-audit-dispatcher-solr-site.xml
@@ -68,7 +68,30 @@
 
     <property>
         <name>ranger.audit.dispatcher.kafka.sasl.mechanism</name>
-        <value>GSSAPI</value>
+        <value>OAUTHBEARER</value>
+        <description>OAUTHBEARER; GSSAPI is also supported and uses 
service.kerberos.principal/keytab</description>
+    </property>
+
+    <property>
+        <name>ranger.audit.dispatcher.kafka.sasl.jaas.option.useKeyTab</name>
+        <value>true</value>
+        <description>GSSAPI Krb5LoginModule option; any option can be set as 
ranger.audit.dispatcher.kafka.sasl.jaas.option.&lt;name&gt;</description>
+    </property>
+
+    <property>
+        <name>ranger.audit.dispatcher.kafka.sasl.jaas.option.storeKey</name>
+        <value>true</value>
+    </property>
+
+    <property>
+        
<name>ranger.audit.dispatcher.kafka.sasl.jaas.option.useTicketCache</name>
+        <value>false</value>
+    </property>
+
+    <property>
+        
<name>ranger.audit.dispatcher.kafka.sasl.oauthbearer.token.endpoint.url</name>
+        <value>file:///etc/kafka-oauth/rangerauditserver.token</value>
+        <description>Dev token minted by the ranger-kafka container; re-read 
on every login so rotation is picked up</description>
     </property>
 
     <!-- Solr Dispatcher Threading Configuration -->
diff --git 
a/dev-support/ranger-docker/scripts/audit-ingestor/audit-ingestor-config.py 
b/dev-support/ranger-docker/scripts/audit-ingestor/audit-ingestor-config.py
new file mode 100755
index 000000000..c981eb6f3
--- /dev/null
+++ b/dev-support/ranger-docker/scripts/audit-ingestor/audit-ingestor-config.py
@@ -0,0 +1,105 @@
+#!/usr/bin/env python3
+
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Renders conf/ranger-audit-ingestor-site.xml from 
configs/ranger-audit-ingestor-site.yaml (plus
+# KAFKA_BOOTSTRAP_SERVERS from env), the same way dba.py prepares Ranger 
Admin. A mounted configs/ranger-audit-ingestor-site.xml
+# is copied as it is instead; nothing is rendered over it.
+
+import logging
+import os
+import shutil
+import sys
+import xml.etree.ElementTree as ET
+
+import yaml
+
+AUDIT_INGESTOR_DIR = os.environ.get('AUDIT_INGESTOR_HOME_DIR', 
'/opt/ranger/audit-ingestor')
+CONF_DIR           = os.environ.get('AUDIT_INGESTOR_CONF_DIR', 
os.path.join(AUDIT_INGESTOR_DIR, 'conf'))
+CONFIGS_DIR        = os.environ.get('AUDIT_INGESTOR_CONFIGS', 
os.path.join(AUDIT_INGESTOR_DIR, 'configs'))
+SITE_XML_NAME      = 'ranger-audit-ingestor-site.xml'
+SITE_YAML_NAME     = 'ranger-audit-ingestor-site.yaml'
+PREFIX             = 'ranger.audit.ingestor.'
+
+# env variable -> property
+ENV_OVERRIDES = {
+    'KAFKA_BOOTSTRAP_SERVERS': PREFIX + 'kafka.bootstrap.servers',
+}
+
+logging.basicConfig(level=logging.INFO, format='%(asctime)s %(levelname)s 
audit-ingestor-config: %(message)s')
+LOG = logging.getLogger('audit-ingestor-config')
+
+
+def to_property_value(value):
+    if isinstance(value, bool):
+        return str(value).lower()
+
+    return '' if value is None else str(value)
+
+
+def render_site_xml():
+    site_xml  = os.path.join(CONFIGS_DIR, SITE_XML_NAME)
+    site_yaml = os.path.join(CONFIGS_DIR, SITE_YAML_NAME)
+    target    = os.path.join(CONF_DIR, SITE_XML_NAME)
+
+    if os.path.isfile(site_xml):
+        shutil.copyfile(site_xml, target)
+        LOG.info(f'Using {site_xml} as it is, conf/{SITE_XML_NAME} is not 
rendered from YAML')
+
+        return
+
+    if not os.path.isfile(site_yaml):
+        raise FileNotFoundError(f'configuration not found: neither {site_xml} 
nor {site_yaml} exists')
+
+    with open(site_yaml) as f:
+        config = yaml.safe_load(f) or {}
+
+    if not isinstance(config, dict) or any(isinstance(value, (dict, list)) for 
value in config.values()):
+        raise ValueError(f'{site_yaml} must be a flat map of property names to 
values')
+
+    props = {str(name): to_property_value(value) for name, value in 
config.items()}
+
+    for env_name, prop_name in ENV_OVERRIDES.items():
+        value = os.environ.get(env_name, '').strip()
+
+        if value:
+            props[prop_name] = value
+            LOG.info(f"{prop_name}: '{value}' (from env {env_name})")
+
+    root = ET.Element('configuration')
+
+    root.append(ET.Comment(f' generated by audit-ingestor-config.py from 
{SITE_YAML_NAME} '))
+
+    for name, value in props.items():
+        prop = ET.SubElement(root, 'property')
+
+        ET.SubElement(prop, 'name').text  = name
+        ET.SubElement(prop, 'value').text = value
+
+    ET.indent(root)
+    os.makedirs(CONF_DIR, exist_ok=True)
+    ET.ElementTree(root).write(target, encoding='utf-8', xml_declaration=True)
+
+    LOG.info(f'Rendered {target} with {len(props)} properties from 
{site_yaml}')
+
+
+if __name__ == '__main__':
+    try:
+        render_site_xml()
+    except (OSError, ValueError, ET.ParseError, yaml.YAMLError) as err:
+        LOG.error(f'{err}')
+        sys.exit(1)
diff --git 
a/dev-support/ranger-docker/scripts/audit-ingestor/configs/ranger-audit-ingestor-site.yaml
 
b/dev-support/ranger-docker/scripts/audit-ingestor/configs/ranger-audit-ingestor-site.yaml
new file mode 100644
index 000000000..4a5c3d478
--- /dev/null
+++ 
b/dev-support/ranger-docker/scripts/audit-ingestor/configs/ranger-audit-ingestor-site.yaml
@@ -0,0 +1,130 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Complete Ranger Audit Ingestor configuration for ranger-docker: 
audit-ingestor-config.py renders
+# conf/ranger-audit-ingestor-site.xml from this file on every start, without 
taking any property from
+# the distribution. Mount a configs/ranger-audit-ingestor-site.xml instead to 
bypass the YAML entirely.
+# The renderer takes ranger.audit.ingestor.kafka.bootstrap.servers from env 
KAFKA_BOOTSTRAP_SERVERS when set.
+
+
+# RANGER AUDIT INGESTOR SERVICE CONFIGURATION
+
+# Server Configuration
+log.dir: '${audit.server.log.dir}'
+ranger.audit.ingestor.webapp.dir: webapp/audit-ingestor
+ranger.audit.ingestor.contextName: /
+ranger.audit.ingestor.host: ranger-audit-ingestor.rangernw
+ranger.audit.ingestor.http.port: '7081'
+ranger.audit.ingestor.https.port: '7182'
+ranger.audit.ingestor.https.attrib.ssl.enabled: 'false'
+
+# HTTPS/SSL Configuration
+ranger.audit.ingestor.https.attrib.keystore.keyalias: myKey
+ranger.audit.ingestor.https.attrib.keystore.pass: _
+ranger.audit.ingestor.https.attrib.keystore.file: 
/etc/ranger/ranger-audit-ingestor/keys/server.jks
+ranger.audit.ingestor.https.attrib.keystore.credential.alias: 
keyStoreCredentialAlias
+ranger.audit.ingestor.tomcat.ciphers: ''
+
+# KERBEROS CONFIGURATION FOR REST API (INCOMING REQUESTS) used by kerberos 
filter
+hadoop.security.authentication: kerberos
+ranger.audit.ingestor.kerberos.enabled: 'true'
+ranger.audit.ingestor.kerberos.type: kerberos
+ranger.audit.ingestor.kerberos.principal: 'HTTP/[email protected]'
+ranger.audit.ingestor.kerberos.keytab: /etc/keytabs/HTTP.keytab
+ranger.audit.ingestor.kerberos.name.rules: DEFAULT
+ranger.audit.ingestor.bind.address: ranger-audit-ingestor.rangernw
+ranger.audit.ingestor.cookie.path: /
+
+# JWT AUTHENTICATION CONFIGURATION to support JWT authentication
+config.prefix: ranger.audit.jwt.auth
+ranger.audit.jwt.auth.enabled: 'false'
+ranger.audit.jwt.auth.provider-url: 'http://localhost:9180/rest/jwks'
+ranger.audit.jwt.auth.public-key: ''
+ranger.audit.jwt.auth.cookie.name: hadoop-jwt
+ranger.audit.jwt.auth.audiences: ''
+
+# Trusted Header Auth Configs
+ranger.audit.ingestor.authn.header.enabled: 'false'
+ranger.audit.ingestor.authn.header.username: ''
+ranger.audit.ingestor.authn.header.spiffe: ''
+ranger.audit.ingestor.service.dev_hdfs.allowed.users: hdfs
+ranger.audit.ingestor.service.dev_yarn.allowed.users: yarn
+ranger.audit.ingestor.service.dev_hive.allowed.users: hive
+ranger.audit.ingestor.service.dev_hbase.allowed.users: hbase
+ranger.audit.ingestor.service.dev_kafka.allowed.users: kafka
+ranger.audit.ingestor.service.dev_knox.allowed.users: knox
+ranger.audit.ingestor.service.dev_kms.allowed.users: rangerkms
+ranger.audit.ingestor.service.dev_trino.allowed.users: trino
+ranger.audit.ingestor.service.dev_ozone.allowed.users: om
+ranger.audit.ingestor.service.dev_solr.allowed.users: solr
+
+# AUTH_TO_LOCAL RULES FOR KERBEROS PRINCIPAL MAPPING
+ranger.audit.ingestor.auth.to.local: |
+  RULE:[2:$1/$2@$0]([ndj]n/.*@.*|hdfs/.*@.*)s/.*/hdfs/
+  RULE:[2:$1/$2@$0]([rn]m/.*@.*|yarn/.*@.*)s/.*/yarn/
+  RULE:[2:$1/$2@$0](jhs/.*@.*)s/.*/mapred/
+  RULE:[1:$1@$0](.*@.*)s/@.*//
+  DEFAULT
+ranger.audit.ingestor.service.kerberos.principal: 
'rangerauditserver/[email protected]'
+ranger.audit.ingestor.service.kerberos.keytab: 
/etc/keytabs/rangerauditserver.keytab
+
+# CONFIGURATION TO ENABLE AUDIT DESTINATION CONFIGURATION
+xasecure.audit.destination.kafka: 'true'
+
+# KAFKA PRODUCER CONFIGURATION IN THE INGESTOR
+ranger.audit.ingestor.kafka.bootstrap.servers: 'ranger-kafka.rangernw:9092'
+ranger.audit.ingestor.kafka.topic.name: ranger_audits
+ranger.audit.ingestor.kafka.partitioner.class: 
org.apache.ranger.audit.producer.kafka.AuditPartitioner
+ranger.audit.ingestor.kafka.configured.plugins: 
'hdfs,yarn,knox,hiveServer2,hiveMetastore,kafka,hbaseRegional,hbaseMaster,solr,trino,ozone,kudu,nifi'
+ranger.audit.ingestor.kafka.topic.partitions.per.configured.plugin: '3'
+ranger.audit.ingestor.kafka.topic.partitions.buffer: '9'
+
+# partition allocation overrides for plugins
+ranger.audit.ingestor.kafka.topic.partitions: '10'
+ranger.audit.ingestor.kafka.replication.factor: '1'
+ranger.audit.ingestor.kafka.security.protocol: SASL_PLAINTEXT
+# OAUTHBEARER; GSSAPI is also supported and uses 
service.kerberos.principal/keytab
+ranger.audit.ingestor.kafka.sasl.mechanism: OAUTHBEARER
+# GSSAPI Krb5LoginModule options; any option can be set as 
ranger.audit.ingestor.kafka.sasl.jaas.option.<name>
+ranger.audit.ingestor.kafka.sasl.jaas.option.useKeyTab: 'true'
+ranger.audit.ingestor.kafka.sasl.jaas.option.storeKey: 'true'
+ranger.audit.ingestor.kafka.sasl.jaas.option.useTicketCache: 'false'
+# dev token minted by the ranger-kafka container; re-read on every login so 
rotation is picked up
+ranger.audit.ingestor.kafka.sasl.oauthbearer.token.endpoint.url: 
'file:///etc/kafka-oauth/rangerauditserver.token'
+ranger.audit.ingestor.kafka.request.timeout.ms: '60000'
+
+# KAFKA PRODUCER PERFORMANCE (AuditProducer → ProducerConfig)
+ranger.audit.ingestor.kafka.producer.batch.size: '131072'
+ranger.audit.ingestor.kafka.producer.linger.ms: '20'
+ranger.audit.ingestor.kafka.producer.buffer.memory: '134217728'
+ranger.audit.ingestor.kafka.producer.compression.type: lz4
+ranger.audit.ingestor.kafka.producer.delivery.timeout.ms: '120000'
+ranger.audit.ingestor.kafka.producer.max.request.size: '1048576'
+ranger.audit.ingestor.kafka.producer.max.block.ms: '60000'
+ranger.audit.ingestor.kafka.producer.batch.send.timeout.ms: '30000'
+
+# Optional topic configs at create time (leave unset for broker defaults; 
enable for production RF>=3)
+ranger.audit.ingestor.kafka.connections.max.idle.ms: '90000'
+
+# AUDIT RECOVERY CONFIGURATION (FOR KAFKA SEND FAILURES)
+ranger.audit.ingestor.recovery.enabled: 'true'
+ranger.audit.ingestor.recovery.spool.dir: 
/var/log/ranger/ranger-audit-ingestor/audit/spool
+ranger.audit.ingestor.recovery.archive.dir: 
/var/log/ranger/ranger-audit-ingestor/audit/archive
+ranger.audit.ingestor.recovery.file.rotation.interval.sec: '300'
+ranger.audit.ingestor.recovery.max.messages.per.file: '10000'
+ranger.audit.ingestor.recovery.retry.interval.sec: '60'
+ranger.audit.ingestor.recovery.archive.max.processed.files: '100'
+ranger.audit.ingestor.recovery.retry.max.attempts: '3'
diff --git 
a/dev-support/ranger-docker/scripts/audit-ingestor/ranger-audit-ingestor.sh 
b/dev-support/ranger-docker/scripts/audit-ingestor/ranger-audit-ingestor.sh
index 295358404..36eaacecf 100755
--- a/dev-support/ranger-docker/scripts/audit-ingestor/ranger-audit-ingestor.sh
+++ b/dev-support/ranger-docker/scripts/audit-ingestor/ranger-audit-ingestor.sh
@@ -38,6 +38,13 @@ echo "=========================================="
 # Source service check functions
 source /home/ranger/scripts/service-check-functions.sh
 
+# Render conf/ranger-audit-ingestor-site.xml from 
configs/ranger-audit-ingestor-site.yaml
+# (KAFKA_BOOTSTRAP_SERVERS from env); a mounted 
configs/ranger-audit-ingestor-site.xml wins
+if ! python3 /home/ranger/scripts/audit-ingestor-config.py; then
+  echo "[ERROR] Audit ingestor configuration failed, see 
audit-ingestor-config.py logs above"
+  exit 1
+fi
+
 # Quick check for Kafka availability
 # The audit server has a built-in recovery/spool mechanism for when Kafka is 
unavailable
 KAFKA_BOOTSTRAP_SERVERS="${KAFKA_BOOTSTRAP_SERVERS:-ranger-kafka:9092}"
diff --git 
a/dev-support/ranger-docker/scripts/kafka/kafka-oauth-token-minter.sh 
b/dev-support/ranger-docker/scripts/kafka/kafka-oauth-token-minter.sh
new file mode 100755
index 000000000..8ba0a8b42
--- /dev/null
+++ b/dev-support/ranger-docker/scripts/kafka/kafka-oauth-token-minter.sh
@@ -0,0 +1,65 @@
+#!/bin/bash
+
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Dev-only stand-in for a platform token issuer (e.g. the kubelet writing 
projected
+# service-account tokens). Mints unsigned (alg=none) JWTs for the Ranger Kafka 
clients
+# into a volume shared with their containers and rewrites them in place on a 
loop, so
+# the SASL/OAUTHBEARER login handler's re-read-on-refresh path is exercised. 
The broker
+# accepts them through Kafka's OAuthBearerUnsecuredValidatorCallbackHandler; 
never use
+# this outside the docker dev setup.
+
+set -u
+
+TOKEN_DIR="${KAFKA_OAUTH_TOKEN_DIR:-/etc/kafka-oauth}"
+TOKEN_TTL="${KAFKA_OAUTH_TOKEN_TTL_SECONDS:-600}"
+REFRESH="${KAFKA_OAUTH_TOKEN_REFRESH_SECONDS:-120}"
+SUBJECTS="${KAFKA_OAUTH_TOKEN_SUBJECTS:-rangerauditserver rangertagsync}"
+ISSUER="${KAFKA_OAUTH_TOKEN_ISSUER:-ranger-docker}"
+
+b64url() {
+  base64 -w0 | tr '+/' '-_' | tr -d '='
+}
+
+mint_token() {
+  local subject="$1"
+  local file="${TOKEN_DIR}/${subject}.token"
+  local now exp header payload
+
+  now=$(date +%s)
+  exp=$((now + TOKEN_TTL))
+  header=$(printf '{"alg":"none","typ":"JWT"}' | b64url)
+  payload=$(printf '{"iss":"%s","sub":"%s","aud":"kafka","iat":%d,"exp":%d}' 
"${ISSUER}" "${subject}" "${now}" "${exp}" | b64url)
+
+  # same atomic replace kubelet does: write a temp file, then rename over the 
old token
+  printf '%s.%s.' "${header}" "${payload}" > "${file}.tmp"
+  chmod 644 "${file}.tmp"
+  mv -f "${file}.tmp" "${file}"
+}
+
+mkdir -p "${TOKEN_DIR}"
+chmod 755 "${TOKEN_DIR}"
+
+echo "[INFO] kafka-oauth-token-minter: writing tokens for [${SUBJECTS}] to 
${TOKEN_DIR} (ttl=${TOKEN_TTL}s, refresh every ${REFRESH}s)"
+
+while true; do
+  for subject in ${SUBJECTS}; do
+    mint_token "${subject}"
+  done
+
+  sleep "${REFRESH}"
+done
diff --git a/dev-support/ranger-docker/scripts/kafka/ranger-kafka-setup.sh 
b/dev-support/ranger-docker/scripts/kafka/ranger-kafka-setup.sh
index 067042ada..f87bf508b 100755
--- a/dev-support/ranger-docker/scripts/kafka/ranger-kafka-setup.sh
+++ b/dev-support/ranger-docker/scripts/kafka/ranger-kafka-setup.sh
@@ -43,8 +43,12 @@ if [ "${KERBEROS_ENABLED}" == "true" ]; then
   echo "Configuring Kafka with Kerberos (SASL_PLAINTEXT)"
   cat <<EOF >> ${KAFKA_HOME}/config/server.properties
 
-    # Enable SASL/GSSAPI mechanism
-    sasl.enabled.mechanisms=GSSAPI
+    # SASL/GSSAPI for inter-broker, the kafka CLI/healthcheck and clients that 
opt in; SASL/OAUTHBEARER
+    # for the Ranger clients (default). OAUTHBEARER tokens are dev tokens from 
kafka-oauth-token-minter.sh,
+    # accepted unsigned by Kafka's unsecured validator; the token's "sub" 
becomes the Kafka principal.
+    sasl.enabled.mechanisms=GSSAPI,OAUTHBEARER
+    
listener.name.sasl_plaintext.oauthbearer.sasl.jaas.config=org.apache.kafka.common.security.oauthbearer.OAuthBearerLoginModule
 required unsecuredLoginStringClaim_sub="kafka";
+    
listener.name.sasl_plaintext.oauthbearer.sasl.server.callback.handler.class=org.apache.kafka.common.security.oauthbearer.internals.unsecured.OAuthBearerUnsecuredValidatorCallbackHandler
     sasl.mechanism.inter.broker.protocol=GSSAPI
     security.inter.broker.protocol=SASL_PLAINTEXT
 
diff --git a/dev-support/ranger-docker/scripts/kafka/ranger-kafka.sh 
b/dev-support/ranger-docker/scripts/kafka/ranger-kafka.sh
index 1e22101c3..0e1d862cf 100755
--- a/dev-support/ranger-docker/scripts/kafka/ranger-kafka.sh
+++ b/dev-support/ranger-docker/scripts/kafka/ranger-kafka.sh
@@ -45,6 +45,8 @@ fi
 # Configure KAFKA_OPTS based on KERBEROS_ENABLED
 if [ "${KERBEROS_ENABLED}" == "true" ]; then
   echo "Starting Kafka with Kerberos authentication (SASL_PLAINTEXT)"
+  # dev token issuer for the SASL/OAUTHBEARER clients (audit ingestor, 
dispatchers, tagsync)
+  "${RANGER_SCRIPTS}"/kafka-oauth-token-minter.sh &
   KAFKA_OPTS='-Djava.security.krb5.conf=/etc/krb5.conf 
-Djava.security.auth.login.config=/opt/kafka/config/kafka-server-jaas.conf'
 else
   echo "Starting Kafka with PLAINTEXT (no Kerberos authentication)"
diff --git 
a/dev-support/ranger-docker/scripts/tagsync/ranger-tagsync-install.properties 
b/dev-support/ranger-docker/scripts/tagsync/ranger-tagsync-install.properties
index 14101965d..576dc05f4 100755
--- 
a/dev-support/ranger-docker/scripts/tagsync/ranger-tagsync-install.properties
+++ 
b/dev-support/ranger-docker/scripts/tagsync/ranger-tagsync-install.properties
@@ -33,6 +33,9 @@ TAG_SOURCE_ATLAS_KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR = 1
 TAG_SOURCE_ATLAS_KAFKA_BOOTSTRAP_SERVERS = ranger-kafka.rangernw:9092
 TAG_SOURCE_ATLAS_KAFKA_ENTITIES_GROUP_ID = ranger_entities_consumer
 TAG_SOURCE_ATLAS_KAFKA_SECURITY_PROTOCOL = SASL_PLAINTEXT
+# supported values: OAUTHBEARER (dev token minted by the ranger-kafka 
container) and GSSAPI (the keytab below)
+TAG_SOURCE_ATLAS_KAFKA_SASL_MECHANISM = OAUTHBEARER
+TAG_SOURCE_ATLAS_KAFKA_OAUTH_TOKEN_URL = 
file:///etc/kafka-oauth/rangertagsync.token
 TAG_SOURCE_ATLAS_KAFKA_SERVICE_NAME = kafka
 TAG_SOURCE_ATLAS_KERBEROS_PRINCIPAL = 
rangertagsync/[email protected]
 TAG_SOURCE_ATLAS_KERBEROS_KEYTAB = /etc/keytabs/rangertagsync.keytab
diff --git a/dev-support/ranger-docker/scripts/tagsync/ranger-tagsync.sh 
b/dev-support/ranger-docker/scripts/tagsync/ranger-tagsync.sh
index dcb01cb54..533476718 100755
--- a/dev-support/ranger-docker/scripts/tagsync/ranger-tagsync.sh
+++ b/dev-support/ranger-docker/scripts/tagsync/ranger-tagsync.sh
@@ -45,6 +45,18 @@ then
   fi
 fi
 
+# Atlas Kafka consumer authenticates with the dev SASL/OAUTHBEARER token 
minted by ranger-kafka unless
+# TAG_SOURCE_ATLAS_KAFKA_SASL_MECHANISM selects GSSAPI; give the minter a 
moment on a fresh stack
+if [ "${KERBEROS_ENABLED}" == "true" ] && grep -qiE '^TAG_SOURCE_ATLAS_ENABLED 
*= *true' ${RANGER_HOME}/tagsync/install.properties && ! grep -qiE 
'^TAG_SOURCE_ATLAS_KAFKA_SASL_MECHANISM *= *GSSAPI' 
${RANGER_HOME}/tagsync/install.properties
+then
+  for i in $(seq 1 30)
+  do
+    [ -s /etc/kafka-oauth/rangertagsync.token ] && break
+    echo "Waiting for Kafka OAUTHBEARER token 
/etc/kafka-oauth/rangertagsync.token ($i/30)"
+    sleep 2
+  done
+fi
+
 cd ${RANGER_HOME}/tagsync && ./ranger-tagsync-services.sh start
 
 RANGER_TAGSYNC_PID=`ps -ef  | grep -v grep | grep -i 
"org.apache.ranger.tagsync.process.TagSynchronizer" | awk '{print $2}'`
diff --git a/distro/src/main/assembly/tagsync.xml 
b/distro/src/main/assembly/tagsync.xml
index 59d878747..dd59f051c 100644
--- a/distro/src/main/assembly/tagsync.xml
+++ b/distro/src/main/assembly/tagsync.xml
@@ -58,6 +58,7 @@
                                                        
<include>org.apache.ranger:credentialbuilder</include>
                                                        
<include>org.apache.ranger:ranger-authz-api</include>
                                                        
<include>org.apache.ranger:ranger-common-utils</include>
+                                                       
<include>org.apache.ranger:ranger-kafka-client-auth</include>
                                                        
<include>org.apache.ranger:ranger-plugins-cred</include>
                                                        
<include>org.apache.ranger:ranger-plugins-common</include>
                                                        
<include>org.apache.ranger:ugsync-util</include>
diff --git a/pom.xml b/pom.xml
index e7b33689c..fe52c4f65 100755
--- a/pom.xml
+++ b/pom.xml
@@ -857,6 +857,7 @@
                 <module>ranger-hbase-plugin-shim</module>
                 <module>ranger-hdfs-plugin-shim</module>
                 <module>ranger-hive-plugin-shim</module>
+                <module>ranger-kafka-client-auth</module>
                 <module>ranger-kafka-plugin-shim</module>
                 <module>ranger-kms-plugin-shim</module>
                 <module>ranger-knox-plugin-shim</module>
@@ -1015,6 +1016,7 @@
                 <module>common-utils</module>
                 <module>credentialbuilder</module>
                 <module>plugin-kafka</module>
+                <module>ranger-kafka-client-auth</module>
                 <module>ranger-kafka-plugin-shim</module>
                 <module>ranger-plugin-classloader</module>
                 <module>ranger-util</module>
@@ -1208,6 +1210,7 @@
                 <module>ranger-hbase-plugin-shim</module>
                 <module>ranger-hdfs-plugin-shim</module>
                 <module>ranger-hive-plugin-shim</module>
+                <module>ranger-kafka-client-auth</module>
                 <module>ranger-kafka-plugin-shim</module>
                 <module>ranger-kms-plugin-shim</module>
                 <module>ranger-knox-plugin-shim</module>
@@ -1296,6 +1299,7 @@
                 <module>ranger-hbase-plugin-shim</module>
                 <module>ranger-hdfs-plugin-shim</module>
                 <module>ranger-hive-plugin-shim</module>
+                <module>ranger-kafka-client-auth</module>
                 <module>ranger-kafka-plugin-shim</module>
                 <module>ranger-kms-plugin-shim</module>
                 <module>ranger-knox-plugin-shim</module>
diff --git a/audit-server/audit-common/pom.xml 
b/ranger-kafka-client-auth/pom.xml
similarity index 69%
copy from audit-server/audit-common/pom.xml
copy to ranger-kafka-client-auth/pom.xml
index d9a5cd0b4..40f53e540 100644
--- a/audit-server/audit-common/pom.xml
+++ b/ranger-kafka-client-auth/pom.xml
@@ -17,35 +17,43 @@
 -->
 <project xmlns="http://maven.apache.org/POM/4.0.0"; 
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"; 
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
http://maven.apache.org/xsd/maven-4.0.0.xsd";>
     <modelVersion>4.0.0</modelVersion>
-
     <parent>
         <groupId>org.apache.ranger</groupId>
         <artifactId>ranger</artifactId>
         <version>2.10.0-SNAPSHOT</version>
-        <relativePath>../..</relativePath>
+        <relativePath>..</relativePath>
     </parent>
-
-    <artifactId>ranger-audit-server-common</artifactId>
+    <artifactId>ranger-kafka-client-auth</artifactId>
     <packaging>jar</packaging>
-    <name>Ranger Audit Server Common</name>
-    <description>Shared classes between audit ingestor and 
dispatcher</description>
-
+    <name>Ranger Kafka Client Auth</name>
+    <description>Shared Kafka client authentication (SASL/GSSAPI, 
SASL/OAUTHBEARER) for Ranger services that produce to or consume from 
Kafka</description>
+    <properties>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+    </properties>
     <dependencies>
         <dependency>
-            <groupId>org.apache.kafka</groupId>
-            <artifactId>kafka-clients</artifactId>
-            <version>${kafka.version}</version>
+            <groupId>com.fasterxml.jackson.core</groupId>
+            <artifactId>jackson-databind</artifactId>
+            <version>${fasterxml.jackson.databind.version}</version>
         </dependency>
         <dependency>
-            <groupId>org.apache.ranger</groupId>
-            <artifactId>ranger-plugins-common</artifactId>
-            <version>${project.version}</version>
+            <groupId>org.apache.commons</groupId>
+            <artifactId>commons-lang3</artifactId>
+            <version>${commons.lang3.version}</version>
         </dependency>
         <dependency>
             <groupId>org.slf4j</groupId>
             <artifactId>slf4j-api</artifactId>
             <version>${slf4j.version}</version>
         </dependency>
+        <dependency>
+            <groupId>org.apache.kafka</groupId>
+            <artifactId>kafka-clients</artifactId>
+            <version>${kafka.version}</version>
+            <scope>provided</scope>
+        </dependency>
+
+        <!-- Test -->
         <dependency>
             <groupId>org.junit.jupiter</groupId>
             <artifactId>junit-jupiter</artifactId>
@@ -53,8 +61,4 @@
             <scope>test</scope>
         </dependency>
     </dependencies>
-
-    <build>
-        <finalName>ranger-audit-server-common-${project.version}</finalName>
-    </build>
-</project>
\ No newline at end of file
+</project>
diff --git 
a/ranger-kafka-client-auth/src/main/java/org/apache/ranger/kafka/auth/RangerKafkaClientSecurityConfig.java
 
b/ranger-kafka-client-auth/src/main/java/org/apache/ranger/kafka/auth/RangerKafkaClientSecurityConfig.java
new file mode 100644
index 000000000..1e74fbecc
--- /dev/null
+++ 
b/ranger-kafka-client-auth/src/main/java/org/apache/ranger/kafka/auth/RangerKafkaClientSecurityConfig.java
@@ -0,0 +1,320 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.ranger.kafka.auth;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.kafka.clients.CommonClientConfigs;
+import org.apache.kafka.common.config.SaslConfigs;
+import 
org.apache.kafka.common.security.oauthbearer.OAuthBearerLoginCallbackHandler;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.URI;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Properties;
+import java.util.TreeMap;
+import java.util.stream.Collectors;
+
+/**
+ * Builds the security portion of a Kafka client configuration from 
Ranger-style properties.
+ * <p>
+ * All keys are read relative to a caller-supplied prefix such as {@code 
ranger.audit.ingestor}:
+ * <ul>
+ *   <li>{@code <prefix>.kafka.security.protocol}: PLAINTEXT (default), SSL, 
SASL_PLAINTEXT or SASL_SSL</li>
+ *   <li>{@code <prefix>.kafka.sasl.mechanism}: GSSAPI (default), OAUTHBEARER 
or any other Kafka mechanism</li>
+ *   <li>{@code <prefix>.service.kerberos.principal}: GSSAPI principal, _HOST 
is resolved</li>
+ *   <li>{@code <prefix>.service.kerberos.keytab}: GSSAPI keytab path</li>
+ *   <li>{@code <prefix>.kafka.sasl.oauthbearer.token.endpoint.url}: 
OAUTHBEARER token, file:// or http(s)://</li>
+ *   <li>{@code <prefix>.kafka.sasl.jaas.loginModuleName}: JAAS login module, 
defaults per mechanism</li>
+ *   <li>{@code <prefix>.kafka.sasl.jaas.loginModuleControlFlag}: JAAS control 
flag, default required</li>
+ *   <li>{@code <prefix>.kafka.sasl.jaas.option.<name>}: any JAAS login module 
option (for GSSAPI the shipped configuration sets useKeyTab, storeKey, 
useTicketCache)</li>
+ *   <li>{@code <prefix>.kafka.sasl.*} and {@code <prefix>.kafka.ssl.*}: 
passed through, overriding derived values</li>
+ * </ul>
+ * A file:// token URL selects {@link 
RangerKafkaOAuthBearerLoginCallbackHandler}, an http(s):// URL Kafka's own
+ * client-credentials handler. Producers, consumers and admin clients all 
accept the resulting keys.
+ */
+public final class RangerKafkaClientSecurityConfig {
+    private static final Logger LOG                            = 
LoggerFactory.getLogger(RangerKafkaClientSecurityConfig.class);
+
+    public static final String PROP_SECURITY_PROTOCOL          = 
"kafka.security.protocol";
+    public static final String PROP_SASL_MECHANISM             = 
"kafka.sasl.mechanism";
+    public static final String PROP_SASL_KERBEROS_SERVICE_NAME = 
"kafka.sasl.kerberos.service.name";
+    public static final String PROP_OAUTH_TOKEN_ENDPOINT_URL   = 
"kafka.sasl.oauthbearer.token.endpoint.url";
+    public static final String PROP_JAAS_LOGIN_MODULE_NAME     = 
"kafka.sasl.jaas.loginModuleName";
+    public static final String PROP_JAAS_CONTROL_FLAG          = 
"kafka.sasl.jaas.loginModuleControlFlag";
+    public static final String PROP_JAAS_OPTION_PREFIX         = 
"kafka.sasl.jaas.option.";
+    public static final String PROP_KERBEROS_PRINCIPAL         = 
"service.kerberos.principal";
+    public static final String PROP_KERBEROS_KEYTAB            = 
"service.kerberos.keytab";
+    public static final String PROP_HOST                       = "host";
+
+    public static final String SECURITY_PROTOCOL_PLAINTEXT     = "PLAINTEXT";
+    public static final String SASL_MECHANISM_GSSAPI           = "GSSAPI";
+    public static final String SASL_MECHANISM_OAUTHBEARER      = "OAUTHBEARER";
+    public static final String DEFAULT_SASL_MECHANISM          = 
SASL_MECHANISM_GSSAPI;
+    public static final String DEFAULT_JAAS_CONTROL_FLAG       = "required";
+    public static final String DEFAULT_KERBEROS_SERVICE_NAME   = "kafka";
+    public static final String HOSTNAME_MARKER                 = "_HOST";
+
+    public static final String OAUTHBEARER_LOGIN_MODULE        = 
"org.apache.kafka.common.security.oauthbearer.OAuthBearerLoginModule";
+    public static final String KRB5_LOGIN_MODULE               = 
"com.sun.security.auth.module.Krb5LoginModule";
+
+    public static final String JAAS_OPTION_USE_KEYTAB          = "useKeyTab";
+    public static final String JAAS_OPTION_KEYTAB              = "keyTab";
+    public static final String JAAS_OPTION_SERVICE_NAME        = "serviceName";
+    public static final String JAAS_OPTION_PRINCIPAL           = "principal";
+
+    private static final String SASL_PROTOCOL_MARKER           = "SASL";
+    private static final String SASL_PASSTHROUGH               = "kafka.sasl.";
+    private static final String SSL_PASSTHROUGH                = "kafka.ssl.";
+    private static final String JAAS_RANGER_PREFIX             = 
"kafka.sasl.jaas.";
+    private static final String KAFKA_PREFIX                   = "kafka.";
+    private static final String SENSITIVE_KEY_PART             = "password";
+    private static final String MASKED_VALUE                   = "***";
+    private static final String PROPERTY_NAME_FORMAT           = "%s.%s";
+    private static final String KAFKA_PROPERTY_NAME_FORMAT     = "%s.kafka.%s";
+    private static final String PRINCIPAL_FORMAT               = "%s/%s@%s";
+
+    private RangerKafkaClientSecurityConfig() {
+    }
+
+    /** Returns a fresh map holding only the security keys derived from {@code 
props}. */
+    public static Map<String, Object> build(Properties props, String 
propPrefix) {
+        Map<String, Object> ret = new HashMap<>();
+
+        apply(props, propPrefix, ret);
+
+        return ret;
+    }
+
+    /** Adds the security keys derived from {@code props} to an existing 
client configuration. */
+    public static void apply(Properties props, String propPrefix, Map<? super 
String, ? super Object> target) {
+        Map<String, Object> security = new HashMap<>();
+        String              protocol = getProperty(props, propPrefix, 
PROP_SECURITY_PROTOCOL, SECURITY_PROTOCOL_PLAINTEXT);
+
+        security.put(CommonClientConfigs.SECURITY_PROTOCOL_CONFIG, protocol);
+
+        if (isSasl(protocol)) {
+            String mechanism = getProperty(props, propPrefix, 
PROP_SASL_MECHANISM, DEFAULT_SASL_MECHANISM);
+
+            security.put(SaslConfigs.SASL_MECHANISM, mechanism);
+
+            if (SASL_MECHANISM_GSSAPI.equalsIgnoreCase(mechanism)) {
+                applyKerberos(props, propPrefix, security);
+            } else if (SASL_MECHANISM_OAUTHBEARER.equalsIgnoreCase(mechanism)) 
{
+                applyOAuthBearer(props, propPrefix, security);
+            } else {
+                applyConfiguredJaas(props, propPrefix, security, null);
+            }
+        }
+
+        applyPassThrough(props, propPrefix, security);
+
+        if (isSasl(protocol) && 
!security.containsKey(SaslConfigs.SASL_JAAS_CONFIG)) {
+            throw new IllegalStateException(String.format("Kafka security 
protocol %s with mechanism %s requires %s or %s", protocol, 
security.get(SaslConfigs.SASL_MECHANISM),
+                    propertyName(propPrefix, PROP_JAAS_LOGIN_MODULE_NAME), 
String.format(KAFKA_PROPERTY_NAME_FORMAT, propPrefix, 
SaslConfigs.SASL_JAAS_CONFIG)));
+        }
+
+        target.putAll(security);
+
+        logSecurityConfig(propPrefix, security);
+    }
+
+    public static boolean isSasl(String securityProtocol) {
+        return StringUtils.startsWithIgnoreCase(securityProtocol, 
SASL_PROTOCOL_MARKER);
+    }
+
+    /* Resolves _HOST in a Kerberos principal, falling back to the local 
canonical host name. */
+    static String resolvePrincipal(String principal, String hostName) {
+        String[] components = principal.split("[/@]");
+
+        if (components.length != 3 || !HOSTNAME_MARKER.equals(components[1])) {
+            return principal;
+        }
+
+        String fqdn = hostName;
+
+        if (StringUtils.isBlank(fqdn) || "0.0.0.0".equals(fqdn)) {
+            try {
+                fqdn = InetAddress.getLocalHost().getCanonicalHostName();
+            } catch (IOException e) {
+                throw new IllegalStateException(String.format("Unable to 
resolve %s in principal %s", HOSTNAME_MARKER, principal), e);
+            }
+        }
+
+        return String.format(PRINCIPAL_FORMAT, components[0], 
fqdn.toLowerCase(Locale.ROOT), components[2]);
+    }
+
+    /* Krb5LoginModule whose options are all configuration driven; the shipped 
site XML carries
+     * useKeyTab/storeKey/useTicketCache, so ticket-cache or other setups need 
no code change. */
+    private static void applyKerberos(Properties props, String propPrefix, 
Map<String, Object> security) {
+        String              principal   = getProperty(props, propPrefix, 
PROP_KERBEROS_PRINCIPAL, null);
+        String              keytab      = getProperty(props, propPrefix, 
PROP_KERBEROS_KEYTAB, null);
+        String              hostName    = getProperty(props, propPrefix, 
PROP_HOST, null);
+        String              serviceName = getProperty(props, propPrefix, 
PROP_SASL_KERBEROS_SERVICE_NAME, DEFAULT_KERBEROS_SERVICE_NAME);
+        Map<String, String> options     = new LinkedHashMap<>();
+
+        /* keyTab, serviceName and principal come from the service properties; 
every other Krb5LoginModule option (useKeyTab, storeKey, useTicketCache, ...) 
comes from <prefix>.kafka.sasl.jaas.option.* */
+        if (keytab != null) {
+            options.put(JAAS_OPTION_KEYTAB, keytab);
+        }
+
+        options.put(JAAS_OPTION_SERVICE_NAME, serviceName);
+
+        if (principal != null) {
+            options.put(JAAS_OPTION_PRINCIPAL, principal);
+        }
+
+        options.putAll(configuredJaasOptions(props, propPrefix));
+
+        if (options.containsKey(JAAS_OPTION_PRINCIPAL)) {
+            options.put(JAAS_OPTION_PRINCIPAL, 
resolvePrincipal(options.get(JAAS_OPTION_PRINCIPAL), hostName));
+        }
+
+        if (Boolean.parseBoolean(options.get(JAAS_OPTION_USE_KEYTAB))) {
+            /* keytab login was asked for by configuration, so fail early on a 
missing or unreadable keytab */
+            String keytabPath = options.get(JAAS_OPTION_KEYTAB);
+
+            if (keytabPath == null || 
!options.containsKey(JAAS_OPTION_PRINCIPAL)) {
+                throw new IllegalStateException(String.format("Kafka 
SASL/GSSAPI with %s=true requires both %s and %s", JAAS_OPTION_USE_KEYTAB, 
propertyName(propPrefix, PROP_KERBEROS_PRINCIPAL), propertyName(propPrefix, 
PROP_KERBEROS_KEYTAB)));
+            }
+
+            File keytabFile = new File(keytabPath);
+
+            if (!keytabFile.isFile()) {
+                throw new IllegalStateException(String.format("Keytab file not 
found: %s", keytabPath));
+            }
+
+            if (!keytabFile.canRead()) {
+                throw new IllegalStateException(String.format("Keytab file not 
readable: %s", keytabPath));
+            }
+        }
+
+        security.put(SaslConfigs.SASL_KERBEROS_SERVICE_NAME, serviceName);
+        security.put(SaslConfigs.SASL_JAAS_CONFIG, buildJaasConfig(props, 
propPrefix, KRB5_LOGIN_MODULE, options));
+    }
+
+    private static void applyOAuthBearer(Properties props, String propPrefix, 
Map<String, Object> security) {
+        String url = getProperty(props, propPrefix, 
PROP_OAUTH_TOKEN_ENDPOINT_URL, null);
+
+        if (url == null) {
+            throw new IllegalStateException(String.format("Kafka 
SASL/OAUTHBEARER requires %s (file:// token or http(s):// token endpoint)", 
propertyName(propPrefix, PROP_OAUTH_TOKEN_ENDPOINT_URL)));
+        }
+
+        /* file:// tokens rotate in place, so use the Ranger handler that 
re-reads them; http(s):// endpoints go through Kafka's own client-credentials 
handler. */
+        String handler = isFileUrl(url) ? 
RangerKafkaOAuthBearerLoginCallbackHandler.class.getName() : 
OAuthBearerLoginCallbackHandler.class.getName();
+
+        security.put(SaslConfigs.SASL_OAUTHBEARER_TOKEN_ENDPOINT_URL, url);
+        security.put(SaslConfigs.SASL_LOGIN_CALLBACK_HANDLER_CLASS, handler);
+
+        applyConfiguredJaas(props, propPrefix, security, 
OAUTHBEARER_LOGIN_MODULE);
+    }
+
+    /* JAAS from <prefix>.kafka.sasl.jaas.* with an optional default login 
module; nothing is set when neither is available, leaving the mechanism to a 
passed-through sasl.jaas.config. */
+    private static void applyConfiguredJaas(Properties props, String 
propPrefix, Map<String, Object> security, String defaultLoginModule) {
+        String loginModule = getProperty(props, propPrefix, 
PROP_JAAS_LOGIN_MODULE_NAME, defaultLoginModule);
+
+        if (loginModule != null) {
+            security.put(SaslConfigs.SASL_JAAS_CONFIG, buildJaasConfig(props, 
propPrefix, loginModule, configuredJaasOptions(props, propPrefix)));
+        }
+    }
+
+    private static String buildJaasConfig(Properties props, String propPrefix, 
String defaultLoginModule, Map<String, String> options) {
+        String loginModule = getProperty(props, propPrefix, 
PROP_JAAS_LOGIN_MODULE_NAME, defaultLoginModule);
+        String controlFlag = getProperty(props, propPrefix, 
PROP_JAAS_CONTROL_FLAG, DEFAULT_JAAS_CONTROL_FLAG);
+        String optionText  = options.entrySet().stream()
+                .map(option -> String.format(" %s=\"%s\"", option.getKey(), 
option.getValue()))
+                .collect(Collectors.joining());
+
+        return String.format("%s %s%s;", loginModule, controlFlag, optionText);
+    }
+
+    /* sorted so the generated JAAS string is stable across runs */
+    private static Map<String, String> configuredJaasOptions(Properties props, 
String propPrefix) {
+        Map<String, String> ret          = new TreeMap<>();
+        String              optionPrefix = propertyName(propPrefix, 
PROP_JAAS_OPTION_PREFIX);
+
+        for (String name : props.stringPropertyNames()) {
+            if (name.startsWith(optionPrefix)) {
+                String value = props.getProperty(name);
+
+                if (StringUtils.isNotBlank(value)) {
+                    ret.put(name.substring(optionPrefix.length()), value);
+                }
+            }
+        }
+
+        return ret;
+    }
+
+    private static void applyPassThrough(Properties props, String propPrefix, 
Map<String, Object> security) {
+        String saslPrefix = propertyName(propPrefix, SASL_PASSTHROUGH);
+        String sslPrefix  = propertyName(propPrefix, SSL_PASSTHROUGH);
+        String jaasPrefix = propertyName(propPrefix, JAAS_RANGER_PREFIX);
+        String jaasConfig = String.format(KAFKA_PROPERTY_NAME_FORMAT, 
propPrefix, SaslConfigs.SASL_JAAS_CONFIG);
+
+        for (String name : props.stringPropertyNames()) {
+            boolean rangerOnlyJaasKey = name.startsWith(jaasPrefix) && 
!name.equals(jaasConfig);
+
+            if ((name.startsWith(saslPrefix) || name.startsWith(sslPrefix)) && 
!rangerOnlyJaasKey) {
+                String value = props.getProperty(name);
+
+                if (StringUtils.isNotBlank(value)) {
+                    security.put(name.substring(propPrefix.length() + 1 + 
KAFKA_PREFIX.length()), value);
+                }
+            }
+        }
+    }
+
+    private static boolean isFileUrl(String url) {
+        try {
+            return "file".equalsIgnoreCase(URI.create(url).getScheme());
+        } catch (IllegalArgumentException e) {
+            return false;
+        }
+    }
+
+    private static String getProperty(Properties props, String propPrefix, 
String name, String defaultValue) {
+        String value = props.getProperty(propertyName(propPrefix, name));
+
+        return StringUtils.isBlank(value) ? defaultValue : value;
+    }
+
+    private static String propertyName(String propPrefix, String name) {
+        return String.format(PROPERTY_NAME_FORMAT, propPrefix, name);
+    }
+
+    private static void logSecurityConfig(String propPrefix, Map<String, 
Object> security) {
+        String summary = security.entrySet().stream()
+                .map(entry -> String.format("%s=%s", entry.getKey(), 
isSensitive(entry.getKey()) ? MASKED_VALUE : entry.getValue()))
+                .collect(Collectors.joining(" "));
+
+        LOG.info("Kafka client security for {}: {}", propPrefix, summary);
+    }
+
+    private static boolean isSensitive(String key) {
+        return SaslConfigs.SASL_JAAS_CONFIG.equals(key) || 
StringUtils.containsIgnoreCase(key, SENSITIVE_KEY_PART);
+    }
+}
diff --git 
a/ranger-kafka-client-auth/src/main/java/org/apache/ranger/kafka/auth/RangerKafkaOAuthBearerLoginCallbackHandler.java
 
b/ranger-kafka-client-auth/src/main/java/org/apache/ranger/kafka/auth/RangerKafkaOAuthBearerLoginCallbackHandler.java
new file mode 100644
index 000000000..7bdce8cff
--- /dev/null
+++ 
b/ranger-kafka-client-auth/src/main/java/org/apache/ranger/kafka/auth/RangerKafkaOAuthBearerLoginCallbackHandler.java
@@ -0,0 +1,225 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.ranger.kafka.auth;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.kafka.common.config.ConfigException;
+import org.apache.kafka.common.config.SaslConfigs;
+import org.apache.kafka.common.security.auth.AuthenticateCallbackHandler;
+import org.apache.kafka.common.security.auth.SaslExtensions;
+import org.apache.kafka.common.security.auth.SaslExtensionsCallback;
+import org.apache.kafka.common.security.oauthbearer.OAuthBearerTokenCallback;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.security.auth.callback.Callback;
+import javax.security.auth.callback.UnsupportedCallbackException;
+import javax.security.auth.login.AppConfigurationEntry;
+
+import java.io.IOException;
+import java.net.URI;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Arrays;
+import java.util.Base64;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * SASL/OAUTHBEARER login callback handler that presents a JWT read from a 
local file, such as a
+ * Kubernetes projected service-account token, to the Kafka broker.
+ * <p>
+ * Kafka's bundled {@code OAuthBearerLoginCallbackHandler} also accepts a 
{@code file://} value for
+ * {@code sasl.oauthbearer.token.endpoint.url}, but it reads the file once and 
caches the token, so a
+ * client fails to re-authenticate after the platform rotates the token in 
place. This handler never
+ * caches: every login and every refresh re-reads the file and re-derives 
{@code sub}, {@code exp}
+ * and {@code iat}, so Kafka's refresh scheduling always follows the current 
token. Signature and
+ * audience validation are the broker's responsibility.
+ */
+public class RangerKafkaOAuthBearerLoginCallbackHandler implements 
AuthenticateCallbackHandler, AutoCloseable {
+    private static final Logger       LOG                          = 
LoggerFactory.getLogger(RangerKafkaOAuthBearerLoginCallbackHandler.class);
+
+    /* same system property Kafka's bundled handler consults; when set, the 
token URL must be listed */
+    public static final String        ALLOWED_URLS_SYSTEM_PROPERTY = 
"org.apache.kafka.sasl.oauthbearer.allowed.urls";
+    public static final int           TOKEN_READ_MAX_ATTEMPTS      = 5;
+    public static final long          TOKEN_READ_RETRY_SLEEP_MS    = 50L;
+
+    private static final String       CLAIM_SUBJECT                = "sub";
+    private static final String       CLAIM_EXPIRATION             = "exp";
+    private static final String       CLAIM_ISSUED_AT              = "iat";
+    private static final ObjectMapper MAPPER                       = new 
ObjectMapper();
+
+    private volatile Path tokenFile;
+
+    @Override
+    public void configure(Map<String, ?> configs, String saslMechanism, 
List<AppConfigurationEntry> jaasConfigEntries) {
+        Object urlValue = 
configs.get(SaslConfigs.SASL_OAUTHBEARER_TOKEN_ENDPOINT_URL);
+        String url      = urlValue != null ? urlValue.toString() : null;
+
+        if (StringUtils.isBlank(url)) {
+            throw new ConfigException(String.format("%s must be set to a 
file:// URL for %s", SaslConfigs.SASL_OAUTHBEARER_TOKEN_ENDPOINT_URL, 
getClass().getSimpleName()));
+        }
+
+        throwIfUrlNotAllowed(url);
+
+        tokenFile = resolveTokenFile(url);
+
+        LOG.info("{}: reading SASL/OAUTHBEARER token from {}", 
getClass().getSimpleName(), tokenFile);
+    }
+
+    @Override
+    public void handle(Callback[] callbacks) throws IOException, 
UnsupportedCallbackException {
+        Path file = tokenFile;
+
+        if (file == null) {
+            throw new IllegalStateException(String.format("%s is not 
configured; call configure() first", getClass().getSimpleName()));
+        }
+
+        for (Callback callback : callbacks) {
+            if (callback instanceof OAuthBearerTokenCallback) {
+                RangerOAuthBearerToken token = readToken(file);
+
+                ((OAuthBearerTokenCallback) callback).token(token);
+
+                LOG.debug("{}: token loaded from {} for principal={}, 
expiresAtMs={}", getClass().getSimpleName(), file, token.principalName(), 
token.lifetimeMs());
+            } else if (callback instanceof SaslExtensionsCallback) {
+                ((SaslExtensionsCallback) 
callback).extensions(SaslExtensions.empty());
+            } else {
+                throw new UnsupportedCallbackException(callback);
+            }
+        }
+    }
+
+    @Override
+    public void close() {
+        tokenFile = null;
+    }
+
+    private void throwIfUrlNotAllowed(String url) {
+        String allowed = System.getProperty(ALLOWED_URLS_SYSTEM_PROPERTY);
+
+        if (allowed == null) {
+            return;
+        }
+
+        if 
(Arrays.stream(allowed.split(",")).map(String::trim).noneMatch(url::equalsIgnoreCase))
 {
+            throw new ConfigException(String.format("%s is not allowed. Update 
system property '%s' to allow it", url, ALLOWED_URLS_SYSTEM_PROPERTY));
+        }
+    }
+
+    private Path resolveTokenFile(String url) {
+        URI uri;
+
+        try {
+            uri = URI.create(url);
+        } catch (IllegalArgumentException e) {
+            throw new ConfigException(String.format("%s is not a valid URL: 
%s", SaslConfigs.SASL_OAUTHBEARER_TOKEN_ENDPOINT_URL, url), e);
+        }
+
+        if (!"file".equalsIgnoreCase(uri.getScheme())) {
+            throw new ConfigException(String.format("%s supports only file:// 
token URLs, got: %s", getClass().getSimpleName(), url));
+        }
+
+        Path path = Paths.get(uri);
+
+        if (!Files.isRegularFile(path)) {
+            throw new ConfigException(String.format("SASL/OAUTHBEARER token 
file does not exist: %s", path));
+        }
+
+        return path;
+    }
+
+    /* Re-reads and reparses on every call. Retries briefly to ride out the 
moment where kubelet atomically replaces the projected token file. */
+    private RangerOAuthBearerToken readToken(Path file) throws IOException {
+        IOException lastFailure = null;
+
+        for (int attempt = 1; attempt <= TOKEN_READ_MAX_ATTEMPTS; attempt++) {
+            try {
+                String jwt = new String(Files.readAllBytes(file), 
StandardCharsets.UTF_8).trim();
+
+                if (jwt.isEmpty()) {
+                    throw new IOException(String.format("SASL/OAUTHBEARER 
token file is empty: %s", file));
+                }
+
+                return parseToken(jwt);
+            } catch (IOException e) {
+                lastFailure = e;
+
+                LOG.debug("{}: token read/parse failed (attempt {}/{}): {}", 
getClass().getSimpleName(), attempt, TOKEN_READ_MAX_ATTEMPTS, e.getMessage());
+
+                if (attempt < TOKEN_READ_MAX_ATTEMPTS) {
+                    sleepBeforeRetry();
+                }
+            }
+        }
+
+        throw new IOException(String.format("Failed to read SASL/OAUTHBEARER 
token from %s after %d attempts", file, TOKEN_READ_MAX_ATTEMPTS), lastFailure);
+    }
+
+    private RangerOAuthBearerToken parseToken(String jwt) throws IOException {
+        String[] parts = jwt.split("\\.");
+
+        if (parts.length < 2) {
+            throw new IOException("Malformed JWT: expected 
header.payload[.signature]");
+        }
+
+        JsonNode claims;
+
+        try {
+            claims = MAPPER.readTree(Base64.getUrlDecoder().decode(parts[1]));
+        } catch (IllegalArgumentException e) {
+            throw new IOException("Malformed JWT: payload is not base64url", 
e);
+        }
+
+        JsonNode sub = claims.get(CLAIM_SUBJECT);
+        JsonNode exp = claims.get(CLAIM_EXPIRATION);
+        JsonNode iat = claims.get(CLAIM_ISSUED_AT);
+
+        if (sub == null || !sub.isTextual() || 
StringUtils.isBlank(sub.asText())) {
+            throw new IOException(String.format("JWT is missing required '%s' 
claim", CLAIM_SUBJECT));
+        }
+
+        if (exp == null || !exp.isNumber()) {
+            throw new IOException(String.format("JWT is missing required 
numeric '%s' claim", CLAIM_EXPIRATION));
+        }
+
+        Long startTimeMs = iat != null && iat.isNumber() ? toEpochMillis(iat) 
: null;
+
+        return new RangerOAuthBearerToken(jwt, sub.asText(), 
toEpochMillis(exp), startTimeMs);
+    }
+
+    private long toEpochMillis(JsonNode epochSeconds) {
+        return Math.round(epochSeconds.asDouble() * 1000);
+    }
+
+    private void sleepBeforeRetry() throws IOException {
+        try {
+            Thread.sleep(TOKEN_READ_RETRY_SLEEP_MS);
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+
+            throw new IOException("Interrupted while waiting to re-read 
SASL/OAUTHBEARER token", e);
+        }
+    }
+}
diff --git 
a/ranger-kafka-client-auth/src/main/java/org/apache/ranger/kafka/auth/RangerOAuthBearerToken.java
 
b/ranger-kafka-client-auth/src/main/java/org/apache/ranger/kafka/auth/RangerOAuthBearerToken.java
new file mode 100644
index 000000000..b9f923c64
--- /dev/null
+++ 
b/ranger-kafka-client-auth/src/main/java/org/apache/ranger/kafka/auth/RangerOAuthBearerToken.java
@@ -0,0 +1,69 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.ranger.kafka.auth;
+
+import org.apache.kafka.common.security.oauthbearer.OAuthBearerToken;
+
+import java.util.Collections;
+import java.util.Set;
+
+/**
+ * {@link OAuthBearerToken} backed by a JWT whose claims were already 
extracted by the caller.
+ * Carries no scopes; {@code expiresOnTimeMs} is the {@code exp} claim and 
{@code startTimeMs} the optional {@code iat},
+ * both in epoch milliseconds, which is what Kafka's credential refresh 
scheduling expects.
+ */
+public final class RangerOAuthBearerToken implements OAuthBearerToken {
+    private final String value;
+    private final String principalName;
+    private final long   expiresOnTimeMs;
+    private final Long   startTimeMs;
+
+    public RangerOAuthBearerToken(String value, String principalName, long 
expiresOnTimeMs, Long startTimeMs) {
+        this.value           = value;
+        this.principalName   = principalName;
+        this.expiresOnTimeMs = expiresOnTimeMs;
+        this.startTimeMs     = startTimeMs;
+    }
+
+    @Override
+    public String value() {
+        return value;
+    }
+
+    @Override
+    public Set<String> scope() {
+        return Collections.emptySet();
+    }
+
+    @Override
+    public long lifetimeMs() {
+        return expiresOnTimeMs;
+    }
+
+    @Override
+    public String principalName() {
+        return principalName;
+    }
+
+    @Override
+    public Long startTimeMs() {
+        return startTimeMs;
+    }
+}
diff --git 
a/ranger-kafka-client-auth/src/test/java/org/apache/ranger/kafka/auth/RangerKafkaClientSecurityConfigTest.java
 
b/ranger-kafka-client-auth/src/test/java/org/apache/ranger/kafka/auth/RangerKafkaClientSecurityConfigTest.java
new file mode 100644
index 000000000..feffe91d4
--- /dev/null
+++ 
b/ranger-kafka-client-auth/src/test/java/org/apache/ranger/kafka/auth/RangerKafkaClientSecurityConfigTest.java
@@ -0,0 +1,301 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.ranger.kafka.auth;
+
+import org.apache.kafka.clients.CommonClientConfigs;
+import org.apache.kafka.common.config.SaslConfigs;
+import 
org.apache.kafka.common.security.oauthbearer.OAuthBearerLoginCallbackHandler;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Map;
+import java.util.Properties;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class RangerKafkaClientSecurityConfigTest {
+    private static final String PREFIX = "ranger.audit.ingestor";
+
+    @TempDir
+    Path tempDir;
+
+    @Test
+    public void testPlaintextByDefaultSetsNoSaslKeys() {
+        Map<String, Object> config = RangerKafkaClientSecurityConfig.build(new 
Properties(), PREFIX);
+
+        assertEquals("PLAINTEXT", 
config.get(CommonClientConfigs.SECURITY_PROTOCOL_CONFIG));
+        assertFalse(config.containsKey(SaslConfigs.SASL_MECHANISM));
+        assertFalse(config.containsKey(SaslConfigs.SASL_JAAS_CONFIG));
+        
assertFalse(config.containsKey(SaslConfigs.SASL_KERBEROS_SERVICE_NAME));
+    }
+
+    @Test
+    public void testSaslDefaultsToGssapiWithConfiguredKeytabOptions() throws 
IOException {
+        Path       keytab = Files.createFile(tempDir.resolve("ranger.keytab"));
+        Properties props  = keytabLoginProps(keytab);
+
+        props.setProperty(PREFIX + ".service.kerberos.principal", 
"rangeraudit/[email protected]");
+        props.setProperty(PREFIX + ".host", "Audit-Host.example.com");
+
+        Map<String, Object> config = 
RangerKafkaClientSecurityConfig.build(props, PREFIX);
+
+        assertEquals("SASL_PLAINTEXT", 
config.get(CommonClientConfigs.SECURITY_PROTOCOL_CONFIG));
+        assertEquals("GSSAPI", config.get(SaslConfigs.SASL_MECHANISM));
+        assertEquals("kafka", 
config.get(SaslConfigs.SASL_KERBEROS_SERVICE_NAME));
+        assertEquals(RangerKafkaClientSecurityConfig.KRB5_LOGIN_MODULE + " 
required keyTab=\"" + keytab + "\" serviceName=\"kafka\" 
principal=\"rangeraudit/[email protected]\" storeKey=\"true\" 
useKeyTab=\"true\" useTicketCache=\"false\";",
+                config.get(SaslConfigs.SASL_JAAS_CONFIG));
+        
assertFalse(config.containsKey(SaslConfigs.SASL_LOGIN_CALLBACK_HANDLER_CLASS));
+    }
+
+    @Test
+    public void testGssapiWithoutOptionsOnlyPassesServiceCredentials() {
+        Properties props = new Properties();
+
+        props.setProperty(PREFIX + ".kafka.security.protocol", 
"SASL_PLAINTEXT");
+        props.setProperty(PREFIX + ".service.kerberos.principal", 
"[email protected]");
+        props.setProperty(PREFIX + ".service.kerberos.keytab", 
"/etc/keytabs/missing.keytab");
+
+        Map<String, Object> config = 
RangerKafkaClientSecurityConfig.build(props, PREFIX);
+
+        assertEquals(RangerKafkaClientSecurityConfig.KRB5_LOGIN_MODULE + " 
required keyTab=\"/etc/keytabs/missing.keytab\" serviceName=\"kafka\" 
principal=\"[email protected]\";",
+                config.get(SaslConfigs.SASL_JAAS_CONFIG));
+    }
+
+    @Test
+    public void testGssapiHonoursConfiguredServiceName() throws IOException {
+        Path       keytab = Files.createFile(tempDir.resolve("ranger.keytab"));
+        Properties props  = keytabLoginProps(keytab);
+
+        props.setProperty(PREFIX + ".kafka.security.protocol", "SASL_SSL");
+        props.setProperty(PREFIX + ".kafka.sasl.mechanism", "GSSAPI");
+        props.setProperty(PREFIX + ".kafka.sasl.kerberos.service.name", 
"kafka-broker");
+        props.setProperty(PREFIX + ".service.kerberos.principal", 
"[email protected]");
+        props.setProperty(PREFIX + ".service.kerberos.keytab", 
keytab.toString());
+
+        Map<String, Object> config = 
RangerKafkaClientSecurityConfig.build(props, PREFIX);
+
+        assertEquals("kafka-broker", 
config.get(SaslConfigs.SASL_KERBEROS_SERVICE_NAME));
+        
assertTrue(config.get(SaslConfigs.SASL_JAAS_CONFIG).toString().contains("serviceName=\"kafka-broker\"
 principal=\"[email protected]\" storeKey=\"true\" useKeyTab=\"true\""));
+    }
+
+    @Test
+    public void testGssapiKeytabLoginRequiresPrincipalAndKeytab() {
+        Properties props = new Properties();
+
+        props.setProperty(PREFIX + ".kafka.security.protocol", 
"SASL_PLAINTEXT");
+        props.setProperty(PREFIX + ".kafka.sasl.jaas.option.useKeyTab", 
"true");
+        props.setProperty(PREFIX + ".service.kerberos.principal", 
"[email protected]");
+
+        IllegalStateException e = assertThrows(IllegalStateException.class, () 
-> RangerKafkaClientSecurityConfig.build(props, PREFIX));
+
+        assertTrue(e.getMessage().contains(PREFIX + 
".service.kerberos.keytab"), e.getMessage());
+    }
+
+    @Test
+    public void testGssapiKeytabLoginRequiresReadableKeytab() {
+        Properties props = new Properties();
+
+        props.setProperty(PREFIX + ".kafka.security.protocol", 
"SASL_PLAINTEXT");
+        props.setProperty(PREFIX + ".kafka.sasl.jaas.option.useKeyTab", 
"true");
+        props.setProperty(PREFIX + ".service.kerberos.principal", 
"[email protected]");
+        props.setProperty(PREFIX + ".service.kerberos.keytab", 
tempDir.resolve("missing.keytab").toString());
+
+        IllegalStateException e = assertThrows(IllegalStateException.class, () 
-> RangerKafkaClientSecurityConfig.build(props, PREFIX));
+
+        assertTrue(e.getMessage().contains("not found"), e.getMessage());
+    }
+
+    @Test
+    public void testGssapiTicketCacheNeedsNoKeytab() {
+        Properties props = new Properties();
+
+        props.setProperty(PREFIX + ".kafka.security.protocol", 
"SASL_PLAINTEXT");
+        props.setProperty(PREFIX + ".kafka.sasl.jaas.option.useTicketCache", 
"true");
+        props.setProperty(PREFIX + ".kafka.sasl.jaas.option.renewTGT", "true");
+
+        Map<String, Object> config = 
RangerKafkaClientSecurityConfig.build(props, PREFIX);
+
+        assertEquals(RangerKafkaClientSecurityConfig.KRB5_LOGIN_MODULE + " 
required serviceName=\"kafka\" renewTGT=\"true\" useTicketCache=\"true\";",
+                config.get(SaslConfigs.SASL_JAAS_CONFIG));
+    }
+
+    @Test
+    public void testJaasOptionsOverrideDefaultsAndResolveHost() throws 
IOException {
+        Path       keytab = Files.createFile(tempDir.resolve("other.keytab"));
+        Properties props  = new Properties();
+
+        props.setProperty(PREFIX + ".kafka.security.protocol", 
"SASL_PLAINTEXT");
+        props.setProperty(PREFIX + ".kafka.sasl.jaas.loginModuleControlFlag", 
"requisite");
+        props.setProperty(PREFIX + ".kafka.sasl.jaas.option.useKeyTab", 
"true");
+        props.setProperty(PREFIX + ".kafka.sasl.jaas.option.keyTab", 
keytab.toString());
+        props.setProperty(PREFIX + ".kafka.sasl.jaas.option.principal", 
"svc/[email protected]");
+        props.setProperty(PREFIX + ".kafka.sasl.jaas.option.storeKey", 
"false");
+        props.setProperty(PREFIX + ".host", "node1.example.com");
+
+        Map<String, Object> config = 
RangerKafkaClientSecurityConfig.build(props, PREFIX);
+        String              jaas   = 
config.get(SaslConfigs.SASL_JAAS_CONFIG).toString();
+
+        
assertTrue(jaas.startsWith(RangerKafkaClientSecurityConfig.KRB5_LOGIN_MODULE + 
" requisite "), jaas);
+        assertTrue(jaas.contains("keyTab=\"" + keytab + "\""), jaas);
+        assertTrue(jaas.contains("storeKey=\"false\""), jaas);
+        
assertTrue(jaas.contains("principal=\"svc/[email protected]\""), 
jaas);
+        assertFalse(config.containsKey("sasl.jaas.option.keyTab"));
+        assertFalse(config.containsKey("sasl.jaas.loginModuleControlFlag"));
+    }
+
+    @Test
+    public void testOAuthBearerFileTokenUsesRangerHandler() throws IOException 
{
+        Path       token = Files.createFile(tempDir.resolve("token"));
+        Properties props = new Properties();
+
+        props.setProperty(PREFIX + ".kafka.security.protocol", "SASL_SSL");
+        props.setProperty(PREFIX + ".kafka.sasl.mechanism", "OAUTHBEARER");
+        props.setProperty(PREFIX + 
".kafka.sasl.oauthbearer.token.endpoint.url", token.toUri().toString());
+
+        Map<String, Object> config = 
RangerKafkaClientSecurityConfig.build(props, PREFIX);
+
+        assertEquals("OAUTHBEARER", config.get(SaslConfigs.SASL_MECHANISM));
+        assertEquals(RangerKafkaClientSecurityConfig.OAUTHBEARER_LOGIN_MODULE 
+ " required;", config.get(SaslConfigs.SASL_JAAS_CONFIG));
+        assertEquals(token.toUri().toString(), 
config.get(SaslConfigs.SASL_OAUTHBEARER_TOKEN_ENDPOINT_URL));
+        
assertEquals(RangerKafkaOAuthBearerLoginCallbackHandler.class.getName(), 
config.get(SaslConfigs.SASL_LOGIN_CALLBACK_HANDLER_CLASS));
+        
assertFalse(config.containsKey(SaslConfigs.SASL_KERBEROS_SERVICE_NAME));
+    }
+
+    @Test
+    public void testOAuthBearerHttpEndpointUsesKafkaHandlerWithJaasOptions() {
+        Properties props = new Properties();
+
+        props.setProperty(PREFIX + ".kafka.security.protocol", "SASL_SSL");
+        props.setProperty(PREFIX + ".kafka.sasl.mechanism", "OAUTHBEARER");
+        props.setProperty(PREFIX + 
".kafka.sasl.oauthbearer.token.endpoint.url", 
"https://idp.example.com/oauth/token";);
+        props.setProperty(PREFIX + ".kafka.sasl.jaas.option.clientId", 
"ranger");
+        props.setProperty(PREFIX + ".kafka.sasl.jaas.option.clientSecret", 
"secret");
+
+        Map<String, Object> config = 
RangerKafkaClientSecurityConfig.build(props, PREFIX);
+
+        assertEquals(OAuthBearerLoginCallbackHandler.class.getName(), 
config.get(SaslConfigs.SASL_LOGIN_CALLBACK_HANDLER_CLASS));
+        assertEquals(RangerKafkaClientSecurityConfig.OAUTHBEARER_LOGIN_MODULE 
+ " required clientId=\"ranger\" clientSecret=\"secret\";", 
config.get(SaslConfigs.SASL_JAAS_CONFIG));
+    }
+
+    @Test
+    public void testOAuthBearerRequiresTokenUrl() {
+        Properties props = new Properties();
+
+        props.setProperty(PREFIX + ".kafka.security.protocol", "SASL_SSL");
+        props.setProperty(PREFIX + ".kafka.sasl.mechanism", "OAUTHBEARER");
+
+        IllegalStateException e = assertThrows(IllegalStateException.class, () 
-> RangerKafkaClientSecurityConfig.build(props, PREFIX));
+
+        assertTrue(e.getMessage().contains(PREFIX + 
".kafka.sasl.oauthbearer.token.endpoint.url"), e.getMessage());
+    }
+
+    @Test
+    public void testPassThroughOverridesDerivedValues() throws IOException {
+        Path       token = Files.createFile(tempDir.resolve("token"));
+        Properties props = new Properties();
+
+        props.setProperty(PREFIX + ".kafka.security.protocol", "SASL_SSL");
+        props.setProperty(PREFIX + ".kafka.sasl.mechanism", "OAUTHBEARER");
+        props.setProperty(PREFIX + 
".kafka.sasl.oauthbearer.token.endpoint.url", token.toUri().toString());
+        props.setProperty(PREFIX + ".kafka.sasl.login.callback.handler.class", 
"com.example.CustomHandler");
+        props.setProperty(PREFIX + ".kafka.sasl.login.refresh.window.factor", 
"0.7");
+        props.setProperty(PREFIX + ".kafka.ssl.truststore.location", 
"/etc/ssl/truststore.jks");
+        props.setProperty(PREFIX + ".kafka.ssl.truststore.password", " ");
+        props.setProperty(PREFIX + ".kafka.bootstrap.servers", "kafka:9093");
+
+        Map<String, Object> config = 
RangerKafkaClientSecurityConfig.build(props, PREFIX);
+
+        assertEquals("com.example.CustomHandler", 
config.get(SaslConfigs.SASL_LOGIN_CALLBACK_HANDLER_CLASS));
+        assertEquals("0.7", 
config.get(SaslConfigs.SASL_LOGIN_REFRESH_WINDOW_FACTOR));
+        assertEquals("/etc/ssl/truststore.jks", 
config.get("ssl.truststore.location"));
+        assertFalse(config.containsKey("ssl.truststore.password"));
+        assertFalse(config.containsKey("bootstrap.servers"));
+    }
+
+    @Test
+    public void testOtherMechanismsUseConfiguredLoginModuleOrExplicitJaas() {
+        Properties props = new Properties();
+
+        props.setProperty(PREFIX + ".kafka.security.protocol", "SASL_SSL");
+        props.setProperty(PREFIX + ".kafka.sasl.mechanism", "SCRAM-SHA-512");
+
+        assertThrows(IllegalStateException.class, () -> 
RangerKafkaClientSecurityConfig.build(props, PREFIX));
+
+        props.setProperty(PREFIX + ".kafka.sasl.jaas.loginModuleName", 
"org.apache.kafka.common.security.scram.ScramLoginModule");
+        props.setProperty(PREFIX + ".kafka.sasl.jaas.option.username", 
"ranger");
+        props.setProperty(PREFIX + ".kafka.sasl.jaas.option.password", 
"secret");
+
+        Map<String, Object> config = 
RangerKafkaClientSecurityConfig.build(props, PREFIX);
+
+        assertEquals("SCRAM-SHA-512", config.get(SaslConfigs.SASL_MECHANISM));
+        assertEquals("org.apache.kafka.common.security.scram.ScramLoginModule 
required password=\"secret\" username=\"ranger\";", 
config.get(SaslConfigs.SASL_JAAS_CONFIG));
+
+        props.setProperty(PREFIX + ".kafka.sasl.jaas.config", 
"org.apache.kafka.common.security.scram.ScramLoginModule required 
username=\"explicit\" password=\"x\";");
+
+        config = RangerKafkaClientSecurityConfig.build(props, PREFIX);
+
+        
assertTrue(config.get(SaslConfigs.SASL_JAAS_CONFIG).toString().contains("username=\"explicit\""));
+    }
+
+    @Test
+    public void testApplyAddsToExistingProperties() throws IOException {
+        Path       keytab = Files.createFile(tempDir.resolve("ranger.keytab"));
+        Properties props  = new Properties();
+        Properties target = new Properties();
+
+        props.setProperty(PREFIX + ".kafka.security.protocol", 
"SASL_PLAINTEXT");
+        props.setProperty(PREFIX + ".service.kerberos.principal", 
"[email protected]");
+        props.setProperty(PREFIX + ".service.kerberos.keytab", 
keytab.toString());
+        target.put("bootstrap.servers", "kafka:9092");
+
+        RangerKafkaClientSecurityConfig.apply(props, PREFIX, target);
+
+        assertEquals("kafka:9092", target.get("bootstrap.servers"));
+        assertEquals("SASL_PLAINTEXT", 
target.get(CommonClientConfigs.SECURITY_PROTOCOL_CONFIG));
+        assertEquals("GSSAPI", target.get(SaslConfigs.SASL_MECHANISM));
+    }
+
+    /* SASL_PLAINTEXT with the keytab-login options the shipped site XML 
carries */
+    private static Properties keytabLoginProps(Path keytab) {
+        Properties props = new Properties();
+
+        props.setProperty(PREFIX + ".kafka.security.protocol", 
"SASL_PLAINTEXT");
+        props.setProperty(PREFIX + ".service.kerberos.keytab", 
keytab.toString());
+        props.setProperty(PREFIX + ".kafka.sasl.jaas.option.useKeyTab", 
"true");
+        props.setProperty(PREFIX + ".kafka.sasl.jaas.option.storeKey", "true");
+        props.setProperty(PREFIX + ".kafka.sasl.jaas.option.useTicketCache", 
"false");
+
+        return props;
+    }
+
+    @Test
+    public void testResolvePrincipal() {
+        assertEquals("svc/host.example.com@REALM", 
RangerKafkaClientSecurityConfig.resolvePrincipal("svc/_HOST@REALM", 
"Host.Example.COM"));
+        assertEquals("svc/other@REALM", 
RangerKafkaClientSecurityConfig.resolvePrincipal("svc/other@REALM", 
"host.example.com"));
+        assertEquals("svc@REALM", 
RangerKafkaClientSecurityConfig.resolvePrincipal("svc@REALM", null));
+        
assertFalse(RangerKafkaClientSecurityConfig.resolvePrincipal("svc/_HOST@REALM", 
null).contains("_HOST"));
+    }
+}
diff --git 
a/ranger-kafka-client-auth/src/test/java/org/apache/ranger/kafka/auth/RangerKafkaOAuthBearerLoginCallbackHandlerTest.java
 
b/ranger-kafka-client-auth/src/test/java/org/apache/ranger/kafka/auth/RangerKafkaOAuthBearerLoginCallbackHandlerTest.java
new file mode 100644
index 000000000..c72f22d68
--- /dev/null
+++ 
b/ranger-kafka-client-auth/src/test/java/org/apache/ranger/kafka/auth/RangerKafkaOAuthBearerLoginCallbackHandlerTest.java
@@ -0,0 +1,231 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.ranger.kafka.auth;
+
+import org.apache.kafka.common.config.ConfigException;
+import org.apache.kafka.common.config.SaslConfigs;
+import org.apache.kafka.common.security.auth.SaslExtensionsCallback;
+import org.apache.kafka.common.security.oauthbearer.OAuthBearerToken;
+import org.apache.kafka.common.security.oauthbearer.OAuthBearerTokenCallback;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import javax.security.auth.callback.Callback;
+import javax.security.auth.callback.NameCallback;
+import javax.security.auth.callback.UnsupportedCallbackException;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Base64;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class RangerKafkaOAuthBearerLoginCallbackHandlerTest {
+    private static final String SUBJECT = 
"system:serviceaccount:ranger:ranger-audit";
+
+    @TempDir
+    Path tempDir;
+
+    @Test
+    public void testTokenFromFileWithoutScopeClaim() throws Exception {
+        Path   tokenFile = writeToken("token", buildJwt(SUBJECT, 
1_700_000_000L, 1_700_003_600L));
+        String jwt       = new String(Files.readAllBytes(tokenFile), 
StandardCharsets.UTF_8);
+
+        try (RangerKafkaOAuthBearerLoginCallbackHandler handler = 
configuredHandler(tokenFile)) {
+            OAuthBearerToken token = login(handler);
+
+            assertEquals(jwt, token.value());
+            assertEquals(SUBJECT, token.principalName());
+            assertEquals(1_700_003_600_000L, token.lifetimeMs());
+            assertEquals(Long.valueOf(1_700_000_000_000L), 
token.startTimeMs());
+            assertTrue(token.scope().isEmpty());
+        }
+    }
+
+    @Test
+    public void testRotatedTokenIsReadOnNextLogin() throws Exception {
+        String jwtV1      = buildJwt(SUBJECT, 1_700_000_000L, 1_700_003_600L);
+        String jwtV2      = buildJwt(SUBJECT, 1_700_003_601L, 1_700_007_201L);
+        Path   tokenFile  = writeToken("token", jwtV1);
+
+        try (RangerKafkaOAuthBearerLoginCallbackHandler handler = 
configuredHandler(tokenFile)) {
+            assertEquals(jwtV1, login(handler).value());
+
+            Files.write(tokenFile, jwtV2.getBytes(StandardCharsets.UTF_8));
+
+            OAuthBearerToken refreshed = login(handler);
+
+            assertEquals(jwtV2, refreshed.value());
+            assertEquals(1_700_007_201_000L, refreshed.lifetimeMs());
+        }
+    }
+
+    @Test
+    public void testTokenWithoutIssuedAtHasNoStartTime() throws Exception {
+        Path tokenFile = writeToken("token", buildJwt(SUBJECT, null, 
1_700_003_600L));
+
+        try (RangerKafkaOAuthBearerLoginCallbackHandler handler = 
configuredHandler(tokenFile)) {
+            assertNull(login(handler).startTimeMs());
+        }
+    }
+
+    @Test
+    public void testTokenWithoutExpirationIsRejected() throws Exception {
+        String payload   = base64Url("{\"sub\":\"" + SUBJECT + "\"}");
+        Path   tokenFile = writeToken("token", 
base64Url("{\"alg\":\"RS256\"}") + "." + payload + ".sig");
+
+        try (RangerKafkaOAuthBearerLoginCallbackHandler handler = 
configuredHandler(tokenFile)) {
+            IOException e = assertThrows(IOException.class, () -> 
login(handler));
+
+            assertTrue(e.getCause().getMessage().contains("exp"), 
e.getCause().getMessage());
+        }
+    }
+
+    @Test
+    public void testEmptyTokenFileIsRejected() throws Exception {
+        Path tokenFile = writeToken("token", "");
+
+        try (RangerKafkaOAuthBearerLoginCallbackHandler handler = 
configuredHandler(tokenFile)) {
+            assertThrows(IOException.class, () -> login(handler));
+        }
+    }
+
+    @Test
+    public void testNonFileUrlIsRejected() {
+        Map<String, Object> configs = new HashMap<>();
+
+        configs.put(SaslConfigs.SASL_OAUTHBEARER_TOKEN_ENDPOINT_URL, 
"https://idp.example.com/token";);
+
+        assertThrows(ConfigException.class, () -> new 
RangerKafkaOAuthBearerLoginCallbackHandler().configure(configs, "OAUTHBEARER", 
Collections.emptyList()));
+    }
+
+    @Test
+    public void testMissingTokenFileIsRejectedAtConfigure() {
+        Map<String, Object> configs = new HashMap<>();
+
+        configs.put(SaslConfigs.SASL_OAUTHBEARER_TOKEN_ENDPOINT_URL, 
tempDir.resolve("missing").toUri().toString());
+
+        assertThrows(ConfigException.class, () -> new 
RangerKafkaOAuthBearerLoginCallbackHandler().configure(configs, "OAUTHBEARER", 
Collections.emptyList()));
+    }
+
+    @Test
+    public void testMissingUrlIsRejectedAtConfigure() {
+        assertThrows(ConfigException.class, () -> new 
RangerKafkaOAuthBearerLoginCallbackHandler().configure(Collections.emptyMap(), 
"OAUTHBEARER", Collections.emptyList()));
+    }
+
+    @Test
+    public void testAllowedUrlsSystemPropertyIsEnforced() throws Exception {
+        Path   tokenFile = writeToken("token", buildJwt(SUBJECT, 
1_700_000_000L, 1_700_003_600L));
+        String url       = tokenFile.toUri().toString();
+
+        
System.setProperty(RangerKafkaOAuthBearerLoginCallbackHandler.ALLOWED_URLS_SYSTEM_PROPERTY,
 "file:///somewhere/else");
+
+        try {
+            assertThrows(ConfigException.class, () -> 
configuredHandler(tokenFile));
+
+            
System.setProperty(RangerKafkaOAuthBearerLoginCallbackHandler.ALLOWED_URLS_SYSTEM_PROPERTY,
 "file:///somewhere/else, " + url);
+
+            try (RangerKafkaOAuthBearerLoginCallbackHandler handler = 
configuredHandler(tokenFile)) {
+                assertEquals(SUBJECT, login(handler).principalName());
+            }
+        } finally {
+            
System.clearProperty(RangerKafkaOAuthBearerLoginCallbackHandler.ALLOWED_URLS_SYSTEM_PROPERTY);
+        }
+    }
+
+    @Test
+    public void testExtensionsCallbackGetsEmptyExtensions() throws Exception {
+        Path tokenFile = writeToken("token", buildJwt(SUBJECT, 1_700_000_000L, 
1_700_003_600L));
+
+        try (RangerKafkaOAuthBearerLoginCallbackHandler handler = 
configuredHandler(tokenFile)) {
+            SaslExtensionsCallback callback = new SaslExtensionsCallback();
+
+            handler.handle(new Callback[] {callback});
+
+            assertTrue(callback.extensions().map().isEmpty());
+        }
+    }
+
+    @Test
+    public void testUnsupportedCallbackIsRejected() throws Exception {
+        Path tokenFile = writeToken("token", buildJwt(SUBJECT, 1_700_000_000L, 
1_700_003_600L));
+
+        try (RangerKafkaOAuthBearerLoginCallbackHandler handler = 
configuredHandler(tokenFile)) {
+            assertThrows(UnsupportedCallbackException.class, () -> 
handler.handle(new Callback[] {new NameCallback("name")}));
+        }
+    }
+
+    @Test
+    public void testHandleBeforeConfigureFails() {
+        assertThrows(IllegalStateException.class, () -> new 
RangerKafkaOAuthBearerLoginCallbackHandler().handle(new Callback[] {new 
OAuthBearerTokenCallback()}));
+    }
+
+    private RangerKafkaOAuthBearerLoginCallbackHandler configuredHandler(Path 
tokenFile) {
+        Map<String, Object> configs = new HashMap<>();
+
+        configs.put(SaslConfigs.SASL_OAUTHBEARER_TOKEN_ENDPOINT_URL, 
tokenFile.toUri().toString());
+
+        RangerKafkaOAuthBearerLoginCallbackHandler handler = new 
RangerKafkaOAuthBearerLoginCallbackHandler();
+
+        handler.configure(configs, "OAUTHBEARER", Collections.emptyList());
+
+        return handler;
+    }
+
+    private static OAuthBearerToken 
login(RangerKafkaOAuthBearerLoginCallbackHandler handler) throws Exception {
+        OAuthBearerTokenCallback callback = new OAuthBearerTokenCallback();
+
+        handler.handle(new Callback[] {callback});
+
+        return callback.token();
+    }
+
+    private Path writeToken(String name, String content) throws IOException {
+        Path file = tempDir.resolve(name);
+
+        Files.write(file, content.getBytes(StandardCharsets.UTF_8));
+
+        return file;
+    }
+
+    static String buildJwt(String subject, Long issuedAtSeconds, long 
expirationSeconds) {
+        StringBuilder payload = new 
StringBuilder("{\"sub\":\"").append(subject).append("\",\"exp\":").append(expirationSeconds);
+
+        if (issuedAtSeconds != null) {
+            payload.append(",\"iat\":").append(issuedAtSeconds);
+        }
+
+        
payload.append(",\"iss\":\"https://kubernetes.default.svc.cluster.local\"}";);
+
+        return base64Url("{\"alg\":\"RS256\",\"typ\":\"JWT\"}") + "." + 
base64Url(payload.toString()) + ".signature";
+    }
+
+    private static String base64Url(String value) {
+        return 
Base64.getUrlEncoder().withoutPadding().encodeToString(value.getBytes(StandardCharsets.UTF_8));
+    }
+}
diff --git a/tagsync/conf/templates/installprop2xml.properties 
b/tagsync/conf/templates/installprop2xml.properties
index aea6537ee..c12d043be 100644
--- a/tagsync/conf/templates/installprop2xml.properties
+++ b/tagsync/conf/templates/installprop2xml.properties
@@ -28,6 +28,8 @@ TAG_SOURCE_ATLAS_KAFKA_ENTITIES_GROUP_ID = 
atlas.kafka.entities.group.id
 
 TAG_SOURCE_ATLAS_KAFKA_SERVICE_NAME = atlas.kafka.sasl.kerberos.service.name
 TAG_SOURCE_ATLAS_KAFKA_SECURITY_PROTOCOL = atlas.kafka.security.protocol
+TAG_SOURCE_ATLAS_KAFKA_SASL_MECHANISM = atlas.kafka.sasl.mechanism
+TAG_SOURCE_ATLAS_KAFKA_OAUTH_TOKEN_URL = 
atlas.kafka.sasl.oauthbearer.token.endpoint.url
 
 TAG_SOURCE_ATLAS_KERBEROS_PRINCIPAL = atlas.jaas.kafkaClient.option.principal
 TAG_SOURCE_ATLAS_KERBEROS_KEYTAB = atlas.jaas.kafkaClient.option.keyTab
diff --git a/tagsync/pom.xml b/tagsync/pom.xml
index 7b263f6aa..90f675309 100644
--- a/tagsync/pom.xml
+++ b/tagsync/pom.xml
@@ -253,6 +253,11 @@
             <artifactId>ranger-common-ha</artifactId>
             <version>${project.version}</version>
         </dependency>
+        <dependency>
+            <groupId>org.apache.ranger</groupId>
+            <artifactId>ranger-kafka-client-auth</artifactId>
+            <version>${project.version}</version>
+        </dependency>
         <dependency>
             <groupId>org.apache.ranger</groupId>
             <artifactId>ranger-plugins-common</artifactId>
diff --git a/tagsync/scripts/install.properties 
b/tagsync/scripts/install.properties
index 278df0ff8..ff3812f0e 100644
--- a/tagsync/scripts/install.properties
+++ b/tagsync/scripts/install.properties
@@ -33,6 +33,10 @@ TAG_SOURCE_ATLAS_KAFKA_ENTITIES_GROUP_ID = 
ranger_entities_consumer
 
 TAG_SOURCE_ATLAS_KAFKA_SERVICE_NAME = kafka
 TAG_SOURCE_ATLAS_KAFKA_SECURITY_PROTOCOL = PLAINTEXTSASL
+# SASL mechanism for the Atlas Kafka consumer; supported values: GSSAPI 
(Kerberos keytab, default) and OAUTHBEARER (token)
+TAG_SOURCE_ATLAS_KAFKA_SASL_MECHANISM = GSSAPI
+# OAUTHBEARER only: file:// URL of a JWT that is re-read on every login (e.g. 
Kubernetes projected service-account token) or http(s):// token endpoint for 
Kafka's own client-credentials handler
+TAG_SOURCE_ATLAS_KAFKA_OAUTH_TOKEN_URL =
 
 TAG_SOURCE_ATLAS_KERBEROS_PRINCIPAL =
 TAG_SOURCE_ATLAS_KERBEROS_KEYTAB =
diff --git a/tagsync/scripts/setup.py b/tagsync/scripts/setup.py
index c7ca4b3b0..c681bdb86 100755
--- a/tagsync/scripts/setup.py
+++ b/tagsync/scripts/setup.py
@@ -73,6 +73,8 @@
 
 TAG_SOURCE_ATLAS_KAKFA_SERVICE_NAME_KEY = 'TAG_SOURCE_ATLAS_KAFKA_SERVICE_NAME'
 TAG_SOURCE_ATLAS_KAFKA_SECURITY_PROTOCOL_KEY = 
'TAG_SOURCE_ATLAS_KAFKA_SECURITY_PROTOCOL'
+TAG_SOURCE_ATLAS_KAFKA_SASL_MECHANISM_KEY = 
'TAG_SOURCE_ATLAS_KAFKA_SASL_MECHANISM'
+TAG_SOURCE_ATLAS_KAFKA_OAUTH_TOKEN_URL_KEY = 
'TAG_SOURCE_ATLAS_KAFKA_OAUTH_TOKEN_URL'
 TAG_SOURCE_ATLAS_KERBEROS_PRINCIPAL_KEY = 'TAG_SOURCE_ATLAS_KERBEROS_PRINCIPAL'
 TAG_SOURCE_ATLAS_KERBEROS_KEYTAB_KEY = 'TAG_SOURCE_ATLAS_KERBEROS_KEYTAB'
 TAGSYNC_ATLAS_TO_RANGER_SERVICE_MAPPING = 
'ranger.tagsync.atlas.to.ranger.service.mapping'
@@ -89,6 +91,12 @@
 TAG_SOURCE_FILE_ENABLED_KEY = 'TAG_SOURCE_FILE_ENABLED'
 TAG_SOURCE_FILE_ENABLED = 'ranger.tagsync.source.file'
 
+KAFKA_SASL_MECHANISM_GSSAPI = 'GSSAPI'
+KAFKA_SASL_MECHANISM_OAUTHBEARER = 'OAUTHBEARER'
+KAFKA_OAUTHBEARER_LOGIN_MODULE = 
'org.apache.kafka.common.security.oauthbearer.OAuthBearerLoginModule'
+KAFKA_OAUTHBEARER_LOGIN_HANDLER = 
'org.apache.kafka.common.security.oauthbearer.OAuthBearerLoginCallbackHandler'
+RANGER_OAUTHBEARER_LOGIN_HANDLER = 
'org.apache.ranger.kafka.auth.RangerKafkaOAuthBearerLoginCallbackHandler'
+
 hadoopConfFileName = 'core-site.xml'
 ENV_HADOOP_CONF_FILE = "ranger-tagsync-env-hadoopconfdir.sh"
 ENV_PID_FILE = 'ranger-tagsync-env-piddir.sh'
@@ -215,8 +223,8 @@ def updatePropertyInJCKSFile(jcksFileName,propName,value):
                sys.exit(1)
        return ret
 
-def atlas_kafka_uses_kerberos(props):
-       """Kerberos JAAS for Atlas Kafka consumer only when SASL is configured 
(not PLAINTEXT)."""
+def atlas_kafka_uses_sasl(props):
+       """SASL settings for the Atlas Kafka consumer only when a SASL protocol 
is configured (not PLAINTEXT)."""
        if not configure_security:
                return False
        protocol = props.get(TAG_SOURCE_ATLAS_KAFKA_SECURITY_PROTOCOL_KEY, 
'PLAINTEXT')
@@ -224,6 +232,13 @@ def atlas_kafka_uses_kerberos(props):
                return False
        return protocol.strip().upper().startswith('SASL')
 
+def atlas_kafka_sasl_mechanism(props):
+       """GSSAPI unless TAG_SOURCE_ATLAS_KAFKA_SASL_MECHANISM says otherwise 
(OAUTHBEARER for token files)."""
+       mechanism = props.get(TAG_SOURCE_ATLAS_KAFKA_SASL_MECHANISM_KEY, '')
+       if mechanism is None or mechanism.strip() == '':
+               return KAFKA_SASL_MECHANISM_GSSAPI
+       return mechanism.strip().upper()
+
 def convertInstallPropsToXML(props):
        directKeyMap = 
getPropertiesConfigMap(join(installTemplateDirName,install2xmlMapFileName))
        ret = {}
@@ -233,7 +248,11 @@ def convertInstallPropsToXML(props):
 
        atlas_principal = ''
        atlas_keytab = ''
-       atlas_kafka_kerberos = atlas_kafka_uses_kerberos(props)
+       atlas_oauth_token_url = ''
+       atlas_kafka_sasl = atlas_kafka_uses_sasl(props)
+       atlas_kafka_mechanism = atlas_kafka_sasl_mechanism(props)
+       atlas_kafka_kerberos = atlas_kafka_sasl and atlas_kafka_mechanism == 
KAFKA_SASL_MECHANISM_GSSAPI
+       atlas_kafka_oauth = atlas_kafka_sasl and atlas_kafka_mechanism == 
KAFKA_SASL_MECHANISM_OAUTHBEARER
 
        for k,v in props.items():
                if (k in list(directKeyMap)):
@@ -252,6 +271,12 @@ def convertInstallPropsToXML(props):
                                atlas_principal = v
                        elif (atlas_kafka_kerberos and k == 
TAG_SOURCE_ATLAS_KERBEROS_KEYTAB_KEY):
                                atlas_keytab = v
+                       elif (k == TAG_SOURCE_ATLAS_KAFKA_SASL_MECHANISM_KEY):
+                               if atlas_kafka_sasl:
+                                       atlasOutFile.write(newKey + "=" + 
atlas_kafka_mechanism + "\n")
+                       elif (k == TAG_SOURCE_ATLAS_KAFKA_OAUTH_TOKEN_URL_KEY):
+                               if atlas_kafka_oauth:
+                                       atlas_oauth_token_url = v.strip()
                        else:
                                ret[newKey] = v
                else:
@@ -266,6 +291,19 @@ def convertInstallPropsToXML(props):
                atlasOutFile.write("atlas.jaas.KafkaClient.option.keyTab = " + 
atlas_keytab + "\n")
                atlasOutFile.write("atlas.jaas.KafkaClient.option.principal = " 
+ atlas_principal + "\n")
 
+       if atlas_kafka_oauth:
+               if atlas_oauth_token_url == '':
+                       print("ERROR: %s must be set when %s is %s" % 
(TAG_SOURCE_ATLAS_KAFKA_OAUTH_TOKEN_URL_KEY, 
TAG_SOURCE_ATLAS_KAFKA_SASL_MECHANISM_KEY, KAFKA_SASL_MECHANISM_OAUTHBEARER))
+                       sys.exit(1)
+               # file:// tokens rotate in place, so use the Ranger handler 
that re-reads them on every login
+               if atlas_oauth_token_url.lower().startswith('file:'):
+                       handler = RANGER_OAUTHBEARER_LOGIN_HANDLER
+               else:
+                       handler = KAFKA_OAUTHBEARER_LOGIN_HANDLER
+               
atlasOutFile.write("atlas.kafka.sasl.oauthbearer.token.endpoint.url=" + 
atlas_oauth_token_url + "\n")
+               
atlasOutFile.write("atlas.kafka.sasl.login.callback.handler.class=" + handler + 
"\n")
+               atlasOutFile.write("atlas.kafka.sasl.jaas.config=" + 
KAFKA_OAUTHBEARER_LOGIN_MODULE + " required;" + "\n")
+
        atlasOutFile.close()
 
        if (TAG_SOURCE_ATLAS_ENABLED_KEY in ret):

Reply via email to