Step-by-Step Guide to Enabling mTLS on Kafka Brokers
Mutual TLS (mTLS) authenticates both sides of a Kafka connection with X.509 certificates: the client verifies the broker, and the broker verifies the client. Plain TLS only does the former. This guide walks Platform, SRE, and backend engineers through a production-grade mTLS rollout: building a CA, issuing broker and client certificates with the correct Subject Alternative Names, configuring server.properties, validating the handshake, and managing the certificate lifecycle. It fits within the broader Operations, Security & Observability practice and pairs with SASL/SCRAM as covered in Securing Kafka with mTLS and SASL/SCRAM.
One thing to internalize up front: mTLS gives you authentication, not authorization. Once a client’s certificate is accepted, Kafka maps it to a principal but grants nothing until you write ACLs. We close on that.
Prerequisites and Certificate Authority Setup
mTLS rests on a Public Key Infrastructure (PKI). In production you would use an internal CA or a tool such as HashiCorp Vault’s PKI secrets engine. For a self-contained walkthrough we build a self-signed root CA with OpenSSL; the broker/client steps are identical once your real CA is signing CSRs.
# Generate the CA private key
openssl genrsa -out ca.key 4096
# Generate a self-signed CA certificate (valid 10 years)
openssl req -new -x509 -days 3650 -key ca.key -out ca.crt \
-subj "/C=US/ST=California/L=SanFrancisco/O=MyOrg/CN=KafkaRootCA"
Protect ca.key as a top-tier secret: anyone holding it can mint a certificate trusted by every broker and client. Distribute ca.crt (the public certificate) as the truststore anchor. In production, sign from an intermediate CA so a compromised issuing key can be revoked without re-rooting the cluster. NIST’s key management guidelines apply directly here.
Generating and Signing Broker Certificates
Each broker needs its own certificate whose Subject Alternative Name (SAN) matches the hostname clients use to reach it — i.e., the host in advertised.listeners. This matters because since Kafka 2.0.0, hostname verification is enabled by default for both client-to-broker and inter-broker connections (ssl.endpoint.identification.algorithm defaults to https). Clients validate the connection hostname against the certificate’s SAN, not the CN — CN-based matching has been deprecated since RFC 6125. A SAN mismatch is the single most common cause of a failed first handshake.
The example below issues a certificate for kafka-broker-1.internal.example.com.
# Generate the broker private key and CSR
openssl req -new -newkey rsa:4096 -nodes \
-keyout kafka-broker-1.key \
-out kafka-broker-1.csr \
-subj "/C=US/ST=California/L=SanFrancisco/O=MyOrg/CN=kafka-broker-1.internal.example.com"
# Pin the SAN(s) in an extension file
cat > kafka-broker-1.ext <<'EOF'
subjectAltName = DNS:kafka-broker-1.internal.example.com
EOF
# Sign the CSR with the root CA, carrying the SAN into the cert
openssl x509 -req -in kafka-broker-1.csr \
-CA ca.crt -CAkey ca.key -CAcreateserial \
-out kafka-broker-1.crt -days 365 \
-extfile kafka-broker-1.ext
# Package key + cert into a PKCS12 keystore
openssl pkcs12 -export -in kafka-broker-1.crt \
-inkey kafka-broker-1.key \
-out kafka-broker-1.p12 \
-name kafka-broker-1 -password pass:changeit
Repeat for every broker, giving each a unique CN and a SAN matching its advertised hostname. If clients reach the broker by multiple names (e.g., an internal and an external FQDN), list each as a DNS: entry, and add IP: entries if any client connects by raw address. Do not add DNS:localhost to production certs — it widens the set of names the cert will validate for no operational benefit.
Configuring Kafka Brokers for mTLS
Update each broker’s server.properties to add an SSL listener and require client authentication. This example assumes certificates live under /etc/kafka/secrets/.
# Listener definition
listeners=SSL://0.0.0.0:9093
advertised.listeners=SSL://kafka-broker-1.internal.example.com:9093
# Use the SSL listener for inter-broker traffic too (so brokers mutually authenticate)
inter.broker.listener.name=SSL
# Broker keystore (its own identity)
ssl.keystore.location=/etc/kafka/secrets/kafka-broker-1.p12
ssl.keystore.password=changeit
ssl.keystore.type=PKCS12
ssl.key.password=changeit
# Truststore (the CA chain used to validate peers)
ssl.truststore.location=/etc/kafka/secrets/truststore.jks
ssl.truststore.password=changeit
ssl.truststore.type=JKS
# Require client certificates — this is what makes TLS *mutual*
ssl.client.auth=required
# Protocol hardening
ssl.enabled.protocols=TLSv1.2,TLSv1.3
ssl.cipher.suites=TLS_AES_256_GCM_SHA384,TLS_CHACHA20_POLY1305_SHA256,TLS_AES_128_GCM_SHA256
ssl.client.auth accepts required, requested, or none (default none). Only required gives you real mTLS: every connection must present a certificate chaining to a CA in the broker’s truststore. Avoid requested — clients with no certificate or an invalid one still connect, authenticating as the ANONYMOUS principal, which is a false sense of security.
A note on ssl.enabled.protocols: Kafka’s default is TLSv1.2,TLSv1.3. Pinning to TLSv1.3 only is stronger but will reject any client or JVM that cannot negotiate 1.3, so verify your entire fleet first. Also be aware that under TLS 1.3 the JSSE provider restricts cipher selection to the TLS 1.3 suites, so ssl.cipher.suites mainly constrains the TLS 1.2 fallback.
inter.broker.listener.name=SSL routes broker-to-broker traffic over the SSL listener; because ssl.client.auth=required, brokers present their own keystore and authenticate each other using the same truststore. (The alternative when you are not using named listeners is security.inter.broker.protocol=SSL.)
Build the truststore by importing the CA certificate:
keytool -import -trustcacerts -alias kafka-root-ca \
-file ca.crt -keystore truststore.jks \
-storepass changeit -noprompt
Then roll the cluster one broker at a time. Watch the logs for SSL keystore/truststore load errors and handshake failures during each restart. The Apache Kafka SSL encryption and authentication documentation lists every SSL property and its default.
Generating and Configuring Client Certificates
Client certificates are generated the same way, but the Subject DN becomes the client’s Kafka principal, so choose it deliberately — it is what your ACLs will reference.
# Generate the client private key and CSR
openssl req -new -newkey rsa:4096 -nodes \
-keyout client-admin.key \
-out client-admin.csr \
-subj "/C=US/ST=California/L=SanFrancisco/O=MyOrg/CN=admin"
# Sign with the root CA
openssl x509 -req -in client-admin.csr \
-CA ca.crt -CAkey ca.key -CAcreateserial \
-out client-admin.crt -days 365
# Package into a PKCS12 keystore
openssl pkcs12 -export -in client-admin.crt \
-inkey client-admin.key \
-out client-admin.p12 \
-name admin -password pass:changeit
Client certificates do not need a SAN — clients are not authenticated by hostname — but they do need the right Subject DN. Point the CLI tools at a properties file:
security.protocol=SSL
ssl.truststore.location=/etc/kafka/secrets/truststore.jks
ssl.truststore.password=changeit
ssl.keystore.location=/etc/kafka/secrets/client-admin.p12
ssl.keystore.password=changeit
ssl.keystore.type=PKCS12
ssl.key.password=changeit
Test it with the console producer:
kafka-console-producer.sh --bootstrap-server kafka-broker-1.internal.example.com:9093 \
--topic test-mtls --producer.config client-ssl.properties
If the handshake completes and you can type a record, mutual TLS is working.
Validating and Troubleshooting mTLS Connections
Validate from both directions before declaring the rollout production-ready.
Broker-side: Confirm the broker loaded its keystore and truststore cleanly at startup (no SSLException / keystore errors). Then simulate a client handshake with OpenSSL, presenting the client certificate so you also exercise the ssl.client.auth=required path:
openssl s_client -connect kafka-broker-1.internal.example.com:9093 \
-cert client-admin.crt -key client-admin.key \
-CAfile ca.crt -tls1_3
A good result prints the server chain and ends with Verify return code: 0 (ok). unknown CA means your -CAfile/truststore is missing the signing CA; certificate expired means exactly that; a hostname-verification failure means the SAN does not match the connect address.
Client-side: The usual culprits are SAN mismatches, wrong keystore passwords, or a truststore missing the CA. Turn on JSSE debug to see the full handshake:
export KAFKA_OPTS="-Djavax.net.debug=ssl:handshake:verbose"
kafka-console-consumer.sh --bootstrap-server kafka-broker-1.internal.example.com:9093 \
--topic test-mtls --consumer.config client-ssl.properties
In the trace, the broker’s CertificateRequest message confirms ssl.client.auth=required is active (the broker is demanding a client cert). If the client never sends a Certificate in response, its keystore is not loading.
Operational Considerations and Certificate Lifecycle
mTLS is an ongoing operational commitment, and certificate expiry is the leading cause of mTLS outages. Build a lifecycle process:
- Monitor expiry.
openssl x509 -enddate -noout -in <cert>gives the date; alert when any broker or client cert is within, say, 30 days of expiring. - Automate renewal. Issue and distribute from CI/CD or a secrets platform (e.g., Vault’s PKI engine) rather than by hand.
- Roll certificate replacements. Swap the keystore on disk and restart brokers one at a time. Kafka’s controlled shutdown reassigns leadership cleanly, so a properly replicated cluster (replication factor ≥ 3,
min.insync.replicas≥ 2) stays available with no data loss.
Give each broker a unique key/cert rather than sharing one wildcard cert across the fleet — a single compromised key then exposes one broker, not all of them.
Finally, wire mTLS into authorization. By default Kafka derives the principal from the certificate’s full Subject DN in RFC 2253 string form — most-specific component first — so a client issued CN=admin,O=MyOrg,L=SanFrancisco,ST=California,C=US authenticates as exactly that string, and your ACLs must reference it verbatim:
kafka-acls.sh --bootstrap-server kafka-broker-1.internal.example.com:9093 \
--command-config client-ssl.properties \
--add --allow-principal "User:CN=admin,O=MyOrg,L=SanFrancisco,ST=California,C=US" \
--operation All --topic test-mtls
Long DNs are brittle in ACLs, so from Kafka 2.4.0 onward you can collapse them to short names with ssl.principal.mapping.rules (introduced in KIP-371) in server.properties:
ssl.principal.mapping.rules=RULE:^CN=(.*?),O=.*$/$1/,DEFAULT
With that rule the CN=admin,... certificate authenticates simply as User:admin, which is far more maintainable in ACLs. This coupling of mTLS identity to ACLs is the foundation of a secure, multi-tenant Kafka deployment — explored further in Securing Kafka with mTLS and SASL/SCRAM.