Securing Spring Kafka Clients
Connecting a Spring Boot application to a hardened Kafka cluster means moving beyond the default PLAINTEXT listener and speaking SASL_SSL: TLS for transport encryption plus SASL for authentication. Spring for Apache Kafka exposes every underlying client property through spring.kafka.*, so the entire handshake is declarative. The hard part in production is not the configuration itself but keeping credentials out of source control. This page shows a complete, working setup and the secrets-management practices that keep it safe.
How the security protocol fits together
Kafka clients pick a security protocol that combines two independent concerns. Choosing the wrong combination is the most common cause of broker connection failures, so it helps to be explicit about what each one buys you.
| Protocol | Encryption (TLS) | Authentication (SASL) | Typical use |
|---|---|---|---|
PLAINTEXT | No | No | Local dev only |
SSL | Yes | Via mutual TLS (client cert) | Cert-based identity |
SASL_PLAINTEXT | No | Yes | Inside a trusted network |
SASL_SSL | Yes | Yes | Standard for managed/production clusters |
For managed services (Confluent Cloud, Amazon MSK, Aiven) and any internet-facing broker, SASL_SSL is the expected choice. The client encrypts the channel with TLS, validates the broker certificate against a truststore, and then authenticates with a SASL mechanism such as PLAIN, SCRAM-SHA-512, or OAUTHBEARER.
Configuring application.yml
Spring Boot maps the well-known consumer/producer/admin settings to first-class keys and passes anything else through spring.kafka.properties. The security.protocol, SASL mechanism, and JAAS login module belong under the shared properties block so they apply to producers, consumers, and the admin client alike.
spring:
kafka:
bootstrap-servers: ${KAFKA_BOOTSTRAP_SERVERS}
security:
protocol: SASL_SSL
ssl:
trust-store-location: file:${KAFKA_TRUSTSTORE_PATH}
trust-store-password: ${KAFKA_TRUSTSTORE_PASSWORD}
trust-store-type: PKCS12
properties:
sasl.mechanism: SCRAM-SHA-512
sasl.jaas.config: >-
org.apache.kafka.common.security.scram.ScramLoginModule required
username="${KAFKA_USERNAME}"
password="${KAFKA_PASSWORD}";
# Verify the broker hostname against its certificate (leave enabled in prod)
ssl.endpoint.identification.algorithm: https
consumer:
group-id: orders-service
auto-offset-reset: earliest
producer:
acks: all
A few details matter for correctness:
trust-store-locationmust use thefile:(orclasspath:) resource prefix. A bare path is silently ignored.- The
sasl.jaas.configvalue is a single line terminated by a semicolon. YAML’s>-folded scalar lets you wrap it for readability while collapsing newlines into spaces. - For
PLAINinstead of SCRAM, swap the login module toorg.apache.kafka.common.security.plain.PlainLoginModule.
Never set
ssl.endpoint.identification.algorithmto an empty string to “fix” a TLS error. Disabling hostname verification opens you to man-in-the-middle attacks. Fix the certificate or truststore instead.
Keeping secrets out of source
The YAML above contains no literal passwords — every sensitive value is an ${ENV_VAR} placeholder resolved at startup. Spring’s relaxed binding reads these from environment variables, so the same artifact runs unchanged across environments. Inject them from your platform’s secret store rather than committing them.
For Kubernetes, mount secrets as environment variables:
env:
- name: KAFKA_PASSWORD
valueFrom:
secretKeyRef:
name: kafka-credentials
key: password
For HashiCorp Vault, Spring Cloud Vault fetches secrets at boot and exposes them as ordinary properties, so ${KAFKA_PASSWORD} resolves transparently:
spring:
config:
import: "vault://secret/orders-service"
cloud:
vault:
uri: ${VAULT_ADDR}
authentication: KUBERNETES
kubernetes:
role: orders-service
Verifying the connection
A tiny ApplicationRunner confirms authentication works without waiting for real traffic. It asks the admin client for the cluster ID — a request that only succeeds once the SASL_SSL handshake completes.
import org.apache.kafka.clients.admin.AdminClient;
import org.springframework.boot.ApplicationRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.kafka.core.KafkaAdmin;
@Bean
ApplicationRunner verifyKafka(KafkaAdmin kafkaAdmin) {
return args -> {
try (var admin = AdminClient.create(kafkaAdmin.getConfigurationProperties())) {
String clusterId = admin.describeCluster().clusterId().get();
System.out.println("Connected to Kafka cluster: " + clusterId);
}
};
}
Output:
Connected to Kafka cluster: 7vQ2k9xLToK1mF8aPzN0wg
If credentials are wrong you will instead see an org.apache.kafka.common.errors.SaslAuthenticationException; a bad truststore surfaces as an SSLHandshakeException. These distinct errors make it easy to tell an authentication problem from an encryption problem.
Best Practices
- Use
SASL_SSLfor every non-local environment and preferSCRAM-SHA-512orOAUTHBEARERoverPLAIN, which sends the password (TLS-wrapped but still recoverable broker-side). - Resolve all credentials from environment variables or a secret manager (Vault, AWS Secrets Manager, Kubernetes Secrets); never commit passwords or keystores to Git.
- Keep
ssl.endpoint.identification.algorithmset tohttpsso the client verifies the broker’s hostname. - Distribute only the truststore (public CA chain) to clients; reserve keystores for mutual-TLS scenarios and rotate both on a schedule.
- Scope each service to its own SCRAM user with least-privilege ACLs so a leaked credential cannot read or write unrelated topics.
- Add a startup health check (as shown above) so misconfiguration fails fast and loudly rather than at first message send.