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_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.
  • Loading branch information
matt-domsch-sp committed Nov 11, 2024
1 parent f6a9b68 commit 5874883
Show file tree
Hide file tree
Showing 4 changed files with 104 additions and 2 deletions.
32 changes: 30 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,32 @@ 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.

You must add the `aws-sdk-rds` gem to your bundle to use this functionality.

| `: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
1 change: 1 addition & 0 deletions lib/mysql2.rb
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
require 'mysql2/client'
require 'mysql2/field'
require 'mysql2/statement'
require 'mysql2/aws_iam_auth'

# = Mysql2
#
Expand Down
68 changes: 68 additions & 0 deletions lib/mysql2/aws_iam_auth.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
require 'singleton'

module Mysql2
# Generates and caches AWS IAM Authentication tokens to use in place of MySQL user passwords
class AwsIamAuth
include Singleton
attr_reader :mutex
attr_accessor :passwords

# 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
begin
require 'aws-sdk-rds'
rescue LoadError
puts "gem aws-sdk-rds was not found. Please add this gem to your bundle to use AWS IAM Authentication."
exit
end

@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 = {}
instance_credentials = Aws::InstanceProfileCredentials.new
@generator = Aws::RDS::AuthTokenGenerator.new(:credentials => instance_credentials)
end

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

AwsIamAuth.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)
"#{params[:user_name]}/#{params[:endpoint]}/#{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_authentication]
aws = Mysql2::AwsIamAuth.instance
pass = aws.password(user, host, port, opts)
end

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

Expand Down

0 comments on commit 5874883

Please sign in to comment.