From 2b9c6cecda041afff7727b4a950db1a835a43ddd Mon Sep 17 00:00:00 2001 From: Caius Durling Date: Fri, 5 Sep 2025 12:51:37 +0100 Subject: [PATCH] Allow SslContext#max_version to be set on SSL connections We've observed a kafka cluster that attempts to negotiate communication using TLS 1.3 but then fails. This requires us to pin the max version used to TLS 1.2. --- lib/kafka/client.rb | 5 +++-- lib/kafka/ssl_context.rb | 4 +++- spec/ssl_context_spec.rb | 13 +++++++++++++ 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/lib/kafka/client.rb b/lib/kafka/client.rb index af014854..a40d29d5 100644 --- a/lib/kafka/client.rb +++ b/lib/kafka/client.rb @@ -89,7 +89,7 @@ def initialize(seed_brokers:, client_id: "ruby-kafka", logger: nil, connect_time sasl_aws_msk_iam_secret_key_id: nil, sasl_aws_msk_iam_aws_region: nil, sasl_aws_msk_iam_session_token: nil, - sasl_over_ssl: true, ssl_ca_certs_from_system: false, partitioner: nil, sasl_oauth_token_provider: nil, ssl_verify_hostname: true, + sasl_over_ssl: true, ssl_ca_certs_from_system: false, partitioner: nil, sasl_oauth_token_provider: nil, ssl_verify_hostname: true, ssl_max_version: nil, resolve_seed_brokers: false) @logger = TaggedLogger.new(logger) @instrumenter = Instrumenter.new(client_id: client_id) @@ -104,7 +104,8 @@ def initialize(seed_brokers:, client_id: "ruby-kafka", logger: nil, connect_time client_cert_key_password: ssl_client_cert_key_password, client_cert_chain: ssl_client_cert_chain, ca_certs_from_system: ssl_ca_certs_from_system, - verify_hostname: ssl_verify_hostname + verify_hostname: ssl_verify_hostname, + max_version: ssl_max_version ) sasl_authenticator = SaslAuthenticator.new( diff --git a/lib/kafka/ssl_context.rb b/lib/kafka/ssl_context.rb index c5eac454..e8109786 100644 --- a/lib/kafka/ssl_context.rb +++ b/lib/kafka/ssl_context.rb @@ -6,7 +6,7 @@ module Kafka module SslContext CLIENT_CERT_DELIMITER = "\n-----END CERTIFICATE-----\n" - def self.build(ca_cert_file_path: nil, ca_cert: nil, client_cert: nil, client_cert_key: nil, client_cert_key_password: nil, client_cert_chain: nil, ca_certs_from_system: nil, verify_hostname: true) + def self.build(ca_cert_file_path: nil, ca_cert: nil, client_cert: nil, client_cert_key: nil, client_cert_key_password: nil, client_cert_chain: nil, ca_certs_from_system: nil, verify_hostname: true, max_version: nil) return nil unless ca_cert_file_path || ca_cert || client_cert || client_cert_key || client_cert_key_password || client_cert_chain || ca_certs_from_system ssl_context = OpenSSL::SSL::SSLContext.new @@ -60,6 +60,8 @@ def self.build(ca_cert_file_path: nil, ca_cert: nil, client_cert: nil, client_ce # Verify certificate hostname if supported (ruby >= 2.4.0) ssl_context.verify_hostname = verify_hostname if ssl_context.respond_to?(:verify_hostname=) + ssl_context.max_version = max_version if max_version + ssl_context end end diff --git a/spec/ssl_context_spec.rb b/spec/ssl_context_spec.rb index ccadaa80..3ebf84e6 100644 --- a/spec/ssl_context_spec.rb +++ b/spec/ssl_context_spec.rb @@ -61,4 +61,17 @@ expect(subject.extra_chain_cert).to eq expected_chain end end + + context 'with max version specified' do + let(:client_cert) { IO.read("spec/fixtures/client_cert.pem") } + let(:client_cert_key) { IO.read("spec/fixtures/client_cert_key.pem") } + let(:max_version) { OpenSSL::SSL::TLS1_2_VERSION } + + subject { Kafka::SslContext.build(client_cert: client_cert, client_cert_key: client_cert_key, max_version: max_version) } + + it 'configures max version' do + # OpenSSL::SSL::SSLContext doesn't provide public method to read this attribute + expect(subject.instance_variable_get(:@max_proto_version)).to eq(max_version) + end + end end