urllib3 や requests で使用する Cipher Suite を指定する
はじめに
こんにちは、 yosida95 です。
今日は祝日でぼくも休暇でしたが、先日リリースした Gehirn Infrastructure Services でサーバーエラーが発生した旨 Sentry から通知が届き、その修正のデプロイを試みたところ、デプロイスクリプトが依存する requests で requests.exceptions.SSLError 例外が発生してデプロイに失敗したため、この原因と回避方法を調べていました。
トレースバックは以下です。
Traceback (most recent call last):
File "/Users/yosida95/src/DNS/debug/lib/python2.7/site-packages/fabric/main.py", line 743, in main
*args, **kwargs
File "/Users/yosida95/src/DNS/debug/lib/python2.7/site-packages/fabric/tasks.py", line 384, in execute
multiprocessing
File "/Users/yosida95/src/DNS/debug/lib/python2.7/site-packages/fabric/tasks.py", line 274, in _execute
return task.run(*args, **kwargs)
File "/Users/yosida95/src/DNS/debug/lib/python2.7/site-packages/fabric/tasks.py", line 174, in run
return self.wrapped(*args, **kwargs)
File "/Users/yosida95/src/DNS/deploy/fabfile.py", line 166, in upload
source = package()
File "/Users/yosida95/src/DNS/deploy/fabfile.py", line 147, in package
dict(loglevel=env.loglevel))
File "/Users/yosida95/src/DNS/deploy/fabfile.py", line 100, in render_template
auth=env.kvs_registry_auth)
File "/Users/yosida95/src/DNS/debug/lib/python2.7/site-packages/requests/sessions.py", line 477, in get
return self.request('GET', url, **kwargs)
File "/Users/yosida95/src/DNS/debug/lib/python2.7/site-packages/requests/sessions.py", line 465, in request
resp = self.send(prep, **send_kwargs)
File "/Users/yosida95/src/DNS/debug/lib/python2.7/site-packages/requests/sessions.py", line 573, in send
r = adapter.send(request, **kwargs)
File "/Users/yosida95/src/DNS/debug/lib/python2.7/site-packages/requests/adapters.py", line 431, in send
raise SSLError(e, request=request)
requests.exceptions.SSLError: [Errno 1] _ssl.c:510: error:14077410:SSL routines:SSL23_GET_SERVER_HELLO:sslv3 alert handshake failure
発生原因
大雑把にいうとクライアントにインストールされている OpenSSL のバージョンが古いと発生します。
urllib3 は Cipher として
ECDH+AESGCM:DH+AESGCM:ECDH+AES256:DH+AES256:ECDH+AES128:DH+AES:ECDH+HIGH:DH+HIGH:ECDH+3DES:DH+3DES:RSA+AESGCM:RSA+AES:RSA+HIGH:RSA+3DES:!aNULL:!eNULL:!MD5
を使っています(該当ソースコード)。
このリストに対して Mac OS X にデフォルトでインストールされている OpenSSL 0.9.8zd と、最新の OpenSSL 1.0.2a ではサポートする Cipher Suite にこれだけの差があります。
*** openssl0.9.8zd.txt 2015-04-29 19:16:26.000000000 +0900
--- openssl1.0.2a-1.txt 2015-04-29 19:17:01.000000000 +0900
***************
*** 1,9 ****
--- 1,73 ----
+ AES128-GCM-SHA256 TLSv1.2 Kx=RSA Au=RSA Enc=AESGCM(128) Mac=AEAD
AES128-SHA SSLv3 Kx=RSA Au=RSA Enc=AES(128) Mac=SHA1
+ AES128-SHA256 TLSv1.2 Kx=RSA Au=RSA Enc=AES(128) Mac=SHA256
+ AES256-GCM-SHA384 TLSv1.2 Kx=RSA Au=RSA Enc=AESGCM(256) Mac=AEAD
AES256-SHA SSLv3 Kx=RSA Au=RSA Enc=AES(256) Mac=SHA1
+ AES256-SHA256 TLSv1.2 Kx=RSA Au=RSA Enc=AES(256) Mac=SHA256
+ CAMELLIA128-SHA SSLv3 Kx=RSA Au=RSA Enc=Camellia(128) Mac=SHA1
+ CAMELLIA256-SHA SSLv3 Kx=RSA Au=RSA Enc=Camellia(256) Mac=SHA1
DES-CBC3-SHA SSLv3 Kx=RSA Au=RSA Enc=3DES(168) Mac=SHA1
+ DH-DSS-AES128-GCM-SHA256 TLSv1.2 Kx=DH/DSS Au=DH Enc=AESGCM(128) Mac=AEAD
+ DH-DSS-AES128-SHA SSLv3 Kx=DH/DSS Au=DH Enc=AES(128) Mac=SHA1
+ DH-DSS-AES128-SHA256 TLSv1.2 Kx=DH/DSS Au=DH Enc=AES(128) Mac=SHA256
+ DH-DSS-AES256-GCM-SHA384 TLSv1.2 Kx=DH/DSS Au=DH Enc=AESGCM(256) Mac=AEAD
+ DH-DSS-AES256-SHA SSLv3 Kx=DH/DSS Au=DH Enc=AES(256) Mac=SHA1
+ DH-DSS-AES256-SHA256 TLSv1.2 Kx=DH/DSS Au=DH Enc=AES(256) Mac=SHA256
+ DH-DSS-CAMELLIA128-SHA SSLv3 Kx=DH/DSS Au=DH Enc=Camellia(128) Mac=SHA1
+ DH-DSS-CAMELLIA256-SHA SSLv3 Kx=DH/DSS Au=DH Enc=Camellia(256) Mac=SHA1
+ DH-DSS-DES-CBC3-SHA SSLv3 Kx=DH/DSS Au=DH Enc=3DES(168) Mac=SHA1
+ DH-RSA-AES128-GCM-SHA256 TLSv1.2 Kx=DH/RSA Au=DH Enc=AESGCM(128) Mac=AEAD
+ DH-RSA-AES128-SHA SSLv3 Kx=DH/RSA Au=DH Enc=AES(128) Mac=SHA1
+ DH-RSA-AES128-SHA256 TLSv1.2 Kx=DH/RSA Au=DH Enc=AES(128) Mac=SHA256
+ DH-RSA-AES256-GCM-SHA384 TLSv1.2 Kx=DH/RSA Au=DH Enc=AESGCM(256) Mac=AEAD
+ DH-RSA-AES256-SHA SSLv3 Kx=DH/RSA Au=DH Enc=AES(256) Mac=SHA1
+ DH-RSA-AES256-SHA256 TLSv1.2 Kx=DH/RSA Au=DH Enc=AES(256) Mac=SHA256
+ DH-RSA-CAMELLIA128-SHA SSLv3 Kx=DH/RSA Au=DH Enc=Camellia(128) Mac=SHA1
+ DH-RSA-CAMELLIA256-SHA SSLv3 Kx=DH/RSA Au=DH Enc=Camellia(256) Mac=SHA1
+ DH-RSA-DES-CBC3-SHA SSLv3 Kx=DH/RSA Au=DH Enc=3DES(168) Mac=SHA1
+ DHE-DSS-AES128-GCM-SHA256 TLSv1.2 Kx=DH Au=DSS Enc=AESGCM(128) Mac=AEAD
DHE-DSS-AES128-SHA SSLv3 Kx=DH Au=DSS Enc=AES(128) Mac=SHA1
+ DHE-DSS-AES128-SHA256 TLSv1.2 Kx=DH Au=DSS Enc=AES(128) Mac=SHA256
+ DHE-DSS-AES256-GCM-SHA384 TLSv1.2 Kx=DH Au=DSS Enc=AESGCM(256) Mac=AEAD
DHE-DSS-AES256-SHA SSLv3 Kx=DH Au=DSS Enc=AES(256) Mac=SHA1
+ DHE-DSS-AES256-SHA256 TLSv1.2 Kx=DH Au=DSS Enc=AES(256) Mac=SHA256
+ DHE-DSS-CAMELLIA128-SHA SSLv3 Kx=DH Au=DSS Enc=Camellia(128) Mac=SHA1
+ DHE-DSS-CAMELLIA256-SHA SSLv3 Kx=DH Au=DSS Enc=Camellia(256) Mac=SHA1
+ DHE-RSA-AES128-GCM-SHA256 TLSv1.2 Kx=DH Au=RSA Enc=AESGCM(128) Mac=AEAD
DHE-RSA-AES128-SHA SSLv3 Kx=DH Au=RSA Enc=AES(128) Mac=SHA1
+ DHE-RSA-AES128-SHA256 TLSv1.2 Kx=DH Au=RSA Enc=AES(128) Mac=SHA256
+ DHE-RSA-AES256-GCM-SHA384 TLSv1.2 Kx=DH Au=RSA Enc=AESGCM(256) Mac=AEAD
DHE-RSA-AES256-SHA SSLv3 Kx=DH Au=RSA Enc=AES(256) Mac=SHA1
+ DHE-RSA-AES256-SHA256 TLSv1.2 Kx=DH Au=RSA Enc=AES(256) Mac=SHA256
+ DHE-RSA-CAMELLIA128-SHA SSLv3 Kx=DH Au=RSA Enc=Camellia(128) Mac=SHA1
+ DHE-RSA-CAMELLIA256-SHA SSLv3 Kx=DH Au=RSA Enc=Camellia(256) Mac=SHA1
+ ECDH-ECDSA-AES128-GCM-SHA256 TLSv1.2 Kx=ECDH/ECDSA Au=ECDH Enc=AESGCM(128) Mac=AEAD
+ ECDH-ECDSA-AES128-SHA SSLv3 Kx=ECDH/ECDSA Au=ECDH Enc=AES(128) Mac=SHA1
+ ECDH-ECDSA-AES128-SHA256 TLSv1.2 Kx=ECDH/ECDSA Au=ECDH Enc=AES(128) Mac=SHA256
+ ECDH-ECDSA-AES256-GCM-SHA384 TLSv1.2 Kx=ECDH/ECDSA Au=ECDH Enc=AESGCM(256) Mac=AEAD
+ ECDH-ECDSA-AES256-SHA SSLv3 Kx=ECDH/ECDSA Au=ECDH Enc=AES(256) Mac=SHA1
+ ECDH-ECDSA-AES256-SHA384 TLSv1.2 Kx=ECDH/ECDSA Au=ECDH Enc=AES(256) Mac=SHA384
+ ECDH-ECDSA-DES-CBC3-SHA SSLv3 Kx=ECDH/ECDSA Au=ECDH Enc=3DES(168) Mac=SHA1
+ ECDH-RSA-AES128-GCM-SHA256 TLSv1.2 Kx=ECDH/RSA Au=ECDH Enc=AESGCM(128) Mac=AEAD
+ ECDH-RSA-AES128-SHA SSLv3 Kx=ECDH/RSA Au=ECDH Enc=AES(128) Mac=SHA1
+ ECDH-RSA-AES128-SHA256 TLSv1.2 Kx=ECDH/RSA Au=ECDH Enc=AES(128) Mac=SHA256
+ ECDH-RSA-AES256-GCM-SHA384 TLSv1.2 Kx=ECDH/RSA Au=ECDH Enc=AESGCM(256) Mac=AEAD
+ ECDH-RSA-AES256-SHA SSLv3 Kx=ECDH/RSA Au=ECDH Enc=AES(256) Mac=SHA1
+ ECDH-RSA-AES256-SHA384 TLSv1.2 Kx=ECDH/RSA Au=ECDH Enc=AES(256) Mac=SHA384
+ ECDH-RSA-DES-CBC3-SHA SSLv3 Kx=ECDH/RSA Au=ECDH Enc=3DES(168) Mac=SHA1
+ ECDHE-ECDSA-AES128-GCM-SHA256 TLSv1.2 Kx=ECDH Au=ECDSA Enc=AESGCM(128) Mac=AEAD
+ ECDHE-ECDSA-AES128-SHA SSLv3 Kx=ECDH Au=ECDSA Enc=AES(128) Mac=SHA1
+ ECDHE-ECDSA-AES128-SHA256 TLSv1.2 Kx=ECDH Au=ECDSA Enc=AES(128) Mac=SHA256
+ ECDHE-ECDSA-AES256-GCM-SHA384 TLSv1.2 Kx=ECDH Au=ECDSA Enc=AESGCM(256) Mac=AEAD
+ ECDHE-ECDSA-AES256-SHA SSLv3 Kx=ECDH Au=ECDSA Enc=AES(256) Mac=SHA1
+ ECDHE-ECDSA-AES256-SHA384 TLSv1.2 Kx=ECDH Au=ECDSA Enc=AES(256) Mac=SHA384
+ ECDHE-ECDSA-DES-CBC3-SHA SSLv3 Kx=ECDH Au=ECDSA Enc=3DES(168) Mac=SHA1
+ ECDHE-RSA-AES128-GCM-SHA256 TLSv1.2 Kx=ECDH Au=RSA Enc=AESGCM(128) Mac=AEAD
+ ECDHE-RSA-AES128-SHA SSLv3 Kx=ECDH Au=RSA Enc=AES(128) Mac=SHA1
+ ECDHE-RSA-AES128-SHA256 TLSv1.2 Kx=ECDH Au=RSA Enc=AES(128) Mac=SHA256
+ ECDHE-RSA-AES256-GCM-SHA384 TLSv1.2 Kx=ECDH Au=RSA Enc=AESGCM(256) Mac=AEAD
+ ECDHE-RSA-AES256-SHA SSLv3 Kx=ECDH Au=RSA Enc=AES(256) Mac=SHA1
+ ECDHE-RSA-AES256-SHA384 TLSv1.2 Kx=ECDH Au=RSA Enc=AES(256) Mac=SHA384
+ ECDHE-RSA-DES-CBC3-SHA SSLv3 Kx=ECDH Au=RSA Enc=3DES(168) Mac=SHA1
EDH-DSS-DES-CBC3-SHA SSLv3 Kx=DH Au=DSS Enc=3DES(168) Mac=SHA1
EDH-RSA-DES-CBC3-SHA SSLv3 Kx=DH Au=RSA Enc=3DES(168) Mac=SHA1
このため、クライアントにインストールされている OpenSSL のバージョンが古くサポートしている Cipher が少ない状態で、加えてサーバーでは古い Cipher や脆弱な Cipher のサポートが切られている場合、サーバーとクライアントが共通でサポートしている Cipher がひとつもなくなり、ハンドシェイクに失敗する場合があるのです。
回避方法
urllib3 では urllib3.util.ssl_.DEFAULT_CIPHERS を書き換えてやることで利用する Cipher を指定することができます。 requests を使っている場合は、 requests.packages.urllib3.util.ssl_.DEFAULT_CIPHERS です。
この値を
ALL:!aNULL:!SSLv2:!LOW:!EXP:!MD5
など、サーバーがサポートする Cipher が含まれるような無難な値にすることで正常にハンドシェイクできるようになります。
ただし、上記の方法は Python 標準の ssl モジュールを使っている場合で、 ssl モジュールの代わりに pyOpenSSL を利用している場合は、 ドキュメントで説明されているように、 requests.packages.urllib3.contrib.pyopenssl.DEFAULT_SSL_CIPHER_LIST を変更する必要があります。
なお、 requests を使っている場合は、 pyOpenSSL がインストールされていると勝手に pyOpenSSL を使ってくる ので注意してください。
おわりに
気づいたら祝日も終わりに差し掛かっています。 明日からは宮崎出張です。 ぼくの休日とは一体……
ありがとうございました。