Search code examples
ruby-on-railsamazon-web-servicesamazon-rdsamazon-iam

AWS RDS - IAM Database Authentication with Rails


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?


Solution

  • 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