Connecting a Java Application in a Docker Container to Amazon RDS MySQL with SSL Using Default Java cacerts

Introduction

Connecting a Java application running inside a Docker container to an Amazon RDS MySQL database is a common requirement for cloud-based applications. Ensuring a secure connection using SSL is crucial to protect data in transit. This article covers the technical steps needed to achieve this, focusing on importing the SSL certificates provided by Amazon RDS into the default Java cacerts using a specified Dockerfile.

Prerequisites

Before proceeding, ensure you have the following:

  1. A running Java application inside a Docker container.
  2. An Amazon RDS MySQL instance.
  3. Access to the Amazon RDS SSL certificates.
  4. Access to the Dockerfile or the ability to modify your Docker container.

Step-by-Step Guide

Step 1: Configure Database Connection

In your Java application, configure the connection to the Amazon RDS MySQL instance using SSL. Ensure the JDBC URL includes the SSL parameters. Also do not forget to modify your-rds-endpoint and yourdbname parts of the JDBC URL string. Place the Java file into this folder src/main/java/com/example.

package com.example;

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

public class MySQLConnectionExample {
    public static void main(String[] args) {
        String jdbcUrl = "jdbc:mysql://your-rds-endpoint:3306/yourdbname?verifyServerCertificate=true&useSSL=true&requireSSL=true";
        String username = "yourusername";
        String password = "yourpassword";

        try {
            Connection connection = DriverManager.getConnection(jdbcUrl, username, password);
            System.out.println("Connection established successfully!");
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}


Step 2: Build the Java application

To build that Java application use the provided pom.xml, ensure Maven is installed and navigate to the directory containing the pom.xml file (next to the src folder). Run mvn clean package in the terminal. This command cleans the target directory, compiles the source code using Java 17, and packages the compiled code and dependencies into an executable JAR file. The resulting JAR file will be located in the target directory. Copy the resulting my-app-1.0-SNAPSHOT.jar file in a directory where you will place your Dockerfile and other artifacts (as described in the next steps).

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.example</groupId>
    <artifactId>my-app</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
    </properties>

    <dependencies>
        <!-- MySQL Connector/J -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.29</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
                <configuration>
                    <source>17</source>
                    <target>17</target>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jar-plugin</artifactId>
                <version>3.1.0</version>
                <configuration>
                    <archive>
                        <manifest>
                            <mainClass>com.example.MySQLConnectionExample</mainClass>
                        </manifest>
                    </archive>
                </configuration>
	</plugin>
	<plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-shade-plugin</artifactId>
            <version>3.2.4</version>
            <executions>
                <execution>
                    <phase>package</phase>
                    <goals>
                        <goal>shade</goal>
                    </goals>
                </execution>
            </executions>
        </plugin>
        </plugins>
    </build>
</project>


Step 3: Obtain Amazon RDS SSL Certificates

Amazon RDS provides SSL certificates that need to be imported into your Java cacerts. Download the certificate bundle either for all AWS regions or for your specific AWS region from the Amazon RDS documentation or directly using one of the following links:

# Certificates for ll AWS regions
wget https://truststore.pki.rds.amazonaws.com/global/global-bundle.pem

# Certificates for a specific AWS region (e.g. us-east-1) 
wget https://truststore.pki.rds.amazonaws.com/us-east-1/us-east-1-bundle.pem

Step 4: Prepare the Dockerfile

Use the this Dockerfile to build your Java application container, which automates the import of the Amazon RDS SSL certificates into the default Java cacerts:

FROM ibm-semeru-runtimes:open-17-jre-jammy

# AWS RDS certificates for all regions
COPY global-bundle.pem /tmp/all-certs.crt

# AWS RDS certificates of one specific region
# COPY us-east-1-bundle.pem /tmp/all-certs.crt

# Import all certificates from the combined certificate file
RUN awk 'BEGIN {c=0;} /-----BEGIN CERTIFICATE-----/ {outfile=sprintf("/tmp/cert%d.crt", c++);} {print > outfile;}' /tmp/all-certs.crt && \
  for cert in /tmp/cert*.crt; do keytool -import -trustcacerts -cacerts -storepass changeit -noprompt -alias $(basename $cert) -file $cert; done && \
  rm /tmp/cert*.crt

RUN mkdir /opt/app
COPY my-app-1.0-SNAPSHOT.jar /opt/app/my-app.jar

CMD ["java", "-jar", "/opt/app/my-app.jar"]

Step 5: Build and Run the Docker Container

Build your Docker image using the Dockerfile:

sudo docker build -t my-app .

Run the Docker container with the newly built image:

sudo docker run -it my-app


If everything went well, the application connects to your MySQL database successfully using the SSL connection.

Connection established successfully!

On the other hand if the connection fails, you might get following error “java.security.cert.CertPathValidatorException: Path does not chain with any of the trust anchors“, which indicates that the SSL handshake failed because the server’s certificate chain could not be validated against the trust anchors (trusted certificates) in the default Java truststore.

com.mysql.cj.jdbc.exceptions.CommunicationsException: Communications link failure

The last packet sent successfully to the server was 0 milliseconds ago. The driver has not received any packets from the server.
	at com.mysql.cj.jdbc.exceptions.SQLError.createCommunicationsException(SQLError.java:174)
	at com.mysql.cj.jdbc.exceptions.SQLExceptionsMapping.translateException(SQLExceptionsMapping.java:64)
	at com.mysql.cj.jdbc.ConnectionImpl.createNewIO(ConnectionImpl.java:828)
	at com.mysql.cj.jdbc.ConnectionImpl.<init>(ConnectionImpl.java:448)
	at com.mysql.cj.jdbc.ConnectionImpl.getInstance(ConnectionImpl.java:241)
	at com.mysql.cj.jdbc.NonRegisteringDriver.connect(NonRegisteringDriver.java:198)
	at java.sql/java.sql.DriverManager.getConnection(Unknown Source)
	at java.sql/java.sql.DriverManager.getConnection(Unknown Source)
	at com.example.MySQLConnectionExample.main(MySQLConnectionExample.java:14)
Caused by: com.mysql.cj.exceptions.CJCommunicationsException: Communications link failure

The last packet sent successfully to the server was 0 milliseconds ago. The driver has not received any packets from the server.
	at java.base/jdk.internal.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
	at java.base/jdk.internal.reflect.NativeConstructorAccessorImpl.newInstance(Unknown Source)
	at java.base/jdk.internal.reflect.DelegatingConstructorAccessorImpl.newInstance(Unknown Source)
	at java.base/java.lang.reflect.Constructor.newInstanceWithCaller(Unknown Source)
	at java.base/java.lang.reflect.Constructor.newInstance(Unknown Source)
	at com.mysql.cj.exceptions.ExceptionFactory.createException(ExceptionFactory.java:61)
	at com.mysql.cj.exceptions.ExceptionFactory.createException(ExceptionFactory.java:105)
	at com.mysql.cj.exceptions.ExceptionFactory.createException(ExceptionFactory.java:151)
	at com.mysql.cj.exceptions.ExceptionFactory.createCommunicationsException(ExceptionFactory.java:167)
	at com.mysql.cj.protocol.a.NativeProtocol.negotiateSSLConnection(NativeProtocol.java:378)
	at com.mysql.cj.protocol.a.NativeAuthenticationProvider.connect(NativeAuthenticationProvider.java:205)
	at com.mysql.cj.protocol.a.NativeProtocol.connect(NativeProtocol.java:1433)
	at com.mysql.cj.NativeSession.connect(NativeSession.java:133)
	at com.mysql.cj.jdbc.ConnectionImpl.connectOneTryOnly(ConnectionImpl.java:948)
	at com.mysql.cj.jdbc.ConnectionImpl.createNewIO(ConnectionImpl.java:818)
	... 6 more
Caused by: javax.net.ssl.SSLHandshakeException: java.security.cert.CertPathValidatorException: Path does not chain with any of the trust anchors
	at java.base/sun.security.ssl.Alert.createSSLException(Unknown Source)
	at java.base/sun.security.ssl.TransportContext.fatal(Unknown Source)
	at java.base/sun.security.ssl.TransportContext.fatal(Unknown Source)
	at java.base/sun.security.ssl.TransportContext.fatal(Unknown Source)
	at java.base/sun.security.ssl.CertificateMessage$T13CertificateConsumer.checkServerCerts(Unknown Source)
	at java.base/sun.security.ssl.CertificateMessage$T13CertificateConsumer.onConsumeCertificate(Unknown Source)
	at java.base/sun.security.ssl.CertificateMessage$T13CertificateConsumer.consume(Unknown Source)
	at java.base/sun.security.ssl.SSLHandshake.consume(Unknown Source)
	at java.base/sun.security.ssl.HandshakeContext.dispatch(Unknown Source)
	at java.base/sun.security.ssl.HandshakeContext.dispatch(Unknown Source)
	at java.base/sun.security.ssl.TransportContext.dispatch(Unknown Source)
	at java.base/sun.security.ssl.SSLTransport.decode(Unknown Source)
	at java.base/sun.security.ssl.SSLSocketImpl.decode(Unknown Source)
	at java.base/sun.security.ssl.SSLSocketImpl.readHandshakeRecord(Unknown Source)
	at java.base/sun.security.ssl.SSLSocketImpl.startHandshake(Unknown Source)
	at java.base/sun.security.ssl.SSLSocketImpl.startHandshake(Unknown Source)
	at com.mysql.cj.protocol.ExportControlled.performTlsHandshake(ExportControlled.java:347)
	at com.mysql.cj.protocol.StandardSocketFactory.performTlsHandshake(StandardSocketFactory.java:191)
	at com.mysql.cj.protocol.a.NativeSocketConnection.performTlsHandshake(NativeSocketConnection.java:101)
	at com.mysql.cj.protocol.a.NativeProtocol.negotiateSSLConnection(NativeProtocol.java:369)
	... 11 more
Caused by: java.security.cert.CertificateException: java.security.cert.CertPathValidatorException: Path does not chain with any of the trust anchors
	at com.mysql.cj.protocol.ExportControlled$X509TrustManagerWrapper.checkServerTrusted(ExportControlled.java:408)
	at java.base/sun.security.ssl.AbstractTrustManagerWrapper.checkServerTrusted(Unknown Source)
	... 27 more
Caused by: java.security.cert.CertPathValidatorException: Path does not chain with any of the trust anchors
	at java.base/sun.security.provider.certpath.PKIXCertPathValidator.validate(Unknown Source)
	at java.base/sun.security.provider.certpath.PKIXCertPathValidator.engineValidate(Unknown Source)
	at java.base/java.security.cert.CertPathValidator.validate(Unknown Source)
	at com.mysql.cj.protocol.ExportControlled$X509TrustManagerWrapper.checkServerTrusted(ExportControlled.java:402)
	... 28 more

Here are some possible causes for this issue:

  1. Incomplete Certificate Chain: The server’s certificate chain is incomplete, meaning that one or more intermediate certificates are missing.
  2. Untrusted Root Certificate: The root certificate of the server’s certificate chain is not in the truststore.
  3. Wrong Truststore Path or Password: The application is not correctly pointing to the truststore or the password is incorrect.
  4. Expired or Revoked Certificates: One of the certificates in the chain is expired or revoked.

Leave a Reply

Your email address will not be published. Required fields are marked *