exanubes
Q&A

Connecting to RDS Postgres using IAM Authentication

Using a username and password in application’s environment variables may pose a risk. RDS IAM Authentication resolves this by enabling authentication with IAM roles or users. This eliminates the need to store credentials in the application and simplifies permission management, utilizing the same IAM Policy for various AWS services. Additionally, IAM tokens have a short lifespan (15 minutes), enhancing security compared to indefinitely valid passwords.

Even though the IAM token is valid for only 15 minutes that does not mean the established database connection closes after that time.

You can find finished code for this article on Github and I’ve also recorded a video that covers more implementation details

TOC

Create a VPC

To keep this article all about the RDS IAM Authentication, I chose to forgo deploying the application to an EC2 instance to keep this brief and on topic. Instead, I will make a public database instance and connect to it from my local machine.

const vpc = new Vpc(this, `vpc`, {
	natGateways: 0
});

Creating an IAM Authentication enabled RDS Postgres instance

With cdk, enabling IAM Authentication could not be any easier. Simply add the iamAuthentication property to the configuration.

const rds = new DatabaseInstance(this, `rds-instance`, {
	vpc,
	vpcSubnets: {
		subnets: vpc.publicSubnets
	},
	publiclyAccessible: true,
	databaseName: 'exanubes',
	iamAuthentication: true,
	credentials: Credentials.fromGeneratedSecret('postgres'),
	engine: DatabaseInstanceEngine.POSTGRES,
	instanceType: InstanceType.of(InstanceClass.T3, InstanceSize.MICRO),
	removalPolicy: RemovalPolicy.DESTROY
});

Here, I am using the previously created vpc and specifying that the database should be publicly accessible so that I can access it from my local environment. I am also using the Credentials construct to generate a username and password for the database. I updated the removal policy to DESTROY to avoid creating a snapshot when destroying the database as that will incur storage charges. Last but not least, I’ve enabled IAM Authentication by setting the iamAuthentication property to true.

Database is made public for demonstration purposes, it goes without saying that doing this in production is not a good idea

Database security group

Before moving on, I need to configure the security group for the database so that it allows inbound traffic from my local machine.

rds.connections.allowFrom(Peer.anyIpv4(), Port.tcp(rds.instanceEndpoint.port), 'Public connection');

IAM Policy for RDS

Whether using a role or a user, the IAM policy for RDS is the same. We need to grant access to the database instance for a particular database user.

Creating the resource ARN

The first step is to create the resource ARN for the database user.

const arn = this.formatArn({
	service: 'rds-db',
	resource: 'dbuser:' + rds.instanceIdentifier,
	resourceName: 'admin'
});

This will create a resource arn that we want to grant access to. It will end up looking something like arn:aws:rds-db:eu-central-1:123456789012:dbuser:rds-instance-id/admin. First half of this arn as always defines the service, ownership and region. The second half says that the resource we want to access is dbuser on the database with id rds-instance-id and the user is admin.

Attaching policy to IAM user

Now that we have the resource arn, we can create the policy and attach it to the user or role. I’m using an IAM User for this example for ease of use with my local environment.

const user = User.fromUserName(this, 'admin-user', 'admin');

user.attachInlinePolicy(
	new Policy(this, 'rds-access-policy', {
		statements: [
			new PolicyStatement({
				effect: Effect.ALLOW,
				actions: ['rds-db:connect'],
				resources: [arn]
			})
		]
	})
);

This attaches a policy that will allow the user to connect to the database resource that we created in the previous step.

Deploying the stack

At this point, we are done with the infrastructure and need to deploy the stack to move forward.

If you’re using my repository, you can deploy the stack with the following commands:

npm run synth && npm run deploy

Adding admin user to the database

Now that we have the database up and running, we need to create the admin user in the database. To do that we need to connect to the database with the credentials we generated when creating the database.

You can find the credentials in the AWS Secrets Manager console in the same region in which you created the database.

Connecting to the database

I’m using the psql command line tool to connect to the database, if you’d rather use a database explorer tool like DataGrip , the sql command will work the same

psql -h DB_HOST -p 5432 -U postgres -d exanubes

The -h flag specifies the host of the database, -p specifies the port, -U specifies the user and -d specifies the database to connect to. you can find all these values in the AWS Secrets Manager. Once you run the command, you will be prompted for the password. Enter the password from the AWS Secrets Manager.

Creating the admin user

First, we need to create the admin user in the database and then grant him the role of rds_iam to allow for IAM Authentication.

CREATE USER admin;
GRANT rds_iam TO admin;

That’s it, we are done with the database setup. To exit the psql shell, type exit.

You might be tempted to assign rds_iam role to the postgres user, but keep in mind that it will make the current password invalid as it's not a valid IAM Token which is now required to authenticate

Connecting to the database with IAM Authentication from the CLI

First I want to prepare some variables that I will use to connect to the database to avoid having extremely long strings in the command.

Preparing an IAM Token

To start with, I’m going to create a variable for the database host

export RDSHOST="RDS_HOST_FROM_AWS_SECRETS_MANAGER"

Next, I’m gonna use the host variable to generate an IAM Token and put it in a separate variable

export IAMTOKEN=$(aws rds generate-db-auth-token --hostname $RDSHOST --port 5432 --region REGION --username USERNAME)

This will run the command between the parentheses and put the output in the variable. You can find all relevant data in your AWS Secrets Manager, the username is the name of the database user you’ve created in the previous step.

Connecting to the database

To connect to the database we’re gonna use the psql command again, but this time we’re gonna use the IAM Token as the password for admin user.

psql -h $RDSHOST -p 5432 "dbname=exanubes user=admin password=$IAMTOKEN"

Programmatic access to the database

To access the database programmatically, we need to retrace the steps we did in the CLI. To do this, you will need the @aws-sdk/rds-signer module and a postgres client, I chose postgres.js .

Creating a signer instance

This is pretty much a one-to-one translation of the CLI command we used when creating the IAMTOKEN variable. We’re defining a hostname, port and username. The region is passed implicitly from my aws configuration.

const { Signer } = require('@aws-sdk/rds-signer');
const signer = new Signer({
	hostname: host,
	port,
	username
});

Creating a database connection

This is also a very similar situation to the CLI command we used to authenticate to the database.

const postgres = require('postgres');

const client = postgres({
	host,
	port,
	username,
	db,
	ssl: 'allow',
	password: async () => signer.getAuthToken()
});

We need to provide the usual connection parameters, but instead of a password, we’re providing a function that uses the Signer instance to generate an IAM Token.

Permission denied for database

If you’re getting a permission denied for database error, at the time of writing the only fix for this I’ve found was to assign the rds_superuser role to the user. This is not ideal, but it’s the only way I’ve found to make it work. Once I find a better solution, I will update this article.

GRANT rds_superuser TO admin;

Summary

In this article, we’ve learned how to enable IAM Authentication for an RDS Postgres instance, setup relevant IAM permissions and prepare a database user for IAM Authentication. We also covered connecting to the database using IAM Authentication both from the CLI as well as programmatically from application code.