Skip to content

Commit cd90ea8

Browse files
authored
Merge pull request #498 from fresh-borzoni/sasl-scram-support
SASL Auth implementation(PLAIN + SCRAM)
2 parents 6eebf98 + b740a35 commit cd90ea8

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+2550
-327
lines changed

AUTH.md

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
# SASL Authentication
2+
3+
KafkaEx supports SASL authentication for secure Kafka clusters. Multiple mechanisms are available with flexible configuration options.
4+
5+
## Supported Mechanisms
6+
7+
- **PLAIN** - Simple username/password (requires SSL/TLS)
8+
- **SCRAM-SHA-256** - Secure challenge-response authentication (Kafka 0.10.2+)
9+
- **SCRAM-SHA-512** - Secure challenge-response with stronger hash (Kafka 0.10.2+)
10+
11+
## Configuration
12+
13+
### Via Application Config
14+
15+
```elixir
16+
# config/config.exs
17+
config :kafka_ex,
18+
brokers: [{"localhost", 9292}],
19+
use_ssl: true,
20+
ssl_options: [
21+
verify: :verify_peer,
22+
cacertfile: "/path/to/ca-cert"
23+
],
24+
sasl: %{
25+
mechanism: :scram,
26+
username: System.get_env("KAFKA_USERNAME"),
27+
password: System.get_env("KAFKA_PASSWORD"),
28+
mechanism_opts: %{algo: :sha256} # :sha256 or :sha512
29+
}
30+
```
31+
32+
### Via Worker Options
33+
34+
```elixir
35+
opts = [
36+
uris: [{"broker1", 9092}, {"broker2", 9092}],
37+
use_ssl: true,
38+
ssl_options: [verify: :verify_none],
39+
auth: KafkaEx.Auth.Config.new(%{
40+
mechanism: :plain,
41+
username: "alice",
42+
password: "secret123"
43+
})
44+
]
45+
46+
{:ok, pid} = KafkaEx.create_worker(:my_worker, opts)
47+
```
48+
49+
### Docker Compose Setup
50+
51+
The project includes Docker configurations for testing SASL authentication:
52+
53+
```bash
54+
# Start Kafka with SASL enabled
55+
docker-compose up -d
56+
57+
# Ports:
58+
# 9092 - No authentication (SSL)
59+
# 9192 - SASL/PLAIN (SSL)
60+
# 9292 - SASL/SCRAM (SSL)
61+
```
62+
63+
## Security Considerations
64+
65+
- Always use SSL/TLS with PLAIN mechanism - plain text passwords must be encrypted in transit
66+
- Use environment variables for credentials - never hardcode passwords
67+
- SCRAM is preferred over PLAIN when both are available
68+
69+
### Minimum Kafka Versions
70+
71+
- PLAIN: Kafka 0.9.0+
72+
- SCRAM: Kafka 0.10.2+
73+
74+
## Testing with Different Mechanisms
75+
76+
```elixir
77+
# Test PLAIN authentication
78+
config :kafka_ex,
79+
brokers: [{"localhost", 9192}],
80+
use_ssl: true,
81+
ssl_options: [verify: :verify_none],
82+
sasl: %{mechanism: :plain, username: "test", password: "secret"}
83+
84+
# Test SCRAM-SHA-256
85+
config :kafka_ex,
86+
brokers: [{"localhost", 9292}],
87+
use_ssl: true,
88+
ssl_options: [verify: :verify_none],
89+
sasl: %{
90+
mechanism: :scram,
91+
username: "test",
92+
password: "secret",
93+
mechanism_opts: %{algo: :sha256}
94+
}
95+
```
96+
97+
## Integration with Existing Code
98+
99+
SASL authentication is transparent to the rest of your KafkaEx usage:
100+
101+
```elixir
102+
# Once configured, use KafkaEx normally
103+
KafkaEx.metadata()
104+
KafkaEx.produce("my-topic", 0, "message")
105+
messages = KafkaEx.fetch("my-topic", 0, offset: 0)
106+
```
107+
108+
## Troubleshooting
109+
110+
- **Connection refused**: Ensure you're using the correct port for SASL (9192 for PLAIN, 9292 for SCRAM in test setup)
111+
- **Authentication failed**: Check credentials and ensure the user exists in Kafka with the correct SASL mechanism configured
112+
- **SSL handshake error**: Verify SSL certificates or use verify: :verify_none for testing (not production!)
113+
- **Unsupported mechanism**: Ensure your Kafka version supports the mechanism (SCRAM requires 0.10.2+)
114+
115+
## Advanced: Custom Authentication
116+
117+
For OAuth or custom mechanisms, implement the `KafkaEx.Auth.Mechanism` behaviour:
118+
119+
```elixir
120+
defmodule MyAuth do
121+
@behaviour KafkaEx.Auth.Mechanism
122+
123+
def mechanism_name(_), do: "OAUTHBEARER"
124+
125+
def authenticate(config, send_fun) do
126+
# Custom authentication logic
127+
:ok
128+
end
129+
end
130+
```
131+
132+
## Implementation Notes
133+
134+
### Version Compatibility
135+
136+
The SASL implementation handles different Kafka versions appropriately:
137+
138+
- Kafka 0.9.x: Skips API versions call (not supported)
139+
- Kafka 0.10.0-0.10.1: Queries API versions, supports PLAIN only
140+
- Kafka 0.10.2+: Full support including SCRAM mechanisms
141+
142+
### Technical Details
143+
144+
- Authentication occurs immediately after socket creation
145+
- The implementation handles packet mode switching between raw and length-prefixed formats
146+
- Correlation IDs are used to match requests with responses
147+
- Server signatures are validated in SCRAM authentication
148+
- Passwords are never logged and are redacted in inspect output

README.md

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -355,6 +355,47 @@ KafkaEx.produce(produce_request)
355355

356356
Compression is handled automatically on the consuming/fetching end.
357357

358+
## SASL Authentication
359+
360+
KafkaEx supports connecting to secure Kafka clusters with SASL mechanisms.
361+
362+
Example:
363+
364+
```elixir
365+
# config/config.exs
366+
config :kafka_ex,
367+
brokers: [{"localhost", 9292}],
368+
use_ssl: true,
369+
ssl_options: [verify: :verify_none],
370+
sasl: %{
371+
mechanism: :scram,
372+
username: System.get_env("KAFKA_USERNAME"),
373+
password: System.get_env("KAFKA_PASSWORD"),
374+
mechanism_opts: %{algo: :sha256} # or :sha512
375+
}
376+
```
377+
378+
Or via worker options:
379+
380+
```elixir
381+
{:ok, _pid} = KafkaEx.create_worker(:sasl_worker, [
382+
uris: [{"localhost", 9292}],
383+
use_ssl: true,
384+
ssl_options: [verify: :verify_none],
385+
auth: KafkaEx.Auth.Config.new(%{
386+
mechanism: :plain,
387+
username: "alice",
388+
password: "secret123"
389+
})
390+
])
391+
```
392+
393+
> ✅ Use SSL/TLS with PLAIN (never send passwords in cleartext).
394+
> ✅ Prefer SCRAM over PLAIN when supported.
395+
> ✅ PLAIN requires Kafka 0.9.0+, SCRAM requires 0.10.2+.
396+
397+
👉 See [AUTH.md](./AUTH.md) for full details, configuration examples, and troubleshooting tips.
398+
358399
## Testing
359400

360401
It is strongly recommended to test using the Dockerized test cluster described
@@ -366,13 +407,18 @@ asynchronous issues, the test suite sometimes fails on the first try.
366407
### Dockerized Test Cluster
367408

368409
Testing KafkaEx requires a local SSL-enabled Kafka cluster with 3 nodes: one
369-
node listening on each port 9092, 9093, and 9093. The easiest way to do this
410+
node listening on appropriate port. The easiest way to do this
370411
is using the scripts in
371412
this repository that utilize [Docker](https://www.docker.com) and
372413
[Docker Compose](https://www.docker.com/products/docker-compose) (both of which
373414
are freely available). This is the method we use for our CI testing of
374415
KafkaEx.
375416

417+
Ports:
418+
9092-9094 - No authentication (SSL)
419+
9192-9194 - SASL/PLAIN (SSL)
420+
9292-9294 - SASL/SCRAM (SSL)
421+
376422
To launch the included test cluster, run
377423

378424
```

config/config.exs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,19 @@ config :kafka_ex,
7979
# use "kayrock" for the new client
8080
kafka_version: "0.10.1"
8181

82+
# SASL Authentication (optional)
83+
84+
# Configure SASL credentials and mechanism
85+
# sasl: %{
86+
# mechanism: :scram, # :plain or :scram
87+
# username: "kafka_user", # USE ENV VARS and don't expose secrets
88+
# password: "kafka_password", # USE ENV VARS and don't expose secrets
89+
# mechanism_opts: %{algorithm: :sha256} # For SCRAM only, :sha256 or :sha512
90+
# }
91+
#
92+
# Note: SASL/PLAIN requires SSL to be enabled for security
93+
94+
8295
env_config = Path.expand("#{Mix.env()}.exs", __DIR__)
8396

8497
if File.exists?(env_config) do

0 commit comments

Comments
 (0)