Amazon RDSやCloud SQLといったCloud Foundry外部に存在するMySQLにアクセスする場合は通信を暗号化するのが良いです。
例えば、次のようにmanifest.yml
にRDSのTLS通信を有効にした接続情報を環境変数に設定し、cf push
してみると、、
applications:
- name: foo
path: target/demo-0.0.1-SNAPSHOT.jar
env:
SPRING_DATASOURCE_DRIVER_CLASS_NAME: com.mysql.jdbc.Driver
SPRING_DATASOURCE_URL: jdbc:mysql://example.ap-northeast-1.rds.amazonaws.com:3306/cfdemo?useSSL=true
SPRING_DATASOURCE_USERNAME: user
SPRING_DATASOURCE_PASSWORD: password
次のようなエラーが発生するでしょう。これはCloud Foundry上でなくてもローカル開発時でも同じです。
Caused by: javax.net.ssl.SSLHandshakeException: java.security.cert.CertificateException: java.security.cert.CertPathValidatorException: Path does not chain with any of the trust anchors
at sun.security.ssl.Alerts.getSSLException(Alerts.java:192) ~[na:1.8.0_66]
at sun.security.ssl.SSLSocketImpl.fatal(SSLSocketImpl.java:1949) ~[na:1.8.0_66]
at sun.security.ssl.Handshaker.fatalSE(Handshaker.java:302) ~[na:1.8.0_66]
at sun.security.ssl.Handshaker.fatalSE(Handshaker.java:296) ~[na:1.8.0_66]
at sun.security.ssl.ClientHandshaker.serverCertificate(ClientHandshaker.java:1509) ~[na:1.8.0_66]
at sun.security.ssl.ClientHandshaker.processMessage(ClientHandshaker.java:216) ~[na:1.8.0_66]
at sun.security.ssl.Handshaker.processLoop(Handshaker.java:979) ~[na:1.8.0_66]
at sun.security.ssl.Handshaker.process_record(Handshaker.java:914) ~[na:1.8.0_66]
at sun.security.ssl.SSLSocketImpl.readRecord(SSLSocketImpl.java:1062) ~[na:1.8.0_66]
at sun.security.ssl.SSLSocketImpl.performInitialHandshake(SSLSocketImpl.java:1375) ~[na:1.8.0_66]
at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1403) ~[na:1.8.0_66]
at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1387) ~[na:1.8.0_66]
at com.mysql.jdbc.ExportControlled.transformSocketToSSLSocket(ExportControlled.java:188) ~[mysql-connector-java-5.1.44.jar:5.1.44]
... 39 common frames omitted
Caused by: java.security.cert.CertificateException: java.security.cert.CertPathValidatorException: Path does not chain with any of the trust anchors
at com.mysql.jdbc.ExportControlled$X509TrustManagerWrapper.checkServerTrusted(ExportControlled.java:304) ~[mysql-connector-java-5.1.44.jar:5.1.44]
at sun.security.ssl.AbstractTrustManagerWrapper.checkServerTrusted(SSLContextImpl.java:922) ~[na:1.8.0_66]
at sun.security.ssl.ClientHandshaker.serverCertificate(ClientHandshaker.java:1491) ~[na:1.8.0_66]
... 47 common frames omitted
Caused by: java.security.cert.CertPathValidatorException: Path does not chain with any of the trust anchors
at sun.security.provider.certpath.PKIXCertPathValidator.validate(PKIXCertPathValidator.java:153) ~[na:1.8.0_66]
at sun.security.provider.certpath.PKIXCertPathValidator.engineValidate(PKIXCertPathValidator.java:79) ~[na:1.8.0_66]
at java.security.cert.CertPathValidator.validate(CertPathValidator.java:292) ~[na:1.8.0_66]
at com.mysql.jdbc.ExportControlled$X509TrustManagerWrapper.checkServerTrusted(ExportControlled.java:297) ~[mysql-connector-java-5.1.44.jar:5.1.44]
... 49 common frames omitted
Path does not chain with any of the trust anchors
というエラーメッセージが出るのは、接続先のCA証明書が信頼済みの証明書一覧に含まれていないためです。
Javaの場合、信頼済みの証明書はKeystoreに設定されており、JDK側で管理されいます。
CA証明書を追加する方法を3パターン紹介します。
目次
keytool
を利用して信頼されたCA証明書を追加する
Amazon RDSもCloud SQLもCA証明書は公開されているので、keytool
コマンドを使ってこれをimportすれば良いです。
Amazon RDSの場合
からリージョンに合った中間証明書をダウンロードしてください。
ap-northeast-1
の例で説明します。
次のコマンドでrds-ca-2015-ap-northeast-1.pem
からsrc/main/resources
直下にrds-ca.jks
を作成します。
cd your-app
wget https://s3.amazonaws.com/rds-downloads/rds-ca-2015-ap-northeast-1.pem
keytool -keystore src/main/resources/rds-ca.jks -storepass changeit -importcert -noprompt -alias MyCert -file rds-ca-2015-ap-northeast-1.pem
再度、mvn package
を行い、rds-ca.jks
を同梱したjarファイルを作成します。
そして、環境変数JAVA_OPT
に作成したkeystoreのパスとパスワードを設定します。
applications:
- name: foo
path: target/demo-0.0.1-SNAPSHOT.jar
env:
SPRING_DATASOURCE_DRIVER_CLASS_NAME: com.mysql.jdbc.Driver
SPRING_DATASOURCE_URL: jdbc:mysql://example.ap-northeast-1.rds.amazonaws.com:3306/cfdemo?useSSL=true
SPRING_DATASOURCE_USERNAME: user
SPRING_DATASOURCE_PASSWORD: password
JAVA_OPTS: '-Djavax.net.ssl.trustStore=/home/vcap/app/BOOT-INF/classes/rds-ca.jks -Djavax.net.ssl.trustStorePassword=changeit'
Cloud Foundryのコンテナ上ではjarファイルは/home/vcap/app
に展開された形になりますので、keystoreのパスは/home/vcap/app/BOOT-INF/classes/rds-ca.jks
です。
これでcf push
し直せばエラーが発生することなくRDSにTLSで通信できるでしょう。
ローカル開発環境で
useSSL=true
を有効にしたい場合は、JVMオプションに-Djavax.net.ssl.trustStore=/Users/.../you-app/src/main/resources/rds-ca.jks -Djavax.net.ssl.trustStorePassword=changeit
を追加すれば良いです。
追記 Auroraの場合
証明書は https://s3.amazonaws.com/rds-downloads/rds-combined-ca-bundle.pem で JDBC URLは
jdbc:mysql:aurora://xxxx.cluster-xxxxx.ap-northeast-1.rds.amazonaws.com:3306/cfdemo?useSSL=true&trustServerCertificate=true
にする必要がありました。(DriverはもちろんMariaDB)
Cloud SQLの場合
"SSL"タブの"View Server CA Certificate"をクリックし、
"Download server-ca.pem"をクリックし、you-app
ディレクトリ(アプリケーションプロジェクトのルートディレクトリ)に保存してください。
次のコマンドでserver-ca.pem
からsrc/main/resources
直下にcloudsql-ca.jks
を作成します。
cd your-app
keytool -keystore src/main/resources/cloudsql-ca.jks -storepass changeit -importcert -noprompt -alias MyCert -file server-ca.pem
あとはRDSの場合と同じです。
再度、mvn package
を行い、cloudsql-ca.jks
を同梱したjarファイルを作成します。
そして、環境変数JAVA_OPT
に作成したkeystoreのパスとパスワードを設定します。
applications:
- name: foo
path: target/demo-0.0.1-SNAPSHOT.jar
env:
SPRING_DATASOURCE_DRIVER_CLASS_NAME: com.mysql.jdbc.Driver
SPRING_DATASOURCE_URL: jdbc:mysql://aaa.bbb.ccc.ddd:3306/cfdemo?useSSL=true
SPRING_DATASOURCE_USERNAME: user
SPRING_DATASOURCE_PASSWORD: password
JAVA_OPTS: '-Djavax.net.ssl.trustStore=/home/vcap/app/BOOT-INF/classes/cloudsql-ca.jks -Djavax.net.ssl.trustStorePassword=changeit'
Cloud Foundryのコンテナ上ではjarファイルは/home/vcap/app
に展開された形になりますので、keystoreのパスは/home/vcap/app/BOOT-INF/classes/cloudsql-ca.jks
です。
これでcf push
し直せばエラーが発生することなくCloud SQLにTLSで通信できるでしょう。
プログラマティクに信頼されたCA証明書を追加する
前述のやり方はkeytool
を使ってkeystoreファイルを作成し、jarに同梱し、JAVA_OPTS
にシステムプロパティを設定するというやり方で少し手間がかかります。
実は全く同じことは次のJavaコードで実現可能です。
String serverCaPem = "-----BEGIN CERTIFICATE-----\n" +
"...\n" +
"-----END CERTIFICATE-----";
CertificateFactory fact = CertificateFactory.getInstance("X.509");
Certificate certificate = fact.generateCertificate(new ByteArrayInputStream(serverCaPem.getBytes()));
KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
trustStore.load(null); // init empty keystore
trustStore.setCertificateEntry("imported", certificate);
String password = UUID.randomUUID().toString();
File trustStoreOutputFile = File.createTempFile("truststore", null);
trustStoreOutputFile.deleteOnExit();
trustStore.store(new FileOutputStream(trustStoreOutputFile), password.toCharArray());
System.setProperty("javax.net.ssl.trustStore", trustStoreOutputFile.getAbsolutePath());
System.setProperty("javax.net.ssl.trustStorePassword", password);
このコードをデータベースアクセスの前に行えば良いです。main
メソッドのSpringApplication.run(DemoApplication.class, args);
の前で実行しておけば確実です。
certificate-importer
を利用する
すべてのアプリケーションで上記のコードをコピペするのは面倒ですので、 これを自動で実行してくれるcertificate-importerと言うライブラリを作成したので、より簡単にCA証明書をKeystoreに追加可能です。
使い方はpom.xml
に次の依存ライブラリを追加し、
<dependency>
<groupId>am.ik.certificate</groupId>
<artifactId>certificate-importer</artifactId>
<version>0.0.1</version>
</dependency>
Spring Bootアプリの場合は、環境変数CA_CERTS
またはプロパティca.certs
に追加したいCA証明書をPEM形式で設定するだけです。
application-cloud.yml
に次のように設定するのが便利でしょう。
spring.datasource.driver-class-name: com.mysql.jdbc.Driver
spring.datasource.url: jdbc:mysql://example.ap-northeast-1.rds.amazonaws.com:3306/cfdemo?useSSL=true
spring.datasource.username: user
spring.datasource.password: password
ca.certs: |
-----BEGIN CERTIFICATE-----
MIIEATCCAumgAwIBAgIBRDANBgkqhkiG9w0BAQUFADCBijELMAkGA1UEBhMCVVMx
EzARBgNVBAgMCldhc2hpbmd0b24xEDAOBgNVBAcMB1NlYXR0bGUxIjAgBgNVBAoM
GUFtYXpvbiBXZWIgU2VydmljZXMsIEluYy4xEzARBgNVBAsMCkFtYXpvbiBSRFMx
GzAZBgNVBAMMEkFtYXpvbiBSRFMgUm9vdCBDQTAeFw0xNTAyMDUyMjAzMDZaFw0y
MDAzMDUyMjAzMDZaMIGUMQswCQYDVQQGEwJVUzETMBEGA1UECAwKV2FzaGluZ3Rv
bjEQMA4GA1UEBwwHU2VhdHRsZTEiMCAGA1UECgwZQW1hem9uIFdlYiBTZXJ2aWNl
cywgSW5jLjETMBEGA1UECwwKQW1hem9uIFJEUzElMCMGA1UEAwwcQW1hem9uIFJE
UyBhcC1ub3J0aGVhc3QtMSBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC
ggEBAMmM2B4PfTXCZjbZMWiDPyxvk/eeNwIRJAhfzesiGUiLozX6CRy3rwC1ZOPV
AcQf0LB+O8wY88C/cV+d4Q2nBDmnk+Vx7o2MyMh343r5rR3Na+4izd89tkQVt0WW
vO21KRH5i8EuBjinboOwAwu6IJ+HyiQiM0VjgjrmEr/YzFPL8MgHD/YUHehqjACn
C0+B7/gu7W4qJzBL2DOf7ub2qszGtwPE+qQzkCRDwE1A4AJmVE++/FLH2Zx78Egg
fV1sUxPtYgjGH76VyyO6GNKM6rAUMD/q5mnPASQVIXgKbupr618bnH+SWHFjBqZq
HvDGPMtiiWII41EmGUypyt5AbysCAwEAAaNmMGQwDgYDVR0PAQH/BAQDAgEGMBIG
A1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0OBBYEFIiKM0Q6n1K4EmLxs3ZXxINbwEwR
MB8GA1UdIwQYMBaAFE4C7qw+9hXITO0s9QXBj5yECEmDMA0GCSqGSIb3DQEBBQUA
A4IBAQBezGbE9Rw/k2e25iGjj5n8r+M3dlye8ORfCE/dijHtxqAKasXHgKX8I9Tw
JkBiGWiuzqn7gO5MJ0nMMro1+gq29qjZnYX1pDHPgsRjUX8R+juRhgJ3JSHijRbf
4qNJrnwga7pj94MhcLq9u0f6dxH6dXbyMv21T4TZMTmcFduf1KgaiVx1PEyJjC6r
M+Ru+A0eM+jJ7uCjUoZKcpX8xkj4nmSnz9NMPog3wdOSB9cAW7XIc5mHa656wr7I
WJxVcYNHTXIjCcng2zMKd1aCcl2KSFfy56sRfT7J5Wp69QSr+jq8KM55gw8uqAwi
VPrXn2899T1rcTtFYFP16WXjGuc0
-----END CERTIFICATE-----
Spring Bootアプリ以外の場合は、次のようなコードでimport可能です。
String serverCaPem = "-----BEGIN CERTIFICATE-----\n" +
"...\n" +
"-----END CERTIFICATE-----";
CertificateImporter certificateImporter = new CertificateImporter();
certificateImporter.doImport(serverCaPem);
Cloud Foundry全体で信頼されたCA証明書を追加する
上記の2方法はどれも、各アプリケーションに対して設定が必要です。
Cloud FoundryのJava BuildpackにはContainer Security Providerという仕組みがあり、Container Security ProviderはBOSH Directorに設定されたTrusted Certificatesを自動でKeystoreに追加してくれます。これを利用すると各アプリケーションに対して設定する必要がありません。
Javaに依らず、BOSH Trusted Certificatesはコンテナ内の
/etc/ssl/certs/ca-certificates.crt
に含まれます。
Pivotal Cloud Foundryの場合は、"Ops Manager Director" Tileの"Security"タブでBOSH Trusted Certificatesを設定可能です。
http://docs.pivotal.io/pivotalcf/1-12/customizing/cloudform-om-config.html#security
この設定を行った場合は、Java BuildpackでデプロイしたアプリのKeystoreに自動でCA証明書が追加されます。
MySQL for PCFは記事執筆時点で暗号化通信未対応😨です...(現在対応中) Pivotal Cloud Foundryユーザーは、代替手段としてIPsec Add-onを使って、通信の暗号化することができます😌
本記事で紹介したCA証明書の追加方法はMySQLに限った話ではありません。HTTPや他のTCP通信で同じです。
こちらのKnowledge Baseが役に立ちました。