I am looking to use AWS RDS IAM database authentication with Ruby on Rails, as it allows a convenient way for AWS users to manage database permissions and avoid storing database passwords in their codebases.
At a high level, it works by generating a password based on your AWS credentials to connect to the database that are valid for only 15 minutes. If you want to connect again after 15 minutes, you would need to generate a new password.
This password can be generating using the AWS Ruby SDK easily, and thus can theoretically be embedded in config/database.yml
like so;
production:
adapter: mysql2
host: db_host
database: db_name
username: db_user
password: <%=
Aws::RDS::AuthTokenGenerator
.new(credentials: Aws::InstanceProfileCredentials.new)
.auth_token(
region: 'us-east-1',
endpoint: 'db_host:3306',
user_name: 'db_user'
)
%>
However, as far as I can tell, config/database.yml
is evaluated only once on startup, and remains cached in that state for Rails' lifetime.
Therefore, by using this approach, the Rails server would initially successfully connect to the database, but if at any point after the first 15 minute window Rails tried to open a new DB connection or reconnect a dropped connection, the now-expired credentials would be rejected.
What would be the best way to get IAM database authentication working with Rails? Do I need to somehow have a database configuration with a password that is re-evaluated on each connection establishment?
I did some thinking about a solution to this problem, and the best approach I came up with is monkeypatching Mysql2::Client#initialize
so that you can enable IAM Database Authentication and it will transparently change the password attribute to the RDS password. This seems to work in Rails 5.2.2.1 with mysql
0.5.2.
A key caveat is you can't have the Client's reconnect feature enabled, as we need to make sure Rails recycles the Client whenever a connection error happens (which seems to happen by default in the above Rails version).
# config/database.rb
require 'aws-sdk-rds'
require 'mysql2'
Aws.config.update(
region: 'your_region',
)
class RdsIamPasswordGenerator
def self.generate(region, host, user, port)
Aws::RDS::AuthTokenGenerator
.new(credentials: Aws::InstanceProfileCredentials.new)
.auth_token(
region: region,
endpoint: host.to_s + ':' + port.to_s,
user_name: user
)
end
end
module MysqlClientIamMonkeyPatch
def initialize(opts = {})
opts = opts.dup
aws_iam_auth = opts.delete(:aws_iam_auth)
if aws_iam_auth
raise ArgumentError, 'reconnect must be false if aws_iam_auth is true' if opts[:reconnect]
user = opts[:username] || opts[:user]
host = opts[:host] || opts[:hostname]
port = opts[:port] || 3306
raise ArgumentError, 'username/user and host/hostname must be present' if user.nil? || host.nil?
opts.delete(:pass)
opts.delete(:password)
opts[:password] = RdsIamPasswordGenerator.generate(Aws.config[:region], host, user, port)
opts[:enable_cleartext_plugin] = true # Necessary for IAM auth
end
super(opts)
end
end
Mysql2::Client.prepend(MysqlClientIamMonkeyPatch)
# config/boot.rb
ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__)
require 'bundler/setup' # Set up gems listed in the Gemfile.
require 'bootsnap/setup' # Speed up boot time by caching expensive operations.
require_relative './database' # Handles patching in IAM auth
# config/database.yml
production:
adapter: mysql2
database: production
ssl_mode: verify_identity
sslverify: true
sslca: /opt/aws/rds-combined-ca-bundle.pem
aws_iam_auth: true
host: db_host
username: db_user
password: null
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With