Skip to content

Commit 5874883

Browse files
Initial pass adding AWS IAM Authentication #1263
This adds AWS IAM authentication as a replacement for defining a password in the configuration. When the configuration option :use_iam_authentication = true, an authentication token (password) will be fetched from IAM and cached for the next 14 minutes (tokens expire in 15 minutes). These can then be reused by all new connections until it expires, at which point a new token will be fetched when next needed. To allow for multiple Mysql2::Client configurations to multiple servers, the cache is keyed by database username, host name, port, and region. Two new configuration options are necessary: - :use_iam_credentials = true - :host_region is a string region name, e.g. 'us-east-1'. If not set, ENV['AWS_REGION'] will be used. If this is not present, authenticaiton will fail. As prerequisites, you must enable IAM authentication on the RDS instance, create an IAM policy, attach the policy to the target IAM user or role, create the database user set to use the AWS Authentication Plugin, and then run your ruby code using that user or role. See https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/UsingWithRDS.IAMDBAuth.Connecting.html for details on these steps. You must include the aws-sdk-rds gem in your bundle to use this feature.
1 parent f6a9b68 commit 5874883

File tree

4 files changed

+104
-2
lines changed

4 files changed

+104
-2
lines changed

README.md

+30-2
Original file line numberDiff line numberDiff line change
@@ -284,8 +284,10 @@ Mysql2::Client.new(
284284
:get_server_public_key = true/false,
285285
:default_file = '/path/to/my.cfg',
286286
:default_group = 'my.cfg section',
287-
:default_auth = 'authentication_windows_client'
288-
:init_command => sql
287+
:default_auth = 'authentication_windows_client',
288+
:init_command => sql,
289+
:use_iam_authentication => true/false,
290+
:host_region,
289291
)
290292
```
291293

@@ -348,6 +350,32 @@ When secure_auth is enabled, the server will refuse a connection if the account
348350
The MySQL 5.6.5 client library may also refuse to attempt a connection if provided an older format password.
349351
To bypass this restriction in the client, pass the option `:secure_auth => false` to Mysql2::Client.new().
350352

353+
### AWS IAM Authentication
354+
355+
You may use AWS IAM Authentication instead of setting a password in
356+
the configuration. A temporary token used in place of the password
357+
will be fetched as necessary and used for connections until it
358+
expires. The value for :host_region will either use the one provided,
359+
or if not provided, the environment variable AWS_REGION.
360+
361+
You must add the `aws-sdk-rds` gem to your bundle to use this functionality.
362+
363+
| `:use_iam_authentication` | true |
364+
| --- | --- |
365+
| `:username` | The database username configured to use IAM Authentication |
366+
| `:host` | The database host |
367+
| `:port` | The database port |
368+
| `:host_region` | An AWS region name, e.g. `us-east-1` |
369+
370+
As prerequisites, you must enable IAM authentication on the RDS
371+
instance, create an IAM policy, attach the policy to the target IAM
372+
user or role, create the database user set to use the AWS
373+
Authentication Plugin, and then run your ruby code using that IAM user or
374+
role. See
375+
[AWS documentation](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/UsingWithRDS.IAMDBAuth.Connecting.html)
376+
for details on these steps.
377+
378+
351379
### Flags option parsing
352380

353381
The `:flags` parameter accepts an integer, a string, or an array. The integer

lib/mysql2.rb

+1
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
require 'mysql2/client'
3939
require 'mysql2/field'
4040
require 'mysql2/statement'
41+
require 'mysql2/aws_iam_auth'
4142

4243
# = Mysql2
4344
#

lib/mysql2/aws_iam_auth.rb

+68
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
require 'singleton'
2+
3+
module Mysql2
4+
# Generates and caches AWS IAM Authentication tokens to use in place of MySQL user passwords
5+
class AwsIamAuth
6+
include Singleton
7+
attr_reader :mutex
8+
attr_accessor :passwords
9+
10+
# Tokens are valid for up to 15 minutes.
11+
# We will assume ours expire in 14 minutes to be safe.
12+
TOKEN_EXPIRES_IN = (60 * 14) # 14 minutes
13+
14+
def initialize
15+
begin
16+
require 'aws-sdk-rds'
17+
rescue LoadError
18+
puts "gem aws-sdk-rds was not found. Please add this gem to your bundle to use AWS IAM Authentication."
19+
exit
20+
end
21+
22+
@mutex = Mutex.new
23+
# Key identifies a unique set of authentication parameters
24+
# Value is a Hash
25+
# :password is the token value
26+
# :expires_at is (just before) the token was generated plus 14 minutes
27+
@passwords = {}
28+
instance_credentials = Aws::InstanceProfileCredentials.new
29+
@generator = Aws::RDS::AuthTokenGenerator.new(:credentials => instance_credentials)
30+
end
31+
32+
def password(user, host, port, opts)
33+
params = to_params(user, host, port, opts)
34+
key = key_from_params(params)
35+
passwd = nil
36+
AwsIamAuth.instance.mutex.synchronize do
37+
begin
38+
passwd = @passwords[key][:password] if @passwords.dig(key, :password) && Time.now.utc < @passwords.dig(key, :expires_at)
39+
rescue KeyError
40+
passwd = nil
41+
end
42+
end
43+
return passwd unless passwd.nil?
44+
45+
AwsIamAuth.instance.mutex.synchronize do
46+
@passwords[key] = {}
47+
@passwords[key][:expires_at] = Time.now.utc + TOKEN_EXPIRES_IN
48+
@passwords[key][:password] = password_from_iam(params)
49+
end
50+
end
51+
52+
def password_from_iam(params)
53+
@generator.auth_token(params)
54+
end
55+
56+
def to_params(user, host, port, opts)
57+
params = {}
58+
params[:region] = opts[:host_region] || ENV['AWS_REGION']
59+
params[:endpoint] = "#{host}:#{port}"
60+
params[:user_name] = user
61+
params
62+
end
63+
64+
def key_from_params(params)
65+
"#{params[:user_name]}/#{params[:endpoint]}/#{params[:region]}"
66+
end
67+
end
68+
end

lib/mysql2/client.rb

+5
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,11 @@ def initialize(opts = {})
9494
socket = socket.to_s unless socket.nil?
9595
conn_attrs = parse_connect_attrs(opts[:connect_attrs])
9696

97+
if opts[:use_iam_authentication]
98+
aws = Mysql2::AwsIamAuth.instance
99+
pass = aws.password(user, host, port, opts)
100+
end
101+
97102
connect user, pass, host, port, database, socket, flags, conn_attrs
98103
end
99104

0 commit comments

Comments
 (0)