Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 9c89990

Browse files
authored
Merge pull request #937 from kirkokada/aws-iam-auth
AWS MSK IAM auth
2 parents d5def61 + 9d44e51 commit 9c89990

File tree

7 files changed

+176
-3
lines changed

7 files changed

+176
-3
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ Changes and additions to the library will be listed here.
44

55
## Unreleased
66

7+
- Add support for AWS IAM Authentication to an MSK cluster (#907).
8+
79
## 1.4.0
810

911
- Refresh a stale cluster's metadata if necessary on `Kafka::Client#deliver_message` (#901).

README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1121,6 +1121,20 @@ kafka = Kafka.new(
11211121
)
11221122
```
11231123

1124+
##### AWS MSK (IAM)
1125+
In order to authenticate using IAM w/ an AWS MSK cluster, set your access key, secret key, and region when initializing the Kafka client:
1126+
1127+
```ruby
1128+
k = Kafka.new(
1129+
["kafka1:9092"],
1130+
sasl_aws_msk_iam_access_key_id: 'iam_access_key',
1131+
sasl_aws_msk_iam_secret_key_id: 'iam_secret_key',
1132+
sasl_aws_msk_iam_aws_region: 'us-west-2',
1133+
ssl_ca_certs_from_system: true,
1134+
# ...
1135+
)
1136+
```
1137+
11241138
##### PLAIN
11251139
In order to authenticate using PLAIN, you must set your username and password when initializing the Kafka client:
11261140

lib/kafka/client.rb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,10 @@ def initialize(seed_brokers:, client_id: "ruby-kafka", logger: nil, connect_time
8585
ssl_client_cert_key_password: nil, ssl_client_cert_chain: nil, sasl_gssapi_principal: nil,
8686
sasl_gssapi_keytab: nil, sasl_plain_authzid: '', sasl_plain_username: nil, sasl_plain_password: nil,
8787
sasl_scram_username: nil, sasl_scram_password: nil, sasl_scram_mechanism: nil,
88+
sasl_aws_msk_iam_access_key_id: nil,
89+
sasl_aws_msk_iam_secret_key_id: nil,
90+
sasl_aws_msk_iam_aws_region: nil,
91+
sasl_aws_msk_iam_session_token: nil,
8892
sasl_over_ssl: true, ssl_ca_certs_from_system: false, partitioner: nil, sasl_oauth_token_provider: nil, ssl_verify_hostname: true,
8993
resolve_seed_brokers: false)
9094
@logger = TaggedLogger.new(logger)
@@ -112,6 +116,10 @@ def initialize(seed_brokers:, client_id: "ruby-kafka", logger: nil, connect_time
112116
sasl_scram_username: sasl_scram_username,
113117
sasl_scram_password: sasl_scram_password,
114118
sasl_scram_mechanism: sasl_scram_mechanism,
119+
sasl_aws_msk_iam_access_key_id: sasl_aws_msk_iam_access_key_id,
120+
sasl_aws_msk_iam_secret_key_id: sasl_aws_msk_iam_secret_key_id,
121+
sasl_aws_msk_iam_aws_region: sasl_aws_msk_iam_aws_region,
122+
sasl_aws_msk_iam_session_token: sasl_aws_msk_iam_session_token,
115123
sasl_oauth_token_provider: sasl_oauth_token_provider,
116124
logger: @logger
117125
)

lib/kafka/protocol/sasl_handshake_request.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ module Protocol
88

99
class SaslHandshakeRequest
1010

11-
SUPPORTED_MECHANISMS = %w(GSSAPI PLAIN SCRAM-SHA-256 SCRAM-SHA-512 OAUTHBEARER)
11+
SUPPORTED_MECHANISMS = %w(AWS_MSK_IAM GSSAPI PLAIN SCRAM-SHA-256 SCRAM-SHA-512 OAUTHBEARER)
1212

1313
def initialize(mechanism)
1414
unless SUPPORTED_MECHANISMS.include?(mechanism)

lib/kafka/sasl/awsmskiam.rb

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
# frozen_string_literal: true
2+
3+
require 'securerandom'
4+
require 'base64'
5+
require 'json'
6+
7+
module Kafka
8+
module Sasl
9+
class AwsMskIam
10+
AWS_MSK_IAM = "AWS_MSK_IAM"
11+
12+
def initialize(aws_region:, access_key_id:, secret_key_id:, session_token: nil, logger:)
13+
@semaphore = Mutex.new
14+
15+
@aws_region = aws_region
16+
@access_key_id = access_key_id
17+
@secret_key_id = secret_key_id
18+
@session_token = session_token
19+
@logger = TaggedLogger.new(logger)
20+
end
21+
22+
def ident
23+
AWS_MSK_IAM
24+
end
25+
26+
def configured?
27+
@aws_region && @access_key_id && @secret_key_id
28+
end
29+
30+
def authenticate!(host, encoder, decoder)
31+
@logger.debug "Authenticating #{@access_key_id} with SASL #{AWS_MSK_IAM}"
32+
33+
host_without_port = host.split(':', -1).first
34+
35+
time_now = Time.now.utc
36+
37+
msg = authentication_payload(host: host_without_port, time_now: time_now)
38+
@logger.debug "Sending first client SASL AWS_MSK_IAM message:"
39+
@logger.debug msg
40+
encoder.write_bytes(msg)
41+
42+
begin
43+
@server_first_message = decoder.bytes
44+
@logger.debug "Received first server SASL AWS_MSK_IAM message: #{@server_first_message}"
45+
46+
raise Kafka::Error, "SASL AWS_MSK_IAM authentication failed: unknown error" unless @server_first_message
47+
rescue Errno::ETIMEDOUT, EOFError => e
48+
raise Kafka::Error, "SASL AWS_MSK_IAM authentication failed: #{e.message}"
49+
end
50+
51+
@logger.debug "SASL #{AWS_MSK_IAM} authentication successful"
52+
end
53+
54+
private
55+
56+
def bin_to_hex(s)
57+
s.each_byte.map { |b| b.to_s(16).rjust(2, '0') }.join
58+
end
59+
60+
def digest
61+
@digest ||= OpenSSL::Digest::SHA256.new
62+
end
63+
64+
def authentication_payload(host:, time_now:)
65+
{
66+
'version' => "2020_10_22",
67+
'host' => host,
68+
'user-agent' => "ruby-kafka",
69+
'action' => "kafka-cluster:Connect",
70+
'x-amz-algorithm' => "AWS4-HMAC-SHA256",
71+
'x-amz-credential' => @access_key_id + "/" + time_now.strftime("%Y%m%d") + "/" + @aws_region + "/kafka-cluster/aws4_request",
72+
'x-amz-date' => time_now.strftime("%Y%m%dT%H%M%SZ"),
73+
'x-amz-signedheaders' => "host",
74+
'x-amz-expires' => "900",
75+
'x-amz-security-token' => @session_token,
76+
'x-amz-signature' => signature(host: host, time_now: time_now)
77+
}.delete_if { |_, v| v.nil? }.to_json
78+
end
79+
80+
def canonical_request(host:, time_now:)
81+
"GET\n" +
82+
"/\n" +
83+
canonical_query_string(time_now: time_now) + "\n" +
84+
canonical_headers(host: host) + "\n" +
85+
signed_headers + "\n" +
86+
hashed_payload
87+
end
88+
89+
def canonical_query_string(time_now:)
90+
params = {
91+
"Action" => "kafka-cluster:Connect",
92+
"X-Amz-Algorithm" => "AWS4-HMAC-SHA256",
93+
"X-Amz-Credential" => @access_key_id + "/" + time_now.strftime("%Y%m%d") + "/" + @aws_region + "/kafka-cluster/aws4_request",
94+
"X-Amz-Date" => time_now.strftime("%Y%m%dT%H%M%SZ"),
95+
"X-Amz-Expires" => "900",
96+
"X-Amz-Security-Token" => @session_token,
97+
"X-Amz-SignedHeaders" => "host"
98+
}.delete_if { |_, v| v.nil? }
99+
100+
URI.encode_www_form(params)
101+
end
102+
103+
def canonical_headers(host:)
104+
"host" + ":" + host + "\n"
105+
end
106+
107+
def signed_headers
108+
"host"
109+
end
110+
111+
def hashed_payload
112+
bin_to_hex(digest.digest(""))
113+
end
114+
115+
def string_to_sign(host:, time_now:)
116+
"AWS4-HMAC-SHA256" + "\n" +
117+
time_now.strftime("%Y%m%dT%H%M%SZ") + "\n" +
118+
time_now.strftime("%Y%m%d") + "/" + @aws_region + "/kafka-cluster/aws4_request" + "\n" +
119+
bin_to_hex(digest.digest(canonical_request(host: host, time_now: time_now)))
120+
end
121+
122+
def signature(host:, time_now:)
123+
date_key = OpenSSL::HMAC.digest("SHA256", "AWS4" + @secret_key_id, time_now.strftime("%Y%m%d"))
124+
date_region_key = OpenSSL::HMAC.digest("SHA256", date_key, @aws_region)
125+
date_region_service_key = OpenSSL::HMAC.digest("SHA256", date_region_key, "kafka-cluster")
126+
signing_key = OpenSSL::HMAC.digest("SHA256", date_region_service_key, "aws4_request")
127+
signature = bin_to_hex(OpenSSL::HMAC.digest("SHA256", signing_key, string_to_sign(host: host, time_now: time_now)))
128+
129+
signature
130+
end
131+
end
132+
end
133+
end

lib/kafka/sasl_authenticator.rb

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,18 @@
44
require 'kafka/sasl/gssapi'
55
require 'kafka/sasl/scram'
66
require 'kafka/sasl/oauth'
7+
require 'kafka/sasl/awsmskiam'
78

89
module Kafka
910
class SaslAuthenticator
1011
def initialize(logger:, sasl_gssapi_principal:, sasl_gssapi_keytab:,
1112
sasl_plain_authzid:, sasl_plain_username:, sasl_plain_password:,
1213
sasl_scram_username:, sasl_scram_password:, sasl_scram_mechanism:,
13-
sasl_oauth_token_provider:)
14+
sasl_oauth_token_provider:,
15+
sasl_aws_msk_iam_access_key_id:,
16+
sasl_aws_msk_iam_secret_key_id:,
17+
sasl_aws_msk_iam_aws_region:,
18+
sasl_aws_msk_iam_session_token: nil)
1419
@logger = TaggedLogger.new(logger)
1520

1621
@plain = Sasl::Plain.new(
@@ -33,12 +38,20 @@ def initialize(logger:, sasl_gssapi_principal:, sasl_gssapi_keytab:,
3338
logger: @logger,
3439
)
3540

41+
@aws_msk_iam = Sasl::AwsMskIam.new(
42+
access_key_id: sasl_aws_msk_iam_access_key_id,
43+
secret_key_id: sasl_aws_msk_iam_secret_key_id,
44+
aws_region: sasl_aws_msk_iam_aws_region,
45+
session_token: sasl_aws_msk_iam_session_token,
46+
logger: @logger,
47+
)
48+
3649
@oauth = Sasl::OAuth.new(
3750
token_provider: sasl_oauth_token_provider,
3851
logger: @logger,
3952
)
4053

41-
@mechanism = [@gssapi, @plain, @scram, @oauth].find(&:configured?)
54+
@mechanism = [@gssapi, @plain, @scram, @oauth, @aws_msk_iam].find(&:configured?)
4255
end
4356

4457
def enabled?

spec/sasl_authenticator_spec.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@
4141
sasl_scram_password: nil,
4242
sasl_scram_mechanism: nil,
4343
sasl_oauth_token_provider: nil,
44+
sasl_aws_msk_iam_access_key_id: nil,
45+
sasl_aws_msk_iam_secret_key_id: nil,
46+
sasl_aws_msk_iam_aws_region: nil
4447
}
4548
}
4649

0 commit comments

Comments
 (0)