On This Page
Your producer or consumer dies on startup with one of these:
org.apache.kafka.common.errors.SaslAuthenticationException: Authentication failed: Invalid username or password
or the SCRAM variant:
org.apache.kafka.common.errors.SaslAuthenticationException: Authentication failed during authentication due to invalid credentials with SASL mechanism SCRAM-SHA-512
In Spring Boot logs it usually surfaces wrapped like this:
org.apache.kafka.common.KafkaException: Failed to construct kafka consumer
...
Caused by: org.apache.kafka.common.errors.SaslAuthenticationException: Authentication failed during authentication due to invalid credentials with SASL mechanism SCRAM-SHA-512
Two close cousins land people on this page too:
java.lang.IllegalArgumentException: Could not find a 'KafkaClient' entry in the JAAS configuration. System property 'java.security.auth.login.config' is not set
org.apache.kafka.common.errors.UnsupportedSaslMechanismException: Client SASL mechanism 'PLAIN' not enabled in the server, enabled mechanisms are [SCRAM-SHA-512]
Typical setup where this bites: kafka-clients 3.6–3.8 or Spring Kafka 3.1/3.2 talking to a SASL-enabled broker — Confluent Cloud, Amazon MSK with SASL/SCRAM, Strimzi, or a local Docker KRaft cluster you just secured. Nothing retries, nothing recovers; the client is dead on arrival.
One important thing before you touch any config: this error is fatal by design. Since KIP-152 (Kafka 1.0), authentication failures are reported to the client as a non-retriable SaslAuthenticationException instead of an endless silent reconnect loop. The client will not fix itself. Something in the credential chain is actually wrong.
2. How to Reproduce It (step-by-step)
Minimal KRaft single-node broker with SASL/PLAIN on the client listener (apache/kafka:3.7.0):
# docker-compose.yml
services:
kafka:
image: apache/kafka:3.7.0
ports:
- "9092:9092"
environment:
KAFKA_NODE_ID: 1
KAFKA_PROCESS_ROLES: broker,controller
KAFKA_CONTROLLER_QUORUM_VOTERS: 1@kafka:29093
KAFKA_LISTENERS: SASL_EXT://0.0.0.0:9092,INTERNAL://0.0.0.0:29092,CONTROLLER://0.0.0.0:29093
KAFKA_ADVERTISED_LISTENERS: SASL_EXT://localhost:9092,INTERNAL://kafka:29092
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: SASL_EXT:SASL_PLAINTEXT,INTERNAL:PLAINTEXT,CONTROLLER:PLAINTEXT
KAFKA_INTER_BROKER_LISTENER_NAME: INTERNAL
KAFKA_CONTROLLER_LISTENER_NAMES: CONTROLLER
KAFKA_SASL_ENABLED_MECHANISMS: PLAIN
KAFKA_LISTENER_NAME_SASL__EXT_PLAIN_SASL_JAAS_CONFIG: |
org.apache.kafka.common.security.plain.PlainLoginModule required
user_app="app-secret";
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
The broker knows exactly one user: app / app-secret. Now connect with a wrong password:
cat > client-bad.properties <<'EOF'
security.protocol=SASL_PLAINTEXT
sasl.mechanism=PLAIN
sasl.jaas.config=org.apache.kafka.common.security.plain.PlainLoginModule required \
username="app" password="wrong-secret";
EOF
kafka-console-producer.sh --bootstrap-server localhost:9092 \
--topic test --producer.config client-bad.properties
Type a message and you get:
ERROR [Producer clientId=console-producer] Connection to node -1 (localhost/127.0.0.1:9092) failed authentication due to: Authentication failed: Invalid username or password
org.apache.kafka.common.errors.SaslAuthenticationException: Authentication failed: Invalid username or password
Environment-specific triggers worth knowing:
- SCRAM user never created. With SASL/SCRAM the broker validates against stored credentials, not JAAS entries. A fresh cluster has zero SCRAM users — every client fails until you create them (Section 5). This is the classic MSK SASL/SCRAM and Strimzi first-run failure.
- Plaintext client on a SASL port. The client hangs and eventually times out; the truth is only in the broker log:
Failed authentication with /172.18.0.4 (Unexpected Kafka request of type METADATA during SASL handshake.) - Password rotated in the secret store but the app still holds the old one — worked yesterday, fails after today's deploy.
3. Why It Happens — Surface Level
The SASL handshake completed at the TCP and protocol level, and the broker actively rejected your identity. With PLAIN, the username/password pair didn't match a user_<name> entry in the broker's JAAS config for that listener. With SCRAM, the challenge-response proof didn't verify against the salted credential stored in the cluster — either the password is wrong or the user simply doesn't exist for that mechanism.
This is not a network problem, not a listener problem, not TLS. The broker heard you fine and said no.
4. Why It Happens — Under the Hood
The client connection goes through SaslClientAuthenticator in a strict state machine: SaslHandshakeRequest (advertising the mechanism) → one or more SaslAuthenticateRequest round trips carrying the mechanism's opaque tokens → connection promoted to READY.
Three separate failure points map to the three error variants:
Mechanism negotiation. The broker only accepts mechanisms listed in sasl.enabled.mechanisms for that listener. If the client advertises PLAIN and the broker only enabled SCRAM-SHA-512, the handshake dies immediately with UnsupportedSaslMechanismException — you never even get to credentials. Check both sides agree on the exact string; SCRAM-SHA-256 and SCRAM-SHA-512 are different mechanisms with independently stored credentials.
PLAIN validation. PlainSaslServer looks up user_<username>="<password>" entries from listener.name.<listener>.plain.sasl.jaas.config. It's a static map loaded at broker start (dynamic reconfig via kafka-configs.sh is possible since KIP-226). Wrong password or unknown username → Authentication failed: Invalid username or password. Note the sharp edge in JAAS: the broker's own username/password fields in that same login module are its inter-broker client credentials, not a user definition — only user_-prefixed keys define accounts.
SCRAM validation. SCRAM credentials are salted, iterated hashes (RFC 5802) stored in cluster metadata — the __cluster_metadata log in KRaft, ZooKeeper in legacy clusters. The broker never sees your password; it verifies a cryptographic proof against the stored StoredKey/ServerKey. Consequence: credentials must be provisioned per mechanism. Creating a SCRAM-SHA-256 credential does nothing for a client configured with SCRAM-SHA-512. In KRaft, SCRAM management via kafka-configs.sh works from Kafka 3.5 (KIP-900); on older KRaft versions this was the source of much pain.
After the failure, the broker deliberately delays the error response (connection.failed.authentication.delay.ms, default 100 ms) to blunt brute-force attempts, sends the error, and closes the connection. On the client, NetworkClient marks the node with a fatal AuthenticationException; KafkaProducer.send() fails futures with it, and KafkaConsumer.poll() throws it straight out. No retries, no backoff loop — KIP-152 made this a hard stop because retrying an auth failure just hammers the broker.
5. The Fix
Fix 1: Wrong credentials (PLAIN)
Client side, before/after:
security.protocol=SASL_PLAINTEXT
sasl.mechanism=PLAIN
sasl.jaas.config=org.apache.kafka.common.security.plain.PlainLoginModule required \
- username="app" password="wrong-secret";
+ username="app" password="app-secret";
If the username itself is unknown, add it broker-side (note the user_ prefix):
KAFKA_LISTENER_NAME_SASL__EXT_PLAIN_SASL_JAAS_CONFIG: |
org.apache.kafka.common.security.plain.PlainLoginModule required
- user_app="app-secret";
+ user_app="app-secret"
+ user_analytics="analytics-secret";
Fix 2: Missing SCRAM user
Create the credential for the exact mechanism the client uses:
kafka-configs.sh --bootstrap-server localhost:29092 \
--alter --entity-type users --entity-name app \
--add-config 'SCRAM-SHA-512=[iterations=8192,password=app-secret]'
# verify
kafka-configs.sh --bootstrap-server localhost:29092 \
--describe --entity-type users --entity-name app
For a brand-new KRaft cluster where even the admin client can't get in yet, bootstrap the first user at format time:
kafka-storage.sh format -t "$CLUSTER_ID" -c server.properties \
--add-scram 'SCRAM-SHA-512=[name=admin,password=admin-secret]'
Fix 3: Mechanism mismatch
security.protocol=SASL_SSL
-sasl.mechanism=PLAIN
+sasl.mechanism=SCRAM-SHA-512
-sasl.jaas.config=org.apache.kafka.common.security.plain.PlainLoginModule required \
+sasl.jaas.config=org.apache.kafka.common.security.scram.ScramLoginModule required \
username="app" password="app-secret";
The login module class must match the mechanism — PlainLoginModule with sasl.mechanism=SCRAM-SHA-512 fails before the network is even touched.
Fix 4: Could not find a 'KafkaClient' entry
You configured SASL but gave the client no JAAS at all. Don't reach for -Djava.security.auth.login.config — set sasl.jaas.config per client as shown above. It's scoped, overrides the global JAAS file, and works cleanly with multiple clients per JVM.
Spring Boot, the right shape
spring:
kafka:
bootstrap-servers: broker:9092
security:
protocol: SASL_SSL
properties:
sasl.mechanism: SCRAM-SHA-512
sasl.jaas.config: >
org.apache.kafka.common.security.scram.ScramLoginModule required
username="${KAFKA_USER}"
password="${KAFKA_PASSWORD}";
Watch the trailing semicolon inside sasl.jaas.config — omitting it produces a parse error that reads like an auth failure at first glance.
6. Best Practices & The Better Design
SASL_PLAINTEXT + PLAIN means credentials cross the wire unencrypted and the broker stores them in config files. It's acceptable for a laptop compose file, nowhere else. The production-grade shape:
SASL_SSLalways. SASL decides who you are; TLS protects the exchange. PLAIN without TLS is credential broadcast.- SCRAM-SHA-512 over PLAIN for password-style auth: broker stores salted hashes, passwords never transit in cleartext within the SASL exchange, and users are managed at runtime via
kafka-configs.shinstead of broker restarts. - One principal per service, never a shared "app" user. Auth failures become attributable, rotation doesn't take down five services, and ACLs can follow least privilege.
- Credentials from a secret manager (Vault, AWS Secrets Manager, K8s secrets), injected as env vars — never literals in
application.ymlor compose files that end up in git. - For fleets at scale, consider mTLS or OAUTHBEARER (KIP-768, OIDC) and retire passwords entirely.
Runtime rotation without restarts:
# rotate the SCRAM password; clients reconnect with the new secret, zero broker downtime
kafka-configs.sh --bootstrap-server broker:9092 \
--alter --entity-type users --entity-name orders-service \
--add-config 'SCRAM-SHA-512=[password=new-secret]'
Rotate with an overlap window: since SCRAM allows one credential per (user, mechanism), stage rotations by deploying the new secret to clients immediately after altering, or run dual users (orders-service-a / orders-service-b) blue-green style if you can't coordinate the deploy.
7. How to Prevent It Long-Term
Monitor failed authentications on the broker. The metric is kafka.server:type=socket-server-metrics,...,name=failed-authentication-total (per listener/processor), with a matching failed-authentication-rate. Alert on any sustained non-zero rate: a spike after a deploy means a bad secret shipped; a slow steady rate from an unknown IP is someone guessing passwords. The broker also logs each failure at INFO with the remote address — ship those to your SIEM.
Fail fast in CI/CD. A pre-deploy smoke test that builds an AdminClient with the candidate credentials and calls describeCluster() catches dead secrets before they reach production:
try (AdminClient admin = AdminClient.create(props)) {
admin.describeCluster().clusterId().get(10, TimeUnit.SECONDS);
} catch (ExecutionException e) {
if (e.getCause() instanceof SaslAuthenticationException) {
throw new IllegalStateException("Kafka credentials invalid — aborting deploy", e);
}
throw e;
}
Conventions that remove the recurring causes: mechanism pinned cluster-wide and documented (pick SCRAM-SHA-512, delete the rest from sasl.enabled.mechanisms); user provisioning as code (Strimzi KafkaUser, Terraform, or a Julie/topic-operator pipeline) so "user exists for the right mechanism" is guaranteed, not tribal knowledge; secret rotation runbooks that alter the SCRAM credential and roll the consumers in one change window.
Spring Kafka note: by default an auth failure stops the listener container. If you rotate credentials via a secret store and want the container to survive the propagation gap, set authExceptionRetryInterval on the container properties — but treat it as a rotation-window bridge, not a license to ignore the alert.
8. Key Takeaways
SaslAuthenticationExceptionis fatal and non-retriable by design (KIP-152). The broker heard you and rejected the credentials — fix the config, don't add retries.- SCRAM credentials are per user, per mechanism. A
SCRAM-SHA-256credential is invisible to aSCRAM-SHA-512client. Verify withkafka-configs.sh --describe --entity-type users. - In PLAIN JAAS, only
user_<name>="<password>"entries define accounts; the module'susername/passwordare the broker's own inter-broker credentials. - If the client only shows timeouts, read the broker log — "Unexpected Kafka request of type METADATA during SASL handshake" means a plaintext client hit a SASL port.
- Alert on
failed-authentication-totaland smoke-test credentials with anAdminClientin CI; bad secrets should fail the deploy, not the 3 a.m. pager.
Gopi Gorantala Newsletter
Join the newsletter to receive the latest updates in your inbox.