Доступен — Локальные цифровые продукты
HCA · Studio
TR — Контакты ↗
Заметка 07 · Сервер

Ловушка staging в acme.sh: авария по расписанию

Это самый труднозаметный тип сбоя TLS, потому что сегодня всё в порядке. Сертификат действителен, браузер доволен, мониторинг зелёный. Проблема проявляется в день, когда отработает cron продления — и в этот день весь сайт выдаёт предупреждение безопасности.

Инструмент
acme.sh
Симптом
Сегодня нет, через 60 дней есть
Проверка
строка Le_API
Короткий ответ

acme.sh хранит конфигурацию по каждому домену в ~/.acme.sh/<домен>/<домен>.conf. Значение Le_API определяет, из какого центра сертификации домен будет продлеваться, и фиксируется при первом выпуске. Если сертификат когда-либо выпускали с --staging или CA по умолчанию был задан как staging, каждое следующее продление придёт оттуда. Staging-сертификатам не доверяет ни один браузер. Пока установленный боевой сертификат действителен, проблема невидима; в день продления сайт становится недоступен. Проверка: grep -r acme-staging ~/.acme.sh/*/*.conf.

Почему молча

У Let's Encrypt два раздельных окружения. Боевое подписывает корнями, которым доверяют браузеры. Staging говорит по тому же протоколу и выполняет тот же процесс, но подписывает нарочито фальшивым корнем — и прямо об этом сообщает названием: «Pretend Pear», «Artificial Apricot».

Staging существует, чтобы отрабатывать автоматизацию, не упираясь в жёсткие лимиты. Беда начинается, когда репетиция превращается в постоянную настройку. acme.sh записывает конфигурацию домена при первом выпуске и больше не переспрашивает:

# ~/.acme.sh/example.com/example.com.conf
Le_API='https://acme-staging-v02.api.letsencrypt.org/directory'

Пока эта строка на месте, каждый запуск acme.sh --cron идёт в staging. А если сейчас установлен боевой сертификат, выпущенный вручную, симптомов нет вовсе. Это настоящая отложенная во времени авария: её триггер — не изменение кода, а календарь.

Обнаружение

Одна строка проверяет сразу все домены на сервере:

grep -r 'acme-staging' ~/.acme.sh/*/*.conf

Пустой вывод означает, что всё чисто. Каждое совпадение — домен, который сломается при следующем продлении.

Можно спросить и у самого сервера, кто подписал отдаваемый им сертификат:

openssl s_client -connect example.com:443 -servername example.com </dev/null 2>/dev/null \
  | openssl x509 -noout -issuer -dates

Если в строке issuer есть «(STAGING)» или «Pretend» — проблема уже боевая. Если нет, но grep дал совпадение, — проблема запланирована.

И ещё ловушка: на одном сервере могут быть десятки доменов, и лишь один из них смотрит на staging. Проверяйте каталог целиком, а не по одному домену.

Исправление

Править конфигурационный файл вручную недостаточно: acme.sh может его перегенерировать. Правильный путь — перевыпустить сертификат, явно указав боевой сервер:

acme.sh --issue -d example.com -d www.example.com \
  --server letsencrypt \
  --webroot /usr/local/lsws/Example/html \
  --force

--force необходим, потому что иначе acme.sh пропустит задачу: срок текущего сертификата ещё не истёк. А --server letsencrypt закрепляет Le_API за боевым окружением.

Чтобы та же ошибка не повторилась на новых доменах, один раз задайте CA по умолчанию:

acme.sh --set-default-ca --server letsencrypt

Не забудьте шаг установки: получить сертификат и развернуть его — не одно и то же. Задайте целевые файлы и команду перезагрузки через --install-cert, чтобы продление завершалось полностью автоматически:

acme.sh --install-cert -d example.com \
  --key-file       /etc/letsencrypt/live/example.com/privkey.pem \
  --fullchain-file /etc/letsencrypt/live/example.com/fullchain.pem \
  --reloadcmd      "/usr/local/lsws/bin/lswsctrl reload"

Проверка

После исправления проверьте три вещи по порядку:

# 1) Конфигурация теперь смотрит на боевое окружение?
grep Le_API ~/.acme.sh/example.com/example.com.conf

# 2) Кто подписал то, что сервер реально отдаёт?
openssl s_client -connect example.com:443 -servername example.com </dev/null 2>/dev/null \
  | openssl x509 -noout -issuer -enddate

# 3) Пробный прогон продления проходит без ошибок?
acme.sh --cron --home ~/.acme.sh --dry-run

Третий шаг самый ценный: вместо ожидания дня продления вы видите сегодня, что произойдёт в этот день.

Раннее предупреждение

Общая черта сбоев этого класса — они невидимы, пока вы не пойдёте их искать. Простая регулярная проверка снимает сюрприз: раз в сутки читайте срок и issuer сертификата и присылайте уведомление, когда осталось меньше 21 дня или issuer не тот, что ожидается.

Проверяйте снаружи, по TLS-соединению, а не файл на диске. Файл может быть правильным, а веб-сервер всё ещё отдавать другое; значение имеет именно тот сертификат, который получает посетитель.

Итог

Staging-окружения нужны для тестов, а тестовые настройки имеют привычку просачиваться в постоянную конфигурацию. В acme.sh эта утечка прячется в одной строке и молчит неделями. Принимая сервер или разбираясь с проблемами сертификатов, вводите первой командой grep -r 'acme-staging' ~/.acme.sh/*/*.conf — она занимает три секунды и однажды спасёт целый день.

Быстрая справка
Ловушка
Le_API фиксируется при первом выпуске: раз staging — всегда staging
Почему молча
Пока установленный боевой сертификат действителен, симптомов нет
Триггер
Не деплой, а календарь — день запуска cron продления
Обнаружение
grep -r 'acme-staging' ~/.acme.sh/*/*.conf
Проверка
Ищите (STAGING) в выводе openssl x509 -noout -issuer
Исправление
Перевыпуск с --server letsencrypt --force
Профилактика
acme.sh --set-default-ca --server letsencrypt
Мониторинг
Issuer и остаток дней проверяйте снаружи, по TLS
Частые вопросы

О ловушке staging-сертификата.

Мой сертификат сейчас выглядит действительным. Я всё равно в зоне риска?

Возможно, да. Если установлен боевой сертификат, выпущенный вручную, ни один браузер не пожалуется. Но если конфигурация acme.sh смотрит на staging, cron продления в день запуска установит поверх недоверенный сертификат. Проверить сегодня гораздо дешевле, чем дождаться дня продления.

Разве нельзя просто отредактировать строку в конфигурационном файле?

Ненадёжно. acme.sh может перегенерировать этот файл в ходе собственных операций, и ручная правка исчезнет. Правильный способ — перевыпуск с --server letsencrypt --force: он и записывает файл верно, и сразу устанавливает боевой сертификат.

Как распознать staging-сертификат?

По полю issuer. Staging-сертификаты подписаны откровенно фальшивыми именами: (STAGING) Pretend Pear, (STAGING) Artificial Apricot. Передайте вывод openssl s_client в openssl x509 -noout -issuer, и имя будет видно напрямую.

Достаточно ли проверить один домен на сервере?

Нет. Проблема существует на уровне домена: из десяти доменов на сервере лишь один может смотреть на staging, остальные выглядят здоровыми, а этот сломается в день продления. Проверяйте всегда весь каталог ~/.acme.sh.

Доступность и смета

Есть идея?
Можно даже одной фразой.