RDS Database Authentication with Spring Boot: Part 2, IAM Authentication

by
Tags: , ,
Category:

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 retrieve those credentials whenever you make a database connection.

Yesterday I looked at one way to achieve this, using a database driver from AWSLabs. Today I’m going to dive a little deeper, and show you how to customize a standard JDBC DataSource. This requires a little more work to keep Spring Boot happy, but gives you the flexibility to retrieve your credentials from anywhere, not just Secrets Manager. As before, I’ve created an example that you can build and run.

For my credential source, I’m using IAM database authentication. which creates credentials that are only valid for 15 minutes. This feature is available for MySQL and Postgres on RDS and Aurora, with some restrictions on supported versions. To use, you must enable the feature on your RDS instance, enable it for individual database users, and then grant the rds-db:connect IAM permission to the application.

With that in place, it’s time to create a custom DataSource that retrieves the password for each connection. This isn’t difficult: subclass the standard datasource for your DBMS and overrides the getConnection() method. My example is based on the Postgres implementation, PGSimpleDataSource, but the same idea (and all of the code other than the calls to setProperty()) applies to MysqlDataSource.

public class IAMAuthDataSource
extends PGSimpleDataSource
{
    private final static long serialVersionUID = 1L;
    
    private Logger logger = LoggerFactory.getLogger(getClass());

    @Override
    public Connection getConnection(String user, String password)
    throws SQLException
    {
        // I'd like to do this in constructor, but it can throw SQLException
        setProperty("ssl", "true");
        setProperty("sslmode", "require");
        
        logger.debug("requesting IAM token for user {}", user);

        // adapted from https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/UsingWithRDS.IAMDBAuth.Connecting.Java.html
        RdsIamAuthTokenGenerator generator = RdsIamAuthTokenGenerator.builder()
            .credentials(new DefaultAWSCredentialsProviderChain())
            .region((new DefaultAwsRegionProviderChain()).getRegion())
            .build();

        GetIamAuthTokenRequest request = GetIamAuthTokenRequest.builder()
            .hostname(getServerName())
            .port(getPortNumber())
            .userName(user)
            .build();

        String authToken = generator.getAuthToken(request);

        return super.getConnection(user, authToken);
    }
}

If you want more information on the process of retrieving an authentication token, the AWS docs are a good resource. Where I want to focus is on making it work with Spring Boot, which is surprisingly challenging.

In a “traditional” Spring application, you explicitly define all of the beans it uses, including the connection pool and underlying datasource. Spring Boot, however, takes that out of your hands: it decides what beans to create based on the contents of application.properties. It you configure a database URL, for example, Spring Boot creates a connection pool that uses that URL to establish connections. If you don’t specify a URL, it assumes you want to use an “embedded” database (typically for testing). You can configure a custom datasource, but that leads you down a path where you have to define most of your data access layer explicitly.

What I discovered after some experimentation is that you provide a factory method to create Spring Boot’s default datasource bean, spring.datasource:

@SpringBootApplication
public class Application
{
    public static void main(String[] args)
    {	
        SpringApplication.run(Application.class, args);
    }
	

    @Bean
    @ConfigurationProperties(prefix = "spring.datasource")
    public HikariDataSource dataSource()
    {
        HikariDataSource ds = DataSourceBuilder.create()
                              .type(HikariDataSource.class)
                              .build();
        return ds;
    }
}

Before going any further, I think I need to spend some time talking about the word “datasource.” When JDBC was first released, implementations provided a “driver”: a class that implemented java.sql.Driver. It didn’t take long for developers to create connection pools, to minimize the time wasted when establishing a new connection. However, the Driver interface exposed functions that weren’t relevant for connection pools. So JDBC 3.0, introduced with Java 1.4 in 2002, included the javax.sql.DataSource interface, which was an abstraction that covered both drivers and connection pools. And that was great if you’re an application developer: as long as something gives you a connection, you don’t have to think about what it is. But to avoid confusion, in the rest of this post I’m going to differentiate between “datasource” and “connection pool”.

So, with that out of the way, by explicitly telling Spring to use a particular connection pool implementation, we can move configure that pool for our custom datasource:

spring.datasource.dataSourceClassName=com.chariotsolutions.example.springboot.datasource.IAMAuthDataSource
spring.datasource.dataSourceProperties.serverName=${PGHOST}
spring.datasource.dataSourceProperties.portNumber=${PGPORT}
spring.datasource.dataSourceProperties.user=${PGUSER}
spring.datasource.dataSourceProperties.databaseName=${PGDATABASE}

The first property tells the pool to use my custom datasource. Note that I can’t use a connection URL to do this, as the URL implies the datasource implementation (well, I could, if I wanted to implement the JDBC service provider interface, but that’s a blog post by itself).

The following properties, all of which start with dataSourceProperties, are the way that the Hikari connection pool configures the underlying datasource. Each of the values corresponds to a settable property of the PGSimpleDataSource implementation class.

So, to summarize, if you want to use connection-time passwords with Spring Boot, you need to do the following:

  1. Create a DataSource class based on a DBMS-specific implementation, and override the getConnection() method.
  2. Add a factory method to your Spring application that will explicitly create a connection pool bean named “spring.datasource”.
  3. Configure both connection pool and underlying datasource in application.properties.

You can use this same technique to retrieve data from AWS Parameter Store, or AWS Secrets Manager, or a third-party secrets store like HashiCorp Vault.