Happy Dussehra! May good always win. Happy Diwali! Wishing you a bright year ahead. Merry Christmas! Warm wishes from Vizag. Happy New Year! Here’s to a great year ahead.

Archive · DevOps

Self-Hosted Kafka: Security, Monitoring and Day-Two Operations

Encrypt and authenticate a KRaft cluster with TLS and SCRAM, set ACLs and quotas, monitor the signals that matter, and run upgrades, rebalancing and disaster recovery.

5 min read

  • Apache Kafka 4.0
  • SASL/SCRAM
  • TLS
  • Prometheus JMX exporter
  • MirrorMaker 2

A cluster on a private network still needs authentication, encryption and monitoring. This post hardens the KRaft cluster from the previous post and covers routine operations. The settings apply to Kafka 4.0 and to Kafka 4.1, which was released on 2 September 2025.

Security design

flowchart TB
  P[Producers and consumers<br/>SCRAM user with TLS] -->|SASL SSL 9092| B[Brokers]
  A[Admin tools<br/>admin user] -->|SASL SSL 9092| B
  B <-->|mutual TLS 9093| C[Controllers]
  B <-->|SASL SSL replication| B2[Other brokers]
  CA[Private CA] -.->|issues certificates| B
  CA -.-> C
  B --> ACL[ACL authorizer<br/>StandardAuthorizer]
  PR[Prometheus] -->|JMX exporter 7071| B
  PR --> C
Channel Protection
Clients to brokers TLS encryption and SASL/SCRAM-SHA-512 authentication
Broker to broker The same SASL_SSL listener, with an admin identity
Broker and controller Mutual TLS on the controller listener
Authorisation ACLs per user, deny by default

Certificates

Issue one certificate per node with its DNS names as subject alternative names, and keep the CA key offline:

openssl req -new -newkey rsa:4096 -nodes -keyout kb1.key -out kb1.csr \
  -subj "/CN=kb1.corp.example" -addext "subjectAltName=DNS:kb1.corp.example,DNS:kb1"
openssl x509 -req -in kb1.csr -CA ca.crt -CAkey ca.key -CAcreateserial -days 365 \
  -copy_extensions copy -out kb1.crt
openssl pkcs12 -export -in kb1.crt -inkey kb1.key -certfile ca.crt -name kb1 -out kb1.p12
keytool -importcert -noprompt -alias ca -file ca.crt -keystore ca.p12 -storetype PKCS12

Track expiry dates and rotate before they pass; Kafka can reload broker keystores dynamically through kafka-configs.sh, which avoids a restart.

Broker configuration

listeners=SASL_SSL://:9092,CONTROLLER://:9093
advertised.listeners=SASL_SSL://kb1.corp.example:9092
listener.security.protocol.map=SASL_SSL:SASL_SSL,CONTROLLER:SSL
inter.broker.listener.name=SASL_SSL
sasl.enabled.mechanisms=SCRAM-SHA-512
sasl.mechanism.inter.broker.protocol=SCRAM-SHA-512
listener.name.sasl_ssl.scram-sha-512.sasl.jaas.config=org.apache.kafka.common.security.scram.ScramLoginModule required username="admin" password="<from a secret store>";
ssl.keystore.type=PKCS12
ssl.keystore.location=/etc/kafka/tls/kb1.p12
ssl.keystore.password=<from a secret store>
ssl.truststore.type=PKCS12
ssl.truststore.location=/etc/kafka/tls/ca.p12
ssl.truststore.password=<from a secret store>
listener.name.controller.ssl.client.auth=required
authorizer.class.name=org.apache.kafka.metadata.authorizer.StandardAuthorizer
super.users=User:admin;User:CN=kb1.corp.example;User:CN=kb2.corp.example;User:CN=kb3.corp.example

super.users must include the admin account and the identities of the broker certificates, because the controller listener authenticates brokers by the subject of their certificate. Apply the controller listener settings on the controller nodes in the same way.

The first SCRAM user in KRaft. Brokers need a SCRAM identity to talk to each other before any user can be created through the cluster, so add the admin credential when you format storage (Kafka’s security documentation describes this option):

bin/kafka-storage.sh format -t $CID -c /etc/kafka/server.properties \
  --add-scram 'SCRAM-SHA-512=[name=admin,password=<secret>]'

Create further users through the running cluster, using an admin client configuration file (admin.properties) with the TLS and SASL settings:

bin/kafka-configs.sh --bootstrap-server kb1.corp.example:9092 --command-config admin.properties \
  --alter --add-config 'SCRAM-SHA-512=[password=<secret>]' --entity-type users --entity-name orders-svc
bin/kafka-acls.sh --bootstrap-server kb1.corp.example:9092 --command-config admin.properties \
  --add --allow-principal User:orders-svc --operation Read --operation Write --operation Describe --topic orders
bin/kafka-acls.sh --bootstrap-server kb1.corp.example:9092 --command-config admin.properties \
  --add --allow-principal User:orders-svc --operation Read --group orders-consumers

Add quotas so one noisy client cannot starve others:

bin/kafka-configs.sh --bootstrap-server kb1.corp.example:9092 --command-config admin.properties \
  --alter --add-config 'producer_byte_rate=10485760,consumer_byte_rate=20971520' \
  --entity-type users --entity-name orders-svc

Monitoring

Attach the Prometheus JMX exporter to each broker and controller as a Java agent:

KAFKA_OPTS="-javaagent:/opt/jmx_prometheus_javaagent.jar=7071:/etc/kafka/jmx-rules.yml"
Signal (JMX name) Alert when
UnderReplicatedPartitions Above zero for 5 minutes
OfflinePartitionsCount Above zero (page immediately)
ActiveControllerCount Total across the cluster is not exactly 1
Consumer group lag (kafka-consumer-groups.sh --describe, or an exporter such as Kafka Exporter) Growing for 15 minutes
RequestHandlerAvgIdlePercent Below 30 percent
Disk used on data volumes Above 75 percent
KRaft metrics (raft-metrics: leader, commit latency) Leader changes frequently, or commit latency climbs
Certificate expiry Within 21 days

Prometheus metric names depend on the rules in your exporter file, so write the alert expressions against the names it actually produces. Keep dashboards for throughput per topic, request latency, and ISR shrink and expand rates.

Day-two operations

Rolling restarts and upgrades. Change one node at a time. In KRaft, upgrade the controllers first, then the brokers, and after every node runs the new version, raise the metadata version:

bin/kafka-features.sh --bootstrap-server kb1.corp.example:9092 --command-config admin.properties upgrade --metadata 4.1

Between nodes, wait until under-replicated partitions return to zero. Read the release notes first; the 4.0 line removed older protocol versions and MirrorMaker 1, and a 3.x ZooKeeper cluster must migrate to KRaft on 3.9 before moving to 4.x.

Adding brokers and rebalancing. New brokers get no partitions until you move some:

bin/kafka-reassign-partitions.sh --bootstrap-server kb1.corp.example:9092 --command-config admin.properties \
  --generate --topics-to-move-json-file topics.json --broker-list 101,102,103,104
bin/kafka-reassign-partitions.sh ... --execute --reassignment-json-file plan.json --throttle 50000000
bin/kafka-reassign-partitions.sh ... --verify --reassignment-json-file plan.json

Always set a throttle, run during quiet hours, and remove the throttle after --verify reports completion.

Retention. Set retention by bytes (retention.bytes) as well as time (retention.ms), so a surge in traffic cannot fill the disks, and alert before volumes reach 75 percent.

Disaster recovery. Replication inside one cluster protects against node loss, not against losing a site or an operator mistake. For a second site, run MirrorMaker 2 (it replicates topics, consumer group offsets and ACL-relevant metadata between clusters). Decide the recovery point and recovery time you need, practise the failover, and back up configuration, certificates and the ACL list outside the cluster.

Runbook starting points

Symptom First checks
Producers time out Broker network and disk saturation; min.insync.replicas not met because a broker is down
Consumer lag rising Slow consumers, rebalances, a hot partition; compare produce rate and consume rate
Under-replicated partitions A slow or dead broker, a full disk, a throttled reassignment
Authentication failures Expired certificate, wrong SCRAM user, missing ACL (check broker authorizer logs)
Controller election loops Network partitions between controllers, clock drift, disk latency on the metadata log