# cert\_exists() в Let's Encrypt усечает цепочку CA, что приводит к принудительному повторному выпуску сертификатов и ошибкам лимитов

**URL:** <https://meta.discourse.org/t/lets-encrypt-cert-exists-truncates-the-ca-chain-causing-forced-reissuance-and-rate-limit-failures/414167>\
**Category:** Bug\
**Tags:** letsencrypt\
**Created:** [07.Октябрь.2026 17:05:24 UTC](https://meta.discourse.org/t/lets-encrypt-cert-exists-truncates-the-ca-chain-causing-forced-reissuance-and-rate-limit-failures/414167 "2026-10-07T17:05:24Z")\
**Posts on this page:** 1\
**Page:** 1

<div class="post-metadata">

**Author:** ![jmx](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/jmx/32/579660_2.png) [@jmx](https://meta.discourse.org/u/jmx)\
**Post date:** [07.Октябрь.2026 17:05:25 UTC](https://meta.discourse.org/t/lets-encrypt-cert-exists-truncates-the-ca-chain-causing-forced-reissuance-and-rate-limit-failures/414167/1 "2026-10-07T17:05:25Z")

</div>

Я обнаружил воспроизводимый баг в `web.letsencrypt.ssl.template.yml`: функция `cert_exists()` отклоняет валидный сертификат, так как она использует только первый сертификат из пакета издателя, сохраненного в `ca.cer`. Это необоснованно запускает механизм отката `--force`.

Имена хостов и пути в выводе ниже анонимизированы.

### Воспроизведение

7 октября 2026 года acme.sh успешно выпустил RSA-сертификат. Его файл `ca.cer` содержал:

```plaintext
subject=C=US, O=Let's Encrypt, CN=YR2
issuer=C=US, O=ISRG, CN=Root YR

subject=C=US, O=ISRG, CN=Root YR
issuer=C=US, O=Internet Security Research Group, CN=ISRG Root X1

```

Команда, используемая в шаблоне, завершается ошибкой на этих фактических файлах:

```plaintext
$ openssl verify -CAfile <(openssl x509 -in ca.cer) fullchain.cer
C=US, O=Let's Encrypt, CN=YR2
error 2 at 1 depth lookup: unable to get issuer certificate
error fullchain.cer: verification failed

```

Использование полного пакета издателя в качестве входных данных для построения цепочки успешно:

```plaintext
$ openssl verify -show_chain -untrusted ca.cer fullchain.cer
fullchain.cer: OK
Chain:
depth=0: CN=forum.example.com (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

```

Команда `openssl x509 -in ca.cer` выводит только первый сертификат в формате PEM, отбрасывая перекрестно подписанный `Root YR`, необходимый для связывания `YR2` с доверенным `ISRG Root X1`.

### Почему существующая проверка была написана именно так

[#576](https://github.com/discourse/discourse_docker/pull/576) / [`8e2ccee`](https://github.com/discourse/discourse_docker/commit/8e2ccee0f2989885632c27c7f33a87719d3838ec) ввел эту проверку в октябре 2021 года, чтобы избежать использования просроченного корневого сертификата в старой цепочке Let’s Encrypt. В [отчете 2021 года](https://meta.discourse.org/t/test-for-valid-cert-to-enable-force-https-is-broken-leaving-it-off-when-it-should-be-on/205547) также описано повторное выпущение сертификата при пересборке.

С [текущей иерархией](https://letsencrypt.org/certificates/) одного первого издателя недостаточно на этом хосте. Флаг `-untrusted` предоставляет все сертификаты для построения цепочки, оставляя точки доверия в системном хранилище, что сохраняет намерение исправления 2021 года.

### Влияние

Шаблон выпускает RSA, проверяет его, а затем принудительно инициирует выпуск, если проверка не проходит. Он повторяет этот шаблон для ECDSA.

Это была чистая переустановка с повторным использованием имени хоста, установленного один или два дня назад, поэтому его квота могла быть уже частично исчерпана. Журнал и порядок действий в шаблоне показывают:

1. Обычный RSA: успех в 10:30:55 UTC.
2. Принудительный откат RSA: ошибка 429 в 10:30:56 UTC.
3. Обычный ECDSA: ошибка 429 в 10:30:58 UTC.
4. Принудительный откат ECDSA: ошибка 429 в 10:31:01 UTC.

Принудительный запрос RSA последовал за успешным выпуском валидного сертификата, использованного в воспроизведении выше. Сертификат ECDSA не был выпущен, и HTTPS не запускался до тех пор, пока валидная пара RSA не была скопирована в имена файлов `_ecc`, ожидаемые nginx.

Неудачный выпуск, оставляющий неиспользуемый файл сертификата, также может привести к ошибке nginx, описанной в связанных отчетах:

```plaintext
PEM_read_bio_X509_AUX() failed
... no start line ...
Expecting: TRUSTED CERTIFICATE

```

Квота для точного набора идентификаторов [восстанавливается постепенно](https://letsencrypt.org/docs/rate-limits/#new-certificates-per-exact-set-of-identifiers), примерно один сертификат каждые 34 часа. Судя по порядку действий в шаблоне, ненужный принудительный выпуск RSA может использовать возвращенную квоту до ECDSA, поэтому ожидание и пересборка без исправления проверки могут снова заблокировать ECDSA.

### Исправление и валидация

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

```

Валидация завершена:

- Фактические файлы RSA YR2: существующая проверка не проходит; предложенная команда успешна.

- Фактические файлы RSA (`YR1 → Root YR → ISRG Root X1`) и ECDSA (`YE1 → Root YE → ISRG Root X2`) оба успешно верифицируются внутри контейнера Discourse с OpenSSL 3.5.7.

- Полная пересборка генерирует исправленный скрипт и переиспользует оба сертификата: два сообщения `Skip`, обе цепочки установлены, и `nginx -t` проходит успешно.

- Синтетические случаи регрессии для цепочки 2021 года с наличием и удалением просроченного старого корневого сертификата оба проходят с `-untrusted` на OpenSSL 3.0.13. Старая команда с полным `-CAfile` воспроизводит соответствующие ошибки истечения срока действия на глубине 3 и ошибки издателя на глубине 2.

### Связанные отчеты

[411097](https://meta.discourse.org/t/need-help-with-dual-container-issue-with-letsencrypt-for-a-few-days-now/411097) объясняет неудачу проверки `_ecc` отсутствием каталога, согласно диагностике, вставленной автором. Здесь файлы RSA существуют, и неудача воспроизводится непосредственно командой верификации.

[413078](https://meta.discourse.org/t/the-too-many-certificates-error-ports-80-443-open-then-close/413078) содержит совпадающие ошибки издателей `YR1`/`YE2`; его последующий сбой приложения не был связан с этой проблемой.

PR: [FIX: preserve the Let's Encrypt issuer chain in cert\_exists - Pull Request #1136 - discourse/discourse\_docker - GitHub](https://github.com/discourse/discourse_docker/pull/1136)
