Yappli Tech Blog

株式会社ヤプリの開発メンバーによるブログです。最新の技術情報からチーム・働き方に関するテーマまで、日々の熱い想いを持って発信していきます。

SSL通信がエラーになったので調べてみた

どうも、株式会社ヤプリのサバゲー部所属のわたなべです。
先日、夏に提携したばかりのフラーさんも含めてサバゲー部の活動をしてきて、久しぶりの筋肉痛になりました。
もっと鍛えねば、と思いました。
さて、今回はSSL通信がエラーになって、いろいろ調べて大変だった話をしたいと思います。

ある日エラーは突然に…

とある日、いつものように仕事をサボっていたちゃんとしていた私は、とあるお客様のサーバーでエラーが出ているアラートに気がつきました。
まあ、お客様側のサーバーが落ちてるのかな?と思いながら、エラーログを調べてみるとタイムアウトや500系エラーではなくSSL証明書のエラーががが

SSL certificate problem: certificate has expired

おおっと!
とはいえ、お客様側の問題で証明書の期限が切れているだけなので、連絡して新しい証明書を配置してもらって終わり、のはずでしたが…

新しい証明書を配置してもエラーが出ちゃう?

新しい期限が有効な証明書を配置してもらうと、エラー内容は変わりましたが、いまだにエラーのまま…

SSL certificate problem: unable to get local issuer certificate

これはまずいですね…
ということで、いろいろ調査を始めました。

まずは状況の再現から

まずは、何がなんだかわからないので、curlコマンドをローカルのPCから打ってみました。

$ curl -v https://<お客様のドメイン>/ 2>&1 | grep 'HTTP/1.1'
> GET / HTTP/1.1
< HTTP/1.1 200 OK

ん?ちゃんと200が返ってくる?
ちゃんと動いてるじゃん。なんでエラーが出てるの?
まあ、待て。まだ慌てる時じゃない。

ということで、エラーログが出ているサーバーで同じコマンドを打ってみると…

$ curl -v https://<お客様のドメイン>/ 
*   Trying xxx.xxx.xxx.xxx (xxx.xxx.xxx.xxx) port 443
* ALPN: curl offers h2,http/1.1
* Cipher selection: ALL:!EXPORT:!EXPORT40:!EXPORT56:!aNULL:!LOW:!RC4:@STRENGTH
* TLSv1.2 (OUT), TLS handshake, Client hello (1):
*  CAfile: /etc/pki/tls/certs/ca-bundle.crt
*  CApath: none
* TLSv1.2 (IN), TLS handshake, Server hello (2):
* TLSv1.2 (IN), TLS handshake, Certificate (11):
* TLSv1.2 (OUT), TLS alert, unknown CA (560):
* SSL certificate problem: unable to get local issuer certificate
* Closing connection
curl: (60) SSL certificate problem: unable to get local issuer certificate
More details here: https://curl.se/docs/sslcerts.html

curl failed to verify the legitimacy of the server and therefore could not
establish a secure connection to it. To learn more about this situation and
how to fix it, please visit the web page mentioned above.

おおう…。まさに今エラーログに出ている「SSL certificate problem: unable to get local issuer certificate」のエラーが出ました。
これはいったいどうしたことだ?
まあ、理由はわかりませんが、エラーの再現はできました。

原因調査してみるも…

とりあえず、出ていたエラーの「SSL certificate problem: unable to get local issuer certificate」をそのままGoogle検索してみるといくつか記事がヒットしました。
しかしながら、「curl -k (該当url) で証明書の検証をスキップする」という根本的な解決ではない内容で、参考になりませんでした…。

そこで、ChatGPT様のお告げを聞いてみたところ、opensslコマンドで確認できるとのこと!

openssl コマンドを使用して、証明書チェーンが正しく構成されているかを確認できます。

openssl s_client -connect <ドメイン名>:443 -showcerts
・正常な出力: 証明書チェーンが正しく表示される。
・エラー: 中間証明書が欠落している、もしくはルート証明書が見つからない可能性がある。

早速やってみたところ、最後が「Verify return code: 21 (unable to verify the first certificate)」って感じの結果が…

# openssl s_client -connect <お客様のドメイン>:443 -showcerts
CONNECTED(00000007)
depth=0 CN = <お客様のドメイン>
verify error:num=20:unable to get local issuer certificate
verify return:1
depth=0 CN = <お客様のドメイン>
verify error:num=21:unable to verify the first certificate
verify return:1
depth=0 CN = <お客様のドメイン>
verify return:1
---
Certificate chain
 0 s:CN = <お客様のドメイン>
   i:C = JP, ST = Tokyo, L = Shibuya-ku, O = Nijimo K.K., CN = FujiSSL SHA2 Domain Secure Site CA
   a:PKEY: rsaEncryption, 2048 (bit); sigalg: RSA-SHA256
   v:NotBefore: Nov 22 00:00:00 2024 GMT; NotAfter: Dec 23 23:59:59 2025 GMT
-----BEGIN CERTIFICATE-----
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
-----END CERTIFICATE-----
---
Server certificate
subject=CN = <お客様のドメイン>
issuer=C = JP, ST = Tokyo, L = Shibuya-ku, O = Nijimo K.K., CN = FujiSSL SHA2 Domain Secure Site CA
---
No client certificate CA names sent
Peer signing digest: SHA256
Peer signature type: RSA
Server Temp Key: ECDH, prime256v1, 256 bits
---
SSL handshake has read xxxx bytes and written xxx bytes
Verification error: unable to verify the first certificate
---
New, TLSv1.2, Cipher is ECDHE-RSA-AES256-GCM-SHA384
Server public key is 2048 bit
Secure Renegotiation IS supported
Compression: NONE
Expansion: NONE
No ALPN negotiated
SSL-Session:
    Protocol  : TLSv1.2
    Cipher    : ECDHE-RSA-AES256-GCM-SHA384
    Session-ID: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
    Session-ID-ctx: 
    Master-Key: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
    PSK identity: None
    PSK identity hint: None
    SRP username: None
    TLS session ticket lifetime hint: 300 (seconds)
    TLS session ticket:
    0000 - xx xx xx xx xx xx xx xx-xx xx xx xx xx xx xx xx   ................
    0010 - xx xx xx xx xx xx xx xx-xx xx xx xx xx xx xx xx   ................
    0020 - xx xx xx xx xx xx xx xx-xx xx xx xx xx xx xx xx   ................
    0030 - xx xx xx xx xx xx xx xx-xx xx xx xx xx xx xx xx   ................
    0040 - xx xx xx xx xx xx xx xx-xx xx xx xx xx xx xx xx   ................
    0050 - xx xx xx xx xx xx xx xx-xx xx xx xx xx xx xx xx   ................
    0060 - xx xx xx xx xx xx xx xx-xx xx xx xx xx xx xx xx   ................
    0070 - xx xx xx xx xx xx xx xx-xx xx xx xx xx xx xx xx   ................
    0080 - xx xx xx xx xx xx xx xx-xx xx xx xx xx xx xx xx   ................
    0090 - xx xx xx xx xx xx xx xx-xx xx xx xx xx xx xx xx   ................
    00a0 - xx xx xx xx xx xx xx xx-xx xx xx xx xx xx xx xx   ................
    00b0 - xx xx xx xx xx xx xx xx-xx xx xx xx xx xx xx xx   ................

    Start Time: xxxxxxxx
    Timeout   : 7200 (sec)
    Verify return code: 21 (unable to verify the first certificate)
    Extended master secret: no
---

よくみると、やはり以下のエラーが出ているのと、証明書がサーバー証明書だけしかなく中間CA証明書がないっぽい。

verify error:num=20:unable to get local issuer certificate
verify error:num=21:unable to verify the first certificate

中間CA証明書は必ず返ってくるものではないらしいけど、「unable to get local issuer certificate」のエラーが出ている場合は、中間CA証明書がなくてエラーになることが多いらしいことが判明。
ここまで来れば、あとは簡単ですね?

あとは、中間CA証明書を配置するだけ

そう、サーバー側に中間CA証明書を配置するだけです。
なので、お客様にその旨をお伝えしたのですが…、これが伝わらない。
最終的には、以下のようにコマンドレベルの情報をお知らせして、なんとか対応してもらいました。

お世話になっております。
現在、以下のドメインに対してクライアントサーバーからcurlコマンドを用いたアクセスを試みていますが、証明書チェーンの不完全さにより以下のエラーが発生しております:

curl: (60) SSL certificate problem: unable to get local issuer certificate
問題の原因として、接続先サーバーで証明書チェーンが適切に構成されていない可能性が高いと考えられます。
つきましては、以下の手順で証明書チェーンを正しく構成していただけますでしょうか?

1.証明書発行元(CA)から提供されている中間CA証明書をダウンロードしてください。
2.サーバー証明書(server.crt)と中間CA証明書(intermediate.crt)を結合して、証明書チェーンファイル(fullchain.crt)を作成してください:
  cat server.crt intermediate.crt > fullchain.crt
3.Webサーバー(ApacheまたはNGINX)の設定で、fullchain.crt を適切に指定してください。
  Apacheの場合: SSLCertificateChainFile ディレクティブを使用
  NGINXの場合: ssl_certificate に fullchain.crt を指定
4.サーバー設定をリロードし、動作をご確認ください。

ご対応いただけますと幸いです。
以上、よろしくお願いいたします。

最終的には、エラーの出ていた環境でopensslコマンドを打っても、「Verify return code: 0 (ok)」になりました!良かったー

openssl s_client -connect <お客様のドメイン>:443 -showcerts

======= 中略 =====

    Verify return code: 0 (ok)

で、どうすればよかったんだろ?的な結論

まあわかってみれば、こんな感じでトラブルシュートするのが効率が良さそうかなと思ったので、まとめておきます。

  • SSL通信時に「certificate has expired」のエラーが出たら、SSL証明書の期限切れなので、新しい期限の証明書を発行しよう!
  • SSL通信時に「unable to get local issuer certificate」のエラーが出たら、証明書チェーンが不完全なので、中間CA証明書の状況を把握しよう!
  • 中間CA証明書の状況の把握には、エラーの出ている環境で以下のコマンドを実行し最後に「Verify return code」が0 以外の場合、詳細なエラー内容をチェック。
openssl s_client -connect <お客様のドメイン>:443 -showcerts
  • 今回は「Verify return code: 21 (unable to verify the first certificate)」となってエラーになっていました。
  • エラーの場合は、サーバー証明書だけでなく、正しい中間CA証明書も同時に取得できているかを確認しよう!
  • 中間CA証明書がない場合は、お客様のドメインの管理者に中間CA証明書の配置をお願いしよう!

謎の解明:ローカルのPCからのcurlコマンドだとなぜ動いたのか?

しかし、序盤の確認では、ローカルのPCからのcurlコマンドは動いてたじゃんって思ったキミ!するどい!!

私もそう思って調べたところ、どうやら、近年のブラウザやクライアント(最新のcurlやOpenSSL)は、欠落している中間CA証明書をサーバーが送信していなくても、ルート証明書と接続先証明書の情報を元に中間CA証明書をオンラインで取得する仕組みがあるそうです。
なので古いクライアント(例えば、古いcurlやOpenSSL)だけ、中間CA証明書が欠落していた場合にエラーになってしまうようでした。

皆さんも、SSL証明書がこっちの環境だと動くのに向こうの環境だと動かないって時は、この辺のバージョンを確認してみるといいでしょう。

最後に

株式会社ヤプリでは、人材を募集しています。
ほんと、カジュアルにお話を聞くだけでもいいので、声を掛けてくれると嬉しいです! open.talentio.com