AWS RDS - IAM Database Authentication with Rails
Asked Answered
D

2

11

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?

Disorganize answered 26/11, 2017 at 23:48 Comment(2)
This article (and parts 1/2) may be helpful: aws.amazon.com/blogs/developer/…Volumetric
@Volumetric Unfortunately the crux of the issue is that I believe Rails evaluates the dynamic database config once per session, so any approach needs to get around that issue. For instance, if I could provide a lambda instead of a string for the password entry, that would work, but I'm not sure that is possible.Disorganize
D
6

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
Disorganize answered 11/11, 2019 at 0:46 Comment(0)
B
0

In case anyone else is trying to solve this, I incorporated RDS IAM support into a gem I'm building using a custom database config handler. Feel free to try out the gem or check out the code for how I solved it. Feedback welcome.

Bracer answered 2/2 at 21:59 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.