Browse Source
notification.kafka: add SASL authentication and TLS support (#8832)
notification.kafka: add SASL authentication and TLS support (#8832)
* notification.kafka: add SASL authentication and TLS support (#8827) Wire sarama SASL (PLAIN, SCRAM-SHA-256, SCRAM-SHA-512) and TLS configuration into the Kafka notification producer and consumer, enabling connections to secured Kafka clusters. * notification.kafka: validate mTLS config * kafka notification: validate partial mTLS config, replace panics with errors - Reject when only one of tls_client_cert/tls_client_key is provided - Replace three panic() calls in KafkaInput.initialize with returned errors * kafka notification: enforce minimum TLS 1.2 for Kafka connectionspull/8837/head
committed by
GitHub
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 226 additions and 10 deletions
-
2go.mod
-
2go.sum
-
11weed/command/scaffold/notification.toml
-
18weed/notification/kafka/kafka_queue.go
-
112weed/notification/kafka/kafka_sasl_tls.go
-
65weed/notification/kafka/kafka_sasl_tls_test.go
-
26weed/replication/sub/notification_kafka.go
@ -0,0 +1,112 @@ |
|||||
|
package kafka |
||||
|
|
||||
|
import ( |
||||
|
"crypto/tls" |
||||
|
"crypto/x509" |
||||
|
"fmt" |
||||
|
"os" |
||||
|
"strings" |
||||
|
|
||||
|
"github.com/Shopify/sarama" |
||||
|
"github.com/xdg-go/scram" |
||||
|
) |
||||
|
|
||||
|
// SASLTLSConfig holds SASL and TLS configuration for Kafka connections.
|
||||
|
type SASLTLSConfig struct { |
||||
|
SASLEnabled bool |
||||
|
SASLMechanism string |
||||
|
SASLUsername string |
||||
|
SASLPassword string |
||||
|
|
||||
|
TLSEnabled bool |
||||
|
TLSCACert string |
||||
|
TLSClientCert string |
||||
|
TLSClientKey string |
||||
|
TLSInsecureSkipVerify bool |
||||
|
} |
||||
|
|
||||
|
// ConfigureSASLTLS applies SASL and TLS settings to a sarama config.
|
||||
|
func ConfigureSASLTLS(config *sarama.Config, st SASLTLSConfig) error { |
||||
|
if st.SASLEnabled { |
||||
|
config.Net.SASL.Enable = true |
||||
|
config.Net.SASL.User = st.SASLUsername |
||||
|
config.Net.SASL.Password = st.SASLPassword |
||||
|
|
||||
|
mechanism := strings.ToUpper(st.SASLMechanism) |
||||
|
switch mechanism { |
||||
|
case "PLAIN", "": |
||||
|
config.Net.SASL.Mechanism = sarama.SASLTypePlaintext |
||||
|
case "SCRAM-SHA-256": |
||||
|
config.Net.SASL.Mechanism = sarama.SASLTypeSCRAMSHA256 |
||||
|
config.Net.SASL.SCRAMClientGeneratorFunc = func() sarama.SCRAMClient { |
||||
|
return &scramClient{HashGeneratorFcn: scram.SHA256} |
||||
|
} |
||||
|
case "SCRAM-SHA-512": |
||||
|
config.Net.SASL.Mechanism = sarama.SASLTypeSCRAMSHA512 |
||||
|
config.Net.SASL.SCRAMClientGeneratorFunc = func() sarama.SCRAMClient { |
||||
|
return &scramClient{HashGeneratorFcn: scram.SHA512} |
||||
|
} |
||||
|
default: |
||||
|
return fmt.Errorf("unsupported SASL mechanism: %s", mechanism) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if st.TLSEnabled { |
||||
|
if (st.TLSClientCert == "") != (st.TLSClientKey == "") { |
||||
|
return fmt.Errorf("both tls_client_cert and tls_client_key must be provided for mTLS, or neither") |
||||
|
} |
||||
|
|
||||
|
tlsConfig := &tls.Config{ |
||||
|
MinVersion: tls.VersionTLS12, |
||||
|
InsecureSkipVerify: st.TLSInsecureSkipVerify, |
||||
|
} |
||||
|
|
||||
|
if st.TLSCACert != "" { |
||||
|
caCert, err := os.ReadFile(st.TLSCACert) |
||||
|
if err != nil { |
||||
|
return fmt.Errorf("failed to read CA certificate: %w", err) |
||||
|
} |
||||
|
caCertPool := x509.NewCertPool() |
||||
|
if !caCertPool.AppendCertsFromPEM(caCert) { |
||||
|
return fmt.Errorf("failed to parse CA certificate") |
||||
|
} |
||||
|
tlsConfig.RootCAs = caCertPool |
||||
|
} |
||||
|
|
||||
|
if st.TLSClientCert != "" && st.TLSClientKey != "" { |
||||
|
cert, err := tls.LoadX509KeyPair(st.TLSClientCert, st.TLSClientKey) |
||||
|
if err != nil { |
||||
|
return fmt.Errorf("failed to load client certificate/key: %w", err) |
||||
|
} |
||||
|
tlsConfig.Certificates = []tls.Certificate{cert} |
||||
|
} |
||||
|
|
||||
|
config.Net.TLS.Enable = true |
||||
|
config.Net.TLS.Config = tlsConfig |
||||
|
} |
||||
|
|
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
// scramClient implements the sarama.SCRAMClient interface.
|
||||
|
type scramClient struct { |
||||
|
*scram.ClientConversation |
||||
|
scram.HashGeneratorFcn |
||||
|
} |
||||
|
|
||||
|
func (c *scramClient) Begin(userName, password, authzID string) (err error) { |
||||
|
client, err := c.HashGeneratorFcn.NewClient(userName, password, authzID) |
||||
|
if err != nil { |
||||
|
return err |
||||
|
} |
||||
|
c.ClientConversation = client.NewConversation() |
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
func (c *scramClient) Step(challenge string) (string, error) { |
||||
|
return c.ClientConversation.Step(challenge) |
||||
|
} |
||||
|
|
||||
|
func (c *scramClient) Done() bool { |
||||
|
return c.ClientConversation.Done() |
||||
|
} |
||||
@ -0,0 +1,65 @@ |
|||||
|
package kafka |
||||
|
|
||||
|
import ( |
||||
|
"strings" |
||||
|
"testing" |
||||
|
|
||||
|
"github.com/Shopify/sarama" |
||||
|
) |
||||
|
|
||||
|
func TestConfigureSASLTLSRejectsPartialMTLSConfig(t *testing.T) { |
||||
|
tests := []struct { |
||||
|
name string |
||||
|
cfg SASLTLSConfig |
||||
|
}{ |
||||
|
{ |
||||
|
name: "missing key", |
||||
|
cfg: SASLTLSConfig{ |
||||
|
TLSEnabled: true, |
||||
|
TLSClientCert: "/tmp/client.crt", |
||||
|
}, |
||||
|
}, |
||||
|
{ |
||||
|
name: "missing cert", |
||||
|
cfg: SASLTLSConfig{ |
||||
|
TLSEnabled: true, |
||||
|
TLSClientKey: "/tmp/client.key", |
||||
|
}, |
||||
|
}, |
||||
|
} |
||||
|
|
||||
|
for _, tt := range tests { |
||||
|
t.Run(tt.name, func(t *testing.T) { |
||||
|
err := ConfigureSASLTLS(sarama.NewConfig(), tt.cfg) |
||||
|
if err == nil { |
||||
|
t.Fatal("expected error") |
||||
|
} |
||||
|
if !strings.Contains(err.Error(), "both tls_client_cert and tls_client_key must be provided") { |
||||
|
t.Fatalf("unexpected error: %v", err) |
||||
|
} |
||||
|
}) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func TestConfigureSASLTLSConfiguresSCRAMSHA256(t *testing.T) { |
||||
|
config := sarama.NewConfig() |
||||
|
err := ConfigureSASLTLS(config, SASLTLSConfig{ |
||||
|
SASLEnabled: true, |
||||
|
SASLMechanism: "SCRAM-SHA-256", |
||||
|
SASLUsername: "alice", |
||||
|
SASLPassword: "secret", |
||||
|
}) |
||||
|
if err != nil { |
||||
|
t.Fatalf("ConfigureSASLTLS returned error: %v", err) |
||||
|
} |
||||
|
|
||||
|
if !config.Net.SASL.Enable { |
||||
|
t.Fatal("expected SASL to be enabled") |
||||
|
} |
||||
|
if config.Net.SASL.Mechanism != sarama.SASLTypeSCRAMSHA256 { |
||||
|
t.Fatalf("unexpected mechanism: %v", config.Net.SASL.Mechanism) |
||||
|
} |
||||
|
if config.Net.SASL.SCRAMClientGeneratorFunc == nil { |
||||
|
t.Fatal("expected SCRAM client generator") |
||||
|
} |
||||
|
} |
||||
Write
Preview
Loading…
Cancel
Save
Reference in new issue