急に外部APIとの通信が "dh key too small" で失敗するようになったのはなぜ?
こんにちは。最近TRAVEL Nowの開発にも顔を出すようになったうなすけです。今回はTRAVEL Nowの開発において発生した問題について書こうと思います。
外部API連携部分で突然のエラー
TRAVEL Nowでは、外部のOTAと連携し、旅行商品を皆さんに提供しています。 そんな数多くのAPIのうち、ある特定のAPIで次のような例外が発生して通信ができなくなってしまいました。
OpenSSL::SSL::SSLError (SSL_connect returned=1 errno=0 state=error: dh key too small)
それも、本番環境でのみ発生します。
始めはこのエラーについてよく理解しておらず、 http.verify_mode = OpenSSL::SSL::VERIFY_NONE
を指定してみたり、 apt install ca-certificates
をしてみたりしたのですが、エラーの解決には至りませんでした。
翌朝、詳しく原因調査
翌日、腰を据えて調査にかかりました。
まず、 curlの結果を見てみます。
$ curl -v https://example.com # 実際は異なるURL # ...snip... * TLSv1.3 (OUT), TLS handshake, Client hello (1): * TLSv1.3 (IN), TLS handshake, Server hello (2): * TLSv1.2 (IN), TLS handshake, Certificate (11): * TLSv1.2 (IN), TLS handshake, Server key exchange (12): * TLSv1.2 (OUT), TLS alert, handshake failure (552): * error:141A318A:SSL routines:tls_process_ske_dhe:dh key too small * Closing connection 0 curl: (35) error:141A318A:SSL routines:tls_process_ske_dhe:dh key too small
production環境では、上記のように失敗してしまいます。しかしstaging環境では……
$ curl -v https://example.com/ # 実際は異なるURL # ...snip... > GET / HTTP/1.1 > Host: dh1024.badssl.com > User-Agent: curl/7.52.1 > Accept: */* > < HTTP/1.1 200 OK < Server: nginx/1.10.3 (Ubuntu) < Date: Mon, 29 Jul 2019 06:08:23 GMT < Content-Type: text/html < Content-Length: 568 < Last-Modified: Thu, 11 Jul 2019 17:37:21 GMT < Connection: keep-alive < ETag: "5d2773d1-238" < Cache-Control: no-store < Accept-Ranges: bytes < # ...snip...
成功してしまいます。
OpenSSLのバージョンが環境ごとに違う?
さて、 "dh key too small" というエラーメッセージで検索をすると、OpenSSLをアップグレードしたためにエラーが出る、という情報が出てきます。
そこで、環境ごとのOpenSSLのバージョンを確認してみます。
# staging $ openssl version OpenSSL 1.1.0j 20 Nov 2018
# production $ openssl version OpenSSL 1.1.1c 28 May 2019
このように、production環境ではstaging環境よりも新しいOpenSSLが使用されていることがわかりました。
なぜこうなっているのかはさておき、このバージョン間でOpenSSLに入った変更を確認してみます。
OpenSSL 1.1.1 からデフォルトの鍵長が変更された
OpenSSL 1.1.1 でのCHANGELOGを確認すると……
*) Change the default RSA, DSA and DH size to 2048 bit instead of 1024. This changes the size when using the genpkey app when no size is given. It fixes an omission in earlier changes that changed all RSA, DSA and DH generation apps to use 2048 bits by default. [Kurt Roeckx]
という記述が見付かりました。
commitはこれになります。
実際にQualys SSL Labs で、問題のAPIで使用されているDH鍵の鍵長を確認してみたところ、1024 bitsでした。
なぜOpenSSLのバージョンが上がってしまったのか?
DH鍵の強度が不足しているために通信ができないことが判明したのはいいですが、ではなぜOpenSSLのバージョンが突然変化したのでしょう?
TRAVEL Now では、サーバーの実行環境はDockerによってコンテナ化されています。だからこそ、stagingとproductionでOpenSSLのバージョンが異なるという事態は不可解です。
調査のため、 debian openssl 1.1.1
で調べ、パッケージ情報を見てハッと気付きました。
Debian -- Details of package openssl in buster
「buster……?」
そう、Debianの最新安定版のbusterが、2019年7月6日にリリースされていたことをすっかり忘れていました。
そして、Dockerflieに指定していたFROM
は、 FROM ruby:2.6.3
です。
Docker Hubを確認しに行くと……
無印の 2.6.3
は busterがbaseに使用されるようになっています!
このPull Requestによると、busterが使われるようになったのは7月11日からのようですね。
原因
つまり、原因をまとめると……
- 対象のAPIがDH鍵の鍵長として1024 bitsを使用している
- OpenSSL 1.1.1 から DH鍵の鍵長はデフォルトで2048 bitsとなった
- OpenSSL 1.1.1系 がインストールされるようになったDebian busterがリリースされた
- Docker公式のRubyイメージでbusterをbase imageとして使うようになった
という流れによって、今回の問題が発生したことになります。
DH鍵の鍵長における問題点
そもそも、なぜOpenSSL 1.1.1 から DH鍵の鍵長はデフォルトで2048 bitsとなったのでしょうか?
詳しく調べると、DH鍵交換には "Logjam Attack" と呼ばれる脆弱性があり、発表時点で鍵長として 512 bitsを使用するのをやめ1024 bits 以上、可能なら2048 bits以上を使用することが対策として提示されています。
今回のデフォルトの鍵長を2048 bitsにする修正は、直接この脆弱性と関係がある訳ではなさそう1ですが、デフォルトの状態でより安全になったということになるのでしょうか。
対応
原因から、対応としては stretch を使用すればよさそうです。
$ docker run -it --rm buildpack-deps:stretch bash root@3ef99d19ca47:/# openssl version OpenSSL 1.1.0k 28 May 2019
よって、以下の1行の変更で、こちら側の対応は完了です。
-FROM ruby:2.6.3 +FROM ruby:2.6.3-stretch
また、APIを提供しているサービス運用事業者にも、上記のような調査結果、対応方法をまとめて連絡しました。先方の対応が完了し次第、再度buster baseのイメージを使用するよう変更するつもりです。
まとめ
- Docker化しているからといって、実行環境が完全に固定されているという思い込みは危険
- Docker imageのtagはなるべく詳細になるよう記述する
2.6
よりも2.6-buster
など
ちなみに、同様の挙動をするURLとして、https://badssl.com が https://dh1024.badssl.com/ を提供しています。皆さんも、 buildpack-deps:stretch
や buildpack-deps:buster
などの Docker image で curl -v https://dh1024.badssl.com/
の結果を見てみてはいかがでしょうか。
2019-08-20 修正
「Ruby公式Dockerイメージ」という記述をしていましたが、Dockerによる公式Rubyイメージであり、事実と異なるので記述を修正しました。@knuさん、ご指摘ありがとうございます。
また、追記の情報をより詳細にし、誤字の修正しました。@whywaitaさん、@orisanoさんありがとうございます。
-
2011年以降、認証局で安全のためRSAの鍵長に2048 bitsを要求するのが一般的であるためとのこと https://github.com/openssl/openssl/issues/8737 https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-131a.pdf↩