Let's Encrypt:「発行者証明書を取得できません」(エラー 2)、強制再発行とレート制限

Discourse のインストールまたは再構築時に、有効な Let’s Encrypt 証明書が以下のエラーで拒否される場合があります。

C=US, O=Let's Encrypt, CN=YR2
error 2 at 1 depth lookup: unable to get issuer certificate
error fullchain.cer: verification failed

私は RSA 発行が成功した直後にこれを再現しました。このチェックの失敗により、不要な --force リクエストがトリガーされ、HTTP 429 / rateLimited / “too many certificates” を引き起こす可能性があります。私の環境では、その後 ECDSA 発行がブロックされ、HTTPS が起動しませんでした。

影響を受けるチェーンの場合、証明書がまだ有効であっても、再構築やコンテナの再起動ごとに RSA と ECDSA 証明書の不要な強制再発行がトリガーされる可能性があります。再発行の成功は Let’s Encrypt のクォータを消費するため、繰り返される再構築や再起動により、レート制限に素早く到達する可能性があります。

すでにインストール済みの証明書で HTTPS が動作し続ける間、これは気づかれにくいことがあります。同じホスト名でインスタンスを削除して再インストールしても、そのクォータはリセットされません。新しいインストールに必要な証明書が欠けている場合、レート制限された発行により HTTPS の起動が妨げられる可能性があります。

修正方法

templates/web.letsencrypt.ssl.template.yml の cert_exists() 内で、以下を置き換えます:

- openssl verify -CAfile <(openssl x509 -in ca.cer) fullchain.cer
+ openssl verify -untrusted ca.cer fullchain.cer

関数の残りの部分は変更しないでください。修正されたテンプレートは、/usr/local/bin/letsencrypt を更新するために、次のコンテナ再構築に含める必要があります。

すでにレート制限に達している場合、このパッチはクォータをリセットしません。修正を適用し、さらに発行を試みる前に retry after の時間を遵守してください。

PR: FIX: preserve the Let's Encrypt issuer chain in cert_exists - Pull Request #1136 - discourse/discourse_docker - GitHub

チェーン検証の問題の確認

コンテナ内の影響を受ける証明書ディレクトリ(/shared/letsencrypt/<hostname> または <hostname>_ecc)から、以下を比較してください:

openssl verify -CAfile <(openssl x509 -in ca.cer) fullchain.cer
openssl verify -untrusted ca.cer fullchain.cer

再現されたケースでは、最初のコマンドはエラー 2 で失敗し、2 つ目のコマンドは以下を返します:

fullchain.cer: OK

原因と検証

openssl x509 -in ca.cer は、発行者バンドルから最初の証明書のみを読み取ります。観察された YR2 チェーンでは、ISRG Root X1 に到達するために必要なクロス署名された Root YR が欠落します。

-untrusted ca.cer は、信頼アンカーをシステムストアに保ちつつ、完全な発行者バンドルを供給するため、2021 年の修正 (#576) の意図を維持します。

私はコンテナ内で実際の RSA と ECDSA チェーンを検証しました。パッチを適用した完全な再構築では、既存の両方の証明書が再利用されました:2 つの Skip メッセージ、両方のチェーンがインストールされ、nginx -t がパスしました。

関連する報告

411097
413078

PR: FIX: preserve the Let's Encrypt issuer chain in cert_exists - Pull Request #1136 - discourse/discourse_docker - GitHub

「いいね!」 6

評価できるよう、有効な証明書とチェーンをここに投稿してください。

「いいね!」 3

@supermathie 件を調査してくださりありがとうございます。影響を受けたインスタンスから fullchain.cer と ca.cer を添付します。
fullchain.cer.txt (5.9 KB)
ca.cer.txt (3.8 KB)

以下の OpenSSL の出力では、元の検証コマンドがエラー 2 で失敗し、同じ証明書とチェーンに対して -untrusted を使用すると成功していることが示されています。

openssl version
openssl verify -CAfile <(openssl x509 -in ca.cer) fullchain.cer
openssl verify -show_chain -untrusted ca.cer fullchain.cer

OpenSSL 3.5.7 9 Jun 2026 (Library: OpenSSL 3.5.7 9 Jun 2026)
C=US, O=Let's Encrypt, CN=YR2
error 2 at 1 depth lookup: unable to get issuer certificate
error fullchain.cer: verification failed
fullchain.cer: OK
Chain:
depth=0: CN=hubprod.surikai.fr (untrusted)
depth=1: C=US, O=Let's Encrypt, CN=YR2 (untrusted)
depth=2: C=US, O=ISRG, CN=Root YR (untrusted)
depth=3: C=US, O=Internet Security Research Group, CN=ISRG Root X1

この問題は、2つの異なる環境で発生しました。PR を作成してから開発環境のインスタンスで修正を適用しており、これまで期待通りに動作しています。

「いいね!」 2

参考までに、ACME クライアントは ${LETSENCRYPT_DIR} に以下の関連ファイルを作成します:

  • ${DISCOURSE_HOSTNAME}.cer: 発行された証明書を含む
  • ca.cer: 証明書チェーンを含む(やや紛らわしい名前になっている)
  • fullchain.cer: 上記の2つのファイルを結合したもの

最も正確なコマンドは以下です:

openssl verify -untrusted ca.cer ${DISCOURSE_HOSTNAME}.cer

現在のコマンドは混乱している(システムルートまでチェーンが有効であるかをテストしていないため不正確です)。その他の方法は、副作用や前提条件に依存しています。PRを更新します。

「いいね!」 2

理にかなっています。フルチェーンの fullchain.cer に対して OpenSSL が最初の証明書のみを読み込むことに頼るよりも、リーフ証明書自体を直接検証する方がクリーンです。

PR #1136 はあなたのものに合わせてクローズします。

必要であれば、新しい PR のテストにも協力します。

「いいね!」 1

マージしました :partying_face:

@jmx さん、問題を明確に特定し、修正を提案してくれてありがとう。良い提案でした。ただ、もう少し手を加えたくて。

「いいね!」 3

このトピックは14時間後に自動的にクローズされました。新しい返信は許可されていません。