IK.AM

@making's tech note


Spring Boot on Cloud FoundryからAmazon RDS / Cloud SQLへの通信を暗号化する


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の場合

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のパスとパスワードを設定します。

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

"Download server-ca.pem"をクリックし、you-appディレクトリ(アプリケーションプロジェクトのルートディレクトリ)に保存してください。

image.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のパスとパスワードを設定します。

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を設定可能です。

image.png

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が役に立ちました。


✒️️ Edit  ⏰ History  🗑 Delete