急に外部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をアップグレードしたためにエラーが出る、という情報が出てきます。

bugs.debian.org

そこで、環境ごとの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を確認すると……

www.openssl.org

*) 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はこれになります。

github.com

実際にQualys SSL Labs で、問題のAPIで使用されているDH鍵の鍵長を確認してみたところ、1024 bitsでした。

f:id:yu_suke1994:20190729163041p:plain
WEAK と警告されています

なぜ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日にリリースされていたことをすっかり忘れていました。

www.debian.org

そして、Dockerflieに指定していたFROM は、 FROM ruby:2.6.3 です。

Docker Hubを確認しに行くと……

hub.docker.com

無印の 2.6.3 は busterがbaseに使用されるようになっています!

f:id:yu_suke1994:20190729163238p:plain

github.com

このPull Requestによると、busterが使われるようになったのは7月11日からのようですね。

原因

つまり、原因をまとめると……

  1. 対象のAPIがDH鍵の鍵長として1024 bitsを使用している
  2. OpenSSL 1.1.1 から DH鍵の鍵長はデフォルトで2048 bitsとなった
  3. OpenSSL 1.1.1系 がインストールされるようになったDebian busterがリリースされた
  4. Docker公式のRubyイメージでbusterをbase imageとして使うようになった

という流れによって、今回の問題が発生したことになります。

DH鍵の鍵長における問題点

そもそも、なぜOpenSSL 1.1.1 から DH鍵の鍵長はデフォルトで2048 bitsとなったのでしょうか?

詳しく調べると、DH鍵交換には "Logjam Attack" と呼ばれる脆弱性があり、発表時点で鍵長として 512 bitsを使用するのをやめ1024 bits 以上、可能なら2048 bits以上を使用することが対策として提示されています。

weakdh.org

piyolog.hatenadiary.jp

今回のデフォルトの鍵長を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.comhttps://dh1024.badssl.com/ を提供しています。皆さんも、 buildpack-deps:stretchbuildpack-deps:buster などの Docker image で curl -v https://dh1024.badssl.com/ の結果を見てみてはいかがでしょうか。

badssl.com

2019-08-20 修正

Ruby公式Dockerイメージ」という記述をしていましたが、Dockerによる公式Rubyイメージであり、事実と異なるので記述を修正しました。@knuさん、ご指摘ありがとうございます。

また、追記の情報をより詳細にし、誤字の修正しました。@whywaitaさん、@orisanoさんありがとうございます。


  1. 2011年以降、認証局で安全のためRSAの鍵長に2048 bitsを要求するのが一般的であるためとのこと https://github.com/openssl/openssl/issues/8737 https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-131a.pdf