AWS RDS IAM Authentication with Terraform
Asked Answered
R

4

15

I am trying to set up my infrastructure properly with no passwords or keys laying around. AWS RDS has an option to do so, by enabling users(applications) to authenticate with generated tokens.

However, in the documentation, one of the steps(this one) requires running a query in the Postgres database to create an user and grant him specific permissions:

CREATE USER test_rds WITH LOGIN;
GRANT rds_iam TO test_rds;

I want to provision entire stack with Terraform. I have looked up some "hacks" to run the query (here) after RDS instantiation by either using:

resource "null_resource" "db_setup" {
  depends_on = ["aws_db_instance.your_database_instance",   "aws_security_group.sg_allowing_external_access"]

    provisioner "local-exec" {
 // run shell commands to manually psql into the db

or:

resource "aws_instance" "web" {


  provisioner "remote-exec" {
    inline = [
   // run shell commands to manually psql into the db

but both of them require creating master-password and somehow delivering it inside the "scripts".

Is it possible to do that with Terraform cleanly, with no hardcoded passwords getting passed around?

I would love to provision the database and enable only specific EC2/ECS instances with correct permissions to access it, without any passwords in my git repository.

Reproval answered 24/4, 2019 at 16:5 Comment(0)
O
9

Once you enable IAM authentication for an RDS database user/role, you are no longer able to use password based authentication for that user/role.

This means you can either use a less secure password or even just generate a random password (using the random_id resource) that you use to set the master password and first use to authenticate so that you can grant the rds_iam permissions to the master user and any other users you create.

While this password will end up in the state file (even if randomly generated), as mentioned, once the rds_iam grant has been applied then you won't be able to use this password to login to your database.

Outshine answered 24/4, 2019 at 16:19 Comment(6)
Isn't the master password still active for others though? I was able to run required query (from my own workstation) to enable the IAM-based auth and that did not invalidate the credentials. I was still able to access the database using master password.Reproval
You'd need to add the grant to the user you are logging in as as well. I can't find it documented anywhere but I was confused when I enabled it on my own RDS databases that I was no longer able to log in with username/password credentials after applying the grant and AWS support confirmed that this was the case once the rds_iam privileges are given to a user.Outshine
That sounds much more reasonable now, thank you very much! I will try to implement it tomorrow and accept your answer afterwards. What Terraform "method" would you recommend to execute the query?Reproval
You should be able to use a local-exec provisioner on the aws_db_instance resource itself so it will only be ran on RDS instance creation. The postgresql provider is also useful but has some issues with RDS right now so I'm maintaining a fork of it until things can be fixed.Outshine
It worked seamlessly! Thank you very much @ydaetskcoR. Could you tweak your answer and specify that one should grant the rds_iam role to the "master user" to automatically invalidate the master password? That should be helpful for people in the future stumbling with the same issue.Reproval
Edited it now, hope that's clear enough? I'd much prefer to link to some documentation but I can't find anything that expressly says that this behaviour happens although it makes sense when you think about what is happening under the covers (RDS is rotating the user's password every 15 minutes and then vends that password when requested after passing IAM auth.Outshine
M
4

To provision an RDS database user ready for IAM authentication, the following terraform configuration can be added:

resource "postgresql_role" "db_user" {
  name  = "db_userx"
  login = true
  roles = ["rds_iam"]
}

The code above makes use of the cyrilgdn/postgresql provider. If you configure the provider using your aws_db_instance properties for its connection parameters as shown below, then the correct order of dependencies is enforced.

provider "postgresql" {
  host            = aws_db_instance.web.address
  port            = aws_db_instance.web.port
  database        = "postgres"
  username        = aws_db_instance.web.username
  password        = aws_db_instance.web.password
  sslmode         = "require"
  connect_timeout = 15
  superuser       = false # postgres user is not a true superuser in RDS
}

The RDS User Guide now includes the following:

For PostgreSQL, if the IAM role (rds_iam) is added to the master user, IAM authentication takes precedence over Password authentication so the master user has to log in as an IAM user

Milena answered 9/12, 2021 at 20:53 Comment(0)
V
1

There are several components involved here that lack proper support for what you're asking, namely :

  • RDS does not provide a way to create an instance direcly with the rds_iam role assigned to the master user
  • cyrilgdn/postgresql does not support IAM database authentication

We can however work around both.

Requirements

I'll be assuming that the machine running terraform has :

  • Network connectivity to the RDS instance
  • A psql cli client installed
  • jq installed
  • The AWS cli (v2) installed
  • An AWS identity configured, with enough permissions to :
    • create a RDS instance
    • authenticate with RDS IAM

How exactly you achieve the network connectivity (exposing the instance to the internet, running terraform on an ec2, a proxy...) and the AWS identity (access keys, AWS SSO, default role on an ec2...) is up to you and your context.

I'm personally using an AWS cli named profile with AWS SSO, so you'll be seiing --profile everywhere, feel free to remove them.

The configuration

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 4.58"
    }
    postgresql = {
      source  = "cyrilgdn/postgresql"
      version = "~> 1.20"
    }
  }
  required_version = ">= 0.14.9"
}

# Feel free to change the AWS auth config to match your own
provider "aws" {
  region  = "<some_region>"
  profile = "my-aws-profile"
  assume_role {
    role_arn = "<some_role>"
    external_id = "<some_external_id>"
  }
  allowed_account_ids = ["<some_account_id>"]
}

# Instead of using passwords, we use IAM database authentication whenever possible :
# https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/UsingWithRDS.IAMDBAuth.html
# Users set to use it will not have a long-lived password, and will instead derive a temporary
# password from their AWS IAM identity.
# When this is enabled on an instance, any given pg role can only connect through one
# method ; either password or IAM ; depending on whether it has the rds_iam role.
#
# AWS does not provide a way to create an instance where the master user, created with
# the instance, has this role. So we initially create it with a password, and manually connect
# with a psql client immediately after creating the RDS instance, to switch it to RDS IAM auth.
# Once this is done, we can connect normally (ie through IAM) with the pg provider
resource "random_password" "rds_bootstrap_master_password" {
  length  = 30
  upper   = true
  lower   = true
  numeric = true
  special = false
}

# Now let's create the RDS instance itself with this password
resource "aws_db_instance" "app" {
  username = "terraform"
  password = random_password.rds_bootstrap_master_password.result
  iam_database_authentication_enabled = true

  # Put the rest of your configuration here, there are many more required parameters

  # Once the RDS instance is created, manually switch the terraform user to
  # IAM database authentication. We also delete its password, although technically
  # it isn't really required, as enabling IAM db auth disables it.
  # You can get the CA file with :
  # `curl -o rds-ca-2019.pem https://truststore.pki.rds.amazonaws.com/global/global-bundle.pem`
  provisioner "local-exec" {
    interpreter = ["bash", "-c"]
    command     = <<EOF
      psql "
        host=${self.address}
        port=${self.port}
        sslmode=verify-full
        sslrootcert=rds-ca-2019.pem
        dbname=postgres
        user=${self.username}
        password=${self.password}
      " --command="
        GRANT rds_iam TO ${self.username};
        ALTER ROLE ${self.username} WITH PASSWORD NULL;
      "
    EOF
  }
}


# Since the postgresql provider does not natively support deriving short-lived postgresql creds from
# an AWS identity, we use the AWS cli to do it
data "external" "rds_auth_token" {
  program = [
    "bash", "-c", replace(
      <<-EOF
        aws rds generate-db-auth-token
          --hostname  ${aws_db_instance.app.address}
          --port      ${aws_db_instance.app.port}
          --region    ${replace(aws_db_instance.app.availability_zone, "/[[:lower:]]$/", "")}
          --username  ${aws_db_instance.app.username}
          --profile   my-aws-profile
        | jq --raw-input '{ password: . }'
      EOF
    , "\n", " ")
  ]
}

# Please note that for this provider to work, you need direct network access to the RDS instance
provider "postgresql" {
  scheme    = "awspostgres"
  host      = aws_db_instance.app.address
  port      = aws_db_instance.app.port
  sslmode   = "verify-full" # NB : the "awspostgres" scheme takes care of finding the CA by itself
  username  = aws_db_instance.app.username
  password  = data.external.rds_auth_token.result.password
  superuser = false
}

# This is a dummy user to test your configuration. Create whatever you need with the postgres provider here
resource "postgresql_role" "dummy" {
  name  = "tf_dummy_role"
  login = false
  roles = ["rds_iam"]
}

Explanation

In both cases, we work around the lack of first-party support by using the AWS cli.

  • For the master user, we only need a single initial connection, to run a single SQL query. A local-exec is exactly what we're looking for here, as it does not add anything into the terraform dependency graph, while doing exactly what we need (one-time local command immediately after resource creation)
  • For every subsequent run, we need to generate a short-lived access token, to use in the postgresql provider. The external provider is literally made for that. It does require json output though ; fortunately, we can get away with some jq-fu, which saves us from having to write a separate wrapper script.

This configuration should allow you to entirely bootstrap a RDS instance, with no long-lived secrets of any kind !

I hope it helps.

Vindicable answered 31/7, 2023 at 16:50 Comment(0)
V
1

To complete my previous answer, the postgresql provider actually has native support for RDS IAM authentication. We can therefore simplify this part, and only keep the provisionner for initial setup :

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 4.58"
    }
    postgresql = {
      source  = "cyrilgdn/postgresql"
      version = "~> 1.20"
    }
  }
  required_version = ">= 0.14.9"
}

provider "aws" {
  # whatever auth method you prefer
}

# Instead of using passwords, we use IAM database authentication whenever possible :
# https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/UsingWithRDS.IAMDBAuth.html
# Users set to use it will not have a long-lived password, and will instead derive a temporary
# password from their AWS IAM identity.
# When this is enabled on an instance, any given pg role can only connect through one
# method ; either password or IAM ; depending on whether it has the rds_iam role.
#
# AWS does not provide a way to create an instance where the master user, created with
# the instance, has this role. So we initially create it with a password, and manually connect
# with a psql client immediately after creating the RDS instance, to switch it to RDS IAM auth.
# Once this is done, we can connect normally (ie through IAM) with the pg provider
resource "random_password" "rds_bootstrap_master_password" {
  length  = 30
  upper   = true
  lower   = true
  numeric = true
  special = false
}

# Now let's create the RDS instance itself with this password
resource "aws_db_instance" "app" {
  username = "terraform"
  password = random_password.rds_bootstrap_master_password.result
  iam_database_authentication_enabled = true

  # Put the rest of your configuration here, there are many more required parameters

  # Once the RDS instance is created, manually switch the terraform user to
  # IAM database authentication. We also delete its password, although technically
  # it isn't really required, as enabling IAM db auth disables it.
  # You can get the CA file with :
  # `curl -o rds-ca-2019.pem https://truststore.pki.rds.amazonaws.com/global/global-bundle.pem`
  provisioner "local-exec" {
    interpreter = ["bash", "-c"]
    command     = <<EOF
      psql "
        host=${self.address}
        port=${self.port}
        sslmode=verify-full
        sslrootcert=rds-ca-2019.pem
        dbname=postgres
        user=${self.username}
        password=${self.password}
      " --command="
        GRANT rds_iam TO ${self.username};
        ALTER ROLE ${self.username} WITH PASSWORD NULL;
      "
    EOF
  }
}

# Please note that for this provider to work, you need direct network access to the RDS instance
provider "postgresql" {
  scheme    = "awspostgres"
  host      = aws_db_instance.app.address
  port      = aws_db_instance.app.port
  sslmode   = "verify-full" # NB : the "awspostgres" scheme takes care of finding the CA by itself
  username  = aws_db_instance.app.username
  password  = data.external.rds_auth_token.result.password
  superuser = false
}

# This is a dummy user to test your configuration. Create whatever you need with the postgres provider here
resource "postgresql_role" "dummy" {
  name  = "tf_dummy_role"
  login = false
  roles = ["rds_iam"]
}
Vindicable answered 1/8, 2023 at 14:40 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.