JDBCでSSLを有効にした複数のMySQLインスタンスに接続する


この記事は

Speee Advent Calendar 2018の8日目の記事です!
よければ購読お願いします。

昨日は@selmertsxさんのAWS re:Invent 2018に行ってきました!でした。

今日はJDBCでSSLを有効にした複数のMySQLインスタンスに接続するです。
この記事を読めば、JDBCで複数のMySQLインスタンスにSSLで接続する際に頭を悩まさなくてすむかもしれません。

はじめに

以前、以下の図の様な形で、マスターとレプリカの二つのMySQLサーバをGCPのCloud SQLに移行しました。

その時に、これまでやれていなかったSSLを有効にしたところRuby(Rails)では簡単に切り替えられたのですが、Javaではすぐにはできませんでした。

この記事は、その時の解決できた方法のメモです。

いきなり余談

せっかくなので、Railsでの接続方法を。
Railsの場合、database.yamlにSSL用の設定を追加するだけです。
Cloud SQLで証明書を発行し、適切な場所に配置してしまうのが良さそうです。
以下の様な形で設定してみました(抜粋)。

database.yaml
default: &default
    adapter: mysql2
    ...

  ssl: &ssl
    sslca: <%= ENV.fetch("HOGE_DATABASE_SSL_SERVER_CA") %>
    sslcert: <%= ENV.fetch("HOGE_DATABASE_SSL_CLIENT_CERT") %>
    sslkey: <%= ENV.fetch("HOGE_DATABASE_SSL_CLIENT_KEY") %>

  development:
    <<: *default
    database: hoge_development
    ...

  test:
    <<: *default
    database: hoge_test
    ...

  staging:
    <<: *default
    <<: *ssl
    database: hoge_staging
    ...

  production:
    <<: *default
    <<: *ssl
    database: hoge_production
    ...

今回の切り替え作業で接続先を切り替えたRailsアプリケーションは、複数DBに接続するために、読み込むdatabase.yamlを変更してあるベースモデルをDB毎につくっていたので、それぞれのdatabase.yamlに追加するだけで、SSL化ができました。

簡単ですね。

本題

Javaの場合、Railsに比べるとちょっと面倒です。
前提として、接続先毎にJDBCの設定があるものとします。

下準備

Javaの場合、Railsのように3つのpemをそのままの形で利用することができません。
Java Key Store形式(以下JKS)の入れ物にインポートする必要があります。
また、秘密鍵をインポートする場合は一度opensshコマンドでPKCS12形式にしてあげないと、JKSにインポートできません。

インポートは以下のように行います。
これを、接続するMySQLインスタンス毎にファイルを分けて作ります。

$ keytool -import -file ./server-ca.pem -alias <なにか識別名> -keystore <入れ物のファイル名>
$ openssl pkcs12 -export -inkey ./client-key.pem -in ./client-cert.pem -name <なにか識別名> -out ./temp.p12
$ keytool -importkeystore -srckeystore ./temp.p12 -srcstoretype pkcs12 -destkeystore <入れ物のファイル名>
  • 初めてJava Key Store形式の入れ物を作る場合は指定したファイル名で新規作成されます。
  • keytoolはJDKなどに付属してくるツールで、もしかしたらパスが通ってないこともあるかもしれません。JDKのインストールパスの下のbinに入っているようです。
  • コマンドを叩くと、都度都度パスワードを要求されます。これは、JKS形式の入れ物のパスワードで、ユーザのパスワードとかとは関係ありません。パス付きZipみたいな感じです。

接続の設定

JKSを各DB毎に用意したら、JDBCの接続URLに、設定を追加します。
以下の6つを指定します。

  • clientCertificateKeyStoreUrl
    JKSのファイルパスを指定します。
  • clientCertificateKeyStoreType
    JKSを指定します。
  • clientCertificateKeyStorePassword
    JKSを作った時に入力したパスワードを指定します。
  • trustCertificateKeyStoreUrl
    JKSのファイルパスを指定します。
    clientCertificateKeyStoreUrlと同じもので良さそうです。
  • trustCertificateKeyStoreType
    JKSを指定します。
  • trustCertificateKeyStorePassword
    JKSを作った時に入力したパスワードを指定します。

以下の例は、IP: www.xxx.yyy.zzz のレプリカ用MySQLインスタンスにMySQLのJDBCドライバーで接続する際の接続URLです。

jdbc:mysql://www.xxx.yyy.zzz:3306/hoge?useSSL=true&amp;requireSSL=true&amp;clientCertificateKeyStoreUrl=file:///path/to/secrets/hoge_replica.jks&amp;clientCertificateKeyStoreType=JKS&amp;clientCertificateKeyStorePassword=<設定したパスワード&amp;trustCertificateKeyStoreUrl=file:///path/to/secrets/hoge_replica.jks&amp;trustCertificateKeyStoreType=JKS&amp;trustCertificateKeyStorePassword=<設定したパスワード>"

マスター用にする場合は、 JKSのファイルパスと、パスワードをマスター用のものに切り替えるだけです。
これで、SSLを有効にした複数のMySQLインスタンスに接続できる様になりました。

余談

JavaでSSLを有効にしたMySQLインスタンスに接続する方法として、Javaの実行時にオプションで指定するという方法がありました。
当初、それに従ってマスターとレプリカの証明書や鍵を一つのJKSにまとめて試してみたのですが、なぜか正しくプログラムが動かず、困ってしまっていました。

いろいろ調べてみたところ、どうやらJavaのバグ?仕様?で、一つのJKSに発行元(CloudSQLの場合はGoogleになっている)が同じ証明書を複数入れてしまった場合に、DBの接続先毎に適切に選択してくれないようです。
なので、たとえばレプリカにマスターの証明書で接続しようとして、 SSLのコネクションが切られるといった現象が発生してしまうみたいです。

おわりに

実際にやったのはCloudSQLのMySQLインスタンスだったので、文中ではMySQLとしていましたが、JavaでJDBCを使ってSSL接続をする場合には応用が効くんじゃないかなとおもっています。