DEV Community

Cover image for Securing AWS RDS Connections & Updating SSL/TLS Certificates on Client Applications
srikanth597
srikanth597

Posted on

Securing AWS RDS Connections & Updating SSL/TLS Certificates on Client Applications

Did you know AWS RDS 2019 CA is going to expire in August 2024, Let's find out how it's gonna impact you, what should be done & some important info on the right usage with examples.

Introduction:

AWS RDS (Relational Database Service) is a popular choice for businesses looking to streamline their data management needs, allowing them to scale and manage their databases easily. However, with the increasing number of cyber attacks, secure communication and data exchange have become of utmost importance. The SSL/TLS certificates play a crucial role in encrypting data, thus managing a secure connection between the client machine and the RDS Database. As the rds-ca-2019 certificate approaches its expiration in Aug 2024, it’s vital to renew it to ensure that data transmission between AWS RDS and client applications remains secure & avoid connectivity failures on the SSL Port after Aug 2024.


Understanding SSL/TLS certificates for Databases:

The SSL/TLS protocol ensures that data transmitted over a network is secure by encrypting it. There are two important things related to SSL/TLS certificates that users must understand when dealing with AWS RDS - authentication and encryption. The SSL/TLS certificates verify the authenticity of the server, thus ensuring clients connect to the right server, and also encrypt the data to protect it from unauthorized access. AWS RDS credentials, including SSL/TLS certificates, expire periodically, causing applications to stop working correctly or, worse, to be insecure.

As per AWS recommendations, if your applications connect to an RDS instance using the SSL/TLS protocol, you need to renew the SSL/TLS certificate before August 2024, to prevent any connectivity issues. Renewing SSL/TLS certificates is a crucial process of maintaining a secure connection between your client applications and RDS instances as it verifies the authenticity of the server and encrypts data transmissions. Check here if you want to know how SSL/TLS works

Certs

To renew the SSL/TLS certificate, on a high level you should follow the steps:

  • Update your client applications with the new SSL/TLS certificate. You can download the CA certificate from the RDS dashboard, and then update the trust stores on your client applications. This step is essential because the client application needs to trust the new CA to establish a secure connection.

  • Update the SSL/TLS certificate on all affected database instances to the new ones that were issued. The current recommendation is to use rds-ca-rsa2048-g1 since it does not involve any algorithm changes. However, other CAs with new key algorithms are available, but it may take more time to test whether they work with your client setup.

NOTE: It's important to note that make your client changes first before any change on the RDS Database to provide backward compatibility & support the application.


Performing changes at client applications:

Note: I am Considering the Java TechStack examples it will be mostly similar in other languages

RDS Database: Postgres & Oracle
Client Application: Java + Springboot
Environment: Virtual machine & docker

Load all Root CA certificates to Java TrustStore:

There's a Root CA Certificate Bundle provided by AWS (Root Cert Bundle) which needs to be available on the client side to establish the SSL/TLS Session, however, out of the root bundle which contains all Region Certs, you can load only regions you are applications needs. For example, I am using us-west-2 & us-east-2 certificates as part of the trust store.

Script to load the certs into the custom trust store:

Create a trust store called clientkeystore.jks & load all available certs of the us-west-2 & us-east-2 region which includes rds-ca-2019, rds-ca-rsa2048-g1, rds-ca-rsa4096-g1 and rds-ca-ecc384-g1.

#!/bin/bash

#### filename: import_certs.sh

mydir=/usr/certs
if [ ! -e "${mydir}" ]
then
mkdir -p "${mydir}"
fi
truststore=${mydir}/clientkeystore.jks
storepassword=strongpassword

curl -sS "https://truststore.pki.rds.amazonaws.com/global/global-bundle.pem" > ${mydir}/global-bundle.pem
awk 'split_after == 1 {n++;split_after=0} /-----END CERTIFICATE-----/ {split_after=1}{print > "rds-ca-" n ".pem"}' < ${mydir}/global-bundle.pem

for CERT in rds-ca-*; do
  alias=$(openssl x509 -noout -text -in $CERT | perl -ne 'next unless /Subject:/; s/.*(CN=|CN = )//; print')
  if echo $alias | grep -q "us-west-2\|us-east-2"; then
    echo "Importing $alias"
    keytool -import -file ${CERT} -alias "${alias}" -storepass ${storepassword} -keystore ${truststore} -noprompt
  fi
  rm $CERT
done

rm ${mydir}/global-bundle.pem

echo "Trust store content is: "

keytool -list -v -keystore "$truststore" -storepass ${storepassword} | grep Alias | cut -d " " -f3- | while read alias
do
   expiry=`keytool -list -v -keystore "$truststore" -storepass ${storepassword} -alias "${alias}" | grep Valid | perl -ne 'if(/until: (.*?)\n/) { print "$1\n"; }'`
   echo " Certificate ${alias} expires in '$expiry'"
done

Enter fullscreen mode Exit fullscreen mode

NOTE: We are loading these certs into the Custom Trust store, & NOT loading into the Java Default trust store i.e ${JAVA_HOME}/jre/lib/security/cacerts. This is because in some cases will not be allowed to have write access into this System Trust Store in Certain Environments where Java is installed under the root user you may not have access to the changes to the default trust store, if not you can feel free to point the trust store location to default trust store.

Docker Instructions ( Optional if not using the dockerized environment) :

FROM public.ecr.aws/amazoncorretto/amazoncorretto:17
........
COPY import_certs.sh /usr
RUN chmod +x /usr/import_certs.sh
# Import RDS certs for both west & east
RUN /usr/import_certs.sh
RUN chmod 644 /usr/certs/clientkeystore.jks
.........
.........
Enter fullscreen mode Exit fullscreen mode

DataSource Changes ( For SpringBoot based applications):

    @Bean("pgdataSource")
    public DataSource getDataSource() {
        DataSource postgresDataSource = null;
        try{
            LOGGER.debug("Configuring the dataSource with the configuration :: " +
                    "driver={}, url={}, userName={}, timeout={}, minIdle={} & maxPoolSize={}", dbDriver, dbUrl, dbUsername, connectionTimeout, minimumIdle, maximumPoolSize);
            HikariConfig config = new HikariConfig();
            config.setConnectionTimeout(connectionTimeout);
            config.setMinimumIdle(minimumIdle);
            config.setMaximumPoolSize(maximumPoolSize);
            config.setDriverClassName(dbDriver);
            config.setJdbcUrl(dbUrl);
            LOGGER.info("Postgres SSL Protocol isSSLEnabled : {}", isSSLEnabled);
            if(isSSLEnabled) {
                LOGGER.debug("Setting Truststore & root certificate");
                config.addDataSourceProperty(JAVAX_TRUST_STORE_TYPE_KEY, keyStoreType);
                config.addDataSourceProperty(JAVAX_TRUST_STORE_PATH_KEY, keyStoreFilePath);
                config.addDataSourceProperty(JAVAX_TRUST_STORE_PASSWORD_KEY, keyStoreFilePassword);
            }
            config.setUsername(dbUsername);
            config.setPassword(dbPassword);
            postgresDataSource = new HikariDataSource(config);
        }catch (Exception ex){
            LOGGER.error("exception while connecting to postgres database {}",ex);
            ex.printStackTrace();
        }
        return postgresDataSource;
    }

Enter fullscreen mode Exit fullscreen mode
  • JAVAX_TRUST_STORE_TYPE_KEY : JKS
  • JAVAX_TRUST_STORE_PATH_KEY : /usr/certs/clientkeystore.jks
  • JAVAX_TRUST_STORE_PASSWORD_KEY : strongpassword
  • The remaining should be DB user,password, URL etc pass it as per ur configurations.

Plain Java Changes(If springboot not used, follow similar style to this) :

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;

public class PostgresConnection {
    public static void main(String[] args) {
        String dbURL = "jdbc:postgresql://mydb.123456789012.us-east-1.rds.amazonaws.com:5432/myrds?useSSL=true&sslmode=verify-ca&sslfactory=org.postgresql.ssl.DefaultJavaSSLFactory";
        String username = "dbUser";
        String password = "******";

        try {
            // Class.forName("org.postgresql.Driver");
            Connection conn = DriverManager.getConnection(dbURL, username, password);
            if (conn != null) {
                System.out.println("Connected to PostgreSQL database");
            }
        } catch (SQLException ex) {
            System.out.println("Error: " + ex.getMessage());
        }
    }

}

Enter fullscreen mode Exit fullscreen mode

You can Execute this way:

If Custom Truststore is used:

java -cp /Users/drivers/postgresql-42.7.1.jar:./ -Djavax.net.ssl.trustStore=/usr/clientkeystore.jks -Djavax.net.ssl.trustStorePassword=strongpassword PostgresConnection
Enter fullscreen mode Exit fullscreen mode

If Default Java TrustStore used (${JAVA_HOME}/jre/lib/security/cacerts) :

java -cp /Users/drivers/postgresql-42.7.1.jar:./ PostgresConnection

PS: download the postgres jdbc driver here

OracleDriver example:

If you are using the Oracle Driver & RDS Oracle Database, the setup will look mostly the same as the Postgres client, here's the example

Springboot Example:

    @Bean("dataSource")
    @Primary
    public DataSource getDataSource() {
        DataSource hikariDataSource = null;
        HikariConfig config = new HikariConfig();
        config.setConnectionTimeout(connectionTimeout);
        config.setMinimumIdle(minimumIdle);
        config.setMaximumPoolSize(maximumPoolSize);
        config.setDriverClassName(dbDriver);
        config.setUsername(dbUsername);
        config.setPassword(dbPassword);

        if(isSSLEnabled) {
            config.setJdbcUrl(dbUrlTCPS);
            config.addDataSourceProperty(ORACLE_JDBC_COMPLIANT_KEY, ORACLE_JDBC_COMPLIANT);
            config.addDataSourceProperty(JAVAX_TRUST_STORE_TYPE_KEY, keyStoreType);
            config.addDataSourceProperty(JAVAX_TRUST_STORE_PATH_KEY, keyStoreFilePath);
            config.addDataSourceProperty(JAVAX_TRUST_STORE_PASSWORD_KEY, keyStoreFilePassword);
            config.addDataSourceProperty(ORACLE_DN_MATCH_KEY, sslDNMatch);
            hikariDataSource = new HikariDataSource(config);
            log.info("DB connection is established using SSL Protocol. Configuring the dataSource with the configuration :: " +
                            "driver={}, urlTCPS={}, userName={}, timeout={}, minIdle={} & maxPoolSize={}",
                    dbDriver, dbUrlTCPS, dbUsername, connectionTimeout, minimumIdle, maximumPoolSize);
        } else {
            config.setJdbcUrl(dbUrlTCP);
            hikariDataSource = new HikariDataSource(config);
            log.info("DB connection is established using NON-SSL Protocol. Configuring the dataSource with the configuration :: " +
                            "driver={}, urlTCP={}, userName={}, timeout={}, minIdle={} & maxPoolSize={}",
                    dbDriver, dbUrlTCP, dbUsername, connectionTimeout, minimumIdle, maximumPoolSize);
        }
        return hikariDataSource;
    }

Enter fullscreen mode Exit fullscreen mode

Plain Java Example:

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.Properties;
import java.sql.*;
public class ConnectionTest {
    private static final String DB_SERVER_NAME = "db.mydbs32eds.us-west-2.rds.amazonaws.com";

    private static final Integer SSL_PORT = 15000;
    private static final String DB_SID = "some_sid";
    private static final String DB_USER = "someuser";
    private static final String DB_PASSWORD = "somepassword";
    // This key store has only the prod root ca.
    private static final String KEY_STORE_FILE_PATH = "/usr/clientkeystore.jks";
    private static final String KEY_STORE_PASS = "strongpassword";

    public static void main(String[] args) throws SQLException {
        final Properties properties = new Properties();

        final String connectionString = String.format("jdbc:oracle:thin:@(DESCRIPTION=(ADDRESS=(PROTOCOL=TCPS)(HOST=%s)(PORT=%d))"+"(CONNECT_DATA=(SERVICE_NAME=%s))"+"(SECURITY = (SSL_SERVER_CERT_DN = \"CN=%s, OU=RDS, O=Amazon.com, L=Seattle, ST=Washington, C=US\")))",DB_SERVER_NAME, SSL_PORT, DB_SID, DB_SERVER_NAME);

        try {
            // Class.forName("oracle.jdbc.driver.OracleDriver");
            properties.put("user", DB_USER);
            properties.put("password", DB_PASSWORD);
            properties.put("oracle.jdbc.J2EE13Compliant", "true");
            properties.put("javax.net.ssl.trustStore", KEY_STORE_FILE_PATH);
            properties.put("javax.net.ssl.trustStoreType", "JKS");
            properties.put("javax.net.ssl.trustStorePassword", KEY_STORE_PASS);
            properties.put("oracle.net.ssl_server_dn_match", "TRUE");
            final Connection con = DriverManager.getConnection(connectionString, properties);
            // If no exception, that means the handshake has passed, and an SSL connection can be opened

            if(con != null) {
                System.out.println("Connected to the database!");
            } else {
                System.out.println("Failed to make connection!");
            }
        }
        catch (Exception err) {
            System.out.println(err.${getMessage()});
        }
        finally {
            System.out.println("Connection closed");
            con.close();    //closing connection
        }      
    }
}

Enter fullscreen mode Exit fullscreen mode

Client Connection Settings Review:

Adding the SSL/TLS certificate to the trust store of your client application is not enough to ensure a secure environment. It's crucial to configure your connection string correctly for Postgres clients, especially the SSL modes. Using the right SSL modes and certificates when connecting to the database ensures that your communication is encrypted and authenticated, making it difficult for attackers to intercept and read your data. By specifying the SSL mode, SSL root certificate file, SSL cert file, and SSL key file in the connection string, you can ensure that your client application connects to the Postgres database securely. Ensure that you specify the right details and files in the connection string to protect your environment from cyber threats.

Postgres clients:

when forming the Connection to the Postgres Database with the Certificate part of the Trust Store we are not proving it a secure environment, some of these settings will not be effective unless the connection string is formed correctly for Postgres clients, especially SSL modes.

So let us review the Connection String which has been used in the above example.

Postgres JDBC Connection String :

jdbc:postgresql://localhost:5432/db?useSSL=true&sslmode=verify-ca&sslfactory=org.postgresql.ssl.DefaultJavaSSLFactory
Enter fullscreen mode Exit fullscreen mode
  • useSSL : Ensures that the Client can make SSL Connections
  • sslfactory: org.postgresql.ssl.DefaultJavaSSLFactory is specified in the JDBC URL because we want to avoid default LibPQ because it will deal with different implementations considering the use of the JKS truststore we can rely on JavaSSLFactory.
  • sslmode: SSLModes values will define the standard used when forming the SSL connection to the Database.

Postgres SSL modes: (ordered from least to most recommended)

  • disable - disables SSL encryption completely.
  • allow - allows SSL connections, but doesn't require it.
  • prefer - prefers SSL connection, but falls back to non-SSL if it fails.
  • require - requires SSL connection, PostgreSQL will still establish an SSL connection, but it won't verify the server's certificate. This means that the server could potentially be an imposter, and your connection could be vulnerable to a man-in-the-middle (MITM) attack.
  • verify-ca - requires the SSL connection and verifies that the server certificate is issued by a trusted CA. Recommended only when you want to use the Route53 DNS record instead of the actual Postgres DNS record.
  • verify-full - requires the SSL connection and verifies that the server certificate is issued by a trusted CA and hostname matches. (Most Recommended)

As you can see Sample Connection string that I used is the verify-ca, I am not using verify-full because the Host is localhost which cannot be matched with the RDS Database hostname on the Certificate, that's because I am using port-forwarding instead of direct hostname.

The best practice is to use the SSL mode verify-full wherever possible, verify-ca may still be used in cases where you would want to have a custom DNS record instead of a direct RDS Hostname DNS Record, even though this sacrifices the hostname check in proving the RDS Database Identity.

Oracle Clients:

Oracle Clients have almost the same characteristics as Postgres clients do except SSL mode is strict TLS, unlike the different SSLMode in Postgres clients.

Sample Oracle JDBC Connection String :

jdbc:oracle:thin:@(DESCRIPTION=(ADDRESS=(PROTOCOL=TCPS)(HOST=%s)(PORT=%d))"+"(CONNECT_DATA=(SERVICE_NAME=%s))"+"(SECURITY = (SSL_SERVER_CERT_DN = \"CN=%s, OU=RDS, O=Amazon.com, L=Seattle, ST=Washington, C=US\")))
Enter fullscreen mode Exit fullscreen mode

However the major difference is in Connection String, but when it comes to the Strict Hostname check Oracle Clients do have the same behavior as Postgres clients, where the Strict Hostname check can be relaxed with the property called oracle.net.ssl_server_dn_match, the property can be toggled off when using the Custom DNS. But to prove the RDS Database Server Certificate to clients, it's always best to keep this property on & use the RDS Hostname directly.


Conclusion:
The SSL/TLS protocol plays a significant role in providing secure communication between client applications and RDS environments. Given the importance of RDS environments for businesses, it is vital to keep them secure. Since the 2019-ca SSL/TLS certificate is set to expire very soon, AWS RDS users need to renew their certificates. An outdated certificate may place your business data at risk of a cyber-attack. This post provides instructions for performing changes to client applications and also offers best practices to ensure strong SSL/TLS security when applications connect to Databases. By following these guidelines, AWS RDS Database Clients can keep their data transmission secure and continue to take advantage of the benefits of RDS environments.


References:

-

https://jdbc.postgresql.org/documentation/ssl/

https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/UsingWithRDS.SSL-certificate-rotation.html

https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/UsingWithRDS.SSL.html

Also, please let me know your thoughts on this article, Your feedback is appreciated.

Top comments (0)