Skip to content

AxonOps — AI-Native Control Plane for Open Source Data Platforms

Kafka Delegation Tokens

Delegation tokens provide lightweight, short-lived authentication tokens for Kafka. They enable distributed frameworks (Spark, Flink, Kubernetes Jobs) to access Kafka without distributing primary credentials to every executor.


Use CaseRecommendation
Spark/Flink jobsRecommended
Kubernetes batch jobsRecommended
Short-lived processesRecommended
Temporary access grantsRecommended
Primary authenticationUse SCRAM/Kerberos
Long-running servicesUse SCRAM/Kerberos
Driver.MasterKafka BrokerExecutor.WorkerDriver/MasterDriver/MasterKafka BrokerKafka BrokerExecutor/WorkerExecutor/WorkerInitial AuthenticationAuthenticate (SCRAM/Kerberos)Session establishedToken CreationCreateDelegationToken requestGenerate token(HMAC from master key)Token ID + HMACToken DistributionDistribute token(secure channel)Executor AuthenticationSASL/SCRAM with tokenVerify HMACAuthenticatedExecutor authenticatedwithout primary credentials

Key concepts:

  • Token - Lightweight credential (token ID + HMAC)
  • Owner - User who created the token
  • Renewers - Users allowed to renew the token
  • Master Key - Broker secret for HMAC generation
FeatureBenefit
No credential distributionPrimary credentials stay secure
Short-livedLimited exposure window
RevocableCan be expired immediately
AuditableToken operations logged
Delegated identityActions attributed to owner
FeatureKafka Version
Delegation tokens1.1.0+
Token describe API2.0.0+
SCRAM + delegation tokens1.1.0+

server.properties
# Enable token authentication
delegation.token.master.key=${DELEGATION_TOKEN_MASTER_KEY}
# Token lifetime settings
delegation.token.max.lifetime.ms=604800000 # 7 days
delegation.token.expiry.time.ms=86400000 # 24 hours
delegation.token.expiry.check.interval.ms=3600000 # 1 hour
# Primary authentication (required for token creation)
sasl.enabled.mechanisms=SCRAM-SHA-512
PropertyDefaultDescription
delegation.token.master.keyNoneRequired. Secret for HMAC generation
delegation.token.max.lifetime.ms604800000 (7d)Maximum token lifetime
delegation.token.expiry.time.ms86400000 (24h)Default token expiry
delegation.token.expiry.check.interval.ms3600000 (1h)Expiry check frequency

Master Key Security

The delegation.token.master.key must be:

  • Same across all brokers in the cluster
  • Kept secret (use environment variable)
  • At least 16 characters
  • Changed periodically (invalidates all tokens)
server.properties
# Listeners
listeners=SASL_SSL://0.0.0.0:9093
advertised.listeners=SASL_SSL://kafka1.example.com:9093
security.inter.broker.protocol=SASL_SSL
# SASL configuration
sasl.enabled.mechanisms=SCRAM-SHA-512
sasl.mechanism.inter.broker.protocol=SCRAM-SHA-512
# Delegation tokens
delegation.token.master.key=${DELEGATION_TOKEN_MASTER_KEY}
delegation.token.max.lifetime.ms=604800000
delegation.token.expiry.time.ms=86400000
# JAAS configuration
listener.name.sasl_ssl.scram-sha-512.sasl.jaas.config=\
org.apache.kafka.common.security.scram.ScramLoginModule required \
username="kafka-broker" \
password="broker-password";
# SSL configuration
ssl.keystore.type=PKCS12
ssl.keystore.location=/etc/kafka/ssl/kafka.keystore.p12
ssl.keystore.password=${KEYSTORE_PASSWORD}
ssl.truststore.type=PKCS12
ssl.truststore.location=/etc/kafka/ssl/kafka.truststore.p12
ssl.truststore.password=${TRUSTSTORE_PASSWORD}

Terminal window
# Using primary credentials
kafka-delegation-tokens.sh --bootstrap-server kafka:9093 \
--command-config admin.properties \
--create \
--max-life-time-period 86400000 \
--renewer-principal User:spark-admin
# Output:
# Token ID: abc123-token-id
# HMAC: Ahsx...base64...==
# Owner: User:admin
# Renewers: [User:spark-admin]
# Token expires at: Thu Jan 15 12:00:00 UTC 2026

admin.properties:

security.protocol=SASL_SSL
sasl.mechanism=SCRAM-SHA-512
sasl.jaas.config=org.apache.kafka.common.security.scram.ScramLoginModule required \
username="admin" \
password="admin-password";
ssl.truststore.location=/etc/kafka/ssl/client.truststore.p12
ssl.truststore.password=truststore-password
Terminal window
# List all tokens for current user
kafka-delegation-tokens.sh --bootstrap-server kafka:9093 \
--command-config admin.properties \
--describe
# Describe specific owner's tokens
kafka-delegation-tokens.sh --bootstrap-server kafka:9093 \
--command-config admin.properties \
--describe \
--owner-principal User:spark-user
Terminal window
# Renew token (must be owner or renewer)
kafka-delegation-tokens.sh --bootstrap-server kafka:9093 \
--command-config renewer.properties \
--renew \
--hmac "Ahsx...base64...==" \
--renew-time-period 86400000
Terminal window
# Immediately expire token
kafka-delegation-tokens.sh --bootstrap-server kafka:9093 \
--command-config admin.properties \
--expire \
--hmac "Ahsx...base64...=="

token.properties:

security.protocol=SASL_SSL
sasl.mechanism=SCRAM-SHA-512
sasl.jaas.config=org.apache.kafka.common.security.scram.ScramLoginModule required \
username="abc123-token-id" \
password="Ahsx...base64-hmac...==" \
tokenauth="true";
ssl.truststore.location=/etc/kafka/ssl/client.truststore.p12
ssl.truststore.password=truststore-password

Token as SCRAM Credentials

Delegation tokens use SCRAM authentication with:

  • username = Token ID
  • password = Token HMAC
  • tokenauth="true" = Indicates token authentication
Properties props = new Properties();
props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "kafka1:9093,kafka2:9093");
props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
// Token authentication
props.put("security.protocol", "SASL_SSL");
props.put("sasl.mechanism", "SCRAM-SHA-512");
props.put("sasl.jaas.config",
"org.apache.kafka.common.security.scram.ScramLoginModule required " +
"username=\"" + tokenId + "\" " +
"password=\"" + tokenHmac + "\" " +
"tokenauth=\"true\";");
// TLS configuration
props.put("ssl.truststore.location", "/etc/kafka/ssl/client.truststore.p12");
props.put("ssl.truststore.password", "truststore-password");
KafkaProducer<String, String> producer = new KafkaProducer<>(props);
from confluent_kafka import Producer
config = {
'bootstrap.servers': 'kafka1:9093,kafka2:9093',
'security.protocol': 'SASL_SSL',
'sasl.mechanism': 'SCRAM-SHA-512',
'sasl.username': token_id,
'sasl.password': token_hmac,
'ssl.ca.location': '/etc/kafka/ssl/ca-cert.pem',
}
# Note: confluent-kafka doesn't support tokenauth flag directly
# Token ID/HMAC work as username/password with SCRAM
producer = Producer(config)

// SparkSession with Kafka delegation tokens
val spark = SparkSession.builder()
.appName("KafkaTokenExample")
.config("spark.kafka.security.protocol", "SASL_SSL")
.config("spark.kafka.sasl.mechanism", "SCRAM-SHA-512")
.config("spark.kafka.sasl.jaas.config",
s"""org.apache.kafka.common.security.scram.ScramLoginModule required
|username="$primaryUser"
|password="$primaryPassword";""".stripMargin)
.config("spark.kafka.bootstrap.servers", "kafka1:9093,kafka2:9093")
// Enable delegation token support
.config("spark.security.credentials.kafka.enabled", "true")
.getOrCreate()

Spark automatically obtains and distributes delegation tokens:

spark-defaults.conf
spark.security.credentials.kafka.enabled=true
spark.kafka.clusters.default.bootstrap.servers=kafka1:9093,kafka2:9093
spark.kafka.clusters.default.security.protocol=SASL_SSL
spark.kafka.clusters.default.sasl.mechanism=SCRAM-SHA-512
spark.kafka.clusters.default.sasl.jaas.config=org.apache.kafka.common.security.scram.ScramLoginModule required \
username="spark-user" \
password="spark-password";

For custom token management:

import org.apache.kafka.clients.admin.{AdminClient, AdminClientConfig}
import org.apache.kafka.common.security.token.delegation.DelegationToken
// Driver: Create token
val adminProps = new Properties()
adminProps.put(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, "kafka:9093")
// ... SASL/SSL config
val admin = AdminClient.create(adminProps)
val createResult = admin.createDelegationToken()
val token = createResult.delegationToken().get()
// Broadcast to executors
val tokenBroadcast = spark.sparkContext.broadcast(
(token.tokenInfo().tokenId(), token.hmac())
)
// Executor: Use token
val df = spark.readStream
.format("kafka")
.option("kafka.bootstrap.servers", "kafka:9093")
.option("kafka.security.protocol", "SASL_SSL")
.option("kafka.sasl.mechanism", "SCRAM-SHA-512")
.option("kafka.sasl.jaas.config",
s"""org.apache.kafka.common.security.scram.ScramLoginModule required
|username="${tokenBroadcast.value._1}"
|password="${new String(tokenBroadcast.value._2)}"
|tokenauth="true";""".stripMargin)
.option("subscribe", "my-topic")
.load()

// FlinkKafkaConsumer with delegation token
Properties props = new Properties();
props.setProperty("bootstrap.servers", "kafka1:9093,kafka2:9093");
props.setProperty("group.id", "flink-consumer");
props.setProperty("security.protocol", "SASL_SSL");
props.setProperty("sasl.mechanism", "SCRAM-SHA-512");
props.setProperty("sasl.jaas.config",
"org.apache.kafka.common.security.scram.ScramLoginModule required " +
"username=\"" + tokenId + "\" " +
"password=\"" + tokenHmac + "\" " +
"tokenauth=\"true\";");
FlinkKafkaConsumer<String> consumer = new FlinkKafkaConsumer<>(
"my-topic",
new SimpleStringSchema(),
props
);

apiVersion: batch/v1
kind: Job
metadata:
name: kafka-processor
spec:
template:
spec:
containers:
- name: processor
image: my-processor:latest
env:
- name: KAFKA_TOKEN_ID
valueFrom:
secretKeyRef:
name: kafka-token
key: token-id
- name: KAFKA_TOKEN_HMAC
valueFrom:
secretKeyRef:
name: kafka-token
key: token-hmac
restartPolicy: Never
apiVersion: batch/v1
kind: CronJob
metadata:
name: kafka-token-refresh
spec:
schedule: "0 0 * * *" # Daily
jobTemplate:
spec:
template:
spec:
containers:
- name: token-creator
image: kafka:latest
command:
- /bin/bash
- -c
- |
kafka-delegation-tokens.sh \
--bootstrap-server kafka:9093 \
--command-config /etc/kafka/admin.properties \
--create \
--max-life-time-period 172800000 | \
# Parse and update Kubernetes secret
update-k8s-secret.sh
volumeMounts:
- name: admin-config
mountPath: /etc/kafka
volumes:
- name: admin-config
secret:
secretName: kafka-admin-credentials
restartPolicy: OnFailure

CreatedActiveExpiredToken can authenticateRenewable if withinmax lifetimeToken rejectedCannot be renewedCreateTokenImmediatelyRenewMax lifetimeor explicit expire
EventBehavior
Token expiresNew connections rejected
Existing connectionsRemain valid until disconnect
Renewal attemptedRejected after expiry
Token describedShows expired status

Implement automatic renewal in long-running applications:

public class TokenRenewalService {
private final AdminClient admin;
private final byte[] tokenHmac;
private ScheduledExecutorService scheduler;
public void startRenewalScheduler() {
scheduler = Executors.newSingleThreadScheduledExecutor();
// Renew at 80% of expiry time
long renewalInterval = (long) (expiryTimeMs * 0.8);
scheduler.scheduleAtFixedRate(
this::renewToken,
renewalInterval,
renewalInterval,
TimeUnit.MILLISECONDS
);
}
private void renewToken() {
try {
admin.renewDelegationToken(tokenHmac, renewPeriodMs).get();
log.info("Token renewed successfully");
} catch (Exception e) {
log.error("Token renewal failed", e);
}
}
}

PracticeDescription
Short expiryUse minimum needed lifetime
Limited renewersRestrict who can renew tokens
Secure distributionEncrypted channels only
Audit token usageMonitor token operations
Rotate master keyPeriodically change (invalidates all tokens)
Terminal window
# Never store tokens in:
# - Source code
# - Config files in repositories
# - Logs
# Acceptable storage:
# - Kubernetes Secrets (encrypted at rest)
# - HashiCorp Vault
# - Environment variables (ephemeral)
# - Secure inter-process communication

Token holders have the same permissions as the token owner:

Terminal window
# ACLs apply to the owner, not the token
# If User:admin creates token, token holder has admin's permissions
kafka-acls.sh --bootstrap-server kafka:9093 \
--command-config admin.properties \
--add \
--allow-principal User:spark-user \
--operation Read \
--topic spark-input

ErrorCauseSolution
DelegationTokenDisabledExceptionMaster key not setConfigure delegation.token.master.key
DelegationTokenExpiredExceptionToken expiredCreate new token or renew
DelegationTokenNotFoundExceptionInvalid token IDVerify token ID
DelegationTokenOwnerMismatchExceptionWrong ownerUse owner's credentials to manage
# Broker logging
log4j.logger.kafka.server.DelegationTokenManager=DEBUG
log4j.logger.org.apache.kafka.common.security=DEBUG
Terminal window
# Describe token to check status
kafka-delegation-tokens.sh --bootstrap-server kafka:9093 \
--command-config admin.properties \
--describe
# Test authentication with token
kafka-broker-api-versions.sh --bootstrap-server kafka:9093 \
--command-config token.properties

All brokers must have the same master key. Mismatched keys cause:

  • Tokens created on one broker fail on others
  • Intermittent authentication failures
Terminal window
# Verify by creating token and authenticating to each broker
for broker in kafka1 kafka2 kafka3; do
kafka-broker-api-versions.sh --bootstrap-server $broker:9093 \
--command-config token.properties
done