OpenSSL x509 コマンドはPEMのフォーマットの検証まで行っていない

10X SREの栗原です。
この記事は10X 新春ブログリレー 2026の1月7日分の記事です。

OpenSSLのコマンドではエラーにならなくても、Google Cloud側の証明書アップロードで弾かれることがあるため、PEMを厳密に検証するステップを追加して再発を防いだ、という話です。

弊社では、一部でTLS証明書の更新が手動で行われています。 証明書更新の際、受領したデータが正しいかどうか、次の手順で確認していました。

  1. 受領した証明書に不備はないか?(期限/サブジェクト)
  2. 受領した秘密鍵に不備はないか?(形式/パスフレーズの有無)
  3. 受領した証明書と秘密鍵はペアになっているか?

手順1の検証にはOpenSSLの openssl コマンドを利用していました。コマンドは次のとおりです。

openssl x509 -in crt.pem -text -noout

サブジェクトや有効期限を確認し、問題なければ証明書には不備がなく、手順1の要件を満たせるものだと思っていました。 また、手順2・3(秘密鍵の形式確認/証明書と秘密鍵のペア確認)も問題ないことを確認したうえで、Google Cloud Load Balancingの証明書を更新するため、先ほどの証明書をアップロードしました。すると、次のエラーが出て更新できませんでした。 Error:googleapi: Error 400: The SSL certificate could not be parsed., sslCertificateCouldNotParseCert

原因はPEMフッター(END行)のハイフン不足でした(正しくは前後ともハイフンが5本ずつ必要です)。

誤: -----END CERTIFICATE----
正: -----END CERTIFICATE-----

ハイフンを追加してエラーを解消しました。 なぜ末尾のハイフンが欠けたのかというと、証明書は購入後にファイル添付ではなくメール本文へ直接貼り付けた形で送られてきており、それをこちらでコピペしてファイル化する必要があったためです。そのコピペの際に末尾を取りこぼしてしまいました。すべての証明書会社が同じ運用とは限りませんが、少なくとも弊社が取引している証明書会社ではこの形式で届くことがあり、今回のようなミスにつながってしまいました。

OpenSSLは互換性を重視しているため、PEMのパースが比較的寛容で、RFC 7468が求める厳密な表記揺れまで弾かないケースがありました。なぜこの挙動が維持されているのかは断定できませんが、後方互換性のコストが大きいのだろう、と推測しています。

OpenSSLはフォーマットに寛容ですが、Google Cloudはフォーマットに不備があると、証明書更新のプロセスの途中でエラーになります。 その際、旧証明書へ自動でロールバックされず、Load Balancerが「証明書なし」扱いになるため、直ちに復旧作業(正しい証明書の再投入など)を進めない限り、TLS証明書エラーでサイトに接続できなくなります。
(余談)GoogleはOpenSSLをフォークしたBoringSSLを使っています。今回の挙動が直接の理由かは分かりませんが、互換性と厳格さのバランスは実装思想が出る部分だと感じました。

最終的に弊社では次のステップを追加することにしました。

python3 -c "import ssl; f=open('crt.pem'); ssl.PEM_cert_to_DER_cert(f.read())"

これはPythonの標準ライブラリ ssl を使って、証明書(PEM)をDER形式へ変換できるかを試すことで、「PEMとして厳密にパースできるか」を検証するコマンドです。フォーマットが壊れている場合はここで例外が発生して失敗します。なお、このチェックはフォーマットの妥当性確認が主目的で、証明書の内容(CNなど)や秘密鍵とのペア確認までを行うものではありません。

OpenSSLでエラーが出ないのにクラウド側で弾かれる、という事実には正直驚きましたし、「何をもって正しいとみなすのか」は改めて考えさせられました。