Security & Production
The idea in one sentence
Section titled “The idea in one sentence”Kafka security is three real, separately-provable layers — authorization (ACLs, which do nothing at all until you turn on an authorizer), authentication (SASL or mTLS, which decides who a principal even is), and encryption (TLS) — and skipping the first step, as a lot of tutorials do, produces a command that looks like it should work and instead throws immediately.
Proof: kafka-acls.sh with no authorizer configured
Section titled “Proof: kafka-acls.sh with no authorizer configured”Kafka ships with no authorizer by default. Running the exact command a lot of tutorials open with, against a broker that has not enabled one:
$ kafka-acls.sh --bootstrap-server localhost:9092 --listError while executing ACL command: org.apache.kafka.common.errors.SecurityDisabledException: No Authorizer is configured on the brokerjava.util.concurrent.ExecutionException: org.apache.kafka.common.errors.SecurityDisabledException: No Authorizer is configured on the brokerThis is not a permissions error — it is kafka-acls.sh telling you, correctly, that there is no ACL system running at all to query. The fix is a broker config, set before the broker starts:
# section: brokerauthorizer.class.name=org.apache.kafka.metadata.authorizer.StandardAuthorizerStandardAuthorizer is the KRaft-native authorizer (the ZooKeeper-era AclAuthorizer is gone along with ZooKeeper). Pair it with super.users (documented under Kafka’s authorization/ACL configuration, not the general broker-configs table above) — a comma-separated principalType:name list that bypasses every ACL check. Every real cluster needs at least one super user, or a locked-down authorizer can lock out its own admins:
super.users=User:ANONYMOUSProof: authorizer on, no ACL yet — a real denied produce
Section titled “Proof: authorizer on, no ACL yet — a real denied produce”With the authorizer enabled and a SASL/SCRAM listener added (full setup below), a client authenticated as order-service — a real principal, not a super user — tries to produce to a topic it has never been granted access to:
$ kafka-console-producer.sh --bootstrap-server localhost:39094 --topic secure-orders \ --command-config order-service-client.properties[...] WARN [Producer clientId=console-producer] The metadata response from the cluster reported a recoverable issue with correlation id 1 : {secure-orders=TOPIC_AUTHORIZATION_FAILED} (org.apache.kafka.clients.NetworkClient)[...] ERROR [Producer clientId=console-producer] Topic authorization failed for topics [secure-orders] (org.apache.kafka.clients.Metadata)org.apache.kafka.common.errors.TopicAuthorizationException: Not authorized to access topics: [secure-orders]Real, verbatim TopicAuthorizationException — authentication succeeded (SCRAM verified the password, proven further down), but authorization did not. kafka-acls.sh --add is the fix:
$ kafka-acls.sh --bootstrap-server localhost:9092 \ --add --allow-principal User:order-service \ --operation Write --topic secure-ordersAdding ACLs for resource `ResourcePattern(resourceType=TOPIC, name=secure-orders, patternType=LITERAL)`: (principal=User:order-service, host=*, operation=WRITE, permissionType=ALLOW)
$ kafka-console-producer.sh --bootstrap-server localhost:39094 --topic secure-orders \ --command-config order-service-client.properties(exits 0, no output -- the record went through)sequenceDiagram participant C as order-service (authenticated, not super) participant B as Broker C->>B: Produce to secure-orders (no ACL granted) B-->>C: TOPIC_AUTHORIZATION_FAILED -> real TopicAuthorizationException Note over B: kafka-acls.sh --add --allow-principal User:order-service --operation Write --topic secure-orders C->>B: Produce to secure-orders (retry, same credentials) B-->>C: accepted -- exit 0
Two real Docker-image bugs found setting this up
Section titled “Two real Docker-image bugs found setting this up”Building this lesson’s own SASL/SCRAM broker hit two genuine, undocumented-until-you-hit-them failures in the official image — both worth knowing, since either one produces a startup crash that has nothing to do with your Kafka config being wrong.
SASL/SCRAM in KRaft, end to end
Section titled “SASL/SCRAM in KRaft, end to end”Two ways to provision SCRAM credentials in KRaft (no ZooKeeper, so no separate --zookeeper flag exists for this either): at format time, or dynamically against a running cluster. The format-time flag, confirmed against the real tool’s own help text:
/opt/kafka/bin/kafka-storage.sh format --help --add-scram ADD_SCRAM, -S ADD_SCRAM A SCRAM_CREDENTIAL to add to the __cluster_metadata log e.g. 'SCRAM-SHA-256=[name=alice,password=alice-secret]' 'SCRAM-SHA-512=[name=alice,iterations=8192,salt="N3E=",saltedpassword="YCE="]'This lesson used the second path — adding a user to an already-running cluster, which is what most real operational workflows actually do (rotating or adding a credential without reformatting anything):
# Self-contained: creates its own SCRAM user, proves it, then deletes it./opt/kafka/bin/kafka-configs.sh --bootstrap-server "$BOOTSTRAP" \ --alter --entity-type users --entity-name lesson-scram-demo \ --add-config 'SCRAM-SHA-256=[password=demo-secret-pw]'
/opt/kafka/bin/kafka-configs.sh --bootstrap-server "$BOOTSTRAP" \ --describe --entity-type users --entity-name lesson-scram-demo
/opt/kafka/bin/kafka-configs.sh --bootstrap-server "$BOOTSTRAP" \ --alter --entity-type users --entity-name lesson-scram-demo \ --delete-config 'SCRAM-SHA-256'Completed updating config for user lesson-scram-demo.SCRAM credential configs for user-principal 'lesson-scram-demo' are SCRAM-SHA-256=iterations=4096Completed updating config for user lesson-scram-demo.The credential itself — never the plaintext password — is what gets stored: --describe shows iterations=4096, never demo-secret-pw. The listener needs to actually offer SCRAM too:
# section: brokersasl.enabled.mechanisms=SCRAM-SHA-256security.inter.broker.protocol=PLAINTEXTProof: a real client authenticates — and a real one fails to
Section titled “Proof: a real client authenticates — and a real one fails to”A genuine KafkaJS producer, run from the host against the SASL_PLAINTEXT listener, with the correct credentials:
const kafka = new Kafka({ clientId: 'security-lesson-probe', brokers: ['localhost:39094'], sasl: { mechanism: 'scram-sha-256', username: 'order-service', password: 'order-secret-pw' },});const producer = kafka.producer();await producer.connect();await producer.send({ topic: 'secure-orders', messages: [{ value: 'hello from KafkaJS over SASL_PLAINTEXT' }] });SASL/SCRAM connect OK as order-servicesend OKThe exact same client, wrong password only:
{"level":"ERROR", ..., "message":"[SaslAuthenticator-SCRAM-SHA-256] SASL SCRAM SHA256 authentication failed: Authentication failed during authentication due to invalid credentials with SASL mechanism SCRAM-SHA-256", ...}connect FAILED: KafkaJSSASLAuthenticationError - SASL SCRAM SHA256 authentication failed: Authentication failed during authentication due to invalid credentials with SASL mechanism SCRAM-SHA-256And the Java-tooling side of the identical mistake, for comparison — same broker, same wrong password, via kafka-console-producer.sh instead:
[...] ERROR [Producer clientId=console-producer] Connection to node -1 (localhost/127.0.0.1:39094) failed authentication due to: Authentication failed during authentication due to invalid credentials with SASL mechanism SCRAM-SHA-256 (org.apache.kafka.clients.NetworkClient)org.apache.kafka.common.errors.SaslAuthenticationException: Authentication failed during authentication due to invalid credentials with SASL mechanism SCRAM-SHA-256Both clients hit the identical broker-side check and get the identical human-readable reason — only the exception class name differs by client (KafkaJSSASLAuthenticationError vs. SaslAuthenticationException), because SCRAM’s challenge-response happens on the wire, not inside either client library.
TLS: the shape, and the real config keys
Section titled “TLS: the shape, and the real config keys”This lesson does not generate certificates end to end — the broker config keys are the load-bearing part, and they are documented, not folklore. Confirmed against the official broker config reference (kafka.apache.org/43/configuration/, broker section, fetched for this course):
# section: brokerlisteners=SSL://:9093ssl.keystore.location=/etc/kafka/secrets/broker.keystore.jksssl.keystore.password=changeitssl.key.password=changeitssl.truststore.location=/etc/kafka/secrets/broker.truststore.jksssl.truststore.password=changeitssl.client.auth=requiredssl.endpoint.identification.algorithm=httpsEach broker needs both a keystore (its own identity — the certificate it presents) and a truststore (who it is willing to trust — normally the CA that signed every broker’s and client’s certificate, in a private cluster). ssl.client.auth=required turns on mutual TLS: the broker also demands and verifies a certificate from the client, which is the mTLS authentication mode mentioned below — TLS alone only proves the broker’s identity to the client, not the other way around. ssl.endpoint.identification.algorithm=https enables hostname verification against the certificate’s SAN, which prevents a trivial man-in-the-middle even when the attacker has a valid certificate, just not one for the hostname being connected to — leaving it unset is a real, common misconfiguration.
Production readiness checklist
Section titled “Production readiness checklist”Security is one column; durability and observability are the others, covered in Monitoring & Consumer Lag and Performance & Tuning. Before a cluster carries real traffic:
authorizer.class.name=org.apache.kafka.metadata.authorizer.StandardAuthorizer, withsuper.usersscoped to real admin principals only — proven above to be required before any ACL means anything.- TLS everywhere (
ssl.client.auth=requiredfor mTLS, or SASL overSASL_SSLfor encrypted authenticated access) — plaintext listeners should not be reachable outside a trusted network boundary. - Least-privilege ACLs per principal — proven above with
order-servicegetting exactlyWriteon exactlysecure-orders, nothing broader. - Replication factor >= 3 on important topics with
min.insync.replicas=2and producers usingacks=all— the durable combination this course’s Acks & Durability lesson proves. - Monitoring and alerting on consumer lag, under-replicated partitions, and request latency — the next lesson’s subject.
- Quotas so one client cannot starve the cluster of bandwidth or request capacity.
Reading AI-generated code
Section titled “Reading AI-generated code”🐛 Spot the bug
#!/usr/bin/env bash# AI comment: "Lock down the orders topic to only the order-service producer"set -euo pipefail
kafka-topics.sh --bootstrap-server localhost:9092 \--create --topic orders --partitions 3 --replication-factor 3
kafka-acls.sh --bootstrap-server localhost:9092 \--add --allow-principal User:order-service \--operation Write --topic ordersShow the answer
This script is a fully plausible “secure the topic” runbook — create it, then grant exactly the one principal that should write to it, nothing broader. It will very likely fail on a real cluster with the same SecurityDisabledException proven at the top of this lesson: nothing in this script (or anywhere upstream of it, if this is the first security-related command a team runs) ever set authorizer.class.name. kafka-acls.sh --add against a broker with no authorizer configured does not silently no-op — it errors out immediately, set -euo pipefail kills the script right there, and the topic that line 6 just created is left with zero ACL enforcement at all, not the locked-down state the comment promises. The fix is not a different kafka-acls.sh flag — it’s a broker-level authorizer.class.name=org.apache.kafka.metadata.authorizer.StandardAuthorizer (plus super.users for whoever runs this script) applied and the broker restarted before any ACL command is ever expected to do something.
Mental Model
Section titled “Mental Model”Treat Kafka security as three independent yes/no switches, checked in a fixed order for every request: is the connection encrypted (TLS, or not — orthogonal to the other two), does the broker know who is asking (authentication — SASL, mTLS, or “everyone is ANONYMOUS” by default), and is that identity allowed to do this specific thing (authorization — ACLs, which this lesson proved do not even exist as a concept until authorizer.class.name is set). A production cluster needs all three; a lot of tutorials and AI-generated scripts only ever demonstrate the third, silently assuming the first two are handled — which is exactly the gap this lesson’s SpotTheBug walks into.
Trade-offs
Section titled “Trade-offs”User:ANONYMOUSas a super user on an internal listener — convenient for admin scripting on a listener nothing untrusted can reach (proven as this lesson’s own pattern), and a real security hole the moment that listener is reachable from anywhere it shouldn’t be.- SASL/SCRAM vs. mTLS — SCRAM is self-contained (credentials live in the cluster’s own metadata log, proven above, no external CA needed) and easy to rotate per-user; mTLS ties identity to a certificate, which centralizes trust in a CA but avoids shipping passwords anywhere, including into JAAS config strings.
- Dynamic SCRAM user provisioning (
kafka-configs.sh --alter) vs. format-time (kafka-storage.sh format --add-scram) — dynamic is what real operations use (add/rotate a user without touching every broker’s storage), proven above end to end; format-time is really only for bootstrapping the very first admin credential before any broker has ever started. ssl.endpoint.identification.algorithm=httpson vs. off — on (the safer default) rejects a certificate that is valid but for the wrong hostname; off accepts it, which is occasionally needed for internal service-mesh setups with non-standard naming but reopens exactly the man-in-the-middle gap hostname verification exists to close.
Common Misconceptions
Section titled “Common Misconceptions”- “
kafka-acls.sh --addworks on any broker; you only need an authorizer if you want ACLs to be enforced.” No — proven above: withoutauthorizer.class.nameset,kafka-acls.shcannot even list ACLs, realSecurityDisabledException, not a soft no-op. - “A
TopicAuthorizationExceptionmeans the client’s credentials are wrong.” No — proven above:order-serviceauthenticated successfully (SCRAM verified) and was still denied, because authentication and authorization are two separate checks; the wrong-password case, also proven above, produces a completely different exception (SaslAuthenticationException/KafkaJSSASLAuthenticationError) at a different point in the connection. - “SASL/SCRAM needs ZooKeeper or a separate credential store.” No — proven above:
kafka-configs.sh --alter --entity-type userswrites the credential straight into the KRaft__cluster_metadatalog on a running cluster, no ZooKeeper, no external store. - “TLS and authentication are the same thing — enabling
SSL://means clients are authenticated.” No — proven in the config above: plainSSL://only proves the broker’s identity to the client; a client presents no identity at all unlessssl.client.auth=required(mTLS) or a SASL mechanism is layered on top viaSASL_SSL.