Skip to content

Commit

Permalink
Initial pass adding AWS IAM Authentication #1263
Browse files Browse the repository at this point in the history
This adds AWS IAM authentication as a replacement for defining a
password in the configuration.

When the configuration option :use_iam_credentials = 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.
  • Loading branch information
matt-domsch-sp committed Nov 10, 2024
1 parent f6a9b68 commit d1e98b6
Show file tree
Hide file tree
Showing 4 changed files with 94 additions and 2 deletions.
30 changes: 28 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -284,8 +284,10 @@ Mysql2::Client.new(
:get_server_public_key = true/false,
:default_file = '/path/to/my.cfg',
:default_group = 'my.cfg section',
:default_auth = 'authentication_windows_client'
:init_command => sql
:default_auth = 'authentication_windows_client',
:init_command => sql,
:use_iam_authentication => true/false,
:host_region,
)
```

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

### AWS IAM Authentication

You may use AWS IAM Authentication instead of setting a password in
the configuration. A temporary token used in place of the password
will be fetched as necessary and used for connections until it
expires. The value for :host_region will either use the one provided,
or if not provided, the environment variable AWS_REGION.

| `:use_iam_authentication` | true |
| --- | --- |
| `:username` | The database username configured to use IAM Authentication |
| `:host` | The database host |
| `:port` | The database port |
| `:host_region` | An AWS region name, e.g. `us-east-1` |

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 IAM user or
role. See
[AWS documentation](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/UsingWithRDS.IAMDBAuth.Connecting.html)
for details on these steps.


### Flags option parsing

The `:flags` parameter accepts an integer, a string, or an array. The integer
Expand Down
60 changes: 60 additions & 0 deletions lib/mysql2/aws_iam_auth.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
require 'singleton'
require 'aws-sdk-rds'

module Mysql2
class AwsTokenAuth
include Singleton

# Tokens are valid for up to 15 minutes.
# We will assume ours expire in 14 minutes to be safe.
TOKEN_EXPIRES_IN = (60*14) # 14 minutes

def initialize
@mutex = Mutex.new
# Key identifies a unique set of authentication parameters
# Value is a Hash
# :password is the token value
# :expires_at is (just before) the token was generated plus 14 minutes
@passwords = {}
@generator = Aws::RDS::AuthTokenGenerator.new
end

def password(user, host port, opts)
params = to_params(user, host, port, opts)
key = key_from_params(params)
passwd = nil
AwsTokenAuth.instance.mutex.synchronize do
begin
if @passwords[key][:password] && Time.now.utc < @passwords[key][:expires_at]
passwd = @passwords[key][:password]
end
rescue KeyError
end
end
if passwd return passwd

AwsTokenAuth.instance.mutex.synchronize do
@passwords[key] = {}
@passwords[key][:expires_at] = Time.now.utc + TOKEN_EXPIRES_IN
@passwords[key][:password] = password_from_iam(params)
end
end

def password_from_iam(params)
@generator.auth_token(params)
end

def to_params(user, host, port, opts)
params = {}
params[:region] = opts[:host_region] || ENV['AWS_REGION']
params[:endpoint] = "#{host}:#{port}"
params[:user_name] = user
params
end

def key_from_params(params)
return "#{params[:user_name]}/#{params[:endpoint]}/#{params[:port]}/#{params[:region]}"
end

end
end
5 changes: 5 additions & 0 deletions lib/mysql2/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,11 @@ def initialize(opts = {})
socket = socket.to_s unless socket.nil?
conn_attrs = parse_connect_attrs(opts[:connect_attrs])

if opts[:use_iam_credentials]
aws = Mysql2::AwsTokenAuth.new
pass = aws.password(user, host, port, opts)
end

connect user, pass, host, port, database, socket, flags, conn_attrs
end

Expand Down
1 change: 1 addition & 0 deletions mysql2.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,5 @@ Mysql2::GEMSPEC = Gem::Specification.new do |s|
s.metadata['msys2_mingw_dependencies'] = 'libmariadbclient'

s.add_runtime_dependency 'bigdecimal'
s.add_runtime_dependency 'aws-sdk-rds'
end

0 comments on commit d1e98b6

Please sign in to comment.