Architecture¶
Related Notes
messaging/kafka/index | messaging/kafka/how-to-guides | messaging/kafka/explanation | messaging/index
Overview¶
Apache Kafka is a distributed, partitioned, replicated commit-log service. The cluster is composed of three logical roles since Kafka 4.0 (KRaft-only): brokers that store partitioned logs and serve produce/fetch requests, controllers that form a Raft quorum to maintain cluster metadata, and clients (KafkaProducer, KafkaConsumer, AdminClient, Connect workers, Streams applications) that interact via a versioned binary TCP protocol. Topics are partitioned across brokers. Each partition is an append-only log replicated to a configurable number of brokers, with one elected leader serving reads and writes and the others (followers) replicating asynchronously via Fetch requests but only acknowledged once they are caught up to the in-sync replica (ISR) high-watermark.
Component Architecture¶
graph TB
subgraph Clients["Client Layer"]
KP["KafkaProducer<br/>(idempotent / transactional)"]
KC["KafkaConsumer<br/>(group coordinator client)"]
ADM["AdminClient"]
STR["KafkaStreams runtime"]
CON["Kafka Connect Worker"]
end
subgraph Broker["KafkaServer (Broker)"]
SR["SocketServer<br/>(NIO acceptor + processors)"]
RH["KafkaApis<br/>(request router)"]
RM["ReplicaManager"]
LM["LogManager"]
GC["GroupCoordinator<br/>(consumer + share groups)"]
TC["TransactionCoordinator"]
QM["QuotaManager"]
RLM["RemoteLogManager<br/>(KIP-405)"]
KMP["KafkaMetadataPublisher<br/>(metadata cache)"]
end
subgraph Controller["KafkaController (KRaft)"]
QC["QuorumController"]
RAFT["KafkaRaftClient<br/>(__cluster_metadata)"]
SnapStore["MetadataSnapshotStore"]
end
subgraph Storage["On-Disk Storage"]
SEG["LogSegment files<br/>.log / .index / .timeindex / .txnindex"]
OffStore["__consumer_offsets<br/>(50 partitions, compacted)"]
TxnStore["__transaction_state<br/>(50 partitions, compacted)"]
end
subgraph Remote["Remote Tier"]
RSM["RemoteStorageManager<br/>(S3 / GCS / HDFS plugin)"]
RLMM["RemoteLogMetadataManager<br/>(__remote_log_metadata)"]
end
KP -->|Produce v9| SR
KC -->|Fetch v15 / Heartbeat| SR
ADM -->|CreateTopics, AlterConfigs| SR
STR -->|Streams DSL| KP
STR -->|Streams DSL| KC
CON --> KP
CON --> KC
SR --> RH
RH --> RM
RH --> GC
RH --> TC
RH --> QM
RM --> LM
LM --> SEG
GC --> OffStore
TC --> TxnStore
LM --> RLM
RLM --> RSM
RLM --> RLMM
Controller -->|UpdateMetadataRecord<br/>BrokerRegistration<br/>PartitionRecord| KMP
KMP --> RM
QC --> RAFT
RAFT --> SnapStore
style Controller fill:#1f3a5f,color:#fff
style Remote fill:#3a5a3a,color:#fff
style Broker fill:#4a3a3a,color:#fff
Core Components¶
| Component | Role |
|---|---|
| KafkaServer | Top-level broker process. Bootstraps SocketServer, KafkaApis, ReplicaManager, LogManager, GroupCoordinator, TransactionCoordinator, RemoteLogManager. |
| KafkaController (QuorumController) | KRaft active controller. Processes metadata mutations and replicates them through the Raft log. |
| SocketServer | NIO acceptor + processor threads handling TCP connections. Passes parsed requests to a request channel. |
| KafkaApis | Request router — dispatches each ApiKey (Produce, Fetch, Metadata, OffsetCommit, and more) to the appropriate subsystem. |
| ReplicaManager | Owns local replica state. Appends to the LogManager on Produce, serves Fetch from leaders and from followers replicating from the leader. |
| LogManager | Manages partition logs on disk: segment rolling, retention, compaction scheduling, recovery on startup. |
| GroupCoordinator | Manages consumer-group state machine, member heartbeats, partition assignments, share-group state (KIP-932). |
| TransactionCoordinator | Manages producer transactional IDs, transaction markers (commit/abort), fencing of zombie producers. |
| RemoteLogManager (RLM) | Asynchronously copies cold local segments to the remote tier via the configured RemoteStorageManager plugin. |
| KafkaProducer | Client API: serializes records, batches by partition, compresses, applies idempotence sequence numbers, optionally enrolls in transactions. |
| KafkaConsumer | Client API: implements the group rebalance protocol (KIP-848 in 4.0+), commits offsets, supports read_committed isolation. |
| AdminClient | Cluster administration API: topic CRUD, ACL CRUD, dynamic config, group describe/reset, partition reassignment. |
| KafkaStreams | Embedded JVM library implementing KStream/KTable DSL on top of producer + consumer + RocksDB local state stores. |
| Kafka Connect | Distributed worker framework hosting Source/Sink connectors (Debezium, JDBC, S3, Iceberg, and more). |
KRaft Consensus¶
Since Kafka 4.0, the cluster has no ZooKeeper dependency. A small set of nodes (typically 3 or 5) are designated as controllers by setting process.roles=controller (or controller,broker in combined mode for development). One controller is the active controller elected by Raft. The others are passive replicas.
sequenceDiagram
participant Admin as AdminClient
participant Active as Active KafkaController
participant Followers as Follower Controllers
participant MLog as __cluster_metadata log
participant Brokers as Broker Pool
Admin->>Active: CreateTopicsRequest("orders", parts=12, rf=3)
Active->>Active: Validate; allocate partition-to-broker map
Active->>MLog: Append TopicRecord + PartitionRecord(s)
Active->>Followers: Raft AppendEntries
Followers-->>Active: Ack (quorum reached)
Active->>MLog: Mark records committed
Active->>Brokers: Brokers fetch new metadata via Fetch on __cluster_metadata
Brokers->>Brokers: KafkaMetadataPublisher applies records
Brokers-->>Admin: CreateTopicsResponse(success)
Key properties of KRaft:
- Single source of truth: metadata changes are records in the internal
__cluster_metadatatopic, replicated by the same Raft protocol the cluster uses for everything else. - Snapshotting: periodic snapshots prevent the metadata log from growing unbounded.
- Faster failover: there is no ZK session timeout to wait through. Brokers learn of leadership changes via metadata records they are already fetching.
- Fewer moving parts: a 3-node cluster needs only 3 processes in combined mode, vs. 3 brokers + 3 ZK in the legacy world.
controller.quorum.bootstrap.servers(KIP-995, KRaft v1) replaces the oldercontroller.quorum.votersand supports dynamic voter set changes.
A 3-controller quorum tolerates one controller failure. 5 controllers tolerate two. Controllers must run on stable, low-latency hardware — they are the brain of the cluster.
Topic, Partition, and Log¶
A topic is a named ordered stream split into N partitions. Each partition is an append-only sequence of immutable records identified by 64-bit offsets. Records inside a partition have strict order. Across partitions, order is not guaranteed.
graph LR
subgraph Topic["Topic: orders (RF=3, parts=4)"]
direction TB
subgraph P0["Partition 0"]
S0a["Segment 00000000.log"]
S0b["Segment 00012450.log (active)"]
end
subgraph P1["Partition 1"]
S1a["Segment 00000000.log"]
S1b["Segment 00018932.log (active)"]
end
P2["Partition 2"]
P3["Partition 3"]
end
P0 -->|Leader| KSa["KafkaServer A"]
P0 -->|Follower| KSb["KafkaServer B"]
P0 -->|Follower| KSc["KafkaServer C"]
P1 -->|Leader| KSb
P1 -->|Follower| KSa
P1 -->|Follower| KSc
Log-Structured Storage Internals¶
The log directory of each partition contains a series of segments:
| File | Purpose |
|---|---|
<base-offset>.log |
Append-only batch records (magic v2 record-batch format). |
<base-offset>.index |
Sparse offset → physical position index for the segment. |
<base-offset>.timeindex |
Sparse timestamp → offset index (for time-based seek). |
<base-offset>.txnindex |
Aborted transaction index (used by read_committed consumers). |
<base-offset>.snapshot |
Producer state snapshot (idempotence sequence numbers). |
leader-epoch-checkpoint |
Per-leader-epoch end offsets for truncation safety. |
The active segment receives writes. When it gets to segment.bytes (default 1 GiB) or segment.ms, it is rolled and a new active segment is opened. Old segments become eligible for retention deletion or, with tiered storage, remote upload.
Record-Batch Format (Magic v2)¶
A record batch on disk includes (per Apache Kafka source):
baseOffset: int64
batchLength: int32
partitionLeaderEpoch: int32
magic: int8 (= 2 in current Kafka)
crc: uint32
attributes: int16 (compression: 0=none, 1=gzip, 2=snappy, 3=lz4, 4=zstd; +flags)
lastOffsetDelta: int32
baseTimestamp: int64
maxTimestamp: int64
producerId: int64 <-- idempotence
producerEpoch: int16 <-- idempotence
baseSequence: int32 <-- idempotence
recordsCount: int32
records: [Record] <-- compressed as a unit
Compression is applied to the entire records block. This gives better ratios than per-record compression. This is also what makes the zero-copy sendfile() path so effective: the broker streams the on-disk compressed batch directly to the consumer socket without decompressing or copying through user space.
Log Compaction¶
In addition to time/size retention (cleanup.policy=delete), Kafka supports cleanup.policy=compact. The log cleaner thread periodically rewrites segments retaining only the most recent record per key, which makes compacted topics suitable for state-snapshot use cases (changelog topics for Streams state stores, __consumer_offsets, __transaction_state, K8s configmap-style topics).
Tunables:
log.cleaner.min.compaction.lag.ms— minimum age before a record is eligible for cleaning.log.cleaner.max.compaction.lag.ms— maximum delay before uncompacted head is forced to compact.min.cleanable.dirty.ratio— fraction of dirty bytes triggering a clean.
Tombstones (records with a null value) are retained for delete.retention.ms to make sure that replicas observe the deletion before it is purged.
Replication and ISR¶
Each partition has a leader and N-1 followers (where N = replication.factor). Followers issue Fetch requests to the leader exactly like consumers, with one extra protocol affordance: when the fetch offset of a follower gets to the log-end-offset of the leader, the leader counts it as in-sync (ISR). A write becomes committed when all in-sync replicas replicate it. Only then can a read_committed consumer see it, and only then does the high-watermark advance.
sequenceDiagram
participant Producer as KafkaProducer
participant Leader as KafkaServer (Leader)
participant F1 as KafkaServer (Follower 1)
participant F2 as KafkaServer (Follower 2)
Producer->>Leader: ProduceRequest acks=all batch[B]
Leader->>Leader: ReplicaManager.appendToLocalLog(B)
par Replication
F1->>Leader: FetchRequest(offset=N)
Leader-->>F1: FetchResponse[B]
F1->>F1: append + advance LEO
and
F2->>Leader: FetchRequest(offset=N)
Leader-->>F2: FetchResponse[B]
F2->>F2: append + advance LEO
end
F1->>Leader: FetchRequest(offset=N+1) (ack)
F2->>Leader: FetchRequest(offset=N+1) (ack)
Leader->>Leader: HighWatermark advances to N+1
Leader-->>Producer: ProduceResponse(offset=N, ack)
| Setting | Purpose |
|---|---|
replication.factor |
Total replicas per partition (typically 3 in production). |
min.insync.replicas |
Minimum replicas that must acknowledge for acks=all to succeed (typically 2 for RF=3). |
acks=all |
Producer waits for all ISR. Combined with idempotence, this gives the strongest delivery guarantees. |
unclean.leader.election.enable |
If true, a non-ISR replica can become leader (data loss risk). Default false. |
Eligible Leader Replicas (KIP-966, GA 4.0)¶
Pre-4.0, when the ISR shrank below min.insync.replicas, the high-watermark did not advance and the partition went read-only. KIP-966 introduced Eligible Leader Replicas (ELR) stored in the partition record. The KRaft controller now picks a leader in this priority: ISR (if non-empty) → ELR (if non-empty and not fenced) → last known leader (if unfenced). This restores availability for many ISR-shrinkage scenarios that previously required unclean.leader.election=true. ELR is enabled by default on new clusters from Kafka 4.1.
Consumer Group Protocol¶
Consumers join a group identified by group.id. The GroupCoordinator on a designated broker assigns partitions to members. Pre-KIP-848 ("classic" / "generic" protocol) used a stop-the-world rebalance triggered by JoinGroup → SyncGroup. KIP-848 (GA in 4.0) introduces the next-generation consumer rebalance protocol that pushes assignment computation to the broker and applies changes incrementally — no global synchronization barrier.
stateDiagram-v2
[*] --> Joining: subscribe(topics)
Joining --> Stable: Coordinator computes assignment & member ACKs
Stable --> Reassigning: Member added/removed/heartbeat lost
Reassigning --> Stable: Incremental partition revoke + assign
Stable --> Dead: close()
Dead --> [*]
Other group protocols delivered after KIP-848:
- Streams Rebalance Protocol (KIP-1071, early access in 4.1, GA in 4.2): broker-side task assignment for Streams applications.
- Share Groups (Queues for Kafka) (KIP-932, preview 4.1, GA 4.2): per-record acknowledgement enabling competing-consumer (queue) semantics rather than partition-exclusive ownership.
Exactly-Once Semantics¶
EOS in Kafka builds on two primitives:
- Idempotent Producer (default in 4.x). The broker assigns each producer a
producerId(PID) and tracks per-partition sequence numbers. Duplicate retries are deduplicated server-side. The broker rejects out-of-sequence batches withOutOfOrderSequenceException. - Transactions. A producer with
transactional.id=...callsinitTransactions(), then groups multiple sends and offset commits insidebeginTransaction()/commitTransaction(). Atomic commit/abort is implemented via transaction markers appended to each touched partition by the TransactionCoordinator. Consumers configured withisolation.level=read_committedskip aborted records using the per-segment.txnindex.
Read-process-write loops (the canonical Kafka Streams pattern) achieve end-to-end exactly-once by including the offset commit of the consumer in the transaction of the producer via producer.sendOffsetsToTransaction(...). See the official design doc on Exactly-Once Semantics for the full state machine.
sequenceDiagram
participant App as Kafka Streams Task
participant Prod as Transactional Producer
participant TC as TransactionCoordinator
participant Cons as Consumer
participant T1 as Topic A (input)
participant T2 as Topic B (output)
participant OS as __consumer_offsets
Cons->>T1: poll() -> records[K, V]
App->>Prod: beginTransaction()
Prod->>TC: AddPartitionsToTxn(topic-B-partitions, __consumer_offsets-partition)
Prod->>T2: send(transformed records)
Prod->>OS: sendOffsetsToTransaction(consumer offsets)
Prod->>TC: commitTransaction()
TC->>T2: write commit marker
TC->>OS: write commit marker
Note over Cons: read_committed consumers now see records & advanced offsets atomically
Kafka Streams, Connect, ksqlDB¶
| Layer | Built On | Purpose |
|---|---|---|
| Kafka Streams | Producer + Consumer + RocksDB local stores | Embedded JVM stream-processing library. KStream/KTable DSL, windows, joins, exactly-once via transactions. |
| Kafka Connect | Distributed worker framework | Hosts source connectors (for example, Debezium MySQL/Postgres CDC) and sink connectors (S3, Iceberg, Snowflake, Elastic). Stores task state in 3 internal compacted topics: connect-offsets, connect-configs, connect-status. |
| ksqlDB | Streams + REST API | SQL-like declarative engine for continuous queries over Kafka topics. |
Tiered Storage (KIP-405)¶
Kafka 3.6 made tiered storage generally available. Brokers retain only "hot" segments locally. Cold segments are uploaded to a remote object store via a pluggable RemoteStorageManager.
flowchart TB
subgraph LocalTier["Local Tier (broker disk)"]
Active["Active segment (writes)"]
Hot["Recent segments (page cache hits)"]
end
subgraph RemoteTier["Remote Tier (S3/GCS/HDFS)"]
Cold["Cold segments<br/>(uploaded by RemoteLogManager)"]
end
subgraph Metadata["Remote Log Metadata"]
RLMM["__remote_log_metadata<br/>(per-segment metadata)"]
end
Active -->|roll| Hot
Hot -->|local.retention.ms<br/>elapsed| RemoteLogManager
RemoteLogManager -->|put| Cold
RemoteLogManager -->|append metadata| RLMM
Consumer["Consumer historical fetch"] -->|Fetch (cold offset)| RemoteLogManager
RemoteLogManager -->|get| Cold
RemoteLogManager -->|stream to consumer| Consumer
Per-topic configuration:
remote.storage.enable=truelocal.retention.ms— how long to keep segments locally after upload (typically minutes to hours).retention.ms— total (local + remote) retention before deletion.segment.bytes— segment roll size (smaller = more upload granularity, more metadata overhead).
The reference S3 plugin (org.apache.kafka.server.log.remote.storage.S3RemoteStorageManager and similar Aiven/Confluent implementations) handles segment upload and historical fetch streaming.
Request Flow Examples¶
Produce Path¶
sequenceDiagram
participant App
participant KP as KafkaProducer
participant Net as Network thread
participant SS as SocketServer
participant KA as KafkaApis
participant RM as ReplicaManager
participant LM as LogManager
participant Disk as LogSegment
App->>KP: send(ProducerRecord)
KP->>KP: serialize, partition, batch by topic-partition
KP->>KP: assign idempotence sequence
Net->>SS: ProduceRequest v9
SS->>KA: dispatch
KA->>RM: appendRecords(timeout, acks=all, batch)
RM->>LM: append to leader log
LM->>Disk: append + fsync (per flush policy)
RM-->>KA: produce result (delayed if acks=all until ISR)
KA-->>Net: ProduceResponse(offset)
Net-->>KP: callback success
Fetch Path (with Zero-Copy)¶
sequenceDiagram
participant KC as KafkaConsumer
participant SS as SocketServer
participant KA as KafkaApis
participant RM as ReplicaManager
participant LM as LogManager
participant Kernel as Linux kernel sendfile()
KC->>SS: FetchRequest v15 (topic, partition, offset)
SS->>KA: dispatch
KA->>RM: fetchMessages(maxWait, minBytes)
RM->>LM: read(partition, offset, maxBytes)
LM-->>RM: FileRecords (mmap'd on-disk batch)
RM-->>KA: FetchResponse with FileRecords reference
KA->>Kernel: sendfile(socket_fd, file_fd, offset, len)
Note over Kernel: bytes go disk -> NIC without copy through userspace
Kernel-->>KC: response bytes
Benchmarks¶
The throughput numbers in the sections below come from public sources. Reproduce them in your own environment before sizing.
LinkedIn (2014) — original Kafka benchmark¶
- 3 brokers, commodity hardware: Intel Xeon, 6×7200 RPM SATA, 32 GiB RAM, 1 GbE.
- 2,024,032 msgs/sec with 100-byte messages on a single producer to a 6-partition / RF=3 topic.
- Sustained writes scale linearly with partitions until the disk subsystem saturates.
Confluent OpenMessaging Benchmark (2020) — Kafka vs Pulsar vs RabbitMQ¶
- Test rig: 3 ×
i3en.2xlargebrokers (8 vCPU, 64 GiB RAM, 2 × 2.5 TB NVMe, 25 Gbps). - Topic: 100 partitions, RF=3, snappy compression, 1 KiB messages.
- Peak stable throughput: ~605 MB/s (Kafka), with consumers keeping up.
- Lower p99 publish latency than Pulsar at the same throughput in this rig.
Source: Confluent — "Benchmarking RabbitMQ vs Kafka vs Pulsar Performance".
Dell EMC / Confluent Platform Characterization¶
- Larger configuration with 13 producers writing concurrently.
- 18,623,322 records/sec aggregate (1,776 MB/s), 83 ms average producer latency, 650M records over the test run.
- Best per-producer throughput observed at 9–20 partitions per topic.
Source: Dell EMC — Confluent Kafka Performance Characterization white paper.
Confluent Cloud (Kora) vs Apache Kafka¶
- Confluent claims Kora delivers >10× lower tail latency at 5.6 GB/s aggregate (1.4 GB/s ingress + 4.2 GB/s egress) vs vanilla Apache Kafka.
- This is a vendor benchmark of the cloud-native rewrite. Reproduce it on your workload before drawing conclusions.
Source: Confluent — "Apache Kafka vs Confluent Cloud Latency Benchmarking".
Practical Sizing Heuristics¶
| Cluster size | Brokers | Sustained ingress | Topic / partition budget |
|---|---|---|---|
| Dev | 1 (KRaft combined) | <50 MB/s | <100 partitions |
| Small prod | 3 brokers (RF=3) | 100–500 MB/s | a few thousand partitions |
| Medium prod | 6–12 brokers | 500 MB/s – 2 GB/s | 10k–50k partitions |
| Large prod | 30+ brokers, tiered storage | 2 GB/s+ | 100k+ partitions |
Benchmark Caveat
Throughput is heavily influenced by message size, compression codec (zstd often beats snappy on CPU-rich brokers), batch size, network latency between producer and broker, replication factor, and disk subsystem. Treat published numbers as upper bounds.
Sources¶
- Apache Kafka design documentation
- KIP-405: Tiered Storage
- KIP-848: The Next Generation of the Consumer Rebalance Protocol
- KIP-932: Queues for Kafka
- KIP-966: Eligible Leader Replicas
- KIP-1071: Streams Rebalance Protocol
- Apache Kafka 4.0 release notes
- Confluent — Apache Kafka Performance
Security¶
Related Notes
messaging/kafka/index | messaging/kafka/explanation | messaging/kafka/how-to-guides | messaging/index
Threat Model¶
| Threat Vector | Impact | Mitigation |
|---|---|---|
| Broker compromise | Direct read of all topic data on disk. Ability to forge produce/fetch responses | Disk encryption (LUKS / EBS encryption). Restrict OS access. Audit kafka-authorizer.log. Network isolation |
| KRaft controller compromise | Forge metadata records (create topics, alter ACLs). Rewire partition leadership | Run controllers in isolated mode on hardened hosts. Require mTLS between broker and controller listener. Restrict controller.listener.names to internal-only network paths |
| Client impersonation | Producer writes records as another principal. Consumer reads topics they should not | SASL or mTLS authentication everywhere. ACLs with explicit deny defaults |
| Data exfiltration via consumer | Authenticated consumer subscribes to sensitive topics and exfiltrates | Topic-scoped ACLs (Read on Topic:payments granted to specific principals only). Per-user consumer_byte_rate quotas. Egress monitoring |
| MITM on wire | Eavesdrop produce/fetch traffic. Downgrade to PLAINTEXT | TLS on every listener (SSL or SASL_SSL). Disable PLAINTEXT listeners on prod. Pin TLS 1.2+ |
| Replay attacks | Re-send captured produce requests to duplicate writes | Idempotent producer (PID + sequence number deduplication). Transactional producer with fencing |
| JAAS module injection (CVE-2025-27818) | Attacker with AlterConfigs permission triggers RCE via LDAP login module |
Apply 3.9.1/4.0.0+. Set org.apache.kafka.disallowed.login.modules. Restrict who can call AlterConfigs |
| OAUTHBEARER URL abuse (CVE-2025-27817) | Arbitrary file read / SSRF through sasl.oauthbearer.token.endpoint.url |
Apply 3.9.1/4.0.0+. Set -Dorg.apache.kafka.sasl.oauthbearer.allowed.urls=https://idp.example.com/... |
| MM2 cross-cluster credential leak | Compromised replicator can read source and write target | Separate principals for MM2 source/target. Least-privilege ACLs on internal MM2 topics. Encrypt MM2 worker-to-broker traffic |
| ZooKeeper compromise (legacy clusters) | Read/alter cluster metadata directly | Migrate off ZK to KRaft (Kafka 4.0+). Never expose ZK to untrusted networks. SASL on ZK if you must keep it |
| Sensitive logging | Credentials/payloads written to broker logs at DEBUG level | Keep root logger at INFO. Never enable NetworkClient DEBUG in prod (KIP-714 / CVE related disclosures) |
Authentication (SASL & mTLS)¶
Kafka supports several authentication mechanisms on each listener. The mechanism is selected via security.protocol (transport) and sasl.mechanism (when SASL is in use).
Listener Configuration¶
# server.properties — three listeners with three protocols
listeners=PLAINTEXT://:9092,SASL_SSL://:9094,SSL://:9095
advertised.listeners=PLAINTEXT://broker1:9092,SASL_SSL://broker1.example.com:9094,SSL://broker1.example.com:9095
listener.security.protocol.map=PLAINTEXT:PLAINTEXT,SASL_SSL:SASL_SSL,SSL:SSL,CONTROLLER:SSL
inter.broker.listener.name=SSL # broker-to-broker uses mTLS
sasl.enabled.mechanisms=SCRAM-SHA-512,OAUTHBEARER
sasl.mechanism.inter.broker.protocol=SCRAM-SHA-512
SASL Mechanisms¶
| Mechanism | When to Use | Strengths | Weaknesses |
|---|---|---|---|
| PLAIN | Lab / over a TLS-only listener with an external trust boundary | Simple. Widely supported | Sends plaintext password to broker (must be wrapped in TLS) |
| SCRAM-SHA-256 / SCRAM-SHA-512 | General-purpose username/password without an external IdP | No password on the wire (challenge-response). Credentials stored in cluster metadata | SCRAM-without-TLS is exploitable — always pair with TLS |
| GSSAPI (Kerberos) | Enterprise environments with existing Active Directory / Heimdal KDC | Strong mutual auth. Well-understood by ops | Heavyweight client setup. Keytab management |
| OAUTHBEARER | Modern microservices. Integration with corporate IdP (Keycloak, Okta, Auth0, Azure AD) | Token-based. Integrates with OAuth 2.0 / OIDC. Short-lived credentials | Default Kafka implementation creates unsecured JWTs (RFC 7515 unsecured) — usable only for dev. Production deployments need a real OIDC validator |
SCRAM Setup Example¶
# Create a SCRAM-SHA-512 credential for User:alice
bin/kafka-configs.sh --bootstrap-server kafka:9094 \
--command-config admin.properties \
--alter --add-config 'SCRAM-SHA-512=[iterations=8192,password=ChangeMeNow]' \
--entity-type users --entity-name alice
# Client config snippet
sasl.mechanism=SCRAM-SHA-512
security.protocol=SASL_SSL
sasl.jaas.config=org.apache.kafka.common.security.scram.ScramLoginModule required \
username="alice" password="ChangeMeNow";
OAUTHBEARER (Production with OIDC)¶
# Client side — exchange a refresh token at the IdP, get a short-lived JWT
security.protocol=SASL_SSL
sasl.mechanism=OAUTHBEARER
sasl.login.callback.handler.class=org.apache.kafka.common.security.oauthbearer.OAuthBearerLoginCallbackHandler
sasl.oauthbearer.token.endpoint.url=https://idp.example.com/realms/prod/protocol/openid-connect/token
sasl.jaas.config=org.apache.kafka.common.security.oauthbearer.OAuthBearerLoginModule required \
clientId="orders-svc" clientSecret="${env:OIDC_CLIENT_SECRET}";
# Broker side — validate JWTs against the IdP's JWKS
listener.name.sasl_ssl.oauthbearer.sasl.server.callback.handler.class=\
org.apache.kafka.common.security.oauthbearer.OAuthBearerValidatorCallbackHandler
listener.name.sasl_ssl.oauthbearer.sasl.oauthbearer.jwks.endpoint.url=\
https://idp.example.com/realms/prod/protocol/openid-connect/certs
listener.name.sasl_ssl.oauthbearer.sasl.oauthbearer.expected.audience=kafka
listener.name.sasl_ssl.oauthbearer.sasl.oauthbearer.expected.issuer=\
https://idp.example.com/realms/prod
# Required since 4.0 — explicitly allow-list the JWKS / token endpoints
listener.name.sasl_ssl.oauthbearer.sasl.oauthbearer.allowed.urls=\
https://idp.example.com/realms/prod/protocol/openid-connect/certs,\
https://idp.example.com/realms/prod/protocol/openid-connect/token
OAUTHBEARER in 4.0+
From Kafka 4.0, sasl.oauthbearer.allowed.urls defaults to empty as a hardening for CVE-2025-27817. Operators must list trusted IdP URLs explicitly. In 3.9.1, all URLs are allowed for backward compatibility but the operator must set the JVM property -Dorg.apache.kafka.sasl.oauthbearer.allowed.urls.
Mutual TLS (mTLS / SSL)¶
# server.properties — full TLS listener with client-cert auth
listeners=SSL://:9095
ssl.keystore.location=/etc/kafka/tls/server.keystore.jks
ssl.keystore.password=${env:KAFKA_KEYSTORE_PASSWORD}
ssl.key.password=${env:KAFKA_KEY_PASSWORD}
ssl.truststore.location=/etc/kafka/tls/server.truststore.jks
ssl.truststore.password=${env:KAFKA_TRUSTSTORE_PASSWORD}
ssl.client.auth=required
ssl.enabled.protocols=TLSv1.3,TLSv1.2
ssl.cipher.suites=TLS_AES_256_GCM_SHA384,TLS_CHACHA20_POLY1305_SHA256
ssl.endpoint.identification.algorithm=https
The Subject DN of the client (or any field selected via ssl.principal.mapping.rules) becomes the principal used for ACL evaluation.
Authorization (ACLs)¶
Kafka uses a default-deny ACL model when an authorizer is configured. The built-in standard authorizer is org.apache.kafka.metadata.authorizer.StandardAuthorizer (KRaft-native). The older kafka.security.authorizer.AclAuthorizer was tied to ZooKeeper and is removed in 4.0.
# server.properties
authorizer.class.name=org.apache.kafka.metadata.authorizer.StandardAuthorizer
super.users=User:admin;User:CN=cluster-admin
allow.everyone.if.no.acl.found=false # default deny
Common ACL Recipes¶
# Producer permissions on a topic
bin/kafka-acls.sh --bootstrap-server kafka:9094 --command-config admin.properties \
--add --allow-principal User:orders-svc \
--producer --topic orders
# Consumer permissions for a specific group
bin/kafka-acls.sh --bootstrap-server kafka:9094 --command-config admin.properties \
--add --allow-principal User:billing-svc \
--consumer --topic orders --group billing-aggregator
# Prefix-based topic ACL (every topic starting with "events.")
bin/kafka-acls.sh --bootstrap-server kafka:9094 --command-config admin.properties \
--add --allow-principal User:platform-events \
--producer --topic events. --resource-pattern-type prefixed
# Transactional producer (transactional.id ACL)
bin/kafka-acls.sh --bootstrap-server kafka:9094 --command-config admin.properties \
--add --allow-principal User:orders-svc \
--operation Write --operation Describe \
--transactional-id orders-svc-txn-1
# Cluster admin (use sparingly)
bin/kafka-acls.sh --bootstrap-server kafka:9094 --command-config admin.properties \
--add --allow-principal User:CN=cluster-admin \
--operation All --cluster
OPA Integration¶
For policy-as-code, several open-source authorizers wrap the Kafka authorizer SPI to delegate decisions to Open Policy Agent (for example, Bisnode's kafka-open-policy-agent-plugin, Aiven's similar plugin, and StyraHub's Rego-based examples). The plugin runs in-broker, calls OPA over localhost, and caches decisions. This gives fine-grained, attribute-based authorization (time-of-day, business unit, message-attribute checks) that vanilla ACLs cannot express.
Encryption¶
TLS In-Transit¶
Use TLS on every listener that crosses an untrusted boundary:
- Client → broker
- Broker → broker (
inter.broker.listener.name) - Broker → controller (
controller.listener.names) - MirrorMaker 2 source → MM2 worker → target
Pin ssl.enabled.protocols=TLSv1.3,TLSv1.2. Disable older protocols. Use cert-manager (or the built-in CA of the Strimzi operator) to rotate certificates automatically — Strimzi rotates the cluster CA every 365 days by default.
Encryption At Rest¶
Apache Kafka does not ship native end-to-end record encryption. Options:
| Layer | Approach | Notes |
|---|---|---|
| Disk | LUKS / EBS volume encryption | Transparent. Broker is unaware. Standard practice. |
| Application | Client-side envelope encryption (Vault Transit / KMS) | Producer encrypts before send. Consumer decrypts after receive. Schema/contract for the key-id header. |
| Kafka Connect | SMT (Single Message Transform) for field-level encryption | Several community SMTs implement field-level encryption with KMS-backed DEKs. |
| Confluent Platform | Client-Side Field-Level Encryption (CSFLE) | Commercial. Integrates with Schema Registry tags. |
Native FS encryption
There is no shipped KIP that gives Apache Kafka transparent broker-side record encryption with operator-managed keys (analogous to Pulsar's encryption support). The community discussed several KIPs (KIP-317, KIP-1124), but as of Kafka 4.2 nothing is GA. Until then, disk-encryption + client-side encryption for sensitive fields is the standard layered defense.
Audit Logging¶
Apache Kafka does not ship a dedicated "audit log" subsystem. The de facto audit mechanism is the kafka.authorizer.logger log4j logger, which the StandardAuthorizer (and AclAuthorizer historically) writes to:
- INFO: every
Denydecision is logged with principal, host, operation, and resource. - DEBUG: every
Allowdecision is logged (off by default. High volume).
# log4j2.properties (Kafka 4.x uses log4j2)
appender.authorizer.type = RollingFile
appender.authorizer.name = AuthorizerFile
appender.authorizer.fileName = ${sys:kafka.logs.dir}/kafka-authorizer.log
appender.authorizer.filePattern = ${sys:kafka.logs.dir}/kafka-authorizer.log.%d{yyyy-MM-dd-HH}
appender.authorizer.layout.type = PatternLayout
appender.authorizer.layout.pattern = [%d] %p %m (%c)%n
appender.authorizer.policies.type = Policies
appender.authorizer.policies.time.type = TimeBasedTriggeringPolicy
appender.authorizer.policies.time.interval = 1
logger.authorizer.name = kafka.authorizer.logger
logger.authorizer.level = INFO # raise to DEBUG to also log Allows
logger.authorizer.appenderRef.file.ref = AuthorizerFile
logger.authorizer.additivity = false
A typical authorizer log entry:
[2026-04-28 14:32:11,455] INFO Principal = User:CN=consumer is Denied Operation = Read
from host = 10.0.4.17 on resource = Topic:LITERAL:payments for request = Fetch
with resourceRefCount = 1 (kafka.authorizer.logger)
For richer audit trails:
- Confluent Platform ships
confluent.security.event.routerwhich publishes audit events to a dedicated audit topic in JSON CloudEvents format. - Conduktor, Lenses.io, and proxy-based products (for example, Kroxylicious) provide topic-level proxy logging for record-level audit (which records were consumed by which principal).
- Forward
kafka-authorizer.logto a SIEM (Splunk, Elastic, Loki, Datadog) for retention and alerting. Alert on bursts of Deny decisions.
MirrorMaker 2 Cross-Cluster Replication Security¶
# mm2.properties — secure SASL_SSL on both source and target clusters
clusters = src, dst
src.bootstrap.servers = src-broker1:9094,src-broker2:9094,src-broker3:9094
dst.bootstrap.servers = dst-broker1:9094,dst-broker2:9094,dst-broker3:9094
src.security.protocol = SASL_SSL
src.sasl.mechanism = SCRAM-SHA-512
src.sasl.jaas.config = org.apache.kafka.common.security.scram.ScramLoginModule required \
username="mm2-replicator" password="${env:MM2_SRC_PASSWORD}";
src.ssl.truststore.location = /etc/mm2/src-truststore.jks
src.ssl.truststore.password = ${env:MM2_SRC_TRUST_PASSWORD}
dst.security.protocol = SASL_SSL
dst.sasl.mechanism = SCRAM-SHA-512
dst.sasl.jaas.config = org.apache.kafka.common.security.scram.ScramLoginModule required \
username="mm2-replicator" password="${env:MM2_DST_PASSWORD}";
dst.ssl.truststore.location = /etc/mm2/dst-truststore.jks
dst.ssl.truststore.password = ${env:MM2_DST_TRUST_PASSWORD}
src->dst.enabled = true
src->dst.topics = orders.*, events.*
src->dst.replication.factor = 3
src->dst.sync.topic.acls.enabled = true
src->dst.sync.group.offsets.enabled = true
Best practices:
- Use separate dedicated principals for MM2 in the source (read-only) and target (write + create-internal-topics).
- Restrict MM2 internal topics (
mm2-offsets.dst.internal,mm2-status.dst.internal,mm2-configs.dst.internal,heartbeats,*.checkpoints.internal) with ACLs. Only the MM2 worker principal needs to write them. - Pin TLS 1.2+ on both clusters. Verify the certificate chain of the target cluster in the MM2 truststore.
- Replicate ACLs (
sync.topic.acls.enabled=true) so failover does not silently break authorization. - Encrypt the local Connect state directories of the MM2 worker host. Treat MM2 workers as part of the Kafka security boundary.
Recent CVEs (2024–2025)¶
| CVE | Score | Title | Fixed In |
|---|---|---|---|
| CVE-2025-27817 | 7.5 (HIGH) | Apache Kafka Client SASL/OAUTHBEARER arbitrary file read & SSRF via sasl.oauthbearer.token.endpoint.url / jwks.endpoint.url |
3.9.1, 4.0.0 (set sasl.oauthbearer.allowed.urls) |
| CVE-2025-27818 | 8.8 (HIGH) | Authenticated AlterConfigs operator can configure LdapLoginModule JAAS to trigger Java deserialization RCE on broker / Connect worker |
3.9.1, 4.0.0 (disallowed.login.modules defaults updated) |
| CVE-2025-27819 | 7.5 (HIGH) | SASL JAAS configuration vulnerability allowing RCE/DoS for principals holding AlterConfigs on cluster resource |
3.9.1, 4.0.0 |
| CVE-2024-31141 | 6.5 (MEDIUM) | Kafka Client ConfigProvider plugins (FileConfigProvider, DirectoryConfigProvider, EnvVarConfigProvider) allow disclosure of disk content / env vars when client config is supplied by an untrusted party |
Documented mitigation. Restrict who supplies client configs |
| CVE-2024-27309 | (NVD) | MirrorMaker SCRAM credential exposure under specific replication settings |
Mitigation guidance in advisory |
Patching Cadence
Every major Kafka redistributor backported the 2025 CVE cluster (27817/27818/27819) (Confluent, Strimzi, Bitnami, AWS MSK, Azure Event Hubs). If you operate Kafka 3.x, upgrade to 3.9.1 minimum and apply the documented JVM properties. For new deployments, target Kafka 4.1+ which has the hardened defaults (empty allowed.urls, disallowed login modules) baked in.
The complete list of Apache-disclosed Kafka vulnerabilities is maintained at kafka.apache.org/cve-list.html.
Sources¶
- Apache Kafka — Security documentation
- Apache Kafka — Authentication using SASL
- Apache Kafka — Authorization and ACLs
- Apache Kafka — CVE list
- NVD — CVE-2025-27817
- NVD — CVE-2025-27818
- NVD — CVE-2025-27819
- Confluent Developer — Audit Logs with Log4j
- Strimzi — Securing Kafka
- NetApp Instaclustr — Multiple Kafka CVEs (June 2025)