Reputation: 441
I have a Spring Boot application from which I am trying to connect to a MySQL on AWS RDS. However I am having issues with the following error:
localhost-startStop-1, handling exception: javax.net.ssl.SSLHandshakeException: Received fatal alert: unknown_ca
After enabling debug for the ssl handshake, I see that for the CertificateRequest
step the Cert Authorities:
are empty.
Based on my understanding this is the part where the client (spring boot app) needs to present a cert to the server (mysql db).
Certificate Request
step - The server will issue a certificate request from the client.*** Certificate chain
, which is the certificate the client is sending to the server. In this case for me, it is sending the content of keyStore_cert.jks
.What I think the issue so far:
The server (mySQL db) does not know about this certificate (my keyStore_cert.jks) that the client (my app) is sending.
But, I was under the impression that the client certs are not required unless you set REQUIRE X509
for the user.
The questions:
keyStore_cert.jks
/trustStore_cert.jks
?These are what my settings and what I have tried.
jdbc:mysql://<host>:<port>/<db_name>?useLegacyDatetimeCode=false&verifyServerCertificate=true&useSSL=true&requireSSL=true
ALTER USER '<my_db_user>'@'%' require SSL;
GRANT USAGE ON <db_name>.* TO '<my_db_user>'@'%' REQUIRE SSL;
keytool -import -keystore trustStore_cert.jks -storepass <trustStore_password> -alias "awsrds-us-east1" -file rds-ca-2015-us-east-1.pem
-Djavax.net.ssl.keyStore=path/keyStore_cert.jks
-Djavax.net.ssl.keyStorePassword=<keyStore_password>
-Djavax.net.ssl.trustStore=path/trustStore_cert.jks
-Djavax.net.ssl.trustStorePassword=<trustStore_password>
Host: <host>
Port: <port>
User: <user>
SSL: enabled with DHE-RSA-AES128-SHA
SSL connection error: CA certificate is required if ssl-mode is VERIFY_CA or VERIFY_IDENTITY
. Which makes sense since i am not specifying anything for the CA file.Steps for the handshake (will omit some logs.):
*** ClientHello, TLSv1.1 (seems okay)
***
*** ServerHello, TLSv1.1 (seems okay)
***
*** Certificate chain (has the root key)
chain [0] = [
[
Version: V3
Subject: C=US, ST=Washington, L=Seattle, O=Amazon.com, OU=RDS, CN=**<my_rds_name>abcd.us-east-1.rds.amazonaws.com**
Signature Algorithm: SHA1withRSA, OID = 1.2.840.113549.1.1.5
...
...
chain [1] = [ (has the us-east-1 key)
[
Version: V3
Subject: CN=Amazon RDS us-east-1 CA, OU=Amazon RDS, O="Amazon Web Services, Inc.", L=Seattle, ST=Washington, C=US
Signature Algorithm: SHA1withRSA, OID = 1.2.840.113549.1.1.5
...
...
***
Found trusted certificate:
[
[
Version: V3
Subject: CN=Amazon RDS us-east-1 CA, OU=Amazon RDS, O="Amazon Web Services, Inc.", L=Seattle, ST=Washington, C=US
Signature Algorithm: SHA1withRSA, OID = 1.2.840.113549.1.1.5
...
...
*** CertificateRequest (the Authorities are empty)
Cert Types: RSA, DSS, ECDSA
Cert Authorities:
<Empty>
*** ServerHelloDone
matching alias: <my keyStoreAlias>
*** Certificate chain (seems entries from my own key Store)
chain [0] = [
[
Version: V3
Subject: CN=<the CN on my keyStore>, OU=Web Servers, O=Company , C=US
Signature Algorithm: SHA256withRSA, OID = 1.2.840.113549.1.1.11
***
*** ClientKeyExchange, RSA PreMasterSecret, TLSv1.1 (seems okay)
*** CertificateVerify (seem okay)
*** Finished
verify_data: { 50, 89, 33, 202, 193, 158, 226, 114, 128, 50, 198, 250 }
***
Upvotes: 8
Views: 8194
Reputation: 51
Since version 8.0.22 of the MySQL Connector/J driver you can now use the fallbackToSystemKeyStore property set to false to ignore any system level keystore rather than having to create a dummy one.
I've used this method successfully against an RDS MySQL 5.7 instance and where I initially saw the same "unknown_ca" error it now connects perfectly.
https://dev.mysql.com/doc/connector-j/8.0/en/connector-j-connp-props-security.html
Upvotes: 3
Reputation: 61
We encountered this issue - "Caused by: javax.net.ssl.SSLHandshakeException: Received fatal alert: unknown_ca" while making connection with AWS RDS 8.X
This error came only when keystore was set at the JVM level. If only trustore is set at the JVM level and not the keystore, this issue do not pop up because truststore trusts RDS certificate already -Djavax.net.ssl.keyStore=/path/to/your/keystore.jks -Djavax.net.ssl.keyStorePassword=keystore_password
As RDS is shared service, there is no option in RDS to trust certificates of individual client. If keystore is passed, RDS thinks that the request is for 2 way SSL and it fails. On the other hand, we couldn't get rid of keystore at the JVM level because 2 way SSL was enabled with other backends such as API Gateway
Solution: we provided dummy keystore (trustore itself as it do not contain any private key) to the jdbc to override the keystore that was provided at the JVM level and it worked like a charm!!
public class MySQLSSLTest {
private static final String DB_USER = "username";
private static final String DB_PASSWORD = "password";
// This key store has only the prod root ca.
private static final String KEY_STORE_FILE_PATH = "file-path-to-keystore";
private static final String KEY_STORE_PASS = "keystore-password";
public static void test(String[] args) throws Exception {
Class.forName("com.mysql.jdbc.Driver");
System.setProperty("javax.net.ssl.trustStore", KEY_STORE_FILE_PATH);
System.setProperty("javax.net.ssl.trustStorePassword", KEY_STORE_PASS);
Properties properties = new Properties();
properties.put("user", DB_USER);
properties.put("password", DB_PASSWORD);
properties.put("sslMode", "DISABLED");
properties.put("useSSL", "false");
properties.put("clientCertificateKeyStoreUrl", "file:/path/to/your/trust/store");
properties.put("clientCertificateKeyStorePassword", "truststore_password");
properties.put("clientCertificateKeyStoreType", "jks");
Connection connection = null;
Statement stmt = null;
ResultSet rs = null;
try {
connection = DriverManager.getConnection("jdbc:mysql://mydatabase.123456789012.us-east-1.rds.amazonaws.com:3306",properties);
stmt = connection.createStatement();
rs=stmt.executeQuery("SELECT 1 from dual");
} finally {
if (rs != null) {
try {
rs.close();
} catch (SQLException e) {
}
}
if (stmt != null) {
try {
stmt.close();
} catch (SQLException e) {
}
}
if (connection != null) {
try {
connection.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
return;
}
}
Hopefully this will help you!!!
Upvotes: 0
Reputation: 441
After working with AWS RDS Support I got an explanation why the issue and how to fix it.
Just to clarify, this issue is mainly presented only if you pass your own keystore as part of JVM arguments. Something like this:
-Djavax.net.ssl.keyStore=path/keyStore_cert.jks
-Djavax.net.ssl.keyStorePassword=<keyStore_password>
RDS is a managed service and they do not have a way (currently) to load specific client certificates into the server. Meaning they can’t pass a specific certificates in DB configuration level. Usually this would be possible if you were to standup your own MY SQL server. In the config file for that server you can specify the client/server certs. So, RDS can’t verify the certificate provided by client.
If you were to pass a keystore, which has a key-pair entry (as JVM argument or otherwise), during SSL handshake, the client authentications step would fail. This is expected behavior in database. RDS can’t restrict itself to verify (or not verify) the file loaded in client field for the client during the initial connection. So, if a keystore is passed, server will try to match the certificate keys with existing CA file and if doesn’t match it will not allow the connections.
The solution is to either not pass a keystore at all or pass a blank keystore (one that does not have a key-pair with just Trusted Certificate Entry or one that is just blank).
if you choose to not pass a keystore, then do not specify these properties
-Djavax.net.ssl.keyStore= & -Djavax.net.ssl.keyStorePassword=
. And construct the DB connection URL like this:
-Ddb_jdbc_url=jdbc:mysql://<host>:<port>/<db_name>?useLegacyDatetimeCode=false&verifyServerCertificate=true&useSSL=true&requireSSL=true
. You still need to provide the truststore. See at the bottom for more.
Pass a black keystore or (or pass the trust store in the keystore field) You can pass what ever you want for the keystore and trustore JVM params. And for the URL you construct it like this:
-Ddb_jdbc_url=jdbc:mysql://<host>:<port>/<db_name>?
useLegacyDatetimeCode=false&verifyServerCertificate=true&useSSL=true&requireSSL=true
&clientCertificateKeyStoreUrl=file:/user/documents/projects/trust-store-rds.jks
&clientCertificateKeyStorePassword=<password>
&clientCertificateKeyStoreType=JKS
&trustCertificateKeyStoreUrl=file:/user/documents/projects/trust-store-rds.jks
&trustCertificateKeyStorePassword=<password>
&trustCertificateKeyStoreType=JKS
Note that for both trustCertificateKeyStoreUrl
and clientCertificateKeyStoreUrl
I am passing the same file.
Note, you need to also configure all the previous steps:
CREATE USER 'my_user'@'%' IDENTIFIED BY 'my_password';
ALTER USER 'my_user'@'%' REQUIRE SSL;
GRANT USAGE ON *.* TO 'my_user'@'%' REQUIRE SSL ;
keytool -import -keystore trust-store-rds.jks -storepass changeit -noprompt alias "aws-rds-root" -file rds-ca-2015-root.pem
keytool -import -keystore trust-store-rds.jks -storepass changeit -noprompt -alias "aws-rds-us-east-1" -file rds-ca-2015-us-east-1.pem
Upvotes: 7