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.
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
- Creating an IAM Authentication enabled RDS Postgres instance
- IAM Policy for RDS
- Deploying the stack
- Adding admin user to the database
- Connecting to the database with IAM Authentication from the CLI
- Programmatic access to the database
- Summary
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 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.
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
.
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 authenticateConnecting 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.