Let us understand how this works?
An authentication token
is a unique string of characters that Amazon RDS generates on request. Authentication tokens are generated using AWS Signature Version 4. Each token has a lifetime of 15 minutes. You don't need to store user credentials in the database, because authentication is managed externally using IAM. You can also still use standard database authentication.
Since IAM authentication tokens are short-lived access tokens that are valid for 15 minutes. For the RDS database this token works as a database password that is required to establish a connection and does not determine how long the existing connection can last. The default value for connection to be alive without activity is 28800s (8 hours) as explained here.
This means that if you terminate your connection, or MySQL terminates it due to not being actively used, then you need to get a new token to re-connect if the existing one already expired.
Short-lived database credentials are a best practice because they have a limited “blast radius” if compromised. However, Spring Boot — indeed, Java connection pools, in general, aren’t well-adapted to changing credentials, because they’re normally configured when the application starts. To use short-lived credentials, you must have to refresh those credentials before it expires. Follow the below steps to learn how we refresh database credentials in runtime in the spring boot application.
To connect your Spring Boot Application with the AWS RDS database using IAM authentication, the following steps :
- Creating an IAM user with programmatic access.
- Attach an inline policy with the user to access the RDS instance.
- Setup user access key and secret in the application environment.
- Create or modify RDS instance allowing Password and IAM authentication.
- Setup database to use IAM.
- Implement the application.
1. Creating an IAM user with programmatic access.
Assuming that you have an account on AWS and an IAM user, if not then log in to your AWS account and go to the IAM users page using the link https://console.aws.amazon.com/iam/home#/users. Then click on "Add users" and follow the steps shown, it's pretty simple, don't forget to allow Programmatic access as Access type. At finishing it will ask you to download a CSV file that will contain your access key and secret key, Keep it safe we will use it further.
Now the main thing that you need to focus on is that the newly created user has permission to connect rds database?
2. Attach an inline policy with the user to access the RDS instance
For this go to "users" then select your "user_name" and under the "Permissions" tab click on "Add inline policy" as shown in the below image.
After clicking on "Add inline policy" you will have to add the following JSON policy
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "VisualEditor0",
"Effect": "Allow",
"Action": [
"rds-db:connect",
"rds-db:*"
],
"Resource": "*"
}
]
}
Copy the above JSON and paste as shown in the below image.
Next click "Review policy" then in the next step click on "Save changes".
3. Setup user access key and secret in the application environment.
If your application executes in an AWS instance directly then you do not need to do this step. But if you want to test this functionality from another system like from your local machine then this step is required.
Create a text file without an extention named "credentials" and paste this in .aws
folder in home directory. This file could be found with the following path
In Linux: /home/<username>/.aws/credentials
In Windows: C:\users\<username>\.aws\credentials
In Mac: /users/<username>/.aws/credentials
credentials file content:
[default]
aws_access_key_id = AKIAVGOA4***********
aws_secret_access_key = VJEr+DHzzFESsUgp*********************
4. Create or modify RDS instance allowing Password and IAM authentication.
5. Setup database to use IAM.
Log in to your RDS database using your master username and password that you created while creating the RDS instance. You can get the endpoint and port number in the connectivity & security tab from your DB identifier as shown in the below image.
If you are allowed to access the RDS database publically you can log in from any system with your credentials, otherwise, you can only log in from your AWS instance or from the IP addresses that are allowed in inbound/outbound rules of your security groups that are attached to your RDS instance.
For the testing purpose, I have set "Public access" to yes (You can also do this in Step 4).
Connect to your RDS database using the following command:
$mysql --host easytutorials-demords.chgtv6yotna4.ap-south-1.rds.amazonaws.com --port 3306 -u admin -p
In the next step, you will have to enter your master password.
After successful login, run the following queries.
CREATE USER 'lavkush_rdsuser' IDENTIFIED WITH AWSAuthenticationPlugin as 'RDS' REQUIRE SSL;
The username 'lavkush_rdsuser' must be the same as your IAM user's username created in step 1.
GRANT ALL PRIVILEGES ON rdsiamtestdb.* TO 'lavkush_rdsuser'@'%';
Here, rdsiamtestdb
is the database name on which all permissions will be granted to the user.
6. Implement the application
Here is the step-by-step implementation of RDS IAM authentication in spring boot. You will require AWS java SDK for RDS for generating authentication tokens. Others are common.
In this demo application, there are two application profiles i.e default and prod. The default profile is for the local environment and prod is for production. To understand how spring boot profiling works, click this link.
6.1 build.gradle
plugins {
id 'org.springframework.boot' version '2.5.2'
id 'io.spring.dependency-management' version '1.0.11.RELEASE'
id 'java'
}
group = 'live.easytutorials'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '1.8'
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'com.amazonaws:aws-java-sdk-rds:1.11.600'
runtimeOnly 'mysql:mysql-connector-java'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
test {
useJUnitPlatform()
}
6.2 application.properties
Default properties
spring.profiles.active=prod
spring.datasource.driverClassName=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/rdsiamtest?useSSL=false
spring.datasource.username=root
local.database.password=root
cloud.aws.region=ap-south-1
6.3 application-prod.properties
The database endpoint URL and port number are shown in step 5.
spring.datasource.url=jdbc:mysql://easytutorials-demords.chgtv6yotna4.ap-south-1.rds.amazonaws.com:3306/rdsiamtestdb?useSSL=true
spring.datasource.username=lavkush_rdsuser
6.4 GenerateRDSAuthToken.java
package live.easytutorials.rdsiamauthenticationdemo.datasource;
import com.amazonaws.auth.DefaultAWSCredentialsProviderChain;
import com.amazonaws.services.rds.auth.GetIamAuthTokenRequest;
import com.amazonaws.services.rds.auth.RdsIamAuthTokenGenerator;
public class GenerateRDSAuthToken {
private GenerateRDSAuthToken() {
}
public static String generateAuthToken(String jdbcUrl, String username, String region) {
String hostname = getHostname(jdbcUrl);
Integer port = getPort(jdbcUrl);
RdsIamAuthTokenGenerator generator = RdsIamAuthTokenGenerator.builder()
.credentials(new DefaultAWSCredentialsProviderChain())
.region(region)
.build();
return generator.getAuthToken(
GetIamAuthTokenRequest.builder()
.hostname(hostname)
.port(port)
.userName(username)
.build());
}
private static String getHostname(String url) {
String hostname = url.substring(url.indexOf("://") + 3);
hostname = hostname.substring(0, hostname.lastIndexOf(':'));
return hostname;
}
private static Integer getPort(String url) {
String port = url.substring(url.lastIndexOf(':') + 1);
port = port.substring(0, port.lastIndexOf('/'));
return Integer.parseInt(port);
}
}
6.5 RdsDataSource.java
package live.easytutorials.rdsiamauthenticationdemo.datasource;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;
import javax.sql.DataSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
@Configuration
public class RdsDataSource {
private static final long TEN_MINUTES = 10;
private static final Logger logger = LoggerFactory.getLogger(RdsDataSource.class);
@Value("${spring.datasource.url}")
String jdbcUrl;
@Value("${spring.datasource.username}")
String username;
@Value("${cloud.aws.region}")
String regionName;
@Value("${local.database.password}")
String localDatabasePassword;
@Bean
public DataSource dataSource() {
return createDataSource(jdbcUrl, username, regionName);
}
private HikariDataSource createDataSource(String jdbcUrl, String username, String regionName) {
logger.info("Creating custom HikariDataSource.....");
HikariConfig config = new HikariConfig();
config.setJdbcUrl(jdbcUrl);
config.setUsername(username);
config.setPassword(generateAuthToken(jdbcUrl, username, regionName));
HikariDataSource hikariDataSource = new HikariDataSource(config);
// starting a scheduled thread to refresh IAM password
Executors.newSingleThreadScheduledExecutor().scheduleAtFixedRate(
new IamWorker(hikariDataSource, () -> generateAuthToken(jdbcUrl, username, regionName)),
RdsDataSource.TEN_MINUTES,
RdsDataSource.TEN_MINUTES,
TimeUnit.MINUTES
);
return hikariDataSource;
}
private String generateAuthToken(String jdbcUrl, String username, String regionName) {
String authToken = null;
if (jdbcUrl.contains("localhost")) {
authToken = localDatabasePassword;
} else {
authToken = GenerateRDSAuthToken.generateAuthToken(jdbcUrl, username, regionName);
}
System.out.println("authToken = " + authToken);
return authToken;
}
private class IamWorker implements Runnable {
private final Logger logger = LoggerFactory.getLogger(IamWorker.class);
private HikariDataSource hikariDataSource;
private Supplier<String> tokenGenerator;
public IamWorker(HikariDataSource hikariDataSource, Supplier<String> tokenGenerator) {
this.hikariDataSource = hikariDataSource;
this.tokenGenerator = tokenGenerator;
}
@Override
public void run() {
String authToken = tokenGenerator.get();
logger.info("Refreshing IAM credentials...");
System.out.println("New Token :" + authToken);
hikariDataSource.getHikariConfigMXBean().setPassword(authToken);
}
}
}
Output Screen
Thanks for reading this article, I hope RDS IAM Authentication is now easy for you.
Here is the Github link