Add SASL/PLAIN password protection to the Kafka container - #19117
Add SASL/PLAIN password protection to the Kafka container#19117Guillaume Delahaye (g7ed6e) wants to merge 1 commit into
Conversation
Kafka was the last container integration in microsoft#6155 without authentication. Enable SASL/PLAIN over SASL_PLAINTEXT on the two client facing listeners, with a generated password when none is supplied. Hosting: - KafkaServerResource gains UserNameParameter/PasswordParameter, a UserNameReference defaulting to "kafka", and Username/Password connection properties. - The connection string stays a bare host:port when no password is configured. With a password it becomes a semicolon separated list of Confluent client configuration properties so the credentials can travel with it. - AddKafka takes optional userName/password parameters, alongside a convenience overload preserving the previous signature. WithPassword and WithUserName allow reconfiguring afterwards, and WithPassword(null) turns authentication off. - When a password is configured the client facing listeners are renamed to EXTERNAL/INTERNAL and mapped to SASL_PLAINTEXT. The names deliberately contain no underscore because the Confluent image translates KAFKA_FOO_BAR into foo.bar, which would otherwise require escaping the listener name in KAFKA_LISTENER_NAME_<LISTENER>_PLAIN_SASL_JAAS_CONFIG. The KRaft controller and inter broker listeners stay plaintext on the loopback interface. - The health check built its ProducerConfig by assigning the connection string to BootstrapServers. That is wrong once the connection string is a keyed list, and had no credentials either way, so it now resolves the endpoint and the credentials separately. - WithKafkaUI configures the matching security protocol, mechanism and JAAS properties so the UI can still reach the broker. Client: - Aspire.Confluent.Kafka applies the connection string onto the client configuration. A value without '=' is still treated as a bare bootstrap server list, otherwise it is parsed with DbConnectionStringBuilder and BootstrapServers, SecurityProtocol, SaslMechanism, SaslUsername and SaslPassword are applied. Contributes to microsoft#6155 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
🚀 Dogfood this PR with:
curl -fsSL https://raw.githubusercontent.com/microsoft/aspire/main/eng/scripts/get-aspire-cli-pr.sh | bash -s -- 19117Or
iex "& { $(irm https://raw.githubusercontent.com/microsoft/aspire/main/eng/scripts/get-aspire-cli-pr.ps1) } 19117" |
There was a problem hiding this comment.
Pull request overview
Adds default SASL/PLAIN authentication to the Kafka container-backed integration and updates Aspire clients to consume authenticated connection strings.
Changes:
- Configures Kafka, health checks, and Kafka UI with generated or supplied credentials.
- Parses keyed Kafka connection strings in producer and consumer integrations.
- Adds authentication-focused tests and documentation.
Reviewed changes
Copilot reviewed 14 out of 14 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
src/Aspire.Hosting.Kafka/KafkaBuilderExtensions.cs |
Configures SASL, health checks, and Kafka UI. |
src/Aspire.Hosting.Kafka/KafkaServerResource.cs |
Adds credentials and authenticated connection strings. |
src/Aspire.Hosting.Kafka/README.md |
Documents authentication and connection properties. |
src/Components/Aspire.Confluent.Kafka/KafkaConnectionString.cs |
Parses keyed Kafka connection strings. |
src/Components/Aspire.Confluent.Kafka/KafkaProducerSettings.cs |
Applies connection strings to producer configuration. |
src/Components/Aspire.Confluent.Kafka/KafkaConsumerSettings.cs |
Applies connection strings to consumer configuration. |
src/Components/Aspire.Confluent.Kafka/README.md |
Documents keyed connection-string support. |
tests/Aspire.Hosting.Kafka.Tests/AddKafkaTests.cs |
Tests SASL configuration and manifests. |
tests/Aspire.Hosting.Kafka.Tests/ConnectionPropertiesTests.cs |
Tests credential connection properties. |
tests/Aspire.Hosting.Kafka.Tests/KafkaFunctionalTests.cs |
Tests rejection of unauthenticated clients. |
tests/Aspire.Hosting.Kafka.Tests/KafkaPublicApiTests.cs |
Tests new API validation. |
tests/Aspire.Confluent.Kafka.Tests/CommonHelpers.cs |
Adds shared SASL test data. |
tests/Aspire.Confluent.Kafka.Tests/ProducerConfigurationTests.cs |
Tests producer SASL parsing. |
tests/Aspire.Confluent.Kafka.Tests/ConsumerConfigurationTests.cs |
Tests consumer SASL parsing. |
| if (ConnectionString is not null) | ||
| { | ||
| Config.BootstrapServers = ConnectionString; | ||
| KafkaConnectionString.Apply(ConnectionString, Config); |
| if (ConnectionString is not null) | ||
| { | ||
| Config.BootstrapServers = ConnectionString; | ||
| KafkaConnectionString.Apply(ConnectionString, Config); |
|
|
||
| [Fact] | ||
| [RequiresFeature(TestFeature.Docker)] | ||
| [ActiveIssue("https://github.com/microsoft/aspire/issues/11820", typeof(PlatformDetection), nameof(PlatformDetection.IsRunningFromAzdo))] |
| return ReferenceExpression.Create( | ||
| $"org.apache.kafka.common.security.plain.PlainLoginModule required username=\"{userName}\" password=\"{password}\" user_{userName}=\"{password}\";"); |
| [ActiveIssue("https://github.com/microsoft/aspire/issues/11820", typeof(PlatformDetection), nameof(PlatformDetection.IsRunningFromAzdo))] | ||
| public async Task VerifyKafkaResourceRejectsUnauthenticatedClients() | ||
| { | ||
| var cts = new CancellationTokenSource(TimeSpan.FromMinutes(3)); |
| ```csharp | ||
| var userName = builder.AddParameter("kafka-user"); | ||
| var password = builder.AddParameter("kafka-password", secret: true); | ||
|
|
||
| var kafka = builder.AddKafka("messaging", userName: userName, password: password); | ||
| ``` |
| builder.Append($"{UserNameReference}"); | ||
| builder.Append($";SaslPassword=\"{PasswordParameter}\""); |
|
Retrying the failed CI jobs for this pull request from the CI run attempt. The rerun is being tracked in the rerun attempt. |
|
Retrying the failed CI jobs for this pull request from the CI run attempt. The rerun is being tracked in the rerun attempt. |
|
Retrying the failed CI jobs for this pull request from the CI run attempt. The rerun is being tracked in the rerun attempt. |
Description
Kafka is the last unchecked container in #6155 — every other integration (Garnet, MongoDB, Nats, Redis, Seq, Valkey) already enables authentication by default. This adds SASL/PLAIN over
SASL_PLAINTEXT, with a generated password when none is supplied, following the pattern those integrations established.Hosting
KafkaServerResourcegainsUserNameParameter/PasswordParameter, aUserNameReferencedefaulting tokafka, andUsername/Passwordconnection properties.AddKafkatakes optionaluserName/passwordparameters (plus a convenience overload preserving the previous signature).WithPassword/WithUserNameallow reconfiguring afterwards, andWithPassword(null)turns authentication off.When a password is configured, the two client-facing listeners are renamed
EXTERNAL/INTERNALand mapped toSASL_PLAINTEXT. The KRaft controller and inter-broker listeners stay plaintext on the loopback interface inside the container.The new names deliberately contain no underscore: the Confluent image translates
KAFKA_FOO_BARintofoo.bar, so an underscore inside a listener name would have to be escaped as a double underscore inKAFKA_LISTENER_NAME_<LISTENER>_PLAIN_SASL_JAAS_CONFIG. Underscore-free names avoid relying on that poorly-documented rule. The listener names are unchanged on the no-password path.WithKafkaUIconfigures the matching security protocol, mechanism and JAAS properties so the UI can still reach the broker.Bug fix along the way: the health check built its
ProducerConfigby assigning the whole connection string toBootstrapServers. That breaks once the connection string is a keyed list, and it carried no credentials either way, so it now resolves the endpoint and the credentials separately.Client
Aspire.Confluent.Kafkaapplies the connection string onto the client configuration rather than assuming it is a bootstrap server list. A value containing no=is still assigned toBootstrapServersunchanged; otherwise it is parsed withDbConnectionStringBuilderandBootstrapServers,SecurityProtocol,SaslMechanism,SaslUsernameandSaslPasswordare applied. This mirrorsQdrantClientSettings/MilvusClientSettings.Connection string format
The password is quoted so it may contain
;and=. Generated passwords usespecial: falsesince they also land in a JAAS configuration string.Breaking change
Consistent with the Nats/Valkey/Garnet/Redis changes for this issue, existing app models get authentication turned on and a different connection string shape. Consumers going through
Aspire.Confluent.Kafkaare unaffected, but anything readingConnectionStrings__<name>directly — a non-.NET client, for instance — needs to authenticate.WithPassword(null)restores the previous behavior.Not included
api/Aspire.Hosting.Kafka.csandapi/Aspire.Hosting.Kafka.ats.txtare left untouched, per.agents/skills/hosting-integration-authoring/resources/polyglot-exports.md— these are release compatibility baselines updated by the release workflow after API changes are accepted. Happy to regenerate if you'd prefer them in this PR.playground/kafka/KafkaBasic.AppHost/aspire-manifest.jsonis left to the automated "Update Playground Manifests" job. Note it is already stale onmain(pinned to image tag8.1.1vs8.2.0in code).Verification
Beyond the unit tests, this was verified against real containers:
confluent-local:8.2.0broker started with the emitted environment resolves exactly the intended configuration —listener.security.protocol.map = CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT,EXTERNAL:SASL_PLAINTEXT,INTERNAL:SASL_PLAINTEXTandsasl.enabled.mechanisms = [PLAIN].kafbat/kafka-ui:v1.5.0reports"status":"ONLINE"against the SASL broker, confirming the UI properties authenticate.Contributes to #6155
Checklist
<remarks />and<code />elements on your triple slash comments?Authentication only — no authorizer is configured, so any authenticated client keeps full access. Traffic is not encrypted (
SASL_PLAINTEXT), matching the "plain sasl is sufficient" scope in the issue. The goal is the defense-in-depth layer described in #6155, not transport security.🤖 Generated with Claude Code