--- title: Spring Boot on Cloud FoundryからAmazon RDS / Cloud SQLへの通信を暗号化する tags: ["Cloud Foundry", "Pivotal Web Services", "Pivotal Cloud Foundry", "Spring Boot", "Java", "TLS", "Amazon RDS", "Cloud SQL"] categories: ["Programming", "Java", "org", "springframework", "boot"] date: 2017-11-06T15:05:05Z updated: 2017-12-18T17:58:23Z --- Amazon RDSやCloud SQLといったCloud Foundry外部に存在するMySQLにアクセスする場合は通信を暗号化するのが良いです。 例えば、次のように`manifest.yml`にRDSのTLS通信を有効にした接続情報を環境変数に設定し、`cf push`してみると、、 ``` yml 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の場合 [http://docs.aws.amazon.com/ja_jp/AmazonRDS/latest/UserGuide/UsingWithRDS.SSL.html#UsingWithRDS.SSL.IntermediateCertificates](http://docs.aws.amazon.com/ja_jp/AmazonRDS/latest/UserGuide/UsingWithRDS.SSL.html#UsingWithRDS.SSL.IntermediateCertificates) からリージョンに合った中間証明書をダウンロードしてください。 `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のパスとパスワードを設定します。 ``` yml 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"をクリックし、 ![image.png](https://qiita-image-store.s3.amazonaws.com/0/1852/bc44a476-f593-0f90-3d68-aaaa964dfe67.png) "Download server-ca.pem"をクリックし、`you-app`ディレクトリ(アプリケーションプロジェクトのルートディレクトリ)に保存してください。 ![image.png](https://qiita-image-store.s3.amazonaws.com/0/1852/a738b007-4425-eac1-57fb-a69bee2f177d.png) 次のコマンドで`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のパスとパスワードを設定します。 ``` yml 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コードで実現可能です。 ``` 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](https://github.com/making/certificate-importer)と言うライブラリを作成したので、より簡単にCA証明書をKeystoreに追加可能です。 使い方は`pom.xml`に次の依存ライブラリを追加し、 ``` xml am.ik.certificate certificate-importer 0.0.1 ``` Spring Bootアプリの場合は、環境変数`CA_CERTS`またはプロパティ`ca.certs`に追加したいCA証明書をPEM形式で設定するだけです。 `application-cloud.yml`に次のように設定するのが便利でしょう。 ``` 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可能です。 ``` java 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](https://github.com/cloudfoundry/java-buildpack/blob/master/docs/framework-container_security_provider.md)という仕組みがあり、Container Security ProviderはBOSH Directorに設定された[Trusted Certificates](https://bosh.io/docs/trusted-certs.html)を自動でKeystoreに追加してくれます。これを利用すると各アプリケーションに対して設定する必要がありません。 > Javaに依らず、BOSH Trusted Certificatesはコンテナ内の`/etc/ssl/certs/ca-certificates.crt`に含まれます。 Pivotal Cloud Foundryの場合は、"Ops Manager Director" Tileの"Security"タブでBOSH Trusted Certificatesを設定可能です。 ![image.png](https://qiita-image-store.s3.amazonaws.com/0/1852/931ff871-dacc-8494-3b27-8514e9c7e57a.png) http://docs.pivotal.io/pivotalcf/1-12/customizing/cloudform-om-config.html#security この設定を行った場合は、Java BuildpackでデプロイしたアプリのKeystoreに自動でCA証明書が追加されます。 --- > [MySQL for PCF](http://docs.pivotal.io/p-mysql/)は記事執筆時点で暗号化通信未対応😨です...(現在対応中) > Pivotal Cloud Foundryユーザーは、代替手段として[IPsec Add-on](http://docs.pivotal.io/addon-ipsec/)を使って、通信の暗号化することができます😌 本記事で紹介したCA証明書の追加方法はMySQLに限った話ではありません。HTTPや他のTCP通信で同じです。 こちらの[Knowledge Base](https://discuss.pivotal.io/hc/en-us/articles/223454928-How-to-tell-application-containers-running-Java-apps-to-trust-self-signed-certs-or-a-private-or-internal-CA)が役に立ちました。