Skip to content

Kafka SaslAuthenticationException: Authentication Failed — Fix

Getting SaslAuthenticationException: Authentication failed in Kafka? Fix invalid SASL credentials, JAAS config, and missing SCRAM users with working examples.

Gopi Gorantala
Gopi Gorantala
7 min read
Reading Progress

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_SSL always. 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.sh instead 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.yml or 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

  • SaslAuthenticationException is 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-256 credential is invisible to a SCRAM-SHA-512 client. Verify with kafka-configs.sh --describe --entity-type users.
  • In PLAIN JAAS, only user_<name>="<password>" entries define accounts; the module's username/password are 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-total and smoke-test credentials with an AdminClient in CI; bad secrets should fail the deploy, not the 3 a.m. pager.
apache-kafkakafka-errorssaslauthenticationexceptionkafka-securitysasl-scramspring-kafkakafka-dockerJava

Gopi Gorantala Twitter

I'm Gopi — 15+ years in Java, building Kafka and Flink platforms for banks, where one lost event is a financial discrepancy. I write javahandbook.com because the guides I needed didn't exist. Everything here is tested against a real cluster first.

Comments